diff --git a/docs/_build/doctrees/authors.doctree b/docs/_build/doctrees/authors.doctree index 801c73cf..4ef3eeaa 100644 Binary files a/docs/_build/doctrees/authors.doctree and b/docs/_build/doctrees/authors.doctree differ diff --git a/docs/_build/doctrees/configuration.doctree b/docs/_build/doctrees/configuration.doctree index ed435732..4d18fb69 100644 Binary files a/docs/_build/doctrees/configuration.doctree and b/docs/_build/doctrees/configuration.doctree differ diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle index bc2c85a9..1046103b 100644 Binary files a/docs/_build/doctrees/environment.pickle and b/docs/_build/doctrees/environment.pickle differ diff --git a/docs/_build/doctrees/history.doctree b/docs/_build/doctrees/history.doctree index 3c8bee36..9d4146cf 100644 Binary files a/docs/_build/doctrees/history.doctree and b/docs/_build/doctrees/history.doctree differ diff --git a/docs/_build/doctrees/kicost.distributors.doctree b/docs/_build/doctrees/kicost.distributors.doctree index f8761e57..3bef38d6 100644 Binary files a/docs/_build/doctrees/kicost.distributors.doctree and b/docs/_build/doctrees/kicost.distributors.doctree differ diff --git a/docs/_build/doctrees/kicost.doctree b/docs/_build/doctrees/kicost.doctree index edc19daf..16bd9092 100644 Binary files a/docs/_build/doctrees/kicost.doctree and b/docs/_build/doctrees/kicost.doctree differ diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo index 292c2b8e..32272a99 100644 --- a/docs/_build/html/.buildinfo +++ b/docs/_build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 98975195270f5a3a93724924d432eaa5 +config: 375b3b4efad9f5c43b65b104305e90e0 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_build/html/_modules/index.html b/docs/_build/html/_modules/index.html index 7cf32f55..a499c8f0 100644 --- a/docs/_build/html/_modules/index.html +++ b/docs/_build/html/_modules/index.html @@ -5,7 +5,7 @@ - Overview: module code — kicost 1.1.13 documentation + Overview: module code — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

