udata-hydra 2.0.0.dev3084__tar.gz → 2.0.0.dev3146__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.dev3084 → udata_hydra-2.0.0.dev3146}/PKG-INFO +17 -1
  2. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/README.md +16 -0
  3. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/pyproject.toml +1 -1
  4. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/cli.py +3 -1
  5. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/crawl.py +30 -15
  6. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/db/check.py +4 -4
  7. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/routes/__init__.py +2 -1
  8. udata_hydra-2.0.0.dev3146/udata_hydra/routes/checks.py +68 -0
  9. udata_hydra-2.0.0.dev3084/udata_hydra/routes/checks.py +0 -26
  10. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/__init__.py +0 -0
  11. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/analysis/__init__.py +0 -0
  12. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/analysis/csv.py +0 -0
  13. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/analysis/errors.py +0 -0
  14. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/analysis/helpers.py +0 -0
  15. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/analysis/resource.py +0 -0
  16. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/app.py +0 -0
  17. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/config_default.toml +0 -0
  18. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/context.py +0 -0
  19. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/db/__init__.py +0 -0
  20. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/db/resource.py +0 -0
  21. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/logger.py +0 -0
  22. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/__init__.py +0 -0
  23. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
  24. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
  25. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
  26. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
  27. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
  28. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
  29. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
  30. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
  31. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
  32. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
  33. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
  34. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
  35. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
  36. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
  37. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
  38. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
  39. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/routes/resources.py +0 -0
  40. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/routes/status.py +0 -0
  41. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/schemas/__init__.py +0 -0
  42. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/schemas/check.py +0 -0
  43. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/schemas/resource.py +0 -0
  44. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/__init__.py +0 -0
  45. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/auth.py +0 -0
  46. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/csv.py +0 -0
  47. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/file.py +0 -0
  48. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/http.py +0 -0
  49. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/minio.py +0 -0
  50. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/parquet.py +0 -0
  51. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/queue.py +0 -0
  52. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/reader.py +0 -0
  53. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/udata_hydra/utils/timer.py +0 -0
  54. {udata_hydra-2.0.0.dev3084 → udata_hydra-2.0.0.dev3146}/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.dev3084
3
+ Version: 2.0.0.dev3146
4
4
  Summary: Async crawler and parsing service for data.gouv.fr
5
5
  License: MIT
6
6
  Author: Opendata Team
@@ -124,6 +124,22 @@ You can also override the coverage report configuration when running the tests b
124
124
 
125
125
  ## API
126
126
 
