Skip to content

Commit ef173c4

Browse files
Merge pull request #106 from jverswijver/dynamic_rest_api
Dynamic rest api
2 parents f54f77d + 5c899ed commit ef173c4

File tree

11 files changed

+179
-6
lines changed

11 files changed

+179
-6
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ dist
88
build
99
docs/_build
1010
*.tar.gz
11+
pharus/dynamic_api.py
12+
pharus/dynamic_api_spec.yaml

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ ARG PY_VER
22
ARG DISTRO
33
ARG IMAGE
44
FROM datajoint/${IMAGE}:py${PY_VER}-${DISTRO}
5-
COPY --chown=dja:anaconda ./README.rst ./requirements.txt ./setup.py \
5+
COPY --chown=anaconda:anaconda ./README.rst ./requirements.txt ./setup.py \
66
/main/
7-
COPY --chown=dja:anaconda ./pharus/*.py /main/pharus/
7+
COPY --chown=anaconda:anaconda ./pharus/*.py /main/pharus/
88
RUN \
99
cd /main && \
1010
pip install . && \
@@ -23,4 +23,4 @@ WORKDIR /main
2323
# CMD ["pharus"]
2424

2525
# production service
26-
CMD ["sh", "-lc", "gunicorn --bind 0.0.0.0:${PHARUS_PORT} pharus.server:app"]
26+
CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PHARUS_PORT} pharus.server:app"]

docker-compose-deploy.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ services:
1818
# environment: # configurable values with defaults
1919
# - PHARUS_PORT=5000
2020
# - PHARUS_PREFIX=/
21+
# - API_SPEC_PATH=tests/init/test_dynamic_api_spec.yaml # for dynamic api spec
2122
fakeservices.datajoint.io:
2223
<<: *net
2324
image: datajoint/nginx:v0.0.16

docker-compose-dev.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ services:
2525
service: pharus
2626
environment:
2727
- FLASK_ENV=development # enables logging to console from Flask
28+
- API_SPEC_PATH=tests/init/test_dynamic_api_spec.yaml # for dynamic api spec
2829
volumes:
2930
- ./pharus:/opt/conda/lib/python3.8/site-packages/pharus
3031
command: pharus
3132
fakeservices.datajoint.io:
3233
<<: *net
33-
image: datajoint/nginx:v0.0.16
34+
image: datajoint/nginx:v0.0.18
3435
environment:
3536
- ADD_pharus_TYPE=REST
3637
- ADD_pharus_ENDPOINT=pharus:5000

docker-compose-test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
- TEST_DB_USER=root
2424
- TEST_DB_PASS=simple
2525
- AS_SCRIPT
26+
- API_SPEC_PATH=tests/init/test_dynamic_api_spec.yaml
2627
volumes:
2728
- ./requirements_test.txt:/tmp/pip_requirements.txt
2829
- ./pharus:/opt/conda/lib/python3.8/site-packages/pharus

pharus/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
1+
from . import dynamic_api_gen
12
from .version import __version__
2-
__version__
3+
from os import path, environ
4+
5+
try:
6+
if path.exists(environ.get('API_SPEC_PATH')):
7+
dynamic_api_gen.populate_api()
8+
except TypeError:
9+
print('No Dynamic API path found')
10+
11+
try:
12+
from .dynamic_api import app
13+
except ImportError:
14+
from .server import app
15+
16+
__all__ = ['__version__', 'app']

pharus/dynamic_api_gen.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from textwrap import indent
2+
from pathlib import Path
3+
import os
4+
import yaml
5+
6+
7+
def populate_api():
8+
header_template = """
9+
# Auto-generated rest api
10+
from .server import app, protected_route
11+
from .interface import _DJConnector, dj
12+
import json
13+
import numpy as np
14+
15+
16+
class NumpyEncoder(json.JSONEncoder):
17+
def default(self, obj):
18+
if isinstance(obj, np.ndarray):
19+
return obj.tolist()
20+
return json.JSONEncoder.default(self, obj)
21+
"""
22+
route_template = """
23+
24+
@app.route('{route}', methods=['GET'])
25+
@protected_route
26+
def {method_name}(jwt_payload: dict) -> dict:
27+
28+
{query}
29+
djconn = _DJConnector._set_datajoint_config(jwt_payload)
30+
vm_dict = {{s: dj.VirtualModule(s, s, connection=djconn) for s in dj.list_schemas()}}
31+
query, fetch_args = dj_query(vm_dict)
32+
return json.dumps(query.fetch(**fetch_args), cls=NumpyEncoder)
33+
"""
34+
35+
spec_path = os.environ.get('API_SPEC_PATH')
36+
api_path = 'pharus/dynamic_api.py'
37+
with open(Path(api_path), 'w') as f, open(Path(spec_path), 'r') as y:
38+
f.write(header_template)
39+
values_yaml = yaml.load(y, Loader=yaml.FullLoader)
40+
pages = values_yaml['SciViz']['pages']
41+
42+
# Crawl through the yaml file for the routes in the components
43+
for page in pages.values():
44+
for grid in page['grids'].values():
45+
for comp in grid['components'].values():
46+
f.write(route_template.format(route=comp['route'],
47+
method_name=comp['route'].replace('/', ''),
48+
query=indent(comp['dj_query'], ' ')))

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ gunicorn
33
pyjwt[crypto]
44
datajoint>=0.13.0
55
datajoint_connection_hub
6+
pyyaml

tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from pharus.server import app
2+
from pharus.dynamic_api import app
33
from uuid import UUID
44
from os import getenv
55
import datajoint as dj
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
version: 'v0.0.0'
2+
LabBook: null
3+
Test: "hello world"
4+
SciViz: # top level tab
5+
auth:
6+
mode: database # reuse from DJ LabBook
7+
pages: # individual pages
8+
page2:
9+
route: /session1 # dev, be careful of name collisions
10+
args:
11+
- subject_name
12+
- session_number
13+
hidden: true
14+
grids:
15+
grid2:
16+
components:
17+
component1:
18+
route: /query1
19+
row_span: 0
20+
column_span: 0
21+
type: plot:png
22+
restriction: >
23+
def restriction(**args):
24+
return dict(**args)
25+
dj_query: >
26+
def dj_query(vms):
27+
TableA, TableB = (vms['test_group1_simple'].TableA, vms['test_group1_simple'].TableB)
28+
return TableA * TableB, dict(order_by='b_number')
29+
grid1:
30+
components:
31+
component1:
32+
route: /query2
33+
row_span: 0
34+
column_span: 0
35+
type: plot:png
36+
restriction: >
37+
def restriction(**args):
38+
return dict(**args)
39+
dj_query: >
40+
def dj_query(vms):
41+
TableA, TableB = (vms['test_group1_simple'].TableA, vms['test_group1_simple'].TableB)
42+
return TableA * TableB, dict(order_by='b_number')
43+
44+
page1:
45+
route: /session2 # dev, be careful of name collisions
46+
args:
47+
- subject_name
48+
- session_number
49+
hidden: true
50+
grids:
51+
grid1:
52+
type: fixed
53+
max_component_per_page: 20
54+
rows: 1
55+
columns: 3
56+
components:
57+
component1:
58+
route: /query3
59+
row_span: 0
60+
column_span: 0
61+
type: plot:png
62+
restriction: >
63+
def restriction(**args):
64+
return dict(**args)
65+
dj_query: >
66+
def dj_query(vms):
67+
TableA, TableB = (vms['test_group1_simple'].TableA, vms['test_group1_simple'].TableB)
68+
return TableA * TableB, dict(order_by='b_number')
69+
component2:
70+
route: /query4
71+
row_span: 0
72+
column_span: 1
73+
type: plot:plotly1
74+
restriction: >
75+
def restriction(**args):
76+
return dict(**args)
77+
dj_query: >
78+
def dj_query(vms):
79+
TableA, TableB = (vms['test_group1_simple'].TableA, vms['test_group1_simple'].TableB)
80+
return TableA * TableB, dict(order_by='b_number')
81+
diff_checker: >
82+
def diff_checker(**args):
83+
return TrainingStatsPlotly.proj(hash='trial_mean_hash')

0 commit comments

Comments
 (0)