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
gisserver/parsers/fes20/query.py
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import operator
|
|
4
|
-
from functools import reduce
|
|
5
|
-
|
|
6
|
-
from django.conf import settings
|
|
7
|
-
from django.contrib.gis.db.models.fields import BaseSpatialField
|
|
8
|
-
from django.contrib.gis.db.models.lookups import DWithinLookup
|
|
9
|
-
from django.db import models
|
|
10
|
-
from django.db.models import Q, QuerySet, lookups
|
|
11
|
-
from django.db.models.expressions import Combinable
|
|
12
|
-
|
|
13
|
-
from gisserver.features import FeatureType
|
|
14
|
-
|
|
15
|
-
from . import expressions, sorting
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class CompiledQuery:
|
|
19
|
-
"""Intermediate data for translating FES queries to Django.
|
|
20
|
-
|
|
21
|
-
This class contains all data from the ``<fes:Filter>`` object in a model
|
|
22
|
-
that can be translated to a django QuerySet.
|
|
23
|
-
|
|
24
|
-
This object is passed though all build_...() methods,
|
|
25
|
-
so it can be used to add extra lookups and annotations.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
feature_type: FeatureType | None = None,
|
|
31
|
-
using: str | None = None,
|
|
32
|
-
lookups: list[Q] | None = None,
|
|
33
|
-
typed_lookups: dict[str, list[Q]] | None = None,
|
|
34
|
-
annotations: dict[str, Combinable | Q] | None = None,
|
|
35
|
-
):
|
|
36
|
-
"""The init method is typically used only in unit tests."""
|
|
37
|
-
self.feature_type = feature_type
|
|
38
|
-
self.using = using
|
|
39
|
-
self.lookups = lookups or []
|
|
40
|
-
self.typed_lookups = typed_lookups or {}
|
|
41
|
-
self.annotations = annotations or {}
|
|
42
|
-
self.aliases = 0
|
|
43
|
-
self.extra_lookups: list[Q] = []
|
|
44
|
-
self.ordering: list[str] = []
|
|
45
|
-
self.is_empty = False
|
|
46
|
-
self.distinct = False
|
|
47
|
-
|
|
48
|
-
def add_annotation(self, value: Combinable | Q) -> str:
|
|
49
|
-
"""Create a named-alias for a function/Q object.
|
|
50
|
-
This alias can be used in a comparison, where expressions are used as left-hand-side.
|
|
51
|
-
"""
|
|
52
|
-
self.aliases += 1
|
|
53
|
-
name = f"a{self.aliases}"
|
|
54
|
-
self.annotations[name] = value
|
|
55
|
-
return name
|
|
56
|
-
|
|
57
|
-
def add_distinct(self):
|
|
58
|
-
self.distinct = True
|
|
59
|
-
|
|
60
|
-
def add_lookups(self, q_object: Q, type_name: str | None = None):
|
|
61
|
-
"""Register an extra 'WHERE' clause of the query.
|
|
62
|
-
This is used for comparisons, ID selectors and other query types.
|
|
63
|
-
"""
|
|
64
|
-
if not isinstance(q_object, Q):
|
|
65
|
-
raise TypeError()
|
|
66
|
-
|
|
67
|
-
if type_name is not None:
|
|
68
|
-
if type_name not in self.typed_lookups:
|
|
69
|
-
self.typed_lookups[type_name] = []
|
|
70
|
-
self.typed_lookups[type_name].append(q_object)
|
|
71
|
-
else:
|
|
72
|
-
self.lookups.append(q_object)
|
|
73
|
-
|
|
74
|
-
def add_extra_lookup(self, q_object: Q):
|
|
75
|
-
"""Temporary stash an extra lookup that the expression can't return yet.
|
|
76
|
-
This is used for XPath selectors that also filter on attributes,
|
|
77
|
-
e.g. "element[@attr=..]/child". The attribute lookup is processed as another filter.
|
|
78
|
-
"""
|
|
79
|
-
if not isinstance(q_object, Q):
|
|
80
|
-
raise TypeError()
|
|
81
|
-
self.extra_lookups.append(q_object)
|
|
82
|
-
|
|
83
|
-
def add_sort_by(self, sort_by: sorting.SortBy):
|
|
84
|
-
"""Read the desired result ordering from a ``<fes:SortBy>`` element."""
|
|
85
|
-
self.ordering += sort_by.build_ordering(self.feature_type)
|
|
86
|
-
|
|
87
|
-
def add_value_reference(
|
|
88
|
-
self, value_reference: expressions.ValueReference
|
|
89
|
-
) -> expressions.RhsTypes:
|
|
90
|
-
"""Add a reference that should be returned by the query.
|
|
91
|
-
|
|
92
|
-
This includes the XPath expression to the query, in case that adds
|
|
93
|
-
extra lookups. The name (or alias) is returned that can be used in the
|
|
94
|
-
``queryset.values()`` result. This is needed to support cases like
|
|
95
|
-
these in the future: ``addresses/Address[street="Oxfordstrasse"]/number``
|
|
96
|
-
"""
|
|
97
|
-
# The actual limiting of fields happens inside the decorate_queryset() of the renderer.
|
|
98
|
-
return value_reference.build_rhs(self)
|
|
99
|
-
|
|
100
|
-
def add_property_name(self, property_name: expressions.ValueReference) -> expressions.RhsTypes:
|
|
101
|
-
"""Define which field should be returned by the query."""
|
|
102
|
-
# Make sure any xpath [attr=value] lookups work.
|
|
103
|
-
# This will also validate the name because it resolves the ORM path.
|
|
104
|
-
# The actual limiting of fields happens inside the decorate_queryset() of the renderer.
|
|
105
|
-
return property_name.build_rhs(self)
|
|
106
|
-
|
|
107
|
-
def apply_extra_lookups(self, comparison: Q) -> Q:
|
|
108
|
-
"""Combine stashed lookups with the provided Q object.
|
|
109
|
-
|
|
110
|
-
This is called for functions that compile a "Q" object.
|
|
111
|
-
In case a node added extra lookups (for attributes), these are combined here
|
|
112
|
-
with the actual comparison.
|
|
113
|
-
"""
|
|
114
|
-
if not self.extra_lookups:
|
|
115
|
-
return comparison
|
|
116
|
-
|
|
117
|
-
# The extra lookups are used for XPath queries such as "/node[@attr=..]/foo".
|
|
118
|
-
# A <ValueReference> with such lookup also requires to limit the filtered results,
|
|
119
|
-
# in addition to the comparison operator code that is wrapped up here.
|
|
120
|
-
result = reduce(operator.and_, [comparison] + self.extra_lookups)
|
|
121
|
-
self.extra_lookups.clear()
|
|
122
|
-
return result
|
|
123
|
-
|
|
124
|
-
def mark_empty(self):
|
|
125
|
-
"""Mark as returning no results."""
|
|
126
|
-
self.is_empty = True
|
|
127
|
-
|
|
128
|
-
def filter_queryset(self, queryset: QuerySet, feature_type: FeatureType) -> QuerySet:
|
|
129
|
-
"""Apply the filters and lookups to the queryset.
|
|
130
|
-
|
|
131
|
-
:param queryset: The queryset to filter.
|
|
132
|
-
:param feature_type: The feature type that the queryset originated from.
|
|
133
|
-
"""
|
|
134
|
-
if self.is_empty:
|
|
135
|
-
return queryset.none()
|
|
136
|
-
|
|
137
|
-
if self.extra_lookups:
|
|
138
|
-
# Each time an expression node calls add_extra_lookup(),
|
|
139
|
-
# the parent should have used apply_extra_lookups()
|
|
140
|
-
raise RuntimeError("apply_extra_lookups() was not called")
|
|
141
|
-
|
|
142
|
-
# All are applied at once.
|
|
143
|
-
if self.annotations:
|
|
144
|
-
queryset = queryset.annotate(**self.annotations)
|
|
145
|
-
|
|
146
|
-
lookups = self.lookups
|
|
147
|
-
try:
|
|
148
|
-
lookups += self.typed_lookups[feature_type.name]
|
|
149
|
-
except KeyError:
|
|
150
|
-
pass
|
|
151
|
-
|
|
152
|
-
if lookups:
|
|
153
|
-
queryset = queryset.filter(*lookups)
|
|
154
|
-
|
|
155
|
-
if self.ordering:
|
|
156
|
-
queryset = queryset.order_by(*self.ordering)
|
|
157
|
-
|
|
158
|
-
if self.distinct:
|
|
159
|
-
queryset = queryset.distinct()
|
|
160
|
-
|
|
161
|
-
return queryset
|
|
162
|
-
|
|
163
|
-
def __repr__(self):
|
|
164
|
-
return (
|
|
165
|
-
"<CompiledQuery"
|
|
166
|
-
f" annotations={self.annotations!r},"
|
|
167
|
-
f" lookups={self.lookups!r},"
|
|
168
|
-
f" typed_lookups={self.typed_lookups!r}>"
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
def __eq__(self, other):
|
|
172
|
-
"""For pytest comparisons."""
|
|
173
|
-
if isinstance(other, CompiledQuery):
|
|
174
|
-
return (
|
|
175
|
-
other.lookups == self.lookups
|
|
176
|
-
and other.typed_lookups == self.typed_lookups
|
|
177
|
-
and other.annotations == self.annotations
|
|
178
|
-
)
|
|
179
|
-
else:
|
|
180
|
-
return NotImplemented
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@models.CharField.register_lookup
|
|
184
|
-
@models.TextField.register_lookup
|
|
185
|
-
@models.ForeignObject.register_lookup
|
|
186
|
-
class FesLike(lookups.Lookup):
|
|
187
|
-
"""Allow fieldname__fes_like=... lookups in querysets."""
|
|
188
|
-
|
|
189
|
-
lookup_name = "fes_like"
|
|
190
|
-
|
|
191
|
-
def as_sql(self, compiler, connection):
|
|
192
|
-
"""Generate the required SQL."""
|
|
193
|
-
# lhs = "table"."field"
|
|
194
|
-
# rhs = %s
|
|
195
|
-
# lhs_params = []
|
|
196
|
-
# lhs_params = ["prep-value"]
|
|
197
|
-
lhs, lhs_params = self.process_lhs(compiler, connection)
|
|
198
|
-
rhs, rhs_params = self.process_rhs(compiler, connection)
|
|
199
|
-
return f"{lhs} LIKE {rhs}", lhs_params + rhs_params
|
|
200
|
-
|
|
201
|
-
def get_db_prep_lookup(self, value, connection):
|
|
202
|
-
"""This expects that the right-hand-side already has wildcard characters."""
|
|
203
|
-
return "%s", [value]
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
@models.Field.register_lookup
|
|
207
|
-
@models.ForeignObject.register_lookup
|
|
208
|
-
class FesNotEqual(lookups.Lookup):
|
|
209
|
-
"""Allow fieldname__fes_notequal=... lookups in querysets."""
|
|
210
|
-
|
|
211
|
-
lookup_name = "fes_notequal"
|
|
212
|
-
|
|
213
|
-
def as_sql(self, compiler, connection):
|
|
214
|
-
"""Generate the required SQL."""
|
|
215
|
-
lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
|
|
216
|
-
rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
|
|
217
|
-
return f"{lhs} != {rhs}", (lhs_params + rhs_params)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
@BaseSpatialField.register_lookup
|
|
221
|
-
class FesBeyondLookup(DWithinLookup):
|
|
222
|
-
"""Based on the FES 2.0.3 corrigendum:
|
|
223
|
-
|
|
224
|
-
DWithin(A,B,d) = Distance(A,B) < d
|
|
225
|
-
Beyond(A,B,d) = Distance(A,B) > d
|
|
226
|
-
|
|
227
|
-
See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
lookup_name = "fes_beyond"
|
|
231
|
-
sql_template = "NOT %(func)s(%(lhs)s, %(rhs)s, %(value)s)"
|
|
232
|
-
|
|
233
|
-
def get_rhs_op(self, connection, rhs):
|
|
234
|
-
# Allow the SQL $(func)s to be different from the ORM lookup name.
|
|
235
|
-
# This uses ST_DWithin() on PostGIS
|
|
236
|
-
return connection.ops.gis_operators["dwithin"]
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
240
|
-
from django.contrib.postgres.fields import ArrayField
|
|
241
|
-
|
|
242
|
-
class ArrayAnyMixin:
|
|
243
|
-
any_operators = {
|
|
244
|
-
"exact": "= ANY(%s)",
|
|
245
|
-
"ne": "!= ANY(%s)",
|
|
246
|
-
"gt": "< ANY(%s)",
|
|
247
|
-
"gte": "<= ANY(%s)",
|
|
248
|
-
"lt": "> ANY(%s)",
|
|
249
|
-
"lte": ">= ANY(%s)",
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
def as_sql(self, compiler, connection):
|
|
253
|
-
# For the ANY() comparison, the filter operands need to be reversed.
|
|
254
|
-
# So instead of "field < value", it becomes "value > ANY(field)
|
|
255
|
-
lhs_sql, lhs_params = self.process_lhs(compiler, connection)
|
|
256
|
-
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
|
|
257
|
-
lhs_sql = self.get_rhs_op(connection, lhs_sql)
|
|
258
|
-
return f"{rhs_sql} {lhs_sql}", (rhs_params + lhs_params)
|
|
259
|
-
|
|
260
|
-
def get_rhs_op(self, connection, rhs):
|
|
261
|
-
return self.any_operators[self.lookup_name] % rhs
|
|
262
|
-
|
|
263
|
-
def _register_any_lookup(base: type[lookups.BuiltinLookup]):
|
|
264
|
-
"""Register array lookups under a different name."""
|
|
265
|
-
cls = type(f"FesArrayAny{base.__name__}", (ArrayAnyMixin, base), {})
|
|
266
|
-
ArrayField.register_lookup(cls, lookup_name=f"fes_any{base.lookup_name}")
|
|
267
|
-
|
|
268
|
-
_register_any_lookup(lookups.Exact)
|
|
269
|
-
_register_any_lookup(lookups.Exact)
|
|
270
|
-
_register_any_lookup(lookups.GreaterThan)
|
|
271
|
-
_register_any_lookup(lookups.GreaterThanOrEqual)
|
|
272
|
-
_register_any_lookup(lookups.LessThan)
|
|
273
|
-
_register_any_lookup(lookups.LessThanOrEqual)
|
|
274
|
-
|
|
275
|
-
@ArrayField.register_lookup
|
|
276
|
-
class FesArrayAnyNotEqual(FesNotEqual):
|
|
277
|
-
"""Inequality test for a single item in the array"""
|
|
278
|
-
|
|
279
|
-
lookup_name = "fes_anynotequal"
|
|
280
|
-
|
|
281
|
-
def as_sql(self, compiler, connection):
|
|
282
|
-
"""Generate the required SQL."""
|
|
283
|
-
lhs, lhs_params = self.process_lhs(compiler, connection)
|
|
284
|
-
rhs, rhs_params = self.process_rhs(compiler, connection)
|
|
285
|
-
return f"{rhs} != ANY({lhs})", (rhs_params + lhs_params)
|
gisserver/parsers/tags.py
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from functools import wraps
|
|
4
|
-
from itertools import chain
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
from xml.etree.ElementTree import Element, QName
|
|
7
|
-
|
|
8
|
-
from gisserver.exceptions import ExternalParsingError
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def expect_tag(namespace: str, *tag_names: str, leaf=False):
|
|
12
|
-
"""Validate whether a given tag is need"""
|
|
13
|
-
valid_tags = {str(QName(namespace, name)) for name in tag_names}
|
|
14
|
-
expect0 = str(QName(namespace, tag_names[0]))
|
|
15
|
-
|
|
16
|
-
def _wrapper(func):
|
|
17
|
-
@wraps(func)
|
|
18
|
-
def _expect_tag_decorator(cls, element: Element, *args, **kwargs):
|
|
19
|
-
if element.tag not in valid_tags:
|
|
20
|
-
raise ExternalParsingError(
|
|
21
|
-
f"{cls.__name__} parser expects an <{expect0}> node, got <{element.tag}>"
|
|
22
|
-
)
|
|
23
|
-
if leaf and len(element):
|
|
24
|
-
raise ExternalParsingError(
|
|
25
|
-
f"Unsupported child element for {element.tag} element: {element[0].tag}."
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
return func(cls, element, *args, **kwargs)
|
|
29
|
-
|
|
30
|
-
return _expect_tag_decorator
|
|
31
|
-
|
|
32
|
-
return _wrapper
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def expect_children(min_child_nodes, *expect_types: str | type[BaseNode]):
|
|
36
|
-
def _wrapper(func):
|
|
37
|
-
@wraps(func)
|
|
38
|
-
def _expect_children_decorator(cls, element: Element, *args, **kwargs):
|
|
39
|
-
if len(element) < min_child_nodes:
|
|
40
|
-
type_names = ", ".join(
|
|
41
|
-
sorted(
|
|
42
|
-
set(
|
|
43
|
-
chain.from_iterable(
|
|
44
|
-
(
|
|
45
|
-
[child_type]
|
|
46
|
-
if isinstance(child_type, str)
|
|
47
|
-
else chain.from_iterable(
|
|
48
|
-
sub_type.xml_tags
|
|
49
|
-
for sub_type in child_type.__subclasses__()
|
|
50
|
-
)
|
|
51
|
-
)
|
|
52
|
-
for child_type in expect_types
|
|
53
|
-
)
|
|
54
|
-
)
|
|
55
|
-
)
|
|
56
|
-
)
|
|
57
|
-
suffix = f" (possible tags: {type_names})" if type_names else ""
|
|
58
|
-
raise ExternalParsingError(
|
|
59
|
-
f"<{element.tag}> should have {min_child_nodes} child nodes, "
|
|
60
|
-
f"got {len(element)}{suffix}"
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return func(cls, element, *args, **kwargs)
|
|
64
|
-
|
|
65
|
-
return _expect_children_decorator
|
|
66
|
-
|
|
67
|
-
return _wrapper
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def get_child(root, namespace, localname) -> Element:
|
|
71
|
-
"""Find the element using a fully qualified name."""
|
|
72
|
-
return root.find(QName(namespace, localname).text)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_children(root, namespace, localname) -> list[Element]:
|
|
76
|
-
"""Find the element using a fully qualified name."""
|
|
77
|
-
return root.findall(QName(namespace, localname).text)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def get_attribute(element: Element, name) -> str:
|
|
81
|
-
"""Resolve an attribute, raise an error when it's missing."""
|
|
82
|
-
try:
|
|
83
|
-
return element.attrib[name]
|
|
84
|
-
except KeyError:
|
|
85
|
-
raise ExternalParsingError(
|
|
86
|
-
f"Element {element.tag} misses required attribute '{name}'"
|
|
87
|
-
) from None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def split_ns(tag_name: str) -> tuple[str | None, str]:
|
|
91
|
-
"""Split the element tag into the namespace and local name.
|
|
92
|
-
The stdlib etree doesn't have the properties for this (lxml does).
|
|
93
|
-
"""
|
|
94
|
-
if tag_name.startswith("{"):
|
|
95
|
-
end = tag_name.index("}")
|
|
96
|
-
return tag_name[1:end], tag_name[end + 1 :]
|
|
97
|
-
else:
|
|
98
|
-
return None, tag_name
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if TYPE_CHECKING:
|
|
102
|
-
from gisserver.parsers.base import BaseNode
|
gisserver/queries/__init__.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Entry point to handle queries.
|
|
2
|
-
|
|
3
|
-
WFS defines 2 query types:
|
|
4
|
-
- Adhoc queries are constructed directly from request parameters.
|
|
5
|
-
- Stored queries are defined first, and executed later.
|
|
6
|
-
|
|
7
|
-
Both use the FES (Filter Encoding Syntax) filtering logic internally.
|
|
8
|
-
|
|
9
|
-
The objects in this module closely follow the WFS spec.
|
|
10
|
-
By using the same type definitions, a lot of code logic follows naturally.
|
|
11
|
-
The "GetFeatureById" is a mandatory built-in stored query.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from .adhoc import AdhocQuery
|
|
15
|
-
from .base import QueryExpression
|
|
16
|
-
from .projection import FeatureProjection, FeatureRelation
|
|
17
|
-
from .stored import (
|
|
18
|
-
GetFeatureById,
|
|
19
|
-
QueryExpressionText,
|
|
20
|
-
StoredQuery,
|
|
21
|
-
StoredQueryDescription,
|
|
22
|
-
StoredQueryParameter,
|
|
23
|
-
stored_query_registry,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
__all__ = (
|
|
27
|
-
"QueryExpression",
|
|
28
|
-
"AdhocQuery",
|
|
29
|
-
"QueryExpressionText",
|
|
30
|
-
"StoredQueryDescription",
|
|
31
|
-
"StoredQuery",
|
|
32
|
-
"stored_query_registry",
|
|
33
|
-
"StoredQueryParameter",
|
|
34
|
-
"GetFeatureById",
|
|
35
|
-
"FeatureProjection",
|
|
36
|
-
"FeatureRelation",
|
|
37
|
-
)
|
gisserver/queries/adhoc.py
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"""Handle adhoc-query objects.
|
|
2
|
-
|
|
3
|
-
The adhoc query is based on incoming request parameters,
|
|
4
|
-
such as the "FILTER", "BBOX" and "RESOURCEID" parameters.
|
|
5
|
-
|
|
6
|
-
These definitions follow the WFS spec.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import logging
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
|
|
14
|
-
from django.db.models import Q
|
|
15
|
-
|
|
16
|
-
from gisserver import conf
|
|
17
|
-
from gisserver.exceptions import InvalidParameterValue, MissingParameterValue
|
|
18
|
-
from gisserver.features import FeatureType
|
|
19
|
-
from gisserver.geometries import BoundingBox
|
|
20
|
-
from gisserver.parsers import fes20
|
|
21
|
-
from gisserver.parsers.fes20 import operators
|
|
22
|
-
|
|
23
|
-
from .base import QueryExpression
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@dataclass
|
|
29
|
-
class AdhocQuery(QueryExpression):
|
|
30
|
-
"""The Ad hoc query expression parameters.
|
|
31
|
-
|
|
32
|
-
This represents all dynamic queries received as request (hence "adhoc"),
|
|
33
|
-
such as the "FILTER" and "BBOX" arguments from an HTTP GET.
|
|
34
|
-
|
|
35
|
-
The WFS Spec has 3 class levels for this:
|
|
36
|
-
- AdhocQueryExpression (types, projection, selection, sorting)
|
|
37
|
-
- Query (adds srsName, featureVersion)
|
|
38
|
-
|
|
39
|
-
For KVP requests, this dataclass is almost identical to **params.
|
|
40
|
-
However, it allows combining the filter parameters. These become
|
|
41
|
-
one single XML request for HTTP POST requests later.
|
|
42
|
-
|
|
43
|
-
.. seealso::
|
|
44
|
-
https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/query_xsd.html#AbstractAdhocQueryExpressionType
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
typeNames: list[FeatureType] # typeNames in WFS/FES spec
|
|
48
|
-
# aliases: Optional[List[str]] = None
|
|
49
|
-
handle: str = "" # only for XML POST requests
|
|
50
|
-
|
|
51
|
-
# Projection clause (fes:AbstractProjectionClause)
|
|
52
|
-
property_names: list[fes20.ValueReference] | None = None
|
|
53
|
-
|
|
54
|
-
# Selection clause (fes:AbstractSelectionClause):
|
|
55
|
-
# - for XML POST this is encoded in a <fes:Query>
|
|
56
|
-
# - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
|
|
57
|
-
filter: fes20.Filter | None = None
|
|
58
|
-
filter_language: str = fes20.Filter.query_language
|
|
59
|
-
bbox: BoundingBox | None = None
|
|
60
|
-
|
|
61
|
-
# Sorting Clause (fes:AbstractSortingClause)
|
|
62
|
-
sortBy: fes20.SortBy | None = None
|
|
63
|
-
|
|
64
|
-
# Officially part of the GetFeature/GetPropertyValue request object,
|
|
65
|
-
# but included here for ease of query implementation.
|
|
66
|
-
resourceId: fes20.IdOperator | None = None
|
|
67
|
-
|
|
68
|
-
# GetPropertyValue:
|
|
69
|
-
# In the WFS spec, this is only part of the operation/presentation.
|
|
70
|
-
# For Django, we'd like to make this part of the query too.
|
|
71
|
-
value_reference: fes20.ValueReference | None = None
|
|
72
|
-
|
|
73
|
-
@classmethod
|
|
74
|
-
def from_kvp_request(cls, **params):
|
|
75
|
-
"""Build this object from an HTTP GET (key-value-pair) request."""
|
|
76
|
-
# Validate optionally required parameters
|
|
77
|
-
if not params["typeNames"] and not params["resourceID"]:
|
|
78
|
-
raise MissingParameterValue("Empty TYPENAMES parameter", locator="typeNames")
|
|
79
|
-
|
|
80
|
-
# Validate mutually exclusive parameters
|
|
81
|
-
if params["filter"] and (params["bbox"] or params["resourceID"]):
|
|
82
|
-
raise InvalidParameterValue(
|
|
83
|
-
"The FILTER parameter is mutually exclusive with BBOX and RESOURCEID",
|
|
84
|
-
locator="filter",
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Validate mutually exclusive parameters
|
|
88
|
-
if params["resourceID"]:
|
|
89
|
-
if params["bbox"] or params["filter"]:
|
|
90
|
-
raise InvalidParameterValue(
|
|
91
|
-
"The RESOURCEID parameter is mutually exclusive with BBOX and FILTER",
|
|
92
|
-
locator="resourceID",
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# When ResourceId + typenames is defined, it should be a value from typenames
|
|
96
|
-
# see WFS spec 7.9.2.4.1
|
|
97
|
-
if params["typeNames"]:
|
|
98
|
-
id_type_names = params["resourceID"].type_names
|
|
99
|
-
if id_type_names:
|
|
100
|
-
# Only test when the RESOURCEID has a typename.id format
|
|
101
|
-
# Otherwise, this breaks the CITE RESOURCEID=test-UUID parameter.
|
|
102
|
-
kvp_type_names = {feature_type.name for feature_type in params["typeNames"]}
|
|
103
|
-
if not kvp_type_names.issuperset(id_type_names):
|
|
104
|
-
raise InvalidParameterValue(
|
|
105
|
-
"When TYPENAMES and RESOURCEID are combined, "
|
|
106
|
-
"the RESOURCEID type should be included in TYPENAMES.",
|
|
107
|
-
locator="resourceID",
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
return AdhocQuery(
|
|
111
|
-
typeNames=params["typeNames"],
|
|
112
|
-
property_names=params["propertyName"],
|
|
113
|
-
filter=params["filter"],
|
|
114
|
-
filter_language=params["filter_language"],
|
|
115
|
-
bbox=params["bbox"],
|
|
116
|
-
sortBy=params["sortBy"],
|
|
117
|
-
resourceId=params["resourceID"],
|
|
118
|
-
value_reference=params.get("valueReference"),
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
def bind(self, *args, **kwargs):
|
|
122
|
-
"""Inform this query object of the available feature types"""
|
|
123
|
-
super().bind(*args, **kwargs)
|
|
124
|
-
|
|
125
|
-
if self.resourceId:
|
|
126
|
-
# Early validation whether the selected resourceID type exists.
|
|
127
|
-
feature_types = [
|
|
128
|
-
self.resolve_type_name(type_name, locator="resourceID")
|
|
129
|
-
for type_name in self.resourceId.type_names
|
|
130
|
-
]
|
|
131
|
-
|
|
132
|
-
# Also make the behavior consistent, always supply the type name.
|
|
133
|
-
if not self.typeNames:
|
|
134
|
-
self.typeNames = feature_types
|
|
135
|
-
|
|
136
|
-
def get_type_names(self):
|
|
137
|
-
return self.typeNames
|
|
138
|
-
|
|
139
|
-
def compile_query(self, feature_type: FeatureType, using=None) -> fes20.CompiledQuery:
|
|
140
|
-
"""Return our internal CompiledQuery object that can be applied to the queryset."""
|
|
141
|
-
if self.filter:
|
|
142
|
-
# Generate the internal query object from the <fes:Filter>
|
|
143
|
-
return self.filter.compile_query(feature_type, using=using)
|
|
144
|
-
else:
|
|
145
|
-
# Generate the internal query object from the BBOX and sortBy args.
|
|
146
|
-
return self._compile_non_filter_query(feature_type, using=using)
|
|
147
|
-
|
|
148
|
-
def _compile_non_filter_query(self, feature_type: FeatureType, using=None):
|
|
149
|
-
"""Generate the query based on the remaining parameters.
|
|
150
|
-
|
|
151
|
-
This is slightly more efficient than generating the fes Filter object
|
|
152
|
-
from these KVP parameters (which could also be done within the request method).
|
|
153
|
-
"""
|
|
154
|
-
compiler = fes20.CompiledQuery(feature_type=feature_type, using=using)
|
|
155
|
-
|
|
156
|
-
if self.bbox:
|
|
157
|
-
# Validate whether the provided SRID is supported.
|
|
158
|
-
# While PostGIS would support many more ID's,
|
|
159
|
-
# it would crash when an unsupported ID is given.
|
|
160
|
-
crs = self.bbox.crs
|
|
161
|
-
if (
|
|
162
|
-
conf.GISSERVER_SUPPORTED_CRS_ONLY
|
|
163
|
-
and crs is not None
|
|
164
|
-
and crs not in feature_type.supported_crs
|
|
165
|
-
):
|
|
166
|
-
raise InvalidParameterValue(
|
|
167
|
-
"bbox",
|
|
168
|
-
f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Using __within does not work with geometries
|
|
172
|
-
# that only partially exist within the bbox
|
|
173
|
-
lookup = operators.SpatialOperatorName.BBOX.value # "intersects"
|
|
174
|
-
filters = {
|
|
175
|
-
f"{feature_type.geometry_field.name}__{lookup}": self.bbox.as_polygon(),
|
|
176
|
-
}
|
|
177
|
-
compiler.add_lookups(Q(**filters))
|
|
178
|
-
|
|
179
|
-
if self.resourceId:
|
|
180
|
-
self.resourceId.build_query(compiler=compiler)
|
|
181
|
-
|
|
182
|
-
if self.sortBy:
|
|
183
|
-
compiler.add_sort_by(self.sortBy)
|
|
184
|
-
|
|
185
|
-
return compiler
|