udata-hydra 2.0.0.dev2998__tar.gz → 2.0.0.dev3046__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.dev2998 → udata_hydra-2.0.0.dev3046}/PKG-INFO +1 -1
  2. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/pyproject.toml +1 -1
  3. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/analysis/csv.py +5 -5
  4. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/analysis/helpers.py +4 -4
  5. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/analysis/resource.py +6 -6
  6. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/cli.py +7 -5
  7. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/config_default.toml +1 -1
  8. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/context.py +2 -2
  9. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/crawl.py +4 -3
  10. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/db/check.py +7 -8
  11. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/logger.py +2 -2
  12. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/routes/checks.py +3 -3
  13. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/app_version.py +2 -2
  14. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/auth.py +2 -2
  15. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/file.py +2 -2
  16. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/README.md +0 -0
  17. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/__init__.py +0 -0
  18. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/analysis/__init__.py +0 -0
  19. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/analysis/errors.py +0 -0
  20. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/app.py +0 -0
  21. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/db/__init__.py +0 -0
  22. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/db/resource.py +0 -0
  23. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/__init__.py +0 -0
  24. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
  25. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
  26. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
  27. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
  28. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
  29. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
  30. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
  31. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
  32. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
  33. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
  34. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
  35. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
  36. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
  37. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
  38. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
  39. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
  40. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/routes/__init__.py +0 -0
  41. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/routes/resources.py +0 -0
  42. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/routes/status.py +0 -0
  43. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/schemas/__init__.py +0 -0
  44. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/schemas/check.py +0 -0
  45. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/schemas/resource.py +0 -0
  46. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/__init__.py +0 -0
  47. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/csv.py +0 -0
  48. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/http.py +0 -0
  49. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/minio.py +0 -0
  50. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/parquet.py +0 -0
  51. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/queue.py +0 -0
  52. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/reader.py +0 -0
  53. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/udata_hydra/utils/timer.py +0 -0
  54. {udata_hydra-2.0.0.dev2998 → udata_hydra-2.0.0.dev3046}/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.dev2998
3
+ Version: 2.0.0.dev3046
4
4
  Summary: Async crawler and parsing service for data.gouv.fr
5
5
  License: MIT
6
6
  Author: Opendata Team
@@ -9,7 +9,7 @@ readme = "README.md"
9
9
 
10
10
  [tool.poetry]
11
11
  name = "udata-hydra"
12
- version = "2.0.0.dev2998"
12
+ version = "2.0.0.dev3046"
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"
@@ -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, Iterator, Union
8
+ from typing import Any, Iterator, Optional
9
9
 
10
10
  import sentry_sdk
11
11
  from csv_detective.detection import engine_to_file
@@ -100,9 +100,9 @@ async def notify_udata(check_id: int, table_name: str) -> None:
100
100
 
101
101
 
102
102
  async def analyse_csv(
103
- check_id: Union[int, None] = None,
104
- url: Union[str, None] = None,
105
- file_path: Union[str, None] = None,
103
+ check_id: Optional[int] = None,
104
+ url: Optional[str] = None,
105
+ file_path: Optional[str] = None,
106
106
  debug_insert: bool = False,
107
107
  ) -> None:
108
108
  """Launch csv analysis from a check or an URL (debug), using previsously downloaded file at file_path if any"""
@@ -289,7 +289,7 @@ async def csv_to_db_index(table_name: str, inspection: dict, check: dict) -> Non
289
289
  )
290
290
 
291
291
 
292
- async def perform_csv_inspection(file_path: str) -> Union[dict, None]:
292
+ async def perform_csv_inspection(file_path: str) -> Optional[dict]:
293
293
  """Launch csv-detective against given file"""
294
294
  try:
