udata-hydra 2.0.0.dev2497__tar.gz → 2.0.0.dev2547__tar.gz
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.
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/PKG-INFO +4 -3
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/pyproject.toml +4 -3
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/csv.py +69 -28
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/config_default.toml +3 -2
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/crawl.py +1 -1
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/db/check.py +6 -2
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/resources.py +1 -4
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/__init__.py +0 -1
- udata_hydra-2.0.0.dev2547/udata_hydra/utils/minio.py +46 -0
- udata_hydra-2.0.0.dev2547/udata_hydra/utils/parquet.py +30 -0
- udata_hydra-2.0.0.dev2497/udata_hydra/utils/minio.py +0 -56
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/README.md +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/errors.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/helpers.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/resource.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/app.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/cli.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/context.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/db/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/db/resource.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/logger.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/checks.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/status.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/schemas/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/schemas/check.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/schemas/resource.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/app_version.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/csv.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/file.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/http.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/queue.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/reader.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/timer.py +0 -0
- {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: udata-hydra
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev2547
|
|
4
4
|
Summary: Async crawler and parsing service for data.gouv.fr
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Opendata Team
|
|
@@ -15,14 +15,15 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Requires-Dist: aiocontextvars (>=0.2.2,<0.3.0)
|
|
16
16
|
Requires-Dist: aiohttp (>=3.8.1,<4.0.0)
|
|
17
17
|
Requires-Dist: asyncpg (>=0.27.0,<0.28.0)
|
|
18
|
-
Requires-Dist: boto3 (>=1.21.21,<2.0.0)
|
|
19
18
|
Requires-Dist: coloredlogs (>=15.0.1,<16.0.0)
|
|
20
19
|
Requires-Dist: csv-detective (==0.7.1)
|
|
21
20
|
Requires-Dist: dateparser (>=1.1.7,<2.0.0)
|
|
22
21
|
Requires-Dist: humanfriendly (>=10.0,<11.0)
|
|
23
22
|
Requires-Dist: marshmallow (>=3.14.1,<4.0.0)
|
|
24
|
-
Requires-Dist: minicli (>=0.5.
|
|
23
|
+
Requires-Dist: minicli (>=0.5.3,<0.6.0)
|
|
24
|
+
Requires-Dist: minio (==7.2.7)
|
|
25
25
|
Requires-Dist: progressist (>=0.1.0,<0.2.0)
|
|
26
|
+
Requires-Dist: pyarrow (==16.1.0)
|
|
26
27
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
27
28
|
Requires-Dist: python-magic (>=0.4.25,<0.5.0)
|
|
28
29
|
Requires-Dist: redis (>=4.1.4,<5.0.0)
|
|
@@ -9,7 +9,7 @@ readme = "README.md"
|
|
|
9
9
|
|
|
10
10
|
[tool.poetry]
|
|
11
11
|
name = "udata-hydra"
|
|
12
|
-
version = "2.0.0.
|
|
12
|
+
version = "2.0.0.dev2547"
|
|
13
13
|
description = "Async crawler and parsing service for data.gouv.fr"
|
|
14
14
|
authors = ["Opendata Team <opendatateam@data.gouv.fr>"]
|
|
15
15
|
license = "MIT"
|
|
@@ -19,10 +19,9 @@ readme = "README.md"
|
|
|
19
19
|
python = "^3.9"
|
|
20
20
|
aiohttp = "^3.8.1"
|
|
21
21
|
asyncpg = "^0.27.0"
|
|
22
|
-
boto3 = "^1.21.21"
|
|
23
22
|
humanfriendly = "^10.0"
|
|
24
23
|
marshmallow = "^3.14.1"
|
|
25
|
-
minicli = "^0.5.
|
|
24
|
+
minicli = "^0.5.3"
|
|
26
25
|
progressist = "^0.1.0"
|
|
27
26
|
python-magic = "^0.4.25"
|
|
28
27
|
redis = "^4.1.4"
|
|
@@ -37,6 +36,8 @@ sqlalchemy = "^1.4.46"
|
|
|
37
36
|
dateparser = "^1.1.7"
|
|
38
37
|
python-dateutil = "^2.8.2"
|
|
39
38
|
csv-detective = "0.7.1"
|
|
39
|
+
pyarrow = "16.1.0"
|
|
40
|
+
minio = "7.2.7"
|
|
40
41
|
|
|
41
42
|
[tool.poetry.group.dev.dependencies]
|
|
42
43
|
aiohttp-devtools = "^1.0.post0"
|
|
@@ -5,7 +5,7 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
|
-
from typing import Any, Union
|
|
8
|
+
from typing import Any, Iterator, Union
|
|
9
9
|
|
|
10
10
|
import sentry_sdk
|
|
11
11
|
from csv_detective.detection import engine_to_file
|
|
@@ -35,6 +35,8 @@ from udata_hydra.analysis.errors import ParseException
|
|
|
35
35
|
from udata_hydra.db import compute_insert_query
|
|
36
36
|
from udata_hydra.db.check import Check
|
|
37
37
|
from udata_hydra.utils import Reader, Timer, download_resource, queue, send
|
|
38
|
+
from udata_hydra.utils.minio import MinIOClient
|
|
39
|
+
from udata_hydra.utils.parquet import save_as_parquet
|
|
38
40
|
|
|
39
41
|
log = logging.getLogger("udata-hydra")
|
|
40
42
|
|
|
@@ -69,9 +71,10 @@ PYTHON_TYPE_TO_PY = {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
RESERVED_COLS = ("__id", "tableoid", "xmin", "cmin", "xmax", "cmax", "ctid")
|
|
74
|
+
minio_client = MinIOClient()
|
|
72
75
|
|
|
73
76
|
|
|
74
|
-
async def notify_udata(check_id:
|
|
77
|
+
async def notify_udata(check_id: str, table_name: str) -> None:
|
|
75
78
|
"""Notify udata of the result of a parsing"""
|
|
76
79
|
check = await Check.get(check_id)
|
|
77
80
|
resource_id = check["resource_id"]
|
|
@@ -91,6 +94,8 @@ async def notify_udata(check_id: int) -> None:
|
|
|
91
94
|
else None,
|
|
92
95
|
},
|
|
93
96
|
}
|
|
97
|
+
if config.CSV_TO_PARQUET:
|
|
98
|
+
payload["parquet_id"] = table_name
|
|
94
99
|
queue.enqueue(send, _priority="high", **payload)
|
|
95
100
|
|
|
96
101
|
|
|
@@ -101,8 +106,8 @@ async def analyse_csv(
|
|
|
101
106
|
debug_insert: bool = False,
|
|
102
107
|
) -> None:
|
|
103
108
|
"""Launch csv analysis from a check or an URL (debug), using previsously downloaded file at file_path if any"""
|
|
104
|
-
if not config.
|
|
105
|
-
log.debug("
|
|
109
|
+
if not config.CSV_ANALYSIS:
|
|
110
|
+
log.debug("CSV_ANALYSIS turned off, skipping.")
|
|
106
111
|
return
|
|
107
112
|
|
|
108
113
|
exceptions = config.LARGE_RESOURCES_EXCEPTIONS
|
|
@@ -133,6 +138,8 @@ async def analyse_csv(
|
|
|
133
138
|
timer.mark("csv-inspection")
|
|
134
139
|
await csv_to_db(tmp_file.name, csv_inspection, table_name, debug_insert=debug_insert)
|
|
135
140
|
timer.mark("csv-to-db")
|
|
141
|
+
await csv_to_parquet(tmp_file.name, csv_inspection, table_name)
|
|
142
|
+
timer.mark("csv-to-parquet")
|
|
136
143
|
if check_id:
|
|
137
144
|
await Check.update(
|
|
138
145
|
check_id,
|
|
@@ -146,7 +153,7 @@ async def analyse_csv(
|
|
|
146
153
|
await handle_parse_exception(e, check_id, table_name)
|
|
147
154
|
finally:
|
|
148
155
|
if check_id:
|
|
149
|
-
await notify_udata(check_id)
|
|
156
|
+
await notify_udata(check_id, table_name)
|
|
150
157
|
timer.stop()
|
|
151
158
|
tmp_file.close()
|
|
152
159
|
os.remove(tmp_file.name)
|
|
@@ -185,6 +192,40 @@ def compute_create_table_query(table_name: str, columns: list) -> str:
|
|
|
185
192
|
return compiled.string.replace("%%", "%")
|
|
186
193
|
|
|
187
194
|
|
|
195
|
+
def generate_records(file_path: str, inspection: dict, columns: dict) -> Iterator[list]:
|
|
196
|
+
# because we need the iterator twice, not possible to
|
|
197
|
+
# handle parquet and db through the same iteration
|
|
198
|
+
with Reader(file_path, inspection) as reader:
|
|
199
|
+
for line in reader:
|
|
200
|
+
if line:
|
|
201
|
+
yield [smart_cast(t, v, failsafe=True) for t, v in zip(columns.values(), line)]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def csv_to_parquet(file_path: str, inspection: dict, table_name: str) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Convert a csv file to parquet using inspection data.
|
|
207
|
+
|
|
208
|
+
:file_path: CSV file path to convert
|
|
209
|
+
:inspection: CSV detective report
|
|
210
|
+
:table_name: used to name the parquet file
|
|
211
|
+
"""
|
|
212
|
+
if not config.CSV_TO_PARQUET:
|
|
213
|
+
log.debug("CSV_TO_PARQUET turned off, skipping parquet export.")
|
|
214
|
+
return
|
|
215
|
+
log.debug(
|
|
216
|
+
f"Converting from {engine_to_file.get(inspection.get('engine', ''), 'CSV')} "
|
|
217
|
+
f"to parquet for {table_name} and sending to Minio."
|
|
218
|
+
)
|
|
219
|
+
columns = {c: v["python_type"] for c, v in inspection["columns"].items()}
|
|
220
|
+
# save the file as parquet and store it on Minio instance
|
|
221
|
+
parquet_file, _ = save_as_parquet(
|
|
222
|
+
records=generate_records(file_path, inspection, columns),
|
|
223
|
+
columns=columns,
|
|
224
|
+
output_name=table_name,
|
|
225
|
+
)
|
|
226
|
+
minio_client.send_file(parquet_file)
|
|
227
|
+
|
|
228
|
+
|
|
188
229
|
async def csv_to_db(
|
|
189
230
|
file_path: str, inspection: dict, table_name: str, debug_insert: bool = False
|
|
190
231
|
) -> None:
|
|
@@ -197,42 +238,42 @@ async def csv_to_db(
|
|
|
197
238
|
:table_name: used to create tables
|
|
198
239
|
:debug_insert: insert record one by one instead of using postgresql COPY
|
|
199
240
|
"""
|
|
241
|
+
if not config.CSV_TO_DB:
|
|
242
|
+
log.debug("CSV_TO_DB turned off, skipping.")
|
|
243
|
+
return
|
|
200
244
|
log.debug(
|
|
201
245
|
f"Converting from {engine_to_file.get(inspection.get('engine', ''), 'CSV')} "
|
|
202
246
|
f"to db for {table_name}"
|
|
203
247
|
)
|
|
204
|
-
columns = inspection["columns"]
|
|
205
248
|
# build a `column_name: type` mapping and explicitely rename reserved column names
|
|
206
249
|
columns = {
|
|
207
250
|
f"{c}__hydra_renamed" if c.lower() in RESERVED_COLS else c: v["python_type"]
|
|
208
|
-
for c, v in columns.items()
|
|
251
|
+
for c, v in inspection["columns"].items()
|
|
209
252
|
}
|
|
210
253
|
q = f'DROP TABLE IF EXISTS "{table_name}"'
|
|
211
254
|
db = await context.pool("csv")
|
|
212
255
|
await db.execute(q)
|
|
213
256
|
q = compute_create_table_query(table_name, columns)
|
|
214
257
|
await db.execute(q)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
q = compute_insert_query(table_name=table_name, data=data, returning="__id")
|
|
235
|
-
await db.execute(q, *data.values())
|
|
258
|
+
# this use postgresql COPY from an iterator, it's fast but might be difficult to debug
|
|
259
|
+
if not debug_insert:
|
|
260
|
+
# NB: also see copy_to_table for a file source
|
|
261
|
+
try:
|
|
262
|
+
await db.copy_records_to_table(
|
|
263
|
+
table_name,
|
|
264
|
+
records=generate_records(file_path, inspection, columns),
|
|
265
|
+
columns=columns.keys(),
|
|
266
|
+
)
|
|
267
|
+
except Exception as e: # I know what I'm doing, pinky swear
|
|
268
|
+
raise ParseException("copy_records_to_table") from e
|
|
269
|
+
# this inserts rows from iterator one by one, slow but useful for debugging
|
|
270
|
+
else:
|
|
271
|
+
bar = ProgressBar(total=inspection["total_lines"])
|
|
272
|
+
for r in bar.iter(generate_records(file_path, inspection, columns)):
|
|
273
|
+
data = {k: v for k, v in zip(columns.keys(), r)}
|
|
274
|
+
# NB: possible sql injection here, but should not be used in prod
|
|
275
|
+
q = compute_insert_query(table_name=table_name, data=data, returning="__id")
|
|
276
|
+
await db.execute(q, *data.values())
|
|
236
277
|
|
|
237
278
|
|
|
238
279
|
async def csv_to_db_index(table_name: str, inspection: dict, check: dict) -> None:
|
|
@@ -55,7 +55,8 @@ MAX_FILESIZE_ALLOWED.xlsx = 13107200 # /8
|
|
|
55
55
|
MAX_FILESIZE_ALLOWED.ods = 10485760 # /10
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
CSV_ANALYSIS = true
|
|
59
|
+
CSV_TO_DB = true
|
|
59
60
|
TEMPORARY_DOWNLOAD_FOLDER = ""
|
|
60
61
|
|
|
61
62
|
# -- Worker settings -- #
|
|
@@ -67,7 +68,7 @@ UDATA_URI = ""
|
|
|
67
68
|
UDATA_URI_API_KEY = ""
|
|
68
69
|
|
|
69
70
|
# -- Minio / datalake settings -- #
|
|
70
|
-
|
|
71
|
+
CSV_TO_PARQUET = false
|
|
71
72
|
MINIO_FOLDER = ""
|
|
72
73
|
MINIO_URL = ""
|
|
73
74
|
MINIO_BUCKET = ""
|
|
@@ -204,7 +204,7 @@ def fix_surrogates(value):
|
|
|
204
204
|
"""FIX Unicode low surrogate must follow a high surrogate.
|
|
205
205
|
eg in 'TREMI_2017-R\xe9sultats enqu\xeate bruts.csv'
|
|
206
206
|
"""
|
|
207
|
-
if
|
|
207
|
+
if not isinstance(value, str):
|
|
208
208
|
value = str(value)
|
|
209
209
|
return value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
|
|
210
210
|
|
|
@@ -24,7 +24,9 @@ class Check:
|
|
|
24
24
|
return check
|
|
25
25
|
|
|
26
26
|
@classmethod
|
|
27
|
-
async def get_latest(
|
|
27
|
+
async def get_latest(
|
|
28
|
+
cls, url: Union[str, None], resource_id: Union[str, None]
|
|
29
|
+
) -> Union[dict, None]:
|
|
28
30
|
column: str = "url" if url else "resource_id"
|
|
29
31
|
pool = await context.pool()
|
|
30
32
|
async with pool.acquire() as connection:
|
|
@@ -38,7 +40,9 @@ class Check:
|
|
|
38
40
|
return await connection.fetchrow(q, url or resource_id)
|
|
39
41
|
|
|
40
42
|
@classmethod
|
|
41
|
-
async def get_all(
|
|
43
|
+
async def get_all(
|
|
44
|
+
cls, url: Union[str, None], resource_id: Union[str, None]
|
|
45
|
+
) -> Union[dict, None]:
|
|
42
46
|
column: str = "url" if url else "resource_id"
|
|
43
47
|
pool = await context.pool()
|
|
44
48
|
async with pool.acquire() as connection:
|
|
@@ -6,7 +6,7 @@ from marshmallow import ValidationError
|
|
|
6
6
|
from udata_hydra import config
|
|
7
7
|
from udata_hydra.db.resource import Resource
|
|
8
8
|
from udata_hydra.schemas import ResourceSchema
|
|
9
|
-
from udata_hydra.utils import
|
|
9
|
+
from udata_hydra.utils import get_request_params
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
async def get_resource(request: web.Request) -> web.Response:
|
|
@@ -82,13 +82,10 @@ async def delete_resource(request: web.Request) -> web.Response:
|
|
|
82
82
|
except ValidationError as err:
|
|
83
83
|
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
84
84
|
|
|
85
|
-
dataset_id = valid_payload["dataset_id"]
|
|
86
85
|
resource_id = valid_payload["resource_id"]
|
|
87
86
|
|
|
88
87
|
pool = request.app["pool"]
|
|
89
88
|
async with pool.acquire() as connection:
|
|
90
|
-
if config.SAVE_TO_MINIO:
|
|
91
|
-
delete_resource_from_minio(dataset_id, resource_id)
|
|
92
89
|
# Mark resource as deleted in catalog table
|
|
93
90
|
q = f"""UPDATE catalog SET deleted = TRUE WHERE resource_id = '{resource_id}';"""
|
|
94
91
|
await connection.execute(q)
|
|
@@ -2,7 +2,6 @@ from .app_version import get_app_version
|
|
|
2
2
|
from .csv import detect_tabular_from_headers
|
|
3
3
|
from .file import compute_checksum_from_file, download_resource, read_csv_gz
|
|
4
4
|
from .http import get_request_params, send
|
|
5
|
-
from .minio import delete_resource_from_minio, get_resource_minio_url, save_resource_to_minio
|
|
6
5
|
from .queue import enqueue
|
|
7
6
|
from .reader import Reader, generate_dialect
|
|
8
7
|
from .timer import Timer
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from minio import Minio
|
|
5
|
+
|
|
6
|
+
from udata_hydra import config
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger("udata-hydra")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MinIOClient:
|
|
12
|
+
def __init__(self, bucket=config.MINIO_BUCKET):
|
|
13
|
+
self.url = config.MINIO_URL
|
|
14
|
+
self.user = config.MINIO_USER
|
|
15
|
+
self.password = config.MINIO_PWD
|
|
16
|
+
self.bucket = bucket
|
|
17
|
+
self.client = Minio(
|
|
18
|
+
self.url or "test",
|
|
19
|
+
access_key=self.user or "test",
|
|
20
|
+
secret_key=self.password or "test",
|
|
21
|
+
secure=True,
|
|
22
|
+
)
|
|
23
|
+
if self.bucket:
|
|
24
|
+
self.bucket_exists = self.client.bucket_exists(self.bucket)
|
|
25
|
+
if not self.bucket_exists:
|
|
26
|
+
raise ValueError(f"Bucket '{self.bucket}' does not exist.")
|
|
27
|
+
|
|
28
|
+
def send_file(
|
|
29
|
+
self,
|
|
30
|
+
file_path,
|
|
31
|
+
delete_source=True,
|
|
32
|
+
):
|
|
33
|
+
if self.bucket is None:
|
|
34
|
+
raise AttributeError("A bucket has to be specified.")
|
|
35
|
+
is_file = os.path.isfile(os.path.join(file_path))
|
|
36
|
+
if is_file:
|
|
37
|
+
dest_path = f"{config.MINIO_FOLDER}{file_path.split('/')[-1]}"
|
|
38
|
+
self.client.fput_object(
|
|
39
|
+
self.bucket,
|
|
40
|
+
dest_path,
|
|
41
|
+
file_path,
|
|
42
|
+
)
|
|
43
|
+
if delete_source:
|
|
44
|
+
os.remove(file_path)
|
|
45
|
+
else:
|
|
46
|
+
raise Exception(f"file '{file_path}' does not exists")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
import pyarrow as pa
|
|
4
|
+
import pyarrow.parquet as pq
|
|
5
|
+
|
|
6
|
+
PYTHON_TYPE_TO_PA = {
|
|
7
|
+
"string": pa.string(),
|
|
8
|
+
"float": pa.float64(),
|
|
9
|
+
"int": pa.int64(),
|
|
10
|
+
"bool": pa.bool_(),
|
|
11
|
+
"json": pa.string(),
|
|
12
|
+
"date": pa.date32(),
|
|
13
|
+
"datetime": pa.date64(),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save_as_parquet(
|
|
18
|
+
records: Generator,
|
|
19
|
+
columns: dict,
|
|
20
|
+
output_name: str,
|
|
21
|
+
save_output: bool = True,
|
|
22
|
+
) -> tuple[str, pa.Table]:
|
|
23
|
+
# the "save_output" argument is only used in tests
|
|
24
|
+
table = pa.Table.from_pylist(
|
|
25
|
+
[{c: v for c, v in zip(columns, values)} for values in records],
|
|
26
|
+
schema=pa.schema([pa.field(c, PYTHON_TYPE_TO_PA[columns[c]]) for c in columns]),
|
|
27
|
+
)
|
|
28
|
+
if save_output:
|
|
29
|
+
pq.write_table(table, f"{output_name}.parquet")
|
|
30
|
+
return f"{output_name}.parquet", table
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from typing import BinaryIO
|
|
3
|
-
|
|
4
|
-
import boto3
|
|
5
|
-
from botocore.client import Config
|
|
6
|
-
from botocore.exceptions import ClientError
|
|
7
|
-
|
|
8
|
-
from udata_hydra import config
|
|
9
|
-
|
|
10
|
-
log = logging.getLogger("udata-hydra")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_resource_minio_url(key: str, resource_id: str) -> str:
|
|
14
|
-
"""Returns location of given resource in minio once it is saved"""
|
|
15
|
-
return f"{config.MINIO_URL}/{config.MINIO_BUCKET}/{config.MINIO_FOLDER}/{key}/{resource_id}"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def save_resource_to_minio(resource_file: BinaryIO, key: str, resource_id: str) -> None:
|
|
19
|
-
log.info("Saving to minio")
|
|
20
|
-
log.debug(f"Bucket is {config.MINIO_BUCKET}")
|
|
21
|
-
s3 = boto3.client(
|
|
22
|
-
"s3",
|
|
23
|
-
endpoint_url=config.MINIO_URL,
|
|
24
|
-
aws_access_key_id=config.MINIO_USER,
|
|
25
|
-
aws_secret_access_key=config.MINIO_PWD,
|
|
26
|
-
config=Config(signature_version="s3v4"),
|
|
27
|
-
)
|
|
28
|
-
try:
|
|
29
|
-
with open(resource_file.name, "rb") as f:
|
|
30
|
-
s3.upload_fileobj(
|
|
31
|
-
Fileobj=f,
|
|
32
|
-
Bucket=config.MINIO_BUCKET,
|
|
33
|
-
Key=f"{config.MINIO_FOLDER}/{key}/{resource_id}",
|
|
34
|
-
)
|
|
35
|
-
log.info(f"Resource saved into minio at {get_resource_minio_url(key, resource_id)}")
|
|
36
|
-
except ClientError as e:
|
|
37
|
-
log.error(e)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def delete_resource_from_minio(key: str, resource_id: str) -> None:
|
|
41
|
-
log.info("Deleting from minio")
|
|
42
|
-
s3 = boto3.client(
|
|
43
|
-
"s3",
|
|
44
|
-
endpoint_url=config.MINIO_URL,
|
|
45
|
-
aws_access_key_id=config.MINIO_USER,
|
|
46
|
-
aws_secret_access_key=config.MINIO_PWD,
|
|
47
|
-
config=Config(signature_version="s3v4"),
|
|
48
|
-
)
|
|
49
|
-
try:
|
|
50
|
-
s3.delete_object(
|
|
51
|
-
Bucket=config.MINIO_BUCKET,
|
|
52
|
-
Key=f"{config.MINIO_FOLDER}/{key}/{resource_id}",
|
|
53
|
-
)
|
|
54
|
-
log.info(f"Resource deleted from minio at {get_resource_minio_url(key, resource_id)}")
|
|
55
|
-
except ClientError as e:
|
|
56
|
-
log.error(e)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|