Skip to content

Commit 5b1850f

Browse files
authored
Add swagger to both APIs (#19)
1 parent 73700b0 commit 5b1850f

File tree

8 files changed

+1143
-416
lines changed

8 files changed

+1143
-416
lines changed

api_tabular/app.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import aiohttp_cors
44

55
from aiohttp import web, ClientSession
6+
from aiohttp_swagger import setup_swagger
67

78
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
89
from api_tabular import config
@@ -11,7 +12,7 @@
1112
get_resource_data,
1213
get_resource_data_streamed,
1314
)
14-
from api_tabular.utils import build_sql_query_string, build_link_with_page, url_for
15+
from api_tabular.utils import build_sql_query_string, build_link_with_page, url_for, build_swagger_file
1516
from api_tabular.error import QueryException
1617

1718
routes = web.RouteTableDef()
@@ -58,6 +59,16 @@ async def resource_profile(request):
5859
return web.json_response(resource)
5960

6061

62+
@routes.get(r"/api/resources/{rid}/swagger/", name="swagger")
63+
async def resource_swagger(request):
64+
resource_id = request.match_info["rid"]
65+
resource = await get_resource(
66+
request.app["csession"], resource_id, ["profile:csv_detective"]
67+
)
68+
swagger_string = build_swagger_file(resource['profile']['columns'], resource_id)
69+
return web.Response(body=swagger_string)
70+
71+
6172
@routes.get(r"/api/resources/{rid}/data/", name="data")
6273
async def resource_data(request):
6374
resource_id = request.match_info["rid"]
@@ -159,6 +170,9 @@ async def on_cleanup(app):
159170
)
160171
for route in list(app.router.routes()):
161172
cors.add(route)
173+
174+
setup_swagger(app, swagger_url=config.DOC_PATH, ui_version=3, swagger_from_file="ressource_app_swagger.yaml")
175+
162176
return app
163177

164178

api_tabular/config_default.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
PGREST_ENDPOINT = "http://localhost:8080"
2-
SERVER_NAME = 'localhost:8005'
3-
SCHEME = 'http'
2+
SERVER_NAME = "localhost:8005"
3+
SCHEME = "http"
44
SENTRY_DSN = ""
55
PAGE_SIZE_DEFAULT = 20
66
PAGE_SIZE_MAX = 50
77
BATCH_SIZE = 50000
8+
DOC_PATH = "/api/doc"

api_tabular/metrics.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sentry_sdk
33
import aiohttp_cors
44

5+
from aiohttp_swagger import setup_swagger
56
from aiohttp import web, ClientSession
67

78
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
@@ -57,6 +58,9 @@ async def get_object_data_streamed(
5758

5859
@routes.get(r"/api/{model}/data/")
5960
async def metrics_data(request):
61+
"""
62+
Retrieve metric data for a specified model with optional filtering and sorting.
63+
"""
6064
model = request.match_info["model"]
6165
query_string = request.query_string.split("&") if request.query_string else []
6266
page = int(request.query.get("page", "1"))
@@ -70,7 +74,6 @@ async def metrics_data(request):
7074
offset = page_size * (page - 1)
7175
else:
7276
offset = 0
73-
7477
try:
7578
sql_query = build_sql_query_string(query_string, page_size, offset)
7679
except ValueError:
@@ -145,6 +148,9 @@ async def on_cleanup(app):
145148
)
146149
for route in list(app.router.routes()):
147150
cors.add(route)
151+
152+
setup_swagger(app, swagger_url=config.DOC_PATH, ui_version=3, swagger_from_file="metrics_swagger.yaml")
153+
148154
return app
149155

150156

api_tabular/utils.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import yaml
12
from aiohttp.web_request import Request
23

