django-gisserver 1.4.1__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.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.4.1.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 +63 -60
- gisserver/exceptions.py +47 -9
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +267 -240
- gisserver/geometries.py +34 -39
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +129 -305
- gisserver/operations/wfs20.py +428 -336
- gisserver/output/__init__.py +10 -48
- gisserver/output/base.py +198 -143
- gisserver/output/csv.py +81 -85
- gisserver/output/geojson.py +63 -72
- gisserver/output/gml32.py +310 -281
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +71 -30
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -154
- gisserver/output/xmlschema.py +86 -47
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +15 -11
- gisserver/parsers/fes20/expressions.py +89 -50
- 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 +336 -128
- gisserver/parsers/fes20/sorting.py +107 -34
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +6 -3
- 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 +11 -11
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +375 -258
- gisserver/views.py +206 -75
- django_gisserver-1.4.1.dist-info/RECORD +0 -53
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -275
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -34
- gisserver/queries/adhoc.py +0 -181
- gisserver/queries/base.py +0 -146
- gisserver/queries/stored.py +0 -205
- 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.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/parsers/ast.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
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:`BaseNode` 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:`BaseNode.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 :meth:`SomeNode.child_from_xml()` will also
|
|
14
|
+
initialize the right subclass and initialize it.
|
|
15
|
+
|
|
16
|
+
Since clients may not follow the desired XML schema, and make mistakes, one should avoid
|
|
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 itertools import chain
|
|
31
|
+
from typing import TypeVar
|
|
32
|
+
from xml.etree.ElementTree import QName
|
|
33
|
+
|
|
34
|
+
from gisserver.exceptions import ExternalParsingError
|
|
35
|
+
|
|
36
|
+
__all__ = (
|
|
37
|
+
"TagNameEnum",
|
|
38
|
+
"BaseNode",
|
|
39
|
+
"TagRegistry",
|
|
40
|
+
"tag_registry",
|
|
41
|
+
"expect_children",
|
|
42
|
+
"expect_tag",
|
|
43
|
+
"expect_no_children",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from gisserver.parsers.xml import NSElement, xmlns
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TagNameEnum(Enum):
|
|
50
|
+
"""An enumeration 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
|
+
tag_name = element.tag
|
|
60
|
+
if tag_name.startswith("{"):
|
|
61
|
+
# Split the element tag into the namespace and local name.
|
|
62
|
+
# The stdlib etree doesn't have the properties for this (lxml does).
|
|
63
|
+
end = tag_name.index("}")
|
|
64
|
+
tag_name = tag_name[end + 1 :]
|
|
65
|
+
|
|
66
|
+
return cls[tag_name]
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _missing_(cls, value):
|
|
70
|
+
raise NotImplementedError(f"<{value}> is not registered as valid {cls.__name__}")
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
# Make repr(filter) easier to copy-paste
|
|
74
|
+
return f"{self.__class__.__name__}.{self.name}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BaseNode:
|
|
78
|
+
"""The base node for all classes that represent an XML tag.
|
|
79
|
+
|
|
80
|
+
All subclasses of this class build an Abstract Syntax Tree (AST)
|
|
81
|
+
that describes the XML content in Python objects. Each object can handle
|
|
82
|
+
implement additional logic to
|
|
83
|
+
|
|
84
|
+
Each subclass should implement the :meth:`from_xml` to translate
|
|
85
|
+
an XML tag into a Python (data) class.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
xml_ns: xmlns | str | None = None
|
|
89
|
+
xml_tags = []
|
|
90
|
+
|
|
91
|
+
def __init_subclass__(cls):
|
|
92
|
+
# Each class level has a fresh list of supported child tags.
|
|
93
|
+
cls.xml_tags = []
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_xml(cls, element: NSElement):
|
|
97
|
+
"""Initialize this Python class from the data of the corresponding XML tag.
|
|
98
|
+
Each subclass overrides this to implement the XMl parsing of that particular XML tag.
|
|
99
|
+
"""
|
|
100
|
+
raise NotImplementedError(
|
|
101
|
+
f"{cls.__name__}.from_xml() is not implemented to parse <{element.tag}>"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def child_from_xml(cls, element: NSElement) -> BaseNode:
|
|
106
|
+
"""Parse the element, returning the correct subclass of this tag.
|
|
107
|
+
|
|
108
|
+
When ``Expression.child_from_xml(some_node)`` is given, it may
|
|
109
|
+
return a ``Literal``, ``ValueReference``, ``Function`` or ``BinaryOperator`` node.
|
|
110
|
+
"""
|
|
111
|
+
sub_class = tag_registry.resolve_class(element, allowed_types=(cls,))
|
|
112
|
+
return sub_class.from_xml(element)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_tag_names(cls) -> Iterable[str]:
|
|
116
|
+
"""Provide all known XMl tags that this code can parse."""
|
|
117
|
+
return chain.from_iterable(sub_type.xml_tags for sub_type in cls.__subclasses__())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
BN = TypeVar("BN", bound=BaseNode)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TagRegistry:
|
|
124
|
+
"""Registration of all classes that can parse XML nodes.
|
|
125
|
+
|
|
126
|
+
The same class can be registered multiple times for different tag names.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
parsers: dict[str, type[BaseNode]]
|
|
130
|
+
|
|
131
|
+
def __init__(self):
|
|
132
|
+
self.parsers = {}
|
|
133
|
+
|
|
134
|
+
def register(
|
|
135
|
+
self,
|
|
136
|
+
tag: str | type[TagNameEnum] | None = None,
|
|
137
|
+
namespace: xmlns | str | None = None,
|
|
138
|
+
hidden: bool = False,
|
|
139
|
+
):
|
|
140
|
+
"""Decorator to register a class as XML element parser.
|
|
141
|
+
|
|
142
|
+
Usage:
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
@tag_registry.register()
|
|
146
|
+
class SomeXmlTag(BaseNode):
|
|
147
|
+
xml_ns = FES
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_xml(cls, element: NSElement):
|
|
151
|
+
return cls(
|
|
152
|
+
...
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
Whenever an element of the registered XML name is found,
|
|
156
|
+
the given "SomeXmlTag" will be initialized.
|
|
157
|
+
|
|
158
|
+
It's also possible to register tag names using an enum;
|
|
159
|
+
each member name is assumed to be an XML tag name.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def _dec(node_class: type[BaseNode]) -> type[BaseNode]:
|
|
163
|
+
if tag is None or isinstance(tag, str):
|
|
164
|
+
# Single tag name for the class.
|
|
165
|
+
self._register_tag_parser(
|
|
166
|
+
node_class, tag=tag or node_class.__name__, namespace=namespace, hidden=hidden
|
|
167
|
+
)
|
|
168
|
+
elif issubclass(tag, TagNameEnum):
|
|
169
|
+
# Allow tags to be an Enum listing possible tag names.
|
|
170
|
+
# Note using __members__, not _member_names_.
|
|
171
|
+
# The latter will skip aliased items (like BBOX/Within).
|
|
172
|
+
for member_name in tag.__members__:
|
|
173
|
+
self._register_tag_parser(node_class, tag=member_name, namespace=namespace)
|
|
174
|
+
else:
|
|
175
|
+
raise TypeError("tag type incorrect")
|
|
176
|
+
|
|
177
|
+
return node_class
|
|
178
|
+
|
|
179
|
+
return _dec
|
|
180
|
+
|
|
181
|
+
def _register_tag_parser(
|
|
182
|
+
self,
|
|
183
|
+
node_class: type[BaseNode],
|
|
184
|
+
tag: str,
|
|
185
|
+
namespace: xmlns | str | None = None,
|
|
186
|
+
hidden: bool = False,
|
|
187
|
+
):
|
|
188
|
+
"""Register a Python (data) class as parser for an XML node."""
|
|
189
|
+
if not issubclass(node_class, BaseNode):
|
|
190
|
+
raise TypeError(f"{node_class} must be a subclass of BaseNode")
|
|
191
|
+
|
|
192
|
+
if namespace is None and node_class.xml_ns is None:
|
|
193
|
+
raise RuntimeError(
|
|
194
|
+
f"{node_class.__name__}.xml_ns should be set, or namespace should be given."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
xml_name = QName((namespace or node_class.xml_ns), tag=tag).text
|
|
198
|
+
if xml_name in self.parsers:
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
f"Another class is already registered to parse the <{xml_name}> tag."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.parsers[xml_name] = node_class # Track this parser to resolve the tag.
|
|
204
|
+
if not hidden:
|
|
205
|
+
node_class.xml_tags.append(xml_name) # Allow fetching all names later
|
|
206
|
+
|
|
207
|
+
def node_from_xml(
|
|
208
|
+
self, element: NSElement, allowed_types: tuple[type[BN]] | None = None
|
|
209
|
+
) -> BN:
|
|
210
|
+
"""Find the ``BaseNode`` subclass that corresponds to the given XML element,
|
|
211
|
+
and initialize it with the element. This is a convenience shortcut.
|
|
212
|
+
``"""
|
|
213
|
+
node_class = self.resolve_class(element, allowed_types)
|
|
214
|
+
return node_class.from_xml(element)
|
|
215
|
+
|
|
216
|
+
def resolve_class(
|
|
217
|
+
self, element: NSElement, allowed_types: tuple[type[BN]] | None = None
|
|
218
|
+
) -> type[BN]:
|
|
219
|
+
"""Find the ``BaseNode`` subclass that corresponds to the given XML element."""
|
|
220
|
+
try:
|
|
221
|
+
node_class = self.parsers[element.tag]
|
|
222
|
+
except KeyError:
|
|
223
|
+
msg = f"Unsupported tag: <{element.tag}>"
|
|
224
|
+
if "{" not in element.tag:
|
|
225
|
+
msg = f"{msg} without an XML namespace"
|
|
226
|
+
if allowed_types:
|
|
227
|
+
# Show better exception message
|
|
228
|
+
types = ", ".join(chain.from_iterable(c.get_tag_names() for c in allowed_types))
|
|
229
|
+
msg = f"{msg}, expected one of: {types}"
|
|
230
|
+
|
|
231
|
+
raise ExternalParsingError(msg) from None
|
|
232
|
+
|
|
233
|
+
# Check whether the resolved class is indeed a valid option here.
|
|
234
|
+
if allowed_types is not None and not issubclass(node_class, allowed_types):
|
|
235
|
+
types = ", ".join(c.__name__ for c in allowed_types)
|
|
236
|
+
raise ExternalParsingError(
|
|
237
|
+
f"Unexpected {node_class.__name__} for <{element.tag}> node, "
|
|
238
|
+
f"expected one of: {types}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return node_class
|
|
242
|
+
|
|
243
|
+
def get_parser_class(self, xml_qname) -> type[BaseNode]:
|
|
244
|
+
"""Provide the parser class for a given XML Qualified name."""
|
|
245
|
+
return self.parsers[xml_qname]
|
|
246
|
+
|
|
247
|
+
def find_subclasses(self, node_type: type[BN]) -> list[type[BN]]:
|
|
248
|
+
"""Find all registered parsers for a given node."""
|
|
249
|
+
return {
|
|
250
|
+
tag: node_class
|
|
251
|
+
for tag, node_class in self.parsers.items()
|
|
252
|
+
if issubclass(node_class, node_type)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def expect_tag(namespace: xmlns | str, *tag_names: str):
|
|
257
|
+
"""Validate whether a given tag is need."""
|
|
258
|
+
valid_tags = {QName(namespace, name).text for name in tag_names}
|
|
259
|
+
expect0 = QName(namespace, tag_names[0]).text
|
|
260
|
+
|
|
261
|
+
def _wrapper(func):
|
|
262
|
+
@wraps(func)
|
|
263
|
+
def _expect_tag_decorator(cls, element: NSElement, *args, **kwargs):
|
|
264
|
+
if element.tag not in valid_tags:
|
|
265
|
+
raise ExternalParsingError(
|
|
266
|
+
f"{cls.__name__} parser expects an <{expect0}> node, got <{element.tag}>"
|
|
267
|
+
)
|
|
268
|
+
return func(cls, element, *args, **kwargs)
|
|
269
|
+
|
|
270
|
+
return _expect_tag_decorator
|
|
271
|
+
|
|
272
|
+
return _wrapper
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def expect_no_children(from_xml_func):
|
|
276
|
+
"""Validate that the XML tag has no child nodes."""
|
|
277
|
+
|
|
278
|
+
@wraps(from_xml_func)
|
|
279
|
+
def _expect_no_children_decorator(cls, element: NSElement, *args, **kwargs):
|
|
280
|
+
if len(element):
|
|
281
|
+
raise ExternalParsingError(
|
|
282
|
+
f"Unsupported child element for {element.tag} element: {element[0].tag}."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return from_xml_func(cls, element, *args, **kwargs)
|
|
286
|
+
|
|
287
|
+
return _expect_no_children_decorator
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def expect_children(min_child_nodes, *expect_types: str | type[BaseNode]):
|
|
291
|
+
"""Validate whether an element has enough children to continue parsing."""
|
|
292
|
+
known_tag_names = set()
|
|
293
|
+
for child_type in expect_types:
|
|
294
|
+
if isinstance(child_type, type) and issubclass(child_type, BaseNode):
|
|
295
|
+
known_tag_names.update(child_type.get_tag_names())
|
|
296
|
+
elif isinstance(child_type, str):
|
|
297
|
+
known_tag_names.add(child_type)
|
|
298
|
+
else:
|
|
299
|
+
raise TypeError(f"Unexpected {child_type!r}")
|
|
300
|
+
known_tag_names = sorted(known_tag_names)
|
|
301
|
+
|
|
302
|
+
def _wrapper(func):
|
|
303
|
+
@wraps(func)
|
|
304
|
+
def _expect_children_decorator(cls, element: NSElement, *args, **kwargs):
|
|
305
|
+
if len(element) < min_child_nodes:
|
|
306
|
+
type_names = ", ".join(known_tag_names)
|
|
307
|
+
suffix = f" (possible tags: {type_names})" if type_names else ""
|
|
308
|
+
raise ExternalParsingError(
|
|
309
|
+
f"<{element.tag}> should have {min_child_nodes} child nodes, "
|
|
310
|
+
f"got {len(element)}{suffix}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return func(cls, element, *args, **kwargs)
|
|
314
|
+
|
|
315
|
+
return _expect_children_decorator
|
|
316
|
+
|
|
317
|
+
return _wrapper
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
tag_registry = TagRegistry()
|
|
@@ -1,24 +1,28 @@
|
|
|
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.
|
|
7
|
+
|
|
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
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from . import lookups # noqa: F401 (need to register ORM lookups)
|
|
1
16
|
from .expressions import ValueReference
|
|
2
17
|
from .filters import Filter
|
|
3
|
-
from .functions import function_registry
|
|
4
18
|
from .identifiers import ResourceId
|
|
5
19
|
from .operators import IdOperator
|
|
6
|
-
from .query import CompiledQuery
|
|
7
20
|
from .sorting import SortBy
|
|
8
21
|
|
|
9
22
|
__all__ = [
|
|
10
23
|
"Filter",
|
|
11
|
-
"CompiledQuery",
|
|
12
24
|
"ValueReference",
|
|
13
25
|
"ResourceId",
|
|
14
26
|
"IdOperator",
|
|
15
27
|
"SortBy",
|
|
16
|
-
"function_registry",
|
|
17
28
|
]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def parse_resource_id_kvp(value) -> IdOperator:
|
|
21
|
-
"""Parse the RESOURCEID parameter.
|
|
22
|
-
This returns an IdOperator, as it needs to support multiple pairs.
|
|
23
|
-
"""
|
|
24
|
-
return IdOperator([ResourceId(rid) for rid in value.split(",")])
|
|
@@ -5,21 +5,27 @@ The class names are identical to those in the FES spec.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import operator
|
|
8
|
-
from dataclasses import dataclass
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
9
|
from datetime import date, datetime
|
|
10
10
|
from decimal import Decimal as D
|
|
11
11
|
from functools import cached_property
|
|
12
12
|
from typing import Union
|
|
13
|
-
from xml.etree.ElementTree import Element
|
|
14
13
|
|
|
15
|
-
from django.contrib.gis
|
|
14
|
+
from django.contrib.gis import geos
|
|
15
|
+
from django.contrib.gis.db import models as gis_models
|
|
16
16
|
from django.db import models
|
|
17
|
-
from django.db.models import
|
|
17
|
+
from django.db.models import Q, Value
|
|
18
18
|
from django.db.models.expressions import Combinable
|
|
19
19
|
|
|
20
20
|
from gisserver.exceptions import ExternalParsingError
|
|
21
|
-
from gisserver.
|
|
22
|
-
from gisserver.parsers.
|
|
21
|
+
from gisserver.extensions.functions import function_registry
|
|
22
|
+
from gisserver.parsers.ast import (
|
|
23
|
+
BaseNode,
|
|
24
|
+
TagNameEnum,
|
|
25
|
+
expect_no_children,
|
|
26
|
+
expect_tag,
|
|
27
|
+
tag_registry,
|
|
28
|
+
)
|
|
23
29
|
from gisserver.parsers.gml import (
|
|
24
30
|
GM_Envelope,
|
|
25
31
|
GM_Object,
|
|
@@ -27,12 +33,12 @@ from gisserver.parsers.gml import (
|
|
|
27
33
|
is_gml_element,
|
|
28
34
|
parse_gml_node,
|
|
29
35
|
)
|
|
30
|
-
from gisserver.parsers.
|
|
36
|
+
from gisserver.parsers.query import RhsTypes
|
|
31
37
|
from gisserver.parsers.values import auto_cast
|
|
32
|
-
from gisserver.
|
|
38
|
+
from gisserver.parsers.xml import NSElement, parse_qname, xmlns
|
|
39
|
+
from gisserver.types import ORMPath, XsdTypes
|
|
33
40
|
|
|
34
41
|
NoneType = type(None)
|
|
35
|
-
RhsTypes = Union[Combinable, Func, Q, GEOSGeometry, bool, int, str, date, datetime, tuple]
|
|
36
42
|
ParsedValue = Union[int, str, date, D, datetime, GM_Object, GM_Envelope, TM_Object, NoneType]
|
|
37
43
|
|
|
38
44
|
OUTPUT_FIELDS = {
|
|
@@ -43,14 +49,23 @@ OUTPUT_FIELDS = {
|
|
|
43
49
|
datetime: models.DateTimeField(),
|
|
44
50
|
float: models.FloatField(),
|
|
45
51
|
D: models.DecimalField(),
|
|
52
|
+
geos.GEOSGeometry: gis_models.GeometryField(),
|
|
53
|
+
geos.Point: gis_models.PointField(),
|
|
54
|
+
geos.LineString: gis_models.LineStringField(),
|
|
55
|
+
geos.LinearRing: gis_models.LineStringField(),
|
|
56
|
+
geos.Polygon: gis_models.PolygonField(),
|
|
57
|
+
geos.MultiPoint: gis_models.MultiPointField(),
|
|
58
|
+
geos.MultiPolygon: gis_models.MultiPolygonField(),
|
|
59
|
+
geos.MultiLineString: gis_models.MultiLineStringField(),
|
|
60
|
+
geos.GeometryCollection: gis_models.GeometryCollectionField(),
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
|
|
49
64
|
class BinaryOperatorType(TagNameEnum):
|
|
50
65
|
"""FES 1.0 Arithmetic operators.
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
still assume the server supports these. Hence these need to be included.
|
|
67
|
+
These are no longer part of the FES 2.0 spec, but clients (like QGis)
|
|
68
|
+
still assume the server supports these. Hence, these need to be included.
|
|
54
69
|
"""
|
|
55
70
|
|
|
56
71
|
Add = operator.add
|
|
@@ -60,9 +75,18 @@ class BinaryOperatorType(TagNameEnum):
|
|
|
60
75
|
|
|
61
76
|
|
|
62
77
|
class Expression(BaseNode):
|
|
63
|
-
"""Abstract base class, as defined by FES spec.
|
|
78
|
+
"""Abstract base class, as defined by FES spec.
|
|
79
|
+
|
|
80
|
+
The FES spec defines the following subclasses:
|
|
81
|
+
* :class:`ValueReference` (pointing to a field name)
|
|
82
|
+
* :class:`Literal` (a scalar value)
|
|
83
|
+
* :class:`Function` (a transformation for a value/field)
|
|
84
|
+
|
|
85
|
+
When code uses ``Expression.child_from_xml(element)``, the AST logic will
|
|
86
|
+
initialize the correct subclass for those elements.
|
|
87
|
+
"""
|
|
64
88
|
|
|
65
|
-
xml_ns =
|
|
89
|
+
xml_ns = xmlns.fes20
|
|
66
90
|
|
|
67
91
|
def build_lhs(self, compiler) -> str:
|
|
68
92
|
"""Get the expression as the left-hand-side of the equation.
|
|
@@ -86,7 +110,21 @@ class Expression(BaseNode):
|
|
|
86
110
|
@dataclass(repr=False)
|
|
87
111
|
@tag_registry.register("Literal")
|
|
88
112
|
class Literal(Expression):
|
|
89
|
-
"""The <fes:Literal> element that holds a literal value
|
|
113
|
+
"""The <fes:Literal> element that holds a literal value.
|
|
114
|
+
|
|
115
|
+
This can be a string value, possibly annotated with a type::
|
|
116
|
+
|
|
117
|
+
<fes:Literal type="xs:boolean">true</fes:Literal>
|
|
118
|
+
|
|
119
|
+
Following the spec, the value may also contain a complete geometry:
|
|
120
|
+
|
|
121
|
+
<fes:Literal>
|
|
122
|
+
<gml:Envelope xmlns:gml="http://www.opengis.net/gml/3.2" srsName="urn:ogc:def:crs:EPSG::4326">
|
|
123
|
+
<gml:lowerCorner>5.7 53.1</gml:lowerCorner>
|
|
124
|
+
<gml:upperCorner>6.1 53.5</gml:upperCorner>
|
|
125
|
+
</gml:Envelope>
|
|
126
|
+
</fes:Literal>
|
|
127
|
+
"""
|
|
90
128
|
|
|
91
129
|
# The XSD definition even defines a sequence of xsd:any as possible member!
|
|
92
130
|
raw_value: NoneType | str | GM_Object | GM_Envelope | TM_Object
|
|
@@ -100,7 +138,7 @@ class Literal(Expression):
|
|
|
100
138
|
|
|
101
139
|
@cached_property
|
|
102
140
|
def value(self) -> ParsedValue: # officially <xsd:any>
|
|
103
|
-
"""Access the value of the element,
|
|
141
|
+
"""Access the value of the element, cast to the appropriate data type."""
|
|
104
142
|
if not isinstance(self.raw_value, str):
|
|
105
143
|
return self.raw_value # GML element or None
|
|
106
144
|
elif self.type:
|
|
@@ -112,19 +150,20 @@ class Literal(Expression):
|
|
|
112
150
|
|
|
113
151
|
@cached_property
|
|
114
152
|
def type(self) -> XsdTypes | None:
|
|
153
|
+
"""Tell which datatype the literal holds.
|
|
154
|
+
This returns the type="..." value of the element.
|
|
155
|
+
"""
|
|
115
156
|
if not self.raw_type:
|
|
116
157
|
return None
|
|
117
158
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# No idea what XMLSchema was prefixed as (could be ns0 instead of xs:)
|
|
123
|
-
return XsdTypes(localname)
|
|
159
|
+
# The raw type is already translated into a fully qualified XML name,
|
|
160
|
+
# which also allows defining types as "ns0:string", "xs:string" or "xsd:string".
|
|
161
|
+
# These are directly matched to the XsdTypes enum value.
|
|
162
|
+
return XsdTypes(self.raw_type)
|
|
124
163
|
|
|
125
164
|
@classmethod
|
|
126
|
-
@expect_tag(
|
|
127
|
-
def from_xml(cls, element:
|
|
165
|
+
@expect_tag(xmlns.fes20, "Literal")
|
|
166
|
+
def from_xml(cls, element: NSElement):
|
|
128
167
|
children = len(element)
|
|
129
168
|
if not children:
|
|
130
169
|
# Common case: value is raw text
|
|
@@ -137,7 +176,10 @@ class Literal(Expression):
|
|
|
137
176
|
f"Unsupported child element for <Literal> element: {element[0].tag}."
|
|
138
177
|
)
|
|
139
178
|
|
|
140
|
-
return cls(
|
|
179
|
+
return cls(
|
|
180
|
+
raw_value=raw_value,
|
|
181
|
+
raw_type=parse_qname(element.attrib.get("type"), element.ns_aliases),
|
|
182
|
+
)
|
|
141
183
|
|
|
142
184
|
def build_lhs(self, compiler) -> str:
|
|
143
185
|
"""Alias the value when it's used in the left-hand-side.
|
|
@@ -174,6 +216,7 @@ class ValueReference(Expression):
|
|
|
174
216
|
"""
|
|
175
217
|
|
|
176
218
|
xpath: str
|
|
219
|
+
xpath_ns_aliases: dict[str, str] | None = field(compare=False, default=None)
|
|
177
220
|
|
|
178
221
|
def __str__(self):
|
|
179
222
|
return self.xpath
|
|
@@ -182,25 +225,26 @@ class ValueReference(Expression):
|
|
|
182
225
|
return f"ValueReference({self.xpath!r})"
|
|
183
226
|
|
|
184
227
|
@classmethod
|
|
185
|
-
@expect_tag(
|
|
186
|
-
|
|
187
|
-
|
|
228
|
+
@expect_tag(xmlns.fes20, "ValueReference", "PropertyName")
|
|
229
|
+
@expect_no_children
|
|
230
|
+
def from_xml(cls, element: NSElement):
|
|
231
|
+
return cls(xpath=element.text, xpath_ns_aliases=element.ns_aliases)
|
|
188
232
|
|
|
189
233
|
def build_lhs(self, compiler) -> str:
|
|
190
234
|
"""Optimized LHS: there is no need to alias a field lookup through an annotation."""
|
|
191
|
-
match = self.parse_xpath(compiler.
|
|
235
|
+
match = self.parse_xpath(compiler.feature_types)
|
|
192
236
|
return match.build_lhs(compiler)
|
|
193
237
|
|
|
194
238
|
def build_rhs(self, compiler) -> RhsTypes:
|
|
195
239
|
"""Return the value as F-expression"""
|
|
196
|
-
match = self.parse_xpath(compiler.
|
|
240
|
+
match = self.parse_xpath(compiler.feature_types)
|
|
197
241
|
return match.build_rhs(compiler)
|
|
198
242
|
|
|
199
|
-
def parse_xpath(self,
|
|
200
|
-
"""Convert the XPath into
|
|
201
|
-
if
|
|
243
|
+
def parse_xpath(self, feature_types: list) -> ORMPath:
|
|
244
|
+
"""Convert the XPath into the required ORM query elements."""
|
|
245
|
+
if feature_types:
|
|
202
246
|
# Can resolve against XSD paths, find the correct DB field name
|
|
203
|
-
return
|
|
247
|
+
return feature_types[0].resolve_element(self.xpath, self.xpath_ns_aliases)
|
|
204
248
|
else:
|
|
205
249
|
# Only used by unit testing (when feature_type is not given).
|
|
206
250
|
parts = [word.strip() for word in self.xpath.split("/")]
|
|
@@ -209,12 +253,7 @@ class ValueReference(Expression):
|
|
|
209
253
|
@cached_property
|
|
210
254
|
def element_name(self):
|
|
211
255
|
"""Tell which element this reference points to."""
|
|
212
|
-
|
|
213
|
-
pos = self.xpath.rindex("/")
|
|
214
|
-
except ValueError:
|
|
215
|
-
return self.xpath
|
|
216
|
-
else:
|
|
217
|
-
return self.xpath[pos + 1 :]
|
|
256
|
+
return self.xpath.rpartition("/")[2]
|
|
218
257
|
|
|
219
258
|
|
|
220
259
|
@dataclass
|
|
@@ -226,14 +265,14 @@ class Function(Expression):
|
|
|
226
265
|
arguments: list[Expression] # xsd:element ref="fes20:expression"
|
|
227
266
|
|
|
228
267
|
@classmethod
|
|
229
|
-
@expect_tag(
|
|
230
|
-
def from_xml(cls, element:
|
|
268
|
+
@expect_tag(xmlns.fes20, "Function")
|
|
269
|
+
def from_xml(cls, element: NSElement):
|
|
231
270
|
return cls(
|
|
232
|
-
name=
|
|
233
|
-
arguments=[Expression.
|
|
271
|
+
name=element.get_str_attribute("name"),
|
|
272
|
+
arguments=[Expression.child_from_xml(child) for child in element],
|
|
234
273
|
)
|
|
235
274
|
|
|
236
|
-
def build_rhs(self, compiler) ->
|
|
275
|
+
def build_rhs(self, compiler) -> models.Func:
|
|
237
276
|
"""Build the SQL function object"""
|
|
238
277
|
db_function = function_registry.resolve_function(self.name)
|
|
239
278
|
args = [arg.build_rhs(compiler) for arg in self.arguments]
|
|
@@ -241,24 +280,24 @@ class Function(Expression):
|
|
|
241
280
|
|
|
242
281
|
|
|
243
282
|
@dataclass
|
|
244
|
-
@tag_registry.
|
|
283
|
+
@tag_registry.register(BinaryOperatorType)
|
|
245
284
|
class BinaryOperator(Expression):
|
|
246
285
|
"""Support for FES 1.0 arithmetic operators.
|
|
247
286
|
|
|
248
|
-
|
|
249
|
-
still assume the server supports these. Hence these need to be included.
|
|
287
|
+
These are no longer part of the FES 2.0 spec, but clients (like QGis)
|
|
288
|
+
still assume the server supports these. Hence, these need to be included.
|
|
250
289
|
"""
|
|
251
290
|
|
|
252
291
|
_operatorType: BinaryOperatorType
|
|
253
292
|
expression: tuple[Expression, Expression]
|
|
254
293
|
|
|
255
294
|
@classmethod
|
|
256
|
-
def from_xml(cls, element:
|
|
295
|
+
def from_xml(cls, element: NSElement):
|
|
257
296
|
return cls(
|
|
258
297
|
_operatorType=BinaryOperatorType.from_xml(element),
|
|
259
298
|
expression=(
|
|
260
|
-
Expression.
|
|
261
|
-
Expression.
|
|
299
|
+
Expression.child_from_xml(element[0]),
|
|
300
|
+
Expression.child_from_xml(element[1]),
|
|
262
301
|
),
|
|
263
302
|
)
|
|
264
303
|
|