udata 9.1.2.dev30355__py2.py3-none-any.whl → 9.1.2.dev30382__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 +135 -124
- 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 +56 -54
- 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/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/static/chunks/{11.e9b9ca1f3e03d4020377.js → 11.52e531c19f8de80c00cf.js} +3 -3
- udata/static/chunks/{11.e9b9ca1f3e03d4020377.js.map → 11.52e531c19f8de80c00cf.js.map} +1 -1
- udata/static/chunks/{13.038c0d9aa0dfa0181c4b.js → 13.c3343a7f1070061c0e10.js} +2 -2
- udata/static/chunks/{13.038c0d9aa0dfa0181c4b.js.map → 13.c3343a7f1070061c0e10.js.map} +1 -1
- udata/static/chunks/{16.0baa2b64a74a2dcde25c.js → 16.8fa42440ad75ca172e6d.js} +2 -2
- udata/static/chunks/{16.0baa2b64a74a2dcde25c.js.map → 16.8fa42440ad75ca172e6d.js.map} +1 -1
- udata/static/chunks/{19.350a9f150b074b4ecefa.js → 19.9c6c8412729cd6d59cfa.js} +3 -3
- udata/static/chunks/{19.350a9f150b074b4ecefa.js.map → 19.9c6c8412729cd6d59cfa.js.map} +1 -1
- udata/static/chunks/{5.6ebbce2b9b3e696d3da5.js → 5.71d15c2e4f21feee2a9a.js} +3 -3
- udata/static/chunks/{5.6ebbce2b9b3e696d3da5.js.map → 5.71d15c2e4f21feee2a9a.js.map} +1 -1
- udata/static/chunks/{6.d8a5f7b017bcbd083641.js → 6.9139dc098b8ea640b890.js} +3 -3
- udata/static/chunks/{6.d8a5f7b017bcbd083641.js.map → 6.9139dc098b8ea640b890.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- 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 +5 -6
- udata/tests/api/test_auth_api.py +395 -321
- udata/tests/api/test_base_api.py +31 -33
- 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 +76 -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_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.dev30382.dist-info}/METADATA +3 -2
- udata-9.1.2.dev30382.dist-info/RECORD +704 -0
- udata-9.1.2.dev30355.dist-info/RECORD +0 -704
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/LICENSE +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/WHEEL +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/entry_points.txt +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/top_level.txt +0 -0
udata/core/storages/api.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
2
|
from datetime import datetime
|
|
4
3
|
|
|
5
4
|
from flask import json
|
|
@@ -7,40 +6,39 @@ from werkzeug.datastructures import FileStorage
|
|
|
7
6
|
|
|
8
7
|
from udata.api import api, fields
|
|
9
8
|
|
|
10
|
-
from . import
|
|
11
|
-
|
|
9
|
+
from . import chunks, utils
|
|
12
10
|
|
|
13
|
-
META =
|
|
11
|
+
META = "meta.json"
|
|
14
12
|
|
|
15
|
-
IMAGES_MIMETYPES = (
|
|
13
|
+
IMAGES_MIMETYPES = ("image/jpeg", "image/png", "image/webp")
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
uploaded_image_fields = api.model(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
uploaded_image_fields = api.model(
|
|
17
|
+
"UploadedImage",
|
|
18
|
+
{
|
|
19
|
+
"success": fields.Boolean(
|
|
20
|
+
description="Whether the upload succeeded or not.", readonly=True, default=True
|
|
21
|
+
),
|
|
22
|
+
"image": fields.ImageField(),
|
|
23
|
+
},
|
|
24
|
+
)
|
|
24
25
|
|
|
25
|
-
chunk_status_fields = api.model(
|
|
26
|
-
'success': fields.Boolean,
|
|
27
|
-
'error': fields.String
|
|
28
|
-
})
|
|
26
|
+
chunk_status_fields = api.model("UploadStatus", {"success": fields.Boolean, "error": fields.String})
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
image_parser = api.parser()
|
|
32
|
-
image_parser.add_argument(
|
|
33
|
-
image_parser.add_argument(
|
|
30
|
+
image_parser.add_argument("file", type=FileStorage, location="files")
|
|
31
|
+
image_parser.add_argument("bbox", type=str, location="form")
|
|
34
32
|
|
|
35
33
|
|
|
36
34
|
upload_parser = api.parser()
|
|
37
|
-
upload_parser.add_argument(
|
|
38
|
-
upload_parser.add_argument(
|
|
39
|
-
upload_parser.add_argument(
|
|
40
|
-
upload_parser.add_argument(
|
|
41
|
-
upload_parser.add_argument(
|
|
42
|
-
upload_parser.add_argument(
|
|
43
|
-
upload_parser.add_argument(
|
|
35
|
+
upload_parser.add_argument("file", type=FileStorage, location="files")
|
|
36
|
+
upload_parser.add_argument("uuid", type=str, location="form")
|
|
37
|
+
upload_parser.add_argument("filename", type=str, location="form")
|
|
38
|
+
upload_parser.add_argument("partindex", type=int, location="form")
|
|
39
|
+
upload_parser.add_argument("partbyteoffset", type=int, location="form")
|
|
40
|
+
upload_parser.add_argument("totalparts", type=int, location="form")
|
|
41
|
+
upload_parser.add_argument("chunksize", type=int, location="form")
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
class UploadStatus(Exception):
|
|
@@ -51,22 +49,24 @@ class UploadStatus(Exception):
|
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
class UploadProgress(UploadStatus):
|
|
54
|
-
|
|
52
|
+
"""Raised on successful chunk uploaded"""
|
|
53
|
+
|
|
55
54
|
pass
|
|
56
55
|
|
|
57
56
|
|
|
58
57
|
class UploadError(UploadStatus):
|
|
59
|
-
|
|
58
|
+
"""Raised on any upload error"""
|
|
59
|
+
|
|
60
60
|
def __init__(self, error=None):
|
|
61
61
|
super(UploadError, self).__init__(ok=False, error=error)
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def on_upload_status(status):
|
|
65
|
-
|
|
65
|
+
"""Not an error, just raised when chunk is processed"""
|
|
66
66
|
if status.ok:
|
|
67
|
-
return {
|
|
67
|
+
return {"success": True}, 200
|
|
68
68
|
else:
|
|
69
|
-
return {
|
|
69
|
+
return {"success": False, "error": status.error}, 400
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
@api.errorhandler(UploadStatus)
|
|
@@ -74,7 +74,7 @@ def on_upload_status(status):
|
|
|
74
74
|
@api.errorhandler(UploadProgress)
|
|
75
75
|
@api.marshal_with(chunk_status_fields, code=200)
|
|
76
76
|
def api_upload_status(status):
|
|
77
|
-
|
|
77
|
+
"""API Upload response handler"""
|
|
78
78
|
return on_upload_status(status)
|
|
79
79
|
|
|
80
80
|
|
|
@@ -91,34 +91,40 @@ def get_file_size(file):
|
|
|
91
91
|
|
|
92
92
|
def save_chunk(file, args):
|
|
93
93
|
# Check file size
|
|
94
|
-
if get_file_size(file) != args[
|
|
95
|
-
raise UploadProgress(ok=False, error=
|
|
96
|
-
filename = chunk_filename(args[
|
|
94
|
+
if get_file_size(file) != args["chunksize"]:
|
|
95
|
+
raise UploadProgress(ok=False, error="Chunk size mismatch")
|
|
96
|
+
filename = chunk_filename(args["uuid"], args["partindex"])
|
|
97
97
|
chunks.save(file, filename=filename)
|
|
98
|
-
meta_filename = chunk_filename(args[
|
|
99
|
-
chunks.write(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
meta_filename = chunk_filename(args["uuid"], META)
|
|
99
|
+
chunks.write(
|
|
100
|
+
meta_filename,
|
|
101
|
+
json.dumps(
|
|
102
|
+
{
|
|
103
|
+
"uuid": str(args["uuid"]),
|
|
104
|
+
"filename": args["filename"],
|
|
105
|
+
"totalparts": args["totalparts"],
|
|
106
|
+
"lastchunk": datetime.utcnow(),
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
overwrite=True,
|
|
110
|
+
)
|
|
105
111
|
raise UploadProgress()
|
|
106
112
|
|
|
107
113
|
|
|
108
114
|
def combine_chunks(storage, args, prefix=None):
|
|
109
|
-
|
|
115
|
+
"""
|
|
110
116
|
Combine a chunked file into a whole file again.
|
|
111
117
|
Goes through each part, in order,
|
|
112
118
|
and appends that part's bytes to another destination file.
|
|
113
119
|
Chunks are stored in the chunks storage.
|
|
114
|
-
|
|
115
|
-
uuid = args[
|
|
120
|
+
"""
|
|
121
|
+
uuid = args["uuid"]
|
|
116
122
|
# Normalize filename including extension
|
|
117
|
-
target = utils.normalize(args[
|
|
123
|
+
target = utils.normalize(args["filename"])
|
|
118
124
|
if prefix:
|
|
119
125
|
target = os.path.join(prefix, target)
|
|
120
|
-
with storage.open(target,
|
|
121
|
-
for i in range(args[
|
|
126
|
+
with storage.open(target, "wb") as out:
|
|
127
|
+
for i in range(args["totalparts"]):
|
|
122
128
|
partname = chunk_filename(uuid, i)
|
|
123
129
|
out.write(chunks.read(partname))
|
|
124
130
|
chunks.delete(partname)
|
|
@@ -128,8 +134,8 @@ def combine_chunks(storage, args, prefix=None):
|
|
|
128
134
|
|
|
129
135
|
def handle_upload(storage, prefix=None):
|
|
130
136
|
args = upload_parser.parse_args()
|
|
131
|
-
is_chunk = args[
|
|
132
|
-
uploaded_file = args[
|
|
137
|
+
is_chunk = args["totalparts"] and args["totalparts"] > 1
|
|
138
|
+
uploaded_file = args["file"]
|
|
133
139
|
|
|
134
140
|
if is_chunk:
|
|
135
141
|
if uploaded_file:
|
|
@@ -137,34 +143,30 @@ def handle_upload(storage, prefix=None):
|
|
|
137
143
|
else:
|
|
138
144
|
fs_filename = combine_chunks(storage, args, prefix=prefix)
|
|
139
145
|
elif not uploaded_file:
|
|
140
|
-
raise UploadError(
|
|
146
|
+
raise UploadError("Missing file parameter")
|
|
141
147
|
else:
|
|
142
148
|
# Normalize filename including extension
|
|
143
149
|
filename = utils.normalize(uploaded_file.filename)
|
|
144
|
-
fs_filename = storage.save(
|
|
145
|
-
uploaded_file,
|
|
146
|
-
prefix=prefix,
|
|
147
|
-
filename=filename
|
|
148
|
-
)
|
|
150
|
+
fs_filename = storage.save(uploaded_file, prefix=prefix, filename=filename)
|
|
149
151
|
|
|
150
152
|
metadata = storage.metadata(fs_filename)
|
|
151
|
-
metadata[
|
|
152
|
-
metadata[
|
|
153
|
-
checksum = metadata.pop(
|
|
154
|
-
algo, checksum = checksum.split(
|
|
153
|
+
metadata["last_modified_internal"] = metadata.pop("modified")
|
|
154
|
+
metadata["fs_filename"] = fs_filename
|
|
155
|
+
checksum = metadata.pop("checksum")
|
|
156
|
+
algo, checksum = checksum.split(":", 1)
|
|
155
157
|
metadata[algo] = checksum
|
|
156
|
-
metadata[
|
|
158
|
+
metadata["format"] = utils.extension(fs_filename)
|
|
157
159
|
return metadata
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
def parse_uploaded_image(field):
|
|
161
|
-
|
|
163
|
+
"""Parse an uploaded image and save into a db.ImageField()"""
|
|
162
164
|
args = image_parser.parse_args()
|
|
163
165
|
|
|
164
|
-
image = args[
|
|
166
|
+
image = args["file"]
|
|
165
167
|
if image.mimetype not in IMAGES_MIMETYPES:
|
|
166
|
-
api.abort(400,
|
|
167
|
-
bbox = args.get(
|
|
168
|
+
api.abort(400, "Unsupported image format")
|
|
169
|
+
bbox = args.get("bbox", None)
|
|
168
170
|
if bbox:
|
|
169
|
-
bbox = [int(float(c)) for c in bbox.split(
|
|
171
|
+
bbox = [int(float(c)) for c in bbox.split(",")]
|
|
170
172
|
field.save(image, bbox=bbox)
|
udata/core/storages/tasks.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from datetime import datetime, timedelta
|
|
2
|
-
from dateutil.parser import parse
|
|
3
2
|
|
|
3
|
+
from dateutil.parser import parse
|
|
4
4
|
from flask import current_app, json
|
|
5
5
|
|
|
6
6
|
from udata.core.storages import chunks
|
|
@@ -10,14 +10,14 @@ from udata.tasks import get_logger, job
|
|
|
10
10
|
log = get_logger(__name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@job(
|
|
13
|
+
@job("purge-chunks")
|
|
14
14
|
def purge_chunks(self):
|
|
15
|
-
log.info(
|
|
16
|
-
max_retention = timedelta(seconds=current_app.config[
|
|
15
|
+
log.info("Purging uploaded chunks")
|
|
16
|
+
max_retention = timedelta(seconds=current_app.config["UPLOAD_MAX_RETENTION"])
|
|
17
17
|
meta_files = (f for f in chunks.list_files() if f.endswith(META))
|
|
18
18
|
for filename in meta_files:
|
|
19
19
|
metadata = json.loads(chunks.read(filename))
|
|
20
|
-
if datetime.utcnow() - parse(metadata[
|
|
21
|
-
uuid = metadata[
|
|
22
|
-
log.info(
|
|
20
|
+
if datetime.utcnow() - parse(metadata["lastchunk"]) >= max_retention:
|
|
21
|
+
uuid = metadata["uuid"]
|
|
22
|
+
log.info("Removing %s expired chunks", uuid)
|
|
23
23
|
chunks.delete(uuid)
|
udata/core/storages/utils.py
CHANGED
|
@@ -6,15 +6,15 @@ import zlib
|
|
|
6
6
|
from flask import current_app
|
|
7
7
|
from slugify import Slugify
|
|
8
8
|
|
|
9
|
-
CHUNK_SIZE = 2
|
|
9
|
+
CHUNK_SIZE = 2**16
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
slugify = Slugify(separator=
|
|
12
|
+
slugify = Slugify(separator="-", to_lower=True, safe_chars=".")
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def hash(file, hasher):
|
|
16
16
|
blk_size_to_read = hasher.block_size * CHUNK_SIZE
|
|
17
|
-
while
|
|
17
|
+
while True:
|
|
18
18
|
read_data = file.read(blk_size_to_read)
|
|
19
19
|
if not read_data:
|
|
20
20
|
break
|
|
@@ -23,28 +23,28 @@ def hash(file, hasher):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def sha1(file):
|
|
26
|
-
|
|
26
|
+
"""Perform a SHA1 digest on file"""
|
|
27
27
|
return hash(file, hashlib.sha1())
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def md5(file):
|
|
31
|
-
|
|
31
|
+
"""Perform a MD5 digest on a file"""
|
|
32
32
|
return hash(file, hashlib.md5())
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def crc32(file):
|
|
36
|
-
|
|
36
|
+
"""Perform a CRC digest on a file"""
|
|
37
37
|
value = zlib.crc32(file.read())
|
|
38
|
-
return
|
|
38
|
+
return "%08X" % (value & 0xFFFFFFFF)
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def mime(url):
|
|
42
|
-
|
|
42
|
+
"""Get the mimetype from an url or a filename"""
|
|
43
43
|
return mimetypes.guess_type(url)[0]
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def extension(filename):
|
|
47
|
-
|
|
47
|
+
"""
|
|
48
48
|
Properly extract the extension from filename.
|
|
49
49
|
We keep the last extension except for archive extensions, where we check
|
|
50
50
|
previous extensions as well.
|
|
@@ -55,23 +55,23 @@ def extension(filename):
|
|
|
55
55
|
- test.2022.zip -> zip
|
|
56
56
|
- test.2022.csv.tar.gz -> csv.tar.gz
|
|
57
57
|
- test.geojson.csv -> csv
|
|
58
|
-
|
|
58
|
+
"""
|
|
59
59
|
filename = os.path.basename(filename)
|
|
60
60
|
extension = None
|
|
61
61
|
|
|
62
|
-
while
|
|
62
|
+
while "." in filename:
|
|
63
63
|
filename, ext = os.path.splitext(filename)
|
|
64
|
-
if ext.startswith(
|
|
64
|
+
if ext.startswith("."):
|
|
65
65
|
ext = ext[1:]
|
|
66
66
|
|
|
67
|
-
if extension and ext not in current_app.config[
|
|
67
|
+
if extension and ext not in current_app.config["ALLOWED_RESOURCES_EXTENSIONS"]:
|
|
68
68
|
# We don't want to add this extension if one has already been detected
|
|
69
69
|
# and this one is not in the allowed resources extensions list.
|
|
70
70
|
break
|
|
71
71
|
|
|
72
|
-
extension = ext if not extension else ext +
|
|
72
|
+
extension = ext if not extension else ext + "." + extension
|
|
73
73
|
|
|
74
|
-
if ext not in current_app.config[
|
|
74
|
+
if ext not in current_app.config["ALLOWED_ARCHIVE_EXTENSIONS"]:
|
|
75
75
|
# We don't want to continue the loop if this ext is not an allowed archived extension
|
|
76
76
|
break
|
|
77
77
|
|
udata/core/storages/views.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import flask_storage as fs
|
|
1
2
|
from flask import Blueprint, jsonify
|
|
2
3
|
from flask_security import login_required
|
|
3
|
-
import flask_storage as fs
|
|
4
|
-
|
|
5
|
-
from .api import handle_upload, on_upload_status, UploadStatus
|
|
6
4
|
|
|
5
|
+
from .api import UploadStatus, handle_upload, on_upload_status
|
|
7
6
|
|
|
8
|
-
blueprint = Blueprint(
|
|
7
|
+
blueprint = Blueprint("storage", __name__)
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
@blueprint.app_errorhandler(UploadStatus)
|
|
@@ -15,8 +14,8 @@ def jsonify_upload_status(status):
|
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
@login_required
|
|
18
|
-
@blueprint.route(
|
|
17
|
+
@blueprint.route("/upload/<name>/", methods=["POST"])
|
|
19
18
|
def upload(name):
|
|
20
|
-
|
|
19
|
+
"""Handle upload on POST if authorized."""
|
|
21
20
|
storage = fs.by_name(name)
|
|
22
21
|
return jsonify(success=True, **handle_upload(storage))
|
udata/core/tags/api.py
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
from udata.api import
|
|
2
|
-
|
|
3
|
-
from udata.tags import slug # TODO: merge this into this package
|
|
1
|
+
from udata.api import API, api
|
|
4
2
|
from udata.models import Tag
|
|
3
|
+
from udata.tags import slug # TODO: merge this into this package
|
|
5
4
|
|
|
6
5
|
DEFAULT_SIZE = 8
|
|
7
6
|
|
|
8
|
-
ns = api.namespace(
|
|
7
|
+
ns = api.namespace("tags", "Tags related operations")
|
|
9
8
|
|
|
10
9
|
parser = api.parser()
|
|
11
10
|
parser.add_argument(
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
"q", type=str, help="The string to autocomplete/suggest", location="args", required=True
|
|
12
|
+
)
|
|
14
13
|
parser.add_argument(
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
"size",
|
|
15
|
+
type=int,
|
|
16
|
+
help="The amount of suggestion to fetch",
|
|
17
|
+
location="args",
|
|
18
|
+
default=DEFAULT_SIZE,
|
|
19
|
+
)
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
@ns.route(
|
|
22
|
+
@ns.route("/suggest/", endpoint="suggest_tags")
|
|
20
23
|
class SuggestTagsAPI(API):
|
|
21
|
-
@api.doc(
|
|
24
|
+
@api.doc("suggest_tags")
|
|
22
25
|
@api.expect(parser)
|
|
23
26
|
def get(self):
|
|
24
|
-
|
|
27
|
+
"""Suggest tags"""
|
|
25
28
|
args = parser.parse_args()
|
|
26
|
-
q = slug(args[
|
|
27
|
-
results = [{
|
|
28
|
-
return sorted(results, key=lambda o: len(o[
|
|
29
|
+
q = slug(args["q"])
|
|
30
|
+
results = [{"text": i.name} for i in Tag.objects(name__icontains=q).limit(args["size"])]
|
|
31
|
+
return sorted(results, key=lambda o: len(o["text"]))
|
udata/core/tags/csv.py
CHANGED
udata/core/tags/models.py
CHANGED
|
@@ -5,21 +5,24 @@ from udata.mongo import db
|
|
|
5
5
|
log = logging.getLogger(__name__)
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
__all__ = (
|
|
8
|
+
__all__ = ("Tag",)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Tag(db.Document):
|
|
12
|
-
|
|
12
|
+
"""
|
|
13
13
|
This collection is auto-populated every hour map-reducing tag properties
|
|
14
14
|
from Datasets dans Reuses.
|
|
15
|
-
|
|
15
|
+
"""
|
|
16
|
+
|
|
16
17
|
name = db.StringField(required=True, unique=True)
|
|
17
18
|
counts = db.DictField()
|
|
18
19
|
total = db.IntField(default=0)
|
|
19
20
|
|
|
20
21
|
meta = {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
"indexes": ["name", "-total"],
|
|
23
|
+
"ordering": [
|
|
24
|
+
"-total",
|
|
25
|
+
],
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
def clean(self):
|
udata/core/tags/tasks.py
CHANGED
|
@@ -7,15 +7,15 @@ from .models import Tag
|
|
|
7
7
|
|
|
8
8
|
log = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
|
-
map_tags =
|
|
10
|
+
map_tags = """
|
|
11
11
|
function() {
|
|
12
12
|
this.tags.forEach(function(tag) {
|
|
13
13
|
emit(tag, 1);
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
"""
|
|
17
17
|
|
|
18
|
-
reduce_tags =
|
|
18
|
+
reduce_tags = """
|
|
19
19
|
function(key, values) {
|
|
20
20
|
var total = 0;
|
|
21
21
|
for(var i = 0; i < values.length; i++) {
|
|
@@ -23,23 +23,21 @@ function(key, values) {
|
|
|
23
23
|
}
|
|
24
24
|
return total;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
"""
|
|
27
27
|
|
|
28
28
|
TAGGED = {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
"datasets": Dataset,
|
|
30
|
+
"reuses": Reuse,
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
@job(
|
|
34
|
+
@job("count-tags")
|
|
35
35
|
def count_tags(self):
|
|
36
|
-
|
|
36
|
+
"""Count tag occurences by type and update the tag collection"""
|
|
37
37
|
for key, model in TAGGED.items():
|
|
38
|
-
collection =
|
|
39
|
-
results =
|
|
40
|
-
.map_reduce(map_tags, reduce_tags, collection))
|
|
38
|
+
collection = "{0}_tags".format(key)
|
|
39
|
+
results = model.objects(tags__exists=True).map_reduce(map_tags, reduce_tags, collection)
|
|
41
40
|
for result in results:
|
|
42
|
-
tag, created = Tag.objects.get_or_create(name=result.key,
|
|
43
|
-
auto_save=False)
|
|
41
|
+
tag, created = Tag.objects.get_or_create(name=result.key, auto_save=False)
|
|
44
42
|
tag.counts[key] = int(result.value) if result.value else 0
|
|
45
43
|
tag.save()
|
udata/core/tags/views.py
CHANGED
|
@@ -9,10 +9,10 @@ from .models import Tag
|
|
|
9
9
|
log = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
blueprint = I18nBlueprint(
|
|
12
|
+
blueprint = I18nBlueprint("tags", __name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
@blueprint.route(
|
|
15
|
+
@blueprint.route("/tags.csv", endpoint="csv")
|
|
16
16
|
def tags_csv():
|
|
17
|
-
adapter = TagCsvAdapter(Tag.objects.order_by(
|
|
18
|
-
return csv.stream(adapter,
|
|
17
|
+
adapter = TagCsvAdapter(Tag.objects.order_by("-total"))
|
|
18
|
+
return csv.stream(adapter, "tags")
|