127
+ The API will need a Bearer token for each request on protected endpoints (any endpoint that isn't a `GET`).
128
+ The token is configured in the `config.toml` file as `API_KEY`, and has a default value set in the `udata_hydra/config_default.toml` file.
129
+
130
+ If you're using hydra as an external service to receive resource events from [udata](https://github.com/opendatateam/udata), then udata needs to also configure this
131
+ API key in its `udata.cfg` file:
132
+
133
+ ```
134
+ # Wether udata should publish the resource events
135
+ PUBLISH_ON_RESOURCE_EVENTS = True
136
+ # Where to publish the events
137
+ RESOURCES_ANALYSER_URI = "http://localhost:8000"
138
+ # The API key that hydra needs
139
+ RESOURCES_ANALYSER_API_KEY = "api_key_to_change"
140
+
141
+ ```
142
+
127
143
  ### Run
128
144
 
129
145
  ```bash
@@ -87,6 +87,22 @@ You can also override the coverage report configuration when running the tests b
87
87
 
88
88
  ## API
89
89
 
90
+ The API will need a Bearer token for each request on protected endpoints (any endpoint that isn't a `GET`).
91
+ The token is configured in the `config.toml` file as `API_KEY`, and has a default value set in the `udata_hydra/config_default.toml` file.
92
+
93
+ If you're using hydra as an external service to receive resource events from [udata](https://github.com/opendatateam/udata), then udata needs to also configure this
94
+ API key in its `udata.cfg` file:
95
+
96
+ ```
97
+ # Wether udata should publish the resource events
98
+ PUBLISH_ON_RESOURCE_EVENTS = True
99
+ # Where to publish the events
100
+ RESOURCES_ANALYSER_URI = "http://localhost:8000"
101
+ # The API key that hydra needs
102
+ RESOURCES_ANALYSER_API_KEY = "api_key_to_change"
103
+
104
+ ```
105
+
90
106
  ### Run
91
107
 
92
108
  ```bash
@@ -9,7 +9,7 @@ readme = "README.md"
9
9
 
10
10
  [tool.poetry]
11
11
  name = "udata-hydra"
12
- version = "2.0.0.dev3084"
12
+ version = "2.0.0.dev3146"
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"
@@ -144,7 +144,9 @@ async def check_resource(resource_id: str, method: str = "get"):
144
144
  log.error("Resource not found in catalog")
145
145
  return
146
146
  async with aiohttp.ClientSession(timeout=None) as session:
147
- await crawl_check_url(url=res[0], resource_id=None, session=session, method=method)
147
+ await crawl_check_url(
148
+ url=res[0], resource_id=None, session=session, method=method, worker_priority="high"
149
+ )
148
150
 
149
151
 
150
152
  @cli(name="analyse-csv")
@@ -18,10 +18,12 @@ from udata_hydra.utils import queue, send
18
18
 
19
19
  results: defaultdict = defaultdict(int)
20
20
 
21
- STATUS_OK = "ok"
22
- STATUS_TIMEOUT = "timeout"
23
- STATUS_ERROR = "error"
24
- STATUS_BACKOFF = "backoff"
21
+ RESOURCE_RESPONSE_STATUSES = {
22
+ "OK": "ok",
23
+ "TIMEOUT": "timeout",
24
+ "ERROR": "error",
25
+ "BACKOFF": "backoff",
26
+ }
25
27
 
26
28
  log = setup_logging()
27
29
 
@@ -235,7 +237,12 @@ def has_nice_head(resp) -> bool:
235
237
 
236
238
 
237
239
  async def check_url(
238
- url: str, resource_id: str, session, sleep: float = 0, method: str = "head"
240
+ url: str,
241
+ resource_id: str,
242
+ session,
243
+ sleep: float = 0,
244
+ method: str = "head",
245
+ worker_priority: str = "default",
239
246
  ) -> str:
240
247
  log.debug(f"check {url}, sleep {sleep}, method {method}")
241
248
 
@@ -254,14 +261,14 @@ async def check_url(
254
261
  "timeout": False,
255
262
  }
256
263
  )
257
- return STATUS_ERROR
264
+ return RESOURCE_RESPONSE_STATUSES["ERROR"]
258
265
 
259
266
  should_backoff, reason = await is_backoff(domain)
260
267
  if should_backoff:
261
268
  log.info(f"backoff {domain} ({reason})")
262
269
  # skip this URL, it will come back in a next batch
263
270
  await Resource.update(resource_id=resource_id, data={"status": None, "priority": False})
264
- return STATUS_BACKOFF
271
+ return RESOURCE_RESPONSE_STATUSES["BACKOFF"]
265
272
 
266
273
  try:
267
274
  start = time.time()
@@ -270,7 +277,9 @@ async def check_url(
270
277
  async with _method(url, timeout=timeout, allow_redirects=True) as resp:
271
278
  end = time.time()
272
279
  if method != "get" and not has_nice_head(resp):
273
- return await check_url(url, resource_id, session, method="get")
280
+ return await check_url(
281
+ url, resource_id, session, method="get", worker_priority=worker_priority
282
+ )
274
283
  resp.raise_for_status()
275
284
 
276
285
  check_id, is_first_check = await process_check_data(
@@ -282,13 +291,12 @@ async def check_url(
282
291
  "headers": convert_headers(resp.headers),
283
292
  "timeout": False,
284
293
  "response_time": end - start,
285
- }
294
+ },
286
295
  )
