udata-hydra-csvapi 0.3.2.dev8__py3-none-any.whl → 0.3.2.dev10__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/__init__.py +1 -3
- api_tabular/config_default.toml +2 -1
- api_tabular/core/cors.py +24 -0
- api_tabular/core/health.py +5 -6
- api_tabular/core/query.py +116 -13
- api_tabular/core/swagger.py +7 -0
- api_tabular/core/utils.py +1 -1
- api_tabular/metrics/app.py +3 -14
- api_tabular/tabular/app.py +8 -15
- {udata_hydra_csvapi-0.3.2.dev8.dist-info → udata_hydra_csvapi-0.3.2.dev10.dist-info}/METADATA +22 -4
- udata_hydra_csvapi-0.3.2.dev10.dist-info/RECORD +23 -0
- udata_hydra_csvapi-0.3.2.dev8.dist-info/RECORD +0 -22
- {udata_hydra_csvapi-0.3.2.dev8.dist-info → udata_hydra_csvapi-0.3.2.dev10.dist-info}/WHEEL +0 -0
- {udata_hydra_csvapi-0.3.2.dev8.dist-info → udata_hydra_csvapi-0.3.2.dev10.dist-info}/licenses/LICENSE +0 -0
- {udata_hydra_csvapi-0.3.2.dev8.dist-info → udata_hydra_csvapi-0.3.2.dev10.dist-info}/top_level.txt +0 -0
api_tabular/__init__.py
CHANGED
|
@@ -27,9 +27,7 @@ class Configurator:
|
|
|
27
27
|
# override with os env settings
|
|
28
28
|
for config_key in configuration:
|
|
29
29
|
if config_key in os.environ:
|
|
30
|
-
value = os.
|
|
31
|
-
if value is None:
|
|
32
|
-
continue
|
|
30
|
+
value = os.environ[config_key]
|
|
33
31
|
# Casting env value
|
|
34
32
|
if isinstance(configuration[config_key], list):
|
|
35
33
|
value = value.split(",")
|
api_tabular/config_default.toml
CHANGED
|
@@ -7,7 +7,8 @@ PAGE_SIZE_DEFAULT = 20
|
|
|
7
7
|
PAGE_SIZE_MAX = 50
|
|
8
8
|
BATCH_SIZE = 50000
|
|
9
9
|
DOC_PATH = "/api/doc"
|
|
10
|
-
ALLOW_AGGREGATION =
|
|
10
|
+
ALLOW_AGGREGATION = false
|
|
11
|
+
ALLOW_AGGREGATION_EXCEPTIONS = [
|
|
11
12
|
"dddddddd-7777-eeee-8888-ffffffffffff", # without indexes
|
|
12
13
|
"aaaaaaaa-9999-bbbb-1010-cccccccccccc", # with indexes
|
|
13
14
|
] # list of resource_ids
|
api_tabular/core/cors.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from aiohttp import web
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@web.middleware
|
|
5
|
+
async def cors_middleware(request, handler):
|
|
6
|
+
"""
|
|
7
|
+
Middleware to handle CORS and the OPTIONS preflight.
|
|
8
|
+
"""
|
|
9
|
+
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)
|
|
14
|
+
|
|
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
|
+
|
|
24
|
+
return response
|
api_tabular/core/health.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
2
|
-
|
|
3
1
|
from aiohttp import web
|
|
4
2
|
from aiohttp.web_request import Request
|
|
5
3
|
|
|
@@ -15,9 +13,10 @@ async def check_health(request: Request, url: str):
|
|
|
15
13
|
"DB unavailable",
|
|
16
14
|
"postgREST has not started yet",
|
|
17
15
|
)
|
|
18
|
-
start_time = request.app["start_time"]
|
|
19
|
-
current_time = datetime.now(timezone.utc)
|
|
20
|
-
uptime_seconds = (current_time - start_time).total_seconds()
|
|
21
16
|
return web.json_response(
|
|
22
|
-
{
|
|
17
|
+
{
|
|
18
|
+
"status": "ok",
|
|
19
|
+
"version": request.app["app_version"],
|
|
20
|
+
"uptime_since": request.app["started_at"].isoformat(),
|
|
21
|
+
}
|
|
23
22
|
)
|
api_tabular/core/query.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from collections import defaultdict
|
|
2
3
|
|
|
3
4
|
from api_tabular.core.utils import is_aggregation_allowed
|
|
@@ -14,6 +15,10 @@ def build_sql_query_string(
|
|
|
14
15
|
aggregators = defaultdict(list)
|
|
15
16
|
sorted = False
|
|
16
17
|
for arg in request_arg:
|
|
18
|
+
if arg.startswith("or=("):
|
|
19
|
+
# top level "and=(...)" should not happen
|
|
20
|
+
sql_query.append(parse_operator(query=arg, operator="or", top_level=True))
|
|
21
|
+
continue
|
|
17
22
|
_split = arg.split("=")
|
|
18
23
|
# filters are expected to have the syntax `<column_name>__<operator>=<value>`
|
|
19
24
|
if len(_split) == 2:
|
|
@@ -72,39 +77,52 @@ def get_column_and_operator(argument: str) -> tuple[str, str]:
|
|
|
72
77
|
return column, normalized_comparator
|
|
73
78
|
|
|
74
79
|
|
|
75
|
-
def add_filter(
|
|
80
|
+
def add_filter(
|
|
81
|
+
argument: str,
|
|
82
|
+
value: str | None,
|
|
83
|
+
*,
|
|
84
|
+
in_operator: bool = False,
|
|
85
|
+
) -> tuple[str | None, bool]:
|
|
76
86
|
if argument in ["page", "page_size"]: # processed differently
|
|
87
|
+
if in_operator:
|
|
88
|
+
raise ValueError(f"Argument `{argument}` can't be set within an operator")
|
|
77
89
|
return None, False
|
|
78
90
|
if argument == "columns":
|
|
91
|
+
if in_operator:
|
|
92
|
+
raise ValueError(f"Argument `{argument}` can't be set within an operator")
|
|
79
93
|
return f"select={value}", False
|
|
80
94
|
if "__" in argument:
|
|
81
95
|
column, normalized_comparator = get_column_and_operator(argument)
|
|
96
|
+
# when encapsulated in an OR statement, the syntax is `col.eq.val` instead of `col=eq.val`
|
|
97
|
+
op = "." if in_operator else "="
|
|
82
98
|
if normalized_comparator == "sort":
|
|
99
|
+
if in_operator:
|
|
100
|
+
raise ValueError(f"Argument `{argument}` can't be set within an operator")
|
|
83
101
|
return f"order={column}.{value}", True
|
|
84
102
|
elif normalized_comparator == "exact":
|
|
85
|
-
return f"{column}
|
|
103
|
+
return f"{column}{op}eq.{value}", False
|
|
86
104
|
elif normalized_comparator == "differs":
|
|
87
|
-
return f"{column}
|
|
105
|
+
return f"{column}{op}isdistinct.{value}", False
|
|
88
106
|
elif normalized_comparator == "isnull":
|
|
89
|
-
return f"{column}
|
|
107
|
+
return f"{column}{op}is.null", False
|
|
90
108
|
elif normalized_comparator == "isnotnull":
|
|
91
|
-
return f"{column}
|
|
109
|
+
return f"{column}{op}not.is.null", False
|
|
92
110
|
elif normalized_comparator == "contains":
|
|
93
|
-
return f"{column}
|
|
111
|
+
return f"{column}{op}ilike.*{value}*", False
|
|
94
112
|
elif normalized_comparator == "notcontains":
|
|
95
|
-
return f"{column}
|
|
113
|
+
return f"{column}{op}not.ilike.*{value}*", False
|
|
96
114
|
elif normalized_comparator == "in":
|
|
97
|
-
return f"{column}
|
|
115
|
+
return f"{column}{op}in.({value})", False
|
|
98
116
|
elif normalized_comparator == "notin":
|
|
99
|
-
return f"{column}
|
|
117
|
+
return f"{column}{op}not.in.({value})", False
|
|
100
118
|
elif normalized_comparator == "less":
|
|
101
|
-
return f"{column}
|
|
119
|
+
return f"{column}{op}lte.{value}", False
|
|
102
120
|
elif normalized_comparator == "greater":
|
|
103
|
-
return f"{column}
|
|
121
|
+
return f"{column}{op}gte.{value}", False
|
|
104
122
|
elif normalized_comparator == "strictly_less":
|
|
105
|
-
return f"{column}
|
|
123
|
+
return f"{column}{op}lt.{value}", False
|
|
106
124
|
elif normalized_comparator == "strictly_greater":
|
|
107
|
-
return f"{column}
|
|
125
|
+
return f"{column}{op}gt.{value}", False
|
|
108
126
|
raise ValueError(f"argument '{argument}={value}' could not be parsed")
|
|
109
127
|
|
|
110
128
|
|
|
@@ -118,6 +136,91 @@ def add_aggregator(argument: str, indexes: set | None) -> tuple[str, str]:
|
|
|
118
136
|
raise ValueError(f"argument '{argument}' could not be parsed")
|
|
119
137
|
|
|
120
138
|
|
|
139
|
+
def split_top_level(s: str) -> list[str]:
|
|
140
|
+
# we can't .split(",") as there may be commas within the params (if nested)
|
|
141
|
+
# so we need a custom "split by ',' if ',' not within parentheses"
|
|
142
|
+
parts = []
|
|
143
|
+
current = ""
|
|
144
|
+
depth = 0
|
|
145
|
+
for char in s:
|
|
146
|
+
if char == "(":
|
|
147
|
+
depth += 1
|
|
148
|
+
current += char
|
|
149
|
+
elif char == ")":
|
|
150
|
+
depth -= 1
|
|
151
|
+
current += char
|
|
152
|
+
elif char == "," and depth == 0:
|
|
153
|
+
parts.append(current)
|
|
154
|
+
current = ""
|
|
155
|
+
else:
|
|
156
|
+
current += char
|
|
157
|
+
if current:
|
|
158
|
+
parts.append(current)
|
|
159
|
+
return parts
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def find_arg_val(param: str) -> tuple[str, str]:
|
|
163
|
+
if param.count('"') not in {0, 2, 4}:
|
|
164
|
+
raise ValueError(f"argument '{param}' could not be parsed")
|
|
165
|
+
column_operator_pattern = r'^"[^"]*"__[a-z]+'
|
|
166
|
+
value_pattern = r'\."[^"]*"$'
|
|
167
|
+
if param.count('"') == 0:
|
|
168
|
+
# no quote, we expect col__op.val
|
|
169
|
+
_split = param.split(".")
|
|
170
|
+
if len(_split) != 2:
|
|
171
|
+
raise ValueError(f"argument '{param}' could not be parsed")
|
|
172
|
+
return tuple[str, str](_split) # ty: ignore[invalid-argument-type]
|
|
173
|
+
elif param.count('"') == 4:
|
|
174
|
+
# 4 quotes, we expect "col.umn"__op."val.ue"
|
|
175
|
+
col_op = re.findall(column_operator_pattern, param)
|
|
176
|
+
val = re.findall(value_pattern, param)
|
|
177
|
+
if len(col_op) != 1 or len(val) != 1:
|
|
178
|
+
raise ValueError(f"argument '{param}' could not be parsed")
|
|
179
|
+
# we drop the double quotes around the column as they will be added back in get_column_and_operator
|
|
180
|
+
# the dot is included in the value result, we pop it in the result
|
|
181
|
+
return col_op[0].replace('"', ""), val[0][1:]
|
|
182
|
+
else:
|
|
183
|
+
# we have any of "col.umn"__op.val, col__op."val.ue"
|
|
184
|
+
col_op = re.findall(column_operator_pattern, param)
|
|
185
|
+
val = re.findall(value_pattern, param)
|
|
186
|
+
if not col_op:
|
|
187
|
+
# case col__op."val.ue"
|
|
188
|
+
return param.split(".")[0], val[0][1:]
|
|
189
|
+
else:
|
|
190
|
+
# case "col.umn"__op.val
|
|
191
|
+
return col_op[0].replace('"', ""), param.split(".")[-1]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_operator(query: str, operator: str, top_level: bool = False):
|
|
195
|
+
# only the top level operator has an "=", nested operators are "or(...)" and "and(...)"
|
|
196
|
+
if not query.endswith(")"):
|
|
197
|
+
raise ValueError(f"argument '{query}' could not be parsed")
|
|
198
|
+
postgrest_params = []
|
|
199
|
+
# we can safely assume that there will be one result for the regex
|
|
200
|
+
params = split_top_level(
|
|
201
|
+
re.findall(rf"^{operator}{'=' if top_level else ''}\((.*)\)$", query)[0]
|
|
202
|
+
)
|
|
203
|
+
for param in params:
|
|
204
|
+
if param.startswith(("and(", "or(")):
|
|
205
|
+
# recursively adding the nested conditions
|
|
206
|
+
postgrest_params.append(parse_operator(query=param, operator=param.split("(")[0]))
|
|
207
|
+
else:
|
|
208
|
+
if param.endswith(("__isnull", "__isnotnull")):
|
|
209
|
+
postgrest_params.append(
|
|
210
|
+
add_filter(
|
|
211
|
+
param.replace('"', ""),
|
|
212
|
+
None,
|
|
213
|
+
in_operator=True,
|
|
214
|
+
)[0]
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
# if a special character is in the column name and/or value, the problematic item should be encapsulated in double quotes
|
|
218
|
+
# so we have 4 possibles cases: col__op.val, "col.umn"__op.val, col__op."val.ue" and "col.umn"__op."val.ue"
|
|
219
|
+
argument, value = find_arg_val(param)
|
|
220
|
+
postgrest_params.append(add_filter(argument, value, in_operator=True)[0])
|
|
221
|
+
return f"{operator}{'=' if top_level else ''}({','.join(postgrest_params)})"
|
|
222
|
+
|
|
223
|
+
|
|
121
224
|
def raise_if_not_index(column_name: str, indexes: set | None) -> None:
|
|
122
225
|
if indexes is None:
|
|
123
226
|
return
|
api_tabular/core/swagger.py
CHANGED
|
@@ -188,6 +188,13 @@ def swagger_parameters(resource_columns: dict, resource_id: str) -> list:
|
|
|
188
188
|
"style": "form",
|
|
189
189
|
"explode": False,
|
|
190
190
|
},
|
|
191
|
+
{
|
|
192
|
+
"name": "or",
|
|
193
|
+
"in": "query",
|
|
194
|
+
"description": "Encapsulates filters to be joined with the `OR` operator. Allows to build complex queries with nested `or(...)` and `and(...)` operators. Inner filters syntax must be `<column>.<operation>.<value>` (or `<column>.<operation>` for `is(not)null`), separated with commas.",
|
|
195
|
+
"required": False,
|
|
196
|
+
"schema": {"type": "string"},
|
|
197
|
+
},
|
|
191
198
|
]
|
|
192
199
|
# expected python types are: string, float, int, bool, date, datetime, json
|
|
193
200
|
# see cast for db here: https://github.com/datagouv/csv-detective/blob/master/csv_detective/output/dataframe.py
|
api_tabular/core/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ from api_tabular.core.error import QueryException
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def is_aggregation_allowed(resource_id: str) -> bool:
|
|
9
|
-
return resource_id in config.
|
|
9
|
+
return config.ALLOW_AGGREGATION or resource_id in config.ALLOW_AGGREGATION_EXCEPTIONS
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def process_total(res: Response | ClientResponse) -> int:
|
api_tabular/metrics/app.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
|
-
import aiohttp_cors
|
|
5
4
|
import sentry_sdk
|
|
6
5
|
import yaml
|
|
7
6
|
from aiohttp import ClientSession, web
|
|
8
7
|
from aiohttp_swagger import setup_swagger
|
|
9
8
|
|
|
10
9
|
from api_tabular import config
|
|
10
|
+
from api_tabular.core.cors import cors_middleware
|
|
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
|
|
@@ -99,7 +99,7 @@ async def get_health(request):
|
|
|
99
99
|
async def app_factory():
|
|
100
100
|
async def on_startup(app):
|
|
101
101
|
app["csession"] = ClientSession()
|
|
102
|
-
app["
|
|
102
|
+
app["started_at"] = datetime.now(timezone.utc)
|
|
103
103
|
app["app_version"] = await get_app_version()
|
|
104
104
|
|
|
105
105
|
with open("metrics_swagger.yaml", "r") as f:
|
|
@@ -111,22 +111,11 @@ async def app_factory():
|
|
|
111
111
|
async def on_cleanup(app):
|
|
112
112
|
await app["csession"].close()
|
|
113
113
|
|
|
114
|
-
app = web.Application()
|
|
114
|
+
app = web.Application(middlewares=[cors_middleware])
|
|
115
115
|
app.add_routes(routes)
|
|
116
116
|
app.on_startup.append(on_startup)
|
|
117
117
|
app.on_cleanup.append(on_cleanup)
|
|
118
118
|
|
|
119
|
-
cors = aiohttp_cors.setup(
|
|
120
|
-
app,
|
|
121
|
-
defaults={
|
|
122
|
-
"*": aiohttp_cors.ResourceOptions(
|
|
123
|
-
allow_credentials=True, expose_headers="*", allow_headers="*"
|
|
124
|
-
)
|
|
125
|
-
},
|
|
126
|
-
)
|
|
127
|
-
for route in list(app.router.routes()):
|
|
128
|
-
cors.add(route)
|
|
129
|
-
|
|
130
119
|
return app
|
|
131
120
|
|
|
132
121
|
|
api_tabular/tabular/app.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
|
-
import aiohttp_cors
|
|
5
4
|
import sentry_sdk
|
|
6
5
|
import yaml
|
|
7
6
|
from aiohttp import ClientSession, web
|
|
8
7
|
from aiohttp_swagger import setup_swagger
|
|
9
8
|
|
|
10
9
|
from api_tabular import config
|
|
10
|
+
from api_tabular.core.cors import cors_middleware
|
|
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
|
|
@@ -149,13 +149,17 @@ async def get_health(request):
|
|
|
149
149
|
@routes.get(r"/api/aggregation-exceptions/")
|
|
150
150
|
async def get_aggregation_exceptions(request):
|
|
151
151
|
"""Return the list of resources for which aggregation queries are allowed"""
|
|
152
|
-
|
|
152
|
+
body = {
|
|
153
|
+
"allowed": config.ALLOW_AGGREGATION,
|
|
154
|
+
"exceptions": config.ALLOW_AGGREGATION_EXCEPTIONS,
|
|
155
|
+
}
|
|
156
|
+
return web.json_response(body)
|
|
153
157
|
|
|
154
158
|
|
|
155
159
|
async def app_factory():
|
|
156
160
|
async def on_startup(app):
|
|
157
161
|
app["csession"] = ClientSession()
|
|
158
|
-
app["
|
|
162
|
+
app["started_at"] = datetime.now(timezone.utc)
|
|
159
163
|
app["app_version"] = await get_app_version()
|
|
160
164
|
|
|
161
165
|
with open("tabular_swagger.yaml", "r") as f:
|
|
@@ -172,22 +176,11 @@ async def app_factory():
|
|
|
172
176
|
async def on_cleanup(app):
|
|
173
177
|
await app["csession"].close()
|
|
174
178
|
|
|
175
|
-
app = web.Application()
|
|
179
|
+
app = web.Application(middlewares=[cors_middleware])
|
|
176
180
|
app.add_routes(routes)
|
|
177
181
|
app.on_startup.append(on_startup)
|
|
178
182
|
app.on_cleanup.append(on_cleanup)
|
|
179
183
|
|
|
180
|
-
cors = aiohttp_cors.setup(
|
|
181
|
-
app,
|
|
182
|
-
defaults={
|
|
183
|
-
"*": aiohttp_cors.ResourceOptions(
|
|
184
|
-
allow_credentials=True, expose_headers="*", allow_headers="*"
|
|
185
|
-
)
|
|
186
|
-
},
|
|
187
|
-
)
|
|
188
|
-
for route in list(app.router.routes()):
|
|
189
|
-
cors.add(route)
|
|
190
|
-
|
|
191
184
|
return app
|
|
192
185
|
|
|
193
186
|
|
{udata_hydra_csvapi-0.3.2.dev8.dist-info → udata_hydra_csvapi-0.3.2.dev10.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: udata-hydra-csvapi
|
|
3
|
-
Version: 0.3.2.
|
|
3
|
+
Version: 0.3.2.dev10
|
|
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
|
|
@@ -8,7 +8,6 @@ Requires-Python: <3.14,>=3.11
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: aiohttp<4.0.0,>=3.13.3
|
|
11
|
-
Requires-Dist: aiohttp-cors<1.0.0,>=0.8.1
|
|
12
11
|
Requires-Dist: aiohttp-swagger<2.0.0,>=1.0.16
|
|
13
12
|
Requires-Dist: gunicorn<24.0.0,>=23.0.0
|
|
14
13
|
Requires-Dist: sentry-sdk<3.0.0,>=2.49.0
|
|
@@ -323,7 +322,9 @@ column_name__sort=desc
|
|
|
323
322
|
```
|
|
324
323
|
|
|
325
324
|
#### Aggregation Operators
|
|
326
|
-
> ⚠️ **WARNING**: Aggregation requests are
|
|
325
|
+
> ⚠️ **WARNING**: Aggregation requests are disabled by default.
|
|
326
|
+
> You can allow or disallow it for all resources with the `ALLOW_AGGREGATION` config. If `ALLOW_AGGREGATION` is set to `false`, you can specify allow exceptions by listing these in the `ALLOW_AGGREGATION_EXCEPTIONS` list.
|
|
327
|
+
> You can get the current status and exceptions at `/api/aggregation-exceptions/` endpoint.
|
|
327
328
|
|
|
328
329
|
```
|
|
329
330
|
# group by values
|
|
@@ -403,6 +404,22 @@ curl http://localhost:8005/api/resources/aaaaaaaa-1111-bbbb-2222-cccccccccccc/da
|
|
|
403
404
|
}
|
|
404
405
|
}
|
|
405
406
|
```
|
|
407
|
+
Top level parameters are considered joined with the `AND` operator. It is possible to add nested conditions with the following syntax:
|
|
408
|
+
```
|
|
409
|
+
or=(<column1>__<operator1>.<value1>,<column2>__<operator2>.<value2>,...)
|
|
410
|
+
```
|
|
411
|
+
> **Note**: the conditions within an `OR` have a `.` instead of a `=` between the operator and the value.
|
|
412
|
+
|
|
413
|
+
> **Note**: the syntax for `isnull` and `isnotnull` is `<column1>__is(not)null` (no value is needed, like outisde of `OR` groups)
|
|
414
|
+
|
|
415
|
+
This scales as much as needed, but the inner operators should be `or(...)` and `and(...)` (without the `=`), like:
|
|
416
|
+
```
|
|
417
|
+
decompte__less=10&or=(is_true__exact.true,birth__greater.1980,and(score__strictly_less.5,liste__isnotnull,or(...)))
|
|
418
|
+
```
|
|
419
|
+
> **Note**: if the column name and/or the value of the filter contains special characters (including dots), the item should be wrapped within double quotes:
|
|
420
|
+
```
|
|
421
|
+
or=("is true"__exact.true,and(score__strictly_less."0.1","avg.views"__greater."15.5"))
|
|
422
|
+
```
|
|
406
423
|
|
|
407
424
|
#### Aggregation with Filtering
|
|
408
425
|
With filters and aggregators (filtering is always done **before** aggregation, no matter the order in the parameters):
|
|
@@ -479,7 +496,8 @@ Configuration is handled through TOML files and environment variables. The defau
|
|
|
479
496
|
| `PAGE_SIZE_MAX` | `50` | Maximum allowed page size |
|
|
480
497
|
| `BATCH_SIZE` | `50000` | Batch size for streaming |
|
|
481
498
|
| `DOC_PATH` | `/api/doc` | Swagger documentation path |
|
|
482
|
-
| `ALLOW_AGGREGATION` | `
|
|
499
|
+
| `ALLOW_AGGREGATION` | `False` | Whether aggregation queries are allowed (`column__groupby`). If False, it can still be explicitly allowed with `ALLOW_AGGREGATION_EXCEPTIONS` |
|
|
500
|
+
| `ALLOW_AGGREGATION_EXCEPTIONS` | `["dddddddd-7777-eeee-8888-ffffffffffff", "aaaaaaaa-9999-bbbb-1010-cccccccccccc"]` | List of resource IDs allowed for aggregation |
|
|
483
501
|
|
|
484
502
|
### Environment Variables
|
|
485
503
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
api_tabular/__init__.py,sha256=sYG6CMz-twob-QLyfsPYRc6cWDtqWXvq_N5Dxk-U5qA,2263
|
|
2
|
+
api_tabular/config_default.toml,sha256=cX506NdqHW7gK_2aZjzLiz0Z4gMSl1tU_tI4i4BvuXs,421
|
|
3
|
+
api_tabular/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
api_tabular/core/cors.py,sha256=QY006pAnR1fMBMJJTAeRGcopL_4LkY7YoA3y8KYk4dI,825
|
|
5
|
+
api_tabular/core/data.py,sha256=5j-3d_el7LMoHpn6V__UdhYHD6GiJbwdH67jrnOYWZQ,1459
|
|
6
|
+
api_tabular/core/error.py,sha256=xRt6fkQfsiupv0tWWzdX0DJjAKbw1n0tRxFyt15VBLA,1062
|
|
7
|
+
api_tabular/core/health.py,sha256=NeFT1C00onvm3CbgesFAcN1N8bmelbQdEYFbmmftICQ,647
|
|
8
|
+
api_tabular/core/query.py,sha256=aI_lqb7LGAC-EQUVfP-q90KxcWFkDTuKmW8I7yCTvO4,10067
|
|
9
|
+
api_tabular/core/sentry.py,sha256=fN5MrDZszLnzLXF3b_nUymPVB0ffNn4PI7xS1GtJNyE,948
|
|
10
|
+
api_tabular/core/swagger.py,sha256=eSCAu0TQsT2tlQiW7i-ueZB14qTd8TfHbUypZeeB-0w,14640
|
|
11
|
+
api_tabular/core/url.py,sha256=af7VKfrMtKMzlHrGgtLDLuRBim4qFv7xlt2PYxAyW2g,761
|
|
12
|
+
api_tabular/core/utils.py,sha256=VTxPiNWE3FF6f2aX5Q21XPezCL3sVZ_KICIrGBJRm_E,1071
|
|
13
|
+
api_tabular/core/version.py,sha256=OKLg0l1CpDRHV2hsqVgtSihoo3tg0aMG6kzMILtQ-bY,250
|
|
14
|
+
api_tabular/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
api_tabular/metrics/app.py,sha256=JY7GZSqCsihEcQy7SFTiKqvoyxfGRFaJ48CovNQTq4Y,4408
|
|
16
|
+
api_tabular/tabular/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
api_tabular/tabular/app.py,sha256=tbf1Ni_r_bbeivxlMNllVKRoqpajZId6SiurCn1hTas,6645
|
|
18
|
+
api_tabular/tabular/utils.py,sha256=BL2nl04-3imBQG-RZiYvVMjxoBORi4lwrKq8NiWecLY,4487
|
|
19
|
+
udata_hydra_csvapi-0.3.2.dev10.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
|
|
20
|
+
udata_hydra_csvapi-0.3.2.dev10.dist-info/METADATA,sha256=TdT5xw8FivU386X-NZPhMeoiosJCaj1JfGj7JJc-TZw,20386
|
|
21
|
+
udata_hydra_csvapi-0.3.2.dev10.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
22
|
+
udata_hydra_csvapi-0.3.2.dev10.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
|
|
23
|
+
udata_hydra_csvapi-0.3.2.dev10.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
api_tabular/__init__.py,sha256=Qso9RRp2-D3hxKwsx4jjBG0LRxHOP699ZuI4v188Om4,2325
|
|
2
|
-
api_tabular/config_default.toml,sha256=3zQMpzr87qwd23dG_scGUptj0T3IUOjaHecI4ppJBqY,384
|
|
3
|
-
api_tabular/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
api_tabular/core/data.py,sha256=5j-3d_el7LMoHpn6V__UdhYHD6GiJbwdH67jrnOYWZQ,1459
|
|
5
|
-
api_tabular/core/error.py,sha256=xRt6fkQfsiupv0tWWzdX0DJjAKbw1n0tRxFyt15VBLA,1062
|
|
6
|
-
api_tabular/core/health.py,sha256=5VAgsu2t1Ave9n3YduzM3E7aUflBJ3pY1LUrrjT-d6w,775
|
|
7
|
-
api_tabular/core/query.py,sha256=m98cO16mVkz0cSNICesgjC8MNVEfDW_645sz-DTd2Pw,5576
|
|
8
|
-
api_tabular/core/sentry.py,sha256=fN5MrDZszLnzLXF3b_nUymPVB0ffNn4PI7xS1GtJNyE,948
|
|
9
|
-
api_tabular/core/swagger.py,sha256=0RZFu941CD3qWt2R6e3nnlcrkYjjAHU5eOFl4jH2liA,14188
|
|
10
|
-
api_tabular/core/url.py,sha256=af7VKfrMtKMzlHrGgtLDLuRBim4qFv7xlt2PYxAyW2g,761
|
|
11
|
-
api_tabular/core/utils.py,sha256=SWU1gVkOq728-eZHmZzlyA3pQg6kelEf3SMlL-uEbns,1032
|
|
12
|
-
api_tabular/core/version.py,sha256=OKLg0l1CpDRHV2hsqVgtSihoo3tg0aMG6kzMILtQ-bY,250
|
|
13
|
-
api_tabular/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
api_tabular/metrics/app.py,sha256=5kZXzAiFh1tSNYO5GEz67m4zTsTVyoIkgcelDGyT1zE,4637
|
|
15
|
-
api_tabular/tabular/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
api_tabular/tabular/app.py,sha256=JgqL1svglQ_fD5ndhft_Cm7HmTh5FuyoxYUPGS1_l-o,6771
|
|
17
|
-
api_tabular/tabular/utils.py,sha256=BL2nl04-3imBQG-RZiYvVMjxoBORi4lwrKq8NiWecLY,4487
|
|
18
|
-
udata_hydra_csvapi-0.3.2.dev8.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
|
|
19
|
-
udata_hydra_csvapi-0.3.2.dev8.dist-info/METADATA,sha256=wlHNzh9fQQSuy3lvGCpHb26fiw2mghgr7gyBPVxFzig,19156
|
|
20
|
-
udata_hydra_csvapi-0.3.2.dev8.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
21
|
-
udata_hydra_csvapi-0.3.2.dev8.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
|
|
22
|
-
udata_hydra_csvapi-0.3.2.dev8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{udata_hydra_csvapi-0.3.2.dev8.dist-info → udata_hydra_csvapi-0.3.2.dev10.dist-info}/top_level.txt
RENAMED
|
File without changes
|