Skip to content

Commit 0cbc59d

Browse files
committed
Cache parts information with a database
1 parent 38fabaf commit 0cbc59d

File tree

5 files changed

+142
-8
lines changed

5 files changed

+142
-8
lines changed

app.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
import json
44
import os
5+
import sqlite3
56

67
from flask import (Flask, g, make_response, request, send_from_directory,
78
url_for)
89
from werkzeug.middleware.proxy_fix import ProxyFix
910

11+
from database import Database
12+
from provider_cache import PartsCache
1013
from provider_partstack import Partstack
1114

1215
app = Flask(__name__)
1316
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
1417

1518
PARTS_MAX_COUNT = 10
19+
PARTS_CACHE_MAX_AGE = 30*24*3600 # 30 days due to quota limits
1620
PARTS_QUERY_TIMEOUT = 8.0
1721

1822

@@ -46,6 +50,20 @@ def _write_status(key_values):
4650
app.logger.critical(str(e))
4751

4852

53+
def _get_db():
54+
db = getattr(g, '_db', None)
55+
if db is None:
56+
db = g._database = sqlite3.connect('/config/db.sqlite')
57+
return db
58+
59+
60+
@app.teardown_appcontext
61+
def _close_db(exception):
62+
db = getattr(g, '_db', None)
63+
if db is not None:
64+
db.close()
65+
66+
4967
@app.route('/api/v1/parts', methods=['GET'])
5068
def parts():
5169
enabled = _get_config('parts_operational', False)
@@ -71,21 +89,37 @@ def parts_query():
7189
parts = payload['parts'][:PARTS_MAX_COUNT]
7290
parts = [dict(mpn=p['mpn'], manufacturer=p['manufacturer']) for p in parts]
7391

74-
# Fetch parts from provider.
92+
# Prepare database.
93+
db = Database(_get_db(), app.logger)
94+
95+
# Fetch parts from providers.
7596
status = dict()
76-
provider = Partstack(_get_config('parts_query_url'),
77-
_get_config('parts_query_token'),
78-
PARTS_QUERY_TIMEOUT, app.logger)
79-
provider.fetch(parts, status)
97+
cache_hits = 0
98+
providers = [
99+
PartsCache(db, max_age=PARTS_CACHE_MAX_AGE),
100+
Partstack(_get_config('parts_query_url'),
101+
_get_config('parts_query_token'),
102+
PARTS_QUERY_TIMEOUT, db, app.logger),
103+
]
104+
for provider in providers:
105+
cache_hits += provider.fetch(parts, status)
80106

81107
# Handle status changes.
82108
if len(status):
83109
_write_status(status)
84110

85111
# Complete parts which were not found.
112+
found = 0
86113
for part in parts:
87114
if 'results' not in part:
88115
part['results'] = 0
116+
if part['results'] > 0:
117+
found += 1
118+
119+
# Store request in database.
120+
app.logger.debug(f"Queried {len(parts)} parts, {cache_hits} from cache: "
121+
f"{found} found, {len(parts) - found} not found")
122+
db.add_parts_request(len(parts), cache_hits, found)
89123

90124
# Return response.
91125
return dict(parts=parts)

