udata-hydra 2.0.0.dev1648__tar.gz → 2.0.0.dev1729__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.dev1648 → udata_hydra-2.0.0.dev1729}/PKG-INFO +33 -8
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/README.md +32 -7
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/pyproject.toml +14 -3
- udata_hydra-2.0.0.dev1729/setup.py +59 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/analysis/csv.py +61 -32
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/analysis/errors.py +0 -1
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/analysis/helpers.py +2 -1
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/analysis/resource.py +42 -24
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/app.py +9 -23
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/cli.py +8 -17
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/context.py +5 -9
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/crawl.py +35 -23
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/logger.py +0 -1
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/__init__.py +0 -1
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/csv.py +8 -8
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/file.py +12 -6
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/http.py +7 -2
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/minio.py +2 -7
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/reader.py +4 -3
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/timer.py +1 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/worker.py +0 -1
- udata_hydra-2.0.0.dev1648/setup.py +0 -59
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/__init__.py +1 -1
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/analysis/__init__.py +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/config_default.toml +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/__init__.py +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/db.py +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/json.py +0 -0
- {udata_hydra-2.0.0.dev1648 → udata_hydra-2.0.0.dev1729}/udata_hydra/utils/queue.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.dev1729
|
|
4
4
|
Summary: Async crawler and parsing service for data.gouv.fr
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Opendata Team
|
|
@@ -45,6 +45,17 @@ Since it's called _hydra_, it also has mythical powers embedded:
|
|
|
45
45
|
- if the remote resource is a CSV, convert it to a PostgreSQL table, ready for APIfication
|
|
46
46
|
- send crawl and analysis info to a udata instance
|
|
47
47
|
|
|
48
|
+
## Architecture schema
|
|
49
|
+
|
|
50
|
+
The architecture for the full workflow is the following:
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
The hydra crawler is one of the components of the architecture. It will check if resource is available, analyze the type of file if the resource has been modified, and analyze the CSV content. It will also convert CSV resources to database tables and send the data to a udata instance.
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
48
59
|
## CLI
|
|
49
60
|
|
|
50
61
|
### Create database structure
|
|
@@ -62,7 +73,7 @@ Install udata-hydra dependencies and cli.
|
|
|
62
73
|
|
|
63
74
|
`poetry run udata-hydra-crawl`
|
|
64
75
|
|
|
65
|
-
It will crawl (forever) the catalog according to config set in `config.
|
|
76
|
+
It will crawl (forever) the catalog according to config set in `udata_hydra/config.toml`, with a default config in `udata_hydra/config_default.toml`.
|
|
66
77
|
|
|
67
78
|
`BATCH_SIZE` URLs are queued at each loop run.
|
|
68
79
|
|
|
@@ -90,7 +101,7 @@ Converted CSV tables will be stored in the database specified via `config.DATABA
|
|
|
90
101
|
|
|
91
102
|
### Run
|
|
92
103
|
|
|
93
|
-
```
|
|
104
|
+
```bash
|
|
94
105
|
poetry install
|
|
95
106
|
poetry run adev runserver udata_hydra/app.py
|
|
96
107
|
```
|
|
@@ -99,7 +110,7 @@ poetry run adev runserver udata_hydra/app.py
|
|
|
99
110
|
|
|
100
111
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
101
112
|
|
|
102
|
-
```
|
|
113
|
+
```bash
|
|
103
114
|
$ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.saintdenis.re/datasets/661e19974bcc48849bbff7c9637c5c28_1.csv" | json_pp
|
|
104
115
|
{
|
|
105
116
|
"status" : 200,
|
|
@@ -136,7 +147,7 @@ $ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.sain
|
|
|
136
147
|
|
|
137
148
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
138
149
|
|
|
139
|
-
```
|
|
150
|
+
```bash
|
|
140
151
|
$ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv.fr/IMG/xls/er864.xls" | json_pp
|
|
141
152
|
[
|
|
142
153
|
{
|
|
@@ -174,7 +185,7 @@ $ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv
|
|
|
174
185
|
|
|
175
186
|
### Get crawling status
|
|
176
187
|
|
|
177
|
-
```
|
|
188
|
+
```bash
|
|
178
189
|
$ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
179
190
|
{
|
|
180
191
|
"fresh_checks_percentage" : 0.4,
|
|
@@ -187,7 +198,7 @@ $ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
|
187
198
|
|
|
188
199
|
### Get worker status
|
|
189
200
|
|
|
190
|
-
```
|
|
201
|
+
```bash
|
|
191
202
|
$ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
192
203
|
{
|
|
193
204
|
"queued" : {
|
|
@@ -200,7 +211,7 @@ $ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
|
200
211
|
|
|
201
212
|
### Get crawling stats
|
|
202
213
|
|
|
203
|
-
```
|
|
214
|
+
```bash
|
|
204
215
|
$ curl -s "http://localhost:8000/api/stats/" | json_pp
|
|
205
216
|
{
|
|
206
217
|
"status" : [
|
|
@@ -313,3 +324,17 @@ Refer to each section to learn how to launch them. The only differences from dev
|
|
|
313
324
|
- use `HYDRA_SETTINGS` env var to point to your custom `config.toml`
|
|
314
325
|
- use `HYDRA_APP_SOCKET_PATH` to configure where aiohttp should listen to a [reverse proxy connection (eg nginx)](https://docs.aiohttp.org/en/stable/deployment.html#nginx-configuration) and use `udata-hydra-app` to launch the app server
|
|
315
326
|
|
|
327
|
+
## Contributing
|
|
328
|
+
|
|
329
|
+
Before contributing to the repository and making any PR, it is necessary to initialize the pre-commit hooks:
|
|
330
|
+
```bash
|
|
331
|
+
pre-commit install
|
|
332
|
+
```
|
|
333
|
+
Once this is done, code formatting and linting, as well as import sorting, will be automatically checked before each commit.
|
|
334
|
+
|
|
335
|
+
If you cannot use pre-commit, it is necessary to format, lint, and sort imports with [Ruff](https://docs.astral.sh/ruff/) before committing:
|
|
336
|
+
```bash
|
|
337
|
+
ruff check --fix --select I .
|
|
338
|
+
ruff format .
|
|
339
|
+
```
|
|
340
|
+
|
|
@@ -9,6 +9,17 @@ Since it's called _hydra_, it also has mythical powers embedded:
|
|
|
9
9
|
- if the remote resource is a CSV, convert it to a PostgreSQL table, ready for APIfication
|
|
10
10
|
- send crawl and analysis info to a udata instance
|
|
11
11
|
|
|
12
|
+
## Architecture schema
|
|
13
|
+
|
|
14
|
+
The architecture for the full workflow is the following:
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
The hydra crawler is one of the components of the architecture. It will check if resource is available, analyze the type of file if the resource has been modified, and analyze the CSV content. It will also convert CSV resources to database tables and send the data to a udata instance.
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
12
23
|
## CLI
|
|
13
24
|
|
|
14
25
|
### Create database structure
|
|
@@ -26,7 +37,7 @@ Install udata-hydra dependencies and cli.
|
|
|
26
37
|
|
|
27
38
|
`poetry run udata-hydra-crawl`
|
|
28
39
|
|
|
29
|
-
It will crawl (forever) the catalog according to config set in `config.
|
|
40
|
+
It will crawl (forever) the catalog according to config set in `udata_hydra/config.toml`, with a default config in `udata_hydra/config_default.toml`.
|
|
30
41
|
|
|
31
42
|
`BATCH_SIZE` URLs are queued at each loop run.
|
|
32
43
|
|
|
@@ -54,7 +65,7 @@ Converted CSV tables will be stored in the database specified via `config.DATABA
|
|
|
54
65
|
|
|
55
66
|
### Run
|
|
56
67
|
|
|
57
|
-
```
|
|
68
|
+
```bash
|
|
58
69
|
poetry install
|
|
59
70
|
poetry run adev runserver udata_hydra/app.py
|
|
60
71
|
```
|
|
@@ -63,7 +74,7 @@ poetry run adev runserver udata_hydra/app.py
|
|
|
63
74
|
|
|
64
75
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
65
76
|
|
|
66
|
-
```
|
|
77
|
+
```bash
|
|
67
78
|
$ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.saintdenis.re/datasets/661e19974bcc48849bbff7c9637c5c28_1.csv" | json_pp
|
|
68
79
|
{
|
|
69
80
|
"status" : 200,
|
|
@@ -100,7 +111,7 @@ $ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.sain
|
|
|
100
111
|
|
|
101
112
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
102
113
|
|
|
103
|
-
```
|
|
114
|
+
```bash
|
|
104
115
|
$ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv.fr/IMG/xls/er864.xls" | json_pp
|
|
105
116
|
[
|
|
106
117
|
{
|
|
@@ -138,7 +149,7 @@ $ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv
|
|
|
138
149
|
|
|
139
150
|
### Get crawling status
|
|
140
151
|
|
|
141
|
-
```
|
|
152
|
+
```bash
|
|
142
153
|
$ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
143
154
|
{
|
|
144
155
|
"fresh_checks_percentage" : 0.4,
|
|
@@ -151,7 +162,7 @@ $ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
|
151
162
|
|
|
152
163
|
### Get worker status
|
|
153
164
|
|
|
154
|
-
```
|
|
165
|
+
```bash
|
|
155
166
|
$ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
156
167
|
{
|
|
157
168
|
"queued" : {
|
|
@@ -164,7 +175,7 @@ $ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
|
164
175
|
|
|
165
176
|
### Get crawling stats
|
|
166
177
|
|
|
167
|
-
```
|
|
178
|
+
```bash
|
|
168
179
|
$ curl -s "http://localhost:8000/api/stats/" | json_pp
|
|
169
180
|
{
|
|
170
181
|
"status" : [
|
|
@@ -276,3 +287,17 @@ For example, to set the log level to `DEBUG` when initializing the database, use
|
|
|
276
287
|
Refer to each section to learn how to launch them. The only differences from dev to prod are:
|
|
277
288
|
- use `HYDRA_SETTINGS` env var to point to your custom `config.toml`
|
|
278
289
|
- use `HYDRA_APP_SOCKET_PATH` to configure where aiohttp should listen to a [reverse proxy connection (eg nginx)](https://docs.aiohttp.org/en/stable/deployment.html#nginx-configuration) and use `udata-hydra-app` to launch the app server
|
|
290
|
+
|
|
291
|
+
## Contributing
|
|
292
|
+
|
|
293
|
+
Before contributing to the repository and making any PR, it is necessary to initialize the pre-commit hooks:
|
|
294
|
+
```bash
|
|
295
|
+
pre-commit install
|
|
296
|
+
```
|
|
297
|
+
Once this is done, code formatting and linting, as well as import sorting, will be automatically checked before each commit.
|
|
298
|
+
|
|
299
|
+
If you cannot use pre-commit, it is necessary to format, lint, and sort imports with [Ruff](https://docs.astral.sh/ruff/) before committing:
|
|
300
|
+
```bash
|
|
301
|
+
ruff check --fix --select I .
|
|
302
|
+
ruff format .
|
|
303
|
+
```
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "udata-hydra"
|
|
3
|
+
version = "2.0.0.dev"
|
|
4
|
+
description = "Async crawler and parsing service for data.gouv.fr"
|
|
5
|
+
authors = [{ name = "Opendata Team", email = "opendatateam@data.gouv.fr" }]
|
|
6
|
+
requires-python = ">=3.9,<3.13"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
1
10
|
[tool.poetry]
|
|
2
11
|
name = "udata-hydra"
|
|
3
|
-
version = "2.0.0.
|
|
12
|
+
version = "2.0.0.dev1729"
|
|
4
13
|
description = "Async crawler and parsing service for data.gouv.fr"
|
|
5
14
|
authors = ["Opendata Team <opendatateam@data.gouv.fr>"]
|
|
6
15
|
license = "MIT"
|
|
@@ -31,8 +40,6 @@ python-dateutil = "^2.8.2"
|
|
|
31
40
|
csv-detective = "0.7.1"
|
|
32
41
|
|
|
33
42
|
[tool.poetry.group.dev.dependencies]
|
|
34
|
-
flake8-quotes = "^3.3.1"
|
|
35
|
-
flake8 = "^4.0.1"
|
|
36
43
|
gunicorn = "^20.1.0"
|
|
37
44
|
aiohttp-devtools = "^1.0.post0"
|
|
38
45
|
aioresponses = "^0.7.3"
|
|
@@ -41,7 +48,11 @@ pytest = "^7.1.1"
|
|
|
41
48
|
pytest-mock = "^3.7.0"
|
|
42
49
|
pytest-asyncio = "^0.18.3"
|
|
43
50
|
bumpr = "^0.3.8"
|
|
51
|
+
ruff = "^0.5.2"
|
|
44
52
|
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
lint = { select = ["I"] } # also sort imports with an isort rule
|
|
55
|
+
line-length = 100
|
|
45
56
|
|
|
46
57
|
[build-system]
|
|
47
58
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
packages = \
|
|
5
|
+
['udata_hydra',
|
|
6
|
+
'udata_hydra.analysis',
|
|
7
|
+
'udata_hydra.migrations',
|
|
8
|
+
'udata_hydra.utils']
|
|
9
|
+
|
|
10
|
+
package_data = \
|
|
11
|
+
{'': ['*'], 'udata_hydra.migrations': ['csv/*', 'main/*']}
|
|
12
|
+
|
|
13
|
+
install_requires = \
|
|
14
|
+
['aiocontextvars>=0.2.2,<0.3.0',
|
|
15
|
+
'aiohttp>=3.8.1,<4.0.0',
|
|
16
|
+
'asyncpg>=0.27.0,<0.28.0',
|
|
17
|
+
'boto3>=1.21.21,<2.0.0',
|
|
18
|
+
'coloredlogs>=15.0.1,<16.0.0',
|
|
19
|
+
'csv-detective==0.7.1',
|
|
20
|
+
'dateparser>=1.1.7,<2.0.0',
|
|
21
|
+
'humanfriendly>=10.0,<11.0',
|
|
22
|
+
'marshmallow>=3.14.1,<4.0.0',
|
|
23
|
+
'minicli>=0.5.0,<0.6.0',
|
|
24
|
+
'pandas>=1.3.3,<2.0.0',
|
|
25
|
+
'progressist>=0.1.0,<0.2.0',
|
|
26
|
+
'python-dateutil>=2.8.2,<3.0.0',
|
|
27
|
+
'python-magic>=0.4.25,<0.5.0',
|
|
28
|
+
'redis>=4.1.4,<5.0.0',
|
|
29
|
+
'rq>=1.11.1,<2.0.0',
|
|
30
|
+
'sentry-sdk>=1.11.1,<2.0.0',
|
|
31
|
+
'sqlalchemy>=1.4.46,<2.0.0',
|
|
32
|
+
'str2bool>=1.1,<2.0',
|
|
33
|
+
'str2float>=0.0.9,<0.0.10',
|
|
34
|
+
'toml>=0.10.2,<0.11.0']
|
|
35
|
+
|
|
36
|
+
entry_points = \
|
|
37
|
+
{'console_scripts': ['udata-hydra = udata_hydra.cli:run',
|
|
38
|
+
'udata-hydra-app = udata_hydra.app:run',
|
|
39
|
+
'udata-hydra-crawl = udata_hydra.crawl:run']}
|
|
40
|
+
|
|
41
|
+
setup_kwargs = {
|
|
42
|
+
'name': 'udata-hydra',
|
|
43
|
+
'version': '2.0.0.dev1729',
|
|
44
|
+
'description': 'Async crawler and parsing service for data.gouv.fr',
|
|
45
|
+
'long_description': '# udata-hydra 🦀\n\n`udata-hydra` is an async metadata crawler for [data.gouv.fr](https://www.data.gouv.fr).\n\nURLs are crawled via _aiohttp_, catalog and crawled metadata are stored in a _PostgreSQL_ database.\n\nSince it\'s called _hydra_, it also has mythical powers embedded:\n- analyse remote resource metadata over time to detect changes in the smartest way possible\n- if the remote resource is a CSV, convert it to a PostgreSQL table, ready for APIfication\n- send crawl and analysis info to a udata instance\n\n## Architecture schema\n\nThe architecture for the full workflow is the following:\n\n\n\n\nThe hydra crawler is one of the components of the architecture. It will check if resource is available, analyze the type of file if the resource has been modified, and analyze the CSV content. It will also convert CSV resources to database tables and send the data to a udata instance. \n\n\n\n## CLI\n\n### Create database structure\n\nInstall udata-hydra dependencies and cli.\n`poetry install`\n\n`poetry run udata-hydra migrate`\n\n### Load (UPSERT) latest catalog version from data.gouv.fr\n\n`poetry run udata-hydra load-catalog`\n\n## Crawler\n\n`poetry run udata-hydra-crawl`\n\nIt will crawl (forever) the catalog according to config set in `udata_hydra/config.toml`, with a default config in `udata_hydra/config_default.toml`.\n\n`BATCH_SIZE` URLs are queued at each loop run.\n\nThe crawler will start with URLs never checked and then proceed with URLs crawled before `SINCE` interval. It will then wait until something changes (catalog or time).\n\nThere\'s a by-domain backoff mecanism. The crawler will wait when, for a given domain in a given batch, `BACKOFF_NB_REQ` is exceeded in a period of `BACKOFF_PERIOD` seconds. It will retry until the backoff is lifted.\n\nIf an URL matches one of the `EXCLUDED_PATTERNS`, it will never be checked.\n\n## Worker\n\nA job queuing system is used to process long-running tasks. Launch the worker with the following command:\n\n`poetry run rq worker -c udata_hydra.worker`\n\nMonitor worker status:\n\n`poetry run rq info -c udata_hydra.worker --interval 1`\n\n## CSV conversion to database\n\nConverted CSV tables will be stored in the database specified via `config.DATABASE_URL_CSV`. For tests it\'s same database as for the catalog. Locally, `docker compose` will launch two distinct database containers.\n\n## API\n\n### Run\n\n```bash\npoetry install\npoetry run adev runserver udata_hydra/app.py\n```\n\n### Get latest check\n\nWorks with `?url={url}` and `?resource_id={resource_id}`.\n\n```bash\n$ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.saintdenis.re/datasets/661e19974bcc48849bbff7c9637c5c28_1.csv" | json_pp\n{\n "status" : 200,\n "catalog_id" : 64148,\n "deleted" : false,\n "error" : null,\n "created_at" : "2021-02-06T12:19:08.203055",\n "response_time" : 0.830198049545288,\n "url" : "http://opendata-sig.saintdenis.re/datasets/661e19974bcc48849bbff7c9637c5c28_1.csv",\n "domain" : "opendata-sig.saintdenis.re",\n "timeout" : false,\n "id" : 114750,\n "dataset_id" : "5c34944606e3e73d4a551889",\n "resource_id" : "b3678c59-5b35-43ad-9379-fce29e5b56fe",\n "headers" : {\n "content-disposition" : "attachment; filename=\\"xn--Dlimitation_des_cantons-bcc.csv\\"",\n "server" : "openresty",\n "x-amz-meta-cachetime" : "191",\n "last-modified" : "Wed, 29 Apr 2020 02:19:04 GMT",\n "content-encoding" : "gzip",\n "content-type" : "text/csv",\n "cache-control" : "must-revalidate",\n "etag" : "\\"20415964703d9ccc4815d7126aa3a6d8\\"",\n "content-length" : "207",\n "date" : "Sat, 06 Feb 2021 12:19:08 GMT",\n "x-amz-meta-contentlastmodified" : "2018-11-19T09:38:28.490Z",\n "connection" : "keep-alive",\n "vary" : "Accept-Encoding"\n }\n}\n```\n\n### Get all checks for an URL or resource\n\nWorks with `?url={url}` and `?resource_id={resource_id}`.\n\n```bash\n$ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv.fr/IMG/xls/er864.xls" | json_pp\n[\n {\n "domain" : "www.drees.sante.gouv.fr",\n "dataset_id" : "53d6eadba3a72954d9dd62f5",\n "timeout" : false,\n "deleted" : false,\n "response_time" : null,\n "error" : "Cannot connect to host www.drees.sante.gouv.fr:443 ssl:True [SSLCertVerificationError: (1, \\"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for \'www.drees.sante.gouv.fr\'. (_ssl.c:1122)\\")]",\n "catalog_id" : 232112,\n "url" : "http://www.drees.sante.gouv.fr/IMG/xls/er864.xls",\n "headers" : {},\n "id" : 165107,\n "created_at" : "2021-02-06T14:32:47.675854",\n "resource_id" : "93dfd449-9d26-4bb0-a6a9-ee49b1b8a4d7",\n "status" : null\n },\n {\n "timeout" : false,\n "deleted" : false,\n "response_time" : null,\n "error" : "Cannot connect to host www.drees.sante.gouv.fr:443 ssl:True [SSLCertVerificationError: (1, \\"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for \'www.drees.sante.gouv.fr\'. (_ssl.c:1122)\\")]",\n "domain" : "www.drees.sante.gouv.fr",\n "dataset_id" : "53d6eadba3a72954d9dd62f5",\n "created_at" : "2020-12-24T17:06:58.158125",\n "resource_id" : "93dfd449-9d26-4bb0-a6a9-ee49b1b8a4d7",\n "status" : null,\n "catalog_id" : 232112,\n "url" : "http://www.drees.sante.gouv.fr/IMG/xls/er864.xls",\n "headers" : {},\n "id" : 65092\n }\n]\n```\n\n### Get crawling status\n\n```bash\n$ curl -s "http://localhost:8000/api/status/crawler/" | json_pp\n{\n "fresh_checks_percentage" : 0.4,\n "pending_checks" : 142153,\n "total" : 142687,\n "fresh_checks" : 534,\n "checks_percentage" : 0.4\n}\n```\n\n### Get worker status\n\n```bash\n$ curl -s "http://localhost:8000/api/status/worker/" | json_pp\n{\n "queued" : {\n "default" : 0,\n "high" : 825,\n "low" : 655\n }\n}\n```\n\n### Get crawling stats\n\n```bash\n$ curl -s "http://localhost:8000/api/stats/" | json_pp\n{\n "status" : [\n {\n "count" : 525,\n "percentage" : 98.3,\n "label" : "ok"\n },\n {\n "label" : "error",\n "percentage" : 1.3,\n "count" : 7\n },\n {\n "label" : "timeout",\n "percentage" : 0.4,\n "count" : 2\n }\n ],\n "status_codes" : [\n {\n "code" : 200,\n "count" : 413,\n "percentage" : 78.7\n },\n {\n "code" : 501,\n "percentage" : 12.4,\n "count" : 65\n },\n {\n "percentage" : 6.1,\n "count" : 32,\n "code" : 404\n },\n {\n "code" : 500,\n "percentage" : 2.7,\n "count" : 14\n },\n {\n "code" : 502,\n "count" : 1,\n "percentage" : 0.2\n }\n ]\n}\n```\n\n## Using Webhook integration\n\n** Set the config values**\n\nCreate a `config.toml` where your service and commands are launched, or specify a path to a TOML file via the `HYDRA_SETTINGS` environment variable. `config.toml` or equivalent will override values from `udata_hydra/config_default.toml`, lookup there for values that can/need to be defined.\n\n```toml\nUDATA_URI = "https://dev.local:7000/api/2"\nUDATA_URI_API_KEY = "example.api.key"\nSENTRY_DSN = "https://{my-sentry-dsn}"\n```\n\nThe webhook integration sends HTTP messages to `udata` when resources are analyzed or checked to fill resources extras.\n\nRegarding analysis, there is a phase called "change detection". It will try to guess if a resource has been modified based on different criterions:\n- harvest modified date in catalog\n- content-length and last-modified headers\n- checksum comparison over time\n\nThe payload should look something like:\n\n```json\n{\n "analysis:content-length": 91661,\n "analysis:mime-type": "application/zip",\n "analysis:checksum": "bef1de04601dedaf2d127418759b16915ba083be",\n "analysis:last-modified-at": "2022-11-27T23:00:54.762000",\n "analysis:last-modified-detection": "harvest-resource-metadata",\n}\n```\n\n## Development\n\n### docker-compose\n\nMultiple docker-compose files are provided:\n- a minimal `docker-compose.yml` with two PostgreSQL containers (one for catalog and metadata, the other for converted CSV to database)\n- `docker-compose.broker.yml` adds a Redis broker\n- `docker-compose.test.yml` launches a test DB, needed to run tests\n\nNB: you can launch compose from multiple files like this: `docker-compose -f docker-compose.yml -f docker-compose.test.yml up`\n\n### Logging & Debugging\n\nThe log level can be adjusted using the environment variable LOG_LEVEL.\nFor example, to set the log level to `DEBUG` when initializing the database, use `LOG_LEVEL="DEBUG" udata-hydra init_db `.\n\n### Writing a migration\n\n1. Add a file named `migrations/{YYYYMMDD}_{description}.sql` and write the SQL you need to perform migration.\n2. `udata-hydra migrate` will migrate the database as needeed.\n\n## Deployment\n\n3 services need to be deployed for the full stack to run:\n- worker\n- api / app\n- crawler\n\nRefer to each section to learn how to launch them. The only differences from dev to prod are:\n- use `HYDRA_SETTINGS` env var to point to your custom `config.toml`\n- use `HYDRA_APP_SOCKET_PATH` to configure where aiohttp should listen to a [reverse proxy connection (eg nginx)](https://docs.aiohttp.org/en/stable/deployment.html#nginx-configuration) and use `udata-hydra-app` to launch the app server\n\n## Contributing\n\nBefore contributing to the repository and making any PR, it is necessary to initialize the pre-commit hooks:\n```bash\npre-commit install\n```\nOnce this is done, code formatting and linting, as well as import sorting, will be automatically checked before each commit.\n\nIf you cannot use pre-commit, it is necessary to format, lint, and sort imports with [Ruff](https://docs.astral.sh/ruff/) before committing:\n```bash\nruff check --fix --select I .\nruff format .\n```\n',
|
|
46
|
+
'author': 'Opendata Team',
|
|
47
|
+
'author_email': 'opendatateam@data.gouv.fr',
|
|
48
|
+
'maintainer': 'None',
|
|
49
|
+
'maintainer_email': 'None',
|
|
50
|
+
'url': 'None',
|
|
51
|
+
'packages': packages,
|
|
52
|
+
'package_data': package_data,
|
|
53
|
+
'install_requires': install_requires,
|
|
54
|
+
'entry_points': entry_points,
|
|
55
|
+
'python_requires': '>=3.9,<4.0',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
setup(**setup_kwargs)
|
|
@@ -3,36 +3,42 @@ import hashlib
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
-
import pytz
|
|
7
6
|
import sys
|
|
8
|
-
|
|
9
7
|
from datetime import datetime
|
|
10
8
|
from typing import Any
|
|
11
9
|
|
|
10
|
+
import pytz
|
|
12
11
|
import sentry_sdk
|
|
13
|
-
|
|
14
|
-
from csv_detective.explore_csv import routine as csv_detective_routine
|
|
15
12
|
from csv_detective.detection import engine_to_file
|
|
13
|
+
from csv_detective.explore_csv import routine as csv_detective_routine
|
|
16
14
|
from progressist import ProgressBar
|
|
17
15
|
from sqlalchemy import (
|
|
18
|
-
|
|
19
|
-
BigInteger,
|
|
16
|
+
JSON,
|
|
17
|
+
BigInteger,
|
|
18
|
+
Boolean,
|
|
19
|
+
Column,
|
|
20
|
+
Date,
|
|
21
|
+
DateTime,
|
|
22
|
+
Float,
|
|
23
|
+
Integer,
|
|
24
|
+
MetaData,
|
|
25
|
+
String,
|
|
26
|
+
Table,
|
|
20
27
|
)
|
|
21
28
|
from sqlalchemy.dialects.postgresql import asyncpg
|
|
22
29
|
from sqlalchemy.schema import CreateTable
|
|
23
30
|
from str2bool import str2bool
|
|
24
31
|
from str2float import str2float
|
|
25
32
|
|
|
26
|
-
from udata_hydra import
|
|
33
|
+
from udata_hydra import config, context
|
|
27
34
|
from udata_hydra.analysis import helpers
|
|
28
35
|
from udata_hydra.analysis.errors import ParseException
|
|
29
|
-
from udata_hydra.utils
|
|
36
|
+
from udata_hydra.utils import queue
|
|
37
|
+
from udata_hydra.utils.db import compute_insert_query, get_check, update_check
|
|
30
38
|
from udata_hydra.utils.file import download_resource
|
|
31
|
-
from udata_hydra.utils.timer import Timer
|
|
32
39
|
from udata_hydra.utils.http import send
|
|
33
40
|
from udata_hydra.utils.reader import Reader
|
|
34
|
-
from udata_hydra.utils import
|
|
35
|
-
|
|
41
|
+
from udata_hydra.utils.timer import Timer
|
|
36
42
|
|
|
37
43
|
log = logging.getLogger("udata-hydra")
|
|
38
44
|
|
|
@@ -82,15 +88,22 @@ async def notify_udata(check_id):
|
|
|
82
88
|
"document": {
|
|
83
89
|
"analysis:parsing:error": check["parsing_error"],
|
|
84
90
|
"analysis:parsing:started_at": check["parsing_started_at"].isoformat()
|
|
85
|
-
if check["parsing_started_at"]
|
|
91
|
+
if check["parsing_started_at"]
|
|
92
|
+
else None,
|
|
86
93
|
"analysis:parsing:finished_at": check["parsing_finished_at"].isoformat()
|
|
87
|
-
if check["parsing_finished_at"]
|
|
88
|
-
|
|
94
|
+
if check["parsing_finished_at"]
|
|
95
|
+
else None,
|
|
96
|
+
},
|
|
89
97
|
}
|
|
90
98
|
queue.enqueue(send, _priority="high", **payload)
|
|
91
99
|
|
|
92
100
|
|
|
93
|
-
async def analyse_csv(
|
|
101
|
+
async def analyse_csv(
|
|
102
|
+
check_id: int = None,
|
|
103
|
+
url: str = None,
|
|
104
|
+
file_path: str = None,
|
|
105
|
+
debug_insert: bool = False,
|
|
106
|
+
) -> None:
|
|
94
107
|
"""Launch csv analysis from a check or an URL (debug), using previsously downloaded file at file_path if any"""
|
|
95
108
|
if not config.CSV_ANALYSIS_ENABLED:
|
|
96
109
|
log.debug("CSV_ANALYSIS_ENABLED turned off, skipping.")
|
|
@@ -105,10 +118,14 @@ async def analyse_csv(check_id: int = None, url: str = None, file_path: str = No
|
|
|
105
118
|
exception_file = str(check.get("resource_id", "")) in exceptions
|
|
106
119
|
|
|
107
120
|
headers = json.loads(check.get("headers") or "{}")
|
|
108
|
-
tmp_file =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
tmp_file = (
|
|
122
|
+
open(file_path, "rb")
|
|
123
|
+
if file_path
|
|
124
|
+
else await download_resource(
|
|
125
|
+
url=url,
|
|
126
|
+
headers=headers,
|
|
127
|
+
max_size_allowed=None if exception_file else float(config.MAX_FILESIZE_ALLOWED["csv"]),
|
|
128
|
+
)
|
|
112
129
|
)
|
|
113
130
|
table_name = hashlib.md5(url.encode("utf-8")).hexdigest()
|
|
114
131
|
timer.mark("download-file")
|
|
@@ -121,10 +138,13 @@ async def analyse_csv(check_id: int = None, url: str = None, file_path: str = No
|
|
|
121
138
|
await csv_to_db(tmp_file.name, csv_inspection, table_name, debug_insert=debug_insert)
|
|
122
139
|
timer.mark("csv-to-db")
|
|
123
140
|
if check_id:
|
|
124
|
-
await update_check(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
await update_check(
|
|
142
|
+
check_id,
|
|
143
|
+
{
|
|
144
|
+
"parsing_table": table_name,
|
|
145
|
+
"parsing_finished_at": datetime.now(pytz.UTC),
|
|
146
|
+
},
|
|
147
|
+
)
|
|
128
148
|
await csv_to_db_index(table_name, csv_inspection, check)
|
|
129
149
|
except ParseException as e:
|
|
130
150
|
await handle_parse_exception(e, check_id, table_name)
|
|
@@ -196,11 +216,9 @@ async def csv_to_db(file_path: str, inspection: dict, table_name: str, debug_ins
|
|
|
196
216
|
await db.execute(q)
|
|
197
217
|
with Reader(file_path, inspection) as reader:
|
|
198
218
|
records = (
|
|
199
|
-
[
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
]
|
|
203
|
-
for line in reader if line
|
|
219
|
+
[smart_cast(t, v, failsafe=True) for t, v in zip(columns.values(), line)]
|
|
220
|
+
for line in reader
|
|
221
|
+
if line
|
|
204
222
|
)
|
|
205
223
|
# this use postgresql COPY from an iterator, it's fast but might be difficult to debug
|
|
206
224
|
if not debug_insert:
|
|
@@ -223,13 +241,21 @@ async def csv_to_db_index(table_name: str, inspection: dict, check: dict):
|
|
|
223
241
|
"""Store meta info about a converted CSV table in `DATABASE_URL_CSV.tables_index`"""
|
|
224
242
|
db = await context.pool("csv")
|
|
225
243
|
q = "INSERT INTO tables_index(parsing_table, csv_detective, resource_id, url) VALUES($1, $2, $3, $4)"
|
|
226
|
-
await db.execute(
|
|
244
|
+
await db.execute(
|
|
245
|
+
q,
|
|
246
|
+
table_name,
|
|
247
|
+
json.dumps(inspection),
|
|
248
|
+
check.get("resource_id"),
|
|
249
|
+
check.get("url"),
|
|
250
|
+
)
|
|
227
251
|
|
|
228
252
|
|
|
229
253
|
async def perform_csv_inspection(file_path):
|
|
230
254
|
"""Launch csv-detective against given file"""
|
|
231
255
|
try:
|
|
232
|
-
return csv_detective_routine(
|
|
256
|
+
return csv_detective_routine(
|
|
257
|
+
file_path, output_profile=True, num_rows=-1, save_results=False
|
|
258
|
+
)
|
|
233
259
|
except Exception as e:
|
|
234
260
|
raise ParseException("csv_detective") from e
|
|
235
261
|
|
|
@@ -242,7 +268,7 @@ async def delete_table(table_name: str):
|
|
|
242
268
|
|
|
243
269
|
async def handle_parse_exception(e: Exception, check_id: int, table_name: str) -> None:
|
|
244
270
|
"""Specific ParsingError handling. Enriches sentry w/ context if available,
|
|
245
|
-
|
|
271
|
+
and store error if in a check context. Also cleanup :table_name: if needed."""
|
|
246
272
|
db = await context.pool("csv")
|
|
247
273
|
await db.execute(f'DROP TABLE IF EXISTS "{table_name}"')
|
|
248
274
|
if check_id:
|
|
@@ -257,7 +283,10 @@ async def handle_parse_exception(e: Exception, check_id: int, table_name: str) -
|
|
|
257
283
|
# e.__cause__ let us access the "inherited" error of ParseException (raise e from cause)
|
|
258
284
|
# it's called explicit exception chaining and it's very cool, look it up (PEP 3134)!
|
|
259
285
|
err = f"{e.step}:sentry:{event_id}" if config.SENTRY_DSN else f"{e.step}:{str(e.__cause__)}"
|
|
260
|
-
await update_check(
|
|
286
|
+
await update_check(
|
|
287
|
+
check_id,
|
|
288
|
+
{"parsing_error": err, "parsing_finished_at": datetime.now(pytz.UTC)},
|
|
289
|
+
)
|
|
261
290
|
log.error("Parsing error", exc_info=e)
|
|
262
291
|
else:
|
|
263
292
|
raise e
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
2
|
|
|
3
3
|
from dateparser import parse as date_parser
|
|
4
|
-
from dateutil.parser import
|
|
4
|
+
from dateutil.parser import ParserError
|
|
5
|
+
from dateutil.parser import parse as dateutil_parser
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def to_json(value: str) -> str:
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
|
-
import pytz
|
|
6
|
-
|
|
7
4
|
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
8
6
|
from typing import Tuple, Union
|
|
9
7
|
|
|
10
8
|
import magic
|
|
11
|
-
|
|
9
|
+
import pytz
|
|
12
10
|
from dateparser import parse as date_parser
|
|
13
11
|
|
|
14
|
-
from udata_hydra import
|
|
15
|
-
from udata_hydra.utils import queue
|
|
12
|
+
from udata_hydra import config, context
|
|
16
13
|
from udata_hydra.analysis.csv import analyse_csv
|
|
14
|
+
from udata_hydra.utils import queue
|
|
17
15
|
from udata_hydra.utils.csv import detect_tabular_from_headers
|
|
18
|
-
from udata_hydra.utils.db import
|
|
16
|
+
from udata_hydra.utils.db import get_check, update_check
|
|
19
17
|
from udata_hydra.utils.file import compute_checksum_from_file, download_resource
|
|
20
18
|
from udata_hydra.utils.http import send
|
|
21
19
|
|
|
@@ -60,8 +58,7 @@ async def process_resource(check_id: int, is_first_check: bool) -> None:
|
|
|
60
58
|
# could it be a CSV? If we get hints, we will analyse the file further depending on change status
|
|
61
59
|
is_tabular, file_format = await detect_tabular_from_headers(check)
|
|
62
60
|
max_size_allowed = (
|
|
63
|
-
None if str(resource_id) in exceptions
|
|
64
|
-
else float(config.MAX_FILESIZE_ALLOWED[file_format])
|
|
61
|
+
None if str(resource_id) in exceptions else float(config.MAX_FILESIZE_ALLOWED[file_format])
|
|
65
62
|
)
|
|
66
63
|
|
|
67
64
|
# if the change status is NO_GUESS or HAS_CHANGED, let's download the file to get more infos
|
|
@@ -79,18 +76,25 @@ async def process_resource(check_id: int, is_first_check: bool) -> None:
|
|
|
79
76
|
dl_analysis["analysis:checksum"] = compute_checksum_from_file(tmp_file.name)
|
|
80
77
|
# Check if checksum has been modified if we don't have other hints
|
|
81
78
|
if change_status == Change.NO_GUESS:
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
(
|
|
80
|
+
change_status,
|
|
81
|
+
change_payload,
|
|
82
|
+
) = await detect_resource_change_from_checksum(
|
|
83
|
+
resource_id, dl_analysis["analysis:checksum"]
|
|
84
|
+
)
|
|
84
85
|
dl_analysis["analysis:mime-type"] = magic.from_file(tmp_file.name, mime=True)
|
|
85
86
|
finally:
|
|
86
87
|
if tmp_file and not is_tabular:
|
|
87
88
|
os.remove(tmp_file.name)
|
|
88
|
-
await update_check(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
await update_check(
|
|
90
|
+
check_id,
|
|
91
|
+
{
|
|
92
|
+
"checksum": dl_analysis.get("analysis:checksum"),
|
|
93
|
+
"analysis_error": dl_analysis.get("analysis:error"),
|
|
94
|
+
"filesize": dl_analysis.get("analysis:content-length"),
|
|
95
|
+
"mime_type": dl_analysis.get("analysis:mime-type"),
|
|
96
|
+
},
|
|
97
|
+
)
|
|
94
98
|
|
|
95
99
|
if change_status == Change.HAS_CHANGED:
|
|
96
100
|
await store_last_modified_date(change_payload or {}, resource_id, check_id)
|
|
@@ -119,11 +123,14 @@ async def store_last_modified_date(change_analysis, resource_id, check_id) -> No
|
|
|
119
123
|
async with pool.acquire() as conn:
|
|
120
124
|
await conn.execute(
|
|
121
125
|
"UPDATE checks SET detected_last_modified_at = $1 WHERE id = $2",
|
|
122
|
-
last_modified,
|
|
126
|
+
last_modified,
|
|
127
|
+
check_id,
|
|
123
128
|
)
|
|
124
129
|
|
|
125
130
|
|
|
126
|
-
async def detect_resource_change_from_checksum(
|
|
131
|
+
async def detect_resource_change_from_checksum(
|
|
132
|
+
resource_id, new_checksum
|
|
133
|
+
) -> Tuple[Change, Union[dict, None]]:
|
|
127
134
|
"""
|
|
128
135
|
Checks if resource checksum has changed over time
|
|
129
136
|
Returns a tuple with a Change status and an optional payload:
|
|
@@ -150,7 +157,9 @@ async def detect_resource_change_from_checksum(resource_id, new_checksum) -> Tup
|
|
|
150
157
|
return Change.NO_GUESS, None
|
|
151
158
|
|
|
152
159
|
|
|
153
|
-
async def detect_resource_change_from_last_modified_header(
|
|
160
|
+
async def detect_resource_change_from_last_modified_header(
|
|
161
|
+
data: dict,
|
|
162
|
+
) -> Tuple[Change, Union[dict, None]]:
|
|
154
163
|
# last modified header check
|
|
155
164
|
|
|
156
165
|
if len(data) == 1 and data[0]["last_modified"]:
|
|
@@ -172,7 +181,9 @@ async def detect_resource_change_from_last_modified_header(data: dict) -> Tuple[
|
|
|
172
181
|
return Change.HAS_NOT_CHANGED, None
|
|
173
182
|
|
|
174
183
|
|
|
175
|
-
async def detect_resource_change_from_content_length_header(
|
|
184
|
+
async def detect_resource_change_from_content_length_header(
|
|
185
|
+
data: dict,
|
|
186
|
+
) -> Tuple[Change, Union[dict, None]]:
|
|
176
187
|
# content-length variation between current and last check
|
|
177
188
|
if len(data) <= 1 or not data[0]["content_length"]:
|
|
178
189
|
return Change.NO_GUESS, None
|
|
@@ -185,7 +196,9 @@ async def detect_resource_change_from_content_length_header(data: dict) -> Tuple
|
|
|
185
196
|
return Change.HAS_NOT_CHANGED, None
|
|
186
197
|
|
|
187
198
|
|
|
188
|
-
async def detect_resource_change_on_early_hints(
|
|
199
|
+
async def detect_resource_change_on_early_hints(
|
|
200
|
+
resource_id: str,
|
|
201
|
+
) -> Tuple[Change, Union[dict, None]]:
|
|
189
202
|
"""
|
|
190
203
|
Try to guess if a resource has been modified from harvest and headers in check data:
|
|
191
204
|
- last-modified header value if it can be found and parsed
|
|
@@ -223,7 +236,10 @@ async def detect_resource_change_on_early_hints(resource_id: str) -> Tuple[Chang
|
|
|
223
236
|
return change_status, change_payload
|
|
224
237
|
|
|
225
238
|
# if not, let's see if we can infer a modifification date from last-modified headers
|
|
226
|
-
|
|
239
|
+
(
|
|
240
|
+
change_status,
|
|
241
|
+
change_payload,
|
|
242
|
+
) = await detect_resource_change_from_last_modified_header(data)
|
|
227
243
|
if change_status != Change.NO_GUESS:
|
|
228
244
|
return change_status, change_payload
|
|
229
245
|
|
|
@@ -231,7 +247,9 @@ async def detect_resource_change_on_early_hints(resource_id: str) -> Tuple[Chang
|
|
|
231
247
|
return await detect_resource_change_from_content_length_header(data)
|
|
232
248
|
|
|
233
249
|
|
|
234
|
-
async def detect_resource_change_from_harvest(
|
|
250
|
+
async def detect_resource_change_from_harvest(
|
|
251
|
+
checks_data: dict, resource_id: str
|
|
252
|
+
) -> Tuple[Change, Union[dict, None]]:
|
|
235
253
|
"""
|
|
236
254
|
Checks if resource has a harvest.modified_at
|
|
237
255
|
Returns a tuple with a Change status and an optional payload:
|