udata 9.2.4.dev32010__py2.py3-none-any.whl → 9.2.4.dev32022__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.

Files changed (27) hide show
  1. udata/api_fields.py +38 -8
  2. udata/core/dataservices/api.py +73 -1
  3. udata/core/dataservices/models.py +5 -1
  4. udata/core/dataset/api.py +11 -0
  5. udata/static/chunks/{11.68f9d93d3cceb8f7d028.js → 11.4a20a75f827c5a1125c3.js} +3 -3
  6. udata/static/chunks/{11.68f9d93d3cceb8f7d028.js.map → 11.4a20a75f827c5a1125c3.js.map} +1 -1
  7. udata/static/chunks/{13.58166c2054c1d8a22f61.js → 13.645dd0b7c0b9210f1b56.js} +2 -2
  8. udata/static/chunks/{13.58166c2054c1d8a22f61.js.map → 13.645dd0b7c0b9210f1b56.js.map} +1 -1
  9. udata/static/chunks/{17.07c060685907b7eb3a84.js → 17.8e19985c4d12a3b7b0c0.js} +2 -2
  10. udata/static/chunks/{17.07c060685907b7eb3a84.js.map → 17.8e19985c4d12a3b7b0c0.js.map} +1 -1
  11. udata/static/chunks/{19.4f7a5b71ef006ac268c1.js → 19.825a43c330157e351fca.js} +3 -3
  12. udata/static/chunks/{19.4f7a5b71ef006ac268c1.js.map → 19.825a43c330157e351fca.js.map} +1 -1
  13. udata/static/chunks/{5.860362216c0ce3ec565f.js → 5.448ccace1bf3389d9b38.js} +3 -3
  14. udata/static/chunks/{5.860362216c0ce3ec565f.js.map → 5.448ccace1bf3389d9b38.js.map} +1 -1
  15. udata/static/chunks/{6.6617c697f229f362d732.js → 6.037369868acc94a7533f.js} +3 -3
  16. udata/static/chunks/{6.6617c697f229f362d732.js.map → 6.037369868acc94a7533f.js.map} +1 -1
  17. udata/static/chunks/{8.291bde987ed97294e4de.js → 8.5ee0cf635c848abbfc05.js} +2 -2
  18. udata/static/chunks/{8.291bde987ed97294e4de.js.map → 8.5ee0cf635c848abbfc05.js.map} +1 -1
  19. udata/static/common.js +1 -1
  20. udata/static/common.js.map +1 -1
  21. udata/tests/api/test_dataservices_api.py +52 -17
  22. {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32022.dist-info}/METADATA +2 -1
  23. {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32022.dist-info}/RECORD +27 -27
  24. {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32022.dist-info}/LICENSE +0 -0
  25. {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32022.dist-info}/WHEEL +0 -0
  26. {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32022.dist-info}/entry_points.txt +0 -0
  27. {udata-9.2.4.dev32010.dist-info → udata-9.2.4.dev32022.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 convert the inner value from Mongo to RestX then we create
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
- def constructor_read(**kwargs):
91
- return restx_fields.List(field_read, **kwargs)
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(field, mongo_fields.ReferenceField) or (
224
+ if isinstance(
225
+ field, (mongo_fields.ReferenceField, mongo_fields.LazyReferenceField)
226
+ ) or (
203
227
  isinstance(field, mongo_fields.ListField)
204
- and isinstance(field.field, mongo_fields.ReferenceField)
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, mongoengine.fields.ReferenceField
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(model_attribute, mongoengine.fields.ReferenceField):
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,
@@ -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.ReferenceField(Dataset),
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