295
295
  return csv_detective_routine(
@@ -1,5 +1,5 @@
1
1
  from datetime import date, datetime
2
- from typing import Union
2
+ from typing import Optional
3
3
 
4
4
  from dateparser import parse as date_parser
5
5
  from dateutil.parser import ParserError
@@ -11,7 +11,7 @@ def to_json(value: str) -> str:
11
11
  return value
12
12
 
13
13
 
14
- def _parse_dt(value: str) -> Union[datetime, None]:
14
+ def _parse_dt(value: str) -> Optional[datetime]:
15
15
  """For performance reasons, we try first with dateutil and fallback on dateparser"""
16
16
  try:
17
17
  return dateutil_parser(value)
@@ -19,10 +19,10 @@ def _parse_dt(value: str) -> Union[datetime, None]:
19
19
  return date_parser(value)
20
20
 
21
21
 
22
- def to_date(value: str) -> Union[date, None]:
22
+ def to_date(value: str) -> Optional[date]:
23
23
  parsed = _parse_dt(value)
24
24
  return parsed.date() if parsed else None
25
25
 
26
26
 
27
- def to_datetime(value: str) -> Union[datetime, None]:
27
+ def to_datetime(value: str) -> Optional[datetime]:
28
28
  return _parse_dt(value)
@@ -3,7 +3,7 @@ import logging
3
3
  import os
4
4
  from datetime import datetime, timezone
5
5
  from enum import Enum
6
- from typing import Tuple, Union
6
+ from typing import Optional, Tuple
7
7
 
8
8
  import magic
9
9
  from dateparser import parse as date_parser
@@ -126,7 +126,7 @@ async def store_last_modified_date(change_analysis: dict, check_id: int) -> None
126
126
 
127
127
  async def detect_resource_change_from_checksum(
128
128
  resource_id, new_checksum
129
- ) -> Tuple[Change, Union[dict, None]]:
129
+ ) -> Tuple[Change, Optional[dict]]:
130
130
  """
131
131
  Checks if resource checksum has changed over time
132
132
  Returns a tuple with a Change status and an optional payload:
@@ -155,7 +155,7 @@ async def detect_resource_change_from_checksum(
155
155
 
156
156
  async def detect_resource_change_from_last_modified_header(
157
157
  data: dict,
158
- ) -> Tuple[Change, Union[dict, None]]:
158
+ ) -> Tuple[Change, Optional[dict]]:
159
159
  # last modified header check
160
160
 
161
161
  if len(data) == 1 and data[0]["last_modified"]:
@@ -179,7 +179,7 @@ async def detect_resource_change_from_last_modified_header(
179
179
 
180
180
  async def detect_resource_change_from_content_length_header(
181
181
  data: dict,
182
- ) -> Tuple[Change, Union[dict, None]]:
182
+ ) -> Tuple[Change, Optional[dict]]:
183
183
  # content-length variation between current and last check
184
184
  if len(data) <= 1 or not data[0]["content_length"]:
185
185
  return Change.NO_GUESS, None
@@ -194,7 +194,7 @@ async def detect_resource_change_from_content_length_header(
194
194
 
195
195
  async def detect_resource_change_on_early_hints(
196
196
  resource_id: str,
197
- ) -> Tuple[Change, Union[dict, None]]:
197
+ ) -> Tuple[Change, Optional[dict]]:
198
198
  """
199
199
  Try to guess if a resource has been modified from harvest and headers in check data:
200
200
  - last-modified header value if it can be found and parsed
@@ -245,7 +245,7 @@ async def detect_resource_change_on_early_hints(
245
245
 
246
246
  async def detect_resource_change_from_harvest(
247
247
  checks_data: dict, resource_id: str
248
- ) -> Tuple[Change, Union[dict, None]]:
248
+ ) -> Tuple[Change, Optional[dict]]:
249
249
  """
250
250
  Checks if resource has a harvest.modified_at
251
251
  Returns a tuple with a Change status and an optional payload:
@@ -4,7 +4,7 @@ import os
4
4
  from datetime import datetime, timezone
5
5
  from pathlib import Path
6
6
  from tempfile import NamedTemporaryFile
7
- from typing import Union
7
+ from typing import Optional
8
8
 
9
9
  import aiohttp
10
10
  import asyncpg
@@ -47,7 +47,9 @@ async def connection(db_name: str = "main"):
47
47
 
48
48
 
49
49
  @cli
50
- async def load_catalog(url=None, drop_meta=False, drop_all=False, quiet=False):
50
+ async def load_catalog(
51
+ url: Optional[str] = None, drop_meta: bool = False, drop_all: bool = False, quiet: bool = False
52
+ ):
51
53
  """Load the catalog into DB from CSV file
52
54
 
53
55
  :url: URL of the catalog to fetch, by default defined in config
@@ -135,7 +137,7 @@ async def check_url(url: str, method: str = "get"):
135
137
 
136
138
 
137
139
  @cli
138
- async def check_resource(resource_id, method: str = "get"):
140
+ async def check_resource(resource_id: str, method: str = "get"):
139
141
  """Trigger a complete check for a given resource_id"""
