udata 9.1.2.dev30355__py2.py3-none-any.whl → 9.1.2.dev30454__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.
- tasks/__init__.py +109 -107
- tasks/helpers.py +18 -18
- udata/__init__.py +4 -4
- udata/admin/views.py +5 -5
- udata/api/__init__.py +111 -134
- udata/api/commands.py +45 -37
- udata/api/errors.py +5 -4
- udata/api/fields.py +23 -21
- udata/api/oauth2.py +55 -74
- udata/api/parsers.py +15 -15
- udata/api/signals.py +1 -1
- udata/api_fields.py +137 -89
- udata/app.py +58 -55
- udata/assets.py +5 -5
- udata/auth/__init__.py +37 -26
- udata/auth/forms.py +23 -15
- udata/auth/helpers.py +1 -1
- udata/auth/mails.py +3 -3
- udata/auth/password_validation.py +19 -15
- udata/auth/views.py +94 -68
- udata/commands/__init__.py +71 -69
- udata/commands/cache.py +7 -7
- udata/commands/db.py +201 -140
- udata/commands/dcat.py +36 -30
- udata/commands/fixtures.py +100 -84
- udata/commands/images.py +21 -20
- udata/commands/info.py +17 -20
- udata/commands/init.py +10 -10
- udata/commands/purge.py +12 -13
- udata/commands/serve.py +41 -29
- udata/commands/static.py +16 -18
- udata/commands/test.py +20 -20
- udata/commands/tests/fixtures.py +26 -24
- udata/commands/worker.py +31 -33
- udata/core/__init__.py +12 -12
- udata/core/activity/__init__.py +0 -1
- udata/core/activity/api.py +59 -49
- udata/core/activity/models.py +28 -26
- udata/core/activity/signals.py +1 -1
- udata/core/activity/tasks.py +16 -10
- udata/core/badges/api.py +6 -6
- udata/core/badges/commands.py +14 -13
- udata/core/badges/fields.py +8 -5
- udata/core/badges/forms.py +7 -4
- udata/core/badges/models.py +16 -31
- udata/core/badges/permissions.py +1 -3
- udata/core/badges/signals.py +2 -2
- udata/core/badges/tasks.py +3 -2
- udata/core/badges/tests/test_commands.py +10 -10
- udata/core/badges/tests/test_model.py +24 -31
- udata/core/contact_point/api.py +19 -18
- udata/core/contact_point/api_fields.py +21 -14
- udata/core/contact_point/factories.py +2 -2
- udata/core/contact_point/forms.py +7 -6
- udata/core/contact_point/models.py +3 -5
- udata/core/dataservices/api.py +26 -21
- udata/core/dataservices/factories.py +13 -11
- udata/core/dataservices/models.py +35 -40
- udata/core/dataservices/permissions.py +4 -4
- udata/core/dataservices/rdf.py +40 -17
- udata/core/dataservices/tasks.py +4 -3
- udata/core/dataset/actions.py +10 -10
- udata/core/dataset/activities.py +21 -23
- udata/core/dataset/api.py +321 -298
- udata/core/dataset/api_fields.py +443 -271
- udata/core/dataset/apiv2.py +305 -229
- udata/core/dataset/commands.py +38 -36
- udata/core/dataset/constants.py +61 -54
- udata/core/dataset/csv.py +70 -74
- udata/core/dataset/events.py +39 -32
- udata/core/dataset/exceptions.py +8 -4
- udata/core/dataset/factories.py +57 -65
- udata/core/dataset/forms.py +87 -63
- udata/core/dataset/models.py +336 -280
- udata/core/dataset/permissions.py +9 -6
- udata/core/dataset/preview.py +15 -17
- udata/core/dataset/rdf.py +156 -122
- udata/core/dataset/search.py +92 -77
- udata/core/dataset/signals.py +1 -1
- udata/core/dataset/tasks.py +63 -54
- udata/core/discussions/actions.py +5 -5
- udata/core/discussions/api.py +124 -120
- udata/core/discussions/factories.py +2 -2
- udata/core/discussions/forms.py +9 -7
- udata/core/discussions/metrics.py +1 -3
- udata/core/discussions/models.py +25 -24
- udata/core/discussions/notifications.py +18 -14
- udata/core/discussions/permissions.py +3 -3
- udata/core/discussions/signals.py +4 -4
- udata/core/discussions/tasks.py +24 -28
- udata/core/followers/api.py +32 -33
- udata/core/followers/models.py +9 -9
- udata/core/followers/signals.py +3 -3
- udata/core/jobs/actions.py +7 -7
- udata/core/jobs/api.py +99 -92
- udata/core/jobs/commands.py +48 -49
- udata/core/jobs/forms.py +11 -11
- udata/core/jobs/models.py +6 -6
- udata/core/metrics/__init__.py +2 -2
- udata/core/metrics/commands.py +34 -30
- udata/core/metrics/models.py +2 -4
- udata/core/metrics/signals.py +1 -1
- udata/core/metrics/tasks.py +3 -3
- udata/core/organization/activities.py +12 -15
- udata/core/organization/api.py +167 -174
- udata/core/organization/api_fields.py +183 -124
- udata/core/organization/apiv2.py +32 -32
- udata/core/organization/commands.py +20 -22
- udata/core/organization/constants.py +11 -11
- udata/core/organization/csv.py +17 -15
- udata/core/organization/factories.py +8 -11
- udata/core/organization/forms.py +32 -26
- udata/core/organization/metrics.py +2 -1
- udata/core/organization/models.py +87 -67
- udata/core/organization/notifications.py +18 -14
- udata/core/organization/permissions.py +10 -11
- udata/core/organization/rdf.py +14 -14
- udata/core/organization/search.py +30 -28
- udata/core/organization/signals.py +7 -7
- udata/core/organization/tasks.py +42 -61
- udata/core/owned.py +38 -27
- udata/core/post/api.py +82 -81
- udata/core/post/constants.py +8 -5
- udata/core/post/factories.py +4 -4
- udata/core/post/forms.py +13 -14
- udata/core/post/models.py +20 -22
- udata/core/post/tests/test_api.py +30 -32
- udata/core/reports/api.py +8 -7
- udata/core/reports/constants.py +1 -3
- udata/core/reports/models.py +10 -10
- udata/core/reuse/activities.py +15 -19
- udata/core/reuse/api.py +123 -126
- udata/core/reuse/api_fields.py +120 -85
- udata/core/reuse/apiv2.py +11 -10
- udata/core/reuse/constants.py +23 -23
- udata/core/reuse/csv.py +18 -18
- udata/core/reuse/factories.py +5 -9
- udata/core/reuse/forms.py +24 -21
- udata/core/reuse/models.py +55 -51
- udata/core/reuse/permissions.py +2 -2
- udata/core/reuse/search.py +49 -46
- udata/core/reuse/signals.py +1 -1
- udata/core/reuse/tasks.py +4 -5
- udata/core/site/api.py +47 -50
- udata/core/site/factories.py +2 -2
- udata/core/site/forms.py +4 -5
- udata/core/site/models.py +94 -63
- udata/core/site/rdf.py +14 -14
- udata/core/spam/api.py +16 -9
- udata/core/spam/constants.py +4 -4
- udata/core/spam/fields.py +13 -7
- udata/core/spam/models.py +27 -20
- udata/core/spam/signals.py +1 -1
- udata/core/spam/tests/test_spam.py +6 -5
- udata/core/spatial/api.py +72 -80
- udata/core/spatial/api_fields.py +73 -58
- udata/core/spatial/commands.py +67 -64
- udata/core/spatial/constants.py +3 -3
- udata/core/spatial/factories.py +37 -54
- udata/core/spatial/forms.py +27 -26
- udata/core/spatial/geoids.py +17 -17
- udata/core/spatial/models.py +43 -47
- udata/core/spatial/tasks.py +2 -1
- udata/core/spatial/tests/test_api.py +115 -130
- udata/core/spatial/tests/test_fields.py +74 -77
- udata/core/spatial/tests/test_geoid.py +22 -22
- udata/core/spatial/tests/test_models.py +5 -7
- udata/core/spatial/translations.py +16 -16
- udata/core/storages/__init__.py +16 -18
- udata/core/storages/api.py +66 -64
- udata/core/storages/tasks.py +7 -7
- udata/core/storages/utils.py +15 -15
- udata/core/storages/views.py +5 -6
- udata/core/tags/api.py +17 -14
- udata/core/tags/csv.py +4 -4
- udata/core/tags/models.py +8 -5
- udata/core/tags/tasks.py +11 -13
- udata/core/tags/views.py +4 -4
- udata/core/topic/api.py +84 -73
- udata/core/topic/apiv2.py +157 -127
- udata/core/topic/factories.py +3 -4
- udata/core/topic/forms.py +12 -14
- udata/core/topic/models.py +14 -19
- udata/core/topic/parsers.py +26 -26
- udata/core/user/activities.py +30 -29
- udata/core/user/api.py +151 -152
- udata/core/user/api_fields.py +132 -100
- udata/core/user/apiv2.py +7 -7
- udata/core/user/commands.py +38 -38
- udata/core/user/factories.py +8 -9
- udata/core/user/forms.py +14 -11
- udata/core/user/metrics.py +2 -2
- udata/core/user/models.py +68 -69
- udata/core/user/permissions.py +4 -5
- udata/core/user/rdf.py +7 -8
- udata/core/user/tasks.py +2 -2
- udata/core/user/tests/test_user_model.py +24 -16
- udata/cors.py +99 -0
- udata/db/tasks.py +2 -1
- udata/entrypoints.py +35 -31
- udata/errors.py +2 -1
- udata/event/values.py +6 -6
- udata/factories.py +2 -2
- udata/features/identicon/api.py +5 -6
- udata/features/identicon/backends.py +48 -55
- udata/features/identicon/tests/test_backends.py +4 -5
- udata/features/notifications/__init__.py +0 -1
- udata/features/notifications/actions.py +9 -9
- udata/features/notifications/api.py +17 -13
- udata/features/territories/__init__.py +12 -10
- udata/features/territories/api.py +14 -15
- udata/features/territories/models.py +23 -28
- udata/features/transfer/actions.py +8 -11
- udata/features/transfer/api.py +84 -77
- udata/features/transfer/factories.py +2 -1
- udata/features/transfer/models.py +11 -12
- udata/features/transfer/notifications.py +19 -15
- udata/features/transfer/permissions.py +5 -5
- udata/forms/__init__.py +5 -2
- udata/forms/fields.py +164 -172
- udata/forms/validators.py +19 -22
- udata/forms/widgets.py +9 -13
- udata/frontend/__init__.py +31 -26
- udata/frontend/csv.py +68 -58
- udata/frontend/markdown.py +40 -44
- udata/harvest/actions.py +89 -77
- udata/harvest/api.py +294 -238
- udata/harvest/backends/__init__.py +4 -4
- udata/harvest/backends/base.py +128 -111
- udata/harvest/backends/dcat.py +80 -66
- udata/harvest/commands.py +56 -60
- udata/harvest/csv.py +8 -8
- udata/harvest/exceptions.py +6 -3
- udata/harvest/filters.py +24 -23
- udata/harvest/forms.py +27 -28
- udata/harvest/models.py +88 -80
- udata/harvest/notifications.py +15 -10
- udata/harvest/signals.py +13 -13
- udata/harvest/tasks.py +11 -10
- udata/harvest/tests/factories.py +23 -24
- udata/harvest/tests/test_actions.py +136 -166
- udata/harvest/tests/test_api.py +220 -214
- udata/harvest/tests/test_base_backend.py +117 -112
- udata/harvest/tests/test_dcat_backend.py +380 -308
- udata/harvest/tests/test_filters.py +33 -22
- udata/harvest/tests/test_models.py +11 -14
- udata/harvest/tests/test_notifications.py +6 -7
- udata/harvest/tests/test_tasks.py +7 -6
- udata/i18n.py +237 -78
- udata/linkchecker/backends.py +5 -11
- udata/linkchecker/checker.py +23 -22
- udata/linkchecker/commands.py +4 -6
- udata/linkchecker/models.py +6 -6
- udata/linkchecker/tasks.py +18 -20
- udata/mail.py +21 -21
- udata/migrations/2020-07-24-remove-s-from-scope-oauth.py +9 -8
- udata/migrations/2020-08-24-add-fs-filename.py +9 -8
- udata/migrations/2020-09-28-update-reuses-datasets-metrics.py +5 -4
- udata/migrations/2020-10-16-migrate-ods-resources.py +9 -10
- udata/migrations/2021-04-08-update-schema-with-new-structure.py +8 -7
- udata/migrations/2021-05-27-fix-default-schema-name.py +7 -6
- udata/migrations/2021-07-05-remove-unused-badges.py +17 -15
- udata/migrations/2021-07-07-update-schema-for-community-resources.py +7 -6
- udata/migrations/2021-08-17-follow-integrity.py +5 -4
- udata/migrations/2021-08-17-harvest-integrity.py +13 -12
- udata/migrations/2021-08-17-oauth2client-integrity.py +5 -4
- udata/migrations/2021-08-17-transfer-integrity.py +5 -4
- udata/migrations/2021-08-17-users-integrity.py +9 -8
- udata/migrations/2021-12-14-reuse-topics.py +7 -6
- udata/migrations/2022-04-21-improve-extension-detection.py +8 -7
- udata/migrations/2022-09-22-clean-inactive-harvest-datasets.py +16 -14
- udata/migrations/2022-10-10-add-fs_uniquifier-to-user-model.py +6 -6
- udata/migrations/2022-10-10-migrate-harvest-extras.py +36 -26
- udata/migrations/2023-02-08-rename-internal-dates.py +46 -28
- udata/migrations/2024-01-29-fix-reuse-and-dataset-with-private-None.py +10 -8
- udata/migrations/2024-03-22-migrate-activity-kwargs-to-extras.py +6 -4
- udata/migrations/2024-06-11-fix-reuse-datasets-references.py +7 -6
- udata/migrations/__init__.py +123 -105
- udata/models/__init__.py +4 -4
- udata/mongo/__init__.py +13 -11
- udata/mongo/badges_field.py +3 -2
- udata/mongo/datetime_fields.py +13 -12
- udata/mongo/document.py +17 -16
- udata/mongo/engine.py +15 -16
- udata/mongo/errors.py +2 -1
- udata/mongo/extras_fields.py +30 -20
- udata/mongo/queryset.py +12 -12
- udata/mongo/slug_fields.py +38 -28
- udata/mongo/taglist_field.py +1 -2
- udata/mongo/url_field.py +5 -5
- udata/mongo/uuid_fields.py +4 -3
- udata/notifications/__init__.py +1 -1
- udata/notifications/mattermost.py +10 -9
- udata/rdf.py +167 -188
- udata/routing.py +40 -45
- udata/search/__init__.py +18 -19
- udata/search/adapter.py +17 -16
- udata/search/commands.py +44 -51
- udata/search/fields.py +13 -20
- udata/search/query.py +23 -18
- udata/search/result.py +9 -10
- udata/sentry.py +21 -19
- udata/settings.py +262 -198
- udata/sitemap.py +8 -6
- udata/storage/s3.py +20 -13
- udata/tags.py +4 -5
- udata/tasks.py +43 -42
- udata/tests/__init__.py +9 -6
- udata/tests/api/__init__.py +8 -6
- udata/tests/api/test_auth_api.py +395 -321
- udata/tests/api/test_base_api.py +33 -35
- udata/tests/api/test_contact_points.py +7 -9
- udata/tests/api/test_dataservices_api.py +211 -158
- udata/tests/api/test_datasets_api.py +823 -812
- udata/tests/api/test_follow_api.py +13 -15
- udata/tests/api/test_me_api.py +95 -112
- udata/tests/api/test_organizations_api.py +301 -339
- udata/tests/api/test_reports_api.py +35 -25
- udata/tests/api/test_reuses_api.py +134 -139
- udata/tests/api/test_swagger.py +5 -5
- udata/tests/api/test_tags_api.py +18 -25
- udata/tests/api/test_topics_api.py +94 -94
- udata/tests/api/test_transfer_api.py +53 -48
- udata/tests/api/test_user_api.py +128 -141
- udata/tests/apiv2/test_datasets.py +290 -198
- udata/tests/apiv2/test_me_api.py +10 -11
- udata/tests/apiv2/test_organizations.py +56 -74
- udata/tests/apiv2/test_swagger.py +5 -5
- udata/tests/apiv2/test_topics.py +69 -87
- udata/tests/cli/test_cli_base.py +8 -8
- udata/tests/cli/test_db_cli.py +21 -19
- udata/tests/dataservice/test_dataservice_tasks.py +8 -12
- udata/tests/dataset/test_csv_adapter.py +44 -35
- udata/tests/dataset/test_dataset_actions.py +2 -3
- udata/tests/dataset/test_dataset_commands.py +7 -8
- udata/tests/dataset/test_dataset_events.py +36 -29
- udata/tests/dataset/test_dataset_model.py +224 -217
- udata/tests/dataset/test_dataset_rdf.py +142 -131
- udata/tests/dataset/test_dataset_tasks.py +15 -15
- udata/tests/dataset/test_resource_preview.py +10 -13
- udata/tests/features/territories/__init__.py +9 -13
- udata/tests/features/territories/test_territories_api.py +71 -91
- udata/tests/forms/test_basic_fields.py +7 -7
- udata/tests/forms/test_current_user_field.py +39 -66
- udata/tests/forms/test_daterange_field.py +31 -39
- udata/tests/forms/test_dict_field.py +28 -26
- udata/tests/forms/test_extras_fields.py +102 -76
- udata/tests/forms/test_form_field.py +8 -8
- udata/tests/forms/test_image_field.py +33 -26
- udata/tests/forms/test_model_field.py +134 -123
- udata/tests/forms/test_model_list_field.py +7 -7
- udata/tests/forms/test_nested_model_list_field.py +117 -79
- udata/tests/forms/test_publish_as_field.py +36 -65
- udata/tests/forms/test_reference_field.py +34 -53
- udata/tests/forms/test_user_forms.py +23 -21
- udata/tests/forms/test_uuid_field.py +6 -10
- udata/tests/frontend/__init__.py +9 -6
- udata/tests/frontend/test_auth.py +7 -6
- udata/tests/frontend/test_csv.py +81 -96
- udata/tests/frontend/test_hooks.py +43 -43
- udata/tests/frontend/test_markdown.py +211 -191
- udata/tests/helpers.py +32 -37
- udata/tests/models.py +2 -2
- udata/tests/organization/test_csv_adapter.py +21 -16
- udata/tests/organization/test_notifications.py +11 -18
- udata/tests/organization/test_organization_model.py +13 -13
- udata/tests/organization/test_organization_rdf.py +29 -22
- udata/tests/organization/test_organization_tasks.py +16 -17
- udata/tests/plugin.py +79 -73
- udata/tests/reuse/test_reuse_model.py +21 -21
- udata/tests/reuse/test_reuse_task.py +11 -13
- udata/tests/search/__init__.py +11 -12
- udata/tests/search/test_adapter.py +60 -70
- udata/tests/search/test_query.py +16 -16
- udata/tests/search/test_results.py +10 -7
- udata/tests/site/test_site_api.py +11 -16
- udata/tests/site/test_site_metrics.py +20 -30
- udata/tests/site/test_site_model.py +4 -5
- udata/tests/site/test_site_rdf.py +94 -78
- udata/tests/test_activity.py +17 -17
- udata/tests/test_cors.py +62 -0
- udata/tests/test_discussions.py +292 -299
- udata/tests/test_i18n.py +37 -40
- udata/tests/test_linkchecker.py +91 -85
- udata/tests/test_mail.py +13 -17
- udata/tests/test_migrations.py +219 -180
- udata/tests/test_model.py +164 -157
- udata/tests/test_notifications.py +17 -17
- udata/tests/test_owned.py +14 -14
- udata/tests/test_rdf.py +25 -23
- udata/tests/test_routing.py +89 -93
- udata/tests/test_storages.py +137 -128
- udata/tests/test_tags.py +44 -46
- udata/tests/test_topics.py +7 -7
- udata/tests/test_transfer.py +42 -49
- udata/tests/test_uris.py +160 -161
- udata/tests/test_utils.py +79 -71
- udata/tests/user/test_user_rdf.py +5 -9
- udata/tests/workers/test_jobs_commands.py +57 -58
- udata/tests/workers/test_tasks_routing.py +23 -29
- udata/tests/workers/test_workers_api.py +125 -131
- udata/tests/workers/test_workers_helpers.py +6 -6
- udata/tracking.py +4 -6
- udata/uris.py +45 -46
- udata/utils.py +68 -66
- udata/wsgi.py +1 -1
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/METADATA +7 -3
- udata-9.1.2.dev30454.dist-info/RECORD +706 -0
- udata-9.1.2.dev30355.dist-info/RECORD +0 -704
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/LICENSE +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/WHEEL +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/entry_points.txt +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/top_level.txt +0 -0
udata/core/dataset/api.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
TODO: We need to cleanup and give more coherence to these APIs
|
|
3
3
|
- upload enpoints should be singular instead of plural
|
|
4
4
|
- community resource APIs should be consistent
|
|
@@ -15,34 +15,34 @@ TODO: We need to cleanup and give more coherence to these APIs
|
|
|
15
15
|
These changes might lead to backward compatibility breakage meaning:
|
|
16
16
|
- new API version
|
|
17
17
|
- admin changes
|
|
18
|
-
|
|
18
|
+
"""
|
|
19
19
|
|
|
20
|
-
import os
|
|
21
20
|
import logging
|
|
22
|
-
import
|
|
21
|
+
import os
|
|
23
22
|
from datetime import datetime
|
|
24
23
|
|
|
24
|
+
import mongoengine
|
|
25
25
|
from bson.objectid import ObjectId
|
|
26
|
-
from flask import
|
|
26
|
+
from flask import abort, current_app, make_response, redirect, request, url_for
|
|
27
27
|
from flask_security import current_user
|
|
28
28
|
from mongoengine.queryset.visitor import Q
|
|
29
29
|
|
|
30
|
-
from udata.
|
|
31
|
-
from udata.api import api, API, errors
|
|
30
|
+
from udata.api import API, api, errors
|
|
32
31
|
from udata.api.parsers import ModelApiParser
|
|
32
|
+
from udata.auth import admin_permission
|
|
33
33
|
from udata.core import storages
|
|
34
|
-
from udata.core.dataset.models import CHECKSUM_TYPES
|
|
35
|
-
from udata.core.storages.api import handle_upload, upload_parser
|
|
36
34
|
from udata.core.badges import api as badges_api
|
|
37
35
|
from udata.core.badges.fields import badge_fields
|
|
36
|
+
from udata.core.dataset.models import CHECKSUM_TYPES
|
|
38
37
|
from udata.core.followers.api import FollowAPI
|
|
38
|
+
from udata.core.storages.api import handle_upload, upload_parser
|
|
39
|
+
from udata.core.topic.models import Topic
|
|
40
|
+
from udata.linkchecker.checker import check_resource
|
|
41
|
+
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
39
42
|
from udata.utils import get_by
|
|
40
|
-
from udata.rdf import (
|
|
41
|
-
RDF_EXTENSIONS,
|
|
42
|
-
negociate_content, graph_response
|
|
43
|
-
)
|
|
44
43
|
|
|
45
44
|
from .api_fields import (
|
|
45
|
+
catalog_schema_fields,
|
|
46
46
|
community_resource_fields,
|
|
47
47
|
community_resource_page_fields,
|
|
48
48
|
dataset_fields,
|
|
@@ -53,95 +53,97 @@ from .api_fields import (
|
|
|
53
53
|
resource_fields,
|
|
54
54
|
resource_type_fields,
|
|
55
55
|
upload_fields,
|
|
56
|
-
catalog_schema_fields,
|
|
57
56
|
)
|
|
58
|
-
from
|
|
59
|
-
from
|
|
57
|
+
from .constants import RESOURCE_TYPES, UPDATE_FREQUENCIES
|
|
58
|
+
from .exceptions import (
|
|
59
|
+
SchemasCacheUnavailableException,
|
|
60
|
+
SchemasCatalogNotFoundException,
|
|
61
|
+
)
|
|
62
|
+
from .forms import CommunityResourceForm, DatasetForm, ResourceForm, ResourcesListForm
|
|
60
63
|
from .models import (
|
|
61
|
-
|
|
62
|
-
CommunityResource,
|
|
64
|
+
Checksum,
|
|
65
|
+
CommunityResource,
|
|
66
|
+
Dataset,
|
|
67
|
+
License,
|
|
68
|
+
Resource,
|
|
69
|
+
ResourceSchema,
|
|
70
|
+
get_resource,
|
|
63
71
|
)
|
|
64
|
-
from .constants import UPDATE_FREQUENCIES, RESOURCE_TYPES
|
|
65
72
|
from .permissions import DatasetEditPermission, ResourceEditPermission
|
|
66
|
-
from .forms import (
|
|
67
|
-
ResourceForm, DatasetForm, CommunityResourceForm, ResourcesListForm
|
|
68
|
-
)
|
|
69
|
-
from .exceptions import (
|
|
70
|
-
SchemasCatalogNotFoundException, SchemasCacheUnavailableException
|
|
71
|
-
)
|
|
72
73
|
from .rdf import dataset_to_rdf
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
SUGGEST_SORTING = '-metrics.followers'
|
|
75
|
+
DEFAULT_SORTING = "-created_at_internal"
|
|
76
|
+
SUGGEST_SORTING = "-metrics.followers"
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
class DatasetApiParser(ModelApiParser):
|
|
80
80
|
sorts = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
"title": "title",
|
|
82
|
+
"created": "created_at_internal",
|
|
83
|
+
"last_update": "last_modified_internal",
|
|
84
|
+
"reuses": "metrics.reuses",
|
|
85
|
+
"followers": "metrics.followers",
|
|
86
|
+
"views": "metrics.views",
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
def __init__(self):
|
|
90
90
|
super().__init__()
|
|
91
|
-
self.parser.add_argument(
|
|
92
|
-
self.parser.add_argument(
|
|
93
|
-
self.parser.add_argument(
|
|
94
|
-
self.parser.add_argument(
|
|
95
|
-
self.parser.add_argument(
|
|
96
|
-
self.parser.add_argument(
|
|
97
|
-
self.parser.add_argument(
|
|
98
|
-
self.parser.add_argument(
|
|
99
|
-
self.parser.add_argument(
|
|
100
|
-
self.parser.add_argument(
|
|
101
|
-
self.parser.add_argument(
|
|
102
|
-
self.parser.add_argument(
|
|
91
|
+
self.parser.add_argument("tag", type=str, location="args")
|
|
92
|
+
self.parser.add_argument("license", type=str, location="args")
|
|
93
|
+
self.parser.add_argument("featured", type=bool, location="args")
|
|
94
|
+
self.parser.add_argument("geozone", type=str, location="args")
|
|
95
|
+
self.parser.add_argument("granularity", type=str, location="args")
|
|
96
|
+
self.parser.add_argument("temporal_coverage", type=str, location="args")
|
|
97
|
+
self.parser.add_argument("organization", type=str, location="args")
|
|
98
|
+
self.parser.add_argument("owner", type=str, location="args")
|
|
99
|
+
self.parser.add_argument("format", type=str, location="args")
|
|
100
|
+
self.parser.add_argument("schema", type=str, location="args")
|
|
101
|
+
self.parser.add_argument("schema_version", type=str, location="args")
|
|
102
|
+
self.parser.add_argument("topic", type=str, location="args")
|
|
103
103
|
|
|
104
104
|
@staticmethod
|
|
105
105
|
def parse_filters(datasets, args):
|
|
106
|
-
if args.get(
|
|
106
|
+
if args.get("q"):
|
|
107
107
|
# Following code splits the 'q' argument by spaces to surround
|
|
108
108
|
# every word in it with quotes before rebuild it.
|
|
109
109
|
# This allows the search_text method to tokenise with an AND
|
|
110
110
|
# between tokens whereas an OR is used without it.
|
|
111
|
-
phrase_query =
|
|
111
|
+
phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
112
112
|
datasets = datasets.search_text(phrase_query)
|
|
113
|
-
if args.get(
|
|
114
|
-
datasets = datasets.filter(tags=args[
|
|
115
|
-
if args.get(
|
|
116
|
-
datasets = datasets.filter(license__in=License.objects.filter(id=args[
|
|
117
|
-
if args.get(
|
|
118
|
-
datasets = datasets.filter(spatial__zones=args[
|
|
119
|
-
if args.get(
|
|
120
|
-
datasets = datasets.filter(spatial__granularity=args[
|
|
121
|
-
if args.get(
|
|
122
|
-
datasets = datasets.filter(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if args.get(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
datasets = datasets.filter(
|
|
136
|
-
if args.get(
|
|
137
|
-
datasets = datasets.filter(
|
|
138
|
-
if args.get(
|
|
139
|
-
datasets = datasets.filter(
|
|
140
|
-
if args.get(
|
|
141
|
-
|
|
142
|
-
|
|
113
|
+
if args.get("tag"):
|
|
114
|
+
datasets = datasets.filter(tags=args["tag"])
|
|
115
|
+
if args.get("license"):
|
|
116
|
+
datasets = datasets.filter(license__in=License.objects.filter(id=args["license"]))
|
|
117
|
+
if args.get("geozone"):
|
|
118
|
+
datasets = datasets.filter(spatial__zones=args["geozone"])
|
|
119
|
+
if args.get("granularity"):
|
|
120
|
+
datasets = datasets.filter(spatial__granularity=args["granularity"])
|
|
121
|
+
if args.get("temporal_coverage"):
|
|
122
|
+
datasets = datasets.filter(
|
|
123
|
+
temporal_coverage__start__gte=args["temporal_coverage"][:9],
|
|
124
|
+
temporal_coverage__start__lte=args["temporal_coverage"][11:],
|
|
125
|
+
)
|
|
126
|
+
if args.get("featured"):
|
|
127
|
+
datasets = datasets.filter(featured=args["featured"])
|
|
128
|
+
if args.get("organization"):
|
|
129
|
+
if not ObjectId.is_valid(args["organization"]):
|
|
130
|
+
api.abort(400, "Organization arg must be an identifier")
|
|
131
|
+
datasets = datasets.filter(organization=args["organization"])
|
|
132
|
+
if args.get("owner"):
|
|
133
|
+
if not ObjectId.is_valid(args["owner"]):
|
|
134
|
+
api.abort(400, "Owner arg must be an identifier")
|
|
135
|
+
datasets = datasets.filter(owner=args["owner"])
|
|
136
|
+
if args.get("format"):
|
|
137
|
+
datasets = datasets.filter(resources__format=args["format"])
|
|
138
|
+
if args.get("schema"):
|
|
139
|
+
datasets = datasets.filter(resources__schema__name=args["schema"])
|
|
140
|
+
if args.get("schema_version"):
|
|
141
|
+
datasets = datasets.filter(resources__schema__version=args["schema_version"])
|
|
142
|
+
if args.get("topic"):
|
|
143
|
+
if not ObjectId.is_valid(args["topic"]):
|
|
144
|
+
api.abort(400, "Topic arg must be an identifier")
|
|
143
145
|
try:
|
|
144
|
-
topic = Topic.objects.get(id=args[
|
|
146
|
+
topic = Topic.objects.get(id=args["topic"])
|
|
145
147
|
except Topic.DoesNotExist:
|
|
146
148
|
pass
|
|
147
149
|
else:
|
|
@@ -151,84 +153,84 @@ class DatasetApiParser(ModelApiParser):
|
|
|
151
153
|
|
|
152
154
|
log = logging.getLogger(__name__)
|
|
153
155
|
|
|
154
|
-
ns = api.namespace(
|
|
156
|
+
ns = api.namespace("datasets", "Dataset related operations")
|
|
155
157
|
|
|
156
158
|
dataset_parser = DatasetApiParser()
|
|
157
159
|
|
|
158
160
|
community_parser = api.parser()
|
|
159
161
|
community_parser.add_argument(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
"sort", type=str, default="-created_at_internal", location="args", help="The sorting attribute"
|
|
163
|
+
)
|
|
162
164
|
community_parser.add_argument(
|
|
163
|
-
|
|
165
|
+
"page", type=int, default=1, location="args", help="The page to fetch"
|
|
166
|
+
)
|
|
164
167
|
community_parser.add_argument(
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
"page_size", type=int, default=20, location="args", help="The page size to fetch"
|
|
169
|
+
)
|
|
167
170
|
community_parser.add_argument(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
"organization",
|
|
172
|
+
type=str,
|
|
173
|
+
help="Filter activities for that particular organization",
|
|
174
|
+
location="args",
|
|
175
|
+
)
|
|
171
176
|
community_parser.add_argument(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
location='args')
|
|
177
|
+
"dataset", type=str, help="Filter activities for that particular dataset", location="args"
|
|
178
|
+
)
|
|
175
179
|
community_parser.add_argument(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
location='args')
|
|
180
|
+
"owner", type=str, help="Filter activities for that particular user", location="args"
|
|
181
|
+
)
|
|
179
182
|
|
|
180
|
-
common_doc = {
|
|
181
|
-
'params': {'dataset': 'The dataset ID or slug'}
|
|
182
|
-
}
|
|
183
|
+
common_doc = {"params": {"dataset": "The dataset ID or slug"}}
|
|
183
184
|
|
|
184
185
|
|
|
185
|
-
@ns.route(
|
|
186
|
+
@ns.route("/", endpoint="datasets")
|
|
186
187
|
class DatasetListAPI(API):
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
"""Datasets collection endpoint"""
|
|
189
|
+
|
|
190
|
+
@api.doc("list_datasets")
|
|
189
191
|
@api.expect(dataset_parser.parser)
|
|
190
192
|
@api.marshal_with(dataset_page_fields)
|
|
191
193
|
def get(self):
|
|
192
|
-
|
|
194
|
+
"""List or search all datasets"""
|
|
193
195
|
args = dataset_parser.parse()
|
|
194
196
|
datasets = Dataset.objects(archived=None, deleted=None, private=False)
|
|
195
197
|
datasets = dataset_parser.parse_filters(datasets, args)
|
|
196
|
-
sort = args[
|
|
197
|
-
return datasets.order_by(sort).paginate(args[
|
|
198
|
+
sort = args["sort"] or ("$text_score" if args["q"] else None) or DEFAULT_SORTING
|
|
199
|
+
return datasets.order_by(sort).paginate(args["page"], args["page_size"])
|
|
198
200
|
|
|
199
201
|
@api.secure
|
|
200
|
-
@api.doc(
|
|
202
|
+
@api.doc("create_dataset", responses={400: "Validation error"})
|
|
201
203
|
@api.expect(dataset_fields)
|
|
202
204
|
@api.marshal_with(dataset_fields, code=201)
|
|
203
205
|
def post(self):
|
|
204
|
-
|
|
206
|
+
"""Create a new dataset"""
|
|
205
207
|
form = api.validate(DatasetForm)
|
|
206
208
|
dataset = form.save()
|
|
207
209
|
return dataset, 201
|
|
208
210
|
|
|
209
211
|
|
|
210
|
-
@ns.route(
|
|
211
|
-
@api.response(404,
|
|
212
|
-
@api.response(410,
|
|
212
|
+
@ns.route("/<dataset:dataset>/", endpoint="dataset", doc=common_doc)
|
|
213
|
+
@api.response(404, "Dataset not found")
|
|
214
|
+
@api.response(410, "Dataset has been deleted")
|
|
213
215
|
class DatasetAPI(API):
|
|
214
|
-
@api.doc(
|
|
216
|
+
@api.doc("get_dataset")
|
|
215
217
|
@api.marshal_with(dataset_fields)
|
|
216
218
|
def get(self, dataset):
|
|
217
|
-
|
|
219
|
+
"""Get a dataset given its identifier"""
|
|
218
220
|
if dataset.deleted and not DatasetEditPermission(dataset).can():
|
|
219
|
-
api.abort(410,
|
|
221
|
+
api.abort(410, "Dataset has been deleted")
|
|
220
222
|
return dataset
|
|
221
223
|
|
|
222
224
|
@api.secure
|
|
223
|
-
@api.doc(
|
|
225
|
+
@api.doc("update_dataset")
|
|
224
226
|
@api.expect(dataset_fields)
|
|
225
227
|
@api.marshal_with(dataset_fields)
|
|
226
228
|
@api.response(400, errors.VALIDATION_ERROR)
|
|
227
229
|
def put(self, dataset):
|
|
228
|
-
|
|
229
|
-
request_deleted = request.json.get(
|
|
230
|
+
"""Update a dataset given its identifier"""
|
|
231
|
+
request_deleted = request.json.get("deleted", True)
|
|
230
232
|
if dataset.deleted and request_deleted is not None:
|
|
231
|
-
api.abort(410,
|
|
233
|
+
api.abort(410, "Dataset has been deleted")
|
|
232
234
|
DatasetEditPermission(dataset).test()
|
|
233
235
|
dataset.last_modified_internal = datetime.utcnow()
|
|
234
236
|
form = api.validate(DatasetForm, dataset)
|
|
@@ -240,57 +242,57 @@ class DatasetAPI(API):
|
|
|
240
242
|
api.abort(400, e.message)
|
|
241
243
|
|
|
242
244
|
@api.secure
|
|
243
|
-
@api.doc(
|
|
244
|
-
@api.response(204,
|
|
245
|
+
@api.doc("delete_dataset")
|
|
246
|
+
@api.response(204, "Dataset deleted")
|
|
245
247
|
def delete(self, dataset):
|
|
246
|
-
|
|
248
|
+
"""Delete a dataset given its identifier"""
|
|
247
249
|
if dataset.deleted:
|
|
248
|
-
api.abort(410,
|
|
250
|
+
api.abort(410, "Dataset has been deleted")
|
|
249
251
|
DatasetEditPermission(dataset).test()
|
|
250
252
|
dataset.deleted = datetime.utcnow()
|
|
251
253
|
dataset.last_modified_internal = datetime.utcnow()
|
|
252
254
|
dataset.save()
|
|
253
|
-
return
|
|
255
|
+
return "", 204
|
|
254
256
|
|
|
255
257
|
|
|
256
|
-
@ns.route(
|
|
258
|
+
@ns.route("/<dataset:dataset>/featured/", endpoint="dataset_featured")
|
|
257
259
|
@api.doc(**common_doc)
|
|
258
260
|
class DatasetFeaturedAPI(API):
|
|
259
261
|
@api.secure(admin_permission)
|
|
260
|
-
@api.doc(
|
|
262
|
+
@api.doc("feature_dataset")
|
|
261
263
|
@api.marshal_with(dataset_fields)
|
|
262
264
|
def post(self, dataset):
|
|
263
|
-
|
|
265
|
+
"""Mark the dataset as featured"""
|
|
264
266
|
dataset.featured = True
|
|
265
267
|
dataset.save()
|
|
266
268
|
return dataset
|
|
267
269
|
|
|
268
270
|
@api.secure(admin_permission)
|
|
269
|
-
@api.doc(
|
|
271
|
+
@api.doc("unfeature_dataset")
|
|
270
272
|
@api.marshal_with(dataset_fields)
|
|
271
273
|
def delete(self, dataset):
|
|
272
|
-
|
|
274
|
+
"""Unmark the dataset as featured"""
|
|
273
275
|
dataset.featured = False
|
|
274
276
|
dataset.save()
|
|
275
277
|
return dataset
|
|
276
278
|
|
|
277
279
|
|
|
278
|
-
@ns.route(
|
|
279
|
-
@api.response(404,
|
|
280
|
-
@api.response(410,
|
|
280
|
+
@ns.route("/<dataset:dataset>/rdf", endpoint="dataset_rdf", doc=common_doc)
|
|
281
|
+
@api.response(404, "Dataset not found")
|
|
282
|
+
@api.response(410, "Dataset has been deleted")
|
|
281
283
|
class DatasetRdfAPI(API):
|
|
282
|
-
@api.doc(
|
|
284
|
+
@api.doc("rdf_dataset")
|
|
283
285
|
def get(self, dataset):
|
|
284
286
|
format = RDF_EXTENSIONS[negociate_content()]
|
|
285
|
-
url = url_for(
|
|
287
|
+
url = url_for("api.dataset_rdf_format", dataset=dataset.id, format=format)
|
|
286
288
|
return redirect(url)
|
|
287
289
|
|
|
288
290
|
|
|
289
|
-
@ns.route(
|
|
290
|
-
@api.response(404,
|
|
291
|
-
@api.response(410,
|
|
291
|
+
@ns.route("/<dataset:dataset>/rdf.<format>", endpoint="dataset_rdf_format", doc=common_doc)
|
|
292
|
+
@api.response(404, "Dataset not found")
|
|
293
|
+
@api.response(410, "Dataset has been deleted")
|
|
292
294
|
class DatasetRdfFormatAPI(API):
|
|
293
|
-
@api.doc(
|
|
295
|
+
@api.doc("rdf_dataset_format")
|
|
294
296
|
def get(self, dataset, format):
|
|
295
297
|
if not DatasetEditPermission(dataset).can():
|
|
296
298
|
if dataset.private:
|
|
@@ -304,58 +306,58 @@ class DatasetRdfFormatAPI(API):
|
|
|
304
306
|
return make_response(*graph_response(resource, format))
|
|
305
307
|
|
|
306
308
|
|
|
307
|
-
@ns.route(
|
|
309
|
+
@ns.route("/badges/", endpoint="available_dataset_badges")
|
|
308
310
|
class AvailableDatasetBadgesAPI(API):
|
|
309
|
-
@api.doc(
|
|
311
|
+
@api.doc("available_dataset_badges")
|
|
310
312
|
def get(self):
|
|
311
|
-
|
|
313
|
+
"""List all available dataset badges and their labels"""
|
|
312
314
|
return Dataset.__badges__
|
|
313
315
|
|
|
314
316
|
|
|
315
|
-
@ns.route(
|
|
317
|
+
@ns.route("/<dataset:dataset>/badges/", endpoint="dataset_badges")
|
|
316
318
|
class DatasetBadgesAPI(API):
|
|
317
|
-
@api.doc(
|
|
319
|
+
@api.doc("add_dataset_badge", **common_doc)
|
|
318
320
|
@api.expect(badge_fields)
|
|
319
321
|
@api.marshal_with(badge_fields)
|
|
320
322
|
@api.secure(admin_permission)
|
|
321
323
|
def post(self, dataset):
|
|
322
|
-
|
|
324
|
+
"""Create a new badge for a given dataset"""
|
|
323
325
|
return badges_api.add(dataset)
|
|
324
326
|
|
|
325
327
|
|
|
326
|
-
@ns.route(
|
|
328
|
+
@ns.route("/<dataset:dataset>/badges/<badge_kind>/", endpoint="dataset_badge")
|
|
327
329
|
class DatasetBadgeAPI(API):
|
|
328
|
-
@api.doc(
|
|
330
|
+
@api.doc("delete_dataset_badge", **common_doc)
|
|
329
331
|
@api.secure(admin_permission)
|
|
330
332
|
def delete(self, dataset, badge_kind):
|
|
331
|
-
|
|
333
|
+
"""Delete a badge for a given dataset"""
|
|
332
334
|
return badges_api.remove(dataset, badge_kind)
|
|
333
335
|
|
|
334
336
|
|
|
335
|
-
@ns.route(
|
|
337
|
+
@ns.route("/r/<uuid:id>", endpoint="resource_redirect")
|
|
336
338
|
class ResourceRedirectAPI(API):
|
|
337
|
-
@api.doc(
|
|
339
|
+
@api.doc("redirect_resource", **common_doc)
|
|
338
340
|
def get(self, id):
|
|
339
|
-
|
|
341
|
+
"""
|
|
340
342
|
Redirect to the latest version of a resource given its identifier.
|
|
341
|
-
|
|
343
|
+
"""
|
|
342
344
|
resource = get_resource(id)
|
|
343
345
|
return redirect(resource.url.strip()) if resource else abort(404)
|
|
344
346
|
|
|
345
347
|
|
|
346
|
-
@ns.route(
|
|
348
|
+
@ns.route("/<dataset:dataset>/resources/", endpoint="resources")
|
|
347
349
|
class ResourcesAPI(API):
|
|
348
350
|
@api.secure
|
|
349
|
-
@api.doc(
|
|
351
|
+
@api.doc("create_resource", **common_doc, responses={400: "Validation error"})
|
|
350
352
|
@api.expect(resource_fields)
|
|
351
353
|
@api.marshal_with(resource_fields, code=201)
|
|
352
354
|
def post(self, dataset):
|
|
353
|
-
|
|
355
|
+
"""Create a new resource for a given dataset"""
|
|
354
356
|
ResourceEditPermission(dataset).test()
|
|
355
357
|
form = api.validate(ResourceForm)
|
|
356
358
|
resource = Resource()
|
|
357
|
-
if form._fields.get(
|
|
358
|
-
api.abort(400,
|
|
359
|
+
if form._fields.get("filetype").data != "remote":
|
|
360
|
+
api.abort(400, "This endpoint only supports remote resources")
|
|
359
361
|
form.populate_obj(resource)
|
|
360
362
|
dataset.add_resource(resource)
|
|
361
363
|
dataset.last_modified_internal = datetime.utcnow()
|
|
@@ -363,17 +365,18 @@ class ResourcesAPI(API):
|
|
|
363
365
|
return resource, 201
|
|
364
366
|
|
|
365
367
|
@api.secure
|
|
366
|
-
@api.doc(
|
|
368
|
+
@api.doc("update_resources", **common_doc, responses={400: "Validation error"})
|
|
367
369
|
@api.expect([resource_fields])
|
|
368
370
|
@api.marshal_list_with(resource_fields)
|
|
369
371
|
def put(self, dataset):
|
|
370
|
-
|
|
372
|
+
"""Reorder resources"""
|
|
371
373
|
ResourceEditPermission(dataset).test()
|
|
372
|
-
data = {
|
|
373
|
-
form = ResourcesListForm.from_json(
|
|
374
|
-
|
|
374
|
+
data = {"resources": request.json}
|
|
375
|
+
form = ResourcesListForm.from_json(
|
|
376
|
+
data, obj=dataset, instance=dataset, meta={"csrf": False}
|
|
377
|
+
)
|
|
375
378
|
if not form.validate():
|
|
376
|
-
api.abort(400, errors=form.errors[
|
|
379
|
+
api.abort(400, errors=form.errors["resources"])
|
|
377
380
|
|
|
378
381
|
dataset = form.save()
|
|
379
382
|
return dataset.resources, 200
|
|
@@ -381,29 +384,32 @@ class ResourcesAPI(API):
|
|
|
381
384
|
|
|
382
385
|
class UploadMixin(object):
|
|
383
386
|
def handle_upload(self, dataset):
|
|
384
|
-
prefix =
|
|
385
|
-
datetime.utcnow().strftime('%Y%m%d-%H%M%S')))
|
|
387
|
+
prefix = "/".join((dataset.slug, datetime.utcnow().strftime("%Y%m%d-%H%M%S")))
|
|
386
388
|
infos = handle_upload(storages.resources, prefix)
|
|
387
|
-
if
|
|
388
|
-
api.abort(415,
|
|
389
|
-
infos[
|
|
390
|
-
checksum_type = next(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
infos[
|
|
394
|
-
|
|
389
|
+
if "html" in infos["mime"]:
|
|
390
|
+
api.abort(415, "Incorrect file content type: HTML")
|
|
391
|
+
infos["title"] = os.path.basename(infos["filename"])
|
|
392
|
+
checksum_type = next(
|
|
393
|
+
checksum_type for checksum_type in CHECKSUM_TYPES if checksum_type in infos
|
|
394
|
+
)
|
|
395
|
+
infos["checksum"] = Checksum(type=checksum_type, value=infos.pop(checksum_type))
|
|
396
|
+
infos["filesize"] = infos.pop("size")
|
|
397
|
+
del infos["filename"]
|
|
395
398
|
return infos
|
|
396
399
|
|
|
397
400
|
|
|
398
|
-
@ns.route(
|
|
401
|
+
@ns.route("/<dataset:dataset>/upload/", endpoint="upload_new_dataset_resource")
|
|
399
402
|
@api.doc(**common_doc)
|
|
400
403
|
class UploadNewDatasetResource(UploadMixin, API):
|
|
401
404
|
@api.secure
|
|
402
|
-
@api.doc(
|
|
405
|
+
@api.doc(
|
|
406
|
+
"upload_new_dataset_resource",
|
|
407
|
+
responses={415: "Incorrect file content type", 400: "Upload error"},
|
|
408
|
+
)
|
|
403
409
|
@api.expect(upload_parser)
|
|
404
410
|
@api.marshal_with(upload_fields, code=201)
|
|
405
411
|
def post(self, dataset):
|
|
406
|
-
|
|
412
|
+
"""Upload a new dataset resource"""
|
|
407
413
|
ResourceEditPermission(dataset).test()
|
|
408
414
|
infos = self.handle_upload(dataset)
|
|
409
415
|
resource = Resource(**infos)
|
|
@@ -413,41 +419,48 @@ class UploadNewDatasetResource(UploadMixin, API):
|
|
|
413
419
|
return resource, 201
|
|
414
420
|
|
|
415
421
|
|
|
416
|
-
@ns.route(
|
|
417
|
-
endpoint='upload_new_community_resource')
|
|
422
|
+
@ns.route("/<dataset:dataset>/upload/community/", endpoint="upload_new_community_resource")
|
|
418
423
|
@api.doc(**common_doc)
|
|
419
424
|
class UploadNewCommunityResources(UploadMixin, API):
|
|
420
425
|
@api.secure
|
|
421
|
-
@api.doc(
|
|
426
|
+
@api.doc(
|
|
427
|
+
"upload_new_community_resource",
|
|
428
|
+
responses={415: "Incorrect file content type", 400: "Upload error"},
|
|
429
|
+
)
|
|
422
430
|
@api.expect(upload_parser)
|
|
423
431
|
@api.marshal_with(upload_fields, code=201)
|
|
424
432
|
def post(self, dataset):
|
|
425
|
-
|
|
433
|
+
"""Upload a new community resource"""
|
|
426
434
|
infos = self.handle_upload(dataset)
|
|
427
|
-
infos[
|
|
428
|
-
infos[
|
|
435
|
+
infos["owner"] = current_user._get_current_object()
|
|
436
|
+
infos["dataset"] = dataset
|
|
429
437
|
community_resource = CommunityResource.objects.create(**infos)
|
|
430
438
|
return community_resource, 201
|
|
431
439
|
|
|
432
440
|
|
|
433
441
|
class ResourceMixin(object):
|
|
434
|
-
|
|
435
442
|
def get_resource_or_404(self, dataset, id):
|
|
436
|
-
resource = get_by(dataset.resources,
|
|
443
|
+
resource = get_by(dataset.resources, "id", id)
|
|
437
444
|
if not resource:
|
|
438
|
-
api.abort(404,
|
|
445
|
+
api.abort(404, "Resource does not exist")
|
|
439
446
|
return resource
|
|
440
447
|
|
|
441
448
|
|
|
442
|
-
@ns.route(
|
|
443
|
-
|
|
444
|
-
|
|
449
|
+
@ns.route(
|
|
450
|
+
"/<dataset:dataset>/resources/<uuid:rid>/upload/",
|
|
451
|
+
endpoint="upload_dataset_resource",
|
|
452
|
+
doc=common_doc,
|
|
453
|
+
)
|
|
454
|
+
@api.param("rid", "The resource unique identifier")
|
|
445
455
|
class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
446
456
|
@api.secure
|
|
447
|
-
@api.doc(
|
|
457
|
+
@api.doc(
|
|
458
|
+
"upload_dataset_resource",
|
|
459
|
+
responses={415: "Incorrect file content type", 400: "Upload error"},
|
|
460
|
+
)
|
|
448
461
|
@api.marshal_with(upload_fields)
|
|
449
462
|
def post(self, dataset, rid):
|
|
450
|
-
|
|
463
|
+
"""Upload a file related to a given resource on a given dataset"""
|
|
451
464
|
ResourceEditPermission(dataset).test()
|
|
452
465
|
resource = self.get_resource_or_404(dataset, rid)
|
|
453
466
|
fs_filename_to_remove = resource.fs_filename
|
|
@@ -462,15 +475,21 @@ class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
|
462
475
|
return resource
|
|
463
476
|
|
|
464
477
|
|
|
465
|
-
@ns.route(
|
|
466
|
-
|
|
467
|
-
|
|
478
|
+
@ns.route(
|
|
479
|
+
"/community_resources/<crid:community>/upload/",
|
|
480
|
+
endpoint="upload_community_resource",
|
|
481
|
+
doc=common_doc,
|
|
482
|
+
)
|
|
483
|
+
@api.param("community", "The community resource unique identifier")
|
|
468
484
|
class ReuploadCommunityResource(ResourceMixin, UploadMixin, API):
|
|
469
485
|
@api.secure
|
|
470
|
-
@api.doc(
|
|
486
|
+
@api.doc(
|
|
487
|
+
"upload_community_resource",
|
|
488
|
+
responses={415: "Incorrect file content type", 400: "Upload error"},
|
|
489
|
+
)
|
|
471
490
|
@api.marshal_with(upload_fields)
|
|
472
491
|
def post(self, community):
|
|
473
|
-
|
|
492
|
+
"""Update the file related to a given community resource"""
|
|
474
493
|
ResourceEditPermission(community).test()
|
|
475
494
|
fs_filename_to_remove = community.fs_filename
|
|
476
495
|
infos = self.handle_upload(community.dataset)
|
|
@@ -481,31 +500,30 @@ class ReuploadCommunityResource(ResourceMixin, UploadMixin, API):
|
|
|
481
500
|
return community
|
|
482
501
|
|
|
483
502
|
|
|
484
|
-
@ns.route(
|
|
485
|
-
|
|
486
|
-
@api.param('rid', 'The resource unique identifier')
|
|
503
|
+
@ns.route("/<dataset:dataset>/resources/<uuid:rid>/", endpoint="resource", doc=common_doc)
|
|
504
|
+
@api.param("rid", "The resource unique identifier")
|
|
487
505
|
class ResourceAPI(ResourceMixin, API):
|
|
488
|
-
@api.doc(
|
|
506
|
+
@api.doc("get_resource")
|
|
489
507
|
@api.marshal_with(resource_fields)
|
|
490
508
|
def get(self, dataset, rid):
|
|
491
|
-
|
|
509
|
+
"""Get a resource given its identifier"""
|
|
492
510
|
if dataset.deleted and not DatasetEditPermission(dataset).can():
|
|
493
|
-
api.abort(410,
|
|
511
|
+
api.abort(410, "Dataset has been deleted")
|
|
494
512
|
resource = self.get_resource_or_404(dataset, rid)
|
|
495
513
|
return resource
|
|
496
514
|
|
|
497
515
|
@api.secure
|
|
498
|
-
@api.doc(
|
|
516
|
+
@api.doc("update_resource", responses={400: "Validation error"})
|
|
499
517
|
@api.expect(resource_fields)
|
|
500
518
|
@api.marshal_with(resource_fields)
|
|
501
519
|
def put(self, dataset, rid):
|
|
502
|
-
|
|
520
|
+
"""Update a given resource on a given dataset"""
|
|
503
521
|
ResourceEditPermission(dataset).test()
|
|
504
522
|
resource = self.get_resource_or_404(dataset, rid)
|
|
505
523
|
form = api.validate(ResourceForm, resource)
|
|
506
524
|
# ensure API client does not override url on self-hosted resources
|
|
507
|
-
if resource.filetype ==
|
|
508
|
-
form._fields.get(
|
|
525
|
+
if resource.filetype == "file":
|
|
526
|
+
form._fields.get("url").data = resource.url
|
|
509
527
|
# populate_obj populates existing resource object with the content of the form.
|
|
510
528
|
# update_resource saves the updated resource dict to the database
|
|
511
529
|
# the additional dataset.save is required as we update the last_modified date.
|
|
@@ -517,51 +535,47 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
517
535
|
return resource
|
|
518
536
|
|
|
519
537
|
@api.secure
|
|
520
|
-
@api.doc(
|
|
538
|
+
@api.doc("delete_resource")
|
|
521
539
|
def delete(self, dataset, rid):
|
|
522
|
-
|
|
540
|
+
"""Delete a given resource on a given dataset"""
|
|
523
541
|
ResourceEditPermission(dataset).test()
|
|
524
542
|
resource = self.get_resource_or_404(dataset, rid)
|
|
525
543
|
dataset.remove_resource(resource)
|
|
526
544
|
dataset.last_modified_internal = datetime.utcnow()
|
|
527
545
|
dataset.save()
|
|
528
|
-
return
|
|
546
|
+
return "", 204
|
|
529
547
|
|
|
530
548
|
|
|
531
|
-
@ns.route(
|
|
549
|
+
@ns.route("/community_resources/", endpoint="community_resources")
|
|
532
550
|
class CommunityResourcesAPI(API):
|
|
533
|
-
@api.doc(
|
|
551
|
+
@api.doc("list_community_resources")
|
|
534
552
|
@api.expect(community_parser)
|
|
535
553
|
@api.marshal_with(community_resource_page_fields)
|
|
536
554
|
def get(self):
|
|
537
|
-
|
|
555
|
+
"""List all community resources"""
|
|
538
556
|
args = community_parser.parse_args()
|
|
539
557
|
community_resources = CommunityResource.objects
|
|
540
|
-
if args[
|
|
541
|
-
community_resources = community_resources(owner=args[
|
|
542
|
-
if args[
|
|
543
|
-
community_resources = community_resources(dataset=args[
|
|
544
|
-
if args[
|
|
545
|
-
community_resources = community_resources(
|
|
546
|
-
|
|
547
|
-
return (community_resources.order_by(args['sort'])
|
|
548
|
-
.paginate(args['page'], args['page_size']))
|
|
558
|
+
if args["owner"]:
|
|
559
|
+
community_resources = community_resources(owner=args["owner"])
|
|
560
|
+
if args["dataset"]:
|
|
561
|
+
community_resources = community_resources(dataset=args["dataset"])
|
|
562
|
+
if args["organization"]:
|
|
563
|
+
community_resources = community_resources(organization=args["organization"])
|
|
564
|
+
return community_resources.order_by(args["sort"]).paginate(args["page"], args["page_size"])
|
|
549
565
|
|
|
550
566
|
@api.secure
|
|
551
|
-
@api.doc(
|
|
567
|
+
@api.doc("create_community_resource", responses={400: "Validation error"})
|
|
552
568
|
@api.expect(community_resource_fields)
|
|
553
569
|
@api.marshal_with(community_resource_fields, code=201)
|
|
554
570
|
def post(self):
|
|
555
|
-
|
|
571
|
+
"""Create a new community resource"""
|
|
556
572
|
form = api.validate(CommunityResourceForm)
|
|
557
|
-
if form._fields.get(
|
|
558
|
-
api.abort(400,
|
|
573
|
+
if form._fields.get("filetype").data != "remote":
|
|
574
|
+
api.abort(400, "This endpoint only supports remote community resources")
|
|
559
575
|
resource = CommunityResource()
|
|
560
576
|
form.populate_obj(resource)
|
|
561
577
|
if not resource.dataset:
|
|
562
|
-
api.abort(400, errors={
|
|
563
|
-
'dataset': 'A dataset identifier is required'
|
|
564
|
-
})
|
|
578
|
+
api.abort(400, errors={"dataset": "A dataset identifier is required"})
|
|
565
579
|
if not resource.organization:
|
|
566
580
|
resource.owner = current_user._get_current_object()
|
|
567
581
|
resource.last_modified_internal = datetime.utcnow()
|
|
@@ -569,26 +583,25 @@ class CommunityResourcesAPI(API):
|
|
|
569
583
|
return resource, 201
|
|
570
584
|
|
|
571
585
|
|
|
572
|
-
@ns.route(
|
|
573
|
-
|
|
574
|
-
@api.param('community', 'The community resource unique identifier')
|
|
586
|
+
@ns.route("/community_resources/<crid:community>/", endpoint="community_resource", doc=common_doc)
|
|
587
|
+
@api.param("community", "The community resource unique identifier")
|
|
575
588
|
class CommunityResourceAPI(API):
|
|
576
|
-
@api.doc(
|
|
589
|
+
@api.doc("retrieve_community_resource")
|
|
577
590
|
@api.marshal_with(community_resource_fields)
|
|
578
591
|
def get(self, community):
|
|
579
|
-
|
|
592
|
+
"""Retrieve a community resource given its identifier"""
|
|
580
593
|
return community
|
|
581
594
|
|
|
582
595
|
@api.secure
|
|
583
|
-
@api.doc(
|
|
596
|
+
@api.doc("update_community_resource", responses={400: "Validation error"})
|
|
584
597
|
@api.expect(community_resource_fields)
|
|
585
598
|
@api.marshal_with(community_resource_fields)
|
|
586
599
|
def put(self, community):
|
|
587
|
-
|
|
600
|
+
"""Update a given community resource"""
|
|
588
601
|
ResourceEditPermission(community).test()
|
|
589
602
|
form = api.validate(CommunityResourceForm, community)
|
|
590
|
-
if community.filetype ==
|
|
591
|
-
form._fields.get(
|
|
603
|
+
if community.filetype == "file":
|
|
604
|
+
form._fields.get("url").data = community.url
|
|
592
605
|
form.populate_obj(community)
|
|
593
606
|
if not community.organization and not community.owner:
|
|
594
607
|
community.owner = current_user._get_current_object()
|
|
@@ -597,146 +610,156 @@ class CommunityResourceAPI(API):
|
|
|
597
610
|
return community
|
|
598
611
|
|
|
599
612
|
@api.secure
|
|
600
|
-
@api.doc(
|
|
613
|
+
@api.doc("delete_community_resource")
|
|
601
614
|
def delete(self, community):
|
|
602
|
-
|
|
615
|
+
"""Delete a given community resource"""
|
|
603
616
|
ResourceEditPermission(community).test()
|
|
604
617
|
# Deletes community resource's file from file storage
|
|
605
618
|
if community.fs_filename is not None:
|
|
606
619
|
storages.resources.delete(community.fs_filename)
|
|
607
620
|
community.delete()
|
|
608
|
-
return
|
|
621
|
+
return "", 204
|
|
609
622
|
|
|
610
623
|
|
|
611
|
-
@ns.route(
|
|
612
|
-
@ns.doc(
|
|
613
|
-
|
|
614
|
-
|
|
624
|
+
@ns.route("/<id>/followers/", endpoint="dataset_followers")
|
|
625
|
+
@ns.doc(
|
|
626
|
+
get={"id": "list_dataset_followers"},
|
|
627
|
+
post={"id": "follow_dataset"},
|
|
628
|
+
delete={"id": "unfollow_dataset"},
|
|
629
|
+
)
|
|
615
630
|
class DatasetFollowersAPI(FollowAPI):
|
|
616
631
|
model = Dataset
|
|
617
632
|
|
|
618
633
|
|
|
619
634
|
suggest_parser = api.parser()
|
|
620
635
|
suggest_parser.add_argument(
|
|
621
|
-
|
|
622
|
-
|
|
636
|
+
"q", help="The string to autocomplete/suggest", location="args", required=True
|
|
637
|
+
)
|
|
623
638
|
suggest_parser.add_argument(
|
|
624
|
-
|
|
625
|
-
|
|
639
|
+
"size", type=int, help="The amount of suggestion to fetch", location="args", default=10
|
|
640
|
+
)
|
|
626
641
|
|
|
627
642
|
|
|
628
|
-
@ns.route(
|
|
643
|
+
@ns.route("/suggest/", endpoint="suggest_datasets")
|
|
629
644
|
class DatasetSuggestAPI(API):
|
|
630
|
-
@api.doc(
|
|
645
|
+
@api.doc("suggest_datasets")
|
|
631
646
|
@api.expect(suggest_parser)
|
|
632
647
|
@api.marshal_with(dataset_suggestion_fields)
|
|
633
648
|
def get(self):
|
|
634
|
-
|
|
649
|
+
"""Datasets suggest endpoint using mongoDB contains"""
|
|
635
650
|
args = suggest_parser.parse_args()
|
|
636
651
|
datasets_query = Dataset.objects(archived=None, deleted=None, private=False)
|
|
637
652
|
datasets = datasets_query.filter(
|
|
638
|
-
Q(title__icontains=args[
|
|
653
|
+
Q(title__icontains=args["q"]) | Q(acronym__icontains=args["q"])
|
|
654
|
+
)
|
|
639
655
|
return [
|
|
640
656
|
{
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
dataset.organization.logo
|
|
647
|
-
|
|
648
|
-
|
|
657
|
+
"id": dataset.id,
|
|
658
|
+
"title": dataset.title,
|
|
659
|
+
"acronym": dataset.acronym,
|
|
660
|
+
"slug": dataset.slug,
|
|
661
|
+
"image_url": (
|
|
662
|
+
dataset.organization.logo
|
|
663
|
+
if dataset.organization
|
|
664
|
+
else dataset.owner.avatar
|
|
665
|
+
if dataset.owner
|
|
666
|
+
else None
|
|
667
|
+
),
|
|
649
668
|
}
|
|
650
|
-
for dataset in datasets.order_by(SUGGEST_SORTING).limit(args[
|
|
669
|
+
for dataset in datasets.order_by(SUGGEST_SORTING).limit(args["size"])
|
|
651
670
|
]
|
|
652
671
|
|
|
653
672
|
|
|
654
|
-
@ns.route(
|
|
673
|
+
@ns.route("/suggest/formats/", endpoint="suggest_formats")
|
|
655
674
|
class FormatsSuggestAPI(API):
|
|
656
|
-
@api.doc(
|
|
675
|
+
@api.doc("suggest_formats")
|
|
657
676
|
@api.expect(suggest_parser)
|
|
658
677
|
def get(self):
|
|
659
|
-
|
|
678
|
+
"""Suggest file formats"""
|
|
660
679
|
args = suggest_parser.parse_args()
|
|
661
|
-
results = [
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
680
|
+
results = [
|
|
681
|
+
{"text": i}
|
|
682
|
+
for i in current_app.config["ALLOWED_RESOURCES_EXTENSIONS"]
|
|
683
|
+
if args["q"] in i
|
|
684
|
+
]
|
|
685
|
+
results = results[: args["size"]]
|
|
686
|
+
return sorted(results, key=lambda o: len(o["text"]))
|
|
665
687
|
|
|
666
688
|
|
|
667
|
-
@ns.route(
|
|
689
|
+
@ns.route("/suggest/mime/", endpoint="suggest_mime")
|
|
668
690
|
class MimesSuggestAPI(API):
|
|
669
|
-
@api.doc(
|
|
691
|
+
@api.doc("suggest_mime")
|
|
670
692
|
@api.expect(suggest_parser)
|
|
671
693
|
def get(self):
|
|
672
|
-
|
|
694
|
+
"""Suggest mime types"""
|
|
673
695
|
args = suggest_parser.parse_args()
|
|
674
|
-
results = [
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
696
|
+
results = [
|
|
697
|
+
{"text": i} for i in current_app.config["ALLOWED_RESOURCES_MIMES"] if args["q"] in i
|
|
698
|
+
]
|
|
699
|
+
results = results[: args["size"]]
|
|
700
|
+
return sorted(results, key=lambda o: len(o["text"]))
|
|
678
701
|
|
|
679
702
|
|
|
680
|
-
@ns.route(
|
|
703
|
+
@ns.route("/licenses/", endpoint="licenses")
|
|
681
704
|
class LicensesAPI(API):
|
|
682
|
-
@api.doc(
|
|
705
|
+
@api.doc("list_licenses")
|
|
683
706
|
@api.marshal_list_with(license_fields)
|
|
684
707
|
def get(self):
|
|
685
|
-
|
|
708
|
+
"""List all available licenses"""
|
|
686
709
|
return list(License.objects)
|
|
687
710
|
|
|
688
711
|
|
|
689
|
-
@ns.route(
|
|
712
|
+
@ns.route("/frequencies/", endpoint="dataset_frequencies")
|
|
690
713
|
class FrequenciesAPI(API):
|
|
691
|
-
@api.doc(
|
|
714
|
+
@api.doc("list_frequencies")
|
|
692
715
|
@api.marshal_list_with(frequency_fields)
|
|
693
716
|
def get(self):
|
|
694
|
-
|
|
695
|
-
return [{
|
|
696
|
-
for id, label in UPDATE_FREQUENCIES.items()]
|
|
717
|
+
"""List all available frequencies"""
|
|
718
|
+
return [{"id": id, "label": label} for id, label in UPDATE_FREQUENCIES.items()]
|
|
697
719
|
|
|
698
720
|
|
|
699
|
-
@ns.route(
|
|
721
|
+
@ns.route("/extensions/", endpoint="allowed_extensions")
|
|
700
722
|
class AllowedExtensionsAPI(API):
|
|
701
|
-
@api.doc(
|
|
702
|
-
@api.response(200,
|
|
723
|
+
@api.doc("allowed_extensions")
|
|
724
|
+
@api.response(200, "Success", [str])
|
|
703
725
|
def get(self):
|
|
704
|
-
|
|
705
|
-
return current_app.config[
|
|
726
|
+
"""List all allowed resources extensions"""
|
|
727
|
+
return current_app.config["ALLOWED_RESOURCES_EXTENSIONS"]
|
|
706
728
|
|
|
707
729
|
|
|
708
|
-
@ns.route(
|
|
709
|
-
|
|
710
|
-
|
|
730
|
+
@ns.route(
|
|
731
|
+
"/<dataset:dataset>/resources/<uuid:rid>/check/",
|
|
732
|
+
endpoint="check_dataset_resource",
|
|
733
|
+
doc=common_doc,
|
|
734
|
+
)
|
|
735
|
+
@api.param("rid", "The resource unique identifier")
|
|
711
736
|
class CheckDatasetResource(API, ResourceMixin):
|
|
712
|
-
|
|
713
|
-
@api.doc('check_dataset_resource')
|
|
737
|
+
@api.doc("check_dataset_resource")
|
|
714
738
|
def get(self, dataset, rid):
|
|
715
|
-
|
|
739
|
+
"""Checks that a resource's URL exists and returns metadata."""
|
|
716
740
|
resource = self.get_resource_or_404(dataset, rid)
|
|
717
741
|
return check_resource(resource)
|
|
718
742
|
|
|
719
743
|
|
|
720
|
-
@ns.route(
|
|
744
|
+
@ns.route("/resource_types/", endpoint="resource_types")
|
|
721
745
|
class ResourceTypesAPI(API):
|
|
722
|
-
@api.doc(
|
|
746
|
+
@api.doc("resource_types")
|
|
723
747
|
@api.marshal_list_with(resource_type_fields)
|
|
724
748
|
def get(self):
|
|
725
|
-
|
|
726
|
-
return [{
|
|
727
|
-
for id, label in RESOURCE_TYPES.items()]
|
|
749
|
+
"""List all resource types"""
|
|
750
|
+
return [{"id": id, "label": label} for id, label in RESOURCE_TYPES.items()]
|
|
728
751
|
|
|
729
752
|
|
|
730
|
-
@ns.route(
|
|
753
|
+
@ns.route("/schemas/", endpoint="schemas")
|
|
731
754
|
class SchemasAPI(API):
|
|
732
|
-
@api.doc(
|
|
755
|
+
@api.doc("schemas")
|
|
733
756
|
@api.marshal_list_with(catalog_schema_fields)
|
|
734
757
|
def get(self):
|
|
735
|
-
|
|
758
|
+
"""List all available schemas"""
|
|
736
759
|
try:
|
|
737
760
|
# This method call is cached as it makes HTTP requests
|
|
738
761
|
return ResourceSchema.assignable_schemas()
|
|
739
762
|
except SchemasCacheUnavailableException:
|
|
740
|
-
abort(503, description=
|
|
763
|
+
abort(503, description="No schemas in cache and endpoint unavailable")
|
|
741
764
|
except SchemasCatalogNotFoundException:
|
|
742
|
-
abort(404, description=
|
|
765
|
+
abort(404, description="Schema catalog endpoint was not found")
|