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/spatial/commands.py
CHANGED
|
@@ -2,16 +2,14 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import signal
|
|
4
4
|
import sys
|
|
5
|
-
|
|
6
5
|
from collections import Counter
|
|
7
6
|
from contextlib import contextmanager
|
|
8
7
|
from datetime import datetime
|
|
9
8
|
from textwrap import dedent
|
|
10
|
-
import requests
|
|
11
9
|
|
|
12
10
|
import click
|
|
11
|
+
import requests
|
|
13
12
|
import slugify
|
|
14
|
-
|
|
15
13
|
from mongoengine import errors
|
|
16
14
|
from mongoengine.context_managers import switch_collection
|
|
17
15
|
|
|
@@ -23,22 +21,22 @@ from udata.core.spatial.models import GeoLevel, GeoZone, SpatialCoverage
|
|
|
23
21
|
log = logging.getLogger(__name__)
|
|
24
22
|
|
|
25
23
|
|
|
26
|
-
DEFAULT_GEOZONES_FILE =
|
|
27
|
-
|
|
24
|
+
DEFAULT_GEOZONES_FILE = (
|
|
25
|
+
"https://www.data.gouv.fr/fr/datasets/r/a1bb263a-6cc7-4871-ab4f-2470235a67bf"
|
|
26
|
+
)
|
|
27
|
+
DEFAULT_LEVELS_FILE = "https://www.data.gouv.fr/fr/datasets/r/e0206442-78b3-4a00-b71c-c065d20561c8"
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
@cli.group(
|
|
30
|
+
@cli.group("spatial")
|
|
31
31
|
def grp():
|
|
32
|
-
|
|
32
|
+
"""Geospatial related operations"""
|
|
33
33
|
pass
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def load_levels(col, json_levels):
|
|
37
37
|
for i, level in enumerate(json_levels):
|
|
38
|
-
col.objects(id=level[
|
|
39
|
-
upsert=True,
|
|
40
|
-
set__name=level['label'],
|
|
41
|
-
set__admin_level=level.get('admin_level')
|
|
38
|
+
col.objects(id=level["id"]).modify(
|
|
39
|
+
upsert=True, set__name=level["label"], set__admin_level=level.get("admin_level")
|
|
42
40
|
)
|
|
43
41
|
return i
|
|
44
42
|
|
|
@@ -46,72 +44,71 @@ def load_levels(col, json_levels):
|
|
|
46
44
|
def load_zones(col, json_geozones):
|
|
47
45
|
loaded_geozones = 0
|
|
48
46
|
for _, geozone in enumerate(json_geozones):
|
|
49
|
-
if geozone.get(
|
|
47
|
+
if geozone.get("is_deleted", False):
|
|
50
48
|
continue
|
|
51
49
|
params = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
"slug": slugify.slugify(geozone["nom"], separator="-"),
|
|
51
|
+
"level": str(geozone["level"]),
|
|
52
|
+
"code": geozone["codeINSEE"],
|
|
53
|
+
"name": geozone["nom"],
|
|
54
|
+
"uri": geozone["uri"],
|
|
57
55
|
}
|
|
58
56
|
try:
|
|
59
|
-
col.objects(id=geozone[
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
col.objects(id=geozone["_id"]).modify(
|
|
58
|
+
upsert=True, **{"set__{0}".format(k): v for k, v in params.items()}
|
|
59
|
+
)
|
|
62
60
|
loaded_geozones += 1
|
|
63
61
|
except errors.ValidationError as e:
|
|
64
|
-
log.warning(
|
|
65
|
-
e, geozone['nom'], params)
|
|
62
|
+
log.warning("Validation error (%s) for %s with %s", e, geozone["nom"], params)
|
|
66
63
|
continue
|
|
67
64
|
return loaded_geozones
|
|
68
65
|
|
|
69
66
|
|
|
70
67
|
@contextmanager
|
|
71
68
|
def handle_error(to_delete=None):
|
|
72
|
-
|
|
69
|
+
"""
|
|
73
70
|
Handle errors while loading.
|
|
74
71
|
In case of error, properly log it, remove the temporary files and collections and exit.
|
|
75
72
|
If `to_delete` is given a collection, it will be deleted deleted.
|
|
76
|
-
|
|
73
|
+
"""
|
|
77
74
|
# Handle keyboard interrupt
|
|
78
75
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
79
76
|
signal.signal(signal.SIGTERM, signal.default_int_handler)
|
|
80
77
|
try:
|
|
81
78
|
yield
|
|
82
79
|
except KeyboardInterrupt:
|
|
83
|
-
print(
|
|
84
|
-
log.warning(
|
|
80
|
+
print("") # Proper warning message under the "^C" display
|
|
81
|
+
log.warning("Interrupted by signal")
|
|
85
82
|
except Exception as e:
|
|
86
83
|
log.error(e)
|
|
87
84
|
else:
|
|
88
85
|
return # Nothing to do in case of success
|
|
89
86
|
if to_delete:
|
|
90
|
-
log.info(
|
|
87
|
+
log.info("Removing temporary collection %s", to_delete._get_collection_name())
|
|
91
88
|
to_delete.drop_collection()
|
|
92
89
|
sys.exit(-1)
|
|
93
90
|
|
|
94
91
|
|
|
95
92
|
@grp.command()
|
|
96
|
-
@click.argument(
|
|
97
|
-
@click.argument(
|
|
98
|
-
@click.option(
|
|
93
|
+
@click.argument("geozones-file", default=DEFAULT_GEOZONES_FILE)
|
|
94
|
+
@click.argument("levels-file", default=DEFAULT_LEVELS_FILE)
|
|
95
|
+
@click.option("-d", "--drop", is_flag=True, help="Drop existing data")
|
|
99
96
|
def load(geozones_file, levels_file, drop=False):
|
|
100
|
-
|
|
97
|
+
"""
|
|
101
98
|
Load a geozones archive from <filename>
|
|
102
99
|
|
|
103
100
|
<filename> can be either a local path or a remote URL.
|
|
104
|
-
|
|
105
|
-
log.info(
|
|
106
|
-
if levels_file.startswith(
|
|
101
|
+
"""
|
|
102
|
+
log.info("Loading GeoZones levels")
|
|
103
|
+
if levels_file.startswith("http"):
|
|
107
104
|
json_levels = requests.get(levels_file).json()
|
|
108
105
|
else:
|
|
109
106
|
with open(levels_file) as f:
|
|
110
107
|
json_levels = json.load(f)
|
|
111
108
|
|
|
112
|
-
ts = datetime.utcnow().isoformat().replace(
|
|
109
|
+
ts = datetime.utcnow().isoformat().replace("-", "").replace(":", "").split(".")[0]
|
|
113
110
|
if drop and GeoLevel.objects.count():
|
|
114
|
-
name =
|
|
111
|
+
name = "_".join((GeoLevel._get_collection_name(), ts))
|
|
115
112
|
target = GeoLevel._get_collection_name()
|
|
116
113
|
with switch_collection(GeoLevel, name):
|
|
117
114
|
with handle_error(GeoLevel):
|
|
@@ -120,17 +117,17 @@ def load(geozones_file, levels_file, drop=False):
|
|
|
120
117
|
else:
|
|
121
118
|
with handle_error():
|
|
122
119
|
total = load_levels(GeoLevel, json_levels)
|
|
123
|
-
log.info(
|
|
120
|
+
log.info("Loaded {total} levels".format(total=total))
|
|
124
121
|
|
|
125
|
-
log.info(
|
|
126
|
-
if geozones_file.startswith(
|
|
122
|
+
log.info("Loading Zones")
|
|
123
|
+
if geozones_file.startswith("http"):
|
|
127
124
|
json_geozones = requests.get(geozones_file).json()
|
|
128
125
|
else:
|
|
129
126
|
with open(geozones_file) as f:
|
|
130
127
|
json_geozones = json.load(f)
|
|
131
128
|
|
|
132
129
|
if drop and GeoZone.objects.count():
|
|
133
|
-
name =
|
|
130
|
+
name = "_".join((GeoZone._get_collection_name(), ts))
|
|
134
131
|
target = GeoZone._get_collection_name()
|
|
135
132
|
with switch_collection(GeoZone, name):
|
|
136
133
|
with handle_error(GeoZone):
|
|
@@ -139,35 +136,35 @@ def load(geozones_file, levels_file, drop=False):
|
|
|
139
136
|
else:
|
|
140
137
|
with handle_error():
|
|
141
138
|
total = load_zones(GeoZone, json_geozones)
|
|
142
|
-
log.info(
|
|
139
|
+
log.info("Loaded {total} zones".format(total=total))
|
|
143
140
|
|
|
144
|
-
log.info(
|
|
141
|
+
log.info("Clean removed geozones in datasets")
|
|
145
142
|
count = fixup_removed_geozone()
|
|
146
|
-
log.info(f
|
|
143
|
+
log.info(f"{count} geozones removed from datasets")
|
|
147
144
|
|
|
148
145
|
|
|
149
146
|
@grp.command()
|
|
150
147
|
def migrate():
|
|
151
|
-
|
|
148
|
+
"""
|
|
152
149
|
Migrate zones from old to new ids in datasets.
|
|
153
150
|
|
|
154
151
|
Should only be run once with the new version of geozones w/ geohisto.
|
|
155
|
-
|
|
156
|
-
counter = Counter([
|
|
157
|
-
qs = GeoZone.objects.only(
|
|
152
|
+
"""
|
|
153
|
+
counter = Counter(["zones", "datasets"])
|
|
154
|
+
qs = GeoZone.objects.only("id", "level")
|
|
158
155
|
# Fetch datasets with non-empty spatial zones
|
|
159
156
|
for dataset in Dataset.objects(spatial__zones__gt=[]):
|
|
160
|
-
counter[
|
|
157
|
+
counter["datasets"] += 1
|
|
161
158
|
new_zones = []
|
|
162
159
|
for current_zone in dataset.spatial.zones:
|
|
163
|
-
counter[
|
|
160
|
+
counter["zones"] += 1
|
|
164
161
|
|
|
165
162
|
level, code = geoids.parse(current_zone.id)
|
|
166
163
|
zone = qs(level=level, code=code).first() or qs(code=code).first()
|
|
167
164
|
|
|
168
165
|
if not zone:
|
|
169
|
-
log.warning(
|
|
170
|
-
counter[
|
|
166
|
+
log.warning("No match for %s: skipped", current_zone.id)
|
|
167
|
+
counter["skipped"] += 1
|
|
171
168
|
continue
|
|
172
169
|
|
|
173
170
|
new_zones.append(zone.id)
|
|
@@ -175,30 +172,37 @@ def migrate():
|
|
|
175
172
|
|
|
176
173
|
# Update dataset with new spatial zones
|
|
177
174
|
dataset.update(
|
|
178
|
-
spatial=SpatialCoverage(
|
|
179
|
-
granularity=dataset.spatial.granularity,
|
|
180
|
-
zones=list(new_zones)
|
|
181
|
-
)
|
|
175
|
+
spatial=SpatialCoverage(granularity=dataset.spatial.granularity, zones=list(new_zones))
|
|
182
176
|
)
|
|
183
177
|
|
|
184
|
-
level_summary =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
level_summary = "\n".join(
|
|
179
|
+
[
|
|
180
|
+
" - {0}: {1}".format(l.id, counter[l.id])
|
|
181
|
+
for l in GeoLevel.objects.order_by("admin_level")
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
summary = "\n".join(
|
|
185
|
+
[
|
|
186
|
+
dedent(
|
|
187
|
+
"""\
|
|
189
188
|
Summary
|
|
190
189
|
=======
|
|
191
190
|
Processed {zones} zones in {datasets} datasets:\
|
|
192
|
-
|
|
191
|
+
""".format(level_summary, **counter)
|
|
192
|
+
),
|
|
193
|
+
level_summary,
|
|
194
|
+
]
|
|
195
|
+
)
|
|
193
196
|
log.info(summary)
|
|
194
|
-
log.info(
|
|
197
|
+
log.info("Done")
|
|
198
|
+
|
|
195
199
|
|
|
196
200
|
def fixup_removed_geozone():
|
|
197
201
|
count = 0
|
|
198
202
|
all_datasets = Dataset.objects(spatial__zones__0__exists=True).timeout(False)
|
|
199
203
|
for dataset in all_datasets:
|
|
200
204
|
zones = dataset.spatial.zones
|
|
201
|
-
new_zones = [z for z in zones if getattr(z,
|
|
205
|
+
new_zones = [z for z in zones if getattr(z, "name", None) is not None]
|
|
202
206
|
|
|
203
207
|
if len(new_zones) < len(zones):
|
|
204
208
|
log.debug(f"Removing deleted zones from dataset '{dataset.title}'")
|
|
@@ -207,4 +211,3 @@ def fixup_removed_geozone():
|
|
|
207
211
|
dataset.save()
|
|
208
212
|
|
|
209
213
|
return count
|
|
210
|
-
|
udata/core/spatial/constants.py
CHANGED
udata/core/spatial/factories.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import factory
|
|
2
|
-
|
|
3
2
|
from faker.providers import BaseProvider
|
|
4
|
-
|
|
5
3
|
from geojson.utils import generate_random
|
|
6
4
|
|
|
7
5
|
from udata.factories import DateRangeFactory, ModelFactory
|
|
@@ -13,90 +11,75 @@ from .models import GeoLevel, GeoZone, SpatialCoverage, spatial_granularities
|
|
|
13
11
|
|
|
14
12
|
@faker_provider
|
|
15
13
|
class GeoJsonProvider(BaseProvider):
|
|
16
|
-
|
|
14
|
+
"""A Fake GeoJSON provider"""
|
|
17
15
|
|
|
18
16
|
def random_range(self, min=2, max=5):
|
|
19
17
|
return range(self.random_int(min, max))
|
|
20
18
|
|
|
21
19
|
def point(self):
|
|
22
|
-
return generate_random(
|
|
20
|
+
return generate_random("Point")
|
|
23
21
|
|
|
24
22
|
def linestring(self):
|
|
25
|
-
return generate_random(
|
|
23
|
+
return generate_random("LineString")
|
|
26
24
|
|
|
27
25
|
def polygon(self):
|
|
28
|
-
return generate_random(
|
|
26
|
+
return generate_random("Polygon")
|
|
29
27
|
|
|
30
28
|
def multipoint(self):
|
|
31
|
-
coordinates = [
|
|
32
|
-
generate_random('Point')['coordinates']
|
|
33
|
-
for _ in self.random_range()
|
|
34
|
-
]
|
|
29
|
+
coordinates = [generate_random("Point")["coordinates"] for _ in self.random_range()]
|
|
35
30
|
|
|
36
|
-
return {
|
|
37
|
-
'type': 'MultiPoint',
|
|
38
|
-
'coordinates': coordinates
|
|
39
|
-
}
|
|
31
|
+
return {"type": "MultiPoint", "coordinates": coordinates}
|
|
40
32
|
|
|
41
33
|
def multilinestring(self):
|
|
42
|
-
coordinates = [
|
|
43
|
-
generate_random('LineString')['coordinates']
|
|
44
|
-
for _ in self.random_range()
|
|
45
|
-
]
|
|
34
|
+
coordinates = [generate_random("LineString")["coordinates"] for _ in self.random_range()]
|
|
46
35
|
|
|
47
|
-
return {
|
|
48
|
-
'type': 'MultiLineString',
|
|
49
|
-
'coordinates': coordinates
|
|
50
|
-
}
|
|
36
|
+
return {"type": "MultiLineString", "coordinates": coordinates}
|
|
51
37
|
|
|
52
38
|
def multipolygon(self):
|
|
53
|
-
coordinates = [
|
|
54
|
-
generate_random('Polygon')['coordinates']
|
|
55
|
-
for _ in self.random_range()
|
|
56
|
-
]
|
|
39
|
+
coordinates = [generate_random("Polygon")["coordinates"] for _ in self.random_range()]
|
|
57
40
|
|
|
58
|
-
return {
|
|
59
|
-
'type': 'MultiPolygon',
|
|
60
|
-
'coordinates': coordinates
|
|
61
|
-
}
|
|
41
|
+
return {"type": "MultiPolygon", "coordinates": coordinates}
|
|
62
42
|
|
|
63
43
|
def geometry_collection(self):
|
|
64
44
|
element_factories = [
|
|
65
|
-
self.point,
|
|
66
|
-
self.
|
|
45
|
+
self.point,
|
|
46
|
+
self.linestring,
|
|
47
|
+
self.polygon,
|
|
48
|
+
self.multipoint,
|
|
49
|
+
self.multilinestring,
|
|
50
|
+
self.multipolygon,
|
|
67
51
|
]
|
|
68
52
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
self.random_element(element_factories)()
|
|
72
|
-
for _ in self.random_range()
|
|
73
|
-
]
|
|
53
|
+
"type": "GeometryCollection",
|
|
54
|
+
"geometries": [self.random_element(element_factories)() for _ in self.random_range()],
|
|
74
55
|
}
|
|
75
56
|
|
|
76
57
|
def feature(self):
|
|
77
58
|
element_factories = [
|
|
78
|
-
self.point,
|
|
79
|
-
self.
|
|
59
|
+
self.point,
|
|
60
|
+
self.linestring,
|
|
61
|
+
self.polygon,
|
|
62
|
+
self.multipoint,
|
|
63
|
+
self.multilinestring,
|
|
64
|
+
self.multipolygon,
|
|
80
65
|
]
|
|
81
66
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
"type": "Feature",
|
|
68
|
+
"geometry": self.random_element(element_factories)(),
|
|
69
|
+
"properties": {},
|
|
85
70
|
}
|
|
86
71
|
|
|
87
72
|
def feature_collection(self):
|
|
88
73
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
74
|
+
"type": "FeatureCollection",
|
|
75
|
+
"features": [self.feature() for _ in self.random_range()],
|
|
91
76
|
}
|
|
92
77
|
|
|
93
78
|
|
|
94
79
|
@faker_provider
|
|
95
80
|
class SpatialProvider(BaseProvider):
|
|
96
81
|
def spatial_granularity(self):
|
|
97
|
-
return self.generator.random_element([
|
|
98
|
-
row[0] for row in spatial_granularities
|
|
99
|
-
])
|
|
82
|
+
return self.generator.random_element([row[0] for row in spatial_granularities])
|
|
100
83
|
|
|
101
84
|
|
|
102
85
|
class GeoZoneFactory(ModelFactory):
|
|
@@ -104,10 +87,10 @@ class GeoZoneFactory(ModelFactory):
|
|
|
104
87
|
model = GeoZone
|
|
105
88
|
|
|
106
89
|
id = factory.LazyAttribute(geoids.from_zone)
|
|
107
|
-
name = factory.Faker(
|
|
108
|
-
slug = factory.Faker(
|
|
109
|
-
code = factory.Faker(
|
|
110
|
-
uri = factory.Faker(
|
|
90
|
+
name = factory.Faker("city")
|
|
91
|
+
slug = factory.Faker("slug")
|
|
92
|
+
code = factory.Faker("zipcode")
|
|
93
|
+
uri = factory.Faker("url")
|
|
111
94
|
level = factory.LazyAttribute(lambda o: GeoLevelFactory().id)
|
|
112
95
|
|
|
113
96
|
|
|
@@ -116,12 +99,12 @@ class SpatialCoverageFactory(ModelFactory):
|
|
|
116
99
|
model = SpatialCoverage
|
|
117
100
|
|
|
118
101
|
zones = factory.LazyAttribute(lambda o: [GeoZoneFactory()])
|
|
119
|
-
granularity = factory.Faker(
|
|
102
|
+
granularity = factory.Faker("spatial_granularity")
|
|
120
103
|
|
|
121
104
|
|
|
122
105
|
class GeoLevelFactory(ModelFactory):
|
|
123
106
|
class Meta:
|
|
124
107
|
model = GeoLevel
|
|
125
108
|
|
|
126
|
-
id = factory.Faker(
|
|
127
|
-
name = factory.Faker(
|
|
109
|
+
id = factory.Faker("unique_string")
|
|
110
|
+
name = factory.Faker("name")
|
udata/core/spatial/forms.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import geojson
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import geojson
|
|
5
|
+
|
|
6
|
+
from udata.forms import ModelForm, validators, widgets
|
|
7
|
+
from udata.forms.fields import Field, FormField, ModelList, SelectField
|
|
7
8
|
from udata.i18n import lazy_gettext as _
|
|
8
9
|
|
|
9
10
|
from .models import GeoZone, SpatialCoverage, spatial_granularities
|
|
@@ -12,15 +13,14 @@ log = logging.getLogger(__name__)
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ZonesAutocompleter(widgets.TextInput):
|
|
15
|
-
classes =
|
|
16
|
+
classes = "zone-completer"
|
|
16
17
|
|
|
17
18
|
def __call__(self, field, **kwargs):
|
|
18
|
-
|
|
19
|
+
"""Store the values as JSON to prefeed selectize"""
|
|
19
20
|
if field.data:
|
|
20
|
-
kwargs[
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} for zone in field.data])
|
|
21
|
+
kwargs["data-values"] = json.dumps(
|
|
22
|
+
[{"id": zone.id, "name": zone.name} for zone in field.data]
|
|
23
|
+
)
|
|
24
24
|
return super(ZonesAutocompleter, self).__call__(field, **kwargs)
|
|
25
25
|
|
|
26
26
|
|
|
@@ -29,12 +29,12 @@ class ZonesField(ModelList, Field):
|
|
|
29
29
|
widget = ZonesAutocompleter()
|
|
30
30
|
|
|
31
31
|
def fetch_objects(self, geoids):
|
|
32
|
-
|
|
32
|
+
"""
|
|
33
33
|
Custom object retrieval.
|
|
34
34
|
|
|
35
35
|
Zones are resolved from their identifier
|
|
36
36
|
instead of the default bulk fetch by ID.
|
|
37
|
-
|
|
37
|
+
"""
|
|
38
38
|
zones = []
|
|
39
39
|
no_match = []
|
|
40
40
|
for geoid in geoids:
|
|
@@ -45,8 +45,9 @@ class ZonesField(ModelList, Field):
|
|
|
45
45
|
no_match.append(geoid)
|
|
46
46
|
|
|
47
47
|
if no_match:
|
|
48
|
-
msg = _(
|
|
49
|
-
identifiers=
|
|
48
|
+
msg = _("Unknown geoid(s): {identifiers}").format(
|
|
49
|
+
identifiers=", ".join(str(id) for id in no_match)
|
|
50
|
+
)
|
|
50
51
|
raise validators.ValidationError(msg)
|
|
51
52
|
|
|
52
53
|
return zones
|
|
@@ -63,15 +64,15 @@ class GeomField(Field):
|
|
|
63
64
|
self.data = geojson.GeoJSON.to_instance(value)
|
|
64
65
|
except:
|
|
65
66
|
self.data = None
|
|
66
|
-
log.exception(
|
|
67
|
-
raise ValueError(self.gettext(
|
|
67
|
+
log.exception("Unable to parse GeoJSON")
|
|
68
|
+
raise ValueError(self.gettext("Not a valid GeoJSON"))
|
|
68
69
|
|
|
69
70
|
def pre_validate(self, form):
|
|
70
71
|
if self.data:
|
|
71
72
|
if not isinstance(self.data, geojson.GeoJSON):
|
|
72
73
|
self.data = geojson.GeoJSON.to_instance(self.data)
|
|
73
74
|
if not isinstance(self.data, geojson.GeoJSON):
|
|
74
|
-
raise validators.ValidationError(
|
|
75
|
+
raise validators.ValidationError("Not a valid GeoJSON")
|
|
75
76
|
if not self.data.is_valid:
|
|
76
77
|
raise validators.ValidationError(self.data.errors())
|
|
77
78
|
return True
|
|
@@ -80,18 +81,18 @@ class GeomField(Field):
|
|
|
80
81
|
class SpatialCoverageForm(ModelForm):
|
|
81
82
|
model_class = SpatialCoverage
|
|
82
83
|
|
|
83
|
-
zones = ZonesField(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
granularity = SelectField(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
zones = ZonesField(
|
|
85
|
+
_("Spatial coverage"), description=_("A list of covered territories"), default=[]
|
|
86
|
+
)
|
|
87
|
+
granularity = SelectField(
|
|
88
|
+
_("Spatial granularity"),
|
|
89
|
+
description=_("The size of the data increment"),
|
|
90
|
+
choices=lambda: spatial_granularities,
|
|
91
|
+
default="other",
|
|
92
|
+
)
|
|
90
93
|
geom = GeomField()
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
class SpatialCoverageField(FormField):
|
|
94
97
|
def __init__(self, *args, **kwargs):
|
|
95
|
-
super(SpatialCoverageField, self).__init__(SpatialCoverageForm,
|
|
96
|
-
*args,
|
|
97
|
-
**kwargs)
|
|
98
|
+
super(SpatialCoverageField, self).__init__(SpatialCoverageForm, *args, **kwargs)
|
udata/core/spatial/geoids.py
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
This module centralize GeoID resources and helpers.
|
|
3
3
|
|
|
4
4
|
See https://github.com/etalab/geoids for more details
|
|
5
|
-
|
|
5
|
+
"""
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
__all__ = ('GeoIDError', 'parse', 'build', 'from_zone')
|
|
7
|
+
__all__ = ("GeoIDError", "parse", "build", "from_zone")
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class GeoIDError(ValueError):
|
|
12
|
-
|
|
11
|
+
"""Raised when an error occur while parsing or building a GeoID"""
|
|
12
|
+
|
|
13
13
|
pass
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def parse(text):
|
|
17
|
-
|
|
17
|
+
"""Parse a geoid from text and return a tuple (level, code, validity)"""
|
|
18
18
|
# Kept validity parsing for legacy parsing and migration.
|
|
19
19
|
# Validity is parsed but ignored.
|
|
20
|
-
if
|
|
21
|
-
spatial, validity = text.split(
|
|
20
|
+
if "@" in text:
|
|
21
|
+
spatial, validity = text.split("@")
|
|
22
22
|
else:
|
|
23
23
|
spatial = text
|
|
24
|
-
spatial = spatial.lower().replace(
|
|
25
|
-
if
|
|
26
|
-
raise GeoIDError(
|
|
24
|
+
spatial = spatial.lower().replace("/", ":") # Backward compatibility
|
|
25
|
+
if ":" not in spatial:
|
|
26
|
+
raise GeoIDError("Bad GeoID format: {0}".format(text))
|
|
27
27
|
# country-subset is a special case:
|
|
28
|
-
if spatial.startswith(
|
|
29
|
-
level, code = spatial.split(
|
|
28
|
+
if spatial.startswith("country-subset:"):
|
|
29
|
+
level, code = spatial.split(":", 1)
|
|
30
30
|
else:
|
|
31
|
-
level, code = spatial.rsplit(
|
|
31
|
+
level, code = spatial.rsplit(":", 1)
|
|
32
32
|
return level, code
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def build(level, code):
|
|
36
|
-
|
|
37
|
-
return
|
|
36
|
+
"""Serialize a GeoID from its parts"""
|
|
37
|
+
return ":".join((level, code))
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def from_zone(zone):
|
|
41
|
-
|
|
41
|
+
"""Build a GeoID from a given zone"""
|
|
42
42
|
return build(zone.level, zone.code)
|