partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 1f4b8d75e7b64b785eb50af5994b60713412f318
parent e39c5f87f9ad44c7b7d4ffb521178f492761320d
Author: Christian <github@christianwolf.email>
Date:   Sat,  7 Nov 2020 11:41:47 +0100

Merge pull request #1150 from sibbi77/octopart_v4

Octopart v4 API
Diffstat:
MCHANGELOG.md | 5+++++
Mapp/config/parameters.php.dist | 9+++++++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/DataApplicator.js | 269++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/SearchPanel.js | 33++++++++++++++++++++++-----------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/SearchWindow.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js | 4++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/UserPreferences/Preferences/OctoPartConfiguration.js | 2+-
Msrc/PartKeepr/OctoPartBundle/Controller/DefaultController.php | 31+++++++++++++++++++------------
Msrc/PartKeepr/OctoPartBundle/Resources/config/services.xml | 1+
Msrc/PartKeepr/OctoPartBundle/Services/OctoPartService.php | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
10 files changed, 432 insertions(+), 179 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +Bugfixes and other features: + * Updated to the most recent Octopart API version 4 [#1150](https://github.com/partkeepr/PartKeepr/pull/1150) + ## PartKeepr 1.4.0 New Features: diff --git a/app/config/parameters.php.dist b/app/config/parameters.php.dist @@ -259,12 +259,17 @@ $container->setParameter('partkeepr.filesystem.data_directory', '%kernel.root_di $container->setParameter('partkeepr.cronjob.check', true); /** - * Specifies which API key PartKeepr should use to talk to OctoPart. You can get an API key by registering at - * https://octopart.com/api/home and then registering an application. + * Specifies which v4 API key PartKeepr should use to talk to OctoPart. You can get an API key by registering at + * https://octopart.com/api and then registering an application. */ $container->setParameter('partkeepr.octopart.apikey', ''); /** + * The number of returned parts from API calls is limited. Try to keep this value low + */ +$container->setParameter('partkeepr.octopart.limit', '3'); + +/** * Specifies which URL contains the patreon status. If you do not wish to display the patreon status, * set this parameter to false. Please note that we rely on your Patreon pledges to ensure further * development of PartKeepr. diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/DataApplicator.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/DataApplicator.js @@ -8,9 +8,8 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { { this.setImport("parameters", true); this.setImport("distributors", true); - this.setImport("datasheets", true); + this.setImport("bestDatasheet", true); this.setImport("cadModels", true); - this.setImport("complianceDocuments", true); this.setImport("referenceDesigns", true); this.setImport("images", true); @@ -24,9 +23,9 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { this.part = part; }, /** - * Loads the data via the PartKeepr API from OctoPart. + * Loads the data via the PartKeepr API from Octopart. * - * @param {String} id The OctoPart UID to load + * @param {String} id The Octopart UID to load */ loadData: function (id) { @@ -37,7 +36,7 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { }); }, /** - * Called after the OctoPart Part data has been loaded. + * Called after the Octopart Part data has been loaded. * * @param {Object} response The response data */ @@ -74,18 +73,22 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (this.import.parameters) { - for (i in this.data.specs) + for (i in this.data["specs"]) { - if (this.data.specs[i].metadata.unit !== null) + var q_value, q_unit, q_siPrefix; + [q_value,q_unit] = this.parseQuantity( this.data.specs[i].display_value ); + [q_value,q_unit2,q_siPrefix] = this.SIUnitPrefix(q_value, q_unit); + if (q_unit2 === null && q_unit) { + // there is a unit (q_unit), but we do not know about it or the prefix of the unit is disabled unit = PartKeepr.getApplication().getUnitStore().findRecord("symbol", - this.data.specs[i].metadata.unit.symbol, 0, false, true, true); + q_unit, 0, false, true, true); if (unit === null) { - this.displayWaitWindow(i18n("Creating Unit…"), this.data.specs[i].metadata.unit.name); + this.displayWaitWindow(i18n("Creating Unit…"), q_unit); unit = Ext.create("PartKeepr.UnitBundle.Entity.Unit"); - unit.set("name", this.data.specs[i].metadata.unit.name); - unit.set("symbol", this.data.specs[i].metadata.unit.symbol); + unit.set("name", q_unit); // v4 API does not have that anymore + unit.set("symbol", q_unit); unit.save({ success: function () { @@ -105,17 +108,17 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (this.import.distributors) { - for (i in this.data.offers) + for (i in this.data['sellers']) { distributor = PartKeepr.getApplication().getDistributorStore().findRecord("name", - this.data.offers[i].seller.name, 0, false, true, true); + this.data.sellers[i].company.name, 0, false, true, true); if (distributor === null) { - this.displayWaitWindow(i18n("Creating Distributor…"), this.data.offers[i].seller.name); + this.displayWaitWindow(i18n("Creating Distributor…"), this.data.sellers[i].company.name); distributor = Ext.create("PartKeepr.DistributorBundle.Entity.Distributor"); - distributor.set("name", this.data.offers[i].seller.name); - distributor.set("website", this.data.offers[i].seller.homepage_url); + distributor.set("name", this.data.sellers[i].company.name); + distributor.set("url", this.data.sellers[i].company.homepage_url); distributor.save({ success: function () { @@ -132,11 +135,12 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { } } - if (this.import.datasheets) + if (this.import.bestDatasheet) { - if (this.data.datasheets.length > 0) + if (this.data['best_datasheet']) { - file = this.data.datasheets.shift(); + file = this.data.best_datasheet; + delete this.data.best_datasheet; this.displayWaitWindow(i18n("Uploading datasheet…"), file.url); if (!this.checkIfAttachmentFilenameExists(file.url)) @@ -154,32 +158,14 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (this.import.cadModels) { - if (this.data.cad_models.length > 0) + if (this.data['cad']) { - file = this.data.cad_models.shift(); - this.displayWaitWindow(i18n("Uploading CAD Model…"), file.url); - if (!this.checkIfAttachmentFilenameExists(file.url)) + file = this.data.cad; + delete this.data.cad; + this.displayWaitWindow(i18n("Uploading CAD Model…"), file.add_to_library_url); + if (!this.checkIfAttachmentFilenameExists(file.add_to_library_url)) { - PartKeepr.getApplication().uploadFileFromURL(file.url, i18n("CAD Model"), this.onFileUploaded, - this); - } else - { - this.applyData(); - } - return false; - } - } - - if (this.import.complianceDocuments) - { - if (this.data.compliance_documents.length > 0) - { - file = this.data.compliance_documents.shift(); - this.displayWaitWindow(i18n("Uploading Compliance Document…"), file.url); - if (!this.checkIfAttachmentFilenameExists(file.url)) - { - PartKeepr.getApplication().uploadFileFromURL(file.url, i18n("Compliance Document"), - this.onFileUploaded, + PartKeepr.getApplication().uploadFileFromURL(file.add_to_library_url, i18n("CAD Model"), this.onFileUploaded, this); } else { @@ -210,42 +196,19 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (this.import.images) { - if (this.data.imagesets.length > 0) + if (this.data['best_image']) { - file = this.data.imagesets.shift(); - image = null; - - if (file.swatch_image !== null) - { - image = file.swatch_image; - } + image = this.data.best_image; + delete this.data.best_image; - if (file.small_image !== null) + this.displayWaitWindow(i18n("Uploading Image…"), image.url); + if (!this.checkIfAttachmentFilenameExists(image.url)) { - image = file.small_image; - } - - if (file.medium_image !== null) - { - image = file.medium_image; - } - - if (file.large_image !== null) - { - image = file.large_image; - } - - if (image !== null) + PartKeepr.getApplication().uploadFileFromURL(image.url, i18n("Image"), this.onFileUploaded, + this); + } else { - this.displayWaitWindow(i18n("Uploading Image…"), image.url); - if (!this.checkIfAttachmentFilenameExists(image.url)) - { - PartKeepr.getApplication().uploadFileFromURL(image.url, i18n("Image"), this.onFileUploaded, - this); - } else - { - this.applyData(); - } + this.applyData(); } return false; @@ -346,26 +309,26 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (this.import.distributors) { - for (i = 0; i < this.data.offers.length; i++) + for (i in this.data['sellers']) { distributor = PartKeepr.getApplication().getDistributorStore().findRecord("name", - this.data.offers[i].seller.name, 0, false, true, true); + this.data.sellers[i].company.name, 0, false, true, true); if (distributor === null) { // @todo put out error message continue; } - for (currency in this.data.offers[i].prices) + for (o in this.data.sellers[i]['offers']) { - for (j = 0; j < this.data.offers[i].prices[currency].length; j++) + for (p in this.data.sellers[i].offers[o].prices) { partDistributor = Ext.create("PartKeepr.PartBundle.Entity.PartDistributor"); partDistributor.setDistributor(distributor); - partDistributor.set("sku", this.data.offers[i].sku); - partDistributor.set("packagingUnit", this.data.offers[i].prices[currency][j][0]); - partDistributor.set("currency", currency); - partDistributor.set("price", this.data.offers[i].prices[currency][j][1]); + partDistributor.set("sku", this.data.sellers[i].offers[o].sku); + partDistributor.set("packagingUnit", this.data.sellers[i].offers[o].prices[p].quantity); + partDistributor.set("currency", this.data.sellers[i].offers[o].prices[p].currency); + partDistributor.set("price", this.data.sellers[i].offers[o].prices[p].price); found = null; @@ -382,7 +345,7 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (found !== null) { - found.set("price", this.data.offers[i].prices[currency][j][1]); + found.set("price", this.data.sellers[i].offers[o].prices[p].price); } else { this.part.distributors().add(partDistributor); @@ -394,54 +357,47 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { if (this.import.parameters) { - for (i in this.data.specs) + for (i in this.data["specs"]) { spec = Ext.create("PartKeepr.PartBundle.Entity.PartParameter"); - spec.set("name", this.data.specs[i].metadata.name); + spec.set("name", this.data.specs[i].attribute.name); - if (this.data.specs[i].metadata.unit !== null) - { - unit = PartKeepr.getApplication().getUnitStore().findRecord("symbol", - this.data.specs[i].metadata.unit.symbol, 0, false, true, true); + var q_value, q_unit; + [q_value,q_unit] = this.parseQuantity( this.data.specs[i].display_value ); - spec.setUnit(unit); + // some fields need to be treated as strings + if (this.data.specs[i].attribute.name == "Case/Package" || + this.data.specs[i].attribute.name == "Case Code (Imperial)" || + this.data.specs[i].attribute.name == "Case Code (Metric)") { + q_value = null; // force string interpretation + q_unit = null; // force string interpretation } - - switch (this.data.specs[i].metadata.datatype) + + if (q_value != null && q_unit != null) { - case "string": - spec.set("valueType", "string"); - spec.set("stringValue", this.data.specs[i].value[0]); - break; - case "decimal": - case "integer": - spec.set("valueType", "numeric"); - - if (this.data.specs[i].min_value !== null) - { - value = parseFloat(this.data.specs[i].min_value); - siPrefix = this.findSiPrefixForValueAndUnit(value, unit); - spec.set("minValue", this.applySiPrefix(value, siPrefix)); - spec.setMinSiPrefix(siPrefix); - } - - if (this.data.specs[i].max_value !== null) - { - value = parseFloat(this.data.specs[i].max_value); - siPrefix = this.findSiPrefixForValueAndUnit(value, unit); - spec.set("maxValue", this.applySiPrefix(value, siPrefix)); - spec.setMaxSiPrefix(siPrefix); - } - - if (this.data.specs[i].value.length === 1) - { - value = parseFloat(this.data.specs[i].value[0]); - siPrefix = this.findSiPrefixForValueAndUnit(value, unit); - spec.set("value", this.applySiPrefix(value, siPrefix)); - spec.setSiPrefix(siPrefix); - } - - break; + [value,unit,siPrefix] = this.SIUnitPrefix(q_value, q_unit); + if (value && unit && siPrefix) { + spec.setUnit(unit); + spec.set("value", value); + spec.setSiPrefix(siPrefix); + } else { + unit = PartKeepr.getApplication().getUnitStore().findRecord("symbol", + q_unit, 0, false, true, true); + spec.setUnit(unit); + siPrefix = this.findSiPrefixForValueAndUnit(q_value, unit); + spec.set("value", this.applySiPrefix(q_value, siPrefix)); + spec.setSiPrefix(siPrefix); + } + spec.set("valueType", "numeric"); + } + else if (q_value != null) { + spec.set("valueType", "numeric"); + spec.set("value", q_value); + } + else + { + spec.set("valueType", "string"); + spec.set("stringValue", this.data.specs[i].display_value); } found = null; @@ -495,5 +451,66 @@ Ext.define("PartKeepr.Components.OctoPart.DataApplicator", { } return siPrefix; + }, + parseQuantity: function( quantity ) + { + try { + quantity = quantity.trim(); + const regex = /[^\d+-.]/g; + var idx = quantity.search(regex); + if (idx == -1) { + // no unit, but maybe value only + var value = parseFloat(quantity); + if (!isNaN(value)) { + return [value,null]; + } + } else { + var value = parseFloat(quantity.slice(0,idx)); + if (isNaN(value)) + return [null,null]; + var unit = quantity.slice(idx).trim(); + return [value,unit]; + } + } + finally {} + return [null,null]; + }, + SIUnitPrefix: function( q_value, q_unit ) + { + // the new Octopart API returns quantities as display strings: e.g. "12 mm" + // try to recognize SI-unit and SI-prefix + + // check if the unit as a whole is already known + var unit = PartKeepr.getApplication().getUnitStore().findRecord("symbol", q_unit, 0, false, true, true); + if (unit) { + var siPrefix = PartKeepr.getApplication().getSiPrefixStore().findRecord("exponent", 0, 0, false, false, true); + return [q_value, unit, siPrefix]; + } + + // assume the first character is an SI-prefix + if (q_unit && q_unit.length >= 2) { + unit = PartKeepr.getApplication().getUnitStore().findRecord("symbol", q_unit.substring(1), 0, false, true, true); + if (unit) { + // now check that the first character is a valid SI-prefix + console.log(unit); + var siPrefix; + for (var i=0; i<unit.prefixes().getData().length; i++) { + var temp = unit.prefixes().getData().getAt(i); + var prefixChar = temp.get("symbol"); + if (prefixChar == "μ") + prefixChar = "µ"; // convert upper case µ to lower case µ + if (q_unit[0] == prefixChar) { + siPrefix = temp; + break; + } + } + if (siPrefix) { + return [q_value, unit, siPrefix]; + } + } + } + + // no matching unit found + return [null, null, null]; } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/SearchPanel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/SearchPanel.js @@ -25,7 +25,8 @@ Ext.define("PartKeepr.Components.OctoPart.SearchPanel", { reader: { type: 'json', totalProperty: 'hits', - rootProperty: 'results' + rootProperty: 'results', + transform: this.checkForErrors } }, autoLoad: false @@ -97,9 +98,9 @@ Ext.define("PartKeepr.Components.OctoPart.SearchPanel", { boxLabel: i18n("Parameters") }, { xtype: 'checkbox', - itemId: "importDatasheets", - checked: PartKeepr.getApplication().getUserPreference("partkeepr.octopart.importDatasheets", true), - boxLabel: i18n("Datasheets") + itemId: "importBestDatasheet", + checked: PartKeepr.getApplication().getUserPreference("partkeepr.octopart.importBestDatasheet", true), + boxLabel: i18n("Best Datasheet") }, { xtype: 'checkbox', itemId: "importCADModels", @@ -107,11 +108,6 @@ Ext.define("PartKeepr.Components.OctoPart.SearchPanel", { boxLabel: i18n("CAD Models") }, { xtype: 'checkbox', - itemId: "importComplianceDocuments", - checked: PartKeepr.getApplication().getUserPreference("partkeepr.octopart.importComplianceDocuments", true), - boxLabel: i18n("Compliance Documents") - }, { - xtype: 'checkbox', itemId: "importReferenceDesigns", checked: PartKeepr.getApplication().getUserPreference("partkeepr.octopart.importReferenceDesigns", true), boxLabel: i18n("Reference Designs") @@ -158,6 +154,8 @@ Ext.define("PartKeepr.Components.OctoPart.SearchPanel", { this.onSelectChange, this); + this.store.on("load", this.checkForApiError, this); + this.callParent(arguments); }, @@ -186,9 +184,8 @@ Ext.define("PartKeepr.Components.OctoPart.SearchPanel", { j.setImport("distributors", this.down("#importDistributors").getValue()); j.setImport("parameters", this.down("#importParameters").getValue()); - j.setImport("datasheets", this.down("#importDatasheets").getValue()); + j.setImport("bestDatasheet", this.down("#importBestDatasheet").getValue()); j.setImport("cadModels", this.down("#importCADModels").getValue()); - j.setImport("complianceDocuments", this.down("#importComplianceDocuments").getValue()); j.setImport("referenceDesigns", this.down("#importReferenceDesigns").getValue()); j.setImport("images", this.down("#importImages").getValue()); @@ -206,5 +203,19 @@ Ext.define("PartKeepr.Components.OctoPart.SearchPanel", { ); this.store.load(); this.searchBar.setValue(query); + }, + checkForErrors: function (data) + { + if (data.results.length == 0 && data.errors.length > 0) { + Ext.Msg.alert(i18n("Octopart Error"), data.errors.map(e => e.message).join()); + } + + return data; + }, + checkForApiError: function (store, records, successful, eOpts ) + { + if (!successful) { + Ext.Msg.alert(i18n("Octopart Error"), "PartKeepr cannot access the Octopart API"); + } } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/SearchWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/OctoPart/SearchWindow.js @@ -1,6 +1,6 @@ Ext.define("PartKeepr.Components.OctoPart.SearchWindow", { extend: "Ext.window.Window", - title: i18n("OctoPart Search"), + title: i18n("Octopart Search"), iconCls: "partkeepr-icon octopart", width: 750, height: 300, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js @@ -62,7 +62,7 @@ Ext.define('PartKeepr.PartEditorWindow', { this.editor.on("itemSaved", this.onItemSaved, this); this.octoPartButton = Ext.create("Ext.button.Button", { - text: i18n("OctoPart…"), + text: i18n("Octopart…"), iconCls: 'partkeepr-icon octopart', handler: Ext.bind(this.onOctoPartClick, this) }); @@ -143,7 +143,7 @@ Ext.define('PartKeepr.PartEditorWindow', { this.octoPartQueryWindow.startSearch(this.editor.nameField.getValue()); this.octoPartQueryWindow.on("refreshData", this.onRefreshData, this); } else { - Ext.MessageBox.alert(i18n("OctoPart is not configured"), i18n("Your administrator needs to configure the API key for OctoPart in the parameters.php file - see parameters.php.dist for instructions")); + Ext.MessageBox.alert(i18n("Octopart is not configured"), i18n("Your administrator needs to configure the API key for Octopart in the parameters.php file - see parameters.php.dist for instructions")); } }, onRefreshData: function () { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/UserPreferences/Preferences/OctoPartConfiguration.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/UserPreferences/Preferences/OctoPartConfiguration.js @@ -100,7 +100,7 @@ Ext.define('PartKeepr.Components.UserPreferences.Preferences.OctoPartConfigurati }, statics: { iconCls: 'partkeepr-icon octopart', - title: i18n('OctoPart'), + title: i18n('Octopart'), menuPath: [{iconCls: 'fugue-icon ui-scroll-pane-image', text: i18n("User Interface")}] } }); diff --git a/src/PartKeepr/OctoPartBundle/Controller/DefaultController.php b/src/PartKeepr/OctoPartBundle/Controller/DefaultController.php @@ -38,7 +38,7 @@ class DefaultController extends FOSRestController */ public function getPartsByQueryAction(Request $request) { - $start = 0; + $start = 1; $responseData = []; @@ -50,20 +50,27 @@ class DefaultController extends FOSRestController $data = $this->get("partkeepr.octopart_service")->getPartyByQuery($query, $start); + $errors = $data["errors"]; + $data = $data["data"]["search"]; + $responseData["hits"] = $data["hits"]; $responseData["results"] = []; + $responseData["errors"] = $errors; - foreach ($data["results"] as $result) { - $responseItem = []; - $responseItem["mpn"] = $result["item"]["mpn"]; - $responseItem["title"] = $result["snippet"]; - $responseItem["manufacturer"] = $result["item"]["manufacturer"]["name"]; - $responseItem["numOffers"] = count($result["item"]["offers"]); - $responseItem["numSpecs"] = count($result["item"]["specs"]); - $responseItem["numDatasheets"] = count($result["item"]["datasheets"]); - $responseItem["url"] = $result["item"]["octopart_url"]; - $responseItem["uid"] = $result["item"]["uid"]; - $responseData["results"][] = $responseItem; + if ($data) { + foreach ($data["results"] as $result) { + $part = $result["part"]; + $responseItem = []; + $responseItem["mpn"] = $part["mpn"]; + $responseItem["title"] = $part["short_description"]; + $responseItem["manufacturer"] = $part["manufacturer"]["name"]; + $responseItem["numOffers"] = count($part["sellers"]); + $responseItem["numSpecs"] = count($part["specs"]); + $responseItem["numDatasheets"] = count($part["document_collections"]); + $responseItem["url"] = "https://octopart.com".$part["slug"]; + $responseItem["uid"] = $part["id"]; + $responseData["results"][] = $responseItem; + } } return $responseData; diff --git a/src/PartKeepr/OctoPartBundle/Resources/config/services.xml b/src/PartKeepr/OctoPartBundle/Resources/config/services.xml @@ -7,6 +7,7 @@ <services> <service id="partkeepr.octopart_service" class="PartKeepr\OctoPartBundle\Services\OctoPartService"> <argument type="string">%partkeepr.octopart.apikey%</argument> + <argument type="string">%partkeepr.octopart.limit%</argument> </service> </services> </container> diff --git a/src/PartKeepr/OctoPartBundle/Services/OctoPartService.php b/src/PartKeepr/OctoPartBundle/Services/OctoPartService.php @@ -3,53 +3,260 @@ namespace PartKeepr\OctoPartBundle\Services; use Guzzle\Http\Client; +use Predis\Client as PredisClient; class OctoPartService { - const OCTOPART_ENDPOINT = "http://octopart.com/api/v3/"; + const OCTOPART_ENDPOINT = "https://octopart.com/api/v4/endpoint"; + + const OCTOPART_QUERY = <<<'EOD' + query MyPartSearch($q: String!, $filters: Map, $limit: Int!, $start: Int, $country: String = "DE", $currency: String = "EUR") { + search(q: $q, filters: $filters, limit: $limit, start: $start, country: $country, currency: $currency) { + hits + + results { + part { + id + mpn + slug + short_description + counts + manufacturer { + name + } + best_datasheet { + name + url + credit_string + credit_url + page_count + mime_type + } + best_image { + url + } + specs { + attribute { + name + group + } + display_value + } + document_collections { + name + documents { + name + url + credit_string + credit_url + } + } + descriptions { + credit_string + text + } + cad { + add_to_library_url + } + reference_designs { + name + url + } + sellers { + company { + homepage_url + is_verified + name + slug + } + is_authorized + is_broker + is_rfq + offers { + click_url + inventory_level + moq + packaging + prices { + conversion_rate + converted_currency + converted_price + currency + price + quantity + } + sku + updated + } + } + } + } + } + } +EOD; + + const OCTOPART_PARTQUERY = <<<'EOD' + query MyPartSearch($id: String!, $country: String = "DE", $currency: String = "EUR") { + parts(ids: [$id], country: $country, currency: $currency) { + id + mpn + slug + short_description + counts + manufacturer { + name + } + best_datasheet { + name + url + credit_string + credit_url + page_count + mime_type + } + best_image { + url + } + specs { + attribute { + name + group + } + display_value + } + document_collections { + name + documents { + name + url + credit_string + credit_url + } + } + descriptions { + credit_string + text + } + cad { + add_to_library_url + } + reference_designs { + name + url + } + sellers { + company { + homepage_url + is_verified + name + slug + } + is_authorized + is_broker + is_rfq + offers { + click_url + inventory_level + moq + packaging + prices { + conversion_rate + converted_currency + converted_price + currency + price + quantity + } + sku + updated + } + } + }} +EOD; private $apiKey; + private $limit = "3"; - public function __construct($apiKey) + public function __construct($apiKey, $limit) { $this->apiKey = $apiKey; + $this->limit = $limit; } public function getPartByUID($uid) { + try { + $redisclient = new PredisClient(); + $redisclient->connect(); + $part = $redisclient->get($uid); + if ($part) { + return json_decode($part, true); + } + $redisclient->disconnect(); + } catch (\Exception $e) { + } + $client = new Client(); - $request = $client->createRequest('GET', self::OCTOPART_ENDPOINT."parts/".$uid); - - $request->getQuery()->add("apikey", $this->apiKey); - $request->getQuery()->add("include", [ - "short_description", - "datasheets", - "compliance_documents", - "descriptions", - "imagesets", - "specs", - "reference_designs", - "cad_models", - ]); + $request = $client->createRequest('POST', self::OCTOPART_ENDPOINT); + $request->setHeader('Content-Type', 'application/json'); + $request->getQuery()->add("token", $this->apiKey); + + $graphql = [ + "query" => self::OCTOPART_PARTQUERY, + "operationName" => "MyPartSearch", + "variables" => [ + "id" => $uid, + ], + ]; + $request->setBody(json_encode($graphql)); $request->send(); - return json_decode($request->getResponse()->getBody(), true); + $body = $request->getResponse()->getBody(); + + $data = json_decode($body, true); + + return $data["data"]["parts"][0]; } - public function getPartyByQuery($query, $start = 0) + public function getPartyByQuery($q, $startpage = 1) { $client = new Client(); - $request = $client->createRequest('GET', self::OCTOPART_ENDPOINT."parts/search"); - $request->getQuery()->add("apikey", $this->apiKey); - $request->getQuery()->add("q", $query); - $request->getQuery()->add("start", ($start - 1) * 20); - $request->getQuery()->add("include", ["short_description", "specs", "datasheets"]); - $request->getQuery()->add("limit", 20); + $request = $client->createRequest('POST', self::OCTOPART_ENDPOINT); + $request->setHeader('Content-Type', 'application/json'); + $request->getQuery()->add("token", $this->apiKey); + + $graphql = [ + "query" => self::OCTOPART_QUERY, + "operationName" => "MyPartSearch", + "variables" => [ + "q" => $q, + "limit" => $this->limit, + "start" => ($startpage - 1) * $this->limit, // "start" is 0-based + ], + ]; + $request->setBody(json_encode($graphql)); $request->send(); - return json_decode($request->getResponse()->getBody(), true); + $body = $request->getResponse()->getBody(); + + $parts = json_decode($body, true); + + // work around the low number of allowed accesses to octopart's api + try { + $redisclient = new PredisClient(); + $redisclient->connect(); + $results = $parts["data"]["search"]["results"]; + foreach ($results as $result) { + $id = $result["part"]["id"]; + $redisclient->set($id, json_encode($result["part"])); + } + $redisclient->disconnect(); + } catch (\Exception $e) { + } + + return $parts; } }