287
296
 
288
- # Analyse the resource
289
- queue.enqueue(process_resource, check_id, is_first_check, _priority="low")
297
+ queue.enqueue(process_resource, check_id, is_first_check, _priority=worker_priority)
290
298
 
291
- return STATUS_OK
299
+ return RESOURCE_RESPONSE_STATUSES["OK"]
292
300
  except asyncio.exceptions.TimeoutError:
293
301
  await process_check_data(
294
302
  {
@@ -298,7 +306,7 @@ async def check_url(
298
306
  "timeout": True,
299
307
  }
300
308
  )
301
- return STATUS_TIMEOUT
309
+ return RESOURCE_RESPONSE_STATUSES["TIMEOUT"]
302
310
  # TODO: debug AssertionError, should be caught in DB now
303
311
  # File "[...]aiohttp/connector.py", line 991, in _create_direct_connection
304
312
  # assert port is not None
@@ -322,7 +330,7 @@ async def check_url(
322
330
  }
323
331
  )
324
332
  log.warning(f"Crawling error for url {url}", exc_info=e)
325
- return STATUS_ERROR
333
+ return RESOURCE_RESPONSE_STATUSES["ERROR"]
326
334
 
327
335
 
328
336
  async def crawl_urls(to_parse: list[str]) -> None:
@@ -332,7 +340,14 @@ async def crawl_urls(to_parse: list[str]) -> None:
332
340
  timeout=None, headers={"user-agent": config.USER_AGENT}
333
341
  ) as session:
334
342
  for row in to_parse:
335
- tasks.append(check_url(url=row["url"], resource_id=row["resource_id"], session=session))
343
+ tasks.append(
344
+ check_url(
345
+ url=row["url"],
346
+ resource_id=row["resource_id"],
347
+ session=session,
348
+ worker_priority="low",
349
+ )
350
+ )
336
351
  for task in asyncio.as_completed(tasks):
337
352
  result = await task
338
353
  results[result] += 1
@@ -62,12 +62,12 @@ class Check:
62
62
  This use the info from the last check of the same resource