34
from api_tabular import config
@@ -59,3 +60,241 @@ def url_for(request: Request, route: str, *args, **kwargs):
5960
if kwargs.pop("_external", None):
6061
return external_url(router[route].url_for(**kwargs))
6162
return router[route].url_for(**kwargs)
63+
64+
65+
def swagger_parameters(resource_columns):
66+
parameters_list = [
67+
{
68+
'name': 'rid',
69+
'in': 'path',
70+
'description': 'ID of resource to return',
71+
'required': True,
72+
'schema': {
73+
'type': 'string'
74+
}
75+
},
76+
{
77+
'name': 'page',
78+
'in': 'query',
79+
'description': 'Specific page',
80+
'required': False,
81+
'schema': {
82+
'type': 'string'
83+
}
84+
},
85+
{
86+
'name': 'page_size',
87+
'in': 'query',
88+
'description': 'Number of results per page',
89+
'required': False,
90+
'schema': {
91+
'type': 'string'
92+
}
93+
}
94+
]
95+
for key, value in resource_columns.items():
96+
parameters_list.extend(
97+
[
98+
{
99+
'name': f'sort ascending {key}',
100+
'in': 'query',
101+
'description': f'{key}__sort=asc.',
102+
'required': False,
103+
'schema': {
104+
'type': 'string'
105+
}
106+
},
107+
{
108+
'name': f'sort descending {key}',
109+
'in': 'query',
110+
'description': f'{key}__sort=desc.',
111+
'required': False,
112+
'schema': {
113+
'type': 'string'
114+
}
115+
}
116+
]
117+
)
118+
if value['python_type'] == 'string':
119+
parameters_list.extend(
120+
[
121+
{
122+
'name': f'exact {key}',
123+
'in': 'query',
124+
'description': f'{key}__exact=value.',
125+
'required': False,
126+
'schema': {
127+
'type': 'string'
128+
}
129+
},
130+
{
131+
'name': f'contains {key}',
132+
'in': 'query',
133+
'description': f'{key}__contains=value.',
134+
'required': False,
135+
'schema': {
136+
'type': 'string'
137+
}
138+
}
139+
]
140+
)
141+
elif value['python_type'] == 'float':
142+
parameters_list.extend(
143+
[
144+
{
145+
'name': f'{key} less',
146+
'in': 'query',
147+
'description': f'{key}__less=value.',
148+
'required': False,
149+
'schema': {
150+
'type': 'string'
151+
}
152+
},
153+
{
154+
'name': f'{key} greater',
155+
'in': 'query',
156+
'description': f'{key}__greater=value.',
157+
'required': False,
158+
'schema': {
159+
'type': 'string'
160+
}
161+
}
162+
]
163+
)
164+
return parameters_list
165+
166+
167+
def swagger_component(resource_columns):
168+
resource_prop_dict = {}
169+
for key, value in resource_columns.items():
170+
type = 'string'
171+
if value['python_type'] == 'float':
172+
type = 'integer'
173+
resource_prop_dict.update({
174+
f'{key}': {
175+
'type': f'{type}'
176+
}
177+
})
178+
component_dict = {
179+
'schemas': {
180+
'ResourceData': {
181+
'type': 'object',
182+
'properties': {
183+
'data': {
184+
'type': 'array',
185+
'items': {
186+
'$ref': '#/components/schemas/Resource'
187+
}
188+
},
189+
'link': {
190+
'type': 'object',
191+
'properties': {
192+
'profile': {
193+
'description': 'Link to the profile endpoint of the resource',
194+
'type': 'string'
195+
},
196+
'next': {
197+
'description': 'Pagination link to the next page of the resource data',
198+
'type': 'string'
199+
},
200+
'prev': {
201+
'description': 'Pagination link to the previous page of the resource data',
202+
'type': 'string'
203+
}
204+
}
205+
},
206+
'meta': {
207+
'type': 'object',
208+
'properties': {
209+
'page': {
210+
'description': 'Current page',
211+
'type': 'integer'
212+
},
213+
'page_size': {
214+
'description': 'Number of results per page',
215+
'type': 'integer'
216+
},
217+
'total': {
218+
'description': 'Total number of results',
219+
'type': 'integer'
220+
}
221+
}
222+
}
223+
}
224+
},
225+
'Resource': {
226+
'type': 'object',
227+
'properties': resource_prop_dict
228+
}
229+
}
230+
}
231+
return component_dict
232+
233+
234+
def build_swagger_file(resource_columns, rid):
235+
parameters_list = swagger_parameters(resource_columns)
236+
component_dict = swagger_component(resource_columns)
237+
swagger_dict = {
238+
'openapi': '3.0.3',
239+
'info': {
240+
'title': 'Resource data API',
241+
'description': 'Retrieve data for a specified resource with optional filtering and sorting.',
242+
'version': '1.0.0'
243+
},
244+
'tags': {
245+
'name': 'Data retrieval',
246+
'description': 'Retrieve data for a specified resource'
247+
},
248+
'paths': {
249+
f'/api/resources/{rid}/data/': {
250+
'get': {
251+
'description': 'Returns resource data based on ID.',
252+
'summary': 'Find resource by ID',
253+
'operationId': 'getResourceById',
254+
'responses': {
255+
'200': {
256+
'description': 'successful operation',
257+
'content': {
258+
'application/json': {
259+
'schema': {
260+
'$ref': '#/components/schemas/ResourceData'
261+
}
262+
}
263+
}
264+
},
265+
'400': {
266+
'description': 'Invalid query string'
267+
},
268+
'404': {
269+
'description': 'Resource not found'
270+
}
271+
}
272+
},
273+
'parameters': parameters_list
274+
},
275+
f'/api/resources/{rid}/data/csv/': {
276+
'get': {
277+
'description': 'Returns resource data based on ID as a CSV file.',
278+
'summary': 'Find resource by ID in CSV',
279+
'operationId': 'getResourceByIdCSV',
280+
'responses': {
281+
'200': {
282+
'description': 'successful operation',
283+
'content': {
284+
'text/csv': {}
285+
}
286+
},
287+
'400': {
288+
'description': 'Invalid query string'
289+
},
290+
'404': {
291+
'description': 'Resource not found'
292+
}
293+
}
294+
},
295+
'parameters': parameters_list
296+
}
297+
},
298+
'components': component_dict
299+
}
300+
return yaml.dump(swagger_dict)

0 commit comments

Comments
 (0)