udata-hydra-csvapi 0.4.3.dev2__py3-none-any.whl → 0.4.3.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api_tabular/core/cors.py +33 -17
- api_tabular/core/query.py +1 -1
- api_tabular/core/swagger.py +4 -3
- api_tabular/core/utils.py +21 -2
- api_tabular/metrics/app.py +5 -7
- api_tabular/tabular/app.py +5 -7
- {udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/METADATA +3 -3
- {udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/RECORD +11 -11
- {udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/WHEEL +0 -0
- {udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
- {udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/top_level.txt +0 -0
api_tabular/core/cors.py
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
from aiohttp import web
|
|
2
|
+
from aiohttp.web_urldispatcher import MatchInfoError
|
|
3
|
+
|
|
4
|
+
CORS_HEADERS = {
|
|
5
|
+
# Using "*" ensures the Nginx cache remains consistent for all origins
|
|
6
|
+
"Access-Control-Allow-Origin": "*",
|
|
7
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
8
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
|
|
9
|
+
"Access-Control-Expose-Headers": "*",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _accepts_method(match_info: web.UrlMappingMatchInfo, method: str) -> bool:
|
|
14
|
+
# No route registers OPTIONS, so an existing path resolves to a 405 listing its allowed methods
|
|
15
|
+
return (
|
|
16
|
+
isinstance(match_info, MatchInfoError)
|
|
17
|
+
and isinstance(match_info.http_exception, web.HTTPMethodNotAllowed)
|
|
18
|
+
and method in match_info.http_exception.allowed_methods
|
|
19
|
+
)
|
|
2
20
|
|
|
3
21
|
|
|
4
22
|
@web.middleware
|
|
5
|
-
async def
|
|
6
|
-
"""
|
|
7
|
-
Middleware to handle CORS and the OPTIONS preflight.
|
|
8
|
-
"""
|
|
23
|
+
async def _preflight_middleware(request: web.Request, handler):
|
|
24
|
+
"""Answer an OPTIONS preflight with a 204, only for a route accepting the requested method."""
|
|
9
25
|
if request.method == "OPTIONS":
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
requested_method = request.headers.get("Access-Control-Request-Method", "GET")
|
|
27
|
+
if _accepts_method(request.match_info, requested_method):
|
|
28
|
+
return web.Response(status=204)
|
|
29
|
+
return await handler(request)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def add_cors_headers(request: web.Request, response: web.StreamResponse) -> None:
|
|
33
|
+
# on_response_prepare also fires for raised HTTP errors, unhandled 500s and streamed responses
|
|
34
|
+
response.headers.update(CORS_HEADERS)
|
|
14
35
|
|
|
15
|
-
# Add CORS headers to the response
|
|
16
|
-
# Using "*" ensures the Nginx cache remains consistent for all origins
|
|
17
|
-
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
18
|
-
response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
|
19
|
-
response.headers["Access-Control-Allow-Headers"] = (
|
|
20
|
-
"Content-Type, Authorization, X-Requested-With"
|
|
21
|
-
)
|
|
22
|
-
response.headers["Access-Control-Expose-Headers"] = "*"
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
def setup_cors(app: web.Application) -> None:
|
|
38
|
+
"""Enable CORS on an app: answer OPTIONS preflights and add CORS headers to every response."""
|
|
39
|
+
app.middlewares.append(_preflight_middleware)
|
|
40
|
+
app.on_response_prepare.append(add_cors_headers)
|
api_tabular/core/query.py
CHANGED
|
@@ -56,7 +56,7 @@ def build_sql_query_string(
|
|
|
56
56
|
agg_query += f'"{column[1:-1]}__{operator}":{column}.{operator}(),'
|
|
57
57
|
# we pop the trailing comma (it's always there, by construction)
|
|
58
58
|
sql_query.append(agg_query[:-1])
|
|
59
|
-
if page_size:
|
|
59
|
+
if page_size is not None:
|
|
60
60
|
sql_query.append(f"limit={page_size}")
|
|
61
61
|
if offset >= 1:
|
|
62
62
|
sql_query.append(f"offset={offset}")
|
api_tabular/core/swagger.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import cast
|
|
|
2
2
|
|
|
3
3
|
import yaml
|
|
4
4
|
|
|
5
|
+
from api_tabular import config
|
|
5
6
|
from api_tabular.core.utils import is_aggregation_allowed
|
|
6
7
|
|
|
7
8
|
TYPE_POSSIBILITIES = {
|
|
@@ -165,9 +166,9 @@ def swagger_parameters(resource_columns: dict, resource_id: str) -> list:
|
|
|
165
166
|
{
|
|
166
167
|
"name": "page",
|
|
167
168
|
"in": "query",
|
|
168
|
-
"description": "Specific page (page=value)",
|
|
169
|
+
"description": "Specific page, starting at 1 (page=value)",
|
|
169
170
|
"required": False,
|
|
170
|
-
"schema": {"type": "integer"},
|
|
171
|
+
"schema": {"type": "integer", "minimum": 1},
|
|
171
172
|
"example": 1,
|
|
172
173
|
},
|
|
173
174
|
{
|
|
@@ -175,7 +176,7 @@ def swagger_parameters(resource_columns: dict, resource_id: str) -> list:
|
|
|
175
176
|
"in": "query",
|
|
176
177
|
"description": "Number of results per page (page_size=value)",
|
|
177
178
|
"required": False,
|
|
178
|
-
"schema": {"type": "integer"},
|
|
179
|
+
"schema": {"type": "integer", "minimum": 1, "maximum": config.PAGE_SIZE_MAX},
|
|
179
180
|
"example": 20,
|
|
180
181
|
},
|
|
181
182
|
{
|
api_tabular/core/utils.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
1
3
|
from aiohttp.client import ClientResponse
|
|
2
4
|
from aiohttp.web_response import Response
|
|
3
5
|
|
|
@@ -19,7 +21,24 @@ def process_total(res: Response | ClientResponse) -> int:
|
|
|
19
21
|
return int(str_total)
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def
|
|
24
|
+
def _parse_int_param(query: Mapping[str, str], name: str, label: str, default: int) -> int:
|
|
25
|
+
raw = query.get(name)
|
|
26
|
+
if raw is None:
|
|
27
|
+
return default
|
|
28
|
+
try:
|
|
29
|
+
return int(raw)
|
|
30
|
+
except ValueError:
|
|
31
|
+
raise QueryException(400, None, "Invalid query string", f"{label} must be an integer")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_pagination(query: Mapping[str, str]) -> tuple[int, int, int]:
|
|
35
|
+
"""Return (page, page_size, offset) from the query, raising a 400 on invalid values"""
|
|
36
|
+
page = _parse_int_param(query, "page", "Page", 1)
|
|
37
|
+
page_size = _parse_int_param(query, "page_size", "Page size", config.PAGE_SIZE_DEFAULT)
|
|
38
|
+
if page < 1:
|
|
39
|
+
raise QueryException(400, None, "Invalid query string", "Page must be at least 1")
|
|
40
|
+
if page_size < 1:
|
|
41
|
+
raise QueryException(400, None, "Invalid query string", "Page size must be at least 1")
|
|
23
42
|
if page_size > config.PAGE_SIZE_MAX:
|
|
24
43
|
raise QueryException(
|
|
25
44
|
400,
|
|
@@ -27,4 +46,4 @@ def build_offset(page: int, page_size: int) -> int:
|
|
|
27
46
|
"Invalid query string",
|
|
28
47
|
f"Page size exceeds allowed maximum: {config.PAGE_SIZE_MAX}",
|
|
29
48
|
)
|
|
30
|
-
return page_size * (page - 1)
|
|
49
|
+
return page, page_size, page_size * (page - 1)
|
api_tabular/metrics/app.py
CHANGED
|
@@ -7,14 +7,14 @@ from aiohttp import ClientSession, web
|
|
|
7
7
|
from aiohttp_swagger import setup_swagger
|
|
8
8
|
|
|
9
9
|
from api_tabular import config
|
|
10
|
-
from api_tabular.core.cors import
|
|
10
|
+
from api_tabular.core.cors import setup_cors
|
|
11
11
|
from api_tabular.core.data import stream_data
|
|
12
12
|
from api_tabular.core.error import QueryException, handle_exception
|
|
13
13
|
from api_tabular.core.health import check_health
|
|
14
14
|
from api_tabular.core.query import build_sql_query_string
|
|
15
15
|
from api_tabular.core.sentry import get_sentry_kwargs
|
|
16
16
|
from api_tabular.core.url import build_link_with_page
|
|
17
|
-
from api_tabular.core.utils import
|
|
17
|
+
from api_tabular.core.utils import parse_pagination, process_total
|
|
18
18
|
from api_tabular.core.version import get_app_version
|
|
19
19
|
|
|
20
20
|
routes = web.RouteTableDef()
|
|
@@ -40,10 +40,7 @@ async def metrics_data(request):
|
|
|
40
40
|
"""
|
|
41
41
|
model = request.match_info["model"]
|
|
42
42
|
query_string = request.query_string.split("&") if request.query_string else []
|
|
43
|
-
page =
|
|
44
|
-
page_size = int(request.query.get("page_size", config.PAGE_SIZE_DEFAULT))
|
|
45
|
-
|
|
46
|
-
offset = build_offset(page, page_size)
|
|
43
|
+
page, page_size, offset = parse_pagination(request.query)
|
|
47
44
|
try:
|
|
48
45
|
sql_query = build_sql_query_string(query_string, page_size=page_size, offset=offset)
|
|
49
46
|
except ValueError as e:
|
|
@@ -111,7 +108,8 @@ async def app_factory():
|
|
|
111
108
|
async def on_cleanup(app):
|
|
112
109
|
await app["csession"].close()
|
|
113
110
|
|
|
114
|
-
app = web.Application(
|
|
111
|
+
app = web.Application()
|
|
112
|
+
setup_cors(app)
|
|
115
113
|
app.add_routes(routes)
|
|
116
114
|
app.on_startup.append(on_startup)
|
|
117
115
|
app.on_cleanup.append(on_cleanup)
|
api_tabular/tabular/app.py
CHANGED
|
@@ -7,12 +7,12 @@ from aiohttp import ClientSession, web
|
|
|
7
7
|
from aiohttp_swagger import setup_swagger
|
|
8
8
|
|
|
9
9
|
from api_tabular import config
|
|
10
|
-
from api_tabular.core.cors import
|
|
10
|
+
from api_tabular.core.cors import setup_cors
|
|
11
11
|
from api_tabular.core.health import check_health
|
|
12
12
|
from api_tabular.core.sentry import get_sentry_kwargs
|
|
13
13
|
from api_tabular.core.swagger import build_swagger_file
|
|
14
14
|
from api_tabular.core.url import build_link_with_page, url_for
|
|
15
|
-
from api_tabular.core.utils import
|
|
15
|
+
from api_tabular.core.utils import parse_pagination
|
|
16
16
|
from api_tabular.core.version import get_app_version
|
|
17
17
|
from api_tabular.tabular.utils import (
|
|
18
18
|
get_potential_indexes,
|
|
@@ -97,10 +97,7 @@ def build_next_page(
|
|
|
97
97
|
async def resource_data(request):
|
|
98
98
|
resource_id = request.match_info["rid"]
|
|
99
99
|
query_string = request.query_string.split("&") if request.query_string else []
|
|
100
|
-
page =
|
|
101
|
-
page_size = int(request.query.get("page_size", config.PAGE_SIZE_DEFAULT))
|
|
102
|
-
|
|
103
|
-
offset = build_offset(page, page_size)
|
|
100
|
+
page, page_size, offset = parse_pagination(request.query)
|
|
104
101
|
|
|
105
102
|
sql_query = await try_build_query(request, query_string, resource_id, page_size, offset)
|
|
106
103
|
resource = await get_resource(request.app["csession"], resource_id, ["parsing_table"])
|
|
@@ -176,7 +173,8 @@ async def app_factory():
|
|
|
176
173
|
async def on_cleanup(app):
|
|
177
174
|
await app["csession"].close()
|
|
178
175
|
|
|
179
|
-
app = web.Application(
|
|
176
|
+
app = web.Application()
|
|
177
|
+
setup_cors(app)
|
|
180
178
|
app.add_routes(routes)
|
|
181
179
|
app.on_startup.append(on_startup)
|
|
182
180
|
app.on_cleanup.append(on_cleanup)
|
{udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: udata-hydra-csvapi
|
|
3
|
-
Version: 0.4.3.
|
|
3
|
+
Version: 0.4.3.dev4
|
|
4
4
|
Summary: API for CSV converted by udata-hydra
|
|
5
5
|
Author-email: "data.gouv.fr" <opendatateam@data.gouv.fr>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -365,8 +365,8 @@ column_name__sum
|
|
|
365
365
|
|
|
366
366
|
#### Pagination
|
|
367
367
|
```
|
|
368
|
-
page=1 # Page number (default: 1)
|
|
369
|
-
page_size=20 # Items per page (default: 20, max: 50)
|
|
368
|
+
page=1 # Page number (default: 1, min: 1)
|
|
369
|
+
page_size=20 # Items per page (default: 20, min: 1, max: 50)
|
|
370
370
|
```
|
|
371
371
|
|
|
372
372
|
#### Column Selection
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
api_tabular/__init__.py,sha256=sYG6CMz-twob-QLyfsPYRc6cWDtqWXvq_N5Dxk-U5qA,2263
|
|
2
2
|
api_tabular/config_default.toml,sha256=cX506NdqHW7gK_2aZjzLiz0Z4gMSl1tU_tI4i4BvuXs,421
|
|
3
3
|
api_tabular/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
api_tabular/core/cors.py,sha256=
|
|
4
|
+
api_tabular/core/cors.py,sha256=xX4qSbv06pVT-NrohQlysFPSR9z7Pj2THz18TCbvJ90,1707
|
|
5
5
|
api_tabular/core/data.py,sha256=5j-3d_el7LMoHpn6V__UdhYHD6GiJbwdH67jrnOYWZQ,1459
|
|
6
6
|
api_tabular/core/error.py,sha256=xRt6fkQfsiupv0tWWzdX0DJjAKbw1n0tRxFyt15VBLA,1062
|
|
7
7
|
api_tabular/core/health.py,sha256=NeFT1C00onvm3CbgesFAcN1N8bmelbQdEYFbmmftICQ,647
|
|
8
|
-
api_tabular/core/query.py,sha256=
|
|
8
|
+
api_tabular/core/query.py,sha256=J7woFLDzA8RpMBXJKpKm8olDdZsp2_TQEROJtIaeokM,10238
|
|
9
9
|
api_tabular/core/sentry.py,sha256=fN5MrDZszLnzLXF3b_nUymPVB0ffNn4PI7xS1GtJNyE,948
|
|
10
|
-
api_tabular/core/swagger.py,sha256=
|
|
10
|
+
api_tabular/core/swagger.py,sha256=mMEESM4UlgK-voyRTKrddVuUrlUMfTDOQtK1VrVp7Ak,14748
|
|
11
11
|
api_tabular/core/url.py,sha256=af7VKfrMtKMzlHrGgtLDLuRBim4qFv7xlt2PYxAyW2g,761
|
|
12
|
-
api_tabular/core/utils.py,sha256=
|
|
12
|
+
api_tabular/core/utils.py,sha256=Ghd9Un0HugFg4oEcEb5yB5FEop7tDSoX4OCzR8mcM-Y,1922
|
|
13
13
|
api_tabular/core/version.py,sha256=OKLg0l1CpDRHV2hsqVgtSihoo3tg0aMG6kzMILtQ-bY,250
|
|
14
14
|
api_tabular/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
api_tabular/metrics/app.py,sha256=
|
|
15
|
+
api_tabular/metrics/app.py,sha256=W2htxUO54bTfQLReJhEY3JtsbLWaefC71DmjV9SvUII,4291
|
|
16
16
|
api_tabular/tabular/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
api_tabular/tabular/app.py,sha256=
|
|
17
|
+
api_tabular/tabular/app.py,sha256=6lwujb2Pf_70QQlTKL8bhQTnhYinRyDlF_yTdit0pQw,6528
|
|
18
18
|
api_tabular/tabular/utils.py,sha256=BL2nl04-3imBQG-RZiYvVMjxoBORi4lwrKq8NiWecLY,4487
|
|
19
|
-
udata_hydra_csvapi-0.4.3.
|
|
20
|
-
udata_hydra_csvapi-0.4.3.
|
|
21
|
-
udata_hydra_csvapi-0.4.3.
|
|
22
|
-
udata_hydra_csvapi-0.4.3.
|
|
23
|
-
udata_hydra_csvapi-0.4.3.
|
|
19
|
+
udata_hydra_csvapi-0.4.3.dev4.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
|
|
20
|
+
udata_hydra_csvapi-0.4.3.dev4.dist-info/METADATA,sha256=h571GAbAb6UrNwO_O4SZbMMmvpId-1FdiVM0s_NRyw8,21835
|
|
21
|
+
udata_hydra_csvapi-0.4.3.dev4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
22
|
+
udata_hydra_csvapi-0.4.3.dev4.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
|
|
23
|
+
udata_hydra_csvapi-0.4.3.dev4.dist-info/RECORD,,
|
|
File without changes
|
{udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{udata_hydra_csvapi-0.4.3.dev2.dist-info → udata_hydra_csvapi-0.4.3.dev4.dist-info}/top_level.txt
RENAMED
|
File without changes
|