Skip to content

Commit 7fcc25b

Browse files
committed
Simple API with timeouts
1 parent a1c9c56 commit 7fcc25b

File tree

1 file changed

+87
-47
lines changed

1 file changed

+87
-47
lines changed

budgetkey_api/modules/simpledb.py

+87-47
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import os
22
from pathlib import Path
33
from contextlib import contextmanager
4+
import datetime
45

56
import requests
67
import json
8+
from hashlib import md5
79

810
from flask import Blueprint, abort, current_app, request
911
from flask_jsonpify import jsonpify
@@ -15,7 +17,7 @@
1517
ROOT_DIR = Path(__file__).parent
1618

1719

18-
class SimpleDBBlueprint(Blueprint):
20+
class TableHolder:
1921

2022
TABLES = [
2123
'budget_items_data',
@@ -28,33 +30,49 @@ class SimpleDBBlueprint(Blueprint):
2830
]
2931

3032
DATAPACKAGE_URL = 'https://next.obudget.org/datapackages/simpledb'
33+
TIMEOUT = 600
3134

32-
def __init__(self, connection_string, search_blueprint):
33-
super().__init__('simpledb', 'simpledb')
35+
def __init__(self, connection_string):
3436
self.connection_string = connection_string
35-
self.tables, self.search_params = self.process_tables()
36-
self.search_blueprint = search_blueprint
37-
self.add_url_rule(
38-
'/tables/<table>/info',
39-
'table-info',
40-
self.get_table,
41-
methods=['GET']
42-
)
43-
44-
self.add_url_rule(
45-
'/tables',
46-
'table-list',
47-
self.get_tables,
48-
methods=['GET']
49-
)
50-
51-
if search_blueprint:
52-
self.add_url_rule(
53-
'/tables/<table>/search',
54-
'table-search',
55-
self.simple_search,
56-
methods=['GET']
57-
)
37+
self.infos = dict()
38+
self.schemas = dict()
39+
40+
def get_info(self, table):
41+
info, _ = self.get_table_data(table)
42+
info['schema'] = self.get_schema(table)
43+
return info
44+
45+
def get_schema(self, table):
46+
if table not in self.schemas:
47+
self.schemas[table] = self.get_schema_from_db(table)
48+
return self.schemas[table]
49+
50+
def get_search_params(self, table):
51+
_, search = self.get_table_data(table)
52+
return search
53+
54+
def get_table_data(self, table):
55+
fetch = True
56+
current_hash = None
57+
if table in self.infos:
58+
info, search, ts, current_hash = self.infos[table]
59+
if (datetime.datetime.now() - ts).seconds < self.TIMEOUT:
60+
fetch = False
61+
if fetch:
62+
info, search, hash = self.fetch_table(table)
63+
if info is not None:
64+
self.infos[table] = (info, search, datetime.datetime.now(), hash)
65+
if current_hash != hash:
66+
self.schemas[table] = None
67+
info, search, ts = self.infos[table]
68+
return info, search
69+
70+
def get_schema_from_db(self, table):
71+
with self.connect_db() as connection:
72+
query = text(f"select generate_create_table_statement('{table}')")
73+
result = connection.execute(query)
74+
create_table = result.fetchone()[0]
75+
return create_table
5876

5977
@contextmanager
6078
def connect_db(self):
@@ -67,45 +85,67 @@ def connect_db(self):
6785
engine.dispose()
6886
del engine
6987

70-
def process_tables(self):
71-
ret = dict()
72-
sp = dict()
73-
with self.connect_db() as connection:
74-
for table in self.TABLES:
88+
def fetch_table(self, table):
89+
if table in self.TABLES:
90+
with self.connect_db() as connection:
7591
try:
7692
rec = {}
7793
datapackage_url = f'{self.DATAPACKAGE_URL}/{table}/datapackage.json'
78-
package_descriptor = requests.get(datapackage_url).json()
94+
response = requests.get(datapackage_url)
95+
hash = md5(response.content).hexdigest()
96+
package_descriptor = response.json()
7997
description = package_descriptor['resources'][0]['description']
8098
fields = package_descriptor['resources'][0]['schema']['fields']
8199
rec['description'] = description
82100
rec['fields'] = [dict(
83101
name=f['name'],
84102
**f.get('details', {})
85103
) for f in fields]
86-
rec['schema'] = self.get_schema(connection, table)
87-
ret[table] = rec
88-
sp[table] = package_descriptor['resources'][0]['search']
104+
return rec, package_descriptor['resources'][0]['search'], hash
89105
except Exception as e:
90106
print(f'Error processing table {table}: {e}')
91-
return ret, sp
107+
return None, None, None
92108

93-
def get_schema(self, connection, table):
94-
query = text(f"select generate_create_table_statement('{table}')")
95-
result = connection.execute(query)
96-
create_table = result.fetchone()[0]
97-
return create_table
109+
class SimpleDBBlueprint(Blueprint):
110+
111+
def __init__(self, connection_string, search_blueprint):
112+
super().__init__('simpledb', 'simpledb')
113+
self.tables = TableHolder(connection_string)
114+
115+
self.search_blueprint = search_blueprint
116+
self.add_url_rule(
117+
'/tables/<table>/info',
118+
'table-info',
119+
self.get_table,
120+
methods=['GET']
121+
)
122+
123+
self.add_url_rule(
124+
'/tables',
125+
'table-list',
126+
self.get_tables,
127+
methods=['GET']
128+
)
129+
130+
if search_blueprint:
131+
self.add_url_rule(
132+
'/tables/<table>/search',
133+
'table-search',
134+
self.simple_search,
135+
methods=['GET']
136+
)
98137

99138
def get_table(self, table):
100-
if table not in self.tables:
101-
abort(404, f'Table {table} not found. Available tables: {", ".join(self.tables.keys())}')
102-
return jsonpify(self.tables[table])
139+
ret = self.tables.get_info(table)
140+
if ret is None:
141+
abort(404, f'Table {table} not found. Available tables: {", ".join(self.tables.TABLES)}')
142+
return jsonpify(ret)
103143

104144
def get_tables(self):
105-
return jsonpify(list(self.tables.keys()))
145+
return jsonpify(self.tables.TABLES)
106146

107147
def simple_search(self, table):
108-
params = self.search_params[table]
148+
params = self.tables.get_search_params(table)
109149

110150
q = request.args.get('q', '')
111151
filters = params.get('filters', {}) or {}
@@ -141,5 +181,5 @@ def simple_search(self, table):
141181

142182
def setup_simpledb(app, es_blueprint):
143183
sdb = SimpleDBBlueprint(os.environ['DATABASE_READONLY_URL'], es_blueprint)
144-
add_cache_header(sdb, 600)
184+
add_cache_header(sdb, TableHolder.TIMEOUT)
145185
app.register_blueprint(sdb, url_prefix='/api/')

0 commit comments

Comments
 (0)