udata 9.2.4.dev32010__py2.py3-none-any.whl → 9.2.4.dev32029__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api_fields.py +38 -8
- udata/core/dataservices/api.py +73 -1
- udata/core/dataservices/models.py +5 -1
- udata/core/dataset/api.py +11 -0
- udata/core/dataset/tasks.py +35 -0
- udata/settings.py +4 -0
- udata/static/chunks/{11.68f9d93d3cceb8f7d028.js → 11.533879c9f0603d62d253.js} +3 -3
- udata/static/chunks/{11.68f9d93d3cceb8f7d028.js.map → 11.533879c9f0603d62d253.js.map} +1 -1
- udata/static/chunks/{13.58166c2054c1d8a22f61.js → 13.bef5fdb3e147e94fea99.js} +2 -2
- udata/static/chunks/{13.58166c2054c1d8a22f61.js.map → 13.bef5fdb3e147e94fea99.js.map} +1 -1
- udata/static/chunks/{17.07c060685907b7eb3a84.js → 17.b91d28f550dc44bc4979.js} +2 -2
- udata/static/chunks/{17.07c060685907b7eb3a84.js.map → 17.b91d28f550dc44bc4979.js.map} +1 -1
- udata/static/chunks/{19.4f7a5b71ef006ac268c1.js → 19.6e0770f66adad68ef253.js} +3 -3
- udata/static/chunks/{19.4f7a5b71ef006ac268c1.js.map → 19.6e0770f66adad68ef253.js.map} +1 -1
- udata/static/chunks/{5.860362216c0ce3ec565f.js → 5.448ccace1bf3389d9b38.js} +3 -3
- udata/static/chunks/{5.860362216c0ce3ec565f.js.map → 5.448ccace1bf3389d9b38.js.map} +1 -1
- udata/static/chunks/{6.6617c697f229f362d732.js → 6.037369868acc94a7533f.js} +3 -3
- udata/static/chunks/{6.6617c697f229f362d732.js.map → 6.037369868acc94a7533f.js.map} +1 -1
- udata/static/chunks/{8.291bde987ed97294e4de.js → 8.2109f27ae53f737a3398.js} +2 -2
- udata/static/chunks/{8.291bde987ed97294e4de.js.map → 8.2109f27ae53f737a3398.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_dataservices_api.py +52 -17
- {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32029.dist-info}/METADATA +3 -1
- {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32029.dist-info}/RECORD +29 -29
- {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32029.dist-info}/LICENSE +0 -0
- {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32029.dist-info}/WHEEL +0 -0
- {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32029.dist-info}/entry_points.txt +0 -0
- {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32029.dist-info}/top_level.txt +0 -0
udata/api_fields.py
CHANGED
|
@@ -76,7 +76,25 @@ def convert_db_to_field(key, field, info):
|
|
|
76
76
|
constructor = custom_restx_fields.ImageField
|
|
77
77
|
|
|
78
78
|
elif isinstance(field, mongo_fields.ListField):
|
|
79
|
-
# For lists, we
|
|
79
|
+
# For lists, we can expose them only by showing a link to the API
|
|
80
|
+
# with the results of the list to avoid listing a lot of sub-ressources
|
|
81
|
+
# (for example for a dataservices with thousands of datasets).
|
|
82
|
+
href = info.get("href", None)
|
|
83
|
+
if href:
|
|
84
|
+
|
|
85
|
+
def constructor_read(**kwargs):
|
|
86
|
+
return restx_fields.Raw(
|
|
87
|
+
attribute=lambda o: {
|
|
88
|
+
"rel": "subsection",
|
|
89
|
+
"href": href(o),
|
|
90
|
+
"type": "GET",
|
|
91
|
+
"total": len(o[key]),
|
|
92
|
+
},
|
|
93
|
+
description="Visit this API link to see the list.",
|
|
94
|
+
**kwargs,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# If it's a standard list, we convert the inner value from Mongo to RestX then we create
|
|
80
98
|
# the `List` RestX type with this converted inner value.
|
|
81
99
|
# There is three level of information, from most important to least
|
|
82
100
|
# 1. `inner_field_info` inside `__additional_field_info__` on the parent
|
|
@@ -87,9 +105,13 @@ def convert_db_to_field(key, field, info):
|
|
|
87
105
|
f"{key}.inner", field.field, {**info, **inner_info, **info.get("inner_field_info", {})}
|
|
88
106
|
)
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
if constructor_read is None:
|
|
109
|
+
# We don't want to set the `constructor_read` if it's already set
|
|
110
|
+
# by the `href` code above.
|
|
111
|
+
def constructor_read(**kwargs):
|
|
112
|
+
return restx_fields.List(field_read, **kwargs)
|
|
92
113
|
|
|
114
|
+
# But we want to keep the `constructor_write` to allow changing the list.
|
|
93
115
|
def constructor_write(**kwargs):
|
|
94
116
|
return restx_fields.List(field_write, **kwargs)
|
|
95
117
|
elif isinstance(
|
|
@@ -98,7 +120,7 @@ def convert_db_to_field(key, field, info):
|
|
|
98
120
|
|
|
99
121
|
def constructor(**kwargs):
|
|
100
122
|
return restx_fields.Nested(lazy_reference, **kwargs)
|
|
101
|
-
elif isinstance(field, mongo_fields.ReferenceField):
|
|
123
|
+
elif isinstance(field, (mongo_fields.ReferenceField, mongo_fields.LazyReferenceField)):
|
|
102
124
|
# For reference we accept while writing a String representing the ID of the referenced model.
|
|
103
125
|
# For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
|
|
104
126
|
# the referenced model, if not we return a String (and RestX will call the `str()` of the model
|
|
@@ -199,9 +221,14 @@ def generate_fields(**kwargs):
|
|
|
199
221
|
|
|
200
222
|
if "constraints" not in filterable:
|
|
201
223
|
filterable["constraints"] = []
|
|
202
|
-
if isinstance(
|
|
224
|
+
if isinstance(
|
|
225
|
+
field, (mongo_fields.ReferenceField, mongo_fields.LazyReferenceField)
|
|
226
|
+
) or (
|
|
203
227
|
isinstance(field, mongo_fields.ListField)
|
|
204
|
-
and isinstance(
|
|
228
|
+
and isinstance(
|
|
229
|
+
field.field,
|
|
230
|
+
(mongo_fields.ReferenceField, mongo_fields.LazyReferenceField),
|
|
231
|
+
)
|
|
205
232
|
):
|
|
206
233
|
filterable["constraints"].append("objectid")
|
|
207
234
|
|
|
@@ -389,11 +416,14 @@ def patch(obj, request):
|
|
|
389
416
|
if hasattr(model_attribute, "from_input"):
|
|
390
417
|
value = model_attribute.from_input(value)
|
|
391
418
|
elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
392
|
-
model_attribute.field,
|
|
419
|
+
model_attribute.field,
|
|
420
|
+
(mongo_fields.ReferenceField, mongo_fields.LazyReferenceField),
|
|
393
421
|
):
|
|
394
422
|
# TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
|
|
395
423
|
value = [wrap_primary_key(key, model_attribute.field, id) for id in value]
|
|
396
|
-
elif isinstance(
|
|
424
|
+
elif isinstance(
|
|
425
|
+
model_attribute, (mongo_fields.ReferenceField, mongo_fields.LazyReferenceField)
|
|
426
|
+
):
|
|
397
427
|
value = wrap_primary_key(key, model_attribute, value)
|
|
398
428
|
elif isinstance(
|
|
399
429
|
model_attribute,
|
udata/core/dataservices/api.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
3
|
import mongoengine
|
|
4
|
+
from bson import ObjectId
|
|
4
5
|
from flask import make_response, redirect, request, url_for
|
|
5
6
|
from flask_login import current_user
|
|
6
7
|
|
|
7
|
-
from udata.api import API, api
|
|
8
|
+
from udata.api import API, api, fields
|
|
8
9
|
from udata.api_fields import patch
|
|
10
|
+
from udata.core.dataset.models import Dataset
|
|
9
11
|
from udata.core.dataset.permissions import OwnablePermission
|
|
10
12
|
from udata.core.followers.api import FollowAPI
|
|
11
13
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
@@ -95,6 +97,76 @@ class DataserviceAPI(API):
|
|
|
95
97
|
return "", 204
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
dataservice_add_datasets_fields = api.model(
|
|
101
|
+
"DataserviceDatasetsAdd",
|
|
102
|
+
{
|
|
103
|
+
"id": fields.String(description="Id of the dataset to add", required=True),
|
|
104
|
+
},
|
|
105
|
+
location="json",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@ns.route("/<dataservice:dataservice>/datasets/", endpoint="dataservice_datasets", doc=common_doc)
|
|
110
|
+
class DataserviceDatasetsAPI(API):
|
|
111
|
+
@api.secure
|
|
112
|
+
@api.doc("dataservice_datasets_create")
|
|
113
|
+
@api.expect([dataservice_add_datasets_fields])
|
|
114
|
+
@api.marshal_with(Dataservice.__read_fields__)
|
|
115
|
+
@api.response(400, "Malformed object id(s) in request")
|
|
116
|
+
@api.response(400, "Expecting a list")
|
|
117
|
+
@api.response(400, "Expecting a list of dicts with id attribute")
|
|
118
|
+
@api.response(404, "Dataservice not found")
|
|
119
|
+
@api.response(403, "Forbidden")
|
|
120
|
+
@api.response(410, "Dataservice has been deleted")
|
|
121
|
+
def post(self, dataservice):
|
|
122
|
+
if dataservice.deleted_at:
|
|
123
|
+
api.abort(410, "Dataservice has been deleted")
|
|
124
|
+
|
|
125
|
+
OwnablePermission(dataservice).test()
|
|
126
|
+
|
|
127
|
+
data = request.json
|
|
128
|
+
|
|
129
|
+
if not isinstance(data, list):
|
|
130
|
+
api.abort(400, "Expecting a list")
|
|
131
|
+
if not all(isinstance(d, dict) and d.get("id") for d in data):
|
|
132
|
+
api.abort(400, "Expecting a list of dicts with id attribute")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
datasets = Dataset.objects.filter(id__in=[d["id"] for d in data]).only("id")
|
|
136
|
+
diff = set(d.id for d in datasets) - set(d.id for d in dataservice.datasets)
|
|
137
|
+
except mongoengine.errors.ValidationError:
|
|
138
|
+
api.abort(400, "Malformed object id(s) in request")
|
|
139
|
+
|
|
140
|
+
if diff:
|
|
141
|
+
dataservice.datasets += [ObjectId(did) for did in diff]
|
|
142
|
+
dataservice.save()
|
|
143
|
+
|
|
144
|
+
return dataservice, 201
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@ns.route(
|
|
148
|
+
"/<dataservice:dataservice>/datasets/<dataset:dataset>/",
|
|
149
|
+
endpoint="dataservice_dataset",
|
|
150
|
+
doc=common_doc,
|
|
151
|
+
)
|
|
152
|
+
class DataserviceDatasetAPI(API):
|
|
153
|
+
@api.secure
|
|
154
|
+
@api.response(404, "Dataservice not found")
|
|
155
|
+
@api.response(404, "Dataset not found in dataservice")
|
|
156
|
+
def delete(self, dataservice, dataset):
|
|
157
|
+
if dataservice.deleted_at:
|
|
158
|
+
api.abort(410, "Dataservice has been deleted")
|
|
159
|
+
|
|
160
|
+
OwnablePermission(dataservice).test()
|
|
161
|
+
|
|
162
|
+
if dataset not in dataservice.datasets:
|
|
163
|
+
api.abort(404, "Dataset not found in dataservice")
|
|
164
|
+
dataservice.datasets = [d for d in dataservice.datasets if d.id != dataset.id]
|
|
165
|
+
dataservice.save()
|
|
166
|
+
|
|
167
|
+
return None, 204
|
|
168
|
+
|
|
169
|
+
|
|
98
170
|
@ns.route("/<dataservice:dataservice>/rdf", endpoint="dataservice_rdf", doc=common_doc)
|
|
99
171
|
@api.response(404, "Dataservice not found")
|
|
100
172
|
@api.response(410, "Dataservice has been deleted")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
|
+
from flask import url_for
|
|
3
4
|
from mongoengine import Q
|
|
4
5
|
|
|
5
6
|
import udata.core.contact_point.api_fields as contact_api_fields
|
|
@@ -171,13 +172,16 @@ class Dataservice(WithMetrics, Owned, db.Document):
|
|
|
171
172
|
datasets = field(
|
|
172
173
|
db.ListField(
|
|
173
174
|
field(
|
|
174
|
-
db.
|
|
175
|
+
db.LazyReferenceField(Dataset, passthrough=True),
|
|
175
176
|
nested_fields=datasets_api_fields.dataset_ref_fields,
|
|
176
177
|
)
|
|
177
178
|
),
|
|
178
179
|
filterable={
|
|
179
180
|
"key": "dataset",
|
|
180
181
|
},
|
|
182
|
+
href=lambda dataservice: url_for(
|
|
183
|
+
"api.datasets", dataservice=dataservice.id, _external=True
|
|
184
|
+
),
|
|
181
185
|
)
|
|
182
186
|
|
|
183
187
|
harvest = field(
|
udata/core/dataset/api.py
CHANGED
|
@@ -33,6 +33,7 @@ from udata.auth import admin_permission
|
|
|
33
33
|
from udata.core import storages
|
|
34
34
|
from udata.core.badges import api as badges_api
|
|
35
35
|
from udata.core.badges.fields import badge_fields
|
|
36
|
+
from udata.core.dataservices.models import Dataservice
|
|
36
37
|
from udata.core.dataset.models import CHECKSUM_TYPES
|
|
37
38
|
from udata.core.followers.api import FollowAPI
|
|
38
39
|
from udata.core.storages.api import handle_upload, upload_parser
|
|
@@ -100,6 +101,7 @@ class DatasetApiParser(ModelApiParser):
|
|
|
100
101
|
self.parser.add_argument("schema", type=str, location="args")
|
|
101
102
|
self.parser.add_argument("schema_version", type=str, location="args")
|
|
102
103
|
self.parser.add_argument("topic", type=str, location="args")
|
|
104
|
+
self.parser.add_argument("dataservice", type=str, location="args")
|
|
103
105
|
|
|
104
106
|
@staticmethod
|
|
105
107
|
def parse_filters(datasets, args):
|
|
@@ -148,6 +150,15 @@ class DatasetApiParser(ModelApiParser):
|
|
|
148
150
|
pass
|
|
149
151
|
else:
|
|
150
152
|
datasets = datasets.filter(id__in=[d.id for d in topic.datasets])
|
|
153
|
+
if args.get("dataservice"):
|
|
154
|
+
if not ObjectId.is_valid(args["dataservice"]):
|
|
155
|
+
api.abort(400, "Dataservice arg must be an identifier")
|
|
156
|
+
try:
|
|
157
|
+
dataservice = Dataservice.objects.get(id=args["dataservice"])
|
|
158
|
+
except Dataservice.DoesNotExist:
|
|
159
|
+
pass
|
|
160
|
+
else:
|
|
161
|
+
datasets = datasets.filter(id__in=[d.id for d in dataservice.datasets])
|
|
151
162
|
return datasets
|
|
152
163
|
|
|
153
164
|
|
udata/core/dataset/tasks.py
CHANGED
|
@@ -5,10 +5,12 @@ from tempfile import NamedTemporaryFile
|
|
|
5
5
|
|
|
6
6
|
from celery.utils.log import get_task_logger
|
|
7
7
|
from flask import current_app
|
|
8
|
+
from mongoengine import ValidationError
|
|
8
9
|
|
|
9
10
|
from udata import mail
|
|
10
11
|
from udata import models as udata_models
|
|
11
12
|
from udata.core import storages
|
|
13
|
+
from udata.core.dataservices.models import Dataservice
|
|
12
14
|
from udata.frontend import csv
|
|
13
15
|
from udata.harvest.models import HarvestJob
|
|
14
16
|
from udata.i18n import lazy_gettext as _
|
|
@@ -231,3 +233,36 @@ def export_csv(self, model=None):
|
|
|
231
233
|
models = (model,) if model else ALLOWED_MODELS
|
|
232
234
|
for model in models:
|
|
233
235
|
export_csv_for_model(model, dataset)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@job("bind-tabular-dataservice")
|
|
239
|
+
def bind_tabular_dataservice(self):
|
|
240
|
+
"""
|
|
241
|
+
Bind the datasets served by TabularAPI to its dataservice objects
|
|
242
|
+
"""
|
|
243
|
+
TABULAR_API_DATASERVICE_ID = current_app.config.get("TABULAR_API_DATASERVICE_ID", [])
|
|
244
|
+
|
|
245
|
+
if not TABULAR_API_DATASERVICE_ID:
|
|
246
|
+
log.error("TABULAR_API_DATASERVICE_ID setting value not set")
|
|
247
|
+
return
|
|
248
|
+
try:
|
|
249
|
+
dataservice = Dataservice.objects.get(id=TABULAR_API_DATASERVICE_ID)
|
|
250
|
+
except Dataservice.DoesNotExist:
|
|
251
|
+
log.error("TABULAR_API_DATASERVICE_ID points to a non existent dataservice")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
datasets = Dataset.objects(
|
|
255
|
+
**{
|
|
256
|
+
"resources__extras__analysis:parsing:finished_at__exists": True,
|
|
257
|
+
"resources__extras__analysis:parsing:error": None,
|
|
258
|
+
}
|
|
259
|
+
).visible()
|
|
260
|
+
|
|
261
|
+
dataservice.datasets = datasets
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
dataservice.save()
|
|
265
|
+
except ValidationError as e:
|
|
266
|
+
log.error(exc_info=e)
|
|
267
|
+
|
|
268
|
+
log.info(f"Bound {datasets.count()} datasets to TabularAPI dataservice")
|
udata/settings.py
CHANGED
|
@@ -538,6 +538,10 @@ class Defaults(object):
|
|
|
538
538
|
###########################################################################
|
|
539
539
|
MATTERMOST_WEBHOOK = None
|
|
540
540
|
|
|
541
|
+
# Tabular API Dataservice ID
|
|
542
|
+
###########################################################################
|
|
543
|
+
TABULAR_API_DATASERVICE_ID = None
|
|
544
|
+
|
|
541
545
|
|
|
542
546
|
class Testing(object):
|
|
543
547
|
"""Sane values for testing. Should be applied as override"""
|