63
63
  """
64
64
  data = convert_dict_values_to_json(data)
65
- q = compute_insert_query(table_name="checks", data=data)
65
+ q1: str = compute_insert_query(table_name="checks", data=data)
66
66
  pool = await context.pool()
67
67
  async with pool.acquire() as connection:
68
- last_check = await connection.fetchrow(q, *data.values())
69
- q = """UPDATE catalog SET last_check = $1 WHERE resource_id = $2"""
70
- await connection.execute(q, last_check["id"], data["resource_id"])
68
+ last_check = await connection.fetchrow(q1, *data.values())
69
+ q2 = """UPDATE catalog SET last_check = $1 WHERE resource_id = $2"""
70
+ await connection.execute(q2, last_check["id"], data["resource_id"])
71
71
  return last_check["id"]
72
72
 
73
73
  @classmethod
@@ -1,6 +1,6 @@
1
1
  from aiohttp import web
2
2
 
3
- from udata_hydra.routes.checks import get_all_checks, get_latest_check
3
+ from udata_hydra.routes.checks import create_check, get_all_checks, get_latest_check
4
4
  from udata_hydra.routes.resources import (
5
5
  create_resource,
6
6
  delete_resource,
@@ -14,6 +14,7 @@ routes: list = [
14
14
  # Routes for checks
15
15
  web.get("/api/checks/latest/", get_latest_check),
16
16
  web.get("/api/checks/all/", get_all_checks),
17
+ web.post("/api/checks/", create_check),
17
18
  # Routes for resources
18
19
  web.get("/api/resources/", get_resource),
19
20
  web.post("/api/resources/", create_resource),
@@ -0,0 +1,68 @@
1
+ import json
2
+ from typing import Optional
3
+
4
+ import aiohttp
5
+ from aiohttp import web
6
+ from marshmallow import ValidationError
7
+
8
+ from udata_hydra import config, context
9
+ from udata_hydra.crawl import RESOURCE_RESPONSE_STATUSES, check_url
10
+ from udata_hydra.db.check import Check
11
+ from udata_hydra.db.resource import Resource
12
+ from udata_hydra.schemas import CheckSchema
13
+ from udata_hydra.utils import get_request_params
14
+
15
+
16
+ async def get_latest_check(request: web.Request) -> web.Response:
17
+ """Get the latest check for a given URL or resource_id"""
18
+ url, resource_id = get_request_params(request, params_names=["url", "resource_id"])
19
+ data: Optional[dict] = await Check.get_latest(url, resource_id)
20
+ if not data:
21
+ raise web.HTTPNotFound()
22
+ if data["deleted"]:
23
+ raise web.HTTPGone()
24
+ return web.json_response(CheckSchema().dump(dict(data)))
25
+
26
+
27
+ async def get_all_checks(request: web.Request) -> web.Response:
28
+ url, resource_id = get_request_params(request, params_names=["url", "resource_id"])
29
+ data: Optional[list] = await Check.get_all(url, resource_id)
30
+ if not data:
31
+ raise web.HTTPNotFound()
32
+ return web.json_response([CheckSchema().dump(dict(r)) for r in data])
33
+
34
+
35
+ async def create_check(request: web.Request) -> web.Response:
36
+ """Create a new check"""
37
+
38
+ # Get resource_id from request
39
+ try:
40
+ payload: dict = await request.json()
41
+ resource_id: str = payload["resource_id"]
42
+ except ValidationError as err:
43
+ raise web.HTTPBadRequest(text=json.dumps(err.messages))
44
+ except KeyError as e:
45
+ raise web.HTTPBadRequest(text=f"Missing key: {e}")
46
+
47
+ # Get URL from resource_id
48
+ try:
49
+ resource: dict = await Resource.get(resource_id, "url")
50
+ url: str = resource["url"]
51
+ except Exception:
52
+ raise web.HTTPNotFound(text=f"Couldn't find URL for resource {resource_id}")
53
+
54
+ context.monitor().set_status(f'Crawling url "{url}"...')
55
+
56
+ async with aiohttp.ClientSession(
57
+ timeout=None, headers={"user-agent": config.USER_AGENT}
58
+ ) as session:
59
+ status: str = await check_url(
60
+ url=url, resource_id=resource_id, session=session, worker_priority="high"
61
+ )
62
+ context.monitor().refresh(status)
63
+
64
+ check: Optional[dict] = await Check.get_latest(url, resource_id)
65
+ if not check:
66
+ raise web.HTTPBadRequest(text=f"Check not created, status: {status}")
67
+
68
+ return web.json_response(CheckSchema().dump(dict(check)))
@@ -1,26 +0,0 @@
1
- from typing import Optional
2
-
3
- from aiohttp import web
4
-
5
- from udata_hydra.db.check import Check
6
- from udata_hydra.schemas import CheckSchema
7
- from udata_hydra.utils import get_request_params
8
-
9
-
10
- async def get_latest_check(request: web.Request) -> web.Response:
11
- """Get the latest check for a given URL or resource_id"""
12
- url, resource_id = get_request_params(request, params_names=["url", "resource_id"])
13
- data: Optional[dict] = await Check.get_latest(url, resource_id)
14
- if not data:
15
- raise web.HTTPNotFound()
16
- if data["deleted"]:
17
- raise web.HTTPGone()
18
- return web.json_response(CheckSchema().dump(dict(data)))
19
-
20
-
21
- async def get_all_checks(request: web.Request) -> web.Response:
22
- url, resource_id = get_request_params(request, params_names=["url", "resource_id"])
23
- data: Optional[list] = await Check.get_all(url, resource_id)
24
- if not data:
25
- raise web.HTTPNotFound()
26
- return web.json_response([CheckSchema().dump(dict(r)) for r in data])