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.
Files changed (54) hide show
  1. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/PKG-INFO +4 -3
  2. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/pyproject.toml +4 -3
  3. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/csv.py +69 -28
  4. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/config_default.toml +3 -2
  5. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/crawl.py +1 -1
  6. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/db/check.py +6 -2
  7. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/resources.py +1 -4
  8. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/__init__.py +0 -1
  9. udata_hydra-2.0.0.dev2547/udata_hydra/utils/minio.py +46 -0
  10. udata_hydra-2.0.0.dev2547/udata_hydra/utils/parquet.py +30 -0
  11. udata_hydra-2.0.0.dev2497/udata_hydra/utils/minio.py +0 -56
  12. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/README.md +0 -0
  13. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/__init__.py +0 -0
  14. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/__init__.py +0 -0
  15. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/errors.py +0 -0
  16. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/helpers.py +0 -0
  17. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/analysis/resource.py +0 -0
  18. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/app.py +0 -0
  19. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/cli.py +0 -0
  20. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/context.py +0 -0
  21. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/db/__init__.py +0 -0
  22. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/db/resource.py +0 -0
  23. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/logger.py +0 -0
  24. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/__init__.py +0 -0
  25. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
  26. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
  27. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
  28. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
  29. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
  30. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
  31. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
  32. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
  33. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
  34. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
  35. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
  36. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
  37. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
  38. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
  39. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
  40. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
  41. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/__init__.py +0 -0
  42. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/checks.py +0 -0
  43. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/routes/status.py +0 -0
  44. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/schemas/__init__.py +0 -0
  45. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/schemas/check.py +0 -0
  46. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/schemas/resource.py +0 -0
  47. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/app_version.py +0 -0
  48. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/csv.py +0 -0
  49. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/file.py +0 -0
  50. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/http.py +0 -0
  51. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/queue.py +0 -0
  52. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/reader.py +0 -0
  53. {udata_hydra-2.0.0.dev2497 → udata_hydra-2.0.0.dev2547}/udata_hydra/utils/timer.py +0 -0
  54. {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.dev2497
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.0,<0.6.0)
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.dev2497"
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.0"
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: int) -> None:
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.CSV_ANALYSIS_ENABLED:
105
- log.debug("CSV_ANALYSIS_ENABLED turned off, skipping.")
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
- with Reader(file_path, inspection) as reader:
216
- records = (
217
- [smart_cast(t, v, failsafe=True) for t, v in zip(columns.values(), line)]
218
- for line in reader
219
- if line
220
- )
221
- # this use postgresql COPY from an iterator, it's fast but might be difficult to debug
222
- if not debug_insert:
223
- # NB: also see copy_to_table for a file source
224
- try:
225
- await db.copy_records_to_table(table_name, records=records, columns=columns.keys())
226
- except Exception as e: # I know what I'm doing, pinky swear
227
- raise ParseException("copy_records_to_table") from e
228
- # this inserts rows from iterator one by one, slow but useful for debugging
229
- else:
230
- bar = ProgressBar(total=inspection["total_lines"])
231
- for r in bar.iter(records):
232
- data = {k: v for k, v in zip(columns.keys(), r)}
233
- # NB: possible sql injection here, but should not be used in prod
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
- CSV_ANALYSIS_ENABLED = true
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
- SAVE_TO_MINIO = false
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 type(value) is not str:
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(cls, url: Union[str, None], resource_id: Union[str, None]) -> dict | None:
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(cls, url: Union[str, None], resource_id: Union[str, None]) -> dict | None:
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 delete_resource_from_minio, get_request_params
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)