udata 8.0.2.dev29253__py2.py3-none-any.whl → 8.0.2.dev29284__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/commands/purge.py +8 -2
- udata/core/dataservices/api.py +11 -3
- udata/core/dataservices/models.py +14 -2
- udata/core/dataservices/permissions.py +7 -0
- udata/core/dataservices/tasks.py +25 -0
- udata/harvest/backends/base.py +93 -103
- udata/harvest/backends/dcat.py +51 -82
- udata/harvest/tasks.py +3 -13
- udata/harvest/tests/factories.py +13 -6
- udata/harvest/tests/test_actions.py +2 -2
- udata/harvest/tests/test_base_backend.py +9 -5
- udata/harvest/tests/test_dcat_backend.py +13 -0
- udata/tests/dataservice/test_dataservice_tasks.py +46 -0
- {udata-8.0.2.dev29253.dist-info → udata-8.0.2.dev29284.dist-info}/METADATA +4 -2
- {udata-8.0.2.dev29253.dist-info → udata-8.0.2.dev29284.dist-info}/RECORD +19 -16
- {udata-8.0.2.dev29253.dist-info → udata-8.0.2.dev29284.dist-info}/LICENSE +0 -0
- {udata-8.0.2.dev29253.dist-info → udata-8.0.2.dev29284.dist-info}/WHEEL +0 -0
- {udata-8.0.2.dev29253.dist-info → udata-8.0.2.dev29284.dist-info}/entry_points.txt +0 -0
- {udata-8.0.2.dev29253.dist-info → udata-8.0.2.dev29284.dist-info}/top_level.txt +0 -0
udata/commands/purge.py
CHANGED
|
@@ -5,6 +5,7 @@ import click
|
|
|
5
5
|
from udata.commands import cli, success
|
|
6
6
|
|
|
7
7
|
from udata.core.dataset.tasks import purge_datasets
|
|
8
|
+
from udata.core.dataservices.tasks import purge_dataservices
|
|
8
9
|
from udata.core.organization.tasks import purge_organizations
|
|
9
10
|
from udata.core.reuse.tasks import purge_reuses
|
|
10
11
|
|
|
@@ -15,13 +16,14 @@ log = logging.getLogger(__name__)
|
|
|
15
16
|
@click.option('-d', '--datasets', is_flag=True)
|
|
16
17
|
@click.option('-r', '--reuses', is_flag=True)
|
|
17
18
|
@click.option('-o', '--organizations', is_flag=True)
|
|
18
|
-
|
|
19
|
+
@click.option('--dataservices', is_flag=True)
|
|
20
|
+
def purge(datasets, reuses, organizations, dataservices):
|
|
19
21
|
'''
|
|
20
22
|
Permanently remove data flagged as deleted.
|
|
21
23
|
|
|
22
24
|
If no model flag is given, all models are purged.
|
|
23
25
|
'''
|
|
24
|
-
purge_all = not any((datasets, reuses, organizations))
|
|
26
|
+
purge_all = not any((datasets, reuses, organizations, dataservices))
|
|
25
27
|
|
|
26
28
|
if purge_all or datasets:
|
|
27
29
|
log.info('Purging datasets')
|
|
@@ -35,4 +37,8 @@ def purge(datasets, reuses, organizations):
|
|
|
35
37
|
log.info('Purging organizations')
|
|
36
38
|
purge_organizations()
|
|
37
39
|
|
|
40
|
+
if purge_all or dataservices:
|
|
41
|
+
log.info('Purging dataservices')
|
|
42
|
+
purge_dataservices()
|
|
43
|
+
|
|
38
44
|
success('Done')
|
udata/core/dataservices/api.py
CHANGED
|
@@ -6,11 +6,12 @@ import mongoengine
|
|
|
6
6
|
from udata.api import api, API
|
|
7
7
|
from udata.api_fields import patch
|
|
8
8
|
from udata.core.dataset.permissions import OwnablePermission
|
|
9
|
+
from udata.core.followers.api import FollowAPI
|
|
9
10
|
from .models import Dataservice
|
|
10
|
-
from udata.models import db
|
|
11
11
|
|
|
12
12
|
ns = api.namespace('dataservices', 'Dataservices related operations (beta)')
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
@ns.route('/', endpoint='dataservices')
|
|
15
16
|
class DataservicesAPI(API):
|
|
16
17
|
'''Dataservices collection endpoint'''
|
|
@@ -39,6 +40,7 @@ class DataservicesAPI(API):
|
|
|
39
40
|
|
|
40
41
|
return dataservice, 201
|
|
41
42
|
|
|
43
|
+
|
|
42
44
|
@ns.route('/<dataservice:dataservice>/', endpoint='dataservice')
|
|
43
45
|
class DataserviceAPI(API):
|
|
44
46
|
@api.doc('get_dataservice')
|
|
@@ -47,7 +49,7 @@ class DataserviceAPI(API):
|
|
|
47
49
|
if dataservice.deleted_at and not OwnablePermission(dataservice).can():
|
|
48
50
|
api.abort(410, 'Dataservice has been deleted')
|
|
49
51
|
return dataservice
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
@api.secure
|
|
52
54
|
@api.doc('update_dataservice', responses={400: 'Validation error'})
|
|
53
55
|
@api.expect(Dataservice.__write_fields__)
|
|
@@ -75,10 +77,16 @@ class DataserviceAPI(API):
|
|
|
75
77
|
api.abort(410, 'dataservice has been deleted')
|
|
76
78
|
|
|
77
79
|
OwnablePermission(dataservice).test()
|
|
78
|
-
|
|
79
80
|
dataservice.deleted_at = datetime.utcnow()
|
|
80
81
|
dataservice.modified_at = datetime.utcnow()
|
|
81
82
|
dataservice.save()
|
|
82
83
|
|
|
83
84
|
return '', 204
|
|
84
85
|
|
|
86
|
+
|
|
87
|
+
@ns.route('/<id>/followers/', endpoint='dataservice_followers')
|
|
88
|
+
@ns.doc(get={'id': 'list_dataservice_followers'},
|
|
89
|
+
post={'id': 'follow_dataservice'},
|
|
90
|
+
delete={'id': 'unfollow_dataservice'})
|
|
91
|
+
class DataserviceFollowersAPI(FollowAPI):
|
|
92
|
+
model = Dataservice
|
|
@@ -3,11 +3,11 @@ from udata.api_fields import field, function_field, generate_fields
|
|
|
3
3
|
from udata.core.dataset.models import Dataset
|
|
4
4
|
from udata.core.metrics.models import WithMetrics
|
|
5
5
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
6
|
-
from udata.i18n import lazy_gettext as _
|
|
7
6
|
import udata.core.contact_point.api_fields as contact_api_fields
|
|
8
7
|
import udata.core.dataset.api_fields as datasets_api_fields
|
|
8
|
+
from udata.i18n import lazy_gettext as _
|
|
9
9
|
|
|
10
|
-
from udata.models import db
|
|
10
|
+
from udata.models import db, Discussion, Follow
|
|
11
11
|
from udata.uris import endpoint_for
|
|
12
12
|
|
|
13
13
|
# "frequency"
|
|
@@ -128,3 +128,15 @@ class Dataservice(WithMetrics, Owned, db.Document):
|
|
|
128
128
|
# temporal_coverage = db.EmbeddedDocumentField(db.DateRange)
|
|
129
129
|
# spatial = db.EmbeddedDocumentField(SpatialCoverage)
|
|
130
130
|
# harvest = db.EmbeddedDocumentField(HarvestDatasetMetadata)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def is_hidden(self):
|
|
134
|
+
return self.private or self.deleted_at or self.archived_at
|
|
135
|
+
|
|
136
|
+
def count_discussions(self):
|
|
137
|
+
self.metrics['discussions'] = Discussion.objects(subject=self, closed=None).count()
|
|
138
|
+
self.save()
|
|
139
|
+
|
|
140
|
+
def count_followers(self):
|
|
141
|
+
self.metrics['followers'] = Follow.objects(until=None).followers(self).count()
|
|
142
|
+
self.save()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from celery.utils.log import get_task_logger
|
|
2
|
+
|
|
3
|
+
from udata.core.dataservices.models import Dataservice
|
|
4
|
+
# from udata.harvest.models import HarvestJob
|
|
5
|
+
from udata.models import (Follow, Discussion, Activity, Transfer)
|
|
6
|
+
from udata.tasks import job
|
|
7
|
+
|
|
8
|
+
log = get_task_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@job('purge-dataservices')
|
|
12
|
+
def purge_dataservices(self):
|
|
13
|
+
for dataservice in Dataservice.objects(deleted_at__ne=None):
|
|
14
|
+
log.info(f'Purging dataservice {dataservice}')
|
|
15
|
+
# Remove followers
|
|
16
|
+
Follow.objects(following=dataservice).delete()
|
|
17
|
+
# Remove discussions
|
|
18
|
+
Discussion.objects(subject=dataservice).delete()
|
|
19
|
+
# Remove HarvestItem references
|
|
20
|
+
# TODO: uncomment when adding dataservice harvest
|
|
21
|
+
# HarvestJob.objects(items__dataservice=dataservice).update(set__items__S__dataservice=None)
|
|
22
|
+
# Remove associated Transfers
|
|
23
|
+
Transfer.objects(subject=dataservice).delete()
|
|
24
|
+
# Remove dataservice
|
|
25
|
+
dataservice.delete()
|
udata/harvest/backends/base.py
CHANGED
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
import traceback
|
|
3
3
|
|
|
4
4
|
from datetime import datetime, date, timedelta
|
|
5
|
+
from typing import Optional
|
|
5
6
|
from uuid import UUID
|
|
6
7
|
|
|
7
8
|
import requests
|
|
@@ -68,7 +69,12 @@ class HarvestFeature(object):
|
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
class BaseBackend(object):
|
|
71
|
-
|
|
72
|
+
"""
|
|
73
|
+
Base class that wrap children methods to add error management and debug logs.
|
|
74
|
+
Also provides a few helpers needed on all or some backends.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
"""
|
|
72
78
|
|
|
73
79
|
name = None
|
|
74
80
|
display_name = None
|
|
@@ -127,92 +133,69 @@ class BaseBackend(object):
|
|
|
127
133
|
def get_filters(self):
|
|
128
134
|
return self.config.get('filters', [])
|
|
129
135
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return self.job
|
|
136
|
+
def inner_harvest(self):
|
|
137
|
+
raise NotImplementedError
|
|
138
|
+
|
|
139
|
+
def inner_process_dataset(self, item: HarvestItem) -> Dataset:
|
|
140
|
+
raise NotImplementedError
|
|
136
141
|
|
|
137
|
-
def
|
|
138
|
-
'
|
|
139
|
-
log.debug('Initializing backend')
|
|
142
|
+
def harvest(self):
|
|
143
|
+
log.debug(f'Starting harvesting {self.source.name} ({self.source.url})…')
|
|
140
144
|
factory = HarvestJob if self.dryrun else HarvestJob.objects.create
|
|
141
|
-
self.job = factory(status='
|
|
145
|
+
self.job = factory(status='initialized',
|
|
142
146
|
started=datetime.utcnow(),
|
|
143
147
|
source=self.source)
|
|
144
148
|
|
|
145
149
|
before_harvest_job.send(self)
|
|
146
150
|
|
|
147
151
|
try:
|
|
148
|
-
self.
|
|
149
|
-
|
|
150
|
-
if
|
|
151
|
-
self.
|
|
152
|
+
self.inner_harvest()
|
|
153
|
+
|
|
154
|
+
if self.source.autoarchive:
|
|
155
|
+
self.autoarchive()
|
|
156
|
+
|
|
157
|
+
self.job.status = 'done'
|
|
158
|
+
|
|
159
|
+
if any(i.status == 'failed' for i in self.job.items):
|
|
160
|
+
self.job.status += '-errors'
|
|
152
161
|
except HarvestValidationError as e:
|
|
153
|
-
log.
|
|
154
|
-
|
|
155
|
-
error = HarvestError(message=safe_unicode(e))
|
|
156
|
-
self.job.errors.append(error)
|
|
157
|
-
self.job.status = 'failed'
|
|
158
|
-
self.end()
|
|
159
|
-
return None
|
|
160
|
-
except Exception as e:
|
|
162
|
+
log.exception(f'Harvesting validation failed for "{safe_unicode(self.source.name)}" ({self.source.backend})')
|
|
163
|
+
|
|
161
164
|
self.job.status = 'failed'
|
|
165
|
+
|
|
162
166
|
error = HarvestError(message=safe_unicode(e))
|
|
163
167
|
self.job.errors.append(error)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
log.exception(msg.format(self.source))
|
|
167
|
-
return None
|
|
168
|
+
except Exception as e:
|
|
169
|
+
log.exception(f'Harvesting failed for "{safe_unicode(self.source.name)}" ({self.source.backend})')
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
self.job.items = self.job.items[:self.max_items]
|
|
171
|
+
self.job.status = 'failed'
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
error = HarvestError(message=safe_unicode(e), details=traceback.format_exc())
|
|
174
|
+
self.job.errors.append(error)
|
|
175
|
+
finally:
|
|
176
|
+
self.end_job()
|
|
177
|
+
|
|
178
|
+
return self.job
|
|
174
179
|
|
|
175
|
-
|
|
180
|
+
def process_dataset(self, remote_id: str, **kwargs):
|
|
181
|
+
log.debug(f'Processing dataset {remote_id}…')
|
|
176
182
|
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
# TODO add `type` to `HarvestItem` to differentiate `Dataset` from `Dataservice`
|
|
184
|
+
item = HarvestItem(status='started', started=datetime.utcnow(), remote_id=remote_id)
|
|
185
|
+
self.job.items.append(item)
|
|
186
|
+
self.save_job()
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
self.process_item(item)
|
|
188
|
+
try:
|
|
189
|
+
if not remote_id:
|
|
190
|
+
raise HarvestSkipException("missing identifier")
|
|
184
191
|
|
|
185
|
-
|
|
186
|
-
log.debug('Processing: %s', item.remote_id)
|
|
187
|
-
item.status = 'started'
|
|
188
|
-
item.started = datetime.utcnow()
|
|
189
|
-
if not self.dryrun:
|
|
190
|
-
self.job.save()
|
|
192
|
+
dataset = self.inner_process_dataset(item, **kwargs)
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
dataset = self.
|
|
194
|
-
if not dataset.harvest:
|
|
195
|
-
dataset.harvest = HarvestDatasetMetadata()
|
|
196
|
-
dataset.harvest.domain = self.source.domain
|
|
197
|
-
dataset.harvest.remote_id = item.remote_id
|
|
198
|
-
dataset.harvest.source_id = str(self.source.id)
|
|
199
|
-
dataset.harvest.last_update = datetime.utcnow()
|
|
200
|
-
dataset.harvest.backend = self.display_name
|
|
201
|
-
|
|
202
|
-
# unset archived status if needed
|
|
203
|
-
if dataset.harvest:
|
|
204
|
-
dataset.harvest.archived_at = None
|
|
205
|
-
dataset.harvest.archived = None
|
|
194
|
+
# Use `item.remote_id` because `inner_process_dataset` could have modified it.
|
|
195
|
+
dataset.harvest = self.update_harvest_info(dataset.harvest, item.remote_id)
|
|
206
196
|
dataset.archived = None
|
|
207
197
|
|
|
208
|
-
# TODO
|
|
209
|
-
if not dataset.organization and not dataset.owner:
|
|
210
|
-
if self.source.organization:
|
|
211
|
-
dataset.organization = self.source.organization
|
|
212
|
-
elif self.source.owner:
|
|
213
|
-
dataset.owner = self.source.owner
|
|
214
|
-
|
|
215
|
-
# TODO: Apply editble mappings
|
|
198
|
+
# TODO: Apply editable mappings
|
|
216
199
|
|
|
217
200
|
if self.dryrun:
|
|
218
201
|
dataset.validate()
|
|
@@ -221,26 +204,54 @@ class BaseBackend(object):
|
|
|
221
204
|
item.dataset = dataset
|
|
222
205
|
item.status = 'done'
|
|
223
206
|
except HarvestSkipException as e:
|
|
224
|
-
log.info('Skipped item %s : %s', item.remote_id, safe_unicode(e))
|
|
225
207
|
item.status = 'skipped'
|
|
208
|
+
|
|
209
|
+
log.info(f'Skipped item {item.remote_id} : {safe_unicode(e)}')
|
|
226
210
|
item.errors.append(HarvestError(message=safe_unicode(e)))
|
|
227
211
|
except HarvestValidationError as e:
|
|
228
|
-
log.info('Error validating item %s : %s', item.remote_id, safe_unicode(e))
|
|
229
212
|
item.status = 'failed'
|
|
213
|
+
|
|
214
|
+
log.info(f'Error validating item {item.remote_id} : {safe_unicode(e)}')
|
|
230
215
|
item.errors.append(HarvestError(message=safe_unicode(e)))
|
|
231
216
|
except Exception as e:
|
|
232
|
-
log.exception('Error while processing %s : %s',
|
|
233
|
-
item.remote_id,
|
|
234
|
-
safe_unicode(e))
|
|
235
|
-
error = HarvestError(message=safe_unicode(e),
|
|
236
|
-
details=traceback.format_exc())
|
|
237
|
-
item.errors.append(error)
|
|
238
217
|
item.status = 'failed'
|
|
218
|
+
log.exception(f'Error while processing {item.remote_id} : {safe_unicode(e)}')
|
|
219
|
+
|
|
220
|
+
error = HarvestError(message=safe_unicode(e), details=traceback.format_exc())
|
|
221
|
+
item.errors.append(error)
|
|
222
|
+
finally:
|
|
223
|
+
item.ended = datetime.utcnow()
|
|
224
|
+
self.save_job()
|
|
225
|
+
|
|
226
|
+
def is_done(self) -> bool:
|
|
227
|
+
'''Should be called after process_dataset to know if we reach the max items'''
|
|
228
|
+
return self.max_items and len(self.job.items) >= self.max_items
|
|
229
|
+
|
|
230
|
+
def update_harvest_info(self, harvest: Optional[HarvestDatasetMetadata], remote_id: int):
|
|
231
|
+
if not harvest:
|
|
232
|
+
harvest = HarvestDatasetMetadata()
|
|
233
|
+
harvest.domain = self.source.domain
|
|
234
|
+
harvest.remote_id = remote_id
|
|
235
|
+
harvest.source_id = str(self.source.id)
|
|
236
|
+
harvest.last_update = datetime.utcnow()
|
|
237
|
+
harvest.backend = self.display_name
|
|
239
238
|
|
|
240
|
-
|
|
239
|
+
harvest.archived_at = None
|
|
240
|
+
harvest.archived = None
|
|
241
|
+
|
|
242
|
+
return harvest
|
|
243
|
+
|
|
244
|
+
def save_job(self):
|
|
245
|
+
if not self.dryrun:
|
|
246
|
+
self.job.save()
|
|
247
|
+
|
|
248
|
+
def end_job(self):
|
|
249
|
+
self.job.ended = datetime.utcnow()
|
|
241
250
|
if not self.dryrun:
|
|
242
251
|
self.job.save()
|
|
243
252
|
|
|
253
|
+
after_harvest_job.send(self)
|
|
254
|
+
|
|
244
255
|
def autoarchive(self):
|
|
245
256
|
'''
|
|
246
257
|
Archive items that exist on the local instance but not on remote platform
|
|
@@ -262,34 +273,13 @@ class BaseBackend(object):
|
|
|
262
273
|
archive_harvested_dataset(dataset, reason='not-on-remote', dryrun=self.dryrun)
|
|
263
274
|
# add a HarvestItem to the job list (useful for report)
|
|
264
275
|
# even when archiving has already been done (useful for debug)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
self.job.save()
|
|
276
|
+
self.job.items.append(HarvestItem(
|
|
277
|
+
remote_id=str(dataset.harvest.remote_id),
|
|
278
|
+
dataset=dataset,
|
|
279
|
+
status='archived'
|
|
280
|
+
))
|
|
271
281
|
|
|
272
|
-
|
|
273
|
-
raise NotImplementedError
|
|
274
|
-
|
|
275
|
-
def add_item(self, identifier, *args, **kwargs):
|
|
276
|
-
item = HarvestItem(remote_id=str(identifier), args=args, kwargs=kwargs)
|
|
277
|
-
self.job.items.append(item)
|
|
278
|
-
return item
|
|
279
|
-
|
|
280
|
-
def finalize(self):
|
|
281
|
-
if self.source.autoarchive:
|
|
282
|
-
self.autoarchive()
|
|
283
|
-
self.job.status = 'done'
|
|
284
|
-
if any(i.status == 'failed' for i in self.job.items):
|
|
285
|
-
self.job.status += '-errors'
|
|
286
|
-
self.end()
|
|
287
|
-
|
|
288
|
-
def end(self):
|
|
289
|
-
self.job.ended = datetime.utcnow()
|
|
290
|
-
if not self.dryrun:
|
|
291
|
-
self.job.save()
|
|
292
|
-
after_harvest_job.send(self)
|
|
282
|
+
self.save_job()
|
|
293
283
|
|
|
294
284
|
def get_dataset(self, remote_id):
|
|
295
285
|
'''Get or create a dataset given its remote ID (and its source)
|
|
@@ -352,4 +342,4 @@ class BaseBackend(object):
|
|
|
352
342
|
msg = str(error)
|
|
353
343
|
errors.append(msg)
|
|
354
344
|
msg = '\n- '.join(['Validation error:'] + errors)
|
|
355
|
-
raise HarvestValidationError(msg)
|
|
345
|
+
raise HarvestValidationError(msg)
|
udata/harvest/backends/dcat.py
CHANGED
|
@@ -7,13 +7,15 @@ import boto3
|
|
|
7
7
|
from flask import current_app
|
|
8
8
|
from datetime import date
|
|
9
9
|
import json
|
|
10
|
-
from typing import List
|
|
10
|
+
from typing import Generator, List
|
|
11
11
|
|
|
12
|
+
from udata.core.dataset.models import Dataset
|
|
12
13
|
from udata.rdf import (
|
|
13
14
|
DCAT, DCT, HYDRA, SPDX, namespace_manager, guess_format, url_from_rdf
|
|
14
15
|
)
|
|
15
16
|
from udata.core.dataset.rdf import dataset_from_rdf
|
|
16
17
|
from udata.storage.s3 import store_as_json, get_from_json
|
|
18
|
+
from udata.harvest.models import HarvestItem
|
|
17
19
|
|
|
18
20
|
from .base import BaseBackend
|
|
19
21
|
|
|
@@ -59,14 +61,17 @@ def extract_graph(source, target, node, specs):
|
|
|
59
61
|
class DcatBackend(BaseBackend):
|
|
60
62
|
display_name = 'DCAT'
|
|
61
63
|
|
|
62
|
-
def
|
|
63
|
-
'''List all datasets for a given ...'''
|
|
64
|
+
def inner_harvest(self):
|
|
64
65
|
fmt = self.get_format()
|
|
65
|
-
graphs = self.parse_graph(self.source.url, fmt)
|
|
66
|
-
|
|
67
66
|
self.job.data = { 'format': fmt }
|
|
68
67
|
|
|
69
|
-
serialized_graphs = [
|
|
68
|
+
serialized_graphs = []
|
|
69
|
+
|
|
70
|
+
for page_number, page in self.walk_graph(self.source.url, fmt):
|
|
71
|
+
self.process_one_datasets_page(page_number, page)
|
|
72
|
+
serialized_graphs.append(page.serialize(format=fmt, indent=None))
|
|
73
|
+
|
|
74
|
+
# TODO call `walk_graph` with `process_dataservices`
|
|
70
75
|
|
|
71
76
|
# The official MongoDB document size in 16MB. The default value here is 15MB to account for other fields in the document (and for difference between * 1024 vs * 1000).
|
|
72
77
|
max_harvest_graph_size_in_mongo = current_app.config.get('HARVEST_MAX_CATALOG_SIZE_IN_MONGO')
|
|
@@ -105,13 +110,11 @@ class DcatBackend(BaseBackend):
|
|
|
105
110
|
raise ValueError(msg)
|
|
106
111
|
return fmt
|
|
107
112
|
|
|
108
|
-
def
|
|
113
|
+
def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, None]:
|
|
109
114
|
"""
|
|
110
|
-
|
|
111
|
-
The index in the list is the page number
|
|
115
|
+
Yield all RDF pages as `Graph` from the source
|
|
112
116
|
"""
|
|
113
|
-
|
|
114
|
-
page = 0
|
|
117
|
+
page_number = 0
|
|
115
118
|
while url:
|
|
116
119
|
subgraph = Graph(namespace_manager=namespace_manager)
|
|
117
120
|
response = self.get(url)
|
|
@@ -128,19 +131,26 @@ class DcatBackend(BaseBackend):
|
|
|
128
131
|
pagination = subgraph.resource(pagination)
|
|
129
132
|
url = url_from_rdf(pagination, prop)
|
|
130
133
|
break
|
|
131
|
-
graphs.append(subgraph)
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
self.add_item(id, **kwargs)
|
|
137
|
-
if self.max_items and len(self.job.items) >= self.max_items:
|
|
138
|
-
# this will stop iterating on pagination
|
|
139
|
-
url = None
|
|
135
|
+
yield page_number, subgraph
|
|
136
|
+
if self.is_done():
|
|
137
|
+
return
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
page_number += 1
|
|
140
|
+
|
|
141
|
+
def process_one_datasets_page(self, page_number: int, page: Graph):
|
|
142
|
+
for node in page.subjects(RDF.type, DCAT.Dataset):
|
|
143
|
+
remote_id = page.value(node, DCT.identifier)
|
|
144
|
+
self.process_dataset(remote_id, page_number=page_number, page=page, node=node)
|
|
145
|
+
|
|
146
|
+
if self.is_done():
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
def inner_process_dataset(self, item: HarvestItem, page_number: int, page: Graph, node):
|
|
150
|
+
item.kwargs['page_number'] = page_number
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
dataset = self.get_dataset(item.remote_id)
|
|
153
|
+
return dataset_from_rdf(page, dataset, node=node)
|
|
144
154
|
|
|
145
155
|
def get_node_from_item(self, graph, item):
|
|
146
156
|
for node in graph.subjects(RDF.type, DCAT.Dataset):
|
|
@@ -148,33 +158,6 @@ class DcatBackend(BaseBackend):
|
|
|
148
158
|
return node
|
|
149
159
|
raise ValueError(f'Unable to find dataset with DCT.identifier:{item.remote_id}')
|
|
150
160
|
|
|
151
|
-
def process(self, item):
|
|
152
|
-
if item.remote_id == 'None':
|
|
153
|
-
raise ValueError('The DCT.identifier is missing on this DCAT.Dataset record')
|
|
154
|
-
graph = Graph(namespace_manager=namespace_manager)
|
|
155
|
-
|
|
156
|
-
if self.job.data.get('graphs') is not None:
|
|
157
|
-
graphs = self.job.data['graphs']
|
|
158
|
-
else:
|
|
159
|
-
bucket = current_app.config.get('HARVEST_GRAPHS_S3_BUCKET')
|
|
160
|
-
if bucket is None:
|
|
161
|
-
raise ValueError(f"No bucket configured but the harvest job item {item.id} on job {self.job.id} doesn't have a graph in MongoDB.")
|
|
162
|
-
|
|
163
|
-
graphs = get_from_json(bucket, self.job.data['filename'])
|
|
164
|
-
if graphs is None:
|
|
165
|
-
raise ValueError(f"The file '{self.job.data['filename']}' is missing in S3 bucket '{bucket}'")
|
|
166
|
-
|
|
167
|
-
data = graphs[item.kwargs['page']]
|
|
168
|
-
format = self.job.data['format']
|
|
169
|
-
|
|
170
|
-
graph.parse(data=bytes(data, encoding='utf8'), format=format)
|
|
171
|
-
node = self.get_node_from_item(graph, item)
|
|
172
|
-
|
|
173
|
-
dataset = self.get_dataset(item.remote_id)
|
|
174
|
-
dataset = dataset_from_rdf(graph, dataset, node=node)
|
|
175
|
-
return dataset
|
|
176
|
-
|
|
177
|
-
|
|
178
161
|
def next_record_if_should_continue(self, start, search_results):
|
|
179
162
|
next_record = int(search_results.attrib['nextRecord'])
|
|
180
163
|
matched_count = int(search_results.attrib['numberOfRecordsMatched'])
|
|
@@ -209,7 +192,10 @@ class CswDcatBackend(DcatBackend):
|
|
|
209
192
|
|
|
210
193
|
DCAT_SCHEMA = 'http://www.w3.org/ns/dcat#'
|
|
211
194
|
|
|
212
|
-
def
|
|
195
|
+
def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, None]:
|
|
196
|
+
"""
|
|
197
|
+
Yield all RDF pages as `Graph` from the source
|
|
198
|
+
"""
|
|
213
199
|
body = '''<csw:GetRecords xmlns:csw="http://www.opengis.net/cat/csw/2.0.2"
|
|
214
200
|
xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
|
215
201
|
service="CSW" version="2.0.2" resultType="results"
|
|
@@ -227,8 +213,7 @@ class CswDcatBackend(DcatBackend):
|
|
|
227
213
|
</csw:GetRecords>'''
|
|
228
214
|
headers = {'Content-Type': 'application/xml'}
|
|
229
215
|
|
|
230
|
-
|
|
231
|
-
page = 0
|
|
216
|
+
page_number = 0
|
|
232
217
|
start = 1
|
|
233
218
|
|
|
234
219
|
response = self.post(url, data=body.format(start=start, schema=self.DCAT_SCHEMA),
|
|
@@ -239,36 +224,29 @@ class CswDcatBackend(DcatBackend):
|
|
|
239
224
|
if tree.tag == '{' + OWS_NAMESPACE + '}ExceptionReport':
|
|
240
225
|
raise ValueError(f'Failed to query CSW:\n{content}')
|
|
241
226
|
while tree:
|
|
242
|
-
graph = Graph(namespace_manager=namespace_manager)
|
|
243
227
|
search_results = tree.find('csw:SearchResults', {'csw': CSW_NAMESPACE})
|
|
244
228
|
if search_results is None:
|
|
245
|
-
log.error(f'No search results found for {url} on page {
|
|
229
|
+
log.error(f'No search results found for {url} on page {page_number}')
|
|
246
230
|
break
|
|
247
231
|
for child in search_results:
|
|
248
232
|
subgraph = Graph(namespace_manager=namespace_manager)
|
|
249
233
|
subgraph.parse(data=ET.tostring(child), format=fmt)
|
|
250
|
-
graph += subgraph
|
|
251
234
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
kwargs['type'] = 'uriref' if isinstance(node, URIRef) else 'blank'
|
|
256
|
-
self.add_item(id, **kwargs)
|
|
257
|
-
graphs.append(graph)
|
|
235
|
+
yield page_number, subgraph
|
|
236
|
+
if self.is_done():
|
|
237
|
+
return
|
|
258
238
|
|
|
259
239
|
next_record = self.next_record_if_should_continue(start, search_results)
|
|
260
240
|
if not next_record:
|
|
261
241
|
break
|
|
262
242
|
|
|
263
243
|
start = next_record
|
|
264
|
-
|
|
244
|
+
page_number += 1
|
|
265
245
|
|
|
266
246
|
tree = ET.fromstring(
|
|
267
247
|
self.post(url, data=body.format(start=start, schema=self.DCAT_SCHEMA),
|
|
268
248
|
headers=headers).content)
|
|
269
249
|
|
|
270
|
-
return graphs
|
|
271
|
-
|
|
272
250
|
|
|
273
251
|
class CswIso19139DcatBackend(DcatBackend):
|
|
274
252
|
'''
|
|
@@ -282,13 +260,10 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
282
260
|
|
|
283
261
|
XSL_URL = "https://raw.githubusercontent.com/SEMICeu/iso-19139-to-dcat-ap/master/iso-19139-to-dcat-ap.xsl"
|
|
284
262
|
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
See https://github.com/SEMICeu/iso-19139-to-dcat-ap for more information on the XSLT.
|
|
290
|
-
'''
|
|
291
|
-
|
|
263
|
+
def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, None]:
|
|
264
|
+
"""
|
|
265
|
+
Yield all RDF pages as `Graph` from the source
|
|
266
|
+
"""
|
|
292
267
|
# Load XSLT
|
|
293
268
|
xsl = ET.fromstring(self.get(self.XSL_URL).content)
|
|
294
269
|
transform = ET.XSLT(xsl)
|
|
@@ -320,8 +295,7 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
320
295
|
</csw:GetRecords>'''
|
|
321
296
|
headers = {'Content-Type': 'application/xml'}
|
|
322
297
|
|
|
323
|
-
|
|
324
|
-
page = 0
|
|
298
|
+
page_number = 0
|
|
325
299
|
start = 1
|
|
326
300
|
|
|
327
301
|
response = self.post(url, data=body.format(start=start, schema=self.ISO_SCHEMA),
|
|
@@ -338,7 +312,7 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
338
312
|
# infos (useful for pagination)
|
|
339
313
|
search_results = tree_before_transform.find('csw:SearchResults', {'csw': CSW_NAMESPACE})
|
|
340
314
|
if search_results is None:
|
|
341
|
-
log.error(f'No search results found for {url} on page {
|
|
315
|
+
log.error(f'No search results found for {url} on page {page_number}')
|
|
342
316
|
break
|
|
343
317
|
|
|
344
318
|
subgraph = Graph(namespace_manager=namespace_manager)
|
|
@@ -347,19 +321,16 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
347
321
|
if not subgraph.subjects(RDF.type, DCAT.Dataset):
|
|
348
322
|
raise ValueError("Failed to fetch CSW content")
|
|
349
323
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
kwargs['type'] = 'uriref' if isinstance(node, URIRef) else 'blank'
|
|
354
|
-
self.add_item(id, **kwargs)
|
|
355
|
-
graphs.append(subgraph)
|
|
324
|
+
yield page_number, subgraph
|
|
325
|
+
if self.is_done():
|
|
326
|
+
return
|
|
356
327
|
|
|
357
328
|
next_record = self.next_record_if_should_continue(start, search_results)
|
|
358
329
|
if not next_record:
|
|
359
330
|
break
|
|
360
331
|
|
|
361
332
|
start = next_record
|
|
362
|
-
|
|
333
|
+
page_number += 1
|
|
363
334
|
|
|
364
335
|
response = self.post(url, data=body.format(start=start, schema=self.ISO_SCHEMA),
|
|
365
336
|
headers=headers)
|
|
@@ -367,5 +338,3 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
367
338
|
|
|
368
339
|
tree_before_transform = ET.fromstring(response.content)
|
|
369
340
|
tree = transform(tree_before_transform, CoupledResourceLookUp="'disabled'")
|
|
370
|
-
|
|
371
|
-
return graphs
|
udata/harvest/tasks.py
CHANGED
|
@@ -18,20 +18,10 @@ def harvest(self, ident):
|
|
|
18
18
|
return # Ignore deleted sources
|
|
19
19
|
Backend = backends.get(current_app, source.backend)
|
|
20
20
|
backend = Backend(source)
|
|
21
|
-
items = backend.perform_initialization()
|
|
22
|
-
if items is None:
|
|
23
|
-
pass
|
|
24
|
-
elif items == 0:
|
|
25
|
-
backend.finalize()
|
|
26
|
-
else:
|
|
27
|
-
finalize = harvest_job_finalize.s(backend.job.id)
|
|
28
|
-
items = [
|
|
29
|
-
harvest_job_item.s(backend.job.id, item.remote_id)
|
|
30
|
-
for item in backend.job.items
|
|
31
|
-
]
|
|
32
|
-
chord(items)(finalize)
|
|
33
|
-
|
|
34
21
|
|
|
22
|
+
backend.harvest()
|
|
23
|
+
|
|
24
|
+
|
|
35
25
|
|
|
36
26
|
@task(ignore_result=False, route='low.harvest')
|
|
37
27
|
def harvest_job_item(job_id, item_id):
|
udata/harvest/tests/factories.py
CHANGED
|
@@ -6,9 +6,10 @@ from flask.signals import Namespace
|
|
|
6
6
|
|
|
7
7
|
from udata.factories import ModelFactory
|
|
8
8
|
from udata.core.dataset.factories import DatasetFactory
|
|
9
|
+
from udata.core.dataset.models import Dataset
|
|
9
10
|
|
|
10
11
|
from .. import backends
|
|
11
|
-
from ..models import HarvestSource, HarvestJob
|
|
12
|
+
from ..models import HarvestItem, HarvestSource, HarvestJob
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def dtfactory(start, end):
|
|
@@ -55,14 +56,20 @@ class FactoryBackend(backends.BaseBackend):
|
|
|
55
56
|
backends.HarvestFeature('toggled', 'Toggled', 'A togglable', True),
|
|
56
57
|
)
|
|
57
58
|
|
|
58
|
-
def
|
|
59
|
+
def inner_harvest(self):
|
|
59
60
|
mock_initialize.send(self)
|
|
60
61
|
for i in range(self.config.get('count', DEFAULT_COUNT)):
|
|
61
|
-
self.
|
|
62
|
+
self.process_dataset(str(i))
|
|
63
|
+
if self.is_done():
|
|
64
|
+
return
|
|
62
65
|
|
|
63
|
-
def
|
|
64
|
-
mock_process.send(self, item=item)
|
|
65
|
-
|
|
66
|
+
def inner_process_dataset(self, item: HarvestItem):
|
|
67
|
+
mock_process.send(self, item=item.remote_id)
|
|
68
|
+
|
|
69
|
+
dataset = self.get_dataset(item.remote_id)
|
|
70
|
+
dataset.title = f'dataset-{item.remote_id}'
|
|
71
|
+
|
|
72
|
+
return dataset
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
class MockBackendsMixin(object):
|
|
@@ -580,7 +580,7 @@ class ExecutionTestMixin(MockBackendsMixin):
|
|
|
580
580
|
|
|
581
581
|
def test_error_on_item(self):
|
|
582
582
|
def process(self, item):
|
|
583
|
-
if item
|
|
583
|
+
if item == '1':
|
|
584
584
|
raise ValueError('test')
|
|
585
585
|
|
|
586
586
|
source = HarvestSourceFactory(backend='factory')
|
|
@@ -723,7 +723,7 @@ class HarvestPreviewTest(MockBackendsMixin):
|
|
|
723
723
|
|
|
724
724
|
def test_preview_with_error_on_item(self):
|
|
725
725
|
def process(self, item):
|
|
726
|
-
if item
|
|
726
|
+
if item == '1':
|
|
727
727
|
raise ValueError('test')
|
|
728
728
|
|
|
729
729
|
source = HarvestSourceFactory(backend='factory')
|
|
@@ -5,6 +5,7 @@ from urllib.parse import urlparse
|
|
|
5
5
|
from dateutil.parser import parse
|
|
6
6
|
from voluptuous import Schema
|
|
7
7
|
|
|
8
|
+
from udata.harvest.models import HarvestItem
|
|
8
9
|
from udata.utils import faker
|
|
9
10
|
from udata.core.dataset import tasks
|
|
10
11
|
from udata.core.dataset.factories import DatasetFactory
|
|
@@ -31,12 +32,16 @@ class FakeBackend(BaseBackend):
|
|
|
31
32
|
HarvestFeature('enabled', 'A test feature enabled by default', default=True),
|
|
32
33
|
)
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
+
def inner_harvest(self):
|
|
35
36
|
for i in range(self.source.config.get('nb_datasets', 3)):
|
|
36
|
-
|
|
37
|
+
remote_id = f'fake-{i}'
|
|
38
|
+
self.process_dataset(remote_id)
|
|
39
|
+
if self.is_done():
|
|
40
|
+
return
|
|
37
41
|
|
|
38
|
-
def
|
|
42
|
+
def inner_process_dataset(self, item: HarvestItem):
|
|
39
43
|
dataset = self.get_dataset(item.remote_id)
|
|
44
|
+
|
|
40
45
|
for key, value in DatasetFactory.as_dict(visible=True).items():
|
|
41
46
|
setattr(dataset, key, value)
|
|
42
47
|
if self.source.config.get('last_modified'):
|
|
@@ -219,8 +224,7 @@ class BaseBackendTest:
|
|
|
219
224
|
assert 'archived_at' not in dataset_no_arch.harvest
|
|
220
225
|
|
|
221
226
|
# test unarchive: archive manually then relaunch harvest
|
|
222
|
-
|
|
223
|
-
dataset = Dataset.objects.get(**q)
|
|
227
|
+
dataset = Dataset.objects.get(**{'harvest__remote_id': 'fake-1'})
|
|
224
228
|
dataset.archived = datetime.utcnow()
|
|
225
229
|
dataset.harvest.archived = 'not-on-remote'
|
|
226
230
|
dataset.harvest.archived_at = datetime.utcnow()
|
|
@@ -240,6 +240,19 @@ class DcatBackendTest:
|
|
|
240
240
|
actions.purge_jobs()
|
|
241
241
|
assert get_from_json(current_app.config.get('HARVEST_GRAPHS_S3_BUCKET'), job.data['filename']) is None
|
|
242
242
|
|
|
243
|
+
@pytest.mark.options(SCHEMA_CATALOG_URL='https://example.com/schemas', HARVEST_MAX_ITEMS=2)
|
|
244
|
+
def test_harvest_max_items(self, rmock):
|
|
245
|
+
rmock.get('https://example.com/schemas', json=ResourceSchemaMockData.get_mock_data())
|
|
246
|
+
|
|
247
|
+
filename = 'bnodes.xml'
|
|
248
|
+
url = mock_dcat(rmock, filename)
|
|
249
|
+
org = OrganizationFactory()
|
|
250
|
+
source = HarvestSourceFactory(backend='dcat', url=url, organization=org)
|
|
251
|
+
|
|
252
|
+
actions.run(source.slug)
|
|
253
|
+
|
|
254
|
+
assert Dataset.objects.count() == 2
|
|
255
|
+
assert HarvestJob.objects.first().status == 'done'
|
|
243
256
|
|
|
244
257
|
@pytest.mark.options(SCHEMA_CATALOG_URL='https://example.com/schemas')
|
|
245
258
|
def test_harvest_spatial(self, rmock):
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from udata.core.dataservices.models import Dataservice
|
|
4
|
+
from udata.core.user.factories import UserFactory
|
|
5
|
+
from udata.core.dataservices import tasks
|
|
6
|
+
from udata.models import Discussion, Follow, Message, Transfer
|
|
7
|
+
from udata.utils import faker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
pytestmark = pytest.mark.usefixtures('clean_db')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_purge_dataservices():
|
|
14
|
+
dataservices = [
|
|
15
|
+
Dataservice.objects.create(
|
|
16
|
+
title='delete me',
|
|
17
|
+
base_api_url='https://example.com/api',
|
|
18
|
+
deleted_at='2016-01-01'),
|
|
19
|
+
Dataservice.objects.create(
|
|
20
|
+
title='keep me',
|
|
21
|
+
base_api_url='https://example.com/api'),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
user = UserFactory()
|
|
25
|
+
transfer = Transfer.objects.create(
|
|
26
|
+
owner=user,
|
|
27
|
+
recipient=user,
|
|
28
|
+
subject=dataservices[0],
|
|
29
|
+
comment='comment',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
discussion = Discussion.objects.create(
|
|
33
|
+
subject=dataservices[0],
|
|
34
|
+
user=user,
|
|
35
|
+
title='test discussion',
|
|
36
|
+
discussion=[Message(content=faker.sentence(), posted_by=user)]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
follower = Follow.objects.create(follower=user, following=dataservices[0])
|
|
40
|
+
|
|
41
|
+
tasks.purge_dataservices()
|
|
42
|
+
|
|
43
|
+
assert Dataservice.objects.count() == 1
|
|
44
|
+
assert Transfer.objects.filter(id=transfer.id).count() == 0
|
|
45
|
+
assert Discussion.objects.filter(id=discussion.id).count() == 0
|
|
46
|
+
assert Follow.objects.filter(id=follower.id).count() == 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: udata
|
|
3
|
-
Version: 8.0.2.
|
|
3
|
+
Version: 8.0.2.dev29284
|
|
4
4
|
Summary: Open data portal
|
|
5
5
|
Home-page: https://github.com/opendatateam/udata
|
|
6
6
|
Author: Opendata Team
|
|
@@ -137,7 +137,9 @@ It is collectively taken care of by members of the
|
|
|
137
137
|
|
|
138
138
|
## Current (in progress)
|
|
139
139
|
|
|
140
|
-
-
|
|
140
|
+
- **breaking change** Harvest backend is now sync [#3030](https://github.com/opendatateam/udata/pull/3030)
|
|
141
|
+
- Allow dataservices to be discussed and followed [#3049](https://github.com/opendatateam/udata/pull/3049)
|
|
142
|
+
- Add purge-dataservices job [#3049](https://github.com/opendatateam/udata/pull/3049)
|
|
141
143
|
|
|
142
144
|
## 8.0.1 (2024-05-28)
|
|
143
145
|
|
|
@@ -45,7 +45,7 @@ udata/commands/fixtures.py,sha256=RrzRdUBJ2FbS-iDsEMPu5Nfp0WJXzU2rk3GB9t6JlRc,58
|
|
|
45
45
|
udata/commands/images.py,sha256=bzVvLj9LZ_nkyTdPlPe4g5U26k53VTH5ADCWmOfM8e4,2037
|
|
46
46
|
udata/commands/info.py,sha256=Gf0AT8yP1tScbKLz8aXurlro55ot19sefrbOhELi034,1548
|
|
47
47
|
udata/commands/init.py,sha256=t0dKNQ0_1Tg4r84h-XDwIZbdhkyEDu6k8jpn5YEAyOg,1524
|
|
48
|
-
udata/commands/purge.py,sha256=
|
|
48
|
+
udata/commands/purge.py,sha256=weytJTaqDRHHrJd7bODkT-vNR2H8PehvWYhZHnNVAps,1203
|
|
49
49
|
udata/commands/serve.py,sha256=EvXYQUzSr6FhDtgSmiKkNpkPesrx4r74KpMd6iUOUN8,2567
|
|
50
50
|
udata/commands/static.py,sha256=tAB4WsJ9MhFjdmVif57RKGUhXpbQPyh1Oktzu86Baz4,2201
|
|
51
51
|
udata/commands/test.py,sha256=h1fgPpWARY7ucu1XzYRje-OuQflS-ThhpBRA35IO_Os,892
|
|
@@ -79,8 +79,10 @@ udata/core/contact_point/factories.py,sha256=ATuV1seBCGKY3CzvPDG5nxfBBqHu-3YtER0
|
|
|
79
79
|
udata/core/contact_point/forms.py,sha256=ggLhSJ1IRn5MclrhydckjAxwr4fFZxgAD4huSSucSsA,598
|
|
80
80
|
udata/core/contact_point/models.py,sha256=NlNKureCpzgTLJuGviZPjNx-ABYRp4j2L-ur9Gmixao,324
|
|
81
81
|
udata/core/dataservices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
82
|
-
udata/core/dataservices/api.py,sha256=
|
|
83
|
-
udata/core/dataservices/models.py,sha256=
|
|
82
|
+
udata/core/dataservices/api.py,sha256=rjCU55NNGgCDRlurfhJUT2byBGJWN5coM8b7AApzEew,3090
|
|
83
|
+
udata/core/dataservices/models.py,sha256=6cE7tQ6GahnFaz1U_8yNmJqlCzoHQxZdet4qXQaqa7k,4449
|
|
84
|
+
udata/core/dataservices/permissions.py,sha256=X9Bh8e0pnx6OgeEf6NowXZUiwyreUa6UY479B16cCqs,175
|
|
85
|
+
udata/core/dataservices/tasks.py,sha256=NOWcTPoLasMrrvq9EkwQMGlUbQQmi_l3s815K-mtZTM,971
|
|
84
86
|
udata/core/dataset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
85
87
|
udata/core/dataset/actions.py,sha256=3pzBg_qOR-w7fwPpTOKUHXWC9lkjALbOn1UQFmmT-s0,1199
|
|
86
88
|
udata/core/dataset/activities.py,sha256=qQnHNL0hOB1IGtQl7JsnVOiUsWT0gm-pts9uDyR3bvU,1536
|
|
@@ -274,16 +276,16 @@ udata/harvest/forms.py,sha256=AxEFhBACjXVT9X1bSK8WzDAZgWg-hdaWBebFb1ihNYo,3389
|
|
|
274
276
|
udata/harvest/models.py,sha256=7mTZKcIxtagW5GimYIqPyzQ4tqExO3RzKxqOh0tB9PE,5988
|
|
275
277
|
udata/harvest/notifications.py,sha256=aNKUtUl73Caj_kl-ENOEbTX_cgtySqLjHhtH9FEDR9Y,774
|
|
276
278
|
udata/harvest/signals.py,sha256=wlXTi1E7rIVyNvxw0yUqyN5gF3thg276LAOmAF9vDJY,1338
|
|
277
|
-
udata/harvest/tasks.py,sha256=
|
|
279
|
+
udata/harvest/tasks.py,sha256=XbJPoposzkxitecDaK5WYnPqzCatAU-eHX5MUDu9nJo,1644
|
|
278
280
|
udata/harvest/backends/__init__.py,sha256=qcLhHKWO97TeWd93ZwymG_Cc9FO7sMM7h4fs6XYdtS8,447
|
|
279
|
-
udata/harvest/backends/base.py,sha256=
|
|
280
|
-
udata/harvest/backends/dcat.py,sha256=
|
|
281
|
+
udata/harvest/backends/base.py,sha256=A0itOkDRPPa7_umHI0q5bybBk0ZtEBTqeaq3DIe-bPw,11752
|
|
282
|
+
udata/harvest/backends/dcat.py,sha256=MzuBgUDNREjstFJIo5Z-epHqveT_nychIynJ633rNVU,13940
|
|
281
283
|
udata/harvest/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
282
|
-
udata/harvest/tests/factories.py,sha256=
|
|
283
|
-
udata/harvest/tests/test_actions.py,sha256=
|
|
284
|
+
udata/harvest/tests/factories.py,sha256=vUFON9GzI5CbD3bP8_ayOs3S9pHbhhHiI7B4GhoQtVE,2218
|
|
285
|
+
udata/harvest/tests/test_actions.py,sha256=TPHb8n8tlQ0l2lu8GzbymKPOpzTeN_VCtFmszZoCAQI,27583
|
|
284
286
|
udata/harvest/tests/test_api.py,sha256=QXhseHfnkBEmMbIJzroMdDYGLDj6Njal1s-2sn0xhEM,14888
|
|
285
|
-
udata/harvest/tests/test_base_backend.py,sha256=
|
|
286
|
-
udata/harvest/tests/test_dcat_backend.py,sha256=
|
|
287
|
+
udata/harvest/tests/test_base_backend.py,sha256=idFssHnN1iv2ktP1b1IlDpGglVR4Rzza-XuJr68KIlA,12240
|
|
288
|
+
udata/harvest/tests/test_dcat_backend.py,sha256=QsCyi00TeWeb3HUg_ym-PjO5f4DaEm4HIzbc8Wgs4Yc,33138
|
|
287
289
|
udata/harvest/tests/test_filters.py,sha256=V2HFZlexIJa6r1DX6g2ktvIgjg4gSY11QPfPOd3_Oug,2370
|
|
288
290
|
udata/harvest/tests/test_models.py,sha256=p2VazyrPXSArBuf8Kf19TGPcQ86SnOGCGmvjcMOw0s0,924
|
|
289
291
|
udata/harvest/tests/test_notifications.py,sha256=ZwtwioittW3XcZc0x6zbHjs1dVaAxPytlVymnJa5w0E,817
|
|
@@ -608,6 +610,7 @@ udata/tests/cli/test_cli_base.py,sha256=piqoq4Ib5bdZQpuUAJh583qfjCSglWZQclKzhO3Y
|
|
|
608
610
|
udata/tests/cli/test_db_cli.py,sha256=-hw9SU3RvNT7fvqVtQHxEpKstnjMU4L_DY9tiBH-ybs,1726
|
|
609
611
|
udata/tests/data/image.jpg,sha256=hdmpaCjOhmAAfNGuTqWKEjv7IC4GXJx-nP_rT274hc8,337049
|
|
610
612
|
udata/tests/data/image.png,sha256=GAqXz7w_u7CapODIUF45UpVddmqelnGQkcrwKZq3448,266488
|
|
613
|
+
udata/tests/dataservice/test_dataservice_tasks.py,sha256=bRL6A0arzAA0a_k0mAc_mZ9vkT23Cw3kNr40NIhj4PM,1375
|
|
611
614
|
udata/tests/dataset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
612
615
|
udata/tests/dataset/test_csv_adapter.py,sha256=2r5z4FuqG9pWgUyfDUw8afQYK5nIKE8XXOCuDHmEcZA,3207
|
|
613
616
|
udata/tests/dataset/test_dataset_actions.py,sha256=4_0F_TCSCfHpzqDt659c8hG4GpkWjVtmT4bAPgmE4B8,723
|
|
@@ -681,9 +684,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=uttB2K8VsqzkEQG-5HfTtFms_3LtV9
|
|
|
681
684
|
udata/translations/pt/LC_MESSAGES/udata.po,sha256=8Ql1Lp7Z9KLnvp-qRxw-NhFu1p35Xj-q6Jg9JHsYhcw,43733
|
|
682
685
|
udata/translations/sr/LC_MESSAGES/udata.mo,sha256=US8beNIMPxP5h-zD_jfP1TheDDd4DdRVS5UIiY5XVZ8,28553
|
|
683
686
|
udata/translations/sr/LC_MESSAGES/udata.po,sha256=TM0yMDvKRljyOzgZZMlTX6OfpF6OC4Ngf_9Zc8n6ayA,50313
|
|
684
|
-
udata-8.0.2.
|
|
685
|
-
udata-8.0.2.
|
|
686
|
-
udata-8.0.2.
|
|
687
|
-
udata-8.0.2.
|
|
688
|
-
udata-8.0.2.
|
|
689
|
-
udata-8.0.2.
|
|
687
|
+
udata-8.0.2.dev29284.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
|
|
688
|
+
udata-8.0.2.dev29284.dist-info/METADATA,sha256=gi8odHkhnGS9sSIVoTNtLJKRsuYqZgEHE2cBYsLF8Gw,123742
|
|
689
|
+
udata-8.0.2.dev29284.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
690
|
+
udata-8.0.2.dev29284.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
|
|
691
|
+
udata-8.0.2.dev29284.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
|
|
692
|
+
udata-8.0.2.dev29284.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|