Navigation

  • modules |
  • - + @@ -42,10 +42,16 @@

    All modules for which code is available

  • kicost.currency_converter.currency_converter
  • kicost.currency_converter.download_rates
  • kicost.distributors
  • - @@ -182,7 +182,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/config.html b/docs/_build/html/_modules/kicost/config.html index 2305010c..bd3ac77c 100644 --- a/docs/_build/html/_modules/kicost/config.html +++ b/docs/_build/html/_modules/kicost/config.html @@ -5,7 +5,7 @@ - kicost.config — kicost 1.1.13 documentation + kicost.config — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -207,9 +207,15 @@

    Source code for kicost.config

             debug_obsessive('Loaded API options {}'.format(api_options))
         else:
             debug_overview('No config file found ({})'.format(file))
    +    # Make sure all APIs are in the options
         for api in get_api_list():
             if api not in api_options:
                 api_options[api] = {}
    +    return api_options
    + + +
    [docs]def fill_missing_with_defaults(): + for api in get_api_list(): # Transfer defaults ops = api_options[api] if 'cache_ttl' not in ops: @@ -253,7 +259,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/currency_converter/currency_converter.html b/docs/_build/html/_modules/kicost/currency_converter/currency_converter.html index 21094eb6..32a0aa95 100644 --- a/docs/_build/html/_modules/kicost/currency_converter/currency_converter.html +++ b/docs/_build/html/_modules/kicost/currency_converter/currency_converter.html @@ -5,7 +5,7 @@ - kicost.currency_converter.currency_converter — kicost 1.1.13 documentation + kicost.currency_converter.currency_converter — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -170,7 +170,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/currency_converter/download_rates.html b/docs/_build/html/_modules/kicost/currency_converter/download_rates.html index 9f9c7cae..52c8b641 100644 --- a/docs/_build/html/_modules/kicost/currency_converter/download_rates.html +++ b/docs/_build/html/_modules/kicost/currency_converter/download_rates.html @@ -5,7 +5,7 @@ - kicost.currency_converter.download_rates — kicost 1.1.13 documentation + kicost.currency_converter.download_rates — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -115,7 +115,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/distributors.html b/docs/_build/html/_modules/kicost/distributors.html index 89bc6e5e..abf4bba4 100644 --- a/docs/_build/html/_modules/kicost/distributors.html +++ b/docs/_build/html/_modules/kicost/distributors.html @@ -5,7 +5,7 @@ - kicost.distributors — kicost 1.1.13 documentation + kicost.distributors — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -188,7 +188,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/distributors/api_digikey.html b/docs/_build/html/_modules/kicost/distributors/api_digikey.html new file mode 100644 index 00000000..a0bbc1bf --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/api_digikey.html @@ -0,0 +1,300 @@ + + + + + + + + kicost.distributors.api_digikey — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.api_digikey

    +# -*- coding: utf-8 -*-
    +# MIT license
    +#
    +# Copyright (C) 2021 by Salvador E. Tropea / Instituto Nacional de Tecnologia Industrial
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to deal
    +# in the Software without restriction, including without limitation the rights
    +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +# copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +# THE SOFTWARE.
    +
    +# Author information.
    +__author__ = 'Salvador Eduardo Tropea'
    +__webpage__ = 'https://github.com/set-soft'
    +__company__ = 'Instituto Nacional de Tecnologia Industrial - Argentina'
    +
    +# Libraries.
    +import pprint
    +
    +# KiCost definitions.
    +from .. import DistData, KiCostError, W_NOINFO, ERR_SCRAPE, W_APIFAIL
    +# Distributors definitions.
    +from .distributor import distributor_class, QueryCache
    +from .log__ import debug_detailed, debug_overview, debug_obsessive, warning
    +
    +available = True
    +try:
    +    from kicost_digikey_api_v3 import by_digikey_pn, by_manf_pn, by_keyword, DigikeyError, DK_API
    +except ImportError:
    +    available = False
    +
    +    class DK_API(object):
    +        api_ops = None
    +
    +DIST_NAME = 'digikey'
    +# Specs known by KiCost
    +SPEC_NAMES = {'tolerance': 'tolerance',
    +              'power (watts)': 'power',
    +              'voltage - rated': 'voltage',
    +              'manufacturer': 'manf',
    +              'size / dimension': 'size',
    +              'temperature coefficient': 'temp_coeff',
    +              'frequency': 'frequency',
    +              'package / case': 'footprint'}
    +__all__ = ['api_digikey']
    +
    +
    +
    [docs]class api_digikey(distributor_class): + name = 'Digi-Key' + type = 'api' + # Currently enabled only by request + enabled = available + url = 'https://developer.digikey.com/' # Web site API information. + api_distributors = [DIST_NAME] + # Options supported by this API + config_options = {'client_id': str, + 'client_secret': str, + 'exclude_market_place_products': bool, + 'sandbox': bool, + 'locale_site': ('US', 'CA', 'JP', 'UK', 'DE', 'AT', 'BE', 'DK', 'FI', 'GR', 'IE', + 'IT', 'LU', 'NL', 'NO', 'PT', 'ES', 'KR', 'HK', 'SG', 'CN', 'TW', + 'AU', 'FR', 'IN', 'NZ', 'SE', 'MX', 'CH', 'IL', 'PL', 'SK', 'SI', + 'LV', 'LT', 'EE', 'CZ', 'HU', 'BG', 'MY', 'ZA', 'RO', 'TH', 'PH'), + 'locale_language': ('en', 'ja', 'de', 'fr', 'ko', 'zhs,' 'zht', 'it', 'es', 'he', + 'nl', 'sv', 'pl', 'fi', 'da', 'no'), + 'locale_currency': ('USD', 'CAD', 'JPY', 'GBP', 'EUR', 'HKD', 'SGD', 'TWD', 'KRW', + 'AUD', 'NZD', 'INR', 'DKK', 'NOK', 'SEK', 'ILS', 'CNY', 'PLN', + 'CHF', 'CZK', 'HUF', 'RON', 'ZAR', 'MYR', 'THB', 'PHP'), + 'locale_ship_to_country': str} + # Legacy environment mapping, others will be automatically filled by `register` + env_prefix = 'DIGIKEY' + env_ops = {'DIGIKEY_STORAGE_PATH': 'cache_path', + 'DIGIKEY_CLIENT_SANDBOX': 'sandbox'} + +
    [docs] @staticmethod + def configure(ops): + DK_API.api_ops = {} + cache_ttl = 7 + cache_path = None + if not available: + debug_obsessive('Digi-Key API not available') + return + for k, v in ops.items(): + if k == 'client_id': + DK_API.id = v + elif k == 'client_secret': + DK_API.secret = v + elif k == 'enable' and available: + api_digikey.enabled = v + elif k == 'sandbox': + DK_API.sandbox = v + elif k == 'cache_ttl': + cache_ttl = v + elif k == 'cache_path': + cache_path = v + elif k == 'exclude_market_place_products': + DK_API.exclude_market_place_products = v + elif k.startswith('locale_'): + DK_API.api_ops[k] = v + if api_digikey.enabled and (DK_API.id is None or DK_API.secret is None or cache_path is None): + warning(W_APIFAIL, "Can't enable Digi-Key without a `client_id`, `client_secret` and `cache_path`") + api_digikey.enabled = False + debug_obsessive('Digi-Key API configured to enabled {} id {} secret {} path {}'. + format(api_digikey.enabled, DK_API.id, DK_API.secret, cache_path)) + if not api_digikey.enabled: + return + # Try to configure the plug-in + cache = QueryCache(cache_path, cache_ttl) + try: + DK_API.configure(cache, a_logger=distributor_class.logger) + except DigikeyError as e: + warning(W_APIFAIL, 'Failed to init Digi-Key API, reason: {}'.format(e.args[0])) + api_digikey.enabled = False
    + + @staticmethod + def _query_part_info(parts, distributors, currency): + '''Fill-in the parts with price/qty/etc info from KitSpace.''' + if DIST_NAME not in distributors: + debug_overview('# Skipping Digi-Key plug-in') + return + debug_overview('# Getting part data from Digi-Key...') + field_cat = DIST_NAME + '#' + + # Setup progress bar to track progress of server queries. + progress = distributor_class.progress(len(parts), distributor_class.logger) + for part in parts: + data = None + # Get the Digi-Key P/N for this part + part_stock = part.fields.get(field_cat) + if part_stock: + debug_detailed('\n**** Digi-Key P/N: {}'.format(part_stock)) + o = by_digikey_pn(part_stock) + data = o.search() + if data is None: + warning(W_NOINFO, 'The \'{}\' Digi-Key code is not valid'.format(part_stock)) + o = by_keyword(part_stock) + data = o.search() + else: + # No Digi-Key P/N, search using the manufacturer code + part_manf = part.fields.get('manf', '') + part_code = part.fields.get('manf#') + if part_code: + if part_manf: + debug_detailed('\n**** Manufacturer: {} P/N: {}'.format(part_manf, part_code)) + else: + debug_detailed('\n**** P/N: {}'.format(part_code)) + o = by_manf_pn(part_code) + data = o.search() + if data is None: + o = by_keyword(part_code) + data = o.search() + if data is None: + warning(W_NOINFO, 'No information found at Digi-Key for part/s \'{}\''.format(part.refs)) + else: + debug_obsessive('* Part info before adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + debug_obsessive('* Data found:') + debug_obsessive(str(data)) + part.datasheet = data.primary_datasheet + part.lifecycle = data.product_status.lower() + specs = {sp.parameter.lower(): (sp.parameter, sp.value) for sp in data.parameters} + specs['rohs'] = ('RoHS', data.ro_hs_status) + part.update_specs(specs) + dd = part.dd.get(DIST_NAME, DistData()) + dd.qty_increment = dd.moq = data.minimum_order_quantity + dd.url = data.product_url + dd.part_num = data.digi_key_part_number + dd.qty_avail = data.quantity_available + dd.currency = data.search_locale_used.currency + dd.price_tiers = {p.break_quantity: p.unit_price for p in data.standard_pricing} + # Extra information + if data.product_description: + dd.extra_info['desc'] = data.product_description + value = '' + for spec in ('capacitance', 'resistance', 'inductance'): + val = specs.get(spec, None) + if val: + value += val[1] + ' ' + if value: + dd.extra_info['value'] = value + for spec, name in SPEC_NAMES.items(): + val = specs.get(spec, None) + if val: + dd.extra_info[name] = val[1] + part.dd[DIST_NAME] = dd + debug_obsessive('* Part info after adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + debug_obsessive(pprint.pformat(dd.__dict__)) + progress.update(1) + progress.close() + +
    [docs] @staticmethod + def query_part_info(parts, distributors, currency): + msg = None + try: + api_digikey._query_part_info(parts, distributors, currency) + except DigikeyError as e: + msg = e.args[0] + if msg is not None: + raise KiCostError(msg, ERR_SCRAPE) + return set([DIST_NAME])
    + + +distributor_class.register(api_digikey, 100) +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/api_element14.html b/docs/_build/html/_modules/kicost/distributors/api_element14.html new file mode 100644 index 00000000..ad212e89 --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/api_element14.html @@ -0,0 +1,539 @@ + + + + + + + + kicost.distributors.api_element14 — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.api_element14

    +# -*- coding: utf-8 -*-
    +# MIT license
    +#
    +# Copyright (C) 2021 by Salvador E. Tropea / Instituto Nacional de Tecnologia Industrial
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to deal
    +# in the Software without restriction, including without limitation the rights
    +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +# copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +# THE SOFTWARE.
    +
    +# Author information.
    +__author__ = 'Salvador Eduardo Tropea'
    +__webpage__ = 'https://github.com/set-soft'
    +__company__ = 'Instituto Nacional de Tecnologia Industrial - Argentina'
    +
    +# Libraries.
    +import pprint
    +import requests
    +import difflib
    +
    +# KiCost definitions.
    +from .. import KiCostError, DistData, W_NOINFO, ERR_SCRAPE, W_APIFAIL, W_AMBIPN
    +# Distributors definitions.
    +from .distributor import distributor_class, QueryCache
    +from .log__ import debug_detailed, debug_overview, debug_obsessive, debug_full, warning, is_debug_full
    +
    +# Countries supported by Farnell (*.farnell.com)
    +FARNELL_SUP = {'bg': 'EUR',
    +               'cz': 'CZK',
    +               'dk': 'DKK',
    +               'at': 'EUR',
    +               'ch': 'CHF',
    +               'de': 'EUR',
    +               'ie': 'EUR',
    +               'il': 'USD',
    +               'uk': 'GBP',
    +               'es': 'EUR',
    +               'ee': 'EUR',
    +               'fi': 'EUR',
    +               'fr': 'EUR',
    +               'hu': 'HUF',
    +               'it': 'EUR',
    +               'lt': 'EUR',
    +               'lv': 'EUR',
    +               'be': 'EUR',
    +               'nl': 'EUR',
    +               'no': 'NOK',
    +               'pl': 'PLN',
    +               'pt': 'EUR',
    +               'ro': 'RON',
    +               'ru': 'EUR',
    +               'sk': 'EUR',
    +               'si': 'EUR',
    +               'se': 'SEK',
    +               'tr': 'EUR'}
    +# Countries supported by Newark
    +NEWARK_SUP = {'us': ('www.newark.com', 'USD'),
    +              'ca': ('canada.newark.com', 'CAD'),
    +              'mx': ('mexico.newark.com', 'USD')}
    +# Countries supported by Element14, they accept Newark and Farnell SKUs
    +ELEMENT14_SUP = {'cn': 'CNY',
    +                 'au': 'AUD',
    +                 'nz': 'NZD',
    +                 'hk': 'HKD',
    +                 'sg': 'SGD',
    +                 'my': 'MYR',
    +                 'ph': 'PHP',
    +                 'th': 'THB',
    +                 'in': 'INR',
    +                 'kr': 'KRW',
    +                 'vn': 'USD',
    +                 # The European Central Bank doesn't provide exchange for Taiwan
    +                 # 'tw': 'TWD'
    +                 }
    +# Countries supported by CPC
    +CPC_SUP = {'uk': ('cpc.farnell.com', 'GBP'),
    +           'ie': ('cpcireland.farnell.com', 'EUR')}
    +# Specs known by KiCost
    +SPEC_NAMES = {'power rating': 'power',
    +              'voltage rating': 'voltage',
    +              'vendorName': 'manf',
    +              'temperature coefficient': 'temp_coeff'}
    +
    +# DIST_NAMES = ['cpc', 'farnell', 'newark']
    +DIST_NAMES = ['farnell', 'newark']
    +PRE_TERM = {'sku': 'id', 'key': 'any', 'mpn': 'manuPartNum'}
    +REPLY = {'sku': 'premierFarnellPartNumberReturn', 'key': 'keywordSearchReturn', 'mpn': 'manufacturerPartNumberSearchReturn'}
    +
    +BASE_URL = 'https://api.element14.com/catalog/products'
    +
    +__all__ = ['api_element14']
    +
    +
    +class Element14Error(Exception):
    +    pass
    +
    +
    +class Element14(object):
    +    def __init__(self, dist, country, key, cache):
    +        if dist == 'farnell':
    +            # Farnell Europe
    +            cur = FARNELL_SUP.get(country, None)
    +            if cur is not None:
    +                self.currency = cur
    +                self.store_info_id = country + '.farnell.com'
    +            else:
    +                # Element14 Asia and Oceania
    +                cur = ELEMENT14_SUP.get(country, None)
    +                if cur is not None:
    +                    self.currency = cur
    +                    self.store_info_id = country + '.element14.com'
    +                else:
    +                    raise Element14Error('Unsupported country `{}` for `{}`'.format(country, dist))
    +        elif dist == 'newark':
    +            # Newark North America
    +            url, cur = NEWARK_SUP.get(country, (None, None))
    +            if cur is not None:
    +                self.currency = cur
    +                self.store_info_id = url
    +            else:
    +                # Element14 Asia and Oceania
    +                cur = ELEMENT14_SUP.get(country, None)
    +                if cur is not None:
    +                    self.currency = cur
    +                    self.store_info_id = country + '.element14.com'
    +                else:
    +                    raise Element14Error('Unsupported country `{}` for `{}`'.format(country, dist))
    +        elif dist == 'cpc':
    +            # Great Britain CPC
    +            url, cur = CPC_SUP.get(country, (None, None))
    +            if cur is not None:
    +                self.currency = cur
    +                self.store_info_id = url
    +            else:
    +                raise Element14Error('Unsupported country `{}` for `{}`'.format(country, dist))
    +        self.key = key
    +        self.cache = cache
    +
    +    def _extract_data(self, data, kind, term):
    +        res = REPLY[kind]
    +        if res not in data:
    +            raise Element14Error("Malformed reply " + str(data))
    +        data = data[res]
    +        c = None
    +        try:
    +            c = data['numberOfResults']
    +        except KeyError:
    +            pass
    +        if c is None:
    +            raise Element14Error("Missing `numberOfResults` " + str(data))
    +        if c == 0:
    +            return None
    +        if 'products' not in data:
    +            raise Element14Error("Missing `products` " + str(data))
    +        prod = data['products']
    +        if c != 1:
    +            warning(W_AMBIPN, "Got {} hits for {} {}".format(c, kind, term))
    +        return prod
    +
    +    def search(self, kind, term, part={}):
    +        # Try to get the data from the cache
    +        full_name = term+'_'+self.store_info_id
    +        data, loaded = self.cache.load_results(kind, full_name)
    +        if loaded:
    +            debug_obsessive('Data from cache: '+pprint.pformat(data))
    +            return self._extract_data(data, kind, term)
    +        # Do a query
    +        params = {'callInfo.responseDataFormat': 'JSON',
    +                  'term': PRE_TERM[kind]+':'+term,
    +                  'storeInfo.id': self.store_info_id,
    +                  'callInfo.apiKey': self.key,
    +                  'resultsSettings.responseGroup': 'large'}
    +        if kind == 'key':
    +            params['resultsSettings.offset'] = '0'
    +            params['resultsSettings.numberOfResults'] = '10'
    +        params.update(part)
    +        debug_obsessive('Query params: '+pprint.pformat(params))
    +        r = requests.get(BASE_URL, params=params)
    +        if r.status_code != 200:
    +            # debug_obsessive(pprint.pformat(r.__dict__))
    +            raise Element14Error("Server error `{}` ({})".format(r.reason, r.status_code))
    +        data = r.json()
    +        debug_obsessive('Data from server: '+pprint.pformat(data))
    +        self.cache.save_results(kind, full_name, data)
    +        return self._extract_data(data, kind, term)
    +
    +    def by_sku(self, sku):
    +        debug_detailed('Search by SKU '+sku)
    +        return self.search('sku', sku)
    +
    +    def by_keyword(self, key):
    +        debug_detailed('Search by keyword '+key)
    +        return self.search('key', key)
    +
    +    def by_manf_pn(self, pn):
    +        debug_detailed('Search by part number '+pn)
    +        return self.search('mpn', pn)
    +
    +
    +
    [docs]class api_element14(distributor_class): + name = 'Element14' + type = 'api' + # Currently enabled only by request + enabled = True + url = 'https://partner.element14.com/' # Web site API information. + api_distributors = DIST_NAMES + # Options supported by this API + config_options = {'key': str, + 'try_by_keyword': bool, + 'farnell_country': ('BG', 'CZ', 'DK', 'AT', 'CH', 'DE', 'IE', 'IL', 'UK', 'ES', 'EE', 'FI', 'FR', 'HU', 'IT', 'LT', + 'LV', 'BE', 'NL', 'NO', 'PL', 'PT', 'RO', 'RU', 'SK', 'SI', 'SE', 'TR', 'CN', 'AU', 'NZ', 'HK', + 'SG', 'MY', 'PH', 'TH', 'IN', 'KR', 'VN'), + 'newark_country': ('CA', 'US', 'MX', 'CN', 'AU', 'NZ', 'HK', 'SG', 'MY', 'PH', 'TH', 'IN', 'KR', 'VN'), + 'cpc_country': ('IE', 'UK')} + key = None + countries = {'cpc': 'uk', 'farnell': 'uk', 'newark': 'us'} + try_by_keyword = False + env_prefix = 'ELEMENT14' + env_ops = {'ELEMENT14_PART_API_KEY': 'key'} + +
    [docs] @staticmethod + def configure(ops): + cache_ttl = 7 + cache_path = None + for k, v in ops.items(): + if k == 'key': + api_element14.key = v + elif k == 'enable': + api_element14.enabled = v + elif k == 'try_by_keyword': + api_element14.try_by_keyword = v + elif k == 'cache_ttl': + cache_ttl = v + elif k == 'cache_path': + cache_path = v + elif k.endswith('_country'): + api_element14.countries[v[:-8]] = v.lower() + if api_element14.enabled and api_element14.key is None: + warning(W_APIFAIL, "Can't enable Elemen14 without a `key`") + api_element14.enabled = False + debug_obsessive('Element14 API configured to enabled {} key {} path {}'.format(api_element14.enabled, api_element14.key, cache_path)) + if not api_element14.enabled: + return + # Configure the cache + api_element14.cache = QueryCache(cache_path, cache_ttl)
    + + @staticmethod + def _query_part_info(dist, country, parts, distributors, currency): + '''Fill-in the parts with price/qty/etc info from KitSpace.''' + debug_overview('# Getting part data from Element14 ({} {})...'.format(dist, country)) + field_cat = dist + '#' + o = Element14(dist, country, api_element14.key, api_element14.cache) + + # Setup progress bar to track progress of server queries. + progress = distributor_class.progress(len(parts), distributor_class.logger) + for part in parts: + data = None + # Get the Element14 P/N for this part + part_stock = part.fields.get(field_cat) + part_manf = part.fields.get('manf', '') + part_code = part.fields.get('manf#') + if part_stock: + debug_detailed('\n**** {} P/N: {}'.format(dist, part_stock)) + data = o.by_sku(part_stock) + if data is None: + warning(W_NOINFO, 'The \'{}\' {} code is not valid'.format(part_stock, dist)) + if api_element14.try_by_keyword: + data = o.by_keyword(part_stock) + else: + # No Element14 P/N, search using the manufacturer code + if part_code: + if part_manf: + debug_detailed('\n**** Manufacturer: {} P/N: {}'.format(part_manf, part_code)) + else: + debug_detailed('\n**** P/N: {}'.format(part_code)) + data = o.by_manf_pn(part_code) + if data is None and api_element14.try_by_keyword: + data = o.by_keyword(part_code) + if data is None: + warning(W_NOINFO, 'No information found at {} for part/s \'{}\''.format(dist, part.refs)) + else: + data = _select_best(data, part_manf, part.qty_total_spreadsheet) + debug_obsessive('* Part info before adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + debug_obsessive('* Data found:') + debug_obsessive(pprint.pformat(data)) + ds = data.get('datasheets', None) + if part.datasheet is None and ds is not None: + part.datasheet = ds[0]['url'] + if part.lifecycle is None: + part.lifecycle = 'obsolete' if data['productStatus'] == 'NO_LONGER_MANUFACTURED' else 'active' + tolerance = footprint = frequency = None + specs = {'rohs': ('RoHS', data['rohsStatusCode'])} + for sp in data.get('attributes', []): + name = sp['attributeLabel'] + name_l = name.lower() + value = sp['attributeValue'] + unit = sp.get('attributeUnit', None) + if unit: + value = value + ' ' + unit + specs[name_l] = (name, value) + if name_l.endswith('tolerance'): + tolerance = value + if name_l.endswith('case style'): + footprint = value + if name_l.endswith('frequency'): + frequency = value + part.update_specs(specs) + dd = part.dd.get(dist, DistData()) + dd.qty_increment = dd.moq = data['translatedMinimumOrderQuality'] + dd.url = 'https://'+o.store_info_id+'/w/search?st='+data['sku'] + dd.part_num = data['sku'] + dd.qty_avail = data['inv'] + dd.currency = o.currency + prices = data.get('prices', None) + if prices: + dd.price_tiers = {p['from']: p['cost'] for p in prices} + # Extra information + dd.extra_info['desc'] = data['displayName'] + value = '' + for spec in ('capacitance', 'resistance', 'inductance'): + val = specs.get(spec, None) + if val: + value += val[1] + ' ' + if value: + dd.extra_info['value'] = value + if tolerance: + dd.extra_info['tolerance'] = tolerance + if footprint: + dd.extra_info['footprint'] = footprint + if frequency: + dd.extra_info['frequency'] = frequency + for spec, name in SPEC_NAMES.items(): + val = specs.get(spec, None) + if val: + dd.extra_info[name] = val[1] + part.dd[dist] = dd + debug_obsessive('* Part info after adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + # debug_obsessive(pprint.pformat(dd.__dict__)) + progress.update(1) + progress.close() + +
    [docs] @staticmethod + def query_part_info(parts, distributors, currency): + if len(set(DIST_NAMES).intersection(distributors)) == 0: + # None of our distributors is used + debug_overview('# Skipping Element14 plug-in') + return set() + msg = None + try: + for dist in DIST_NAMES: + country = api_element14.countries[dist] + api_element14._query_part_info(dist, country, parts, distributors, currency) + except Element14Error as e: + msg = e.args[0] + if msg is not None: + raise KiCostError(msg, ERR_SCRAPE) + return set(DIST_NAMES)
    + + +# Ok, this is special case ... we should add others +MANF_CHANGES = {'fairchild': 'onsemi'} + + +def _get_price(d): + """ Return the first price or -1 if none """ + prices = d.get('prices', None) + return prices[0]['cost'] if prices else -1 + + +def _get_key(d, qty): + """ Sorting criteria for the suggested option """ + price = _get_price(d) + stock = d['inv'] + moq = d['translatedMinimumOrderQuality'] + return (price == -1, # Put first the ones with price + stock == 0, # Put first the ones in stock + stock < qty, # Put first the ones with enough stock + # TODO: some tollerance to the MOQ? Like buy 20% extra + moq > qty, # Put first the ones with a MOQ under the quantity we need + d['productStatus'].startswith('NO_LONGER_'), # Put first the active components + price, + moq) # At equal price suggest the lower MOQ + + +def _filter_by_manf(data, manf): + """ Select the best matches according to the manufacturer """ + if not manf: + return data + manfs = {d['brandName'].lower() for d in data} + if len(manfs) == 1: + return data + manf = manf.lower() + best_matches = difflib.get_close_matches(manf, manfs) + if len(best_matches) == 0: + new_name = None + for k, v in MANF_CHANGES.items(): + if k in manf: + new_name = v + break + if new_name: + best_matches = difflib.get_close_matches(new_name, manfs) + if len(best_matches) == 0: + return data + best_match = best_matches[0] + return list(filter(lambda x: x['brandName'].lower() == best_match, data)) + + +def _list_comp_options(data, show, msg): + """ Debug function used to show the list of options """ + if not show: + return + debug_full(' - '+msg) + for c, d in enumerate(data): + debug_full(' {}) {} {} inv: {} moq: {} status: {} price: {}'. + format(c+1, d['brandName'], d['translatedManufacturerPartNumber'], d['inv'], + d['translatedMinimumOrderQuality'], d['productStatus'], _get_price(d))) + + +def _select_best(data, manf, qty): + """ Selects the best result """ + c = len(data) + if c == 1: + return data[0] + debug_obsessive(' - Choosing the best match ({} options, qty: {} manf: {})'.format(c, qty, manf)) + ultra_debug = is_debug_full() + _list_comp_options(data, ultra_debug, 'Original list') + # Try to choose the best manufacturer + data2 = _filter_by_manf(data, manf) + if data != data2: + debug_obsessive(' - Selected manf `{}`'.format(data2[0]['brandName'])) + _list_comp_options(data2, ultra_debug, 'Manufacturer selected') + if len(data2) == 1: + return data2[0] + # Sort the results according to the best availability/price + data3 = sorted(data2, key=lambda x: _get_key(x, qty)) + _list_comp_options(data3, ultra_debug, 'Sorted') + return data3[0] + + +distributor_class.register(api_element14, 100) +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/api_mouser.html b/docs/_build/html/_modules/kicost/distributors/api_mouser.html new file mode 100644 index 00000000..bc914330 --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/api_mouser.html @@ -0,0 +1,467 @@ + + + + + + + + kicost.distributors.api_mouser — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.api_mouser

    +# -*- coding: utf-8 -*-
    +# MIT license
    +#
    +# Copyright (c) 2021 SPARK Microsystems
    +# Copyright (c) 2021-2022 by Salvador E. Tropea / Instituto Nacional de Tecnologia Industrial
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to deal
    +# in the Software without restriction, including without limitation the rights
    +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +# copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +# THE SOFTWARE.
    +#
    +# Most of the API code comes from https://github.com/sparkmicro/mouser-api
    +
    +# Author information.
    +__author__ = 'Salvador Eduardo Tropea'
    +__webpage__ = 'https://github.com/set-soft'
    +__company__ = 'Instituto Nacional de Tecnologia Industrial - Argentina'
    +
    +# Libraries.
    +import pprint
    +import json
    +import requests
    +import re
    +
    +# KiCost definitions.
    +from .. import KiCostError, DistData, W_NOINFO, W_APIFAIL, ERR_SCRAPE
    +# Distributors definitions.
    +from .distributor import distributor_class, QueryCache
    +from .log__ import debug_detailed, debug_overview, debug_obsessive, warning
    +
    +available = True
    +
    +DIST_NAME = 'mouser'
    +# Mouser Base URL
    +BASE_URL = 'https://api.mouser.com/api/v1.0'
    +IN_STOCK = ('In stock', 'En existencias', 'A magazzino', 'Auf Lager', 'En stock', 'Em estoque', 'Na skladě',
    +            'På lager', 'Склад в США', 'In voorraad', 'Na stanie magazynowym', '库存', '在庫',
    +            'สต็อก', 'Tồn kho')
    +in_stock_re_1 = re.compile(r'(\d+)\s*(?:'+'|'.join(IN_STOCK)+')', re.I)
    +in_stock_re_2 = re.compile(r'\s*(?:'+'|'.join(IN_STOCK)+r')\D*(\d+)', re.I)
    +
    +__all__ = ['api_mouser']
    +
    +
    +class MouserError(Exception):
    +    pass
    +
    +
    +def get_number(string):
    +    index = next((i for i, d in enumerate(string) if d.isdigit()), None)
    +    if index is None:
    +        raise MouserError('Malformed price: ' + string)
    +    string = string.replace(',', '.')
    +    end = next((i for i, d in enumerate(string[index:], start=index) if not (d.isdigit() or d == '.')), None)
    +    if end is not None:
    +        return float(string[index:end])
    +    return float(string[index:])
    +
    +
    +# ####################################
    +# Base classes
    +# ####################################
    +
    +
    +class MouserAPIRequest:
    +    """ Mouser API Request """
    +
    +    url = None
    +    api_url = None
    +    method = None
    +    body = {}
    +    response = None
    +    api_key = None
    +
    +    def __init__(self, url, method, key, *args):
    +        if not url or not method:
    +            return None
    +        self.api_url = BASE_URL + url
    +        self.method = method
    +
    +        # Append argument
    +        if len(args) == 1:
    +            self.api_url += '/' + str(args[0])
    +
    +        # Append API Key
    +        self.api_key = key
    +
    +        if self.api_key:
    +            self.url = self.api_url + '?apiKey=' + self.api_key
    +
    +    def get(self, url):
    +        response = requests.get(url=url)
    +        return response
    +
    +    def post(self, url, body):
    +        headers = {
    +            'Content-Type': 'application/json',
    +        }
    +        response = requests.post(url=url, data=json.dumps(body), headers=headers)
    +        return response
    +
    +    def run(self, body={}):
    +        if self.method == 'GET':
    +            self.response = self.get(self.url)
    +        elif self.method == 'POST':
    +            self.response = self.post(self.url, body)
    +        if self.response:
    +            self.response_parsed = self.get_response()
    +        else:
    +            self.response_parsed = None
    +
    +        return True if self.response else False
    +
    +    def get_response(self):
    +        if self.response is not None:
    +            try:
    +                return json.loads(self.response.text)
    +            except json.decoder.JSONDecodeError:
    +                return self.response.text
    +
    +        return {}
    +
    +    def __str__(self):
    +        if self.response_parsed is None:
    +            return 'None'
    +        return json.dumps(self.response_parsed, indent=4, sort_keys=True)
    +
    +
    +class MouserBaseRequest(MouserAPIRequest):
    +    """ Mouser Base Request """
    +
    +    name = ''
    +    allowed_methods = ['GET', 'POST']
    +    operation = None
    +    operations = {}
    +
    +    def __init__(self, operation, key, *args):
    +        ''' Init '''
    +        if operation not in self.operations:
    +            msg = '[{}] Invalid Operation'.format(self.name)
    +            valid_operations = [op for op, values in self.operations.items() if values[0] and values[1]]
    +            if valid_operations:
    +                msg += ' Valid operations: ' + str(valid_operations)
    +            raise MouserError(msg)
    +            return
    +
    +        self.operation = operation
    +        (method, url) = self.operations.get(self.operation, ('', ''))
    +        if not url or not method or method not in self.allowed_methods:
    +            raise MouserError('[{}]\tOperation "{}" Not Yet Supported'.format(self.name, operation))
    +            return
    +        super().__init__(url, method, key, *args)
    +
    +
    +# ####################################
    +# Part Search
    +# ####################################
    +
    +class MouserPartSearchRequest(MouserBaseRequest):
    +    """ Mouser Part Search Request """
    +
    +    name = 'Part Search'
    +    operations = {
    +        'keyword': ('', ''),
    +        'keywordandmanufacturer': ('', ''),
    +        'partnumber': ('POST', '/search/partnumber'),
    +        'partnumberandmanufacturer': ('', ''),
    +        'manufacturerlist': ('', ''),
    +    }
    +
    +    def __init__(self, operation, key, *args):
    +        ''' Init '''
    +        super().__init__(operation, key, *args)
    +
    +    @staticmethod
    +    def get_clean_response(response):
    +        cleaned_data = {
    +            'Availability': '',
    +            'Category': '',
    +            'DataSheetUrl': '',
    +            'Description': '',
    +            'ImagePath': '',
    +            'LifecycleStatus': '',
    +            'Manufacturer': '',
    +            'ManufacturerPartNumber': '',
    +            'Min': '',
    +            'MouserPartNumber': '',
    +            'ProductDetailUrl': '',
    +            'ProductAttributes': [],
    +            'PriceBreaks': [],
    +        }
    +
    +        if not response:
    +            return None
    +        try:
    +            parts = response['SearchResults'].get('Parts', [])
    +        except AttributeError:
    +            return None
    +
    +        if not parts:
    +            return None
    +        # Process first part
    +        part_data = parts[0]
    +        # Merge
    +        for key in cleaned_data:
    +            cleaned_data[key] = part_data.get(key, cleaned_data[key])
    +        return cleaned_data
    +
    +    def print_clean_response(self):
    +        print(json.dumps(self.get_clean_response(self.response_parsed), indent=4, sort_keys=True))
    +
    +    def get_body(self, **kwargs):
    +        body = {}
    +        if self.operation == 'partnumber':
    +            part_number = kwargs.get('part_number', None)
    +            option = kwargs.get('option', 'None')
    +
    +            if part_number:
    +                body = {
    +                    'SearchByPartRequest': {
    +                        'mouserPartNumber': part_number,
    +                        'partSearchOptions': option,
    +                    }
    +                }
    +        return body
    +
    +    def part_search(self, part_number, option='None'):
    +        '''Mouser Part Number Search '''
    +        kwargs = {
    +            'part_number': part_number,
    +            'option': option,
    +        }
    +        self.body = self.get_body(**kwargs)
    +        if self.api_key:
    +            res = self.run(self.body)
    +            if res and self.response_parsed is not None:
    +                errors = self.response_parsed.get('Errors', None)
    +                if errors is not None and len(errors) >= 1:
    +                    error = errors[0]
    +                    raise MouserError(error['Message'] + ' (' + error['Code'] + ' ' + error['PropertyName'] + ')')
    +            return res
    +        else:
    +            return False
    +
    +
    +
    [docs]class api_mouser(distributor_class): + name = 'Mouser' + type = 'api' + enabled = True + url = 'https://api.mouser.com/' # Web site API information. + api_distributors = [DIST_NAME] + # Options supported by this API + config_options = {'key': str} + key = None + cache = None + env_prefix = 'MOUSER' + env_ops = {'MOUSER_PART_API_KEY': 'key'} + +
    [docs] @staticmethod + def configure(ops): + cache_ttl = 7 + cache_path = None + for k, v in ops.items(): + if k == 'key': + api_mouser.key = v + elif k == 'enable': + api_mouser.enabled = v + elif k == 'cache_ttl': + cache_ttl = v + elif k == 'cache_path': + cache_path = v + if api_mouser.enabled and api_mouser.key is None: + warning(W_APIFAIL, "Can't enable Mouser without a `key`") + api_mouser.enabled = False + debug_obsessive('Mouser API configured to enabled {} key {} path {}'.format(api_mouser.enabled, api_mouser.key, cache_path)) + if not api_mouser.enabled: + return + # Try to configure the plug-in + api_mouser.cache = QueryCache(cache_path, cache_ttl)
    + + @staticmethod + def _query_part_info(parts, distributors, currency): + '''Fill-in the parts with price/qty/etc info from KitSpace.''' + if DIST_NAME not in distributors: + debug_overview('# Skipping Mouser plug-in') + return + debug_overview('# Getting part data from Mouser...') + field_cat = DIST_NAME + '#' + # Setup progress bar to track progress of server queries. + progress = distributor_class.progress(len(parts), distributor_class.logger) + for part in parts: + partnumber = None + data = None + # Get the Mouser P/N for this part + part_stock = part.fields.get(field_cat) + if part_stock: + partnumber = part_stock + prefix = 'mou' + else: + # No Mouser P/N, search using the manufacturer code + partnumber = part.fields.get('manf#') + prefix = 'mpn' + if partnumber: + debug_detailed('P/N: ' + partnumber) + request, loaded = api_mouser.cache.load_results(prefix, partnumber) + if loaded: + data = MouserPartSearchRequest.get_clean_response(request) + else: + request = MouserPartSearchRequest('partnumber', api_mouser.key) + if request.part_search(partnumber): + data = request.get_clean_response(request.response_parsed) + api_mouser.cache.save_results(prefix, partnumber, request.response_parsed) + + if data is None: + warning(W_NOINFO, 'No information found at Mouser for part/s \'{}\''.format(part.refs)) + else: + debug_obsessive('* Part info before adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + debug_obsessive('* Data found:') + debug_obsessive(str(request)) + if not part.datasheet: + datasheet = data['DataSheetUrl'] + if datasheet: + part.datasheet = datasheet + if not part.lifecycle: + lifecycle = data['LifecycleStatus'] + if lifecycle: + part.lifecycle = lifecycle.lower() + dd = part.dd.get(DIST_NAME, DistData()) + dd.qty_increment = dd.moq = int(data['Min']) + dd.url = data['ProductDetailUrl'] + dd.part_num = data['MouserPartNumber'] + dd.qty_avail = 0 + availability = data['Availability'] + debug_detailed('Availability: '+availability) + dd.qty_avail_comment = availability + res_stock = in_stock_re_1.match(availability) + if not res_stock: + res_stock = in_stock_re_2.match(availability) + debug_detailed('- Search for stock: {}'.format(res_stock)) + if res_stock: + dd.qty_avail = int(res_stock.group(1)) + debug_detailed('- Detected stock: {}'.format(dd.qty_avail)) + pb = data['PriceBreaks'] + dd.currency = pb[0]['Currency'] if pb else currency + dd.price_tiers = {p['Quantity']: get_number(p['Price']) for p in pb} + # Extra information + product_description = data['Description'] + if product_description: + dd.extra_info['desc'] = product_description + part.dd[DIST_NAME] = dd + debug_obsessive('* Part info after adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + debug_obsessive(pprint.pformat(dd.__dict__)) + progress.update(1) + progress.close() + +
    [docs] @staticmethod + def query_part_info(parts, distributors, currency): + msg = None + try: + api_mouser._query_part_info(parts, distributors, currency) + except MouserError as e: + msg = e.args[0] + if msg is not None: + raise KiCostError(msg, ERR_SCRAPE) + return set([DIST_NAME])
    + + +distributor_class.register(api_mouser, 100) +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/api_nexar.html b/docs/_build/html/_modules/kicost/distributors/api_nexar.html new file mode 100644 index 00000000..176e7f70 --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/api_nexar.html @@ -0,0 +1,725 @@ + + + + + + + + kicost.distributors.api_nexar — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.api_nexar

    +# -*- coding: utf-8 -*-
    +# Nexar API implementation, replaces Octopart API
    +#
    +# MIT license
    +#
    +# Copyright (C) 2022 by Salvador E. Tropea / Instituto Nacional de Tecnologia Industrial
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to deal
    +# in the Software without restriction, including without limitation the rights
    +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +# copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +# THE SOFTWARE.
    +#
    +# I took the basic query from  https://github.com/NexarDeveloper/nexar-first-supply-query.git
    +# and the example in Nexar's IDE
    +# The class is based on the api_partinfo_kitspace.py class
    +#
    +# Important note:
    +# We get offers from tons of distributors, there is no way to say which ones we want.
    +# In:
    +# https://support.nexar.com/support/solutions/articles/101000434623-how-can-i-get-an-exact-manufacturer-match-with-specific-suppliers
    +# Suggests to use:
    +# options: {filters: {distributor_id: ["459", "1106", "2401", "2628", "2454", "3261", "12947"]}}
    +# But this isn't a "hard" filter, according to the support people you must filter it anyways.
    +
    +# Author information.
    +__author__ = 'Salvador Eduardo Tropea'
    +__webpage__ = 'https://github.com/set-soft'
    +__company__ = 'Instituto Nacional de Tecnologia Industrial - Argentina'
    +
    +# Libraries.
    +import copy
    +import difflib
    +import json
    +import os
    +import pprint
    +import re
    +import requests
    +import sys
    +import time
    +from collections import OrderedDict
    +if sys.version_info[0] < 3:
    +    from urllib import quote_plus
    +else:
    +    from urllib.parse import quote_plus
    +
    +# KiCost definitions.
    +from .. import KiCostError, DistData, DEFAULT_CURRENCY, ERR_SCRAPE, W_NOINFO, NO_PRICE, W_APIFAIL, W_AMBIPN
    +# Distributors definitions.
    +from .distributor import distributor_class, QueryCache
    +from .log__ import debug_overview, debug_obsessive, warning
    +
    +
    +# Uncomment for debug
    +# Use `debug('x + 1')` for instance.
    +# def debug(expression):
    +#     frame = sys._getframe(1)
    +#     distributor_class.logger.info(expression, '=', repr(eval(expression, frame.f_globals, frame.f_locals)))
    +
    +MAX_PARTS_PER_QUERY = 20  # Maximum number of parts in a single query.
    +
    +# Information to return from Nexar API
    +QUERY_ANSWER = '''
    +    hits,
    +    reference,
    +    parts {
    +      id,
    +      slug,
    +      mpn,
    +      manufacturer {name, id},
    +      shortDescription,
    +      specs {
    +        attribute {shortname, name, id},
    +        displayValue
    +      },
    +      octopartUrl,
    +      bestDatasheet {name, url},
    +      sellers(authorizedOnly: false) {
    +        company {name, id},
    +        offers {
    +            sku,
    +            id,
    +            inventoryLevel,
    +            moq,
    +            orderMultiple,
    +            packaging,
    +            prices {quantity, price, currency},
    +            onOrderQuantity,
    +            clickUrl
    +        }
    +      }
    +    }
    +'''
    +QUERY_ANSWER = re.sub(r'[\s\n]', '', QUERY_ANSWER)
    +
    +
    +QUERY_MATCH = ('query MultiMatchSearch($queries: [SupPartMatchQuery!]!) {'
    +               'supMultiMatch(queries: $queries, country: "@COUNTRY@", currency: "@CUR@") {' +
    +               QUERY_ANSWER + '} }')
    +QUERY_URL = 'https://api.nexar.com/graphql'
    +PROD_TOKEN_URL = 'https://identity.nexar.com/connect/token'
    +# Specs known by KiCost
    +SPEC_NAMES = {'tolerance': 'tolerance',
    +              'frequency': 'frequency',
    +              'powerrating': 'power',
    +              'voltagerating': 'voltage',
    +              'temperaturecoefficient': 'temp_coeff',
    +              'case_package': 'footprint',
    +              'maxdccurrent': 'current',
    +              'forwardcurrent': 'current',
    +              'currentrating': 'current'}
    +
    +
    +__all__ = ['api_nexar']
    +
    +
    +class QueryStruct(object):
    +    def __init__(self, id, part, for_dists, seller=None, sku=None, manf=None, mpn=None):
    +        self.id = 'q'+str(id)
    +        query = OrderedDict()
    +        query["reference"] = self.id
    +        query["start"] = 0
    +        query["limit"] = 5
    +        self.seller = seller
    +        self.sku = sku
    +        self.manf = manf
    +        self.mpn = mpn
    +        if sku is not None:
    +            query['seller'] = seller
    +            query['sku'] = sku
    +        elif mpn is not None:
    +            if manf:
    +                query['manufacturer'] = manf
    +            query['mpn'] = mpn
    +        self.query = query  # The query dict
    +        self.part = part    # Pointer to the part.
    +        self.for_dists = for_dists  # List of distributors we want for this query
    +        self.result = None
    +        self.loaded = False
    +
    +    def remove_manfucturer(self):
    +        self.manf = None
    +        del self.query['manufacturer']
    +
    +
    +
    [docs]class api_nexar(distributor_class): + name = 'Nexar' + type = 'api' + enabled = True + url = 'https://nexar.com/api' # Web site API information. + id = None + secret = None + country = 'US' + env_prefix = 'NEXAR' + env_ops = {'NEXAR_STORAGE_PATH': 'cache_path'} + + # This is what we used for Octopart + # https://octopart.com/api/v4/values#sellers + api_distributors = ['arrow', 'digikey', 'farnell', 'lcsc', 'mouser', 'newark', 'rs', 'tme'] + DIST_TRANSLATION = { # Distributor translation. Just a few supported. + 'Arrow Electronics': 'arrow', + 'Digi-Key': 'digikey', + 'Farnell': 'farnell', + 'Mouser': 'mouser', + 'Newark': 'newark', + 'RS Components': 'rs', + 'TME': 'tme', + 'LCSC': 'lcsc', + } + # Dict to translate KiCost field names into KitSpace distributor names + KICOST2NEXAR_DIST = {v: k for k, v in DIST_TRANSLATION.items()} + cache = None + config_options = {'client_id': str, 'client_secret': str, 'country': str} + # Token + expiration = 0 + token = None + access_token = None + +
    [docs] @staticmethod + def configure(ops): + cache_ttl = 7 + cache_path = None + for k, v in ops.items(): + if k == 'enable': + api_nexar.enabled = v + elif k == 'cache_ttl': + cache_ttl = v + elif k == 'cache_path': + cache_path = v + elif k == 'client_id': + api_nexar.id = v + elif k == 'client_secret': + api_nexar.secret = v + elif k == 'country': + api_nexar.country = v + if api_nexar.enabled and (api_nexar.id is None or api_nexar.secret is None or cache_path is None): + warning(W_APIFAIL, "Can't enable Nexar without a `client_id`, `client_secret` and `cache_path`") + api_nexar.enabled = False + debug_obsessive('Nexar API configured to enabled {}'.format(api_nexar.enabled)) + if not api_nexar.enabled: + return + api_nexar.cache = QueryCache(cache_path, cache_ttl)
    + +
    [docs] @staticmethod + def get_token(): + """ Return the Nexar token from the client_id and client_secret provided """ + token = {} + try: + token = requests.post(url=PROD_TOKEN_URL, + data={"grant_type": "client_credentials", + "client_id": api_nexar.id, + "client_secret": api_nexar.secret}, + allow_redirects=False).json() + except Exception as e: + raise KiCostError('Error getting token from Nexar ({})'.format(e), ERR_SCRAPE) + debug_obsessive('Nexar token {}'.format(token)) + return token
    + +
    [docs] @staticmethod + def get_headers(): + """ Get the access token, make sure it isn't expired """ + if os.environ.get('KICOST_NEXAR_NO_TOKEN', None): + # No token for debug + return {} + if api_nexar.expiration < time.time() + 300: + api_nexar.token = api_nexar.get_token() + api_nexar.access_token = api_nexar.token.get('access_token') + api_nexar.expiration = time.time() + api_nexar.token.get('expires_in', 0) + return {'token': api_nexar.access_token}
    + +
    [docs] @staticmethod + def query(query_parts, currency, query_type=QUERY_MATCH): + """ Send query to server and return results. """ + # Allow changing the URL for debug purposes + try: + url = os.environ['KICOST_NEXAR_URL'] + except KeyError: + url = QUERY_URL + query_type = query_type.replace('@COUNTRY@', api_nexar.country) + query_type = query_type.replace('@CUR@', currency) + variables = {"queries": query_parts} + # Remove all spaces, even inside the manf# + # SET comment: this is how the code always worked. Octopart (used by KitSpace) ignores spaces inside manf# codes. + # Do the query using POST + data = 'query={}&variables={}'.format(quote_plus(query_type), quote_plus(str(variables))) + distributor_class.log_request(url, data) + data = OrderedDict() + data["query"] = query_type + data["variables"] = variables + response = requests.post(url, json=data, headers=api_nexar.get_headers()) + response.encoding = 'UTF-8' + distributor_class.log_response(response) + if response.status_code == requests.codes['ok']: # 200 + results = json.loads(response.text) + return results + elif response.status_code == requests.codes['not_found']: # 404 + raise KiCostError('Nexar server not found check your internet connection.', ERR_SCRAPE) + elif response.status_code == requests.codes['request_timeout']: # 408 + raise KiCostError('Nexar is not responding.', ERR_SCRAPE) + elif response.status_code == requests.codes['bad_request']: # 400 + raise KiCostError('Bad request to Nexar server probably due to an incorrect string ' + 'format check your `manf#` codes and contact the suport team.', ERR_SCRAPE) + elif response.status_code == requests.codes['gateway_timeout']: # 504 + raise KiCostError('One of the internal Nexar services may experiencing problems.', ERR_SCRAPE) + else: + raise KiCostError('Nexar error: ' + str(response.status_code), ERR_SCRAPE)
    + +
    [docs] @staticmethod + def get_spec(data, item, default=None): + '''Get the value of `value` field of a dictionary if the `name` field identifier. + Used to get information from the JSON response.''' + for d in data['specs']: + if d['attribute']['shortname'] == item: + return d.get('displayValue', default) + return default
    + +
    [docs] @staticmethod + def query2name(q): + ''' Finds the prefix and name for a query ''' + if q.mpn is not None: + prefix = 'mpn' + name = (q.manf if q.manf else 'unk') + '_' + q.mpn + elif q.sku is not None: + prefix = 'sku' + name = (q.seller if q.seller else 'unk') + '_' + q.sku + name = name.replace(' ', '_') + return prefix, name
    + +
    [docs] @staticmethod + def get_part_info(queries, currency, to_retry): + '''Query PartInfo for quantity/price info. + `distributors` is the list of all distributors we want, in general. ''' + only_query = [q.query for q in queries] + results = api_nexar.query(only_query, currency) + for i, r in enumerate(results['data']['supMultiMatch']): + q = queries[i] + q.result = r + assert r['reference'] == q.id, 'Out of order results, please report' + # Solve the prefix and name + prefix, name = api_nexar.query2name(q) + api_nexar.cache.save_results(prefix, name, r) + if not r.get('hits') and q.mpn is not None and q.manf: + # Found, but has no hits + # Try without specifying a manufacturer + q.remove_manfucturer() + to_retry.append(q)
    + +
    [docs] @staticmethod + def fill_extra_info(result, specs, extra_info): + # We fill only missing information, the mandatory info comes from the original distributor + if 'desc' not in extra_info: + desc = result.get('shortDescription') + if desc: + extra_info['desc'] = desc + if 'manf' not in extra_info: + manf = result.get('manufacturer', {}).get('name') + if manf: + extra_info['manf'] = manf + if 'value' not in extra_info: + value = '' + for spec in ('capacitance', 'resistance', 'inductance'): + val = specs.get(spec, None) + if val: + value += val[1] + ' ' + if value: + extra_info['value'] = value + if 'size' not in extra_info: + size = '' + le = specs.get('length') + if le: + size += 'L: '+le[1]+' ' + w = specs.get('width') + if w: + size += 'W: '+w[1]+' ' + h = specs.get('height') + if h: + size += 'H: '+h[1] + if size: + extra_info['size'] = size + for spec, name in SPEC_NAMES.items(): + if name in extra_info: + continue + val = specs.get(spec, None) + if val: + extra_info[name] = val[1]
    + +
    [docs] @staticmethod + def select_best_part(result, part, native_dists): + """ Select the best part, we discard results that are not distributed by our distributors. + Then we look for the one that best matches the `manf`. """ + hits = result['hits'] + debug_obsessive('Hits: {}'.format(hits)) + # Each result can match one or more components from different manufacturers + # Take only the items with useful offers + useful_items = [] + for item in result['parts']: + for offer in item['sellers']: + if offer['company']['name'] in native_dists: + useful_items.append(item) + break + # debug_obsessive(str(result)) + # debug_obsessive(str(useful_items)) + # If more than one select the best + no_offers = False + if len(useful_items) > 1: + # List of possible manufacturers + manufacturers = {} + for it in useful_items: + manf = it['manufacturer']['name'].lower() + if manf not in manufacturers: + # We store the only the first one + # Nexar provides the best entry first, and sometimes an optional entry + # Example: Vishay CRCW06030000Z0EA vs Vishay CRCW0603-0000Z0EA + manufacturers[manf] = it + # Is the manf included? + manf = part.fields.get('manf', 'none').lower() + item = manufacturers.get(manf) + if not item: + if manf == 'none': + item = useful_items[0] + else: + best_match = difflib.get_close_matches(manf, manufacturers.keys())[0] + item = manufacturers[best_match] + mpn = item['mpn'] + warning(W_AMBIPN, 'Using "{}" for manf#="{}"'.format(item['manufacturer']['name'], mpn)) + warning(W_AMBIPN, 'Ambiguous manf#="{}" please use manf to select the right one, choices: {}'.format( + mpn, list(manufacturers.keys()))) + else: + if len(useful_items): + item = useful_items[0] + else: + item = result['parts'][0] + no_offers = True + return item, no_offers
    + +
    [docs] @staticmethod + def fill_part_data(part, result): + ''' Fill generic data in the PartGroup() structure ''' + # Get the information of the part. + # Datasheet, only if we don't have one + if not part.datasheet: + best_datasheet = result.get('bestDatasheet') + if best_datasheet: + part.datasheet = best_datasheet.get('url') + # Life cycle + lifecycle = api_nexar.get_spec(result, 'lifecyclestatus', '') + if not lifecycle: + lifecycle = api_nexar.get_spec(result, 'manufacturerlifecyclestatus', '') + if lifecycle: + # End Of Live -> obsolete + lifecycle = lifecycle.replace('EOL ', 'obsolete ') + part.lifecycle = lifecycle + # Misc data collected, currently not used inside KiCost + part.update_specs({sp['attribute']['shortname']: (sp['attribute']['name'], sp['displayValue']) + for sp in result['specs'] if sp['displayValue']})
    + +
    [docs] @staticmethod + def fill_part_info(queries, distributors, currency, solved): + ''' Place the results into the parts list. ''' + # Translate from PartInfo distributor names to the names used internally by kicost. + dist_xlate = api_nexar.DIST_TRANSLATION + + # Loop through the response to the query and enter info into the parts list. + for q in queries: + # Unpack the structure + part_query = q.query + part = q.part + dist_want = q.for_dists + result = q.result + # Process it + if not result or not result.get('hits'): + warning(W_NOINFO, 'No information found for parts \'{}\' query `{}`'.format(part.refs, str(part_query))) + continue + # Select the best hit + native_dists = set((api_nexar.KICOST2NEXAR_DIST[d] for d in dist_want)) + result, no_offers = api_nexar.select_best_part(result, part, native_dists) + # Note that it could have no useful offers, in this case we extract some info and then skip it + api_nexar.fill_part_data(part, result) + if no_offers: + continue + # Loop through the offers from various dists for this particular part. + for offer in result['sellers']: + # Get the distributor who made the offer and add their + # price/qty info to the parts list if its one of the accepted distributors. + dist = dist_xlate.get(offer['company']['name'], '') + if dist not in dist_want: + # Not interested in this distributor + # debug_obsessive('Discard offer: {}'.format(offer['company']['name'])) + continue + # Get the DistData for this distributor + dd = part.dd.get(dist, DistData()) + # Extra information + api_nexar.fill_extra_info(result, part.specs, dd.extra_info) + # Each distributor (seller) can have one or more offers + for of in offer['offers']: + # Get pricing information from this distributor. + # The offer could contain more than one currency, so we separate the prices by currency. + dist_currency = {} + for pr in of['prices']: + cur = pr['currency'] + ne = (pr['quantity'], pr['price']) + if cur in dist_currency: + dist_currency[cur].append(ne) + else: + dist_currency[cur] = [ne] + if not dist_currency: + # Some times the API returns minimum purchase 0 and a not valid `price_tiers`. + warning(NO_PRICE, 'No price information found for parts \'{}\' query `{}`'.format(part.refs, str(part_query))) + else: + prices = None + # Get the price tiers prioritizing: + # 1) The asked currency by KiCost user; + # 2) The default currency given by `DEFAULT_CURRENCY` in root `global_vars.py`; + # 3) The first not null tiers + if currency in dist_currency: + prices = dist_currency[currency] + dd.currency = currency + elif DEFAULT_CURRENCY in dist_currency: + prices = dist_currency[DEFAULT_CURRENCY] + dd.currency = DEFAULT_CURRENCY + else: + dd.currency, prices = next(iter(dist_currency.items())) + price_tiers = {qty: float(price) for qty, price in prices} + # Combine price lists for multiple offers from the same distributor + # to build a complete list of cut-tape and reeled components. + dd.price_tiers.update(price_tiers) + part_qty_increment = of.get('orderMultiple', 1) + # Select the part SKU, web page, and available quantity. + # Each distributor can have different stock codes for the same part in different + # quantities / delivery package styles: cut-tape, reel, ... + # Therefore we select and overwrite a previous selection if one of the + # following conditions is met: + # 1. We don't have a selection for this part from this distributor yet. + # 2. The MOQ is smaller than for the current selection. + # 3. The part_qty_increment for this offer smaller than that of the existing selection. + # (we prefer cut-tape style packaging over reels) + # 4. For DigiKey, we can't use part_qty_increment to distinguish between + # reel and cut-tape, so we need to look at the actual DigiKey part number. + # This procedure is made by the definition `distributors_info[dist]['ignore_cat#_re']` + # at the distributor profile. + dist_part_num = of.get('sku', '') + qty_avail = dd.qty_avail + in_stock_quantity = of.get('inventoryLevel') + if not qty_avail or (in_stock_quantity and qty_avail < in_stock_quantity): + # Keeps the information of more availability. + dd.qty_avail = in_stock_quantity # In stock. + ign_stock_code = distributor_class.get_distributor_info(dist).ignore_cat + valid_part = not (ign_stock_code and re.match(ign_stock_code, dist_part_num)) + # debug('dd.part_num') # Uncomment to debug + # debug('dd.qty_increment') # Uncomment to debug + moq = of.get('moq') + if (valid_part and + (not dd.part_num or + (dd.qty_increment is None or part_qty_increment < dd.qty_increment) or + (not dd.moq or (moq and dd.moq > moq)))): + # Save the link, stock code, ... of the page for minimum purchase. + dd.moq = moq # Minimum order qty. + dd.url = of.get('clickUrl', '') # Page to purchase the minimum quantity. + dd.part_num = dist_part_num + dd.qty_increment = part_qty_increment + # Update the DistData for this distributor + part.dd[dist] = dd + # We have data for this distributor + solved.add(dist)
    + +
    [docs] @staticmethod + def look_query_in_cache(query, unsolved): + # Solve the prefix and name + prefix, name = api_nexar.query2name(query) + # Look in the cache + query.result, query.loaded = api_nexar.cache.load_results(prefix, name) + if not query.loaded: + unsolved.append(query) + else: + debug_obsessive('Data from cache: '+pprint.pformat(query.result))
    + +
    [docs] @staticmethod + def solve_queries(unsolved, n_unsolved, currency): + to_retry = [] + # Setup progress bar to track progress of server queries. + progress = distributor_class.progress(n_unsolved, distributor_class.logger) + + # Slice the queries into batches of the largest allowed size and gather + # the part data for each batch. + for i in range(0, n_unsolved, MAX_PARTS_PER_QUERY): + slc = slice(i, i+MAX_PARTS_PER_QUERY) + api_nexar.get_part_info(unsolved[slc], currency, to_retry) + progress.update(len(unsolved[slc])) + + # Done with the scraping progress bar so delete it or else we get an + # error when the program terminates. + progress.close() + return to_retry, len(to_retry)
    + +
    [docs] @staticmethod + def query_part_info(parts, distributors, currency): + '''Fill-in the parts with price/qty/etc info from Nexar.''' + debug_overview('# Getting part data from Nexar ...') + + # Use just the distributors avaliable in this API. + # Note: The user can use --exclude and define it with fields. + distributors = [d for d in distributors if distributor_class.get_distributor_info(d).is_web() + and d in api_nexar.api_distributors] + FIELDS_CAT = sorted([d + '#' for d in distributors]) + + # Create queries to get part price/quantities from PartInfo. + queries = [] + # Translate from PartInfo distributor names to the names used internally by kicost. + available_distributors = set(api_nexar.api_distributors) + for part in parts: + # Create a PartInfo query using the manufacturer's part number or the distributor's SKU. + part_dist_use_manfpn = copy.copy(distributors) + + # Create queries using the distributor SKU + # Check if that part have stock code that is accepted to use by this module (API). + # KiCost will prioritize these codes under "manf#" that will be used for get + # information for the part hat were not filled with the distributor stock code. So + # this is checked after the 'manf#' buv code. + found_codes_for_all_dists = True + for d in FIELDS_CAT: + part_stock = part.fields.get(d) + if part_stock: + part_catalogue_code_dist = d[:-1] + if part_catalogue_code_dist in available_distributors: + part_code_dist = api_nexar.KICOST2NEXAR_DIST[part_catalogue_code_dist] + queries.append(QueryStruct(len(queries), part, [part_catalogue_code_dist], seller=part_code_dist, sku=part_stock)) + part_dist_use_manfpn.remove(part_catalogue_code_dist) + else: + found_codes_for_all_dists = False + + # Create a query using the manufacturer P/N + part_manf = part.fields.get('manf', '') + part_code = part.fields.get('manf#') + if part_code and not found_codes_for_all_dists: + # Not all distributors has code, add a query for the manufaturer P/N + # List of distributors without an specific part number + queries.append(QueryStruct(len(queries), part, part_dist_use_manfpn, manf=part_manf, mpn=part_code)) + + n_queries = len(queries) + debug_overview('Queries {}'.format(n_queries)) + if not n_queries: + return + + # Try to solve the queries from the cache + unsolved = [] + for query in queries: + api_nexar.look_query_in_cache(query, unsolved) + if query.loaded and not query.result.get('hits') and query.mpn is not None: + # Found, but has no hits + # Try without specifying a manufacturer + query.remove_manfucturer() + api_nexar.look_query_in_cache(query, unsolved) + + # Solve the rest from the site + n_unsolved = len(unsolved) + debug_overview('Cached entries {} (of {})'.format(n_queries-n_unsolved, n_queries)) + if n_unsolved: + unsolved, n_unsolved = api_nexar.solve_queries(unsolved, n_unsolved, currency) + # Do we need to retry some queries? + if n_unsolved: + unsolved, n_unsolved = api_nexar.solve_queries(unsolved, n_unsolved, currency) + + # Transfer the results + solved_dist = set() + api_nexar.fill_part_info(queries, distributors, currency, solved_dist) + return solved_dist
    + + +distributor_class.register(api_nexar, 75) +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/api_octopart.html b/docs/_build/html/_modules/kicost/distributors/api_octopart.html deleted file mode 100644 index 1a5b9af7..00000000 --- a/docs/_build/html/_modules/kicost/distributors/api_octopart.html +++ /dev/null @@ -1,574 +0,0 @@ - - - - - - - - kicost.distributors.api_octopart — kicost 1.1.13 documentation - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for kicost.distributors.api_octopart

    -# -*- coding: utf-8 -*-
    -
    -# Obsolete API:
    -# As of June 30, 2022 APIv3 and APIv4 access have been disabled.
    -# If you are looking for your APIv3 or APIv4 key, you no longer have access to it.
    -# In order to access supply chain data again, you must sign up for the Nexar API.
    -
    -# MIT license
    -#
    -# Copyright (C) 2018 by XESS Corporation / Max Maisel / Hildo Guillardi Júnior
    -#
    -# Permission is hereby granted, free of charge, to any person obtaining a copy
    -# of this software and associated documentation files (the "Software"), to deal
    -# in the Software without restriction, including without limitation the rights
    -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    -# copies of the Software, and to permit persons to whom the Software is
    -# furnished to do so, subject to the following conditions:
    -#
    -# The above copyright notice and this permission notice shall be included in
    -# all copies or substantial portions of the Software.
    -#
    -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    -# THE SOFTWARE.
    -
    -# Libraries.
    -import json
    -import requests
    -import re
    -import os
    -import sys
    -import difflib
    -from collections import Counter
    -if sys.version_info[0] < 3:
    -    from urllib import quote_plus
    -else:
    -    from urllib.parse import quote_plus
    -
    -# KiCost definitions.
    -from .. import KiCostError, DistData, ERR_SCRAPE, W_ASSQTY, W_AMBIPN, W_APIFAIL
    -# Distributors definitions.
    -from .distributor import distributor_class, QueryCache
    -from .log__ import debug_overview, debug_obsessive, warning
    -
    -# Author information.
    -__author__ = 'XESS Corporation'
    -__webpage__ = 'info@xess.com'
    -
    -OCTOPART_MAX_PARTBYQUERY = 20  # Maximum part list length to one single query.
    -
    -__all__ = ['api_octopart']
    -
    -
    -class QueryStruct(object):
    -    def __init__(self, id, kind, code, part):
    -        self.id = str(id)  # ID number for this query
    -        self.kind = kind   # mpn/sku
    -        self.code = code   # manf_code/sku
    -        self.part = part   # Pointer to the part.
    -        self.result = None
    -        self.loaded = False
    -
    -
    -
    [docs]class api_octopart(distributor_class): - name = 'Octopart' - type = 'api' - enabled = False - url = 'https://octopart.com/' # Web site API information. - # Options supported by this API - config_options = {'key': str, 'level': int, 'extended': bool} - api_level = 4 - # Include specs and datasheets. Only in the Pro plan. - extended = False - cache = None - - API_KEY = None - api_distributors = ['arrow', 'digikey', 'farnell', 'lcsc', 'mouser', 'newark', 'rs', 'tme'] - DIST_TRANSLATION = { # Distributor translation. Just a few supported. - 'Arrow Electronics': 'arrow', - 'Digi-Key': 'digikey', - 'Farnell': 'farnell', - 'Mouser': 'mouser', - 'Newark': 'newark', - 'RS Components': 'rs', - 'TME': 'tme', - 'LCSC': 'lcsc', - } - # Dict to translate KiCost field names into Octopart distributor names - KICOST2OCTOPART_DIST = {v: k for k, v in DIST_TRANSLATION.items()} - -
    [docs] @staticmethod - def configure(ops): - cache_ttl = 7 - cache_path = None - for k, v in ops.items(): - if k == 'key': - api_octopart.API_KEY = v - if 'enable' not in ops: - # If not explicitly disabled then enable it - api_octopart.enabled = True - elif k == 'enable': - api_octopart.enabled = v - elif k == 'extended': - api_octopart.extended = v - elif k == 'level': - api_octopart.api_level = v - elif k == 'cache_ttl': - cache_ttl = v - elif k == 'cache_path': - cache_path = v - api_octopart.cache = QueryCache(cache_path, cache_ttl) - if api_octopart.enabled and api_octopart.API_KEY is None: - warning(W_APIFAIL, "Can't enable Octopart without a `key`") - api_octopart.enabled = False - debug_obsessive('Octopart API configured to enabled {} key {} level {} extended {}'. - format(api_octopart.enabled, api_octopart.API_KEY, api_octopart.api_level, api_octopart.extended))
    - -
    [docs] @staticmethod - def query(query): - """Send query to Octopart and return results.""" - # url = 'http://octopart.com/api/v3/parts/match' - # payload = {'queries': json.dumps(query), 'include\[\]': 'specs', 'apikey': token} - # response = requests.get(url, params=payload) - data = 'queries=['+', '.join(query)+']' - if api_octopart.API_KEY: - if api_octopart.api_level == 3: - url = 'http://octopart.com/api/v3/parts/match' - else: - url = 'http://octopart.com/api/v4/rest/parts/match' - data += '&apikey=' + api_octopart.API_KEY - else: # Not working 2021/04/28: - url = 'https://temp-octopart-proxy.kitspace.org/parts/match' - # Allow changing the URL for debug purposes - try: - url = os.environ['KICOST_OCTOPART_URL'] - except KeyError: - pass - if api_octopart.extended: - data += '&include[]=specs' - data += '&include[]=datasheets' - distributor_class.log_request(url, data) - response = requests.get(url + '?' + data) - distributor_class.log_response(response) - if response.status_code == 200: # Ok - results = json.loads(response.text).get('results') - return results - elif response.status_code == 400: # Bad request - raise KiCostError('Octopart missing apikey.', ERR_SCRAPE) - elif response.status_code == 404: # Not found - raise KiCostError('Octopart server not found.', ERR_SCRAPE) - elif response.status_code == 403 or 'Invalid API key' in response.text: - raise KiCostError('Octopart KEY invalid, register one at "https://www.octopart.com".', ERR_SCRAPE) - elif response.status_code == 429: # Too many requests - raise KiCostError('Octopart request limit reached.', ERR_SCRAPE) - else: - raise KiCostError('Octopart error: ' + str(response.status_code), ERR_SCRAPE)
    - -
    [docs] @staticmethod - def sku_to_mpn(sku): - """Find manufacturer part number associated with a distributor SKU.""" - part_query = ['{"reference": "1", "sku": "'+quote_plus(sku)+'"}'] - results = api_octopart.query(part_query) - if not results: - return None - result = results[0] - mpns = [item['mpn'] for item in result['items']] - if not mpns: - return None - if len(mpns) == 1: - return mpns[0] - mpn_cnts = Counter(mpns) - return mpn_cnts.most_common(1)[0][0] # Return the most common MPN.
    - -
    [docs] @staticmethod - def skus_to_mpns(parts, distributors): - """Find manufaturer's part number for all parts with just distributor SKUs.""" - for i, part in enumerate(parts): - - # Skip parts that already have a manufacturer's part number. - if part.fields.get('manf#'): - continue - - # Get all the SKUs for this part. - skus = list( - set([part.fields.get(dist + '#', '') for dist in distributors])) - skus = [sku for sku in skus - if sku not in ('', None)] # Remove null SKUs. - - # Skip this part if there are no SKUs. - if not skus: - continue - - # Convert the SKUs to manf. part numbers. - mpns = [api_octopart.sku_to_mpn(sku) for sku in skus] - mpns = [mpn for mpn in mpns - if mpn not in ('', None)] # Remove null manf#. - - # Skip assigning manf. part number to this part if there aren't any to assign. - if not mpns: - continue - - # Assign the most common manf. part number to this part. - mpn_cnts = Counter(mpns) - part.fields['manf#'] = mpn_cnts.most_common(1)[0][0]
    - -
    [docs] @staticmethod - def get_part_info(queries): - """Query Octopart for quantity/price info.""" - only_query = ['{"reference": "'+q.id+'", "'+q.kind+'": "'+quote_plus(q.code)+'"}' for q in queries] - results = api_octopart.query(only_query) - # Copy the results to the queries - q_hash = {q.id: q for q in queries} - for r in results: - query = q_hash[r['reference']] - query.result = r - api_octopart.cache.save_results(query.kind, query.code, r)
    - -
    [docs] @staticmethod - def fill_part_info(queries, distributors, solved, currency='USD'): - ''' Place the results into the parts list. ''' - # Translate from Octopart distributor names to the names used internally by kicost. - dist_xlate = api_octopart.DIST_TRANSLATION - # List of desired distributors in native format - native_dists = set((api_octopart.KICOST2OCTOPART_DIST[d] for d in distributors)) - # Currency priority: 1: User specified, 2: USD, 3: EUR - currency_prio = [currency] - if currency != 'USD': - currency_prio.append('USD') - if currency != 'EUR': - currency_prio.append('EUR') - # Loop through the response to the query and enter info into the parts list. - for query in queries: - result = query.result - part = query.part - # Each result can match one or more components from different manufacturers - # Take only the items with useful offers - useful_items = [] - for item in result['items']: - for offer in item['offers']: - if offer['seller']['name'] in native_dists: - useful_items.append(item) - break - # debug_obsessive(str(result)) - # debug_obsessive(str(useful_items)) - # If more than one select the right one - if len(useful_items) > 1: - # List of possible manufacturers - # TODO: Can we get more than one hit for the same manf? - manufacturers = {it['manufacturer']['name'].lower(): it for it in useful_items} - # Is the manf included? - manf = part.fields.get('manf', 'none').lower() - item = manufacturers.get(manf) - if not item: - if manf == 'none': - item = useful_items[0] - else: - best_match = difflib.get_close_matches(manf, manufacturers.keys())[0] - item = manufacturers[best_match] - mpn = item['mpn'] - warning(W_AMBIPN, 'Using "{}" for manf#="{}"'.format(item['manufacturer']['name'], mpn)) - warning(W_AMBIPN, 'Ambiguous manf#="{}" please use manf to select the right one, choices: {}'.format( - mpn, list(manufacturers.keys()))) - else: - if len(useful_items): - item = useful_items[0] - else: - # No hits, skip - continue - if api_octopart.extended: - # Assign the lifecycle status 'obsolete' (others possible: 'active' and 'not recommended for new designs') but not used. - try: - # API v4 (production, eol, nrnd, ...) we take the first word - part.lifecycle = item['specs']['lifecyclestatus']['value'][0].lower().split(' ')[0] - except KeyError: - try: - # API v3 - part.lifecycle = item['specs']['lifecycle_status']['value'][0].lower() - except KeyError: - # No lifecyclestatus (current name) nor lifecycle_status (old name) - pass - # Take the datasheet provided by the distributor. This will by used - # in the output spreadsheet if not provide any in the BOM/schematic. - # This will be signed in the file. - try: - part.datasheet = item['datasheets'][0]['url'] - except (KeyError, IndexError): - # No datasheet key (KeyError) or empty (IndexError) - pass - # Misc data collected, currently not used inside KiCost - part.update_specs({code: (info['metadata']['name'], ', '.join(info['value'])) for code, info in item['specs'].items()}) - # Loop through the offers from various dists for this particular part. - for offer in item['offers']: - # Get the distributor who made the offer and add their - # price/qty info to the parts list if its one of the accepted distributors. - dist = dist_xlate.get(offer['seller']['name'], '') - if dist not in distributors: - # Unknown or excluded seller - continue - price_tiers = {} - part_qty_increment = float("inf") - # Get the DistData for this distributor - dd = part.dd.get(dist, DistData()) - # Get pricing information from this distributor. - prices = offer['prices'] - if prices: - for curr in currency_prio: - if curr in prices: - dd.currency = curr - price_l = prices[curr] - break - else: - # Use the first entry - dd.currency, price_l = next(iter(prices.items())) - price_tiers = {qty: float(price) for qty, price in price_l} - # Combine price lists for multiple offers from the same distributor - # to build a complete list of cut-tape and reeled components. - dd.price_tiers.update(price_tiers) - # Compute the quantity increment between the lowest two prices. - # This will be used to distinguish the cut-tape from the reeled components. - try: - part_break_qtys = sorted(price_tiers.keys()) - part_qty_increment = part_break_qtys[1] - part_break_qtys[0] - except IndexError: - # This will happen if there are not enough entries in the price/qty list. - # As a stop-gap measure, just assign infinity to the part increment. - # A better alternative may be to examine the packaging field of the offer. - pass - # Select the part SKU, web page, and available quantity. - # Each distributor can have different stock codes for the same part in different - # quantities / delivery package styles: cut-tape, reel, ... - # Therefore we select and overwrite a previous selection if one of the - # following conditions is met: - # 1. We don't have a selection for this part from this distributor yet. - # 2. The MOQ is smaller than for the current selection. - # 3. The part_qty_increment for this offer smaller than that of the existing selection. - # (we prefer cut-tape style packaging over reels) - # 4. For DigiKey, we can't use part_qty_increment to distinguish between - # reel and cut-tape, so we need to look at the actual DigiKey part number. - # This procedure is made by the definition `distributors_info[dist]['ignore_cat#_re']` - # at the distributor profile. - if not dd.part_num: - qty_avail = dd.qty_avail - in_stock_quantity = offer.get('in_stock_quantity') - if not qty_avail or (in_stock_quantity and qty_avail < in_stock_quantity): - # Keep the information with more availability. - dd.qty_avail = in_stock_quantity - moq = dd.moq - moq_offer = offer.get('moq') - if not moq or (moq_offer and moq > moq_offer): - # Save the link, stock code, ... of the page for minimum purchase. - dd.moq = moq_offer # Minimum order qty. - dd.part_num = offer.get('sku') - dd.url = offer.get('product_url') - dd.qty_increment = part_qty_increment - # Otherwise, check qty increment and see if its the smallest for this part & dist. - elif part_qty_increment < dd.qty_increment: - # This part looks more like a cut-tape version, so - # update the SKU, web page, and available quantity. - qty_avail = dd.qty_avail - in_stock_quantity = offer.get('in_stock_quantity') - if not qty_avail or (in_stock_quantity and qty_avail < in_stock_quantity): - # Keep the information with more availability. - dd.qty_avail = in_stock_quantity - # Check for a valid SKU - dist_part_num = offer.get('sku', '') - ign_stock_code = distributor_class.get_distributor_info(dist).ignore_cat - valid_part = not (ign_stock_code and re.match(ign_stock_code, dist_part_num)) - moq_offer = offer.get('moq') - if (valid_part and (not dd.part_num or (part_qty_increment < dd.qty_increment) or - (not dd.moq or (moq_offer and dd.moq > moq_offer)))): - # Save the link, stock code, ... of the page for minimum purchase. - dd.moq = moq_offer # Minimum order qty. - dd.part_num = dist_part_num - dd.url = offer.get('product_url') - dd.qty_increment = part_qty_increment - # Update the DistData for this distributor - part.dd[dist] = dd - # We have data for this distributor - solved.add(dist)
    - -
    [docs] @staticmethod - def query_part_info(parts, distributors, currency): - """Fill-in the parts with price/qty/etc info from Octopart.""" - debug_overview('# Getting part data from Octopart...') - - # Get the valid distributors names used by them part catalog - # that may be index by Octopart. This is used to remove the - # local distributors and future not implemented in the Octopart - # definition. - # Note: The user can use --exclude and define it with fields. - distributors_octopart = [d for d in distributors if distributor_class.get_distributor_info(d).is_web() - and d in api_octopart.api_distributors] - - # Collect all the queries - queries = [] - for i, part in enumerate(parts): - # Create an Octopart query using the manufacturer's part number or distributor SKU. - manf_code = part.fields.get('manf#') - if manf_code: - query = QueryStruct(i, "mpn", manf_code, part) - else: - # No MPN, so use the first distributor SKU that's found. - for octopart_dist_sku in distributors_octopart: - sku = part.fields.get(octopart_dist_sku + '#', '') - if sku: - break - if not sku: - # No MPN or SKU, so skip this part. - continue - # Create the part query using SKU matching. - query = QueryStruct(i, "sku", sku, part) - - # Because was used the distributor (enrolled at Octopart list) - # despite the normal 'manf#' code, take the sub quantity as - # general sub quantity of the current part. - try: - part.fields['manf#_qty'] = part.fields[octopart_dist_sku + '#_qty'] - warning(W_ASSQTY, "Associated {q} quantity to '{r}' due \"{f}#={q}:{c}\".".format( - q=part.fields[octopart_dist_sku + '#_qty'], r=part.refs, - f=octopart_dist_sku, c=part.fields[octopart_dist_sku+'#'])) - except KeyError: - pass - # Add query for this part to the list of part queries. - queries.append(query) - - n_queries = len(queries) - debug_overview('Queries {}'.format(n_queries)) - if not n_queries: - return - - # Try to solve the queries from the cache - unsolved = [] - for query in queries: - # Look in the cache - query.result, query.loaded = api_octopart.cache.load_results(query.kind, query.code) - if not query.loaded: - unsolved.append(query) - - # Solve the rest from the site - n_unsolved = len(unsolved) - debug_overview('Cached entries {}'.format(n_queries-n_unsolved)) - if n_unsolved: - # Setup progress bar to track progress of server queries. - progress = distributor_class.progress(n_unsolved, distributor_class.logger) - - # Slice the queries into batches of the largest allowed size and gather - # the part data for each batch. - for i in range(0, n_unsolved, OCTOPART_MAX_PARTBYQUERY): - slc = slice(i, i+OCTOPART_MAX_PARTBYQUERY) - api_octopart.get_part_info(unsolved[slc]) - progress.update(len(unsolved[slc])) - - # Done with the scraping progress bar so delete it or else we get an - # error when the program terminates. - progress.close() - - # Transfer the results - solved_dist = set() - api_octopart.fill_part_info(queries, distributors_octopart, solved_dist, currency) - return solved_dist
    - -
    [docs] @staticmethod - def from_environment(options, overwrite): - ''' Configuration from the environment. ''' - # Configure the module from the environment - # The command line will overwrite it using set_options() - key = os.getenv('KICOST_OCTOPART_KEY_V3') - if key: - api_octopart._set_from_env('key', key, options, overwrite) - api_octopart._set_from_env('enable', True, options, overwrite) - api_octopart._set_from_env('level', 3, options, overwrite) - else: - key = os.getenv('KICOST_OCTOPART_KEY_V4') - if key: - api_octopart._set_from_env('key', key, options, overwrite) - api_octopart._set_from_env('enable', True, options, overwrite) - api_octopart._set_from_env('level', 4, options, overwrite) - elif os.environ.get('KICOST_OCTOPART'): - # Currently this isn't useful, you can't do anything without a key. - # This is just in case we get a proxy running. - api_octopart._set_from_env('enable', True, options, overwrite) - if os.environ.get('KICOST_OCTOPART_EXTENDED'): - api_octopart._set_from_env('extended', True, options, overwrite)
    - - -distributor_class.register(api_octopart, 60) -
    - -
    -
    -
    -
    - -
    -
    - - - - \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/api_partinfo_kitspace.html b/docs/_build/html/_modules/kicost/distributors/api_partinfo_kitspace.html index a1b69789..6bd1db25 100644 --- a/docs/_build/html/_modules/kicost/distributors/api_partinfo_kitspace.html +++ b/docs/_build/html/_modules/kicost/distributors/api_partinfo_kitspace.html @@ -5,7 +5,7 @@ - kicost.distributors.api_partinfo_kitspace — kicost 1.1.13 documentation + kicost.distributors.api_partinfo_kitspace — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -154,6 +154,7 @@

    Source code for kicost.distributors.api_partinfo_kitspace

    # Dict to translate KiCost field names into KitSpace distributor names KICOST2KITSPACE_DIST = {v: k for k, v in DIST_TRANSLATION.items()} cache = None + env_prefix = 'KITSPACE'
    [docs] @staticmethod def configure(ops): @@ -469,7 +470,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/distributors/api_tme.html b/docs/_build/html/_modules/kicost/distributors/api_tme.html new file mode 100644 index 00000000..a6c1ef4a --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/api_tme.html @@ -0,0 +1,560 @@ + + + + + + + + kicost.distributors.api_tme — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.api_tme

    +# -*- coding: utf-8 -*-
    +# MIT license
    +#
    +# Copyright (C) 2022 by Salvador E. Tropea / Instituto Nacional de Tecnologia Industrial
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to deal
    +# in the Software without restriction, including without limitation the rights
    +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +# copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +# THE SOFTWARE.
    +#
    +# I took the basic query from https://github.com/krzych82/api-client-python3.git
    +
    +# Author information.
    +__author__ = 'Salvador Eduardo Tropea'
    +__webpage__ = 'https://github.com/set-soft'
    +__company__ = 'Instituto Nacional de Tecnologia Industrial - Argentina'
    +
    +# Libraries.
    +import sys
    +import pprint
    +import difflib
    +import collections
    +import json
    +import base64
    +import hmac
    +import hashlib
    +if sys.version_info[0] < 3:
    +    from urllib import quote, urlencode
    +    from urllib2 import Request, urlopen, URLError
    +else:
    +    from urllib.parse import quote, urlencode
    +    from urllib.request import Request, urlopen
    +    from urllib.error import URLError
    +
    +# KiCost definitions.
    +from .. import KiCostError, DistData, W_NOINFO, ERR_SCRAPE, W_APIFAIL
    +# Distributors definitions.
    +from .distributor import distributor_class, QueryCache
    +from .log__ import debug_overview, debug_obsessive, debug_full, is_debug_full, warning
    +
    +# Specs known by KiCost
    +SPEC_NAMES = {'tolerance': 'tolerance',
    +              'frequency': 'frequency',
    +              'power': 'power',
    +              'operating voltage': 'voltage',
    +              'manufacturer': 'manf',
    +              'temperature coefficient': 'temp_coeff'}
    +
    +DIST_NAME = 'tme'
    +BASE_URL = 'https://api.tme.eu'
    +MAX_PARTS_PER_QUERY = 10
    +
    +__all__ = ['api_tme']
    +
    +
    +def do_encode(string):
    +    if sys.version_info[0] < 3:
    +        return string
    +    return string.encode()
    +
    +
    +class TMEError(Exception):
    +    pass
    +
    +
    +class TME(object):
    +    def __init__(self, country, language, app_secret, token, cache, currency):
    +        # Adapt to what TME uses
    +        currency = currency.upper()
    +        country = country.upper()
    +        language = language.lower()
    +        # Store the options in the object
    +        self.currency = currency
    +        self.country = country
    +        self.language = language
    +        self.token = token
    +        self.app_secret = do_encode(app_secret)
    +        self.cache = cache
    +        # Check the language
    +        lng = self.get_languages()
    +        if language not in lng:
    +            self.language = 'EN'
    +            debug_overview('Language `{}` not supported using `EN`'.format(language))
    +        debug_overview('Using `{}`'.format(self.language))
    +        # Check the selected country
    +        cnt = self.get_countries()
    +        country_l = country.lower()
    +        cnt_data = next(iter(filter(lambda x: x['CountryId'].lower() == country_l, cnt)), None)
    +        if cnt_data is None:
    +            raise TMEError("Unsupported country `{}`".format(country))
    +        # Check if the currency is supported for this country
    +        if currency not in cnt_data['CurrencyList']:
    +            # Nope, use the default for this country
    +            self.currency = cnt_data['Currency']
    +            debug_overview('Currency `{}` not supported for `{}` using `{}`'.format(currency, country, self.currency))
    +        debug_overview('Using `{}` for `{}`'.format(self.currency, cnt_data['Name']))
    +
    +    def _get_signature_base(self, url, params):
    +        params = collections.OrderedDict(sorted(params.items()))
    +        encoded_params = urlencode(params)
    +        signature_base = 'POST'+'&'+quote(url, '')+'&'+quote(encoded_params, '')
    +        return do_encode(signature_base)
    +
    +    def _calculate_signature(self, url, params):
    +        hmac_value = hmac.new(self.app_secret, self._get_signature_base(url, params), hashlib.sha1).digest()
    +        return base64.encodebytes(hmac_value).rstrip()
    +
    +    def request(self, endpoint, params, format='json'):
    +        url = BASE_URL+endpoint+'.'+format
    +        params['Token'] = self.token
    +        params['ApiSignature'] = self._calculate_signature(url, params)
    +        data = do_encode(urlencode(params))
    +        headers = {"Content-type": "application/x-www-form-urlencoded"}
    +        return Request(url, data, headers)
    +
    +    def json_request(self, endpoint, params={}):
    +        reason = code = None
    +        debug_obsessive('TME json request endpoint {} params {}'.format(endpoint, params))
    +        try:
    +            response = urlopen(self.request(endpoint, params))
    +        except URLError as e:
    +            reason = e.reason
    +            code = e.code
    +        if reason:
    +            raise TMEError("Server error `{}` ({})".format(reason, code))
    +        data = json.loads(response.read())
    +        return data['Data']
    +
    +    def get_countries(self):
    +        language = self.language
    +        data, loaded = self.cache.load_results('all', 'countries_'+language)
    +        if loaded:
    +            debug_obsessive('Data from cache: '+pprint.pformat(data))
    +            return data['CountryList']
    +        data = self.json_request('/Utils/GetCountries', {'Language': language})
    +        debug_obsessive('Data from web: '+pprint.pformat(data))
    +        self.cache.save_results('all', 'countries_'+language, data)
    +        return data['CountryList']
    +
    +    def get_languages(self):
    +        data, loaded = self.cache.load_results('all', 'languages')
    +        if loaded:
    +            debug_obsessive('Data from cache: '+pprint.pformat(data))
    +            return data['LanguageList']
    +        data = self.json_request('/Utils/GetLanguages')
    +        debug_obsessive('Data from web: '+pprint.pformat(data))
    +        self.cache.save_results('all', 'languages', data)
    +        return data['LanguageList']
    +
    +    def get_from_cache(self, symbol):
    +        full_name = symbol+'_'+self.country+'_'+self.language+'_'+self.currency
    +        data, loaded = self.cache.load_results('data', full_name)
    +        if loaded:
    +            debug_obsessive('Data from cache: '+pprint.pformat(data))
    +            return data
    +        return None
    +
    +    def _get_products(self, symbols, kind, currency=False):
    +        parameters = {'Country': self.country,
    +                      'Language': self.language}
    +        if currency:
    +            parameters['Currency'] = self.currency
    +        for c, s in enumerate(symbols):
    +            parameters['SymbolList[{}]'.format(c)] = s
    +        debug_obsessive(parameters)
    +        data = self.json_request('/Products/'+kind, parameters)
    +        debug_obsessive('Data from web: '+pprint.pformat(data))
    +        return data['ProductList']
    +
    +    def get_products(self, symbols):
    +        return self._get_products(symbols, 'GetProducts')
    +
    +    def get_prices(self, symbols):
    +        return self._get_products(symbols, 'GetPricesAndStocks', currency=True)
    +
    +    def get_parameters(self, symbols):
    +        return self._get_products(symbols, 'GetParameters')
    +
    +    def get_files(self, symbols):
    +        return self._get_products(symbols, 'GetProductsFiles')
    +
    +    def get_part_info(self, missing, known):
    +        products = self.get_products(missing)
    +        prices = self.get_prices(missing)
    +        parameters = self.get_parameters(missing)
    +        files = self.get_files(missing)
    +        # Join all the data
    +        all_data = {s: {} for s in missing}
    +        for d in products+prices+parameters+files:
    +            all_data[d['Symbol']].update(d)
    +        # Cache the data and it to the known
    +        qual_name = '_'+self.country+'_'+self.language+'_'+self.currency
    +        for k, v in all_data.items():
    +            self.cache.save_results('data', k+qual_name, v)
    +            known[k] = v
    +
    +    def search(self, name):
    +        # Try to get the data from the cache
    +        full_name = name+'_'+self.country+'_'+self.language+'_'+self.currency
    +        data, loaded = self.cache.load_results('search', full_name)
    +        if loaded:
    +            debug_obsessive('Data from cache: '+pprint.pformat(data))
    +            return data
    +        # The docs suggest that spaces are allowed, but I get 400 (Bad Request)
    +        name = name.replace(' ', '-')
    +        parameters = {'Country': self.country,
    +                      'Language': self.language,
    +                      'SearchPlain': name,
    +                      'SearchOrder': 'PRICE_FIRST_QUANTITY',
    +                      'SearchOrderType': 'ASC'}
    +        data = self.json_request('/Products/Search', parameters)
    +        debug_obsessive('Data from web: '+pprint.pformat(data))
    +        total = data['Amount']
    +        if total > 20:
    +            # Get the rest of the matches
    +            product_list = data['ProductList']
    +            page = 2
    +            while len(product_list) < total:
    +                parameters = {'Country': self.country,
    +                              'Language': self.language,
    +                              'SearchPlain': name,
    +                              'SearchOrder': 'PRICE_FIRST_QUANTITY',
    +                              'SearchOrderType': 'ASC',
    +                              'SearchPage': str(page)}
    +                data = self.json_request('/Products/Search', parameters)
    +                product_list.extend(data['ProductList'])
    +                page = page+1
    +        else:
    +            product_list = data['ProductList']
    +        self.cache.save_results('search', full_name, product_list)
    +        return product_list
    +
    +
    +
    [docs]class api_tme(distributor_class): + name = 'TME' + type = 'api' + # Currently enabled only by request + enabled = True + + url = 'https://developers.tme.eu/en/' # Web site API information. + api_distributors = [DIST_NAME] + # Options supported by this API + config_options = {'token': str, + 'app_secret': str, + 'country': str, + 'language': str} + token = None + app_secret = None + country = 'US' + language = 'EN' + env_prefix = 'TME' + +
    [docs] @staticmethod + def configure(ops): + cache_ttl = 7 + cache_path = None + for k, v in ops.items(): + if k == 'token': + api_tme.token = v + elif k == 'app_secret': + api_tme.app_secret = v + elif k == 'country': + api_tme.country = v + elif k == 'language': + api_tme.language = v + elif k == 'enable': + api_tme.enabled = v + elif k == 'cache_ttl': + cache_ttl = v + elif k == 'cache_path': + cache_path = v + if api_tme.enabled and (api_tme.token is None or api_tme.app_secret is None): + warning(W_APIFAIL, "Can't enable TME without a `token` and an `app_secret`") + api_tme.enabled = False + debug_obsessive('TME API configured to enabled {} token {} app_secret {} path {}'. + format(api_tme.enabled, api_tme.token, api_tme.app_secret, cache_path)) + if not api_tme.enabled: + return + # Configure the cache + api_tme.cache = QueryCache(cache_path, cache_ttl)
    + + @staticmethod + def _query_part_info(parts, distributors, currency): + '''Fill-in the parts with price/qty/etc info from KitSpace.''' + debug_overview('# Getting part data from TME ...') + field_cat = DIST_NAME + '#' + o = TME(api_tme.country, api_tme.language, api_tme.app_secret, api_tme.token, api_tme.cache, currency) + # + # First pass: collect the missing TME part numbers + # + # Setup progress bar to track progress of server queries. + progress = distributor_class.progress(len(parts), distributor_class.logger) + symbols = collections.OrderedDict() + missing_symbols = [] + for part in parts: + # Get the TME P/N for this part + part_stock = part.fields.get(field_cat) + if not part_stock: + # We can't get information without a valid TME code (symbol) + part_manf = part.fields.get('manf', '') + part_code = part.fields.get('manf#') + if part_code: + debug_obsessive('Searching P/N: {} from {}'.format(part_code, part_manf)) + candidates = o.search(part_code) + debug_obsessive('Found {} matches'.format(len(candidates))) + part_stock = part.fields[field_cat] = _select_best(candidates, part_manf, part.qty_total_spreadsheet) + if part_stock: + # Add this symbol to the list of needed + data = o.get_from_cache(part_stock) + symbols[part_stock] = data + if data is None: + missing_symbols.append(part_stock) + progress.update(1) + progress.close() + # + # Second pass: collect the missing data + # + if missing_symbols: + n_unsolved = len(missing_symbols) + progress = distributor_class.progress(n_unsolved, distributor_class.logger) + for i in range(0, n_unsolved, MAX_PARTS_PER_QUERY): + slc = slice(i, i+MAX_PARTS_PER_QUERY) + o.get_part_info(missing_symbols[slc], symbols) + progress.update(len(missing_symbols[slc])) + progress.close() + # + # Fill the part information + # + for part in parts: + part_stock = part.fields.get(field_cat) + if not part_stock: + warning(W_NOINFO, 'No information found at TME for part/s \'{}\''.format(part.refs)) + continue + data = symbols[part_stock] + debug_obsessive('* Part info before adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + debug_obsessive('* Data found:') + debug_obsessive(pprint.pformat(data)) + if part.datasheet is None: + ds = next(iter(filter(lambda x: x['DocumentType'] in ['INS', 'DTE'], data['Files']['DocumentList'])), None) + if ds: + part.datasheet = ds['DocumentUrl'] + specs = {} + for sp in data.get('ParameterList', []): + name = sp['ParameterName'] + name_l = name.lower() + value = sp['ParameterValue'] + specs[name_l] = (name, value) + part.update_specs(specs) + dd = part.dd.get(DIST_NAME, DistData()) + dd.moq = data['MinAmount'] + dd.qty_increment = data['Multiples'] + dd.url = data['ProductInformationPage'] + if dd.url and dd.url.startswith('//'): + dd.url = 'https:'+dd.url + dd.part_num = part_stock + dd.qty_avail = data['Amount'] + dd.currency = o.currency + prices = data.get('PriceList', None) + if prices: + dd.price_tiers = {p['Amount']: p['PriceValue'] for p in prices} + # Extra information + dd.extra_info['desc'] = data['Description'] + value = '' + for spec in ('capacitance', 'resistance', 'inductance'): + val = specs.get(spec, None) + if val: + value += val[1] + ' ' + if value: + dd.extra_info['value'] = value + for spec, name in SPEC_NAMES.items(): + val = specs.get(spec, None) + if val: + dd.extra_info[name] = val[1] + part.dd[DIST_NAME] = dd + debug_obsessive('* Part info after adding data:') + debug_obsessive(pprint.pformat(part.__dict__)) + # debug_obsessive(pprint.pformat(dd.__dict__)) + +
    [docs] @staticmethod + def query_part_info(parts, distributors, currency): + msg = None + try: + api_tme._query_part_info(parts, distributors, currency) + except TMEError as e: + msg = e.args[0] + if msg is not None: + raise KiCostError(msg, ERR_SCRAPE) + return set([DIST_NAME])
    + + +# Ok, this is special case ... we should add others +MANF_CHANGES = {'fairchild': 'onsemi'} + + +def _get_key(d, qty): + """ Sorting criteria for the suggested option """ + moq = d['MinAmount'] + status = d['ProductStatusList'] + cannot_be_ordered = 'CANNOT_BE_ORDERED' in status + hardly_available = 'HARDLY_AVAILABLE' in status + return (cannot_be_ordered, # Put first the ones in stock + hardly_available, + moq > qty) # Put first the ones with a MOQ under the quantity we need + + +def _filter_by_manf(data, manf): + """ Select the best matches according to the manufacturer """ + if not manf: + return data + manfs = {d['Producer'].lower() for d in data} + if len(manfs) == 1: + return data + manf = manf.lower() + best_matches = difflib.get_close_matches(manf, manfs) + if len(best_matches) == 0: + new_name = None + for k, v in MANF_CHANGES.items(): + if k in manf: + new_name = v + break + if new_name: + best_matches = difflib.get_close_matches(new_name, manfs) + if len(best_matches) == 0: + return data + best_match = best_matches[0] + return list(filter(lambda x: x['Producer'].lower() == best_match, data)) + + +def _list_comp_options(data, show, msg): + """ Debug function used to show the list of options """ + if not show: + return + debug_full(' - '+msg) + for c, d in enumerate(data): + debug_full(' {}) {} {} moq: {} status: {}'. + format(c+1, d['Producer'], d['OriginalSymbol'], d['MinAmount'], d['ProductStatusList'])) + + +def _select_best(data, manf, qty): + """ Selects the best result """ + c = len(data) + if c == 0: + return None + if c == 1: + return data[0]['Symbol'] + debug_obsessive(' - Choosing the best match ({} options, qty: {} manf: {})'.format(c, qty, manf)) + ultra_debug = is_debug_full() + _list_comp_options(data, ultra_debug, 'Original list') + # Try to choose the best manufacturer + data2 = _filter_by_manf(data, manf) + if data != data2: + debug_obsessive(' - Selected manf `{}`'.format(data2[0]['Producer'])) + _list_comp_options(data2, ultra_debug, 'Manufacturer selected') + if len(data2) == 1: + return data2[0]['Symbol'] + # Sort the results according to the best availability/price + data3 = sorted(data2, key=lambda x: _get_key(x, qty)) + _list_comp_options(data3, ultra_debug, 'Sorted') + return data3[0]['Symbol'] + + +distributor_class.register(api_tme, 100) +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/dist_local_template.html b/docs/_build/html/_modules/kicost/distributors/dist_local_template.html index 366059d6..49dbe961 100644 --- a/docs/_build/html/_modules/kicost/distributors/dist_local_template.html +++ b/docs/_build/html/_modules/kicost/distributors/dist_local_template.html @@ -5,7 +5,7 @@ - kicost.distributors.dist_local_template — kicost 1.1.13 documentation + kicost.distributors.dist_local_template — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -252,7 +252,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/distributors/distributor.html b/docs/_build/html/_modules/kicost/distributors/distributor.html index 836d8e73..756659bd 100644 --- a/docs/_build/html/_modules/kicost/distributors/distributor.html +++ b/docs/_build/html/_modules/kicost/distributors/distributor.html @@ -5,7 +5,7 @@ - kicost.distributors.distributor — kicost 1.1.13 documentation + kicost.distributors.distributor — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -72,7 +72,10 @@

    Source code for kicost.distributors.distributor

    < import copy import os import logging -import tqdm +try: + import tqdm +except ImportError: + pass # QueryCache dependencies: import pickle import time @@ -146,6 +149,8 @@

    Source code for kicost.distributors.distributor

    < label2name = {} # Options supported by this API config_options = {} + # Environment variables to configure this API + env_ops = {}
    [docs] @staticmethod def register(api, priority): @@ -157,7 +162,11 @@

    Source code for kicost.distributors.distributor

    < else: index += 1 distributor_class.registered.insert(index, api) - distributor_class.priorities.insert(index, priority)
    + distributor_class.priorities.insert(index, priority) + # Automatically fill the `env_ops` + if api.type != 'local': + for name in list(api.config_options.keys())+['cache_ttl', 'cache_path']: + api.env_ops[api.env_prefix+'_'+name.upper()] = name
    [docs] @staticmethod def update_distributors(parts, distributors): @@ -278,13 +287,16 @@

    Source code for kicost.distributors.distributor

    < ''' Find if an API is enabled ''' return distributor_class._get_api(api).enabled
    -
    [docs] @staticmethod - def from_environment(options, overwrite): - ''' Default configuration from the environment. Just nothing. ''' - pass
    +
    [docs] @classmethod + def from_environment(cls, options, overwrite): + ''' Configuration from the environment. ''' + # Configure the module from the environment + # The command line will overwrite it using set_options() + for k, v in cls.env_ops.items(): + distributor_class._set_from_env(v, os.getenv(k), options, overwrite, cls.name, cls.config_options)
    @staticmethod - def _set_from_env(key, value, options, overwrite, d_types=None): + def _set_from_env(key, value, options, overwrite, name, d_types=None): ''' Helper function to implement `from_environment`. ''' if value is not None and (overwrite or key not in options): if d_types: @@ -296,6 +308,7 @@

    Source code for kicost.distributors.distributor

    < tp = tp[0] # This is a cast value = tp(value) + distributor_class.logger.log(DEBUG_OBSESSIVE, 'Overwriting {} with {} for {}'.format(key, value, name)) options[key] = value
    [docs] @staticmethod @@ -346,7 +359,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/distributors/distributors_info.html b/docs/_build/html/_modules/kicost/distributors/distributors_info.html new file mode 100644 index 00000000..5f6aa05d --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/distributors_info.html @@ -0,0 +1,212 @@ + + + + + + + + kicost.distributors.distributors_info — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.distributors_info

    +# -*- coding: utf-8 -*-
    +# This file keep all the web distributors information used by the different API/scrap modules.
    +
    +# Author information.
    +__author__ = 'Hildo Guillardi Júnior'
    +__webpage__ = 'https://github.com/hildogjr/'
    +__company__ = 'University of Campinas - Brazil'
    +
    +# Used as identification for all user fields allowed for some custom importation in some distributors.
    +# It is used a low probability "word" corresponding to all user fields.
    +ORDER_COL_USERFIELDS = '*__USER__FIELDS__*'
    +# TODO at the GUI, could be a tab with to personalize this configuration, using this file as default, and the user could include or exclude some
    +#      personal field.
    +
    +
    +
    [docs]class DistributorOrder(object): + '''@brief Class to indicate how to place an order in a distributor.''' + def __init__(self, url=None, cols=[], header=None, delimiter=',', replace_by_char='; ', not_allowed_char=',\n', info=None, limit=None): + self.url = url + # Sort-order fields for online orders. The not present fields are by-passed and `None` represent a empty column. + self.cols = cols + # Header to help user undertanding (used in some importations). **Currently unused** + self.header = header + # Delimiter for online orders. + self.delimiter = delimiter + # The `delimiter` is not allowed inside description. This character is used to replace it. + self.replace_by_char = replace_by_char + # Characters not allowed at the BoM for web-site import. + self.not_allowed_char = not_allowed_char + # Descriptive fields size limit + self.limit = limit + self.info = info
    + + +
    [docs]class DistributorLabel(object): + '''@brief Class to describe a distributor column.''' + def __init__(self, name, url, bg, fg='white'): + self.name = name + self.url = url + # Formatting for distributor header in worksheet; bold, font and align are + # `spreadsheet.py` defined but can by overload here. + self.format = {'font_color': fg, 'bg_color': bg}
    + + +
    [docs]class DistributorInfo(object): + '''@brief Class to describe a distributor.''' + def __init__(self, order, label, type='web', ignore_cat=None): + self.type = type # Allowable values: 'local' or 'web'. + self.order = order + self.label = label + # Regex used to ignore some catalogue/stock code format. + # In the Digikey distributor it is used to ignore the Digi-reel package. + self.ignore_cat = ignore_cat + +
    [docs] def is_local(self): + return self.type == 'local'
    + +
    [docs] def is_web(self): + return self.type == 'web'
    + + +distributors_info = { + 'arrow': DistributorInfo( + order=DistributorOrder( + url='https://www.arrow.com/en/bom-tool/', + # header='Stock#,Quantity,Designators', + cols=['part_num', 'purch', 'refs']), + label=DistributorLabel('Arrow', 'https://www.arrow.com/', '#000000')), # Arrow black. + 'digikey': DistributorInfo( + order=DistributorOrder( + url='https://www.digikey.com/ordering/shoppingcart', + # header='Quantity,Stock#,Designators', + cols=['purch', 'part_num', 'refs']), + ignore_cat=r'.+(DKR\-ND|\-6\-ND)$', + label=DistributorLabel('Digi-Key', 'https://www.digikey.com/', '#CC0000')), # Digi-Key red. + 'farnell': DistributorInfo( + order=DistributorOrder( + url='https://fr.farnell.com/en-FR/quick-order?isQuickPaste=true', + # header='Stock#,Quantity,Descriptions,Designators,', + cols=['part_num', 'purch', 'desc', 'refs'], + limit=30), + label=DistributorLabel('Farnell', 'https://www.farnell.com/', '#FF6600')), # Farnell/E14 orange. + 'mouser': DistributorInfo( + order=DistributorOrder( + url='https://mouser.com/bom/', + # header='Stock#|Quantity|Designators', + cols=['part_num', 'purch', 'refs'], + delimiter='|', + not_allowed_char='| \n', + replace_by_char=';__'), + label=DistributorLabel('Mouser', 'https://www.mouser.com/', '#004A85')), # Mouser blue. + 'newark': DistributorInfo( + order=DistributorOrder( + url='https://www.newark.com/quick-order?isQuickPaste=true', + # header='Stock#,Quantity,Designators,Descriptions,User', + cols=['part_num', 'purch', 'refs', 'desc']), + label=DistributorLabel('Newark', 'https://www.newark.com/', '#A2AE06')), # Newark/E14 olive green. + 'rs': DistributorInfo( + order=DistributorOrder( + url='https://uk.rs-online.com/web/mylists/manualQuotes.html?method=showEnquiryCreationPage&mode=new', + # header='Stock#,Quantity,-,-,-,Part,Designators', + cols=['part_num', 'purch', None, None, None, 'manf#', 'refs']), # `None` is used for generate a empty column. + label=DistributorLabel('RS Components', 'https://uk.rs-online.com/', '#FF0000')), # RS Components red. + 'tme': DistributorInfo( + order=DistributorOrder( + url='https://www.tme.eu/en/Profile/QuickBuy/load.html', + # header='Stock# Quantity Designators', + cols=['part_num', 'purch', 'refs'], + delimiter=' ', + not_allowed_char=' \n', + replace_by_char=';'), + label=DistributorLabel('TME', 'https://www.tme.eu/', '#0C4DA1')), # TME blue. + 'lcsc': DistributorInfo( + order=DistributorOrder( + url='https://lcsc.com/bom.html', + header='Quantity,Comment,Designator,Footprint,LCSC Part #(optional)', + cols=['purch', 'refs', 'footprint', 'part_num'], + info='Copy this header and order to a CSV\n' + 'file and use it for JLCPCB \n' + 'manufacturer PCB house.\n' + 'The multipart components that use\n' + '"#" symbol are not allowed by JLCPCB.'), + label=DistributorLabel('LCSC', 'https://lcsc.com/', '#1166DD')), # LCSC blue. + 'local_template': DistributorInfo( + type='local', + order=DistributorOrder(cols=['part_num', 'purch', 'refs']), + label=DistributorLabel('Local', None, '#008000')), # Darker green. +} +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/distributors/log__.html b/docs/_build/html/_modules/kicost/distributors/log__.html new file mode 100644 index 00000000..a4851153 --- /dev/null +++ b/docs/_build/html/_modules/kicost/distributors/log__.html @@ -0,0 +1,135 @@ + + + + + + + + kicost.distributors.log__ — kicost 1.1.14 documentation + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for kicost.distributors.log__

    +# -*- coding: utf-8 -*-
    +# MIT license
    +#
    +# Copyright (C) 2022 by Salvador E. Tropea / Instituto Nacional de Tecnologia Industrial
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to deal
    +# in the Software without restriction, including without limitation the rights
    +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +# copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +# THE SOFTWARE.
    +
    +# Simple wrappers for the log functions in the distributors module
    +
    +from .distributor import distributor_class
    +from .. import DEBUG_OVERVIEW, DEBUG_DETAILED, DEBUG_OBSESSIVE, DEBUG_FULL
    +
    +
    +
    [docs]def debug_detailed(*args): + distributor_class.logger.log(DEBUG_DETAILED, *args)
    + + +
    [docs]def debug_overview(*args): + distributor_class.logger.log(DEBUG_OVERVIEW, *args)
    + + +
    [docs]def debug_obsessive(*args): + distributor_class.logger.log(DEBUG_OBSESSIVE, *args)
    + + +
    [docs]def debug_full(*args): + distributor_class.logger.log(DEBUG_FULL, *args)
    + + +
    [docs]def is_debug_full(): + return distributor_class.logger.getEffectiveLevel() <= DEBUG_FULL
    + + +
    [docs]def warning(code, msg): + distributor_class.logger.warning(code + msg)
    +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/kicost/edas.html b/docs/_build/html/_modules/kicost/edas.html index 3066670c..b863846e 100644 --- a/docs/_build/html/_modules/kicost/edas.html +++ b/docs/_build/html/_modules/kicost/edas.html @@ -5,7 +5,7 @@ - kicost.edas — kicost 1.1.13 documentation + kicost.edas — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -133,7 +133,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/edas/eda_altium.html b/docs/_build/html/_modules/kicost/edas/eda_altium.html index 9dafd8e6..516a6260 100644 --- a/docs/_build/html/_modules/kicost/edas/eda_altium.html +++ b/docs/_build/html/_modules/kicost/edas/eda_altium.html @@ -5,7 +5,7 @@ - kicost.edas.eda_altium — kicost 1.1.13 documentation + kicost.edas.eda_altium — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -226,7 +226,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/edas/eda_kicad.html b/docs/_build/html/_modules/kicost/edas/eda_kicad.html index 28e78eb9..d5bc221a 100644 --- a/docs/_build/html/_modules/kicost/edas/eda_kicad.html +++ b/docs/_build/html/_modules/kicost/edas/eda_kicad.html @@ -5,7 +5,7 @@ - kicost.edas.eda_kicad — kicost 1.1.13 documentation + kicost.edas.eda_kicad — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -238,7 +238,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/edas/generic_csv.html b/docs/_build/html/_modules/kicost/edas/generic_csv.html index 3af458bd..73b6289a 100644 --- a/docs/_build/html/_modules/kicost/edas/generic_csv.html +++ b/docs/_build/html/_modules/kicost/edas/generic_csv.html @@ -5,7 +5,7 @@ - kicost.edas.generic_csv — kicost 1.1.13 documentation + kicost.edas.generic_csv — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -301,7 +301,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/edas/tools.html b/docs/_build/html/_modules/kicost/edas/tools.html index d3aad043..0ac32d69 100644 --- a/docs/_build/html/_modules/kicost/edas/tools.html +++ b/docs/_build/html/_modules/kicost/edas/tools.html @@ -5,7 +5,7 @@ - kicost.edas.tools — kicost 1.1.13 documentation + kicost.edas.tools — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -869,7 +869,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/global_vars.html b/docs/_build/html/_modules/kicost/global_vars.html index 28858ea1..d0f93113 100644 --- a/docs/_build/html/_modules/kicost/global_vars.html +++ b/docs/_build/html/_modules/kicost/global_vars.html @@ -5,7 +5,7 @@ - kicost.global_vars — kicost 1.1.13 documentation + kicost.global_vars — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -198,7 +198,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/kicad_config.html b/docs/_build/html/_modules/kicost/kicad_config.html index b4bf178b..3f78070e 100644 --- a/docs/_build/html/_modules/kicost/kicad_config.html +++ b/docs/_build/html/_modules/kicost/kicad_config.html @@ -5,7 +5,7 @@ - kicost.kicad_config — kicost 1.1.13 documentation + kicost.kicad_config — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -366,7 +366,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/kicost.html b/docs/_build/html/_modules/kicost/kicost.html index 496cc2ca..05dae28c 100644 --- a/docs/_build/html/_modules/kicost/kicost.html +++ b/docs/_build/html/_modules/kicost/kicost.html @@ -5,7 +5,7 @@ - kicost.kicost — kicost 1.1.13 documentation + kicost.kicost — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -394,7 +394,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/kicost_config.html b/docs/_build/html/_modules/kicost/kicost_config.html index 298efdc4..7629647a 100644 --- a/docs/_build/html/_modules/kicost/kicost_config.html +++ b/docs/_build/html/_modules/kicost/kicost_config.html @@ -5,7 +5,7 @@ - kicost.kicost_config — kicost 1.1.13 documentation + kicost.kicost_config — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -393,7 +393,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/kicost_gui.html b/docs/_build/html/_modules/kicost/kicost_gui.html index d6b9d886..a9b3d83f 100644 --- a/docs/_build/html/_modules/kicost/kicost_gui.html +++ b/docs/_build/html/_modules/kicost/kicost_gui.html @@ -5,7 +5,7 @@ - kicost.kicost_gui — kicost 1.1.13 documentation + kicost.kicost_gui — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -1329,7 +1329,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/kicost_kicadplugin.html b/docs/_build/html/_modules/kicost/kicost_kicadplugin.html index 00f881df..2633b9d0 100644 --- a/docs/_build/html/_modules/kicost/kicost_kicadplugin.html +++ b/docs/_build/html/_modules/kicost/kicost_kicadplugin.html @@ -5,7 +5,7 @@ - kicost.kicost_kicadplugin — kicost 1.1.13 documentation + kicost.kicost_kicadplugin — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -169,7 +169,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/log.html b/docs/_build/html/_modules/kicost/log.html index 932b18d3..ab23331b 100644 --- a/docs/_build/html/_modules/kicost/log.html +++ b/docs/_build/html/_modules/kicost/log.html @@ -5,7 +5,7 @@ - kicost.log — kicost 1.1.13 documentation + kicost.log — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -294,7 +294,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/log__.html b/docs/_build/html/_modules/kicost/log__.html index 2f5fe593..f90a6f94 100644 --- a/docs/_build/html/_modules/kicost/log__.html +++ b/docs/_build/html/_modules/kicost/log__.html @@ -5,7 +5,7 @@ - kicost.log__ — kicost 1.1.13 documentation + kicost.log__ — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -166,7 +166,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/sexpdata.html b/docs/_build/html/_modules/kicost/sexpdata.html index 1e9ee8bc..648a534c 100644 --- a/docs/_build/html/_modules/kicost/sexpdata.html +++ b/docs/_build/html/_modules/kicost/sexpdata.html @@ -5,7 +5,7 @@ - kicost.sexpdata — kicost 1.1.13 documentation + kicost.sexpdata — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -746,7 +746,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_modules/kicost/spreadsheet.html b/docs/_build/html/_modules/kicost/spreadsheet.html index fac9c8af..8d3de754 100644 --- a/docs/_build/html/_modules/kicost/spreadsheet.html +++ b/docs/_build/html/_modules/kicost/spreadsheet.html @@ -5,7 +5,7 @@ - kicost.spreadsheet — kicost 1.1.13 documentation + kicost.spreadsheet — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - + @@ -1378,7 +1378,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/_build/html/_sources/configuration.rst.txt b/docs/_build/html/_sources/configuration.rst.txt index 138b064f..7bad00cc 100644 --- a/docs/_build/html/_sources/configuration.rst.txt +++ b/docs/_build/html/_sources/configuration.rst.txt @@ -113,6 +113,10 @@ The current Digi-Key plugin needs to validate the user using a complex mechanism window to get a token. If you get an error about a wrong certificate please ignore it. The obtained token is cached, so you don't need to validate it all the time. +You can also define options using environment variables. As an example, you can specify Mouser's key defining +the `MOUSER_KEY` environment variable. Note that environment variables has more precedence than the default config file. +But command line options, including any configuration file passed using it, has the highest priority. + .. _KitSpace: https://kitspace.org/ .. _API_site: https://developer.digikey.com/get_started .. _Nexar: https://nexar.com/api diff --git a/docs/_build/html/_sources/kicost.distributors.rst.txt b/docs/_build/html/_sources/kicost.distributors.rst.txt index a84eb896..221cfd22 100644 --- a/docs/_build/html/_sources/kicost.distributors.rst.txt +++ b/docs/_build/html/_sources/kicost.distributors.rst.txt @@ -4,10 +4,34 @@ kicost.distributors package Submodules ---------- -kicost.distributors.api\_octopart module ----------------------------------------- +kicost.distributors.api\_digikey module +--------------------------------------- + +.. automodule:: kicost.distributors.api_digikey + :members: + :undoc-members: + :show-inheritance: + +kicost.distributors.api\_element14 module +----------------------------------------- + +.. automodule:: kicost.distributors.api_element14 + :members: + :undoc-members: + :show-inheritance: + +kicost.distributors.api\_mouser module +-------------------------------------- -.. automodule:: kicost.distributors.api_octopart +.. automodule:: kicost.distributors.api_mouser + :members: + :undoc-members: + :show-inheritance: + +kicost.distributors.api\_nexar module +------------------------------------- + +.. automodule:: kicost.distributors.api_nexar :members: :undoc-members: :show-inheritance: @@ -20,6 +44,14 @@ kicost.distributors.api\_partinfo\_kitspace module :undoc-members: :show-inheritance: +kicost.distributors.api\_tme module +----------------------------------- + +.. automodule:: kicost.distributors.api_tme + :members: + :undoc-members: + :show-inheritance: + kicost.distributors.dist\_local\_template module ------------------------------------------------ @@ -36,6 +68,14 @@ kicost.distributors.distributor module :undoc-members: :show-inheritance: +kicost.distributors.distributors\_info module +--------------------------------------------- + +.. automodule:: kicost.distributors.distributors_info + :members: + :undoc-members: + :show-inheritance: + kicost.distributors.global\_vars module --------------------------------------- @@ -44,6 +84,13 @@ kicost.distributors.global\_vars module :undoc-members: :show-inheritance: +kicost.distributors.log\_\_ module +---------------------------------- + +.. automodule:: kicost.distributors.log__ + :members: + :undoc-members: + :show-inheritance: Module contents --------------- diff --git a/docs/_build/html/_static/documentation_options.js b/docs/_build/html/_static/documentation_options.js index 0661c6d5..de7bbc47 100644 --- a/docs/_build/html/_static/documentation_options.js +++ b/docs/_build/html/_static/documentation_options.js @@ -1,6 +1,6 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '1.1.13', + VERSION: '1.1.14', LANGUAGE: 'None', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/_build/html/authors.html b/docs/_build/html/authors.html index c4df3e86..a1a96bbe 100644 --- a/docs/_build/html/authors.html +++ b/docs/_build/html/authors.html @@ -5,7 +5,7 @@ - Credits — kicost 1.1.13 documentation + Credits — kicost 1.1.14 documentation @@ -34,7 +34,7 @@

    Navigation

  • previous |
  • - +
    @@ -52,6 +52,12 @@

    Development Leadinfo@xess.com>

    +
    +

    GUI, main collaborator and maintainer

    + +
    diff --git a/docs/_build/html/configuration.html b/docs/_build/html/configuration.html index 081834f1..d3bb8e8e 100644 --- a/docs/_build/html/configuration.html +++ b/docs/_build/html/configuration.html @@ -5,7 +5,7 @@ - Configuration file — kicost 1.1.13 documentation + Configuration file — kicost 1.1.14 documentation @@ -34,7 +34,7 @@

    Navigation

  • previous |
  • - +
    @@ -147,6 +147,9 @@

    Configuration file previous | - +

    diff --git a/docs/_build/html/contributing.html b/docs/_build/html/contributing.html index fff108c4..aaffcf43 100644 --- a/docs/_build/html/contributing.html +++ b/docs/_build/html/contributing.html @@ -5,7 +5,7 @@ - Contributing — kicost 1.1.13 documentation + Contributing — kicost 1.1.14 documentation @@ -34,7 +34,7 @@

    Navigation

  • previous |
  • - +
    @@ -219,7 +219,7 @@

    Navigation

  • previous |
  • - +
    diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html index 5fa4b31a..e4a1f354 100644 --- a/docs/_build/html/genindex.html +++ b/docs/_build/html/genindex.html @@ -5,7 +5,7 @@ - Index — kicost 1.1.13 documentation + Index — kicost 1.1.14 documentation @@ -26,7 +26,7 @@

    Navigation

  • modules |
  • - +
    @@ -67,6 +67,8 @@

    A

    - +
    @@ -142,9 +158,11 @@

    B

    C

    - +
    @@ -326,14 +424,20 @@

    F

  • (kicost.edas.generic_csv.generic_csv static method)
  • -
  • fill_part_info() (kicost.distributors.api_octopart.api_octopart static method) +
  • fill_extra_info() (kicost.distributors.api_nexar.api_nexar static method) +
  • +
  • fill_missing_with_defaults() (in module kicost.config) +
  • +
  • fill_part_data() (kicost.distributors.api_nexar.api_nexar static method) +
  • + + - -
  • get_part_info() (kicost.distributors.api_octopart.api_octopart static method) +
  • get_part_info() (kicost.distributors.api_nexar.api_nexar static method)