django-gisserver 1.5.0__py3-none-any.whl → 2.1__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.1.dist-info}/METADATA +34 -8
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/crs.py +401 -0
- gisserver/db.py +126 -51
- gisserver/exceptions.py +132 -4
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
- gisserver/extensions/queries.py +266 -0
- gisserver/features.py +253 -181
- gisserver/geometries.py +64 -311
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +311 -0
- gisserver/operations/base.py +130 -312
- gisserver/operations/wfs20.py +399 -375
- gisserver/output/__init__.py +14 -49
- gisserver/output/base.py +198 -144
- gisserver/output/csv.py +78 -75
- gisserver/output/geojson.py +37 -37
- gisserver/output/gml32.py +287 -259
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +73 -61
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +81 -169
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +426 -0
- gisserver/parsers/fes20/__init__.py +89 -31
- gisserver/parsers/fes20/expressions.py +172 -58
- gisserver/parsers/fes20/filters.py +116 -45
- gisserver/parsers/fes20/identifiers.py +66 -28
- gisserver/parsers/fes20/lookups.py +146 -0
- gisserver/parsers/fes20/operators.py +417 -161
- gisserver/parsers/fes20/sorting.py +113 -34
- gisserver/parsers/gml/__init__.py +17 -25
- gisserver/parsers/gml/base.py +36 -15
- gisserver/parsers/gml/geometries.py +105 -44
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +198 -0
- gisserver/parsers/ows/requests.py +160 -0
- gisserver/parsers/query.py +179 -0
- gisserver/parsers/values.py +87 -4
- gisserver/parsers/wfs20/__init__.py +39 -0
- gisserver/parsers/wfs20/adhoc.py +253 -0
- gisserver/parsers/wfs20/base.py +148 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +483 -0
- gisserver/parsers/wfs20/stored.py +193 -0
- gisserver/parsers/xml.py +261 -0
- gisserver/projection.py +367 -0
- gisserver/static/gisserver/index.css +20 -4
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +3 -3
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +445 -313
- gisserver/views.py +227 -62
- 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.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
gisserver/parsers/ast.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Utilities for building an Abstract Syntax Tree (AST) from an XML fragment.
|
|
2
|
+
|
|
3
|
+
By transforming the XML Element nodes into Python objects, most logic naturally follows.
|
|
4
|
+
For example, the FES filter syntax can be processed into objects that build an ORM query.
|
|
5
|
+
|
|
6
|
+
Python classes can inherit :class:`AstNode` and register themselves as the parser/handler
|
|
7
|
+
for a given tag. Both normal Python classes and dataclass work,
|
|
8
|
+
as long as it has an :meth:`AstNode.from_xml` class method.
|
|
9
|
+
The custom `from_xml()` method should copy the XML data into local attributes.
|
|
10
|
+
|
|
11
|
+
Next, when :meth:`TagRegistry.node_from_xml` is called,
|
|
12
|
+
it will detect which class the XML Element refers to and initialize it using the ``from_xml()`` call.
|
|
13
|
+
As convenience, calling a :meth:`AstNode.child_from_xml`
|
|
14
|
+
on a subclass will also initialize the right subclass and initialize it.
|
|
15
|
+
|
|
16
|
+
Since clients may not follow the desired XML schema, and make mistakes, we should guard against
|
|
17
|
+
creating an invalid Abstract Syntax Tree. When using :meth:`TagRegistry.node_from_xml`,
|
|
18
|
+
the allowed child types can also be provided, preventing invalid child elements.
|
|
19
|
+
Furthermore, to support the creation of ``from_xml()`` methods, the :func:`expect_tag`,
|
|
20
|
+
:func:`expect_children` and :func:`expect_no_children` decorators validate
|
|
21
|
+
whether the given tag has the expected elements. This combination should make it easy
|
|
22
|
+
to validate whether a provided XML structure confirms to the supported schema.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Iterable
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from functools import wraps
|
|
30
|
+
from typing import TypeVar
|
|
31
|
+
from xml.etree.ElementTree import QName
|
|
32
|
+
|
|
33
|
+
from django.utils.functional import classproperty
|
|
34
|
+
|
|
35
|
+
from gisserver.exceptions import InvalidXmlElement, XmlElementNotSupported
|
|
36
|
+
from gisserver.parsers.xml import NSElement, xmlns
|
|
37
|
+
|
|
38
|
+
__all__ = (
|
|
39
|
+
"TagNameEnum",
|
|
40
|
+
"AstNode",
|
|
41
|
+
"TagRegistry",
|
|
42
|
+
"tag_registry",
|
|
43
|
+
"expect_children",
|
|
44
|
+
"expect_tag",
|
|
45
|
+
"expect_no_children",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TagNameEnum(Enum):
|
|
50
|
+
"""An base clas for enumerations of XML tag names.
|
|
51
|
+
|
|
52
|
+
All enumerations that represent tag names inherit from this.
|
|
53
|
+
Each member name should be exactly the XML tag that it refers to.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_xml(cls, element: NSElement):
|
|
58
|
+
"""Cast the element tag name into the enum member.
|
|
59
|
+
|
|
60
|
+
This translates the element name
|
|
61
|
+
such as``{http://www.opengis.net/fes/2.0}PropertyIsEqualTo``
|
|
62
|
+
into a ``PropertyIsEqualTo`` member.
|
|
63
|
+
"""
|
|
64
|
+
tag_name = element.tag
|
|
65
|
+
if tag_name.startswith("{"):
|
|
66
|
+
# Split the element tag into the namespace and local name.
|
|
67
|
+
# The stdlib etree doesn't have the properties for this (lxml does).
|
|
68
|
+
end = tag_name.index("}")
|
|
69
|
+
tag_name = tag_name[end + 1 :]
|
|
70
|
+
|
|
71
|
+
return cls[tag_name]
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def _missing_(cls, value):
|
|
75
|
+
raise NotImplementedError(f"<{value}> is not registered as valid {cls.__name__}")
|
|
76
|
+
|
|
77
|
+
def __repr__(self):
|
|
78
|
+
# Make repr(filter) easier to copy-paste
|
|
79
|
+
return f"{self.__class__.__name__}.{self.name}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AstNode:
|
|
83
|
+
"""The base node for all classes that represent an XML tag.
|
|
84
|
+
|
|
85
|
+
All subclasses of this class build an Abstract Syntax Tree (AST)
|
|
86
|
+
that describes the XML content in Python objects. Each object can handle
|
|
87
|
+
implement additional logic to
|
|
88
|
+
|
|
89
|
+
Each subclass should implement the :meth:`from_xml` to translate
|
|
90
|
+
an XML tag into a Python (data) class.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
xml_ns: xmlns | str | None = None
|
|
94
|
+
|
|
95
|
+
_xml_tags = []
|
|
96
|
+
|
|
97
|
+
@classproperty
|
|
98
|
+
def xml_name(cls) -> str:
|
|
99
|
+
"""Tell the default tag by which this class is registered"""
|
|
100
|
+
return cls._xml_tags[0]
|
|
101
|
+
|
|
102
|
+
def __init_subclass__(cls):
|
|
103
|
+
# Each class level has a fresh list of supported child tags.
|
|
104
|
+
cls._xml_tags = []
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_xml(cls, element: NSElement):
|
|
108
|
+
"""Initialize this Python class from the data of the corresponding XML tag.
|
|
109
|
+
Each subclass overrides this to implement the XML parsing of that particular XML tag.
|
|
110
|
+
"""
|
|
111
|
+
raise NotImplementedError(
|
|
112
|
+
f"{cls.__name__}.from_xml() is not implemented to parse <{element.tag}>"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def child_from_xml(cls, element: NSElement) -> AstNode:
|
|
117
|
+
"""Parse the element, returning the correct subclass of this tag.
|
|
118
|
+
|
|
119
|
+
When ``Expression.child_from_xml(some_node)`` is given, it may
|
|
120
|
+
return a ``Literal``, ``ValueReference``, ``Function`` or ``BinaryOperator`` node.
|
|
121
|
+
"""
|
|
122
|
+
sub_class = tag_registry.resolve_class(element, allowed_types=(cls,))
|
|
123
|
+
return sub_class.from_xml(element)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def get_tag_names(cls) -> list[str]:
|
|
127
|
+
"""Provide all known XMl tags that this code can parse."""
|
|
128
|
+
try:
|
|
129
|
+
# Because a cached class property is hard to build
|
|
130
|
+
return _KNOWN_TAG_NAMES[cls]
|
|
131
|
+
except KeyError:
|
|
132
|
+
all_xml_tags = cls._xml_tags.copy()
|
|
133
|
+
for sub_cls in cls.__subclasses__():
|
|
134
|
+
all_xml_tags.extend(sub_cls.get_tag_names())
|
|
135
|
+
_KNOWN_TAG_NAMES[cls] = all_xml_tags
|
|
136
|
+
return all_xml_tags
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_KNOWN_TAG_NAMES = {}
|
|
140
|
+
|
|
141
|
+
BaseNode = AstNode # keep old name around
|
|
142
|
+
A = TypeVar("A", bound=AstNode)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TagRegistry:
|
|
146
|
+
"""Registration of all classes that can parse XML nodes.
|
|
147
|
+
|
|
148
|
+
The same class can be registered multiple times for different tag names.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
parsers: dict[str, type[AstNode]]
|
|
152
|
+
|
|
153
|
+
def __init__(self):
|
|
154
|
+
self.parsers = {}
|
|
155
|
+
|
|
156
|
+
def register(
|
|
157
|
+
self,
|
|
158
|
+
tag: str | type[TagNameEnum] | None = None,
|
|
159
|
+
namespace: xmlns | str | None = None,
|
|
160
|
+
hidden: bool = False,
|
|
161
|
+
):
|
|
162
|
+
"""Decorator to register a class as XML element parser.
|
|
163
|
+
|
|
164
|
+
Usage:
|
|
165
|
+
|
|
166
|
+
.. code-block:: python
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
@tag_registry.register()
|
|
170
|
+
class SomeXmlTag(AstNode):
|
|
171
|
+
xml_ns = FES
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_xml(cls, element: NSElement):
|
|
175
|
+
return cls(
|
|
176
|
+
...
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
Whenever an element of the registered XML name is found,
|
|
180
|
+
the given "SomeXmlTag" will be initialized.
|
|
181
|
+
|
|
182
|
+
It's also possible to register tag names using an enum;
|
|
183
|
+
each member name is assumed to be an XML tag name.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def _dec(node_class: type[AstNode]) -> type[AstNode]:
|
|
187
|
+
if tag is None or isinstance(tag, str):
|
|
188
|
+
# Single tag name for the class.
|
|
189
|
+
self._register_tag_parser(
|
|
190
|
+
node_class, tag=tag or node_class.__name__, namespace=namespace, hidden=hidden
|
|
191
|
+
)
|
|
192
|
+
elif issubclass(tag, TagNameEnum):
|
|
193
|
+
# Allow tags to be an Enum listing possible tag names.
|
|
194
|
+
# Note using __members__, not _member_names_.
|
|
195
|
+
# The latter will skip aliased items (like BBOX/Within).
|
|
196
|
+
for member_name in tag.__members__:
|
|
197
|
+
self._register_tag_parser(
|
|
198
|
+
node_class, tag=member_name, namespace=namespace, hidden=hidden
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
raise TypeError("tag type incorrect")
|
|
202
|
+
|
|
203
|
+
return node_class
|
|
204
|
+
|
|
205
|
+
return _dec
|
|
206
|
+
|
|
207
|
+
def _register_tag_parser(
|
|
208
|
+
self,
|
|
209
|
+
node_class: type[AstNode],
|
|
210
|
+
tag: str,
|
|
211
|
+
namespace: xmlns | str | None = None,
|
|
212
|
+
hidden: bool = False,
|
|
213
|
+
):
|
|
214
|
+
"""Register a Python (data) class as parser for an XML node."""
|
|
215
|
+
if not issubclass(node_class, AstNode):
|
|
216
|
+
raise TypeError(f"{node_class} must be a subclass of AstNode")
|
|
217
|
+
|
|
218
|
+
if namespace is None and node_class.xml_ns is None:
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
f"{node_class.__name__}.xml_ns should be set, or namespace should be given."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
xml_name = QName((namespace or node_class.xml_ns), tag=tag).text
|
|
224
|
+
if xml_name in self.parsers:
|
|
225
|
+
raise RuntimeError(
|
|
226
|
+
f"Another class is already registered to parse the <{xml_name}> tag."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self.parsers[xml_name] = node_class # Track this parser to resolve the tag.
|
|
230
|
+
if not hidden:
|
|
231
|
+
node_class._xml_tags.append(xml_name) # Allow fetching all names later
|
|
232
|
+
|
|
233
|
+
def node_from_xml(self, element: NSElement, allowed_types: tuple[type[A]] | None = None) -> A:
|
|
234
|
+
"""Find the ``AstNode`` subclass that corresponds to the given XML element,
|
|
235
|
+
and initialize it with the element. This is a convenience shortcut.
|
|
236
|
+
``"""
|
|
237
|
+
node_class = self.resolve_class(element, allowed_types)
|
|
238
|
+
return node_class.from_xml(element)
|
|
239
|
+
|
|
240
|
+
def resolve_class(
|
|
241
|
+
self, element: NSElement, allowed_types: tuple[type[A]] | None = None
|
|
242
|
+
) -> type[A]:
|
|
243
|
+
"""Find the ``AstNode`` subclass that corresponds to the given XML element."""
|
|
244
|
+
try:
|
|
245
|
+
node_class = self.parsers[element.tag]
|
|
246
|
+
except KeyError:
|
|
247
|
+
msg = f"Unsupported tag: <{element.qname}>"
|
|
248
|
+
if "{" not in element.tag:
|
|
249
|
+
msg = f"{msg} without an XML namespace"
|
|
250
|
+
if allowed_types:
|
|
251
|
+
allowed = _tag_names_to_text(_get_allowed_tag_names(*allowed_types), element)
|
|
252
|
+
msg = f"{msg}, expected one of: {allowed}."
|
|
253
|
+
|
|
254
|
+
raise XmlElementNotSupported(msg) from None
|
|
255
|
+
|
|
256
|
+
# Check whether the resolved class is indeed a valid option here.
|
|
257
|
+
if allowed_types is not None and not issubclass(node_class, allowed_types):
|
|
258
|
+
types = ", ".join(c.__name__ for c in allowed_types)
|
|
259
|
+
raise InvalidXmlElement(
|
|
260
|
+
f"Unexpected {node_class.__name__} for <{element.qname}> node, "
|
|
261
|
+
f"expected one of: {types}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return node_class
|
|
265
|
+
|
|
266
|
+
def get_parser_class(self, xml_qname) -> type[AstNode]:
|
|
267
|
+
"""Provide the parser class for a given XML Qualified name."""
|
|
268
|
+
return self.parsers[xml_qname]
|
|
269
|
+
|
|
270
|
+
def find_subclasses(self, node_type: type[A]) -> list[type[A]]:
|
|
271
|
+
"""Find all registered parsers for a given node."""
|
|
272
|
+
return {
|
|
273
|
+
tag: node_class
|
|
274
|
+
for tag, node_class in self.parsers.items()
|
|
275
|
+
if issubclass(node_class, node_type)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def expect_tag(namespace: xmlns | str, *tag_names: str):
|
|
280
|
+
"""Decorator for ``from_xml()`` methods that validate whether a given tag is provided.
|
|
281
|
+
|
|
282
|
+
For example:
|
|
283
|
+
|
|
284
|
+
.. code-block:: python
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
@expect_tag(xmlns.fes20, "Literal")
|
|
288
|
+
def from_xml(cls, element):
|
|
289
|
+
...
|
|
290
|
+
|
|
291
|
+
This guard is needed when nodes are passed directly to a ``from_xml()`` method.
|
|
292
|
+
"""
|
|
293
|
+
valid_tags = {QName(namespace, name).text for name in tag_names}
|
|
294
|
+
expect0 = QName(namespace, tag_names[0]).text
|
|
295
|
+
|
|
296
|
+
def _wrapper(func):
|
|
297
|
+
@wraps(func)
|
|
298
|
+
def _expect_tag_decorator(cls, element: NSElement, *args, **kwargs):
|
|
299
|
+
if element.tag not in valid_tags:
|
|
300
|
+
raise InvalidXmlElement(
|
|
301
|
+
f"{cls.__name__} parser expects an <{_replace_common_ns(expect0, element)}> node,"
|
|
302
|
+
f" got <{element.qname}>"
|
|
303
|
+
)
|
|
304
|
+
return func(cls, element, *args, **kwargs)
|
|
305
|
+
|
|
306
|
+
return _expect_tag_decorator
|
|
307
|
+
|
|
308
|
+
return _wrapper
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def expect_no_children(from_xml_func):
|
|
312
|
+
"""Decorator for ``from_xml()`` methods that validate that the XML tag has no child nodes.
|
|
313
|
+
|
|
314
|
+
For example:
|
|
315
|
+
|
|
316
|
+
.. code-block:: python
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
@expect_tag(xmlns.fes20, "ResourceId")
|
|
320
|
+
@expect_no_children
|
|
321
|
+
def from_xml(cls, element):
|
|
322
|
+
...
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
@wraps(from_xml_func)
|
|
326
|
+
def _expect_no_children_decorator(cls, element: NSElement, *args, **kwargs):
|
|
327
|
+
if len(element):
|
|
328
|
+
raise InvalidXmlElement(
|
|
329
|
+
f"Element <{element.qname}> does not support child elements,"
|
|
330
|
+
f" found <{element[0].qname}>."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return from_xml_func(cls, element, *args, **kwargs)
|
|
334
|
+
|
|
335
|
+
return _expect_no_children_decorator
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def expect_children( # noqa: C901
|
|
339
|
+
min_child_nodes, *expect_types: str | type[AstNode], silent_allowed: tuple[str] = ()
|
|
340
|
+
):
|
|
341
|
+
"""Decorator for ``from_xml()`` methods to validate whether an element has the expected children.
|
|
342
|
+
|
|
343
|
+
For example:
|
|
344
|
+
|
|
345
|
+
.. code-block:: python
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
@expect_children(2, Expression)
|
|
349
|
+
def from_xml(cls, element):
|
|
350
|
+
...
|
|
351
|
+
"""
|
|
352
|
+
# Validate arguments early
|
|
353
|
+
for child_type in expect_types + silent_allowed:
|
|
354
|
+
if isinstance(child_type, str):
|
|
355
|
+
if not child_type.startswith("{"):
|
|
356
|
+
raise ValueError(
|
|
357
|
+
f"String arguments to @expect_children() should be"
|
|
358
|
+
f" fully qualified XML namespaces, not {child_type!r}"
|
|
359
|
+
)
|
|
360
|
+
elif not isinstance(child_type, type) or not issubclass(child_type, AstNode):
|
|
361
|
+
raise TypeError(f"Unexpected {child_type!r}")
|
|
362
|
+
|
|
363
|
+
def _get_allowed(known_tag_names, element):
|
|
364
|
+
return _tag_names_to_text(sorted(set(known_tag_names) - set(silent_allowed)), element)
|
|
365
|
+
|
|
366
|
+
def _wrapper(func):
|
|
367
|
+
@wraps(func)
|
|
368
|
+
def _expect_children_decorator(cls, element: NSElement, *args, **kwargs):
|
|
369
|
+
known_tag_names = _get_allowed_tag_names(*expect_types, *silent_allowed)
|
|
370
|
+
|
|
371
|
+
if len(element) < min_child_nodes:
|
|
372
|
+
allowed = _get_allowed(known_tag_names, element)
|
|
373
|
+
raise InvalidXmlElement(
|
|
374
|
+
f"<{element.qname}> should have {min_child_nodes} child nodes,"
|
|
375
|
+
f" got only {len(element)}."
|
|
376
|
+
f" Allowed types are: {allowed}."
|
|
377
|
+
)
|
|
378
|
+
for child in element:
|
|
379
|
+
if child.tag not in known_tag_names:
|
|
380
|
+
allowed = _get_allowed(known_tag_names, element)
|
|
381
|
+
raise InvalidXmlElement(
|
|
382
|
+
f"<{element.qname}> does not support a <{child.qname}> child node."
|
|
383
|
+
f" Allowed types are: {allowed}."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return func(cls, element, *args, **kwargs)
|
|
387
|
+
|
|
388
|
+
return _expect_children_decorator
|
|
389
|
+
|
|
390
|
+
return _wrapper
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _get_allowed_tag_names(*expect_types: type[AstNode] | str) -> list[str]:
|
|
394
|
+
# Resolve arguments later, as get_tag_names() depends on __subclasses__()
|
|
395
|
+
# which may not be completely known at this point.
|
|
396
|
+
tag_names = []
|
|
397
|
+
for child_type in expect_types:
|
|
398
|
+
if isinstance(child_type, type) and issubclass(child_type, AstNode):
|
|
399
|
+
tag_names.extend(child_type.get_tag_names())
|
|
400
|
+
elif isinstance(child_type, str):
|
|
401
|
+
if not child_type.startswith("{"):
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"String arguments should be fully qualified XML namespaces, not {child_type!r}"
|
|
404
|
+
)
|
|
405
|
+
tag_names.append(child_type)
|
|
406
|
+
else:
|
|
407
|
+
raise TypeError(f"Unexpected {child_type!r}")
|
|
408
|
+
return tag_names
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _tag_names_to_text(tag_names: Iterable[str], user_element: NSElement) -> str:
|
|
412
|
+
body = _replace_common_ns(">, <".join(tag_names), user_element)
|
|
413
|
+
return f"<{body}>"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _replace_common_ns(text: str, user_element: NSElement):
|
|
417
|
+
"""In error messages, replace the full XML names with QName/prefixed versions.
|
|
418
|
+
The chosen prefixes reference the tags that the client submitted in their XML body.
|
|
419
|
+
"""
|
|
420
|
+
for prefix, ns in user_element.ns_aliases.items():
|
|
421
|
+
text = text.replace(f"{{{ns}}}", f"{prefix}:")
|
|
422
|
+
return text
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
#: The tag registry to register new parsing classes at.
|
|
426
|
+
tag_registry = TagRegistry()
|
|
@@ -1,42 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
"""Parsing and processing of the OGC Filter Encoding Standard 2.0 (FES).
|
|
2
|
+
|
|
3
|
+
This parses the XML syntax, and translates that into a Django ORM query.
|
|
4
|
+
|
|
5
|
+
It's also possible to register custom functions,
|
|
6
|
+
which will include Django ORM function as part of the filter language.
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
The full spec can be found at: https://www.ogc.org/publications/standard/filter/.
|
|
9
|
+
Secondly, using https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/filter_xsd.html
|
|
10
|
+
can be very helpful to see which options each object type should support.
|
|
11
|
+
"""
|
|
4
12
|
|
|
5
|
-
from
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from . import lookups # noqa: F401 (need to register ORM lookups)
|
|
16
|
+
from .expressions import (
|
|
17
|
+
BinaryOperator,
|
|
18
|
+
BinaryOperatorType,
|
|
19
|
+
Expression,
|
|
20
|
+
Function,
|
|
21
|
+
Literal,
|
|
22
|
+
ValueReference,
|
|
23
|
+
)
|
|
6
24
|
from .filters import Filter
|
|
7
|
-
from .
|
|
8
|
-
from .
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
from .identifiers import Id, ResourceId, VersionActionTokens
|
|
26
|
+
from .operators import (
|
|
27
|
+
BetweenComparisonOperator,
|
|
28
|
+
BinaryComparisonName,
|
|
29
|
+
BinaryComparisonOperator,
|
|
30
|
+
BinaryLogicOperator,
|
|
31
|
+
BinaryLogicType,
|
|
32
|
+
BinarySpatialOperator,
|
|
33
|
+
ComparisonOperator,
|
|
34
|
+
DistanceOperator,
|
|
35
|
+
DistanceOperatorName,
|
|
36
|
+
ExtensionOperator,
|
|
37
|
+
IdOperator,
|
|
38
|
+
LikeOperator,
|
|
39
|
+
LogicalOperator,
|
|
40
|
+
MatchAction,
|
|
41
|
+
Measure,
|
|
42
|
+
NilOperator,
|
|
43
|
+
NonIdOperator,
|
|
44
|
+
NullOperator,
|
|
45
|
+
Operator,
|
|
46
|
+
SpatialOperator,
|
|
47
|
+
SpatialOperatorName,
|
|
48
|
+
TemporalOperator,
|
|
49
|
+
TemporalOperatorName,
|
|
50
|
+
UnaryLogicOperator,
|
|
51
|
+
UnaryLogicType,
|
|
52
|
+
)
|
|
53
|
+
from .sorting import SortBy, SortOrder, SortProperty
|
|
12
54
|
|
|
13
55
|
__all__ = [
|
|
14
56
|
"Filter",
|
|
15
|
-
|
|
57
|
+
# Expressions
|
|
58
|
+
"Expression",
|
|
59
|
+
"Function",
|
|
60
|
+
"Literal",
|
|
16
61
|
"ValueReference",
|
|
62
|
+
# WFS 1.0 expressions
|
|
63
|
+
"BinaryOperator",
|
|
64
|
+
"BinaryOperatorType",
|
|
65
|
+
# Identifiers
|
|
66
|
+
"Id",
|
|
17
67
|
"ResourceId",
|
|
68
|
+
"VersionActionTokens",
|
|
69
|
+
# Operators
|
|
70
|
+
"BetweenComparisonOperator",
|
|
71
|
+
"BinaryComparisonName",
|
|
72
|
+
"BinaryComparisonOperator",
|
|
73
|
+
"BinaryLogicOperator",
|
|
74
|
+
"BinaryLogicType",
|
|
75
|
+
"BinarySpatialOperator",
|
|
76
|
+
"ComparisonOperator",
|
|
77
|
+
"DistanceOperator",
|
|
78
|
+
"DistanceOperatorName",
|
|
79
|
+
"ExtensionOperator",
|
|
18
80
|
"IdOperator",
|
|
81
|
+
"LikeOperator",
|
|
82
|
+
"LogicalOperator",
|
|
83
|
+
"MatchAction",
|
|
84
|
+
"Measure",
|
|
85
|
+
"NilOperator",
|
|
86
|
+
"NonIdOperator",
|
|
87
|
+
"NullOperator",
|
|
88
|
+
"Operator",
|
|
89
|
+
"SpatialOperator",
|
|
90
|
+
"SpatialOperatorName",
|
|
91
|
+
"TemporalOperator",
|
|
92
|
+
"TemporalOperatorName",
|
|
93
|
+
"UnaryLogicOperator",
|
|
94
|
+
"UnaryLogicType",
|
|
95
|
+
# Sorting
|
|
96
|
+
"SortBy",
|
|
97
|
+
"SortOrder",
|
|
98
|
+
"SortProperty",
|
|
19
99
|
"SortBy",
|
|
20
|
-
"function_registry",
|
|
21
100
|
]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def parse_resource_id_kvp(value) -> IdOperator:
|
|
25
|
-
"""Parse the RESOURCEID parameter.
|
|
26
|
-
This returns an IdOperator, as it needs to support multiple pairs.
|
|
27
|
-
"""
|
|
28
|
-
return IdOperator([ResourceId(rid) for rid in value.split(",")])
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def parse_property_name(value) -> list[ValueReference] | None:
|
|
32
|
-
"""Parse the PROPERTYNAME parameter"""
|
|
33
|
-
if not value or value == "*":
|
|
34
|
-
return None # WFS 1 logic
|
|
35
|
-
|
|
36
|
-
if "(" in value:
|
|
37
|
-
raise OperationParsingFailed(
|
|
38
|
-
"Parameter lists to perform multiple queries are not supported yet.",
|
|
39
|
-
locator="propertyname",
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
return [ValueReference(x) for x in value.split(",")]
|