Skip to content

Commit e7b1ddc

Browse files
authored
Merge pull request #1 from OpenBudget/datarecords-endpoint
Add /api/datarecords/{key} endpoint
2 parents 57a4e0c + 0284f92 commit e7b1ddc

3 files changed

Lines changed: 124 additions & 0 deletions

File tree

budgetkey_api/flask_app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ def create_flask_app(session_file_dir=None, cache_dir=None, services=None):
9494
setup_simpledb(app, es_blueprint, db_blueprint)
9595
log.info("SimpleDB setup complete")
9696

97+
# No external dependencies, so always available
98+
log.info("Setting up DataRecords")
99+
from .modules.datarecords import setup_datarecords
100+
setup_datarecords(app, cache)
101+
log.info("DataRecords setup complete")
102+
97103
app.before_request(logging_before)
98104
app.after_request(logging_after)
99105

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
3+
import requests
4+
5+
from flask import Blueprint, abort
6+
from flask_jsonpify import jsonpify
7+
8+
from .caching import add_cache_header
9+
10+
DATARECORDS_URL = os.environ.get('DATARECORDS_URL', 'https://data-input.obudget.org/api/datarecords')
11+
TIMEOUT = 24 * 60 * 60 # one day
12+
13+
KEYS = [
14+
'target_age_group',
15+
'subject',
16+
'intervention',
17+
'target_audience',
18+
]
19+
20+
21+
class DataRecordsBlueprint(Blueprint):
22+
23+
def __init__(self, cache):
24+
super().__init__('datarecords', 'datarecords')
25+
self.cache = cache
26+
27+
self.add_url_rule(
28+
'/datarecords/<key>',
29+
'datarecords',
30+
self.get_datarecords,
31+
methods=['GET']
32+
)
33+
34+
def fetch_datarecords(self, key):
35+
response = requests.get(f'{DATARECORDS_URL}/{key}', timeout=60)
36+
response.raise_for_status()
37+
return response.json()
38+
39+
def get_datarecords(self, key):
40+
if key not in KEYS:
41+
abort(404, f'Data record {key} not found. Available keys: {", ".join(KEYS)}')
42+
cache_key = f'datarecords/{key}'
43+
data = self.cache.get(cache_key)
44+
if data is None:
45+
try:
46+
data = self.fetch_datarecords(key)
47+
except Exception as e:
48+
abort(502, f'Failed to fetch data records for {key}: {e}')
49+
self.cache.set(cache_key, data, timeout=TIMEOUT)
50+
return jsonpify(data)
51+
52+
53+
def setup_datarecords(app, cache):
54+
bp = DataRecordsBlueprint(cache)
55+
add_cache_header(bp, TIMEOUT)
56+
app.register_blueprint(bp, url_prefix='/api/')
57+
return bp

tests/test_datarecords.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import dotenv
2+
3+
dotenv.load_dotenv('tests/sample.env')
4+
5+
6+
def make_client(tmp_path):
7+
from budgetkey_api.flask_app import create_flask_app
8+
9+
app = create_flask_app(session_file_dir=str(tmp_path / 'sessions'), cache_dir=str(tmp_path / 'cache'),
10+
services='none')
11+
app.config.update({'TESTING': True})
12+
app.testing = True
13+
return app.test_client()
14+
15+
16+
def test_datarecords_bad_key(tmp_path):
17+
client = make_client(tmp_path)
18+
resp = client.get('/api/datarecords/no_such_key')
19+
assert resp.status_code == 404
20+
21+
22+
def test_datarecords_fetches_and_caches(tmp_path, monkeypatch):
23+
from budgetkey_api.modules import datarecords
24+
25+
calls = []
26+
27+
class FakeResponse:
28+
def raise_for_status(self):
29+
pass
30+
31+
def json(self):
32+
return {'result': [{'key': 'subject::foo'}]}
33+
34+
def fake_get(url, **kwargs):
35+
calls.append(url)
36+
return FakeResponse()
37+
38+
monkeypatch.setattr(datarecords.requests, 'get', fake_get)
39+
40+
client = make_client(tmp_path)
41+
for _ in range(2):
42+
resp = client.get('/api/datarecords/subject')
43+
assert resp.status_code == 200
44+
assert resp.json == {'result': [{'key': 'subject::foo'}]}
45+
assert resp.headers['Cache-Control'] == f'max-age={datarecords.TIMEOUT}'
46+
47+
# Second call is served from the cache
48+
assert calls == [f'{datarecords.DATARECORDS_URL}/subject']
49+
50+
51+
def test_datarecords_upstream_failure(tmp_path, monkeypatch):
52+
from budgetkey_api.modules import datarecords
53+
54+
def fake_get(url, **kwargs):
55+
raise ConnectionError('boom')
56+
57+
monkeypatch.setattr(datarecords.requests, 'get', fake_get)
58+
59+
client = make_client(tmp_path)
60+
resp = client.get('/api/datarecords/intervention')
61+
assert resp.status_code == 502

0 commit comments

Comments
 (0)