udata-hydra-csvapi 0.4.3.dev3__py3-none-any.whl → 0.4.3.dev5__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 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 cors_middleware(request, handler):
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
- # Handle cases of preflight OPTIONS requests (was managed by aiohttp_cors before)
11
- response = web.Response(status=204)
12
- else:
13
- response = await handler(request)
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
- return response
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
@@ -163,35 +163,20 @@ def split_top_level(s: str) -> list[str]:
163
163
 
164
164
 
165
165
  def find_arg_val(param: str) -> tuple[str, str]:
166
- if param.count('"') not in {0, 2, 4}:
167
- raise ValueError(f"argument '{param}' could not be parsed")
168
- column_operator_pattern = r'^"[^"]*"__[a-z]+'
169
- value_pattern = r'\."[^"]*"$'
170
- if param.count('"') == 0:
171
- # no quote, we expect col__op.val
172
- _split = param.split(".")
173
- if len(_split) != 2:
174
- raise ValueError(f"argument '{param}' could not be parsed")
175
- return tuple[str, str](_split) # ty: ignore[invalid-argument-type]
176
- elif param.count('"') == 4:
177
- # 4 quotes, we expect "col.umn"__op."val.ue"
178
- col_op = re.findall(column_operator_pattern, param)
179
- val = re.findall(value_pattern, param)
180
- if len(col_op) != 1 or len(val) != 1:
181
- raise ValueError(f"argument '{param}' could not be parsed")
166
+ # the operator is not checked here, add_filter validates it like for unquoted columns
167
+ quoted_column = re.match(r'^"([^"]*)"__', param)
168
+ if quoted_column:
182
169
  # we drop the double quotes around the column as they will be added back in get_column_and_operator
183
- # the dot is included in the value result, we pop it in the result
184
- return col_op[0].replace('"', ""), val[0][1:]
170
+ column_prefix = f"{quoted_column.group(1)}__"
171
+ operator_value = param[quoted_column.end() :]
185
172
  else:
186
- # we have any of "col.umn"__op.val, col__op."val.ue"
187
- col_op = re.findall(column_operator_pattern, param)
188
- val = re.findall(value_pattern, param)
189
- if not col_op:
190
- # case col__op."val.ue"
191
- return param.split(".")[0], val[0][1:]
192
- else:
193
- # case "col.umn"__op.val
194
- return col_op[0].replace('"', ""), param.split(".")[-1]
173
+ column_prefix, operator_value = "", param
174
+ operator, dot, value = operator_value.partition(".")
175
+ # a quoted value is kept as is, an unquoted one can't contain a dot
176
+ value_is_valid = re.fullmatch(r'"[^"]*"', value) or not re.search(r'[".]', value)
177
+ if not dot or '"' in operator or not value_is_valid:
178
+ raise ValueError(f"argument '{param}' could not be parsed")
179
+ return column_prefix + operator, value
195
180
 
196
181
 
197
182
  def parse_operator(query: str, operator: str, top_level: bool = False):
@@ -7,7 +7,7 @@ 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 cors_middleware
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
@@ -108,7 +108,8 @@ async def app_factory():
108
108
  async def on_cleanup(app):
109
109
  await app["csession"].close()
110
110
 
111
- app = web.Application(middlewares=[cors_middleware])
111
+ app = web.Application()
112
+ setup_cors(app)
112
113
  app.add_routes(routes)
113
114
  app.on_startup.append(on_startup)
114
115
  app.on_cleanup.append(on_cleanup)
@@ -7,7 +7,7 @@ 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 cors_middleware
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
@@ -173,7 +173,8 @@ async def app_factory():
173
173
  async def on_cleanup(app):
174
174
  await app["csession"].close()
175
175
 
176
- app = web.Application(middlewares=[cors_middleware])
176
+ app = web.Application()
177
+ setup_cors(app)
177
178
  app.add_routes(routes)
178
179
  app.on_startup.append(on_startup)
179
180
  app.on_cleanup.append(on_cleanup)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: udata-hydra-csvapi
3
- Version: 0.4.3.dev3
3
+ Version: 0.4.3.dev5
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
@@ -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=QY006pAnR1fMBMJJTAeRGcopL_4LkY7YoA3y8KYk4dI,825
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=J7woFLDzA8RpMBXJKpKm8olDdZsp2_TQEROJtIaeokM,10238
8
+ api_tabular/core/query.py,sha256=Qf34vtzt_kPJBYIrHX2QsV-K3ADrbDeLeb8xC__cs-k,9616
9
9
  api_tabular/core/sentry.py,sha256=fN5MrDZszLnzLXF3b_nUymPVB0ffNn4PI7xS1GtJNyE,948
10
10
  api_tabular/core/swagger.py,sha256=mMEESM4UlgK-voyRTKrddVuUrlUMfTDOQtK1VrVp7Ak,14748
11
11
  api_tabular/core/url.py,sha256=af7VKfrMtKMzlHrGgtLDLuRBim4qFv7xlt2PYxAyW2g,761
12
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=m2H4qOvzvi8oDjCg1R23JAK6VaqDcrqYCz6ScIk9iAc,4305
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=fBnbIR2H3jm0suVVEfNuVdSccOwZeMKH__iekwmDVa8,6542
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.dev3.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
20
- udata_hydra_csvapi-0.4.3.dev3.dist-info/METADATA,sha256=mBjWOUsIDJAxQWABC7L49YdknGyNsX2tKDG3Sh7spTk,21835
21
- udata_hydra_csvapi-0.4.3.dev3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
22
- udata_hydra_csvapi-0.4.3.dev3.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
23
- udata_hydra_csvapi-0.4.3.dev3.dist-info/RECORD,,
19
+ udata_hydra_csvapi-0.4.3.dev5.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
20
+ udata_hydra_csvapi-0.4.3.dev5.dist-info/METADATA,sha256=2VEJANlzoHtKJt5iU3MCEw1H-jPyirRWHCreKOTFDHg,21835
21
+ udata_hydra_csvapi-0.4.3.dev5.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
22
+ udata_hydra_csvapi-0.4.3.dev5.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
23
+ udata_hydra_csvapi-0.4.3.dev5.dist-info/RECORD,,