140
142
  res = await Resource.get(resource_id)
141
143
  if not res:
@@ -147,14 +149,14 @@ async def check_resource(resource_id, method: str = "get"):
147
149
 
148
150
  @cli(name="analyse-csv")
149
151
  async def analyse_csv_cli(
150
- check_id: Union[int, None] = None, url: Union[str, None] = None, debug_insert: bool = False
152
+ check_id: Optional[int] = None, url: Optional[str] = None, debug_insert: bool = False
151
153
  ):
152
154
  """Trigger a csv analysis from a check_id or an url"""
153
155
  await analyse_csv(check_id=check_id, url=url, debug_insert=debug_insert)
154
156
 
155
157
 
156
158
  @cli
157
- async def csv_sample(size=1000, download: bool = False, max_size: str = "100M"):
159
+ async def csv_sample(size: int = 1000, download: bool = False, max_size: str = "100M"):
158
160
  """Get a csv sample from latest checks
159
161
 
160
162
  :size: Size of the sample (how many files to query)
@@ -12,7 +12,7 @@ TESTING = false
12
12
  MAX_POOL_SIZE = 50
13
13
  USER_AGENT = "udata-hydra/1.0"
14
14
 
15
- API_TOKEN = "to-change"
15
+ API_KEY = "hydra_api_key_to_change"
16
16
 
17
17
  # -- crawler settings -- #
18
18
 
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from typing import Union
2
+ from typing import Optional
3
3
  from unittest.mock import MagicMock
4
4
 
5
5
  import asyncpg
@@ -36,7 +36,7 @@ async def pool(db: str = "main") -> asyncpg.pool.Pool:
36
36
  return context["databases"][db]
37
37
 
38
38
 
39
- def queue(name: str = "default") -> Union[Queue, None]:
39
+ def queue(name: str = "default") -> Optional[Queue]:
40
40
  if not context["queues"].get(name):
41
41
  # we dont need a queue while testing, make sure we're not using a real Redis connection
42
42
  if config.TESTING:
@@ -3,7 +3,7 @@ import json
3
3
  import time
4
4
  from collections import defaultdict
5
5
  from datetime import datetime, timedelta, timezone
6
- from typing import Tuple, Union
6
+ from typing import Optional, Tuple
7
7
  from urllib.parse import urlparse
8
8
 
9
9
  import aiohttp
@@ -26,7 +26,7 @@ STATUS_BACKOFF = "backoff"
26
26
  log = setup_logging()
27
27
 
28
28
 
29
- def is_valid_status(status: str) -> Union[bool, None]:
29
+ def is_valid_status(status: str) -> Optional[bool]:
30
30
  if not status:
31
31
  return False
32
32
  status_nb = int(status)
@@ -52,7 +52,7 @@ async def get_content_type_from_header(headers: dict) -> str:
52
52
 
53
53
 
54
54
  async def compute_check_has_changed(check_data: dict, last_check: dict) -> bool:
55
- is_first_check = not last_check
55
+ is_first_check: bool = last_check is None
56
56
  status_has_changed = last_check and check_data.get("status") != last_check.get("status")
57
57
  status_no_longer_available = (
58
58
  last_check
@@ -285,6 +285,7 @@ async def check_url(
285
285
  }
286
286
  )
287
287
 
288
+ # Analyse the resource
288
289
  queue.enqueue(process_resource, check_id, is_first_check, _priority="low")
289
290
 
290
291
  return STATUS_OK
@@ -1,4 +1,4 @@
1
- from typing import Union
1
+ from typing import Optional
2
2
 
3
3
  from udata_hydra import context
4
4
  from udata_hydra.db import (
@@ -12,7 +12,7 @@ class Check:
12
12
  """Represents a check in the "checks" DB table"""
13
13
 
14
14
  @classmethod
15
- async def get(cls, check_id: int) -> dict:
15
+ async def get(cls, check_id: int) -> Optional[dict]:
16
16
  pool = await context.pool()
17
17
  async with pool.acquire() as connection:
18
18
  q = """
@@ -20,13 +20,12 @@ class Check:
20
20
  ON catalog.last_check = checks.id
21
21
  WHERE checks.id = $1;
