diff options
| author | bnewbold <bnewbold@robocracy.org> | 2012-09-21 13:03:46 +0200 | 
|---|---|---|
| committer | bnewbold <bnewbold@robocracy.org> | 2012-09-21 13:03:46 +0200 | 
| commit | 575dd002be18f170917a98f8bca891b4bd615f78 (patch) | |
| tree | 5db7245e184110fd0a403b052191fe6d5b3205d4 | |
| parent | b0ecc94ef69d489dcc775038a0c6f65c1c960c8e (diff) | |
| download | bommom-575dd002be18f170917a98f8bca891b4bd615f78.tar.gz bommom-575dd002be18f170917a98f8bca891b4bd615f78.zip  | |
basic octopart pricing functionality
| -rw-r--r-- | bommom.go | 5 | ||||
| -rw-r--r-- | core.go | 12 | ||||
| -rw-r--r-- | formats.go | 16 | ||||
| -rw-r--r-- | octopart.go | 63 | ||||
| -rw-r--r-- | octopart_test.go | 16 | ||||
| -rw-r--r-- | serve.go | 6 | ||||
| -rw-r--r-- | templates/bom_view.html | 12 | 
7 files changed, 105 insertions, 25 deletions
@@ -28,6 +28,7 @@ var (  	listenPort    = flag.Uint("port", 7070, "port to listen on (HTTP serve)")  	listenHost    = flag.String("host", "", "hostname to listen on (HTTP serve)")  	sessionSecret = flag.String("sessionSecret", "12345", "cookie session secret") +	octoApiKey    = flag.String("octopartApiKey", "", "octopart.com API key (for pricing info)")  )  func main() { @@ -88,6 +89,10 @@ func openAuthStore() {  	auth = DummyAuth(true)  } +func openPricingSource() { +	pricingSource = NewOctopartClient(*octoApiKey) +} +  func dumpOut(fname string, bm *BomMeta, b *Bom) {  	var outFile io.Writer  	if fname == "" { @@ -112,11 +112,21 @@ func makeTestBom() (*BomMeta, *Bom) {  		Mpn:      "WIDG0001",  		Elements: []string{"W1", "W2"},  		Offers:   []Offer{o}} +	li2 := LineItem{Manufacturer: "Texas Instruments", +		Mpn:      "NE555", +		Elements: []string{"W1", "W2"}, +		Offers:   []Offer{o}} +	li3 := LineItem{Manufacturer: "STMicroelectronics", +		Mpn:      "L7905CV", +		Elements: []string{"W1", "W2"}, +		Offers:   []Offer{o}}  	//li.AddOffer(o)  	b := NewBom("test01")  	b.AddLineItem(&li) +    // tests that lines don't get duplicated  	b.AddLineItem(&li) -	b.AddLineItem(&li) +	b.AddLineItem(&li2) +	b.AddLineItem(&li3)  	bm := &BomMeta{Name: "Some Bom", Owner: "Some Owner", Description: "This is such a thing!", HeadVersion: b.Version, Homepage: "http://bommom.com", IsPublicView: true, IsPublicEdit: false}  	return bm, b  } @@ -53,6 +53,22 @@ func DumpBomAsText(bm *BomMeta, b *Bom, out io.Writer) {  	tabWriter.Flush()  } +func DumpBomMarketInfo(bm *BomMeta, b *Bom, out io.Writer) { +	fmt.Fprintln(out) +	tabWriter := tabwriter.NewWriter(out, 2, 4, 1, ' ', 0) +	// "by line item", not "by element" +	fmt.Fprintf(tabWriter, "qty\tmanufacturer\tmpn\t\tavg_price\tfactor\n") +	for _, li := range b.LineItems { +		fmt.Fprintf(tabWriter, "%d\t%s\t%s\t\t%s\t%s\n", +			len(li.Elements), +			li.Manufacturer, +			li.Mpn, +			li.AggregateInfo["MarketPrice"], +			li.AggregateInfo["MarketFactor"]) +	} +	tabWriter.Flush() +} +  // --------------------- csv -----------------------  func DumpBomAsCSV(b *Bom, out io.Writer) { diff --git a/octopart.go b/octopart.go index 654c03d..9c7db89 100644 --- a/octopart.go +++ b/octopart.go @@ -6,6 +6,7 @@ import (  	"log"  	"net/http"  	"net/url" +    "strconv"  	//"io/ioutil"  ) @@ -31,11 +32,6 @@ func NewOctopartClient(apikey string) *OctopartClient {  	return oc  } -func openPricingSource() { -	// TODO: pass through octopart API key here -	pricingSource = NewOctopartClient("") -} -  func (oc *OctopartClient) apiCall(method string, params map[string]string) (map[string]interface{}, error) {  	paramString := "?apikey=" + oc.ApiKey  	// TODO: assert clean-ness of params @@ -76,6 +72,7 @@ func (oc *OctopartClient) bomApiCall(manufacturers, mpns []string) ([]map[string  		listItem = make(map[string]string)  		listItem["mpn_or_sku"] = mpns[i]  		listItem["manufacturer"] = manufacturers[i] +		//listItem["q"] = mpns[i]  		listItem["limit"] = "1"  		listItem["reference"] = manufacturers[i] + "|" + mpns[i]  		queryList[i] = listItem @@ -95,7 +92,10 @@ func (oc *OctopartClient) bomApiCall(manufacturers, mpns []string) ([]map[string  	ret := make([]map[string]interface{}, len(mpns))  	for i, rawresult := range response["results"].([]interface{}) {  		result := rawresult.(map[string]interface{}) -		hits := int(result["hits"].(float64)) +        hits := int(0) +        if result["hits"] != nil { +            hits = int(result["hits"].(float64)) +        }  		reference := result["reference"].(string)  		if hits == 0 {  			ret[i] = nil @@ -109,7 +109,7 @@ func (oc *OctopartClient) bomApiCall(manufacturers, mpns []string) ([]map[string  }  // this method checks the API query cache -func (oc *OctopartClient) GetMarketInfoList(manufacturers, mpns []string) ([]interface{}, error) { +func (oc *OctopartClient) GetMarketInfoList(manufacturers, mpns []string) ([]map[string]interface{}, error) {  	if len(mpns) < 1 {  		return nil, Error("no mpns strings passed in")  	} @@ -131,20 +131,30 @@ func (oc *OctopartClient) GetMarketInfoList(manufacturers, mpns []string) ([]int  		}  	}  	// if necessary, fetch missing queryHashes remotely -	if len(mpnToQuery) > 0 { -		if _, err := oc.bomApiCall(manufacturersToQuery, mpnToQuery); err != nil { +	for len(mpnToQuery) > 0 { +        high := len(mpnToQuery) +        if high >= 20 { +            high = 20 +        } +		if _, err := oc.bomApiCall(manufacturersToQuery[0:high], mpnToQuery[0:high]); err != nil {  			return nil, err  		} +        mpnToQuery = mpnToQuery[high:len(mpnToQuery)] +        manufacturersToQuery = manufacturersToQuery[high:len(manufacturersToQuery)]  	}  	// construct list of return info -	result := make([]interface{}, len(mpns)) +	result := make([]map[string]interface{}, len(mpns))  	for i, _ := range mpns {  		queryHash = manufacturers[i] + "|" + mpns[i]  		value, hasKey := oc.infoCache[queryHash]  		if hasKey != true {  			return nil, Error("key should be in cache, but isn't: " + queryHash)  		} -		result[i] = value +        if value == nil || mpns[i] == "" || manufacturers[i] == "" { +            result[i] = nil +        } else { +		    result[i] = value.(map[string]interface{}) +        }  	}  	return result, nil  } @@ -154,7 +164,7 @@ func (oc *OctopartClient) GetMarketInfo(manufacturer, mpn string) (map[string]in      if err != nil {          return nil, err      } -    return info[0].(map[string]interface{}), nil +    return info[0], nil  }  func (oc *OctopartClient) GetExtraInfo(manufacturer, mpn string) (map[string]string, error) { @@ -164,11 +174,16 @@ func (oc *OctopartClient) GetExtraInfo(manufacturer, mpn string) (map[string]str  	}      // extract market price, total avail, and "availability factor" from      // market info -	log.Println(marketInfo)      ret := make(map[string]string) -    ret["MarketPrice"] = marketInfo["avg_price"].(string) -    ret["MarketFactor"] = marketInfo["market_availability"].(string) -    ret["MarketTotalAvailable"] = marketInfo["total_avail"].(string) +    if marketInfo != nil { +        if marketInfo["avg_price"].([]interface{})[0] != nil { +            ret["MarketPrice"] = "$" + strconv.FormatFloat(marketInfo["avg_price"].([]interface{})[0].(float64), 'f', 2, 64) +        } else { +            ret["MarketPrice"] = "" +        } +        ret["MarketFactor"] = marketInfo["market_status"].(string) +        ret["OctopartUrl"] = marketInfo["detail_url"].(string) +    }   	return ret, nil  } @@ -188,5 +203,21 @@ func (oc *OctopartClient) AttachMarketInfo(li *LineItem) error {  }  func (oc *OctopartClient) AttachMarketInfoBom(b *Bom) error { +    // first ensure the cache is primed +    manufacturers := make([]string, len(b.LineItems)) +    mpns := make([]string, len(b.LineItems)) +    for i, li := range b.LineItems { +        manufacturers[i] = li.Manufacturer +        mpns[i] = li.Mpn +    } +    _, err := oc.GetMarketInfoList(manufacturers, mpns) +    if err != nil { +        log.Println(err.Error()) +        return err +    } + +    for i := range b.LineItems { +        oc.AttachMarketInfo(&(b.LineItems[i])) +    }  	return nil  } diff --git a/octopart_test.go b/octopart_test.go index d2c8fbd..c86ed9b 100644 --- a/octopart_test.go +++ b/octopart_test.go @@ -26,7 +26,7 @@ func TestGetMarketInfoList(t *testing.T) {  		if r == nil {  			log.Printf("\t%d: %s", i, "nil")  		} else { -			log.Printf("\t%d: %s", i, r.(map[string]interface{})["detail_url"]) +			log.Printf("\t%d: %s", i, r["detail_url"])  		}  	}  	log.Println("Running a second time, results should be cached...") @@ -38,7 +38,7 @@ func TestGetMarketInfoList(t *testing.T) {  		if r == nil {  			log.Printf("\t%d: %s", i, "nil")  		} else { -			log.Printf("\t%d: %s", i, r.(map[string]interface{})["detail_url"]) +			log.Printf("\t%d: %s", i, r["detail_url"])  		}  	}  	log.Println("Running in single mode, result should be cached...") @@ -47,9 +47,9 @@ func TestGetMarketInfoList(t *testing.T) {  		t.Errorf("Error with api call: " + err.Error())  	}      if result_single == nil { -        log.Printf("\t%d: %s", "nil") +        log.Printf("\t%d: %s", 0, "nil")      } else { -        log.Printf("\t%d: %s", result_single["detail_url"]) +        log.Printf("\t%d: %s", 0, result_single["detail_url"])      }  } @@ -58,6 +58,10 @@ func TestAttachInfo(t *testing.T) {  	bm := &BomMeta{}  	oc := NewOctopartClient("")  	oc.AttachMarketInfoBom(b) -	t.Errorf("unimplemented") -	DumpBomAsText(bm, b, os.Stdout) +	//t.Errorf("unimplemented") +	//DumpBomAsText(bm, b, os.Stdout) +	DumpBomMarketInfo(bm, b, os.Stdout) +    log.Println("Running a second time, results should be cached...") +	oc.AttachMarketInfoBom(b) +	DumpBomMarketInfo(bm, b, os.Stdout)  } @@ -146,6 +146,7 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name string) (e  		http.Error(w, "invalid bom name: "+name, 400)  		return  	} +  	context := make(map[string]interface{})  	context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name))  	context["Session"] = session.Values @@ -153,6 +154,10 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name string) (e  		http.Error(w, "404 couldn't open bom: "+user+"/"+name, 404)  		return nil  	} +    err = pricingSource.AttachMarketInfoBom(context["Bom"].(*Bom)) +    if err != nil { +        log.Println("error attaching market info: " + err.Error()) +    }  	err = tmplBomView.Execute(w, context)  	return  } @@ -275,6 +280,7 @@ func serveCmd() {  	openBomStore()  	openAuthStore() +    openPricingSource()  	// serve template static assets (images, CSS, JS)  	http.Handle("/static/", http.FileServer(http.Dir(*templatePath+"/"))) diff --git a/templates/bom_view.html b/templates/bom_view.html index 7eca3e3..94e99a8 100644 --- a/templates/bom_view.html +++ b/templates/bom_view.html @@ -11,11 +11,15 @@    <th>manufacturer    <th>mpn    <th>description +  <th>category +  <!--    <th>form_factor    <th>specs -  <th>category    <th>tag +  -->    <th>comment +  <th>price +  <th>availability  </tr>  {{ range .Bom.LineItems }}  <tr> @@ -24,11 +28,15 @@    <td>{{ .Manufacturer }}    <td>{{ .Mpn }}    <td>{{ .Description }} +  <td>{{ .Category }} +  <!--    <td>{{ .FormFactor }}    <td>{{ .Specs }} -  <td>{{ .Category }}    <td>{{ .Tag }} +  -->    <td>{{ .Comment }} +  <td><a href="{{ .AggregateInfo.OctopartUrl }}">{{ .AggregateInfo.MarketPrice }}</a> +  <td>{{ .AggregateInfo.MarketFactor }}  </tr>  {{ end }}  </table>  | 
