django-gisserver 1.5.0__py3-none-any.whl → 2.0__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.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0
|
|
4
4
|
Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
|
|
5
5
|
Home-page: https://github.com/amsterdam/django-gisserver
|
|
6
6
|
Author: Diederik van der Boor
|
|
7
7
|
Author-email: opensource@edoburu.nl
|
|
8
8
|
License: Mozilla Public License 2.0
|
|
9
|
-
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
11
|
Classifier: Environment :: Web Environment
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
12
13
|
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
@@ -16,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
21
|
Classifier: Framework :: Django
|
|
20
22
|
Classifier: Framework :: Django :: 3.2
|
|
21
23
|
Classifier: Framework :: Django :: 4.0
|
|
@@ -27,12 +29,12 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
|
27
29
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
28
30
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
31
|
Requires: Django (>=3.2)
|
|
30
|
-
Requires-Python: >=3.
|
|
32
|
+
Requires-Python: >=3.9
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
32
34
|
License-File: LICENSE
|
|
33
35
|
Requires-Dist: Django>=3.2
|
|
34
36
|
Requires-Dist: defusedxml>=0.6.0
|
|
35
|
-
Requires-Dist:
|
|
37
|
+
Requires-Dist: lru-dict>=1.1.7
|
|
36
38
|
Requires-Dist: orjson>=3.9.15
|
|
37
39
|
Provides-Extra: tests
|
|
38
40
|
Requires-Dist: django-environ>=0.4.5; extra == "tests"
|
|
@@ -66,6 +68,12 @@ Django speaking WFS 2.0 to expose geo data.
|
|
|
66
68
|
|
|
67
69
|
For more details, see: <https://django-gisserver.readthedocs.io/>
|
|
68
70
|
|
|
71
|
+
## Test drive
|
|
72
|
+
|
|
73
|
+
* Run `docker compose up`.
|
|
74
|
+
* Open http://localhost:8000/ to see this works.
|
|
75
|
+
* Connect to http://localhost:8000/wfs/ in your GIS client (e.g. QGis) and zoom to Amsterdam.
|
|
76
|
+
* Add more data on the map using the Django admin, username and password are both "admin".
|
|
69
77
|
|
|
70
78
|
## Quickstart
|
|
71
79
|
|
|
@@ -168,3 +176,5 @@ software developed with public money is also publicly available.
|
|
|
168
176
|
|
|
169
177
|
This package is initially developed by the City of Amsterdam, but the tools
|
|
170
178
|
and concepts created in this project can be used in any city.
|
|
179
|
+
|
|
180
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
gisserver/__init__.py,sha256=e6waXF42UPN4o15WEYAQ7DskYjyZB_gs3IANkuyGZUk,38
|
|
2
|
+
gisserver/compat.py,sha256=qmLeT7wGRIyA2ujUtVEuUX7qgr98snak9b5OTvnkhas,451
|
|
3
|
+
gisserver/conf.py,sha256=ppoyVS95mMoQXVbPxPar-tkq1t7fE0J3F6IyKitMWuA,2563
|
|
4
|
+
gisserver/db.py,sha256=G4xF3xd8BQjGbKdf6QpOwzUfnm-phTarS71IRCV0Rys,5385
|
|
5
|
+
gisserver/exceptions.py,sha256=q0EZJrlZ7JPf8tWB9de6fPWX74JBrlsoHYN4pBdCGck,6132
|
|
6
|
+
gisserver/features.py,sha256=b7XrDC8XMn6SK3YdOuC5DvXlral5losrFrCeweecJUM,35744
|
|
7
|
+
gisserver/geometries.py,sha256=Ok9e6fbAhW32iglg6gc955z0ZTN2p2lQd3WlN-FPJho,12338
|
|
8
|
+
gisserver/projection.py,sha256=TStCF9xF4V_aozKqTD8uW9mPofZogobVDpdltXwsyZo,14859
|
|
9
|
+
gisserver/types.py,sha256=5J18OUeL8o0VOdxlMGn7fPja7z4s-9Dgy6G-cjXReZM,41453
|
|
10
|
+
gisserver/views.py,sha256=e7Bqr05FaflXOjOXMfQSix59bHqRmb_ASCRqqJlWqEY,18998
|
|
11
|
+
gisserver/extensions/__init__.py,sha256=MsYUsNsoxxjeDHhx4DW9eGXvmOnI5PkoUXvKxRZaRRw,136
|
|
12
|
+
gisserver/extensions/functions.py,sha256=Gv_IoWgJbCONbGg2P4GXdZDI18GvNtHDWLWQjOQMJLY,9085
|
|
13
|
+
gisserver/extensions/queries.py,sha256=_UNWjHMgGgWtBzOl9Ez587e6WeE1_9uN8Qyjzfi4PMo,9435
|
|
14
|
+
gisserver/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
gisserver/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
gisserver/management/commands/loadgeojson.py,sha256=bdCdt37cKPIYV9cQtLFJ0BzFcVgQQ9y59kTtPX7bYkA,11493
|
|
17
|
+
gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
gisserver/operations/base.py,sha256=m56SRTnC6GVWfwugtXgdlIVNh6Vr1h8r_OdfBjWQQ5M,9839
|
|
19
|
+
gisserver/operations/wfs20.py,sha256=WIvGXg1-M01s52VB8nSJGM0tKoviMhnNghQIrfqzudM,24226
|
|
20
|
+
gisserver/output/__init__.py,sha256=5DTFnVEvs-lno_6lOOZheXhF4F96VkwAk71Q5juYe1M,977
|
|
21
|
+
gisserver/output/base.py,sha256=EF3PFUrj2pGzbh2Yt4bpjlwkoGDoI63rHn6wkstXA3g,10303
|
|
22
|
+
gisserver/output/csv.py,sha256=wKtcgwY37NjeLhBxAm2qgKk8UDX-4uUZEzK9LmmVKrQ,6624
|
|
23
|
+
gisserver/output/geojson.py,sha256=fTEB-MRaYLYbJzQVvmZ8ev-Cwrl3lgsR8Jif2zWYSKg,11525
|
|
24
|
+
gisserver/output/gml32.py,sha256=OtBXGup5m1GA9dM_p33E9IXtD-9KcZ58MgMyg-lkGd4,32567
|
|
25
|
+
gisserver/output/iters.py,sha256=YbvtIIy977Snm5m7cYyQext0GCpo8S_u4azTCBNqqgw,8355
|
|
26
|
+
gisserver/output/results.py,sha256=vvwJK1JEgRbs9S4sg5fmsd_avw5ztiC107OizygHk5M,15149
|
|
27
|
+
gisserver/output/stored.py,sha256=m3JcR4MUilyuM_5J6XJDOIyzrhRJbcpg-6FMKy69csc,5763
|
|
28
|
+
gisserver/output/utils.py,sha256=H4lD5Dn_xo8lAHcFv5LjKyj-SNMzzZsZrtDny9HISXk,2671
|
|
29
|
+
gisserver/output/xmlschema.py,sha256=fHj8h9-vG508uRmM-kD5r2omde2wa_myJWWwcWe2lus,6445
|
|
30
|
+
gisserver/parsers/__init__.py,sha256=eGORBeT0F8fR3AadV1uq8dbCeZtIeK0-__CcXwSvXgs,402
|
|
31
|
+
gisserver/parsers/ast.py,sha256=DVUF3977RTjIpsu5yxbKS23Yxepn6JMj1NIUdRmQq2Q,11806
|
|
32
|
+
gisserver/parsers/query.py,sha256=Ndipmq94reShe-DNKwJ96Ig0GdmshRhtVIF6OEGMGeg,6386
|
|
33
|
+
gisserver/parsers/values.py,sha256=7AuOX8Zgb6Cnwb1FuY8Jl0r6TbLrGlNmhICw48sNiCE,2320
|
|
34
|
+
gisserver/parsers/xml.py,sha256=jMsK1enKN0i_rdtjzGqcKdxnr9-n2-9jPY6hX56sU4g,9309
|
|
35
|
+
gisserver/parsers/fes20/__init__.py,sha256=ji0bIRap4fvNW_WWhjnbkBm7ozuEblccIHNzL403Y3E,883
|
|
36
|
+
gisserver/parsers/fes20/expressions.py,sha256=sjBO-S0aQDIorAPg6z7SSUwOreveJEA0w7qcj26UXoA,11275
|
|
37
|
+
gisserver/parsers/fes20/filters.py,sha256=nMAQwgSXl2c7SvVZY_PJSO9XSf55ltvDPnCmIQ5SM9s,6518
|
|
38
|
+
gisserver/parsers/fes20/identifiers.py,sha256=E-6ijz050fjoG-wyzzaIYwxVPplE9RVtoLN_iFNIYw8,3513
|
|
39
|
+
gisserver/parsers/fes20/lookups.py,sha256=nVdqNP0uZntgT1czTxuuUMBVhGKbRsKm8RU-YwGt6EE,5362
|
|
40
|
+
gisserver/parsers/fes20/operators.py,sha256=E5sdepKSxKmklDfHZoNwBwPCqihx5RQJfiA91bv_Hxc,31196
|
|
41
|
+
gisserver/parsers/fes20/sorting.py,sha256=MZXT2RCRKWO9368lkbCbd17IIlTHKuRroafAQVNQuXo,4769
|
|
42
|
+
gisserver/parsers/gml/__init__.py,sha256=DS6CRpFayQp3i5UdwdnAenBb6V6B8vSkn4zQmpo-pBM,1573
|
|
43
|
+
gisserver/parsers/gml/base.py,sha256=hyYAVg-pagH43MFvVgLBzPbIY-uAR1ytDPCtNaXLeww,1165
|
|
44
|
+
gisserver/parsers/gml/geometries.py,sha256=DSBwzjsT16iwSH-bPixLmB5LWpNNUVBL0byl3iMN_Eg,4654
|
|
45
|
+
gisserver/parsers/ows/__init__.py,sha256=0SeM6vfwjWI-WeRD7eS_iN4Jsl8uzXr2yMFpZVHokb4,624
|
|
46
|
+
gisserver/parsers/ows/kvp.py,sha256=et-vlI3EjcK_z1ArGt26RxDyipuwI85NNDe0FVcg7eI,7093
|
|
47
|
+
gisserver/parsers/ows/requests.py,sha256=TZRPX_4RGeNX0qN-OJiRZMGYIWfbn-4vF7zVL4Z1nSk,5496
|
|
48
|
+
gisserver/parsers/wfs20/__init__.py,sha256=6_0TZcLVu48J7Pz802CvkrzEoDDIlI7Bz1t7_VPaOjs,881
|
|
49
|
+
gisserver/parsers/wfs20/adhoc.py,sha256=yM88MrGudYz692QZTL5H6KO1KKZHg0DL44NKWezbYxU,9066
|
|
50
|
+
gisserver/parsers/wfs20/base.py,sha256=_yEIJLrkzPxxj8sjePeSn8PA6L4PTnon5UruDWXejBo,5601
|
|
51
|
+
gisserver/parsers/wfs20/projection.py,sha256=iGpg75VnHj3GrFnSJodEuXbhCF04Phh1IfcAFpurkfw,3930
|
|
52
|
+
gisserver/parsers/wfs20/requests.py,sha256=fl7CUHrqhnEm__M67CHG3hVB9c2r7jFTHJIqlVbW8fI,16756
|
|
53
|
+
gisserver/parsers/wfs20/stored.py,sha256=4YtlildS_lUnY2iZyGQiVQM9TkSjgaaD7iDWtumg82c,7090
|
|
54
|
+
gisserver/static/gisserver/index.css,sha256=h24POXHqJOWtcBRNBFMxZiiknbATRTRWCL6nPyTgPr0,440
|
|
55
|
+
gisserver/templates/gisserver/index.html,sha256=Gq6nasptfquoyd7ihKLtj02tyadijK_Qh8ies6pZxy8,920
|
|
56
|
+
gisserver/templates/gisserver/service_description.html,sha256=y61iVhrcjg44qp-EymaFkUwdsB0LeYTGTbFO0JYOCEs,932
|
|
57
|
+
gisserver/templates/gisserver/wfs/feature_field.html,sha256=Kzbs96GcallCdGLeTPYsFnT6wJ5ar-feyqAmHzJT0u8,587
|
|
58
|
+
gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
|
|
59
|
+
gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=SGZSn7cDU_Oai6d45xrkqhax5CPyI1oIc8BZLOEw1X4,8695
|
|
60
|
+
gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
|
+
gisserver/templatetags/gisserver_tags.py,sha256=L2YG-PxiKR0QWBMRLLrL_VR94p6wCb5HzgnrPMg4hug,995
|
|
62
|
+
django_gisserver-2.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
63
|
+
django_gisserver-2.0.dist-info/METADATA,sha256=c9OYER5wjNqHxdxWRA3E3F-v5QMDqPvZ-yCM0NRlFMk,6217
|
|
64
|
+
django_gisserver-2.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
65
|
+
django_gisserver-2.0.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
|
|
66
|
+
django_gisserver-2.0.dist-info/RECORD,,
|
gisserver/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "2.0" # follows PEP440
|
gisserver/compat.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Compatability imports"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import django
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.db import models
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
"django.contrib.postgres" in settings.INSTALLED_APPS
|
|
11
|
+
or "django.contrib.postgres" in sys.modules
|
|
12
|
+
):
|
|
13
|
+
from django.contrib.postgres.fields import ArrayField
|
|
14
|
+
else:
|
|
15
|
+
ArrayField = None
|
|
16
|
+
|
|
17
|
+
GeneratedField = models.GeneratedField if django.VERSION >= (5, 0) else None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
"ArrayField",
|
|
22
|
+
"GeneratedField",
|
|
23
|
+
)
|
gisserver/conf.py
CHANGED
|
@@ -28,6 +28,13 @@ GISSERVER_SUPPORTED_CRS_ONLY = getattr(settings, "GISSERVER_SUPPORTED_CRS_ONLY",
|
|
|
28
28
|
# 0 = No counting, 1 = all pages, 2 = only for the first page.
|
|
29
29
|
GISSERVER_COUNT_NUMBER_MATCHED = getattr(settings, "GISSERVER_COUNT_NUMBER_MATCHED", 1)
|
|
30
30
|
|
|
31
|
+
# -- output rendering
|
|
32
|
+
|
|
33
|
+
GISSERVER_EXTRA_OUTPUT_FORMATS = getattr(settings, "GISSERVER_EXTRA_OUTPUT_FORMATS", {})
|
|
34
|
+
GISSERVER_GET_FEATURE_OUTPUT_FORMATS = getattr(
|
|
35
|
+
settings, "GISSERVER_GET_FEATURE_OUTPUT_FORMATS", {}
|
|
36
|
+
)
|
|
37
|
+
|
|
31
38
|
# -- max page size
|
|
32
39
|
|
|
33
40
|
# Allow tuning the page size without having to override code.
|
gisserver/db.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from functools import lru_cache, reduce
|
|
6
7
|
|
|
7
8
|
from django.contrib.gis.db.models import functions
|
|
@@ -9,7 +10,9 @@ from django.db import connection, connections, models
|
|
|
9
10
|
|
|
10
11
|
from gisserver import conf
|
|
11
12
|
from gisserver.geometries import CRS
|
|
12
|
-
from gisserver.types import
|
|
13
|
+
from gisserver.types import GeometryXsdElement
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class AsEWKT(functions.GeoFunc):
|
|
@@ -78,63 +81,69 @@ def get_geometries_union(
|
|
|
78
81
|
return reduce(functions.Union, expressions)
|
|
79
82
|
|
|
80
83
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def build_db_annotations(
|
|
91
|
-
selects: dict[str, str | functions.Func],
|
|
92
|
-
name_template: str,
|
|
93
|
-
wrapper_func: type[functions.Func],
|
|
94
|
-
) -> dict:
|
|
95
|
-
"""Utility to build annotations for all geometry fields for an XSD type.
|
|
96
|
-
This is used by various DB-optimized rendering methods.
|
|
84
|
+
def replace_queryset_geometries(
|
|
85
|
+
queryset: models.QuerySet,
|
|
86
|
+
geo_elements: list[GeometryXsdElement],
|
|
87
|
+
output_crs: CRS,
|
|
88
|
+
wrapper_func: type[functions.GeoFunc],
|
|
89
|
+
) -> models.QuerySet:
|
|
90
|
+
"""Replace the queryset geometry retrieval with a database-rendered version.
|
|
91
|
+
|
|
92
|
+
This uses absolute paths in the queryset, but can use relative paths for related querysets.
|
|
97
93
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
defer_names = []
|
|
95
|
+
as_geo_map = {}
|
|
96
|
+
for geo_element in geo_elements:
|
|
97
|
+
if geo_element.source is not None: # excludes GmlBoundedByElement
|
|
98
|
+
defer_names.append(geo_element.local_orm_path)
|
|
99
|
+
annotation_name = _as_annotation_name(geo_element.local_orm_path, wrapper_func)
|
|
100
|
+
as_geo_map[annotation_name] = wrapper_func(
|
|
101
|
+
get_db_geometry_target(geo_element, output_crs, use_relative_path=True)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not defer_names:
|
|
105
|
+
return queryset
|
|
106
|
+
|
|
107
|
+
logger.debug(
|
|
108
|
+
"DB rendering: QuerySet for %s replacing %r with %r",
|
|
109
|
+
queryset.model._meta.label,
|
|
110
|
+
defer_names,
|
|
111
|
+
list(as_geo_map.keys()),
|
|
112
|
+
)
|
|
113
|
+
return queryset.defer(*defer_names).annotate(**as_geo_map)
|
|
102
114
|
|
|
103
115
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
def get_db_rendered_geometry(
|
|
117
|
+
instance: models.Model, geo_element: GeometryXsdElement, replacement: type[functions.GeoFunc]
|
|
118
|
+
) -> str:
|
|
119
|
+
"""Retrieve the database-rendered geometry.
|
|
120
|
+
This includes formatted EWKT or GML output, rendered by the database.
|
|
121
|
+
"""
|
|
122
|
+
annotation_name = _as_annotation_name(geo_element.local_orm_path, replacement)
|
|
108
123
|
try:
|
|
109
|
-
return getattr(instance,
|
|
124
|
+
return getattr(instance, annotation_name)
|
|
110
125
|
except AttributeError as e:
|
|
126
|
+
prefix = _as_annotation_name("", replacement)
|
|
127
|
+
available = ", ".join(key for key in instance.__dict__ if key.startswith(prefix)) or "none"
|
|
111
128
|
raise AttributeError(
|
|
112
|
-
f" DB annotation {instance._meta.
|
|
113
|
-
f" not found (using {name_template})"
|
|
129
|
+
f" DB annotation {instance._meta.label}.{annotation_name} not found. Found {available}."
|
|
114
130
|
) from e
|
|
115
131
|
|
|
116
132
|
|
|
117
133
|
@lru_cache
|
|
118
|
-
def
|
|
134
|
+
def _as_annotation_name(name: str, func: type[functions.GeoFunc]) -> str:
|
|
119
135
|
"""Escape an XML name to be used as annotation name."""
|
|
120
|
-
return
|
|
136
|
+
return f"_{func.__name__}_{name.replace('.', '_')}"
|
|
121
137
|
|
|
122
138
|
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
) ->
|
|
126
|
-
"""
|
|
127
|
-
|
|
139
|
+
def get_db_geometry_target(
|
|
140
|
+
geo_element: GeometryXsdElement, output_crs: CRS, use_relative_path: bool = False
|
|
141
|
+
) -> str | functions.Transform:
|
|
142
|
+
"""Translate a GML geometry field into the proper expression for retrieving it from the database.
|
|
143
|
+
The path will be wrapped into a CRS Transform function if needed.
|
|
128
144
|
"""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def get_db_geometry_target(gml_element: GmlElement, output_crs: CRS) -> str | functions.Transform:
|
|
137
|
-
"""Wrap the selection of a geometry field in a CRS Transform if needed."""
|
|
138
|
-
return conditional_transform(
|
|
139
|
-
gml_element.orm_path, gml_element.source.srid, output_srid=output_crs.srid
|
|
140
|
-
)
|
|
145
|
+
orm_path = geo_element.local_orm_path if use_relative_path else geo_element.orm_path
|
|
146
|
+
if geo_element.source_srid != output_crs.srid:
|
|
147
|
+
return functions.Transform(orm_path, srid=output_crs.srid)
|
|
148
|
+
else:
|
|
149
|
+
return orm_path
|
gisserver/exceptions.py
CHANGED
|
@@ -8,10 +8,32 @@ https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#35
|
|
|
8
8
|
https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#411
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import logging
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
|
|
11
14
|
from django.conf import settings
|
|
12
15
|
from django.http import HttpResponse
|
|
13
16
|
from django.utils.html import format_html
|
|
14
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def wrap_parser_errors(name: str, locator: str):
|
|
23
|
+
"""Convert the value into a Python format.
|
|
24
|
+
This catches any typical exceptions and transforms them into an OWSException.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
except ExternalParsingError as e:
|
|
29
|
+
raise OperationParsingFailed(
|
|
30
|
+
f"Unable to parse {name} argument: {e}", locator=locator
|
|
31
|
+
) from None
|
|
32
|
+
except (TypeError, ValueError, NotImplementedError) as e:
|
|
33
|
+
# TypeError/ValueError are raised by most handlers for unexpected data
|
|
34
|
+
# The NotImplementedError can be raised by fes parsing.
|
|
35
|
+
raise InvalidParameterValue(f"Invalid {name} argument: {e}", locator=locator) from None
|
|
36
|
+
|
|
15
37
|
|
|
16
38
|
class ExternalValueError(ValueError):
|
|
17
39
|
"""Raise a ValueError for external input.
|
|
@@ -38,7 +60,7 @@ class OWSException(Exception):
|
|
|
38
60
|
def __init__(self, text=None, code=None, locator=None, status_code=None):
|
|
39
61
|
text = text or self.text_template.format(code=self.code, locator=locator)
|
|
40
62
|
if (code and len(text) < len(code)) or (locator and len(text) < len(locator)):
|
|
41
|
-
raise ValueError("text/locator arguments are switched")
|
|
63
|
+
raise ValueError(f"text/locator arguments are switched: {text!r}, locator={locator!r}")
|
|
42
64
|
|
|
43
65
|
super().__init__(text)
|
|
44
66
|
self.locator = locator
|
|
@@ -47,8 +69,10 @@ class OWSException(Exception):
|
|
|
47
69
|
self.status_code = status_code or self.status_code
|
|
48
70
|
|
|
49
71
|
def as_response(self):
|
|
72
|
+
logger.debug("Returning HTTP %d for %s: %s", self.status_code, self.code, self.text)
|
|
73
|
+
xml_body = self.as_xml()
|
|
50
74
|
return HttpResponse(
|
|
51
|
-
b'<?xml version="1.0" encoding="UTF-8"?>\n%b' %
|
|
75
|
+
b'<?xml version="1.0" encoding="UTF-8"?>\n%b' % xml_body.encode("utf-8"),
|
|
52
76
|
content_type="text/xml; charset=utf-8",
|
|
53
77
|
status=self.status_code,
|
|
54
78
|
reason=self.reason,
|
|
@@ -22,8 +22,14 @@ FesFunctionBody = Union[models.Func, Callable[..., models.Func]]
|
|
|
22
22
|
|
|
23
23
|
@dataclass(order=True)
|
|
24
24
|
class FesFunction:
|
|
25
|
-
"""
|
|
26
|
-
|
|
25
|
+
"""A registered database function that can be used by ``<fes:Function name="...">`.
|
|
26
|
+
|
|
27
|
+
The :class:`~gisserver.parsers.fes20.expressions.Function` class will resolve
|
|
28
|
+
these registered functions by name, and call :meth:`build_query` to include them
|
|
29
|
+
in the database query. This will actually insert a Django ORM function in the query!
|
|
30
|
+
|
|
31
|
+
This wrapper class also provides the metadata and type descriptions of the function,
|
|
32
|
+
which is exposed in the ``GetCapabilities`` call.
|
|
27
33
|
"""
|
|
28
34
|
|
|
29
35
|
#: Name of the function
|
|
@@ -50,7 +56,7 @@ class FesFunction:
|
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
class FesFunctionRegistry:
|
|
53
|
-
"""Registry of functions to be callable by
|
|
59
|
+
"""Registry of functions to be callable by ``<fes:Function>``.
|
|
54
60
|
|
|
55
61
|
The registered functions should be capable of running an SQL function.
|
|
56
62
|
"""
|
|
@@ -102,7 +108,7 @@ class FesFunctionRegistry:
|
|
|
102
108
|
return self.functions[function_name]
|
|
103
109
|
except KeyError:
|
|
104
110
|
raise InvalidParameterValue(
|
|
105
|
-
|
|
111
|
+
f"Unsupported function: {function_name}", locator="filter"
|
|
106
112
|
) from None
|
|
107
113
|
|
|
108
114
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Storage and registry for stored queries.
|
|
2
|
+
These definitions follow the WFS spec.
|
|
3
|
+
|
|
4
|
+
By using the registry, custom stored queries can be registered in this server.
|
|
5
|
+
Out of the box, only the mandatory built-in ``GetFeatureById`` query is present.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typing
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from functools import partial
|
|
14
|
+
from xml.etree.ElementTree import Element
|
|
15
|
+
|
|
16
|
+
from django.db.models import Q
|
|
17
|
+
|
|
18
|
+
from gisserver.exceptions import InvalidParameterValue, NotFound, OperationNotSupported
|
|
19
|
+
from gisserver.features import FeatureType
|
|
20
|
+
from gisserver.parsers import fes20
|
|
21
|
+
from gisserver.parsers.query import CompiledQuery
|
|
22
|
+
from gisserver.types import XsdTypes
|
|
23
|
+
|
|
24
|
+
if typing.TYPE_CHECKING:
|
|
25
|
+
from gisserver.output import SimpleFeatureCollection
|
|
26
|
+
|
|
27
|
+
__all__ = (
|
|
28
|
+
"GetFeatureById",
|
|
29
|
+
"QueryExpressionText",
|
|
30
|
+
"StoredQueryDescription",
|
|
31
|
+
"StoredQueryRegistry",
|
|
32
|
+
"stored_query_registry",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
WFS_LANGUAGE = "urn:ogc:def:queryLanguage:OGC-WFS::WFS_QueryExpression" # body is <wfs:Query>
|
|
36
|
+
FES_LANGUAGE = "urn:ogc:def:queryLanguage:OGC-FES:Filter" # body is <fes:Filter>
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class QueryExpressionText:
|
|
41
|
+
"""Define the body of a stored query.
|
|
42
|
+
|
|
43
|
+
This object type is defined in the WFS spec.
|
|
44
|
+
It may contain a wfs:Query or wfs:StoredQuery element.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
#: Which types the query will return.
|
|
48
|
+
return_feature_types: list[str] | None = None
|
|
49
|
+
#: The internal language of the query. Can be a WFS/FES-filter, or "python"
|
|
50
|
+
language: str = FES_LANGUAGE
|
|
51
|
+
#: Whether the implementation_text will be show or hidden from users.
|
|
52
|
+
is_private: bool = True
|
|
53
|
+
|
|
54
|
+
#: Body
|
|
55
|
+
implementation_text: str | Element | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class StoredQueryDescription:
|
|
60
|
+
"""WFS metadata of a stored query.
|
|
61
|
+
This is based on the ``<wfs:StoredQueryDescription>`` element,
|
|
62
|
+
and returned in ``DescribeStoredQueries``.
|
|
63
|
+
|
|
64
|
+
While it's possible to define multiple QueryExpressionText nodes
|
|
65
|
+
as metadata to describe a query, there is still only one implementation.
|
|
66
|
+
Note there is no 'typeNames=...' parameter for stored queries.
|
|
67
|
+
Only direct parameters act as input.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
#: The ID of the query
|
|
71
|
+
id: str
|
|
72
|
+
#: User-visible title
|
|
73
|
+
title: str
|
|
74
|
+
#: User-visible description
|
|
75
|
+
abstract: str
|
|
76
|
+
#: Parameter declaration
|
|
77
|
+
parameters: dict[str, XsdTypes]
|
|
78
|
+
|
|
79
|
+
#: Metadata describing the query body
|
|
80
|
+
expressions: list[QueryExpressionText] = field(
|
|
81
|
+
default_factory=lambda: [QueryExpressionText(language=FES_LANGUAGE)]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
#: Python-based implementation for the query.
|
|
85
|
+
implementation_class: type[StoredQueryImplementation] = field(init=False, default=None)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class StoredQueryImplementation:
|
|
89
|
+
"""A custom stored query.
|
|
90
|
+
|
|
91
|
+
This receives the parameters as init arguments,
|
|
92
|
+
and should implement :meth:`build_query`.
|
|
93
|
+
The function is registered using ``StoredQueryRegistry.register()``.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Registered metadata
|
|
97
|
+
_meta: StoredQueryDescription
|
|
98
|
+
|
|
99
|
+
# Allow queries to return only the XML nodes, without any <wfs:FeatureCollection> wrapper.
|
|
100
|
+
has_standalone_output: bool = False
|
|
101
|
+
|
|
102
|
+
def __repr__(self):
|
|
103
|
+
return f"<{self.__class__.__name__} implementing '{self._meta.id}'>"
|
|
104
|
+
|
|
105
|
+
def bind(self, source_query, feature_types: list[FeatureType]):
|
|
106
|
+
"""Associate this query with the application data."""
|
|
107
|
+
self.source_query = source_query
|
|
108
|
+
self.feature_types = feature_types
|
|
109
|
+
if len(feature_types) > 1:
|
|
110
|
+
raise OperationNotSupported("Join queries are not supported", locator="typeNames")
|
|
111
|
+
|
|
112
|
+
def get_type_names(self) -> list[str]:
|
|
113
|
+
"""Tell which type names this query applies to."""
|
|
114
|
+
raise NotImplementedError()
|
|
115
|
+
|
|
116
|
+
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
117
|
+
"""Contribute our filter expression to the internal query.
|
|
118
|
+
|
|
119
|
+
This should add the filter expressions to the internal query compiler.
|
|
120
|
+
The top-level ``<wfs:StoredQuery>`` object will add
|
|
121
|
+
the ``<wfs:PropertyName>`` logic and other elements.
|
|
122
|
+
"""
|
|
123
|
+
raise NotImplementedError()
|
|
124
|
+
|
|
125
|
+
def finalize_results(self, result: SimpleFeatureCollection):
|
|
126
|
+
"""Hook to allow subclasses to inspect the results."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class StoredQueryRegistry:
|
|
130
|
+
"""Registry of functions to be callable by <wfs:StoredQuery>."""
|
|
131
|
+
|
|
132
|
+
def __init__(self):
|
|
133
|
+
self.stored_queries: dict[str, type(StoredQueryImplementation)] = {}
|
|
134
|
+
|
|
135
|
+
def __bool__(self):
|
|
136
|
+
return bool(self.stored_queries)
|
|
137
|
+
|
|
138
|
+
def __iter__(self) -> Iterator[StoredQueryDescription]:
|
|
139
|
+
return iter(self.stored_queries.values())
|
|
140
|
+
|
|
141
|
+
def get_queries(self) -> Iterable[StoredQueryDescription]:
|
|
142
|
+
"""Find all descriptions for stored queries."""
|
|
143
|
+
return self.stored_queries.values()
|
|
144
|
+
|
|
145
|
+
def register(
|
|
146
|
+
self,
|
|
147
|
+
meta: StoredQueryDescription | None = None,
|
|
148
|
+
query_expression: type[StoredQueryImplementation] | None = None,
|
|
149
|
+
**meta_kwargs,
|
|
150
|
+
):
|
|
151
|
+
"""Register a custom class that handles a stored query.
|
|
152
|
+
This function can be used as decorator or normal call.
|
|
153
|
+
"""
|
|
154
|
+
if meta is None:
|
|
155
|
+
meta = StoredQueryDescription(**meta_kwargs)
|
|
156
|
+
elif meta_kwargs:
|
|
157
|
+
raise TypeError("Either provide the 'meta' object or 'meta_kwargs'")
|
|
158
|
+
|
|
159
|
+
if query_expression is not None:
|
|
160
|
+
self._register(meta, query_expression)
|
|
161
|
+
return meta
|
|
162
|
+
else:
|
|
163
|
+
return partial(self._register, meta) # decorator effect.
|
|
164
|
+
|
|
165
|
+
def _register(
|
|
166
|
+
self, meta: StoredQueryDescription, implementation_class: type[StoredQueryImplementation]
|
|
167
|
+
):
|
|
168
|
+
"""Internal registration method."""
|
|
169
|
+
if not issubclass(implementation_class, StoredQueryImplementation):
|
|
170
|
+
raise TypeError(f"Expecting {StoredQueryImplementation}' subclass")
|
|
171
|
+
if meta.implementation_class is not None:
|
|
172
|
+
raise RuntimeError("Can't register same StoredQueryDescription again.")
|
|
173
|
+
|
|
174
|
+
# for now link both. There is always a single implementation for the metadata.
|
|
175
|
+
meta.implementation_class = implementation_class
|
|
176
|
+
self.stored_queries[meta.id] = meta
|
|
177
|
+
|
|
178
|
+
def resolve_query(self, query_id) -> type[StoredQueryDescription]:
|
|
179
|
+
"""Find the stored procedure using the ID."""
|
|
180
|
+
try:
|
|
181
|
+
return self.stored_queries[query_id]
|
|
182
|
+
except KeyError:
|
|
183
|
+
raise InvalidParameterValue(
|
|
184
|
+
f"Stored query does not exist: {query_id}",
|
|
185
|
+
locator="STOREDQUERY_ID",
|
|
186
|
+
) from None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
stored_query_registry = StoredQueryRegistry()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@stored_query_registry.register(
|
|
193
|
+
id="urn:ogc:def:query:OGC-WFS::GetFeatureById",
|
|
194
|
+
title="Get feature by identifier",
|
|
195
|
+
abstract="Returns the single feature that corresponds with the ID argument",
|
|
196
|
+
parameters={"id": XsdTypes.string},
|
|
197
|
+
expressions=[QueryExpressionText(language=WFS_LANGUAGE)],
|
|
198
|
+
)
|
|
199
|
+
class GetFeatureById(StoredQueryImplementation):
|
|
200
|
+
"""The stored query for GetFeatureById.
|
|
201
|
+
|
|
202
|
+
This can be called using::
|
|
203
|
+
|
|
204
|
+
<wfs:StoredQuery id="urn:ogc:def:query:OGC-WFS::GetFeatureById">
|
|
205
|
+
<wfs:Parameter name="ID">typename.ID</wfs:Parameter>
|
|
206
|
+
</wfs:StoredQuery>
|
|
207
|
+
|
|
208
|
+
or using KVP syntax::
|
|
209
|
+
|
|
210
|
+
?...&REQUEST=GetFeature&STOREDQUERY_ID=urn:ogc:def:query:OGC-WFS::GetFeatureById&ID=typename.ID
|
|
211
|
+
|
|
212
|
+
The execution of the ``GetFeatureById`` query is essentially the same as::
|
|
213
|
+
|
|
214
|
+
<wfs:Query xmlns:wfs='..." xmlns:fes='...'>
|
|
215
|
+
<fes:Filter><fes:ResourceId rid='{ID}'/></fes:Filter>
|
|
216
|
+
</wfs:Query>
|
|
217
|
+
|
|
218
|
+
or::
|
|
219
|
+
|
|
220
|
+
<wfs:Query typeName="{typename}">
|
|
221
|
+
<fes:Filter>
|
|
222
|
+
<fes:PropertyIsEqualTo>
|
|
223
|
+
<fes:ValueReference>{primary-key-field}</fes:ValueReference>
|
|
224
|
+
<fes:Literal>{ID-value}</fes:Literal>
|
|
225
|
+
</fes:PropertyIsEqualTo>
|
|
226
|
+
</fes:Filter>
|
|
227
|
+
</wfs:Query>
|
|
228
|
+
|
|
229
|
+
Except that the response is supposed to contain only the item itself.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
# Projection of GetFeatureById only returns the document nodes, not a <wfs:FeatureCollection> wrapper
|
|
233
|
+
has_standalone_output = True
|
|
234
|
+
|
|
235
|
+
def __init__(self, id: str, ns_aliases: dict[str, str]):
|
|
236
|
+
"""Initialize the query with the request parameters."""
|
|
237
|
+
if "." not in id:
|
|
238
|
+
# Always report this as 404
|
|
239
|
+
raise NotFound("Expected typeName.id for ID parameter", locator="ID") from None
|
|
240
|
+
|
|
241
|
+
# GetFeatureById is essentially a ResourceId lookup, reuse that logic here.
|
|
242
|
+
self.resource_id = fes20.ResourceId.from_string(id, ns_aliases)
|
|
243
|
+
|
|
244
|
+
def get_type_names(self) -> list[str]:
|
|
245
|
+
"""Tell which type names this query applies to."""
|
|
246
|
+
return [self.resource_id.get_type_name()]
|
|
247
|
+
|
|
248
|
+
def build_query(self, compiler: CompiledQuery) -> Q:
|
|
249
|
+
"""Contribute our filter expression to the internal query."""
|
|
250
|
+
try:
|
|
251
|
+
return self.resource_id.build_query(compiler)
|
|
252
|
+
except InvalidParameterValue as e:
|
|
253
|
+
raise InvalidParameterValue(f"Invalid ID value: {e.__cause__}", locator="ID") from e
|
|
254
|
+
|
|
255
|
+
def finalize_results(self, results: SimpleFeatureCollection):
|
|
256
|
+
"""Override to implement 404 checking."""
|
|
257
|
+
# Directly attempt to collect the data.
|
|
258
|
+
# Avoid having to do that in the output renderer.
|
|
259
|
+
if results.first() is None:
|
|
260
|
+
# WFS 2.0.2: Return NotFound instead of InvalidParameterValue
|
|
261
|
+
raise NotFound(f"Feature not found with ID {self.resource_id.rid}.", locator="ID")
|