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