22
22
  """
23
- check = await connection.fetchrow(q, check_id)
24
- return check
23
+ return await connection.fetchrow(q, check_id)
25
24
 
26
25
  @classmethod
27
26
  async def get_latest(
28
- cls, url: Union[str, None], resource_id: Union[str, None]
29
- ) -> Union[dict, None]:
27
+ cls, url: Optional[str] = None, resource_id: Optional[str] = None
28
+ ) -> Optional[dict]:
30
29
  column: str = "url" if url else "resource_id"
31
30
  pool = await context.pool()
32
31
  async with pool.acquire() as connection:
@@ -41,8 +40,8 @@ class Check:
41
40
 
42
41
  @classmethod
43
42
  async def get_all(
44
- cls, url: Union[str, None], resource_id: Union[str, None]
45
- ) -> Union[list, None]:
43
+ cls, url: Optional[str] = None, resource_id: Optional[str] = None
44
+ ) -> Optional[list]:
46
45
  column: str = "url" if url else "resource_id"
47
46
  pool = await context.pool()
48
47
  async with pool.acquire() as connection:
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import os
3
- from typing import Union
3
+ from typing import Optional
4
4
 
5
5
  import coloredlogs
6
6
  import sentry_sdk
@@ -18,7 +18,7 @@ def setup_logging() -> logging.Logger:
18
18
  if context.get("inited"):
19
19
  return log
20
20
  release = "hydra@unknown"
21
- app_version: Union[str, None] = get_app_version()
21
+ app_version: Optional[str] = get_app_version()
22
22
  if app_version:
23
23
  release = f"hydra@{app_version}"
24
24
  if config.SENTRY_DSN:
@@ -1,4 +1,4 @@
1
- from typing import Union
1
+ from typing import Optional
2
2
 
3
3
  from aiohttp import web
4
4
 
@@ -10,7 +10,7 @@ from udata_hydra.utils import get_request_params
10
10
  async def get_latest_check(request: web.Request) -> web.Response:
11
11
  """Get the latest check for a given URL or resource_id"""
12
12
  url, resource_id = get_request_params(request, params_names=["url", "resource_id"])
13
- data: Union[dict, None] = await Check.get_latest(url, resource_id)
13
+ data: Optional[dict] = await Check.get_latest(url, resource_id)
14
14
  if not data:
15
15
  raise web.HTTPNotFound()
16
16
  if data["deleted"]:
@@ -20,7 +20,7 @@ async def get_latest_check(request: web.Request) -> web.Response:
20
20
 
21
21
  async def get_all_checks(request: web.Request) -> web.Response:
22
22
  url, resource_id = get_request_params(request, params_names=["url", "resource_id"])
23
- data: Union[list, None] = await Check.get_all(url, resource_id)
23
+ data: Optional[list] = await Check.get_all(url, resource_id)
24
24
  if not data:
25
25
  raise web.HTTPNotFound()
26
26
  return web.json_response([CheckSchema().dump(dict(r)) for r in data])
@@ -1,12 +1,12 @@
1
1
  import logging
2
- from typing import Union
2
+ from typing import Optional
3
3
 
4
4
  import toml
5
5
 
6
6
  log = logging.getLogger("udata-hydra")
7
7
 
8
8
 
9
- def get_app_version() -> Union[str, None]:
9
+ def get_app_version() -> Optional[str]:
10
10
  """Get the app version from pyproject.toml"""
11
11
  try:
12
12
  pyproject: dict = toml.load("pyproject.toml")
@@ -62,8 +62,8 @@ def token_auth_middleware(
62
62
  reason="Invalid token scheme",
63
63
  )
64
64
 
65
- if token == config.API_TOKEN:
66
- request[request_property] = {"username": "admin"}
65
+ if token == config.API_KEY:
66
+ request[request_property] = {"username": "udata"}
67
67
  else:
68
68
  raise web.HTTPForbidden(reason="Invalid authentication token")
69
69
 
@@ -2,7 +2,7 @@ import gzip
2
2
  import hashlib
3
3
  import logging
4
4
  import tempfile
5
- from typing import IO, Union
5
+ from typing import IO, Optional
6
6
 
7
7
  import aiohttp
8
8
  import magic
@@ -33,7 +33,7 @@ def read_csv_gz(file_path: str) -> IO[bytes]:
33
33
  async def download_resource(
34
34
  url: str,
35
35
  headers: dict,
36
- max_size_allowed: Union[int, None],
36
+ max_size_allowed: Optional[int],
37
37
  ) -> IO[bytes]:
38
38
  """
39
39
  Attempts downloading a resource from a given url.