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/api_fields.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
from udata.api import api
|
|
2
1
|
import flask_restx.fields as restx_fields
|
|
3
|
-
import udata.api.fields as custom_restx_fields
|
|
4
|
-
from bson import ObjectId
|
|
5
2
|
import mongoengine
|
|
6
3
|
import mongoengine.fields as mongo_fields
|
|
4
|
+
from bson import ObjectId
|
|
7
5
|
|
|
6
|
+
import udata.api.fields as custom_restx_fields
|
|
7
|
+
from udata.api import api
|
|
8
8
|
from udata.mongo.errors import FieldValidationError
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
def convert_db_to_field(key, field, info={}):
|
|
12
|
+
"""
|
|
12
13
|
This function maps a Mongo field to a Flask RestX field.
|
|
13
14
|
Most of the types are a simple 1-to-1 mapping except lists and references that requires
|
|
14
15
|
more work.
|
|
@@ -17,11 +18,11 @@ def convert_db_to_field(key, field, info = {}):
|
|
|
17
18
|
In the first part of the function we save the RestX constructor as a lambda because we need to call it with the
|
|
18
19
|
params. Since merging the params involve a litte bit of work (merging default params with read/write params and then with
|
|
19
20
|
user-supplied overrides, setting the readonly flag…), it's easier to have do this one time at the end of the function.
|
|
20
|
-
|
|
21
|
-
info = {
|
|
21
|
+
"""
|
|
22
|
+
info = {**getattr(field, "__additional_field_info__", {}), **info}
|
|
22
23
|
|
|
23
24
|
params = {}
|
|
24
|
-
params[
|
|
25
|
+
params["required"] = field.required
|
|
25
26
|
|
|
26
27
|
read_params = {}
|
|
27
28
|
write_params = {}
|
|
@@ -30,21 +31,21 @@ def convert_db_to_field(key, field, info = {}):
|
|
|
30
31
|
constructor_read = None
|
|
31
32
|
constructor_write = None
|
|
32
33
|
|
|
33
|
-
if info.get(
|
|
34
|
+
if info.get("convert_to"):
|
|
34
35
|
# TODO: this is currently never used. We may remove it if the auto-conversion
|
|
35
36
|
# is always good enough.
|
|
36
|
-
return info.get(
|
|
37
|
+
return info.get("convert_to"), info.get("convert_to")
|
|
37
38
|
elif isinstance(field, mongo_fields.StringField):
|
|
38
39
|
constructor = restx_fields.String
|
|
39
|
-
params[
|
|
40
|
-
params[
|
|
41
|
-
params[
|
|
40
|
+
params["min_length"] = field.min_length
|
|
41
|
+
params["max_length"] = field.max_length
|
|
42
|
+
params["enum"] = field.choices
|
|
42
43
|
elif isinstance(field, mongo_fields.ObjectIdField):
|
|
43
44
|
constructor = restx_fields.String
|
|
44
45
|
elif isinstance(field, mongo_fields.FloatField):
|
|
45
46
|
constructor = restx_fields.Float
|
|
46
|
-
params[
|
|
47
|
-
params[
|
|
47
|
+
params["min"] = field.min # TODO min_value?
|
|
48
|
+
params["max"] = field.max
|
|
48
49
|
elif isinstance(field, mongo_fields.BooleanField):
|
|
49
50
|
constructor = restx_fields.Boolean
|
|
50
51
|
elif isinstance(field, mongo_fields.DateTimeField):
|
|
@@ -54,7 +55,9 @@ def convert_db_to_field(key, field, info = {}):
|
|
|
54
55
|
elif isinstance(field, mongo_fields.ListField):
|
|
55
56
|
# For lists, we convert the inner value from Mongo to RestX then we create
|
|
56
57
|
# the `List` RestX type with this converted inner value.
|
|
57
|
-
field_read, field_write = convert_db_to_field(
|
|
58
|
+
field_read, field_write = convert_db_to_field(
|
|
59
|
+
f"{key}.inner", field.field, info.get("inner_field_info", {})
|
|
60
|
+
)
|
|
58
61
|
constructor_read = lambda **kwargs: restx_fields.List(field_read, **kwargs)
|
|
59
62
|
constructor_write = lambda **kwargs: restx_fields.List(field_write, **kwargs)
|
|
60
63
|
elif isinstance(field, mongo_fields.ReferenceField):
|
|
@@ -62,69 +65,83 @@ def convert_db_to_field(key, field, info = {}):
|
|
|
62
65
|
# For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
|
|
63
66
|
# the referenced model, if not we return a String (and RestX will call the `str()` of the model
|
|
64
67
|
# when returning from an endpoint)
|
|
65
|
-
nested_fields = info.get(
|
|
68
|
+
nested_fields = info.get("nested_fields")
|
|
66
69
|
if nested_fields is None:
|
|
67
70
|
# If there is no `nested_fields` convert the object to the string representation.
|
|
68
71
|
constructor_read = restx_fields.String
|
|
69
72
|
else:
|
|
70
73
|
constructor_read = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
|
|
71
74
|
|
|
72
|
-
write_params[
|
|
75
|
+
write_params["description"] = "ID of the reference"
|
|
73
76
|
constructor_write = restx_fields.String
|
|
74
77
|
elif isinstance(field, mongo_fields.EmbeddedDocumentField):
|
|
75
|
-
nested_fields = info.get(
|
|
78
|
+
nested_fields = info.get("nested_fields")
|
|
76
79
|
if nested_fields is not None:
|
|
77
80
|
constructor = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
|
|
78
|
-
elif hasattr(field.document_type_obj,
|
|
79
|
-
constructor_read = lambda **kwargs: restx_fields.Nested(
|
|
80
|
-
|
|
81
|
+
elif hasattr(field.document_type_obj, "__read_fields__"):
|
|
82
|
+
constructor_read = lambda **kwargs: restx_fields.Nested(
|
|
83
|
+
field.document_type_obj.__read_fields__, **kwargs
|
|
84
|
+
)
|
|
85
|
+
constructor_write = lambda **kwargs: restx_fields.Nested(
|
|
86
|
+
field.document_type_obj.__write_fields__, **kwargs
|
|
87
|
+
)
|
|
81
88
|
else:
|
|
82
|
-
raise ValueError(
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"EmbeddedDocumentField `{key}` requires a `nested_fields` param to serialize/deserialize or a `@generate_fields()` definition."
|
|
91
|
+
)
|
|
83
92
|
|
|
84
93
|
else:
|
|
85
94
|
raise ValueError(f"Unsupported MongoEngine field type {field.__class__.__name__}")
|
|
86
|
-
|
|
95
|
+
|
|
87
96
|
read_params = {**params, **read_params, **info}
|
|
88
97
|
write_params = {**params, **write_params, **info}
|
|
89
98
|
|
|
90
99
|
read = constructor_read(**read_params) if constructor_read else constructor(**read_params)
|
|
91
|
-
if write_params.get(
|
|
100
|
+
if write_params.get("readonly", False):
|
|
92
101
|
write = None
|
|
93
102
|
else:
|
|
94
|
-
write =
|
|
103
|
+
write = (
|
|
104
|
+
constructor_write(**write_params) if constructor_write else constructor(**write_params)
|
|
105
|
+
)
|
|
95
106
|
return read, write
|
|
96
107
|
|
|
108
|
+
|
|
97
109
|
def generate_fields(**kwargs):
|
|
98
|
-
|
|
110
|
+
"""
|
|
99
111
|
This decorator will create two auto-generated attributes on the class `__read_fields__` and `__write_fields__`
|
|
100
112
|
that can be used in API endpoint inside `expect()` and `marshall_with()`.
|
|
101
|
-
|
|
113
|
+
"""
|
|
114
|
+
|
|
102
115
|
def wrapper(cls):
|
|
103
116
|
read_fields = {}
|
|
104
117
|
write_fields = {}
|
|
105
118
|
sortables = []
|
|
106
119
|
filterables = []
|
|
107
120
|
|
|
108
|
-
read_fields[
|
|
121
|
+
read_fields["id"] = restx_fields.String(required=True)
|
|
109
122
|
|
|
110
123
|
for key, field in cls._fields.items():
|
|
111
|
-
info = getattr(field,
|
|
112
|
-
if info is None:
|
|
124
|
+
info = getattr(field, "__additional_field_info__", None)
|
|
125
|
+
if info is None:
|
|
126
|
+
continue
|
|
113
127
|
|
|
114
|
-
if info.get(
|
|
128
|
+
if info.get("sortable", False):
|
|
115
129
|
sortables.append(key)
|
|
116
130
|
|
|
117
|
-
filterable = info.get(
|
|
131
|
+
filterable = info.get("filterable", None)
|
|
118
132
|
if filterable is not None:
|
|
119
|
-
if
|
|
120
|
-
filterable[
|
|
121
|
-
if
|
|
122
|
-
filterable[
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
filterable[
|
|
126
|
-
if isinstance(field, mongo_fields.ReferenceField) or (
|
|
127
|
-
|
|
133
|
+
if "key" not in filterable:
|
|
134
|
+
filterable["key"] = key
|
|
135
|
+
if "column" not in filterable:
|
|
136
|
+
filterable["column"] = key
|
|
137
|
+
|
|
138
|
+
if "constraints" not in filterable:
|
|
139
|
+
filterable["constraints"] = []
|
|
140
|
+
if isinstance(field, mongo_fields.ReferenceField) or (
|
|
141
|
+
isinstance(field, mongo_fields.ListField)
|
|
142
|
+
and isinstance(field.field, mongo_fields.ReferenceField)
|
|
143
|
+
):
|
|
144
|
+
filterable["constraints"].append("objectid")
|
|
128
145
|
|
|
129
146
|
# We may add more information later here:
|
|
130
147
|
# - type of mongo query to execute (right now only simple =)
|
|
@@ -139,78 +156,105 @@ def generate_fields(**kwargs):
|
|
|
139
156
|
write_fields[key] = write
|
|
140
157
|
|
|
141
158
|
# The goal of this loop is to fetch all functions (getters) of the class
|
|
142
|
-
# If a function has an `__additional_field_info__` attribute it means
|
|
159
|
+
# If a function has an `__additional_field_info__` attribute it means
|
|
143
160
|
# it has been decorated with `@function_field()` and should be included
|
|
144
161
|
# in the API response.
|
|
145
162
|
for method_name in dir(cls):
|
|
146
|
-
if method_name ==
|
|
147
|
-
|
|
148
|
-
if method_name
|
|
163
|
+
if method_name == "objects":
|
|
164
|
+
continue
|
|
165
|
+
if method_name.startswith("_"):
|
|
166
|
+
continue
|
|
167
|
+
if method_name in read_fields:
|
|
168
|
+
continue # Do not override if the attribute is also callable like for Extras
|
|
149
169
|
|
|
150
170
|
method = getattr(cls, method_name)
|
|
151
|
-
if not callable(method):
|
|
171
|
+
if not callable(method):
|
|
172
|
+
continue
|
|
152
173
|
|
|
153
|
-
info = getattr(method,
|
|
154
|
-
if info is None:
|
|
174
|
+
info = getattr(method, "__additional_field_info__", None)
|
|
175
|
+
if info is None:
|
|
176
|
+
continue
|
|
155
177
|
|
|
156
178
|
def make_lambda(method):
|
|
157
|
-
|
|
179
|
+
"""
|
|
158
180
|
Factory function to create a lambda with the correct scope.
|
|
159
|
-
If we don't have this factory function, the `method` will be the
|
|
181
|
+
If we don't have this factory function, the `method` will be the
|
|
160
182
|
last method assigned in this loop?
|
|
161
|
-
|
|
183
|
+
"""
|
|
162
184
|
return lambda o: method(o)
|
|
163
185
|
|
|
164
|
-
read_fields[method_name] = restx_fields.String(
|
|
165
|
-
|
|
186
|
+
read_fields[method_name] = restx_fields.String(
|
|
187
|
+
attribute=make_lambda(method), **{"readonly": True, **info}
|
|
188
|
+
)
|
|
166
189
|
|
|
167
190
|
cls.__read_fields__ = api.model(f"{cls.__name__} (read)", read_fields, **kwargs)
|
|
168
191
|
cls.__write_fields__ = api.model(f"{cls.__name__} (write)", write_fields, **kwargs)
|
|
169
192
|
|
|
170
|
-
mask = kwargs.pop(
|
|
193
|
+
mask = kwargs.pop("mask", None)
|
|
171
194
|
if mask is not None:
|
|
172
|
-
mask =
|
|
173
|
-
cls.__page_fields__ = api.model(
|
|
195
|
+
mask = "data{{{0}}},*".format(mask)
|
|
196
|
+
cls.__page_fields__ = api.model(
|
|
197
|
+
f"{cls.__name__}Page",
|
|
198
|
+
custom_restx_fields.pager(cls.__read_fields__),
|
|
199
|
+
mask=mask,
|
|
200
|
+
**kwargs,
|
|
201
|
+
)
|
|
174
202
|
|
|
175
203
|
# Parser for index sort/filters
|
|
176
|
-
paginable = kwargs.get(
|
|
204
|
+
paginable = kwargs.get("paginable", True)
|
|
177
205
|
parser = api.parser()
|
|
178
206
|
|
|
179
207
|
if paginable:
|
|
180
|
-
parser.add_argument(
|
|
181
|
-
|
|
182
|
-
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"page", type=int, location="args", default=1, help="The page to display"
|
|
210
|
+
)
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
"page_size", type=int, location="args", default=20, help="The page size"
|
|
213
|
+
)
|
|
214
|
+
|
|
183
215
|
if sortables:
|
|
184
|
-
choices = sortables + [
|
|
185
|
-
parser.add_argument(
|
|
216
|
+
choices = sortables + ["-" + k for k in sortables]
|
|
217
|
+
parser.add_argument(
|
|
218
|
+
"sort",
|
|
219
|
+
type=str,
|
|
220
|
+
location="args",
|
|
221
|
+
choices=choices,
|
|
222
|
+
help="The field (and direction) on which sorting apply",
|
|
223
|
+
)
|
|
186
224
|
|
|
187
225
|
for filterable in filterables:
|
|
188
|
-
parser.add_argument(filterable[
|
|
226
|
+
parser.add_argument(filterable["key"], type=str, location="args")
|
|
189
227
|
|
|
190
228
|
cls.__index_parser__ = parser
|
|
229
|
+
|
|
191
230
|
def apply_sort_filters_and_pagination(base_query):
|
|
192
231
|
args = cls.__index_parser__.parse_args()
|
|
193
232
|
|
|
194
|
-
if sortables and args[
|
|
195
|
-
base_query = base_query.order_by(args[
|
|
233
|
+
if sortables and args["sort"]:
|
|
234
|
+
base_query = base_query.order_by(args["sort"])
|
|
196
235
|
|
|
197
236
|
for filterable in filterables:
|
|
198
|
-
if args.get(filterable[
|
|
199
|
-
for constraint in filterable[
|
|
200
|
-
if constraint ==
|
|
237
|
+
if args.get(filterable["key"]):
|
|
238
|
+
for constraint in filterable["constraints"]:
|
|
239
|
+
if constraint == "objectid" and not ObjectId.is_valid(
|
|
240
|
+
args[filterable["key"]]
|
|
241
|
+
):
|
|
201
242
|
api.abort(400, f'`{filterable["key"]}` must be an identifier')
|
|
202
243
|
|
|
203
|
-
base_query = base_query.filter(
|
|
204
|
-
|
|
205
|
-
|
|
244
|
+
base_query = base_query.filter(
|
|
245
|
+
**{
|
|
246
|
+
filterable["column"]: args[filterable["key"]],
|
|
247
|
+
}
|
|
248
|
+
)
|
|
206
249
|
|
|
207
250
|
if paginable:
|
|
208
|
-
base_query = base_query.paginate(args[
|
|
251
|
+
base_query = base_query.paginate(args["page"], args["page_size"])
|
|
209
252
|
|
|
210
253
|
return base_query
|
|
211
254
|
|
|
212
255
|
cls.apply_sort_filters_and_pagination = apply_sort_filters_and_pagination
|
|
213
256
|
return cls
|
|
257
|
+
|
|
214
258
|
return wrapper
|
|
215
259
|
|
|
216
260
|
|
|
@@ -221,50 +265,53 @@ def function_field(**info):
|
|
|
221
265
|
|
|
222
266
|
return inner
|
|
223
267
|
|
|
268
|
+
|
|
224
269
|
def field(inner, **kwargs):
|
|
225
|
-
|
|
270
|
+
"""
|
|
226
271
|
Simple decorator to mark a field as visible for the API fields.
|
|
227
272
|
We can pass additional arguments that will be forward to the RestX field constructor.
|
|
228
|
-
|
|
273
|
+
"""
|
|
229
274
|
inner.__additional_field_info__ = kwargs
|
|
230
275
|
return inner
|
|
231
276
|
|
|
232
277
|
|
|
233
|
-
def patch(obj, request):
|
|
234
|
-
|
|
278
|
+
def patch(obj, request):
|
|
279
|
+
"""
|
|
235
280
|
Patch the object with the data from the request.
|
|
236
281
|
Only fields decorated with the `field()` decorator will be read (and not readonly).
|
|
237
|
-
|
|
282
|
+
"""
|
|
238
283
|
for key, value in request.json.items():
|
|
239
284
|
field = obj.__write_fields__.get(key)
|
|
240
285
|
if field is not None and not field.readonly:
|
|
241
286
|
model_attribute = getattr(obj.__class__, key)
|
|
242
|
-
if isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
287
|
+
if isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
288
|
+
model_attribute.field, mongoengine.fields.ReferenceField
|
|
289
|
+
):
|
|
243
290
|
# TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
|
|
244
291
|
value = [wrap_primary_key(key, model_attribute.field, id) for id in value]
|
|
245
292
|
if isinstance(model_attribute, mongoengine.fields.ReferenceField):
|
|
246
293
|
value = wrap_primary_key(key, model_attribute, value)
|
|
247
294
|
|
|
248
|
-
|
|
249
|
-
info = getattr(model_attribute, '__additional_field_info__', {})
|
|
295
|
+
info = getattr(model_attribute, "__additional_field_info__", {})
|
|
250
296
|
|
|
251
297
|
# `check` field attribute allows to do validation from the request before setting
|
|
252
298
|
# the attribute
|
|
253
|
-
check = info.get(
|
|
299
|
+
check = info.get("check", None)
|
|
254
300
|
if check is not None:
|
|
255
|
-
check(**{key: value})
|
|
301
|
+
check(**{key: value}) # TODO add other model attributes in function parameters
|
|
256
302
|
|
|
257
303
|
setattr(obj, key, value)
|
|
258
304
|
|
|
259
305
|
return obj
|
|
260
306
|
|
|
307
|
+
|
|
261
308
|
def wrap_primary_key(field_name: str, foreign_field: mongoengine.fields.ReferenceField, value: str):
|
|
262
|
-
|
|
263
|
-
We need to wrap the `String` inside an `ObjectId` most of the time. If the foreign ID is a `String` we need to get
|
|
309
|
+
"""
|
|
310
|
+
We need to wrap the `String` inside an `ObjectId` most of the time. If the foreign ID is a `String` we need to get
|
|
264
311
|
a `DBRef` from the database.
|
|
265
312
|
|
|
266
313
|
TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
|
|
267
|
-
|
|
314
|
+
"""
|
|
268
315
|
document_type = foreign_field.document_type()
|
|
269
316
|
id_field_name = document_type.__class__._meta["id_field"]
|
|
270
317
|
|
|
@@ -275,7 +322,7 @@ def wrap_primary_key(field_name: str, foreign_field: mongoengine.fields.Referenc
|
|
|
275
322
|
foreign_document = document_type.__class__.objects(**{id_field_name: value}).first()
|
|
276
323
|
if foreign_document is None:
|
|
277
324
|
raise FieldValidationError(field=field_name, message=f"Unknown reference '{value}'")
|
|
278
|
-
|
|
325
|
+
|
|
279
326
|
if isinstance(id_field, mongoengine.fields.ObjectIdField):
|
|
280
327
|
return ObjectId(value)
|
|
281
328
|
elif isinstance(id_field, mongoengine.fields.StringField):
|
|
@@ -288,5 +335,6 @@ def wrap_primary_key(field_name: str, foreign_field: mongoengine.fields.Referenc
|
|
|
288
335
|
# … but it may be important to check before-hand that the reference point to a correct document.
|
|
289
336
|
return foreign_document.to_dbref()
|
|
290
337
|
else:
|
|
291
|
-
raise ValueError(
|
|
292
|
-
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"Unknown ID field type {id_field.__class__} for {document_type.__class__} (ID field name is {id_field_name}, value was {value})"
|
|
340
|
+
)
|
udata/app.py
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import bson
|
|
2
1
|
import datetime
|
|
2
|
+
import importlib
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
import importlib
|
|
6
5
|
import types
|
|
6
|
+
from os.path import abspath, dirname, exists, isfile, join
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from flask import
|
|
11
|
-
Flask, abort, g, send_from_directory, json, Blueprint as BaseBlueprint,
|
|
12
|
-
make_response
|
|
13
|
-
)
|
|
8
|
+
import bson
|
|
9
|
+
from flask import Blueprint as BaseBlueprint
|
|
10
|
+
from flask import Flask, abort, g, json, make_response, send_from_directory
|
|
14
11
|
from flask_caching import Cache
|
|
15
|
-
|
|
16
12
|
from flask_wtf.csrf import CSRFProtect
|
|
17
13
|
from speaklater import is_lazy_string
|
|
18
14
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
19
15
|
|
|
20
|
-
from udata import entrypoints
|
|
16
|
+
from udata import cors, entrypoints
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
APP_NAME = __name__.split('.')[0]
|
|
18
|
+
APP_NAME = __name__.split(".")[0]
|
|
24
19
|
ROOT_DIR = abspath(join(dirname(__file__)))
|
|
25
20
|
|
|
26
21
|
log = logging.getLogger(__name__)
|
|
@@ -32,81 +27,78 @@ csrf = CSRFProtect()
|
|
|
32
27
|
def send_static(directory, filename, cache_timeout):
|
|
33
28
|
out = send_from_directory(directory, filename, cache_timeout=cache_timeout)
|
|
34
29
|
response = make_response(out)
|
|
35
|
-
response.headers[
|
|
36
|
-
response.headers[
|
|
30
|
+
response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
|
31
|
+
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
37
32
|
return response
|
|
38
33
|
|
|
39
34
|
|
|
40
35
|
class UDataApp(Flask):
|
|
41
|
-
debug_log_format =
|
|
36
|
+
debug_log_format = "[%(levelname)s][%(name)s:%(lineno)d] %(message)s"
|
|
42
37
|
|
|
43
38
|
# Keep track of static dirs given as register_blueprint argument
|
|
44
39
|
static_prefixes = {}
|
|
45
40
|
|
|
46
41
|
def send_static_file(self, filename):
|
|
47
|
-
|
|
42
|
+
"""
|
|
48
43
|
Override default static handling:
|
|
49
44
|
- raises 404 if not debug
|
|
50
45
|
- handle static aliases
|
|
51
|
-
|
|
46
|
+
"""
|
|
52
47
|
if not self.debug:
|
|
53
|
-
self.logger.error(
|
|
48
|
+
self.logger.error("Static files are only served in debug")
|
|
54
49
|
abort(404)
|
|
55
50
|
|
|
56
51
|
cache_timeout = self.get_send_file_max_age(filename)
|
|
57
52
|
|
|
58
53
|
# Default behavior
|
|
59
54
|
if isfile(join(self.static_folder, filename)):
|
|
60
|
-
return send_static(self.static_folder, filename,
|
|
61
|
-
cache_timeout=cache_timeout)
|
|
55
|
+
return send_static(self.static_folder, filename, cache_timeout=cache_timeout)
|
|
62
56
|
|
|
63
57
|
# Handle aliases
|
|
64
|
-
for prefix, directory in self.config.get(
|
|
58
|
+
for prefix, directory in self.config.get("STATIC_DIRS", tuple()):
|
|
65
59
|
if filename.startswith(prefix):
|
|
66
|
-
real_filename = filename[len(prefix):]
|
|
67
|
-
if real_filename.startswith(
|
|
60
|
+
real_filename = filename[len(prefix) :]
|
|
61
|
+
if real_filename.startswith("/"):
|
|
68
62
|
real_filename = real_filename[1:]
|
|
69
63
|
if isfile(join(directory, real_filename)):
|
|
70
|
-
return send_static(directory, real_filename,
|
|
71
|
-
cache_timeout=cache_timeout)
|
|
64
|
+
return send_static(directory, real_filename, cache_timeout=cache_timeout)
|
|
72
65
|
abort(404)
|
|
73
66
|
|
|
74
67
|
def handle_http_exception(self, e):
|
|
75
68
|
# Make exception/HTTPError available for context processors
|
|
76
|
-
if
|
|
69
|
+
if "error" not in g:
|
|
77
70
|
g.error = e
|
|
78
71
|
return super(UDataApp, self).handle_http_exception(e)
|
|
79
72
|
|
|
80
73
|
def register_blueprint(self, blueprint, **kwargs):
|
|
81
|
-
|
|
82
74
|
if blueprint.name in self.blueprints:
|
|
83
75
|
# TODO: remove this warning and let Flask return a ValueError once
|
|
84
76
|
# we can set a custom blueprint name in Flask-storage
|
|
85
|
-
self.logger.warning(
|
|
77
|
+
self.logger.warning("Blueprint already loaded")
|
|
86
78
|
return self.blueprints[blueprint.name]
|
|
87
79
|
|
|
88
|
-
if blueprint.has_static_folder and
|
|
89
|
-
self.static_prefixes[blueprint.name] = kwargs[
|
|
80
|
+
if blueprint.has_static_folder and "url_prefix" in kwargs:
|
|
81
|
+
self.static_prefixes[blueprint.name] = kwargs["url_prefix"]
|
|
90
82
|
return super(UDataApp, self).register_blueprint(blueprint, **kwargs)
|
|
91
83
|
|
|
92
84
|
|
|
93
85
|
class Blueprint(BaseBlueprint):
|
|
94
|
-
|
|
86
|
+
"""A blueprint allowing to decorate class too"""
|
|
87
|
+
|
|
95
88
|
def route(self, rule, **options):
|
|
96
89
|
def wrapper(func_or_cls):
|
|
97
|
-
endpoint = str(options.pop(
|
|
90
|
+
endpoint = str(options.pop("endpoint", func_or_cls.__name__))
|
|
98
91
|
if isinstance(func_or_cls, types.FunctionType):
|
|
99
92
|
self.add_url_rule(rule, endpoint, func_or_cls, **options)
|
|
100
93
|
else:
|
|
101
|
-
self.add_url_rule(rule,
|
|
102
|
-
view_func=func_or_cls.as_view(endpoint),
|
|
103
|
-
**options)
|
|
94
|
+
self.add_url_rule(rule, view_func=func_or_cls.as_view(endpoint), **options)
|
|
104
95
|
return func_or_cls
|
|
96
|
+
|
|
105
97
|
return wrapper
|
|
106
98
|
|
|
107
99
|
|
|
108
100
|
class UDataJsonEncoder(json.JSONEncoder):
|
|
109
|
-
|
|
101
|
+
"""
|
|
110
102
|
A JSONEncoder subclass to encode unsupported types:
|
|
111
103
|
|
|
112
104
|
- ObjectId
|
|
@@ -115,7 +107,8 @@ class UDataJsonEncoder(json.JSONEncoder):
|
|
|
115
107
|
|
|
116
108
|
Handle special serialize() method and _data attribute.
|
|
117
109
|
Ensure an app context is always present.
|
|
118
|
-
|
|
110
|
+
"""
|
|
111
|
+
|
|
119
112
|
def default(self, obj):
|
|
120
113
|
if is_lazy_string(obj):
|
|
121
114
|
return str(obj)
|
|
@@ -123,12 +116,12 @@ class UDataJsonEncoder(json.JSONEncoder):
|
|
|
123
116
|
return str(obj)
|
|
124
117
|
elif isinstance(obj, datetime.datetime):
|
|
125
118
|
return obj.isoformat()
|
|
126
|
-
elif hasattr(obj,
|
|
119
|
+
elif hasattr(obj, "to_dict"):
|
|
127
120
|
return obj.to_dict()
|
|
128
|
-
elif hasattr(obj,
|
|
121
|
+
elif hasattr(obj, "serialize"):
|
|
129
122
|
return obj.serialize()
|
|
130
123
|
# Serialize Raw data for Document and EmbeddedDocument.
|
|
131
|
-
elif hasattr(obj,
|
|
124
|
+
elif hasattr(obj, "_data"):
|
|
132
125
|
return obj._data
|
|
133
126
|
return super(UDataJsonEncoder, self).default(obj)
|
|
134
127
|
|
|
@@ -136,12 +129,12 @@ class UDataJsonEncoder(json.JSONEncoder):
|
|
|
136
129
|
# These loggers are very verbose
|
|
137
130
|
# We need to put them in WARNING level
|
|
138
131
|
# even if the main level is INFO or DEBUG
|
|
139
|
-
VERBOSE_LOGGERS =
|
|
132
|
+
VERBOSE_LOGGERS = ("requests",)
|
|
140
133
|
|
|
141
134
|
|
|
142
135
|
def init_logging(app):
|
|
143
136
|
logging.captureWarnings(True) # Display warnings
|
|
144
|
-
debug = app.debug or app.config.get(
|
|
137
|
+
debug = app.debug or app.config.get("TESTING")
|
|
145
138
|
log_level = logging.DEBUG if debug else logging.WARNING
|
|
146
139
|
app.logger.setLevel(log_level)
|
|
147
140
|
for name in entrypoints.get_roots(): # Entrypoints loggers
|
|
@@ -151,13 +144,12 @@ def init_logging(app):
|
|
|
151
144
|
return app
|
|
152
145
|
|
|
153
146
|
|
|
154
|
-
def create_app(config=
|
|
155
|
-
|
|
156
|
-
'''Factory for a minimal application'''
|
|
147
|
+
def create_app(config="udata.settings.Defaults", override=None, init_logging=init_logging):
|
|
148
|
+
"""Factory for a minimal application"""
|
|
157
149
|
app = UDataApp(APP_NAME)
|
|
158
150
|
app.config.from_object(config)
|
|
159
151
|
|
|
160
|
-
settings = os.environ.get(
|
|
152
|
+
settings = os.environ.get("UDATA_SETTINGS", join(os.getcwd(), "udata.cfg"))
|
|
161
153
|
if exists(settings):
|
|
162
154
|
app.settings_file = settings # Keep track of loaded settings for diagnostic
|
|
163
155
|
app.config.from_pyfile(settings)
|
|
@@ -167,24 +159,24 @@ def create_app(config='udata.settings.Defaults', override=None,
|
|
|
167
159
|
|
|
168
160
|
# Loads defaults from plugins
|
|
169
161
|
for pkg in entrypoints.get_roots(app):
|
|
170
|
-
if pkg ==
|
|
162
|
+
if pkg == "udata":
|
|
171
163
|
continue # Defaults are already loaded
|
|
172
|
-
module =
|
|
164
|
+
module = "{}.settings".format(pkg)
|
|
173
165
|
try:
|
|
174
166
|
settings = importlib.import_module(module)
|
|
175
167
|
except ImportError:
|
|
176
168
|
continue
|
|
177
169
|
for key, default in settings.__dict__.items():
|
|
178
|
-
if key.startswith(
|
|
170
|
+
if key.startswith("__"):
|
|
179
171
|
continue
|
|
180
172
|
app.config.setdefault(key, default)
|
|
181
173
|
|
|
182
174
|
app.json_encoder = UDataJsonEncoder
|
|
183
175
|
|
|
184
176
|
# `ujson` doesn't support `cls` parameter https://github.com/ultrajson/ultrajson/issues/124
|
|
185
|
-
app.config[
|
|
177
|
+
app.config["RESTX_JSON"] = {"cls": UDataJsonEncoder}
|
|
186
178
|
|
|
187
|
-
app.debug = app.config[
|
|
179
|
+
app.debug = app.config["DEBUG"] and not app.config["TESTING"]
|
|
188
180
|
|
|
189
181
|
app.wsgi_app = ProxyFix(app.wsgi_app)
|
|
190
182
|
|
|
@@ -195,7 +187,7 @@ def create_app(config='udata.settings.Defaults', override=None,
|
|
|
195
187
|
|
|
196
188
|
|
|
197
189
|
def standalone(app):
|
|
198
|
-
|
|
190
|
+
"""Factory for an all in one application"""
|
|
199
191
|
from udata import api, core, frontend
|
|
200
192
|
|
|
201
193
|
core.init_app(app)
|
|
@@ -209,9 +201,20 @@ def standalone(app):
|
|
|
209
201
|
|
|
210
202
|
def register_extensions(app):
|
|
211
203
|
from udata import (
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
auth,
|
|
205
|
+
i18n,
|
|
206
|
+
mail,
|
|
207
|
+
models,
|
|
208
|
+
mongo,
|
|
209
|
+
notifications,
|
|
210
|
+
routing,
|
|
211
|
+
search,
|
|
212
|
+
sentry,
|
|
213
|
+
sitemap,
|
|
214
|
+
tasks,
|
|
214
215
|
)
|
|
216
|
+
|
|
217
|
+
cors.init_app(app)
|
|
215
218
|
tasks.init_app(app)
|
|
216
219
|
i18n.init_app(app)
|
|
217
220
|
mongo.init_app(app)
|
|
@@ -232,5 +235,5 @@ def register_features(app):
|
|
|
232
235
|
|
|
233
236
|
notifications.init_app(app)
|
|
234
237
|
|
|
235
|
-
for ep in entrypoints.get_enabled(
|
|
238
|
+
for ep in entrypoints.get_enabled("udata.plugins", app).values():
|
|
236
239
|
ep.init_app(app)
|