-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.py
More file actions
executable file
·131 lines (106 loc) · 3.69 KB
/
Copy pathapp.py
File metadata and controls
executable file
·131 lines (106 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# -*- coding: utf-8 -*-
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 = 15
PARTS_CACHE_MAX_AGE = 60*24*3600 # 60 days due to quota limits
PARTS_QUERY_TIMEOUT = 8.0
def _get_config(key, fallback=None):
if 'config' not in g:
try:
with open('/config/api.json', 'rb') as f:
g.config = json.load(f)
except Exception as e:
app.logger.critical(str(e))
g.config = dict()
return g.config.get(key, fallback)
def _write_status(key_values):
status = dict()
fp = '/config/status.json'
if os.path.exists(fp):
try:
with open(fp, 'r') as f:
status = json.load(f)
except Exception as e:
app.logger.critical(str(e))
for key, value in key_values.items():
status[key] = value
try:
with open(fp + '~', 'w') as f:
f.write(json.dumps(status, indent=4))
os.replace(fp + '~', fp)
except Exception as e:
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)
provider = Partstack
response = make_response(dict(
provider_name=provider.NAME,
provider_url=provider.URL,
provider_logo_url=url_for('parts_static',
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,
max_parts=PARTS_MAX_COUNT,
))
response.headers['Cache-Control'] = 'max-age=300'
return response
@app.route('/api/v1/parts/query', methods=['POST'])
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]
# Prepare database.
db = Database(_get_db(), app.logger)
# Fetch parts from providers.
status = dict()
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)
@app.route('/api/v1/parts/static/<filename>', methods=['GET'])
def parts_static(filename):
return send_from_directory(
'static', filename, mimetype='image/png', max_age=24*3600)