udata-hydra 2.0.0.dev2347__tar.gz → 2.0.0.dev2384__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.dev2347 → udata_hydra-2.0.0.dev2384}/PKG-INFO +31 -6
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/README.md +30 -5
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/pyproject.toml +2 -2
- udata_hydra-2.0.0.dev2384/udata_hydra/app.py +29 -0
- udata_hydra-2.0.0.dev2384/udata_hydra/routes/__init__.py +23 -0
- udata_hydra-2.0.0.dev2384/udata_hydra/routes/checks.py +31 -0
- udata_hydra-2.0.0.dev2384/udata_hydra/routes/resources.py +83 -0
- udata_hydra-2.0.0.dev2384/udata_hydra/routes/status.py +125 -0
- udata_hydra-2.0.0.dev2347/udata_hydra/app.py +0 -269
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/analysis/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/analysis/csv.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/analysis/errors.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/analysis/helpers.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/analysis/resource.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/cli.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/config_default.toml +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/context.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/crawl.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/db/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/db/check.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/db/resource.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/logger.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/csv/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/csv/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/csv/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20221205_initial_up_rev1.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20221206_rev1_up_rev2.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20221206_rev2_up_rev3.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20221208_rev3_up_rev4.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20221208_rev4_up_rev5.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230119_rev5_up_rev6.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230121_rev6_up_rev7.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230121_rev7_up_rev8.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230130_drop_migrations.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230206_datetime_aware.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230515_rev8_up_rev9.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20230606_rev9_up_rev10.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/migrations/main/20231102_drop_csv_analysis.sql +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/schemas/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/schemas/check.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/schemas/resource_query.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/__init__.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/app_version.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/csv.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/file.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/http.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/minio.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/queue.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/reader.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/utils/timer.py +0 -0
- {udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/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.
|
|
3
|
+
Version: 2.0.0.dev2384
|
|
4
4
|
Summary: Async crawler and parsing service for data.gouv.fr
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Opendata Team
|
|
@@ -120,7 +120,32 @@ poetry install
|
|
|
120
120
|
poetry run adev runserver udata_hydra/app.py
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
###
|
|
123
|
+
### Routes/endpoints
|
|
124
|
+
|
|
125
|
+
The API serves the following endpoints:
|
|
126
|
+
|
|
127
|
+
*Related to checks:*
|
|
128
|
+
- `GET` on `/api/checks/latest/` to get the latest check for a given URL or resource
|
|
129
|
+
- `GET` on `/api/checks/all/` to get all checks for a given URL or resource
|
|
130
|
+
|
|
131
|
+
*Related to resources:*
|
|
132
|
+
- `POST` on `/api/resources/` to receive a resource creation event from a source. It will create a new resource in the DB "catalog" table and mark it as priority for next crawling
|
|
133
|
+
- `PUT` on `/api/resources/` to update a resource in the DB "catalog" table
|
|
134
|
+
- `DELETE` on `/api/resources/` to delete a resource in the DB "catalog" table
|
|
135
|
+
|
|
136
|
+
> :warning: **Warning: the following routes are deprecated and need be removed in the future:**
|
|
137
|
+
> - `POST` on `/api/resource/created` -> use `POST` on `/api/resources/` instead
|
|
138
|
+
> - `POST` on `/api/resource/updated` -> use `PUT` on `/api/resources/` instead
|
|
139
|
+
> - `POST` on `/api/resource/deleted` -> use `DELET`E on `/api/resources/` instead
|
|
140
|
+
|
|
141
|
+
*Related to some status and health check:*
|
|
142
|
+
- `GET` on `/api/status/crawler/` to get the crawling status
|
|
143
|
+
- `GET` on `/api/status/worker/` to get the worker status
|
|
144
|
+
- `GET` on `/api/stats/` to get the crawling stats
|
|
145
|
+
|
|
146
|
+
More details about some enpoints are provided below with examples, but not for all of them:
|
|
147
|
+
|
|
148
|
+
#### Get latest check
|
|
124
149
|
|
|
125
150
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
126
151
|
|
|
@@ -157,7 +182,7 @@ $ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.sain
|
|
|
157
182
|
}
|
|
158
183
|
```
|
|
159
184
|
|
|
160
|
-
|
|
185
|
+
#### Get all checks for an URL or resource
|
|
161
186
|
|
|
162
187
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
163
188
|
|
|
@@ -197,7 +222,7 @@ $ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv
|
|
|
197
222
|
]
|
|
198
223
|
```
|
|
199
224
|
|
|
200
|
-
|
|
225
|
+
#### Get crawling status
|
|
201
226
|
|
|
202
227
|
```bash
|
|
203
228
|
$ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
@@ -210,7 +235,7 @@ $ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
|
210
235
|
}
|
|
211
236
|
```
|
|
212
237
|
|
|
213
|
-
|
|
238
|
+
#### Get worker status
|
|
214
239
|
|
|
215
240
|
```bash
|
|
216
241
|
$ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
@@ -223,7 +248,7 @@ $ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
|
223
248
|
}
|
|
224
249
|
```
|
|
225
250
|
|
|
226
|
-
|
|
251
|
+
#### Get crawling stats
|
|
227
252
|
|
|
228
253
|
```bash
|
|
229
254
|
$ curl -s "http://localhost:8000/api/stats/" | json_pp
|
|
@@ -84,7 +84,32 @@ poetry install
|
|
|
84
84
|
poetry run adev runserver udata_hydra/app.py
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
###
|
|
87
|
+
### Routes/endpoints
|
|
88
|
+
|
|
89
|
+
The API serves the following endpoints:
|
|
90
|
+
|
|
91
|
+
*Related to checks:*
|
|
92
|
+
- `GET` on `/api/checks/latest/` to get the latest check for a given URL or resource
|
|
93
|
+
- `GET` on `/api/checks/all/` to get all checks for a given URL or resource
|
|
94
|
+
|
|
95
|
+
*Related to resources:*
|
|
96
|
+
- `POST` on `/api/resources/` to receive a resource creation event from a source. It will create a new resource in the DB "catalog" table and mark it as priority for next crawling
|
|
97
|
+
- `PUT` on `/api/resources/` to update a resource in the DB "catalog" table
|
|
98
|
+
- `DELETE` on `/api/resources/` to delete a resource in the DB "catalog" table
|
|
99
|
+
|
|
100
|
+
> :warning: **Warning: the following routes are deprecated and need be removed in the future:**
|
|
101
|
+
> - `POST` on `/api/resource/created` -> use `POST` on `/api/resources/` instead
|
|
102
|
+
> - `POST` on `/api/resource/updated` -> use `PUT` on `/api/resources/` instead
|
|
103
|
+
> - `POST` on `/api/resource/deleted` -> use `DELET`E on `/api/resources/` instead
|
|
104
|
+
|
|
105
|
+
*Related to some status and health check:*
|
|
106
|
+
- `GET` on `/api/status/crawler/` to get the crawling status
|
|
107
|
+
- `GET` on `/api/status/worker/` to get the worker status
|
|
108
|
+
- `GET` on `/api/stats/` to get the crawling stats
|
|
109
|
+
|
|
110
|
+
More details about some enpoints are provided below with examples, but not for all of them:
|
|
111
|
+
|
|
112
|
+
#### Get latest check
|
|
88
113
|
|
|
89
114
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
90
115
|
|
|
@@ -121,7 +146,7 @@ $ curl -s "http://localhost:8000/api/checks/latest/?url=http://opendata-sig.sain
|
|
|
121
146
|
}
|
|
122
147
|
```
|
|
123
148
|
|
|
124
|
-
|
|
149
|
+
#### Get all checks for an URL or resource
|
|
125
150
|
|
|
126
151
|
Works with `?url={url}` and `?resource_id={resource_id}`.
|
|
127
152
|
|
|
@@ -161,7 +186,7 @@ $ curl -s "http://localhost:8000/api/checks/all/?url=http://www.drees.sante.gouv
|
|
|
161
186
|
]
|
|
162
187
|
```
|
|
163
188
|
|
|
164
|
-
|
|
189
|
+
#### Get crawling status
|
|
165
190
|
|
|
166
191
|
```bash
|
|
167
192
|
$ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
@@ -174,7 +199,7 @@ $ curl -s "http://localhost:8000/api/status/crawler/" | json_pp
|
|
|
174
199
|
}
|
|
175
200
|
```
|
|
176
201
|
|
|
177
|
-
|
|
202
|
+
#### Get worker status
|
|
178
203
|
|
|
179
204
|
```bash
|
|
180
205
|
$ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
@@ -187,7 +212,7 @@ $ curl -s "http://localhost:8000/api/status/worker/" | json_pp
|
|
|
187
212
|
}
|
|
188
213
|
```
|
|
189
214
|
|
|
190
|
-
|
|
215
|
+
#### Get crawling stats
|
|
191
216
|
|
|
192
217
|
```bash
|
|
193
218
|
$ curl -s "http://localhost:8000/api/stats/" | json_pp
|
|
@@ -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.dev2384"
|
|
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"
|
|
@@ -76,7 +76,7 @@ markers = [
|
|
|
76
76
|
addopts = "--cov=udata_hydra/ --cov-report term-missing"
|
|
77
77
|
|
|
78
78
|
[tool.ruff]
|
|
79
|
-
lint = { select = ["I"] } # also sort imports with an isort rule
|
|
79
|
+
lint = { extend-select = ["I"] } # also sort imports with an isort rule
|
|
80
80
|
line-length = 100
|
|
81
81
|
|
|
82
82
|
[build-system]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from aiohttp import web
|
|
4
|
+
|
|
5
|
+
from udata_hydra import context
|
|
6
|
+
from udata_hydra.routes import routes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def app_factory() -> web.Application:
|
|
10
|
+
async def app_startup(app):
|
|
11
|
+
app["pool"] = await context.pool()
|
|
12
|
+
|
|
13
|
+
async def app_cleanup(app):
|
|
14
|
+
if "pool" in app:
|
|
15
|
+
await app["pool"].close()
|
|
16
|
+
|
|
17
|
+
app = web.Application()
|
|
18
|
+
app.add_routes(routes)
|
|
19
|
+
app.on_startup.append(app_startup)
|
|
20
|
+
app.on_cleanup.append(app_cleanup)
|
|
21
|
+
return app
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run():
|
|
25
|
+
web.run_app(app_factory(), path=os.environ.get("HYDRA_APP_SOCKET_PATH"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
run()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from aiohttp import web
|
|
2
|
+
|
|
3
|
+
from udata_hydra.routes.checks import get_all_checks, get_latest_check
|
|
4
|
+
from udata_hydra.routes.resources import create_resource, delete_resource, update_resource
|
|
5
|
+
from udata_hydra.routes.status import get_crawler_status, get_stats, get_worker_status
|
|
6
|
+
|
|
7
|
+
# routes = web.RouteTableDef()
|
|
8
|
+
routes: list = [
|
|
9
|
+
# Routes for checks
|
|
10
|
+
web.get("/api/checks/latest/", get_latest_check),
|
|
11
|
+
web.get("/api/checks/all/", get_all_checks),
|
|
12
|
+
# Routes for resources
|
|
13
|
+
web.post("/api/resources/", create_resource),
|
|
14
|
+
web.put("/api/resources/", update_resource),
|
|
15
|
+
web.delete("/api/resources/", delete_resource),
|
|
16
|
+
web.post("/api/resource/created/", create_resource), # TODO: legacy, to remove
|
|
17
|
+
web.post("/api/resource/updated/", update_resource), # TODO: legacy, to remove
|
|
18
|
+
web.post("/api/resource/deleted/", delete_resource), # TODO: legacy, to remove
|
|
19
|
+
# Routes for statuses
|
|
20
|
+
web.get("/api/status/crawler/", get_crawler_status),
|
|
21
|
+
web.get("/api/status/worker/", get_worker_status),
|
|
22
|
+
web.get("/api/stats/", get_stats),
|
|
23
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from aiohttp import web
|
|
2
|
+
|
|
3
|
+
from udata_hydra.db.check import Check
|
|
4
|
+
from udata_hydra.schemas import CheckSchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_args(request, params=("url", "resource_id")) -> list:
|
|
8
|
+
"""Get GET parameters from request"""
|
|
9
|
+
data = [request.query.get(param) for param in params]
|
|
10
|
+
if not any(data):
|
|
11
|
+
raise web.HTTPBadRequest()
|
|
12
|
+
return data
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def get_latest_check(request: web.Request) -> web.Response:
|
|
16
|
+
"""Get the latest check for a given URL or resource_id"""
|
|
17
|
+
url, resource_id = _get_args(request)
|
|
18
|
+
data = await Check.get_latest(url, resource_id)
|
|
19
|
+
if not data:
|
|
20
|
+
raise web.HTTPNotFound()
|
|
21
|
+
if data["deleted"]:
|
|
22
|
+
raise web.HTTPGone()
|
|
23
|
+
return web.json_response(CheckSchema().dump(dict(data)))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_all_checks(request: web.Request) -> web.Response:
|
|
27
|
+
url, resource_id = _get_args(request)
|
|
28
|
+
data = await Check.get_all(url, resource_id)
|
|
29
|
+
if not data:
|
|
30
|
+
raise web.HTTPNotFound()
|
|
31
|
+
return web.json_response([CheckSchema().dump(dict(r)) for r in data])
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from aiohttp import web
|
|
4
|
+
from marshmallow import ValidationError
|
|
5
|
+
|
|
6
|
+
from udata_hydra import config
|
|
7
|
+
from udata_hydra.db.resource import Resource
|
|
8
|
+
from udata_hydra.schemas import ResourceQuerySchema
|
|
9
|
+
from udata_hydra.utils.minio import delete_resource_from_minio
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def create_resource(request: web.Request) -> web.Response:
|
|
13
|
+
"""Endpoint to receive a resource creation event from a source
|
|
14
|
+
Will create a new resource in the DB "catalog" table and mark it as priority for next crawling
|
|
15
|
+
Respond with a 200 status code and a JSON body with a message key set to "created"
|
|
16
|
+
If error, respond with a 400 status code
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
payload = await request.json()
|
|
20
|
+
valid_payload: dict = ResourceQuerySchema().load(payload)
|
|
21
|
+
except ValidationError as err:
|
|
22
|
+
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
23
|
+
|
|
24
|
+
resource = valid_payload["document"]
|
|
25
|
+
if not resource:
|
|
26
|
+
raise web.HTTPBadRequest(text="Missing document body")
|
|
27
|
+
|
|
28
|
+
dataset_id = valid_payload["dataset_id"]
|
|
29
|
+
resource_id = valid_payload["resource_id"]
|
|
30
|
+
|
|
31
|
+
await Resource.insert(
|
|
32
|
+
dataset_id=dataset_id,
|
|
33
|
+
resource_id=resource_id,
|
|
34
|
+
url=resource["url"],
|
|
35
|
+
priority=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return web.json_response({"message": "created"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def update_resource(request: web.Request) -> web.Response:
|
|
42
|
+
"""Endpoint to receive a resource update event from a source
|
|
43
|
+
Will update an existing resource in the DB "catalog" table and mark it as priority for next crawling
|
|
44
|
+
Respond with a 200 status code and a JSON body with a message key set to "updated"
|
|
45
|
+
If error, respond with a 400 status code
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
payload = await request.json()
|
|
49
|
+
valid_payload: dict = ResourceQuerySchema().load(payload)
|
|
50
|
+
except ValidationError as err:
|
|
51
|
+
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
52
|
+
|
|
53
|
+
resource = valid_payload["document"]
|
|
54
|
+
if not resource:
|
|
55
|
+
raise web.HTTPBadRequest(text="Missing document body")
|
|
56
|
+
|
|
57
|
+
dataset_id = valid_payload["dataset_id"]
|
|
58
|
+
resource_id = valid_payload["resource_id"]
|
|
59
|
+
|
|
60
|
+
await Resource.update_or_insert(dataset_id, resource_id, resource["url"])
|
|
61
|
+
|
|
62
|
+
return web.json_response({"message": "updated"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def delete_resource(request: web.Request) -> web.Response:
|
|
66
|
+
try:
|
|
67
|
+
payload = await request.json()
|
|
68
|
+
valid_payload: dict = ResourceQuerySchema().load(payload)
|
|
69
|
+
except ValidationError as err:
|
|
70
|
+
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
71
|
+
|
|
72
|
+
dataset_id = valid_payload["dataset_id"]
|
|
73
|
+
resource_id = valid_payload["resource_id"]
|
|
74
|
+
|
|
75
|
+
pool = request.app["pool"]
|
|
76
|
+
async with pool.acquire() as connection:
|
|
77
|
+
if config.SAVE_TO_MINIO:
|
|
78
|
+
delete_resource_from_minio(dataset_id, resource_id)
|
|
79
|
+
# Mark resource as deleted in catalog table
|
|
80
|
+
q = f"""UPDATE catalog SET deleted = TRUE WHERE resource_id = '{resource_id}';"""
|
|
81
|
+
await connection.execute(q)
|
|
82
|
+
|
|
83
|
+
return web.json_response({"message": "deleted"})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
|
|
3
|
+
from aiohttp import web
|
|
4
|
+
from humanfriendly import parse_timespan
|
|
5
|
+
|
|
6
|
+
from udata_hydra import config, context
|
|
7
|
+
from udata_hydra.crawl import get_excluded_clause
|
|
8
|
+
from udata_hydra.worker import QUEUES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_crawler_status(request: web.Request) -> web.Response:
|
|
12
|
+
q = f"""
|
|
13
|
+
SELECT
|
|
14
|
+
SUM(CASE WHEN last_check IS NULL THEN 1 ELSE 0 END) AS count_left,
|
|
15
|
+
SUM(CASE WHEN last_check IS NOT NULL THEN 1 ELSE 0 END) AS count_checked
|
|
16
|
+
FROM catalog
|
|
17
|
+
WHERE {get_excluded_clause()}
|
|
18
|
+
AND catalog.deleted = False
|
|
19
|
+
"""
|
|
20
|
+
stats_catalog = await request.app["pool"].fetchrow(q)
|
|
21
|
+
|
|
22
|
+
since = parse_timespan(config.SINCE)
|
|
23
|
+
since = datetime.now(timezone.utc) - timedelta(seconds=since)
|
|
24
|
+
q = f"""
|
|
25
|
+
SELECT
|
|
26
|
+
SUM(CASE WHEN checks.created_at <= $1 THEN 1 ELSE 0 END) AS count_outdated
|
|
27
|
+
--, SUM(CASE WHEN checks.created_at > $1 THEN 1 ELSE 0 END) AS count_fresh
|
|
28
|
+
FROM catalog, checks
|
|
29
|
+
WHERE {get_excluded_clause()}
|
|
30
|
+
AND catalog.last_check = checks.id
|
|
31
|
+
AND catalog.deleted = False
|
|
32
|
+
"""
|
|
33
|
+
stats_checks = await request.app["pool"].fetchrow(q, since)
|
|
34
|
+
|
|
35
|
+
count_left = stats_catalog["count_left"] + (stats_checks["count_outdated"] or 0)
|
|
36
|
+
# all w/ a check, minus those with an outdated checked
|
|
37
|
+
count_checked = stats_catalog["count_checked"] - (stats_checks["count_outdated"] or 0)
|
|
38
|
+
total = stats_catalog["count_left"] + stats_catalog["count_checked"]
|
|
39
|
+
rate_checked = round(stats_catalog["count_checked"] / total * 100, 1)
|
|
40
|
+
rate_checked_fresh = round(count_checked / total * 100, 1)
|
|
41
|
+
|
|
42
|
+
return web.json_response(
|
|
43
|
+
{
|
|
44
|
+
"total": total,
|
|
45
|
+
"pending_checks": count_left,
|
|
46
|
+
"fresh_checks": count_checked,
|
|
47
|
+
"checks_percentage": rate_checked,
|
|
48
|
+
"fresh_checks_percentage": rate_checked_fresh,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_worker_status(request: web.Request) -> web.Response:
|
|
54
|
+
res = {"queued": {q: len(context.queue(q)) for q in QUEUES}}
|
|
55
|
+
return web.json_response(res)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def get_stats(request: web.Request) -> web.Response:
|
|
59
|
+
q = f"""
|
|
60
|
+
SELECT count(*) AS count_checked
|
|
61
|
+
FROM catalog
|
|
62
|
+
WHERE {get_excluded_clause()}
|
|
63
|
+
AND last_check IS NOT NULL
|
|
64
|
+
AND catalog.deleted = False
|
|
65
|
+
"""
|
|
66
|
+
stats_catalog = await request.app["pool"].fetchrow(q)
|
|
67
|
+
|
|
68
|
+
q = f"""
|
|
69
|
+
SELECT
|
|
70
|
+
SUM(CASE WHEN error IS NULL AND timeout = False THEN 1 ELSE 0 END) AS count_ok,
|
|
71
|
+
SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) AS count_error,
|
|
72
|
+
SUM(CASE WHEN timeout = True THEN 1 ELSE 0 END) AS count_timeout
|
|
73
|
+
FROM catalog, checks
|
|
74
|
+
WHERE {get_excluded_clause()}
|
|
75
|
+
AND catalog.last_check = checks.id
|
|
76
|
+
AND catalog.deleted = False
|
|
77
|
+
"""
|
|
78
|
+
stats_status = await request.app["pool"].fetchrow(q)
|
|
79
|
+
|
|
80
|
+
def cmp_rate(key):
|
|
81
|
+
if stats_catalog["count_checked"] == 0:
|
|
82
|
+
return 0
|
|
83
|
+
return round(stats_status[key] / stats_catalog["count_checked"] * 100, 1)
|
|
84
|
+
|
|
85
|
+
q = f"""
|
|
86
|
+
SELECT checks.status, count(*) as count FROM checks, catalog
|
|
87
|
+
WHERE catalog.last_check = checks.id
|
|
88
|
+
AND checks.status IS NOT NULL
|
|
89
|
+
AND {get_excluded_clause()}
|
|
90
|
+
AND last_check IS NOT NULL
|
|
91
|
+
AND catalog.deleted = False
|
|
92
|
+
GROUP BY checks.status
|
|
93
|
+
ORDER BY count DESC;
|
|
94
|
+
"""
|
|
95
|
+
res = await request.app["pool"].fetch(q)
|
|
96
|
+
return web.json_response(
|
|
97
|
+
{
|
|
98
|
+
"status": sorted(
|
|
99
|
+
[
|
|
100
|
+
{
|
|
101
|
+
"label": s,
|
|
102
|
+
"count": stats_status[f"count_{s}"] or 0,
|
|
103
|
+
"percentage": cmp_rate(f"count_{s}"),
|
|
104
|
+
}
|
|
105
|
+
for s in ["error", "timeout", "ok"]
|
|
106
|
+
],
|
|
107
|
+
key=lambda x: x["count"],
|
|
108
|
+
reverse=True,
|
|
109
|
+
),
|
|
110
|
+
"status_codes": [
|
|
111
|
+
{
|
|
112
|
+
"code": r["status"],
|
|
113
|
+
"count": r["count"],
|
|
114
|
+
"percentage": round(r["count"] / sum(r["count"] for r in res) * 100, 1),
|
|
115
|
+
}
|
|
116
|
+
for r in res
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def health(request: web.Request) -> web.Response:
|
|
123
|
+
test_connection = await request.app["pool"].fetchrow("SELECT 1")
|
|
124
|
+
assert next(test_connection.values()) == 1
|
|
125
|
+
return web.HTTPOk()
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
from datetime import datetime, timedelta, timezone
|
|
4
|
-
|
|
5
|
-
from aiohttp import web
|
|
6
|
-
from humanfriendly import parse_timespan
|
|
7
|
-
from marshmallow import ValidationError
|
|
8
|
-
|
|
9
|
-
from udata_hydra import config, context
|
|
10
|
-
from udata_hydra.crawl import get_excluded_clause
|
|
11
|
-
from udata_hydra.db.check import Check
|
|
12
|
-
from udata_hydra.db.resource import Resource
|
|
13
|
-
from udata_hydra.logger import setup_logging
|
|
14
|
-
from udata_hydra.schemas import CheckSchema, ResourceQuerySchema
|
|
15
|
-
from udata_hydra.utils.minio import delete_resource_from_minio
|
|
16
|
-
from udata_hydra.worker import QUEUES
|
|
17
|
-
|
|
18
|
-
log = setup_logging()
|
|
19
|
-
routes = web.RouteTableDef()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _get_args(request, params=("url", "resource_id")) -> list:
|
|
23
|
-
"""Get GET parameters from request"""
|
|
24
|
-
data = [request.query.get(param) for param in params]
|
|
25
|
-
if not any(data):
|
|
26
|
-
raise web.HTTPBadRequest()
|
|
27
|
-
return data
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@routes.post("/api/resource/created/")
|
|
31
|
-
async def resource_created(request: web.Request) -> web.Response:
|
|
32
|
-
"""Endpoint to receive a resource creation event from a source
|
|
33
|
-
Will create a new resource in the DB "catalog" table and mark it as priority for next crawling
|
|
34
|
-
Respond with a 200 status code and a JSON body with a message key set to "created"
|
|
35
|
-
If error, respond with a 400 status code
|
|
36
|
-
"""
|
|
37
|
-
try:
|
|
38
|
-
payload = await request.json()
|
|
39
|
-
valid_payload: dict = ResourceQuerySchema().load(payload)
|
|
40
|
-
except ValidationError as err:
|
|
41
|
-
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
42
|
-
|
|
43
|
-
resource = valid_payload["document"]
|
|
44
|
-
if not resource:
|
|
45
|
-
raise web.HTTPBadRequest(text="Missing document body")
|
|
46
|
-
|
|
47
|
-
dataset_id = valid_payload["dataset_id"]
|
|
48
|
-
resource_id = valid_payload["resource_id"]
|
|
49
|
-
|
|
50
|
-
await Resource.insert(
|
|
51
|
-
dataset_id=dataset_id,
|
|
52
|
-
resource_id=resource_id,
|
|
53
|
-
url=resource["url"],
|
|
54
|
-
priority=True,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
return web.json_response({"message": "created"})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@routes.post("/api/resource/updated/")
|
|
61
|
-
async def resource_updated(request: web.Request) -> web.Response:
|
|
62
|
-
"""Endpoint to receive a resource update event from a source
|
|
63
|
-
Will update an existing resource in the DB "catalog" table and mark it as priority for next crawling
|
|
64
|
-
Respond with a 200 status code and a JSON body with a message key set to "updated"
|
|
65
|
-
If error, respond with a 400 status code
|
|
66
|
-
"""
|
|
67
|
-
try:
|
|
68
|
-
payload = await request.json()
|
|
69
|
-
valid_payload: dict = ResourceQuerySchema().load(payload)
|
|
70
|
-
except ValidationError as err:
|
|
71
|
-
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
72
|
-
|
|
73
|
-
resource = valid_payload["document"]
|
|
74
|
-
if not resource:
|
|
75
|
-
raise web.HTTPBadRequest(text="Missing document body")
|
|
76
|
-
|
|
77
|
-
dataset_id = valid_payload["dataset_id"]
|
|
78
|
-
resource_id = valid_payload["resource_id"]
|
|
79
|
-
|
|
80
|
-
await Resource.update_or_insert(dataset_id, resource_id, resource["url"])
|
|
81
|
-
|
|
82
|
-
return web.json_response({"message": "updated"})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@routes.post("/api/resource/deleted/")
|
|
86
|
-
async def resource_deleted(request: web.Request) -> web.Response:
|
|
87
|
-
try:
|
|
88
|
-
payload = await request.json()
|
|
89
|
-
valid_payload: dict = ResourceQuerySchema().load(payload)
|
|
90
|
-
except ValidationError as err:
|
|
91
|
-
raise web.HTTPBadRequest(text=json.dumps(err.messages))
|
|
92
|
-
|
|
93
|
-
dataset_id = valid_payload["dataset_id"]
|
|
94
|
-
resource_id = valid_payload["resource_id"]
|
|
95
|
-
|
|
96
|
-
pool = request.app["pool"]
|
|
97
|
-
async with pool.acquire() as connection:
|
|
98
|
-
if config.SAVE_TO_MINIO:
|
|
99
|
-
delete_resource_from_minio(dataset_id, resource_id)
|
|
100
|
-
# Mark resource as deleted in catalog table
|
|
101
|
-
q = f"""UPDATE catalog SET deleted = TRUE WHERE resource_id = '{resource_id}';"""
|
|
102
|
-
await connection.execute(q)
|
|
103
|
-
|
|
104
|
-
return web.json_response({"message": "deleted"})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@routes.get("/api/checks/latest/")
|
|
108
|
-
async def get_check(request: web.Request) -> web.Response:
|
|
109
|
-
"""Get the latest check for a given URL or resource_id"""
|
|
110
|
-
url, resource_id = _get_args(request)
|
|
111
|
-
data = await Check.get_latest(url, resource_id)
|
|
112
|
-
if not data:
|
|
113
|
-
raise web.HTTPNotFound()
|
|
114
|
-
if data["deleted"]:
|
|
115
|
-
raise web.HTTPGone()
|
|
116
|
-
return web.json_response(CheckSchema().dump(dict(data)))
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
@routes.get("/api/checks/all/")
|
|
120
|
-
async def get_checks(request: web.Request) -> web.Response:
|
|
121
|
-
url, resource_id = _get_args(request)
|
|
122
|
-
data = await Check.get_all(url, resource_id)
|
|
123
|
-
if not data:
|
|
124
|
-
raise web.HTTPNotFound()
|
|
125
|
-
return web.json_response([CheckSchema().dump(dict(r)) for r in data])
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@routes.get("/api/status/crawler/")
|
|
129
|
-
async def status_crawler(request: web.Request) -> web.Response:
|
|
130
|
-
q = f"""
|
|
131
|
-
SELECT
|
|
132
|
-
SUM(CASE WHEN last_check IS NULL THEN 1 ELSE 0 END) AS count_left,
|
|
133
|
-
SUM(CASE WHEN last_check IS NOT NULL THEN 1 ELSE 0 END) AS count_checked
|
|
134
|
-
FROM catalog
|
|
135
|
-
WHERE {get_excluded_clause()}
|
|
136
|
-
AND catalog.deleted = False
|
|
137
|
-
"""
|
|
138
|
-
stats_catalog = await request.app["pool"].fetchrow(q)
|
|
139
|
-
|
|
140
|
-
since = parse_timespan(config.SINCE)
|
|
141
|
-
since = datetime.now(timezone.utc) - timedelta(seconds=since)
|
|
142
|
-
q = f"""
|
|
143
|
-
SELECT
|
|
144
|
-
SUM(CASE WHEN checks.created_at <= $1 THEN 1 ELSE 0 END) AS count_outdated
|
|
145
|
-
--, SUM(CASE WHEN checks.created_at > $1 THEN 1 ELSE 0 END) AS count_fresh
|
|
146
|
-
FROM catalog, checks
|
|
147
|
-
WHERE {get_excluded_clause()}
|
|
148
|
-
AND catalog.last_check = checks.id
|
|
149
|
-
AND catalog.deleted = False
|
|
150
|
-
"""
|
|
151
|
-
stats_checks = await request.app["pool"].fetchrow(q, since)
|
|
152
|
-
|
|
153
|
-
count_left = stats_catalog["count_left"] + (stats_checks["count_outdated"] or 0)
|
|
154
|
-
# all w/ a check, minus those with an outdated checked
|
|
155
|
-
count_checked = stats_catalog["count_checked"] - (stats_checks["count_outdated"] or 0)
|
|
156
|
-
total = stats_catalog["count_left"] + stats_catalog["count_checked"]
|
|
157
|
-
rate_checked = round(stats_catalog["count_checked"] / total * 100, 1)
|
|
158
|
-
rate_checked_fresh = round(count_checked / total * 100, 1)
|
|
159
|
-
|
|
160
|
-
return web.json_response(
|
|
161
|
-
{
|
|
162
|
-
"total": total,
|
|
163
|
-
"pending_checks": count_left,
|
|
164
|
-
"fresh_checks": count_checked,
|
|
165
|
-
"checks_percentage": rate_checked,
|
|
166
|
-
"fresh_checks_percentage": rate_checked_fresh,
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
@routes.get("/api/status/worker/")
|
|
172
|
-
async def status_worker(request: web.Request) -> web.Response:
|
|
173
|
-
res = {"queued": {q: len(context.queue(q)) for q in QUEUES}}
|
|
174
|
-
return web.json_response(res)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@routes.get("/api/stats/")
|
|
178
|
-
async def stats(request: web.Request) -> web.Response:
|
|
179
|
-
q = f"""
|
|
180
|
-
SELECT count(*) AS count_checked
|
|
181
|
-
FROM catalog
|
|
182
|
-
WHERE {get_excluded_clause()}
|
|
183
|
-
AND last_check IS NOT NULL
|
|
184
|
-
AND catalog.deleted = False
|
|
185
|
-
"""
|
|
186
|
-
stats_catalog = await request.app["pool"].fetchrow(q)
|
|
187
|
-
|
|
188
|
-
q = f"""
|
|
189
|
-
SELECT
|
|
190
|
-
SUM(CASE WHEN error IS NULL AND timeout = False THEN 1 ELSE 0 END) AS count_ok,
|
|
191
|
-
SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) AS count_error,
|
|
192
|
-
SUM(CASE WHEN timeout = True THEN 1 ELSE 0 END) AS count_timeout
|
|
193
|
-
FROM catalog, checks
|
|
194
|
-
WHERE {get_excluded_clause()}
|
|
195
|
-
AND catalog.last_check = checks.id
|
|
196
|
-
AND catalog.deleted = False
|
|
197
|
-
"""
|
|
198
|
-
stats_status = await request.app["pool"].fetchrow(q)
|
|
199
|
-
|
|
200
|
-
def cmp_rate(key):
|
|
201
|
-
if stats_catalog["count_checked"] == 0:
|
|
202
|
-
return 0
|
|
203
|
-
return round(stats_status[key] / stats_catalog["count_checked"] * 100, 1)
|
|
204
|
-
|
|
205
|
-
q = f"""
|
|
206
|
-
SELECT checks.status, count(*) as count FROM checks, catalog
|
|
207
|
-
WHERE catalog.last_check = checks.id
|
|
208
|
-
AND checks.status IS NOT NULL
|
|
209
|
-
AND {get_excluded_clause()}
|
|
210
|
-
AND last_check IS NOT NULL
|
|
211
|
-
AND catalog.deleted = False
|
|
212
|
-
GROUP BY checks.status
|
|
213
|
-
ORDER BY count DESC;
|
|
214
|
-
"""
|
|
215
|
-
res = await request.app["pool"].fetch(q)
|
|
216
|
-
return web.json_response(
|
|
217
|
-
{
|
|
218
|
-
"status": sorted(
|
|
219
|
-
[
|
|
220
|
-
{
|
|
221
|
-
"label": s,
|
|
222
|
-
"count": stats_status[f"count_{s}"] or 0,
|
|
223
|
-
"percentage": cmp_rate(f"count_{s}"),
|
|
224
|
-
}
|
|
225
|
-
for s in ["error", "timeout", "ok"]
|
|
226
|
-
],
|
|
227
|
-
key=lambda x: x["count"],
|
|
228
|
-
reverse=True,
|
|
229
|
-
),
|
|
230
|
-
"status_codes": [
|
|
231
|
-
{
|
|
232
|
-
"code": r["status"],
|
|
233
|
-
"count": r["count"],
|
|
234
|
-
"percentage": round(r["count"] / sum(r["count"] for r in res) * 100, 1),
|
|
235
|
-
}
|
|
236
|
-
for r in res
|
|
237
|
-
],
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
@routes.get("/api/health/")
|
|
243
|
-
async def health(request: web.Request) -> web.Response:
|
|
244
|
-
test_connection = await request.app["pool"].fetchrow("SELECT 1")
|
|
245
|
-
assert next(test_connection.values()) == 1
|
|
246
|
-
return web.HTTPOk()
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
async def app_factory() -> web.Application:
|
|
250
|
-
async def app_startup(app):
|
|
251
|
-
app["pool"] = await context.pool()
|
|
252
|
-
|
|
253
|
-
async def app_cleanup(app):
|
|
254
|
-
if "pool" in app:
|
|
255
|
-
await app["pool"].close()
|
|
256
|
-
|
|
257
|
-
app = web.Application()
|
|
258
|
-
app.add_routes(routes)
|
|
259
|
-
app.on_startup.append(app_startup)
|
|
260
|
-
app.on_cleanup.append(app_cleanup)
|
|
261
|
-
return app
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def run():
|
|
265
|
-
web.run_app(app_factory(), path=os.environ.get("HYDRA_APP_SOCKET_PATH"))
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if __name__ == "__main__":
|
|
269
|
-
run()
|
|
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
|
{udata_hydra-2.0.0.dev2347 → udata_hydra-2.0.0.dev2384}/udata_hydra/schemas/resource_query.py
RENAMED
|
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
|