Skip to content

Commit 4398608

Browse files
committed
Cache parts information with a database
1 parent 2ebe830 commit 4398608

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,18 +2,22 @@
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_dummy import DummyProvider # noqa: F401
1114
from provider_partstack import Partstack
1215

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

1619
PARTS_MAX_COUNT = 10
20+
PARTS_CACHE_MAX_AGE = 30*24*3600 # 30 days due to quota limits
1721
PARTS_QUERY_TIMEOUT = 8.0
1822

1923

@@ -47,6 +51,20 @@ def _write_status(key_values):
4751
app.logger.critical(str(e))
4852

4953

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

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

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

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

91125
# Return response.
92126
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 query(self, parts, status):
@@ -111,6 +112,8 @@ def query(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)