udata-hydra 2.0.0.dev3146__tar.gz → 2.0.0.dev3158__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.dev3146 → udata_hydra-2.0.0.dev3158}/PKG-INFO +1 -1
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/pyproject.toml +1 -1
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/cli.py +1 -3
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/crawl.py +15 -30
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/db/check.py +4 -4
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/routes/__init__.py +1 -2
- udata_hydra-2.0.0.dev3158/udata_hydra/routes/checks.py +26 -0
- udata_hydra-2.0.0.dev3146/udata_hydra/routes/checks.py +0 -68
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/README.md +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/__init__.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/analysis/__init__.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/analysis/csv.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/analysis/errors.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/analysis/helpers.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/analysis/resource.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/app.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/config_default.toml +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/context.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/db/__init__.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/db/resource.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/logger.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/__init__.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/routes/resources.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/routes/status.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/schemas/__init__.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/schemas/check.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/schemas/resource.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/__init__.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/auth.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/csv.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/file.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/http.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/minio.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/parquet.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/queue.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/reader.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/utils/timer.py +0 -0
- {udata_hydra-2.0.0.dev3146 → udata_hydra-2.0.0.dev3158}/udata_hydra/worker.py +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.dev3158"
|
|
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,9 +144,7 @@ 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(
|
|
148
|
-
url=res[0], resource_id=None, session=session, method=method, worker_priority="high"
|
|
149
|
-
)
|
|
147
|
+
await crawl_check_url(url=res[0], resource_id=None, session=session, method=method)
|
|
150
148
|
|
|
151
149
|
|
|
152
150
|
@cli(name="analyse-csv")
|
|
@@ -18,12 +18,10 @@ from udata_hydra.utils import queue, send
|
|
|
18
18
|
|
|
19
19
|
results: defaultdict = defaultdict(int)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"BACKOFF": "backoff",
|
|
26
|
-
}
|
|
21
|
+
STATUS_OK = "ok"
|
|
22
|
+
STATUS_TIMEOUT = "timeout"
|
|
23
|
+
STATUS_ERROR = "error"
|
|
24
|
+
STATUS_BACKOFF = "backoff"
|
|
27
25
|
|
|
28
26
|
log = setup_logging()
|
|
29
27
|
|
|
@@ -237,12 +235,7 @@ def has_nice_head(resp) -> bool:
|
|
|
237
235
|
|
|
238
236
|
|
|
239
237
|
async def check_url(
|
|
240
|
-
url: str,
|
|
241
|
-
resource_id: str,
|
|
242
|
-
session,
|
|
243
|
-
sleep: float = 0,
|
|
244
|
-
method: str = "head",
|
|
245
|
-
worker_priority: str = "default",
|
|
238
|
+
url: str, resource_id: str, session, sleep: float = 0, method: str = "head"
|
|
246
239
|
) -> str:
|
|
247
240
|
log.debug(f"check {url}, sleep {sleep}, method {method}")
|
|
248
241
|
|
|
@@ -261,14 +254,14 @@ async def check_url(
|
|
|
261
254
|
"timeout": False,
|
|
262
255
|
}
|
|
263
256
|
)
|
|
264
|
-
return
|
|
257
|
+
return STATUS_ERROR
|
|
265
258
|
|
|
266
259
|
should_backoff, reason = await is_backoff(domain)
|
|
267
260
|
if should_backoff:
|
|
268
261
|
log.info(f"backoff {domain} ({reason})")
|
|
269
262
|
# skip this URL, it will come back in a next batch
|
|
270
263
|
await Resource.update(resource_id=resource_id, data={"status": None, "priority": False})
|
|
271
|
-
return
|
|
264
|
+
return STATUS_BACKOFF
|
|
272
265
|
|
|
273
266
|
try:
|
|
274
267
|
start = time.time()
|
|
@@ -277,9 +270,7 @@ async def check_url(
|
|
|
277
270
|
async with _method(url, timeout=timeout, allow_redirects=True) as resp:
|
|
278
271
|
end = time.time()
|
|
279
272
|
if method != "get" and not has_nice_head(resp):
|
|
280
|
-
return await check_url(
|
|
281
|
-
url, resource_id, session, method="get", worker_priority=worker_priority
|
|
282
|
-
)
|
|
273
|
+
return await check_url(url, resource_id, session, method="get")
|
|
283
274
|
resp.raise_for_status()
|
|
284
275
|
|
|
285
276
|
check_id, is_first_check = await process_check_data(
|
|
@@ -291,12 +282,13 @@ async def check_url(
|
|
|
291
282
|
"headers": convert_headers(resp.headers),
|
|
292
283
|
"timeout": False,
|
|
293
284
|
"response_time": end - start,
|
|
294
|
-
}
|
|
285
|
+
}
|
|
295
286
|
)
|
|
296
287
|
|
|
297
|
-
|
|
288
|
+
# Analyse the resource
|
|
289
|
+
queue.enqueue(process_resource, check_id, is_first_check, _priority="low")
|
|
298
290
|
|
|
299
|
-
return
|
|
291
|
+
return STATUS_OK
|
|
300
292
|
except asyncio.exceptions.TimeoutError:
|
|
301
293
|
await process_check_data(
|
|
302
294
|
{
|
|
@@ -306,7 +298,7 @@ async def check_url(
|
|
|
306
298
|
"timeout": True,
|
|
307
299
|
}
|
|
308
300
|
)
|
|
309
|
-
return
|
|
301
|
+
return STATUS_TIMEOUT
|
|
310
302
|
# TODO: debug AssertionError, should be caught in DB now
|
|
311
303
|
# File "[...]aiohttp/connector.py", line 991, in _create_direct_connection
|
|
312
304
|
# assert port is not None
|
|
@@ -330,7 +322,7 @@ async def check_url(
|
|
|
330
322
|
}
|
|
331
323
|
)
|
|
332
324
|
log.warning(f"Crawling error for url {url}", exc_info=e)
|
|
333
|
-
return
|
|
325
|
+
return STATUS_ERROR
|
|
334
326
|
|
|
335
327
|
|
|
336
328
|
async def crawl_urls(to_parse: list[str]) -> None:
|
|
@@ -340,14 +332,7 @@ async def crawl_urls(to_parse: list[str]) -> None:
|
|
|
340
332
|
timeout=None, headers={"user-agent": config.USER_AGENT}
|
|
341
333
|
) as session:
|
|
342
334
|
for row in to_parse:
|
|
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
|
-
)
|
|
335
|
+
tasks.append(check_url(url=row["url"], resource_id=row["resource_id"], session=session))
|
|
351
336
|
for task in asyncio.as_completed(tasks):
|
|
352
337
|
result = await task
|
|
353
338
|
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
|
-
|
|
65
|
+
q = 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(
|
|
69
|
-
|
|
70
|
-
await connection.execute(
|
|
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"])
|
|
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
|
|
3
|
+
from udata_hydra.routes.checks import get_all_checks, get_latest_check
|
|
4
4
|
from udata_hydra.routes.resources import (
|
|
5
5
|
create_resource,
|
|
6
6
|
delete_resource,
|
|
@@ -14,7 +14,6 @@ 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),
|
|
18
17
|
# Routes for resources
|
|
19
18
|
web.get("/api/resources/", get_resource),
|
|
20
19
|
web.post("/api/resources/", create_resource),
|
|
@@ -0,0 +1,26 @@
|
|
|
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])
|
|
@@ -1,68 +0,0 @@
|
|
|
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)))
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|