udata-hydra-csvapi 0.3.2.dev9__py3-none-any.whl → 0.3.2.dev11__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.
@@ -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
@@ -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
@@ -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
- {"status": "ok", "version": request.app["app_version"], "uptime_seconds": uptime_seconds}
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:
@@ -25,7 +30,7 @@ def build_sql_query_string(
25
30
  # is(not)null also has this syntax but is a filter
26
31
  elif len(_split) == 1:
27
32
  if _split[0].split("__")[1] in ["isnull", "isnotnull"]:
28
- _filter, _ = add_filter(_split[0], None)
33
+ _filter, _ = add_filter(_split[0], "")
29
34
  sql_query.append(_filter)
30
35
  else:
31
36
  column, operator = add_aggregator(_split[0], indexes)
@@ -72,39 +77,55 @@ def get_column_and_operator(argument: str) -> tuple[str, str]:
72
77
  return column, normalized_comparator
73
78
 
74
79
 
75
- def add_filter(argument: str, value: str | None) -> tuple[str | None, bool]:
80
+ def add_filter(
81
+ argument: str,
82
+ value: str,
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":
79
- return f"select={value}", False
91
+ if in_operator:
92
+ raise ValueError(f"Argument `{argument}` can't be set within an operator")
93
+ if '"' in value:
94
+ raise ValueError('Forbidden character `"` in a column name')
95
+ columns = ",".join(f'"{col}"' for col in value.split(","))
96
+ return f"select={columns}", False
80
97
  if "__" in argument:
81
98
  column, normalized_comparator = get_column_and_operator(argument)
99
+ # when encapsulated in an OR statement, the syntax is `col.eq.val` instead of `col=eq.val`
100
+ op = "." if in_operator else "="
82
101
  if normalized_comparator == "sort":
102
+ if in_operator:
103
+ raise ValueError(f"Argument `{argument}` can't be set within an operator")
83
104
  return f"order={column}.{value}", True
84
105
  elif normalized_comparator == "exact":
85
- return f"{column}=eq.{value}", False
106
+ return f"{column}{op}eq.{value}", False
86
107
  elif normalized_comparator == "differs":
87
- return f"{column}=isdistinct.{value}", False
108
+ return f"{column}{op}isdistinct.{value}", False
88
109
  elif normalized_comparator == "isnull":
89
- return f"{column}=is.null", False
110
+ return f"{column}{op}is.null", False
90
111
  elif normalized_comparator == "isnotnull":
91
- return f"{column}=not.is.null", False
112
+ return f"{column}{op}not.is.null", False
92
113
  elif normalized_comparator == "contains":
93
- return f"{column}=ilike.*{value}*", False
114
+ return f"{column}{op}ilike.*{value}*", False
94
115
  elif normalized_comparator == "notcontains":
95
- return f"{column}=not.ilike.*{value}*", False
116
+ return f"{column}{op}not.ilike.*{value}*", False
96
117
  elif normalized_comparator == "in":
97
- return f"{column}=in.({value})", False
118
+ return f"{column}{op}in.({value})", False
98
119
  elif normalized_comparator == "notin":
99
- return f"{column}=not.in.({value})", False
120
+ return f"{column}{op}not.in.({value})", False
100
121
  elif normalized_comparator == "less":
101
- return f"{column}=lte.{value}", False
122
+ return f"{column}{op}lte.{value}", False
102
123
  elif normalized_comparator == "greater":
103
- return f"{column}=gte.{value}", False
124
+ return f"{column}{op}gte.{value}", False
104
125
  elif normalized_comparator == "strictly_less":
105
- return f"{column}=lt.{value}", False
126
+ return f"{column}{op}lt.{value}", False
106
127
  elif normalized_comparator == "strictly_greater":
107
- return f"{column}=gt.{value}", False
128
+ return f"{column}{op}gt.{value}", False
108
129
  raise ValueError(f"argument '{argument}={value}' could not be parsed")
109
130
 
110
131
 
@@ -118,6 +139,91 @@ def add_aggregator(argument: str, indexes: set | None) -> tuple[str, str]:
118
139
  raise ValueError(f"argument '{argument}' could not be parsed")
119
140
 
120
141
 
142
+ def split_top_level(s: str) -> list[str]:
143
+ # we can't .split(",") as there may be commas within the params (if nested)
144
+ # so we need a custom "split by ',' if ',' not within parentheses"
145
+ parts = []
146
+ current = ""
147
+ depth = 0
148
+ for char in s:
149
+ if char == "(":
150
+ depth += 1
151
+ current += char
152
+ elif char == ")":
153
+ depth -= 1
154
+ current += char
155
+ elif char == "," and depth == 0:
156
+ parts.append(current)
157
+ current = ""
158
+ else:
159
+ current += char
160
+ if current:
161
+ parts.append(current)
162
+ return parts
163
+
164
+
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")
182
+ # 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:]
185
+ 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]
195
+
196
+
197
+ def parse_operator(query: str, operator: str, top_level: bool = False):
198
+ # only the top level operator has an "=", nested operators are "or(...)" and "and(...)"
199
+ if not query.endswith(")"):
200
+ raise ValueError(f"argument '{query}' could not be parsed")
201
+ postgrest_params = []
202
+ # we can safely assume that there will be one result for the regex
203
+ params = split_top_level(
204
+ re.findall(rf"^{operator}{'=' if top_level else ''}\((.*)\)$", query)[0]
205
+ )
206
+ for param in params:
207
+ if param.startswith(("and(", "or(")):
208
+ # recursively adding the nested conditions
209
+ postgrest_params.append(parse_operator(query=param, operator=param.split("(")[0]))
210
+ else:
211
+ if param.endswith(("__isnull", "__isnotnull")):
212
+ postgrest_params.append(
213
+ add_filter(
214
+ param.replace('"', ""),
215
+ "",
216
+ in_operator=True,
217
+ )[0]
218
+ )
219
+ else:
220
+ # if a special character is in the column name and/or value, the problematic item should be encapsulated in double quotes
221
+ # so we have 4 possibles cases: col__op.val, "col.umn"__op.val, col__op."val.ue" and "col.umn"__op."val.ue"
222
+ argument, value = find_arg_val(param)
223
+ postgrest_params.append(add_filter(argument, value, in_operator=True)[0])
224
+ return f"{operator}{'=' if top_level else ''}({','.join(postgrest_params)})"
225
+
226
+
121
227
  def raise_if_not_index(column_name: str, indexes: set | None) -> None:
122
228
  if indexes is None:
123
229
  return
@@ -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.ALLOW_AGGREGATION
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:
@@ -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["start_time"] = datetime.now(timezone.utc)
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
 
@@ -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
- return web.json_response(config.ALLOW_AGGREGATION)
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["start_time"] = datetime.now(timezone.utc)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: udata-hydra-csvapi
3
- Version: 0.3.2.dev9
3
+ Version: 0.3.2.dev11
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 only available for resources that are listed in the `ALLOW_AGGREGATION` list of the config file, which can be seen at the `/api/aggregation-exceptions/` endpoint, and on columns that have an index.
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` | `["dddddddd-7777-eeee-8888-ffffffffffff", "aaaaaaaa-9999-bbbb-1010-cccccccccccc"]` | List of resource IDs allowed for 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=Twlv-xLYzZ0YGzCDiJ1uJAH2nZT9hEATNSSPiHVnRU8,10226
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.dev11.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
20
+ udata_hydra_csvapi-0.3.2.dev11.dist-info/METADATA,sha256=lXWF7fYmcYSjCifS9tpH3Wyep-08wbK73zMcbocu6FM,20386
21
+ udata_hydra_csvapi-0.3.2.dev11.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ udata_hydra_csvapi-0.3.2.dev11.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
23
+ udata_hydra_csvapi-0.3.2.dev11.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,22 +0,0 @@
1
- api_tabular/__init__.py,sha256=sYG6CMz-twob-QLyfsPYRc6cWDtqWXvq_N5Dxk-U5qA,2263
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.dev9.dist-info/licenses/LICENSE,sha256=fb-HAYw94ffh--z1cZrWbpk6YKM0p9WFG7kdd0K7uGQ,1062
19
- udata_hydra_csvapi-0.3.2.dev9.dist-info/METADATA,sha256=70xGNtnDPIZYktyfsrYw4bhn6SDDBASB6jUQCgpHRrk,19156
20
- udata_hydra_csvapi-0.3.2.dev9.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
21
- udata_hydra_csvapi-0.3.2.dev9.dist-info/top_level.txt,sha256=MAaK09hxwPhIBBC0i1TdEBAgdu3W_gXnAZqk767gVak,12
22
- udata_hydra_csvapi-0.3.2.dev9.dist-info/RECORD,,