From 38fabafa617dc6433ecac99356ccbaca67b6f22d Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Mon, 10 Nov 2025 11:48:12 +0100 Subject: [PATCH 1/2] Factor out provider API access into separate class --- Dockerfile | 2 +- app.py | 286 ++++-------------------------------------- demo-request.json | 4 + provider_dummy.py | 20 +++ provider_partstack.py | 258 +++++++++++++++++++++++++++++++++++++ 5 files changed, 308 insertions(+), 262 deletions(-) create mode 100755 provider_dummy.py create mode 100755 provider_partstack.py diff --git a/Dockerfile b/Dockerfile index 41d4022..4f535e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apk add --no-cache \ py3-requests-pyc # Copy files. -COPY app.py app/ +COPY *.py app/ COPY static/ app/static/ WORKDIR app diff --git a/app.py b/app.py index 3689845..e35c491 100755 --- a/app.py +++ b/app.py @@ -3,72 +3,17 @@ import json import os -import requests from flask import (Flask, g, make_response, request, send_from_directory, url_for) from werkzeug.middleware.proxy_fix import ProxyFix +from provider_partstack import Partstack + app = Flask(__name__) app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) PARTS_MAX_COUNT = 10 PARTS_QUERY_TIMEOUT = 8.0 -PARTS_QUERY_FRAGMENT = """ - fragment f on Stock { - products { - basic { - manufacturer - mfgpartno - status - } - url - imageUrl - datasheetUrl - } - summary { - inStockInventory - medianPrice - suppliersInStock - } - } -""" -PARTS_QUERY_STATUS_MAP = { - 'active': 'Active', - 'active-unconfirmed': 'Active', - 'nrfnd': 'NRND', - 'eol': 'Obsolete', - 'obsolete': 'Obsolete', - 'discontinued': 'Obsolete', - 'transferred': 'Obsolete', - 'contact mfr': None, # Not supported, but here to avoid warning. -} -MANUFACTURER_REPLACEMENTS = { - 'ä': 'ae', - 'ö': 'oe', - 'ü': 'ue', - 'texas instruments': 'ti', - 'stmicroelectronics': 'st', -} -MANUFACTURER_REMOVALS = set([ - 'contact', - 'devices', - 'electronics', - 'inc.', - 'inc', - 'incorporated', - 'integrated', - 'international', - 'limited', - 'ltd.', - 'ltd', - 'microelectronics', - 'semiconductor', - 'semiconductors', - 'solutions', - 'systems', - 'technology', - 'usa', -]) def _get_config(key, fallback=None): @@ -101,171 +46,15 @@ def _write_status(key_values): app.logger.critical(str(e)) -def _build_headers(): - return { - 'Content-Type': 'application/json', - 'Accept': 'application/json, multipart/mixed', - 'Authorization': 'Bearer {}'.format(_get_config('parts_query_token')), - } - - -def _build_request(parts): - args = [] - queries = [] - variables = {} - for i in range(len(parts)): - args.append('$mpn{}:String!'.format(i)) - queries.append('q{}:findStocks(mfgpartno:$mpn{}){{...f}}'.format(i, i)) - variables['mpn{}'.format(i)] = parts[i]['mpn'] - query = 'query Stocks({}) {{\n{}\n}}'.format( - ','.join(args), - '\n'.join(queries) - ) + PARTS_QUERY_FRAGMENT - return dict(query=query, variables=variables) - - -def _get_basic_value(product, key): - if type(product) is dict: - basic = product.get('basic') - if type(basic) is dict: - value = basic.get(key) - if type(value) is str: - return value - return '' - - -def _normalize_manufacturer(mfr): - mfr = mfr.lower() - for old, new in MANUFACTURER_REPLACEMENTS.items(): - mfr = mfr.replace(old, new) - terms = [s for s in mfr.split(' ') if s not in MANUFACTURER_REMOVALS] - return ' '.join(terms) - - -def _calc_product_match_score(p, mpn_n, mfr_n): - score = 0 - - status_p = PARTS_QUERY_STATUS_MAP.get(_get_basic_value(p, 'status')) - if status_p == 'Active': - score += 200 - elif status_p == 'NRND': - score += 100 - - mpn_p = _get_basic_value(p, 'mfgpartno').lower() - if mpn_p == mpn_n: - score += 20 # MPN matches exactly. - elif mpn_p.replace(' ', '') == mpn_n.replace(' ', ''): - score += 10 # MPN matches when ignoring whitespaces. - else: - return 0 # MPN does not match! - - mfr_p = _normalize_manufacturer(_get_basic_value(p, 'manufacturer')) - if mfr_p == mfr_n: - score += 4 # Manufacturer matches exactly. - elif mfr_n in mfr_p: - score += 3 # Manufacturer matches partially. - elif mfr_n.replace(' ', '') in mfr_p.replace(' ', ''): - score += 2 # Manufacturer matches partially when ignoring whitespaces. - elif mfr_n.split(' ')[0] in mfr_p: - score += 1 # The first term of the manufacturer matches. - else: - return 0 # Manufacturer does not match! - - return score - - -def _get_product(data, mpn, manufacturer): - products = (data.get('products') or []) - for p in products: - p['_score'] = _calc_product_match_score( - p, mpn.lower(), _normalize_manufacturer(manufacturer)) - products = sorted([p for p in products if p['_score'] > 0], - key=lambda p: p['_score'], reverse=True) - return products[0] if len(products) else None - - -def _add_pricing_url(out, data): - value = data.get('url') - if value is not None: - out['pricing_url'] = value - - -def _add_image_url(out, data): - value = data.get('imageUrl') - if value is not None: - out['picture_url'] = value - - -def _add_status(out, data): - status = data.get('status') or '' - status_n = status.lower() - value = PARTS_QUERY_STATUS_MAP.get(status_n.lower()) - if value is not None: - out['status'] = value - elif len(status_n) and (status_n not in PARTS_QUERY_STATUS_MAP): - app.logger.warning('Unknown part lifecycle status: {}'.format(status)) - - -def _stock_to_availability(stock): - if stock > 100000: - return 10 # Very Good - elif stock > 5000: - return 5 # Good - elif stock > 200: - return 0 # Normal - elif stock > 0: - return -5 # Bad - else: - return -10 # Very Bad - - -def _suppliers_to_availability(suppliers): - if suppliers > 30: - return 10 # Very Good - elif suppliers > 9: - return 5 # Good - elif suppliers > 1: - return 0 # Normal - elif suppliers > 0: - return -5 # Bad - else: - return -10 # Very Bad - - -def _add_availability(out, data): - stock = data.get('inStockInventory') - suppliers = data.get('suppliersInStock') - values = [] - if type(stock) is int: - values.append(_stock_to_availability(stock)) - if type(suppliers) is int: - values.append(_suppliers_to_availability(suppliers)) - if len(values): - out['availability'] = min(values) - - -def _add_prices(out, summary): - value = summary.get('medianPrice') - if type(value) in [float, int]: - out['prices'] = [dict(quantity=1, price=float(value))] - - -def _add_resources(out, data): - value = data.get('datasheetUrl') - if value is not None: - out['resources'] = [ - dict(name="Datasheet", mediatype="application/pdf", url=value), - ] - - @app.route('/api/v1/parts', methods=['GET']) def parts(): enabled = _get_config('parts_operational', False) + provider = Partstack response = make_response(dict( - provider_name='Partstack', - provider_url='https://partstack.com', + provider_name=provider.NAME, + provider_url=provider.URL, provider_logo_url=url_for('parts_static', - filename='parts-provider-partstack.png', + filename=provider.LOGO_FILENAME, _external=True), info_url='https://api.librepcb.org/api', query_url=url_for('parts_query', _external=True) if enabled else None, @@ -280,51 +69,26 @@ def parts_query(): # Get requested parts. payload = request.get_json() parts = payload['parts'][:PARTS_MAX_COUNT] + parts = [dict(mpn=p['mpn'], manufacturer=p['manufacturer']) for p in parts] - # Query parts from information provider. - query_response = requests.post( - _get_config('parts_query_url'), - headers=_build_headers(), - json=_build_request(parts), - timeout=PARTS_QUERY_TIMEOUT, - ) - query_json = query_response.json() - data = query_json.get('data') or {} - errors = query_json.get('errors') or [] - if (len(data) == 0) and (type(query_json.get('message')) is str): - errors.append(query_json['message']) - for error in errors: - app.logger.warning("GraphQL Error: " + str(error)) - - # Handle quota limit. - next_access_time = query_json.get('nextAccessTime') - if (len(data) == 0) and (next_access_time is not None): - app.logger.warning("Quota limit: " + str(next_access_time)) - _write_status(dict(next_access_time=next_access_time)) - - # Convert query response data and return it to the client. - tx = dict(parts=[]) - for i in range(len(parts)): - mpn = parts[i]['mpn'] - manufacturer = parts[i]['manufacturer'] - part_data = data.get('q' + str(i)) or {} - product = _get_product(part_data, mpn, manufacturer) - part = dict( - mpn=mpn, - manufacturer=manufacturer, - results=0 if product is None else 1, - ) - if product is not None: - basic = product.get('basic') or {} - summary = part_data.get('summary') or {} - _add_pricing_url(part, product) - _add_image_url(part, product) - _add_status(part, basic) - _add_availability(part, summary) - _add_prices(part, summary) - _add_resources(part, product) - tx['parts'].append(part) - return tx + # Fetch parts from provider. + status = dict() + provider = Partstack(_get_config('parts_query_url'), + _get_config('parts_query_token'), + PARTS_QUERY_TIMEOUT, app.logger) + provider.fetch(parts, status) + + # Handle status changes. + if len(status): + _write_status(status) + + # Complete parts which were not found. + for part in parts: + if 'results' not in part: + part['results'] = 0 + + # Return response. + return dict(parts=parts) @app.route('/api/v1/parts/static/', methods=['GET']) diff --git a/demo-request.json b/demo-request.json index 8b794ab..55143fa 100644 --- a/demo-request.json +++ b/demo-request.json @@ -3,6 +3,10 @@ { "mpn": "1N4148", "manufacturer": "Vishay" + }, + { + "mpn": "NE555P", + "manufacturer": "Texas Instruments" } ] } diff --git a/provider_dummy.py b/provider_dummy.py new file mode 100755 index 0000000..7d5171c --- /dev/null +++ b/provider_dummy.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + + +class DummyProvider: + ID = 'dummy' + + def __init__(self): + pass + + def fetch(self, parts, status): + for i in range(len(parts)): + part = parts[i] + if 'results' not in part: + self._fetch_part(part, i) + + def _fetch_part(self, part, i): + if i % 2: + part['results'] = 1 + part['status'] = 'Active' + part['prices'] = [dict(quantity=1, price=13.37)] diff --git a/provider_partstack.py b/provider_partstack.py new file mode 100755 index 0000000..e1df9d1 --- /dev/null +++ b/provider_partstack.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- + +import requests + +QUERY_FRAGMENT = """ + fragment f on Stock { + products { + basic { + manufacturer + mfgpartno + status + } + url + imageUrl + datasheetUrl + } + summary { + inStockInventory + medianPrice + suppliersInStock + } + } +""" +QUERY_STATUS_MAP = { + 'active': 'Active', + 'active-unconfirmed': 'Active', + 'nrfnd': 'NRND', + 'eol': 'Obsolete', + 'obsolete': 'Obsolete', + 'discontinued': 'Obsolete', + 'transferred': 'Obsolete', + 'contact mfr': None, # Not supported, but here to avoid warning. +} +MANUFACTURER_REPLACEMENTS = { + 'ä': 'ae', + 'ö': 'oe', + 'ü': 'ue', + 'texas instruments': 'ti', + 'stmicroelectronics': 'st', +} +MANUFACTURER_REMOVALS = set([ + 'contact', + 'devices', + 'electronics', + 'inc.', + 'inc', + 'incorporated', + 'integrated', + 'international', + 'limited', + 'ltd.', + 'ltd', + 'microelectronics', + 'semiconductor', + 'semiconductors', + 'solutions', + 'systems', + 'technology', + 'usa', +]) + + +class Partstack: + ID = 'partstack' + NAME = 'Partstack' + URL = 'https://partstack.com' + LOGO_FILENAME = 'parts-provider-partstack.png' + + def __init__(self, query_url, query_token, query_timeout, logger): + self._query_url = query_url + self._query_token = query_token + self._query_timeout = query_timeout + self._logger = logger + + def fetch(self, parts, status): + # Request parts data. + query_response = requests.post( + self._query_url, + headers=self._build_headers(), + json=self._build_request(parts), + timeout=self._query_timeout, + ) + query_json = query_response.json() + data = query_json.get('data') or {} + errors = query_json.get('errors') or [] + if (len(data) == 0) and (type(query_json.get('message')) is str): + errors.append(query_json['message']) + for error in errors: + self._logger.warning("GraphQL Error: " + str(error)) + + # Handle quota limit. + next_access_time = query_json.get('nextAccessTime') + if (len(data) == 0) and (next_access_time is not None): + self._logger.warning("Quota limit: " + str(next_access_time)) + status['next_access_time'] = next_access_time + + # Convert query response data. + for i in range(len(parts)): + if 'results' not in parts[i]: + mpn = parts[i]['mpn'] + manufacturer = parts[i]['manufacturer'] + part_data = data.get('q' + str(i)) or {} + product = self._get_product(part_data, mpn, manufacturer) + if product is not None: + parts[i]['results'] = 1 + basic = product.get('basic') or {} + summary = part_data.get('summary') or {} + self._add_pricing_url(parts[i], product) + self._add_image_url(parts[i], product) + self._add_status(parts[i], basic) + self._add_availability(parts[i], summary) + self._add_prices(parts[i], summary) + self._add_resources(parts[i], product) + + def _build_headers(self): + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json, multipart/mixed', + 'Authorization': 'Bearer {}'.format(self._query_token), + } + + def _build_request(self, parts): + args = [] + queries = [] + variables = {} + for i in range(len(parts)): + query = 'q{}:findStocks(mfgpartno:$mpn{}){{...f}}'.format(i, i) + args.append('$mpn{}:String!'.format(i)) + queries.append(query) + variables['mpn{}'.format(i)] = parts[i]['mpn'] + query = 'query Stocks({}) {{\n{}\n}}'.format( + ','.join(args), + '\n'.join(queries) + ) + QUERY_FRAGMENT + return dict(query=query, variables=variables) + + def _get_product(self, data, mpn, manufacturer): + products = (data.get('products') or []) + for p in products: + p['_score'] = self._calc_product_match_score( + p, mpn.lower(), self._normalize_manufacturer(manufacturer)) + products = sorted([p for p in products if p['_score'] > 0], + key=lambda p: p['_score'], reverse=True) + return products[0] if len(products) else None + + def _calc_product_match_score(self, p, mpn_n, mfr_n): + score = 0 + + status_p = QUERY_STATUS_MAP.get(self._get_basic_value(p, 'status')) + if status_p == 'Active': + score += 200 + elif status_p == 'NRND': + score += 100 + + mpn_p = self._get_basic_value(p, 'mfgpartno').lower() + if mpn_p == mpn_n: + score += 20 # MPN matches exactly. + elif mpn_p.replace(' ', '') == mpn_n.replace(' ', ''): + score += 10 # MPN matches when ignoring whitespaces. + else: + return 0 # MPN does not match! + + mfr_p = self._normalize_manufacturer( + self._get_basic_value(p, 'manufacturer')) + if mfr_p == mfr_n: + score += 4 # Manufacturer matches exactly. + elif mfr_n in mfr_p: + score += 3 # Manufacturer matches partially. + elif mfr_n.replace(' ', '') in mfr_p.replace(' ', ''): + score += 2 # Manufacturer matches partially when ignoring spaces. + elif mfr_n.split(' ')[0] in mfr_p: + score += 1 # The first term of the manufacturer matches. + else: + return 0 # Manufacturer does not match! + + return score + + def _get_basic_value(self, product, key): + if type(product) is dict: + basic = product.get('basic') + if type(basic) is dict: + value = basic.get(key) + if type(value) is str: + return value + return '' + + def _normalize_manufacturer(self, mfr): + mfr = mfr.lower() + for old, new in MANUFACTURER_REPLACEMENTS.items(): + mfr = mfr.replace(old, new) + terms = [s for s in mfr.split(' ') if s not in MANUFACTURER_REMOVALS] + return ' '.join(terms) + + def _add_pricing_url(self, out, data): + value = data.get('url') + if value is not None: + out['pricing_url'] = value + + def _add_image_url(self, out, data): + value = data.get('imageUrl') + if value is not None: + out['picture_url'] = value + + def _add_status(self, out, data): + status = data.get('status') or '' + status_n = status.lower() + value = QUERY_STATUS_MAP.get(status_n.lower()) + if value is not None: + out['status'] = value + elif len(status_n) and (status_n not in QUERY_STATUS_MAP): + self._logger.warning(f'Unknown part lifecycle status: {status}') + + def _add_availability(self, out, data): + stock = data.get('inStockInventory') + suppliers = data.get('suppliersInStock') + values = [] + if type(stock) is int: + values.append(self._stock_to_availability(stock)) + if type(suppliers) is int: + values.append(self._suppliers_to_availability(suppliers)) + if len(values): + out['availability'] = min(values) + + def _stock_to_availability(self, stock): + if stock > 100000: + return 10 # Very Good + elif stock > 5000: + return 5 # Good + elif stock > 200: + return 0 # Normal + elif stock > 0: + return -5 # Bad + else: + return -10 # Very Bad + + def _suppliers_to_availability(self, suppliers): + if suppliers > 30: + return 10 # Very Good + elif suppliers > 9: + return 5 # Good + elif suppliers > 1: + return 0 # Normal + elif suppliers > 0: + return -5 # Bad + else: + return -10 # Very Bad + + def _add_prices(self, out, summary): + value = summary.get('medianPrice') + if type(value) in [float, int]: + out['prices'] = [dict(quantity=1, price=float(value))] + + def _add_resources(self, out, data): + value = data.get('datasheetUrl') + if value is not None: + out['resources'] = [ + dict(name="Datasheet", mediatype="application/pdf", url=value), + ] From 0cbc59dbbe3e2928b144ea366bb1b4b1c568727d Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Mon, 10 Nov 2025 11:54:21 +0100 Subject: [PATCH 2/2] Cache parts information with a database --- app.py | 44 ++++++++++++++++++++++++--- database.py | 71 +++++++++++++++++++++++++++++++++++++++++++ provider_cache.py | 24 +++++++++++++++ provider_dummy.py | 6 ++-- provider_partstack.py | 5 ++- 5 files changed, 142 insertions(+), 8 deletions(-) create mode 100755 database.py create mode 100755 provider_cache.py diff --git a/app.py b/app.py index e35c491..2a2ecb6 100755 --- a/app.py +++ b/app.py @@ -2,17 +2,21 @@ import json import os +import sqlite3 from flask import (Flask, g, make_response, request, send_from_directory, url_for) from werkzeug.middleware.proxy_fix import ProxyFix +from database import Database +from provider_cache import PartsCache from provider_partstack import Partstack app = Flask(__name__) app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) PARTS_MAX_COUNT = 10 +PARTS_CACHE_MAX_AGE = 30*24*3600 # 30 days due to quota limits PARTS_QUERY_TIMEOUT = 8.0 @@ -46,6 +50,20 @@ def _write_status(key_values): app.logger.critical(str(e)) +def _get_db(): + db = getattr(g, '_db', None) + if db is None: + db = g._database = sqlite3.connect('/config/db.sqlite') + return db + + +@app.teardown_appcontext +def _close_db(exception): + db = getattr(g, '_db', None) + if db is not None: + db.close() + + @app.route('/api/v1/parts', methods=['GET']) def parts(): enabled = _get_config('parts_operational', False) @@ -71,21 +89,37 @@ def parts_query(): parts = payload['parts'][:PARTS_MAX_COUNT] parts = [dict(mpn=p['mpn'], manufacturer=p['manufacturer']) for p in parts] - # Fetch parts from provider. + # Prepare database. + db = Database(_get_db(), app.logger) + + # Fetch parts from providers. status = dict() - provider = Partstack(_get_config('parts_query_url'), - _get_config('parts_query_token'), - PARTS_QUERY_TIMEOUT, app.logger) - provider.fetch(parts, status) + cache_hits = 0 + providers = [ + PartsCache(db, max_age=PARTS_CACHE_MAX_AGE), + Partstack(_get_config('parts_query_url'), + _get_config('parts_query_token'), + PARTS_QUERY_TIMEOUT, db, app.logger), + ] + for provider in providers: + cache_hits += provider.fetch(parts, status) # Handle status changes. if len(status): _write_status(status) # Complete parts which were not found. + found = 0 for part in parts: if 'results' not in part: part['results'] = 0 + if part['results'] > 0: + found += 1 + + # Store request in database. + app.logger.debug(f"Queried {len(parts)} parts, {cache_hits} from cache: " + f"{found} found, {len(parts) - found} not found") + db.add_parts_request(len(parts), cache_hits, found) # Return response. return dict(parts=parts) diff --git a/database.py b/database.py new file mode 100755 index 0000000..f190e2a --- /dev/null +++ b/database.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +from ast import literal_eval + + +class Database: + def __init__(self, db, logger): + self._db = db + self._logger = logger + + with self._db as db: + db_version = db.execute('PRAGMA user_version').fetchone()[0] + if db_version < 1: + logger.info("Migrating database to version 1...") + db.execute(""" + CREATE TABLE IF NOT EXISTS parts_requests ( + id INTEGER PRIMARY KEY NOT NULL, + datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + count INTEGER NOT NULL, + cache_hits INTEGER NOT NULL, + with_result INTEGER NOT NULL + ) + """) + db.execute(""" + CREATE TABLE IF NOT EXISTS parts_cache ( + id INTEGER PRIMARY KEY NOT NULL, + mpn TEXT NOT NULL, + manufacturer TEXT NOT NULL, + provider TEXT NOT NULL, + datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + part TEXT NOT NULL, + UNIQUE(mpn, manufacturer, provider) + ) + """) + db.execute('PRAGMA user_version=1') + self._db.execute("VACUUM") + + def add_parts_request(self, count: int, cache_hits: int, + with_result: int): + with self._db as db: + db.execute( + "INSERT INTO parts_requests " + "(count, cache_hits, with_result) " + "VALUES (?, ?, ?)", + (count, cache_hits, with_result) + ) + + def add_parts_cache(self, provider: str, part: dict): + with self._db as db: + db.execute( + "INSERT INTO parts_cache " + "(mpn, manufacturer, provider, part) " + "VALUES (?, ?, ?, ?) " + "ON CONFLICT(mpn, manufacturer, provider) DO UPDATE SET " + " datetime = CURRENT_TIMESTAMP, " + " part = excluded.part", + (part['mpn'], part['manufacturer'], provider, str(part)) + ) + + def get_parts_cache(self, mpn, manufacturer, max_age): + with self._db as db: + cur = db.cursor() + cur.execute( + "SELECT part FROM parts_cache " + "WHERE mpn=? AND manufacturer=? " + "AND datetime >= datetime('now', ?)" + "ORDER BY datetime DESC", + (mpn, manufacturer, f"-{max_age} seconds") + ) + row = cur.fetchone() + return literal_eval(row[0]) if row is not None else None diff --git a/provider_cache.py b/provider_cache.py new file mode 100755 index 0000000..797410c --- /dev/null +++ b/provider_cache.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + + +class PartsCache: + def __init__(self, db, max_age): + self._db = db + self._max_age = max_age + + def fetch(self, parts, status): + hits = 0 + for part in parts: + if 'results' not in part: + if self._fetch_part(part): + hits += 1 + return hits + + def _fetch_part(self, part): + result = self._db.get_parts_cache(part['mpn'], part['manufacturer'], + self._max_age) + if result is not None: + part.update(result) + return True + else: + return False diff --git a/provider_dummy.py b/provider_dummy.py index 7d5171c..4d408c0 100755 --- a/provider_dummy.py +++ b/provider_dummy.py @@ -4,17 +4,19 @@ class DummyProvider: ID = 'dummy' - def __init__(self): - pass + def __init__(self, db): + self._db = db def fetch(self, parts, status): for i in range(len(parts)): part = parts[i] if 'results' not in part: self._fetch_part(part, i) + return 0 def _fetch_part(self, part, i): if i % 2: part['results'] = 1 part['status'] = 'Active' part['prices'] = [dict(quantity=1, price=13.37)] + self._db.add_parts_cache(self.ID, part) diff --git a/provider_partstack.py b/provider_partstack.py index e1df9d1..20bcee8 100755 --- a/provider_partstack.py +++ b/provider_partstack.py @@ -66,10 +66,11 @@ class Partstack: URL = 'https://partstack.com' LOGO_FILENAME = 'parts-provider-partstack.png' - def __init__(self, query_url, query_token, query_timeout, logger): + def __init__(self, query_url, query_token, query_timeout, db, logger): self._query_url = query_url self._query_token = query_token self._query_timeout = query_timeout + self._db = db self._logger = logger def fetch(self, parts, status): @@ -111,6 +112,8 @@ def fetch(self, parts, status): self._add_availability(parts[i], summary) self._add_prices(parts[i], summary) self._add_resources(parts[i], product) + self._db.add_parts_cache(self.ID, parts[i]) + return 0 def _build_headers(self): return {