database.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from ast import literal_eval
4+
5+
6+
class Database:
7+
def __init__(self, db, logger):
8+
self._db = db
9+
self._logger = logger
10+
11+
with self._db as db:
12+
db_version = db.execute('PRAGMA user_version').fetchone()[0]
13+
if db_version < 1:
14+
logger.info("Migrating database to version 1...")
15+
db.execute("""
16+
CREATE TABLE IF NOT EXISTS parts_requests (
17+
id INTEGER PRIMARY KEY NOT NULL,
18+
datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
19+
count INTEGER NOT NULL,
20+
cache_hits INTEGER NOT NULL,
21+
with_result INTEGER NOT NULL
22+
)
23+
""")
24+
db.execute("""
25+
CREATE TABLE IF NOT EXISTS parts_cache (
26+
id INTEGER PRIMARY KEY NOT NULL,
27+
mpn TEXT NOT NULL,
28+
manufacturer TEXT NOT NULL,
29+
provider TEXT NOT NULL,
30+
datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
31+
part TEXT NOT NULL,
32+
UNIQUE(mpn, manufacturer, provider)
33+
)
34+
""")
35+
db.execute('PRAGMA user_version=1')
36+
self._db.execute("VACUUM")
37+
38+
def add_parts_request(self, count: int, cache_hits: int,
39+
with_result: int):
40+
with self._db as db:
41+
db.execute(
42+
"INSERT INTO parts_requests "
43+
"(count, cache_hits, with_result) "
44+
"VALUES (?, ?, ?)",
45+
(count, cache_hits, with_result)
46+
)
47+
48+
def add_parts_cache(self, provider: str, part: dict):
49+
with self._db as db:
50+
db.execute(
51+
"INSERT INTO parts_cache "
52+
"(mpn, manufacturer, provider, part) "
53+
"VALUES (?, ?, ?, ?) "
54+
"ON CONFLICT(mpn, manufacturer, provider) DO UPDATE SET "
55+
" datetime = CURRENT_TIMESTAMP, "
56+
" part = excluded.part",
57+
(part['mpn'], part['manufacturer'], provider, str(part))
58+
)
59+
60+
def get_parts_cache(self, mpn, manufacturer, max_age):
61+
with self._db as db:
62+
cur = db.cursor()
63+
cur.execute(
64+
"SELECT part FROM parts_cache "
65+
"WHERE mpn=? AND manufacturer=? "
66+
"AND datetime >= datetime('now', ?)"
67+
"ORDER BY datetime DESC",
68+
(mpn, manufacturer, f"-{max_age} seconds")
69+
)
70+
row = cur.fetchone()
71+
return literal_eval(row[0]) if row is not None else None

provider_cache.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# -*- coding: utf-8 -*-
2+
3+
4+
class PartsCache:
5+
def __init__(self, db, max_age):
6+
self._db = db
7+
self._max_age = max_age
8+
9+
def fetch(self, parts, status):
10+
hits = 0
11+
for part in parts:
12+
if 'results' not in part:
13+
if self._fetch_part(part):
14+
hits += 1
15+
return hits
16+
17+
def _fetch_part(self, part):
18+
result = self._db.get_parts_cache(part['mpn'], part['manufacturer'],
19+
self._max_age)
20+
if result is not None:
21+
part.update(result)
22+
return True
23+
else:
24+
return False

provider_dummy.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@
44
class DummyProvider:
55
ID = 'dummy'
66

7-
def __init__(self):
8-
pass
7+
def __init__(self, db):
8+
self._db = db
99

1010
def fetch(self, parts, status):
1111
for i in range(len(parts)):
1212
part = parts[i]
1313
if 'results' not in part:
1414
self._fetch_part(part, i)
15+
return 0
1516

1617
def _fetch_part(self, part, i):
1718
if i % 2:
1819
part['results'] = 1
1920
part['status'] = 'Active'
2021
part['prices'] = [dict(quantity=1, price=13.37)]
22+
self._db.add_parts_cache(self.ID, part)

provider_partstack.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ class Partstack:
6666
URL = 'https://partstack.com'
6767
LOGO_FILENAME = 'parts-provider-partstack.png'
6868

69-
def __init__(self, query_url, query_token, query_timeout, logger):
69+
def __init__(self, query_url, query_token, query_timeout, db, logger):
7070
self._query_url = query_url
7171
self._query_token = query_token
7272
self._query_timeout = query_timeout
73+
self._db = db
7374
self._logger = logger
7475

7576
def fetch(self, parts, status):
@@ -111,6 +112,8 @@ def fetch(self, parts, status):
111112
self._add_availability(parts[i], summary)
112113
self._add_prices(parts[i], summary)
113114
self._add_resources(parts[i], product)
115+
self._db.add_parts_cache(self.ID, parts[i])
116+
return 0
114117

115118
def _build_headers(self):
116119
return {

0 commit comments

Comments
 (0)