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
|
@@ -4,59 +4,56 @@ The class names and attributes are identical to those in the FES spec.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
import operator
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from decimal import Decimal
|
|
10
11
|
from enum import Enum
|
|
11
12
|
from functools import cached_property, reduce
|
|
12
13
|
from itertools import groupby
|
|
13
|
-
from typing import
|
|
14
|
-
from xml.etree.ElementTree import Element, QName
|
|
14
|
+
from typing import ClassVar, Protocol, Union
|
|
15
15
|
|
|
16
|
-
from django.conf import settings
|
|
17
16
|
from django.contrib.gis import measure
|
|
18
17
|
from django.db.models import Q
|
|
19
18
|
|
|
20
|
-
from gisserver.exceptions import
|
|
19
|
+
from gisserver.exceptions import (
|
|
20
|
+
ExternalParsingError,
|
|
21
|
+
InvalidParameterValue,
|
|
22
|
+
OperationProcessingFailed,
|
|
23
|
+
)
|
|
21
24
|
from gisserver.parsers import gml
|
|
22
|
-
from gisserver.parsers.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
from gisserver.parsers.ast import (
|
|
26
|
+
BaseNode,
|
|
27
|
+
TagNameEnum,
|
|
28
|
+
expect_children,
|
|
29
|
+
expect_tag,
|
|
30
|
+
tag_registry,
|
|
31
|
+
)
|
|
32
|
+
from gisserver.parsers.query import CompiledQuery, RhsTypes
|
|
33
|
+
from gisserver.parsers.values import fix_type_name
|
|
34
|
+
from gisserver.parsers.xml import NSElement, xmlns
|
|
35
|
+
from gisserver.types import GeometryXsdElement, XPathMatch
|
|
36
|
+
|
|
37
|
+
from .expressions import Expression, Literal, ValueReference
|
|
27
38
|
from .identifiers import Id
|
|
28
|
-
from .
|
|
39
|
+
from .lookups import ARRAY_LOOKUPS # also registers the lookups.
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
29
42
|
|
|
30
43
|
SpatialDescription = Union[gml.GM_Object, gml.GM_Envelope, ValueReference]
|
|
31
44
|
TemporalOperand = Union[gml.TM_Object, ValueReference]
|
|
32
45
|
|
|
46
|
+
# Fully qualified tag names
|
|
47
|
+
FES_VALUE_REFERENCE = xmlns.fes20.qname("ValueReference")
|
|
48
|
+
FES_DISTANCE = xmlns.fes20.qname("Distance")
|
|
49
|
+
FES_LOWER_BOUNDARY = xmlns.fes20.qname("LowerBoundary")
|
|
50
|
+
FES_UPPER_BOUNDARY = xmlns.fes20.qname("UpperBoundary")
|
|
33
51
|
|
|
34
|
-
# Define interface for any class that has "build_rhs()"
|
|
35
|
-
try:
|
|
36
|
-
from typing import Protocol
|
|
37
|
-
except ImportError:
|
|
38
|
-
HasBuildRhs = Any # Python 3.7 and below
|
|
39
|
-
else:
|
|
40
|
-
|
|
41
|
-
class HasBuildRhs(Protocol):
|
|
42
|
-
def build_rhs(self, compiler) -> RhsTypes: ...
|
|
43
52
|
|
|
53
|
+
class HasBuildRhs(Protocol):
|
|
54
|
+
"""Define interface for any class that has ``build_rhs()``."""
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
# Comparisons with array fields go through a separate ORM lookup expression,
|
|
47
|
-
# so these can check whether ANY element matches in the array.
|
|
48
|
-
# This gives consistency between other repeated elements (e.g. M2M, reverse FK)
|
|
49
|
-
# where the whole object is returned when one of the sub-objects match.
|
|
50
|
-
ARRAY_LOOKUPS = {
|
|
51
|
-
"exact": "fes_anyexact",
|
|
52
|
-
"fes_notequal": "fes_anynotequal",
|
|
53
|
-
"lt": "fes_anylt",
|
|
54
|
-
"lte": "fes_anylte",
|
|
55
|
-
"gt": "fes_anygt",
|
|
56
|
-
"gte": "fes_anygte",
|
|
57
|
-
}
|
|
58
|
-
else:
|
|
59
|
-
ARRAY_LOOKUPS = None
|
|
56
|
+
def build_rhs(self, compiler) -> RhsTypes: ...
|
|
60
57
|
|
|
61
58
|
|
|
62
59
|
class MatchAction(Enum):
|
|
@@ -73,7 +70,7 @@ class MatchAction(Enum):
|
|
|
73
70
|
|
|
74
71
|
class BinaryComparisonName(TagNameEnum):
|
|
75
72
|
"""XML tag names for value comparisons.
|
|
76
|
-
|
|
73
|
+
This also maps their names to ORM field lookups.
|
|
77
74
|
"""
|
|
78
75
|
|
|
79
76
|
PropertyIsEqualTo = "exact"
|
|
@@ -85,14 +82,14 @@ class BinaryComparisonName(TagNameEnum):
|
|
|
85
82
|
|
|
86
83
|
|
|
87
84
|
class DistanceOperatorName(TagNameEnum):
|
|
88
|
-
"""XML tag names
|
|
85
|
+
"""XML tag names mapped to distance operators for the ORM."""
|
|
89
86
|
|
|
90
87
|
Beyond = "fes_beyond" # using __distance_gt=.. would be slower.
|
|
91
88
|
DWithin = "dwithin" # ST_DWithin uses indices, distance_lte does not.
|
|
92
89
|
|
|
93
90
|
|
|
94
91
|
class SpatialOperatorName(TagNameEnum):
|
|
95
|
-
"""XML tag names
|
|
92
|
+
"""XML tag names mapped to geometry operators.
|
|
96
93
|
|
|
97
94
|
The values correspond with GeoDjango operators. So a ``BBOX`` query
|
|
98
95
|
will translate into ``geometry__intersects=Polygon(...)``.
|
|
@@ -103,18 +100,18 @@ class SpatialOperatorName(TagNameEnum):
|
|
|
103
100
|
# BBOX can either be implemented using bboverlaps (more efficient), or the
|
|
104
101
|
# more correct "intersects" option (e.g. a line near the box would match otherwise).
|
|
105
102
|
BBOX = "intersects" # ISO version: "NOT DISJOINT"
|
|
106
|
-
Equals = "equals" # Test whether
|
|
103
|
+
Equals = "equals" # Test whether two geometries are topologically equal
|
|
107
104
|
Disjoint = "disjoint" # Tests whether two geometries are disjoint (do not interact)
|
|
108
105
|
Intersects = "intersects" # Tests whether two geometries intersect
|
|
109
|
-
Touches = "touches" # Tests whether two geometries touch
|
|
110
|
-
Crosses = "crosses" # Tests whether two geometries cross
|
|
111
|
-
Within = "within" # Tests
|
|
112
|
-
Contains = "contains" # Tests
|
|
106
|
+
Touches = "touches" # Tests whether two geometries touch (e.g. country border).
|
|
107
|
+
Crosses = "crosses" # Tests whether two geometries cross (e.g. two streets).
|
|
108
|
+
Within = "within" # Tests a geometry is within another one (e.g. city within province).
|
|
109
|
+
Contains = "contains" # Tests a geometry contains another one (e.g. province contains city).
|
|
113
110
|
Overlaps = "overlaps" # Test whether two geometries overlap
|
|
114
111
|
|
|
115
112
|
|
|
116
113
|
class TemporalOperatorName(TagNameEnum):
|
|
117
|
-
"""XML tag names
|
|
114
|
+
"""XML tag names mapped to datetime operators.
|
|
118
115
|
|
|
119
116
|
Explanation here: http://old.geotools.org/Temporal-Filters_211091519.html
|
|
120
117
|
and: https://github.com/geotools/geotools/wiki/temporal-filters
|
|
@@ -150,26 +147,44 @@ class UnaryLogicType(TagNameEnum):
|
|
|
150
147
|
|
|
151
148
|
@dataclass
|
|
152
149
|
class Measure(BaseNode):
|
|
153
|
-
"""
|
|
150
|
+
"""A measurement for a distance element.
|
|
154
151
|
|
|
155
|
-
|
|
152
|
+
This parses and handles the syntax::
|
|
153
|
+
|
|
154
|
+
<fes:Distance uom="...">value</fes:Distance>
|
|
155
|
+
|
|
156
|
+
This element is used within the :class:`DistanceOperator` that
|
|
157
|
+
handles the ``<fes:DWithin>`` and ``<fes:Beyond>`` tags.
|
|
158
|
+
|
|
159
|
+
The "unit of measurement" (uom) supports most standard units, like meters (``m``),
|
|
160
|
+
kilometers (``km``), nautical mile (``nm``), miles (``mi``), inches (``inch``).
|
|
161
|
+
The full list can be found at: https://docs.djangoproject.com/en/5.1/ref/contrib/gis/measure/#supported-units
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
xml_ns = xmlns.fes20
|
|
156
165
|
|
|
157
166
|
value: Decimal
|
|
158
167
|
uom: str # Unit of measurement, fes20:UomSymbol | fes20:UomURI
|
|
159
168
|
|
|
160
169
|
@classmethod
|
|
161
|
-
@expect_tag(
|
|
162
|
-
def from_xml(cls, element:
|
|
163
|
-
return cls(value=Decimal(element.text), uom=
|
|
170
|
+
@expect_tag(xmlns.fes20, "Distance")
|
|
171
|
+
def from_xml(cls, element: NSElement):
|
|
172
|
+
return cls(value=Decimal(element.text), uom=element.get_str_attribute("uom"))
|
|
164
173
|
|
|
165
174
|
def build_rhs(self, compiler) -> measure.Distance:
|
|
166
175
|
return measure.Distance(default_unit=self.uom, **{self.uom: self.value})
|
|
167
176
|
|
|
168
177
|
|
|
169
178
|
class Operator(BaseNode):
|
|
170
|
-
"""Abstract base class, as defined by FES spec.
|
|
179
|
+
"""Abstract base class, as defined by FES spec.
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
This base class is also used in parsing; for example the ``<fes:Filter>``
|
|
182
|
+
tag only allows ``Operator`` and ``Expression`` subclasses as allowed arguments.
|
|
183
|
+
Having all those classes as Python types, makes it very easy to validate
|
|
184
|
+
whether a given child element is the expected node type.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
xml_ns = xmlns.fes20
|
|
173
188
|
|
|
174
189
|
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
175
190
|
raise NotImplementedError(f"Using {self.__class__.__name__} is not supported yet.")
|
|
@@ -181,8 +196,7 @@ class IdOperator(Operator):
|
|
|
181
196
|
|
|
182
197
|
id: list[Id]
|
|
183
198
|
|
|
184
|
-
|
|
185
|
-
def type_names(self) -> list[str]:
|
|
199
|
+
def get_type_names(self) -> list[str]:
|
|
186
200
|
"""Provide a list of all type names accessed by this operator"""
|
|
187
201
|
return [type_name for type_name in self.grouped_ids if type_name is not None]
|
|
188
202
|
|
|
@@ -192,7 +206,7 @@ class IdOperator(Operator):
|
|
|
192
206
|
ids = sorted(self.id, key=operator.attrgetter("rid"))
|
|
193
207
|
return {
|
|
194
208
|
type_name: list(items)
|
|
195
|
-
for type_name, items in groupby(ids, key=
|
|
209
|
+
for type_name, items in groupby(ids, key=lambda id: id.get_type_name())
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
def build_query(self, compiler):
|
|
@@ -207,8 +221,20 @@ class IdOperator(Operator):
|
|
|
207
221
|
compiler.mark_empty()
|
|
208
222
|
return
|
|
209
223
|
|
|
224
|
+
type_names = {ft.xml_name for ft in compiler.feature_types}
|
|
210
225
|
for type_name, items in self.grouped_ids.items():
|
|
211
|
-
|
|
226
|
+
type_name = fix_type_name(type_name, compiler.feature_types[0].xml_namespace)
|
|
227
|
+
if type_name not in type_names:
|
|
228
|
+
# When ResourceId + typenames is defined, it should be a value from typenames see WFS spec 7.9.2.4.1
|
|
229
|
+
# This is tested here for RESOURCEID with a typename.id format.
|
|
230
|
+
# Otherwise, this breaks the CITE RESOURCEID=test-UUID parameter.
|
|
231
|
+
raise InvalidParameterValue(
|
|
232
|
+
"When TYPENAMES and RESOURCEID are combined, "
|
|
233
|
+
"the RESOURCEID type should be included in TYPENAMES.",
|
|
234
|
+
locator="resourceId",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
ids_subset = reduce(operator.or_, [id.build_query(compiler) for id in items])
|
|
212
238
|
compiler.add_lookups(ids_subset, type_name=type_name)
|
|
213
239
|
|
|
214
240
|
|
|
@@ -217,9 +243,13 @@ class NonIdOperator(Operator):
|
|
|
217
243
|
|
|
218
244
|
This is used for nearly all operators,
|
|
219
245
|
except those that have <fes:ResourceId> elements as children.
|
|
246
|
+
|
|
247
|
+
Some operators, such as the ``<fes:And>``, ``<fes:Or>`` and ``<fes:Not>`` operators
|
|
248
|
+
explicitly support only ``NonIdOperator`` elements as arguments.
|
|
249
|
+
Hence, having this base class as Python type simplifies parsing.
|
|
220
250
|
"""
|
|
221
251
|
|
|
222
|
-
_source = None
|
|
252
|
+
_source = None # often declared as non-comparing in dataclasses below.
|
|
223
253
|
allow_geometries = False
|
|
224
254
|
|
|
225
255
|
def build_compare(
|
|
@@ -238,7 +268,7 @@ class NonIdOperator(Operator):
|
|
|
238
268
|
if isinstance(lhs, Literal) and isinstance(rhs, ValueReference):
|
|
239
269
|
lhs, rhs = rhs, lhs
|
|
240
270
|
|
|
241
|
-
if compiler.
|
|
271
|
+
if compiler.feature_types:
|
|
242
272
|
lookup = self.validate_comparison(compiler, lhs, lookup, rhs)
|
|
243
273
|
|
|
244
274
|
# Build Django Q-object
|
|
@@ -255,17 +285,27 @@ class NonIdOperator(Operator):
|
|
|
255
285
|
compiler: CompiledQuery,
|
|
256
286
|
lhs: Expression,
|
|
257
287
|
lookup: str,
|
|
258
|
-
rhs: Expression | RhsTypes,
|
|
288
|
+
rhs: Expression | gml.GM_Object | RhsTypes,
|
|
259
289
|
):
|
|
260
290
|
"""Validate whether a given comparison is even possible.
|
|
261
|
-
|
|
291
|
+
|
|
292
|
+
For example, comparisons like ``name == "test"`` are fine,
|
|
293
|
+
but ``geometry < 4`` or ``datefield == 35.2" raise an error.
|
|
294
|
+
|
|
295
|
+
The lhs/rhs are expected to be ordered in a logical sequence.
|
|
296
|
+
So ``<value> == <element>`` should be provided as ``<element> == <value>``.
|
|
297
|
+
|
|
298
|
+
:param compiler: The object that holds the intermediate state
|
|
299
|
+
:param lhs: The left-hand-side of the comparison (e.g. the element).
|
|
300
|
+
:param lookup: The ORM lookup expression being used (e.g. ``equals`` or ``fes_like``).
|
|
301
|
+
:param rhs: The right-hand-side of the comparison (e.g. the value).
|
|
262
302
|
"""
|
|
263
303
|
if isinstance(lhs, ValueReference):
|
|
264
|
-
xsd_element =
|
|
304
|
+
xsd_element = lhs.parse_xpath(compiler.feature_types).child
|
|
265
305
|
tag = self._source if self._source is not None else None
|
|
266
306
|
|
|
267
307
|
# e.g. deny <PropertyIsLessThanOrEqualTo> against <gml:boundedBy>
|
|
268
|
-
if xsd_element.is_geometry and not self.allow_geometries:
|
|
308
|
+
if xsd_element.type.is_geometry and not self.allow_geometries:
|
|
269
309
|
raise OperationProcessingFailed(
|
|
270
310
|
f"Operator '{tag}' does not support comparing"
|
|
271
311
|
f" geometry properties: '{xsd_element.xml_name}'.",
|
|
@@ -273,29 +313,30 @@ class NonIdOperator(Operator):
|
|
|
273
313
|
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
274
314
|
)
|
|
275
315
|
|
|
276
|
-
if isinstance(rhs, Literal):
|
|
277
|
-
# Since the element is resolved, inform the Literal how to parse the value.
|
|
278
|
-
# This avoids various validation errors along the path.
|
|
279
|
-
rhs.bind_type(xsd_element.type)
|
|
280
|
-
|
|
281
|
-
# When a common case of value comparison is done, the inputs
|
|
282
|
-
# can be validated before the ORM query is constructed.
|
|
283
|
-
xsd_element.validate_comparison(rhs.raw_value, lookup=lookup, tag=tag)
|
|
284
|
-
|
|
285
316
|
# Checking scalar values against array fields will fail.
|
|
286
317
|
# However, to make the queries consistent with other unbounded types (i.e. M2M fields),
|
|
287
318
|
# it makes sense to return an object when *one* entry in the array matches.
|
|
288
319
|
if xsd_element.is_array:
|
|
289
320
|
try:
|
|
290
|
-
|
|
321
|
+
lookup = ARRAY_LOOKUPS[lookup]
|
|
291
322
|
except KeyError:
|
|
323
|
+
logger.debug("No array lookup known for %s", lookup)
|
|
292
324
|
raise OperationProcessingFailed(
|
|
293
|
-
"filter",
|
|
294
325
|
f"Operator '{tag}' is not supported for "
|
|
295
326
|
f"the '{xsd_element.name}' property.",
|
|
327
|
+
locator="filter",
|
|
296
328
|
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
297
329
|
) from None
|
|
298
330
|
|
|
331
|
+
if isinstance(rhs, Literal):
|
|
332
|
+
# Since the element is resolved, inform the Literal how to parse the value.
|
|
333
|
+
# This avoids various validation errors along the path.
|
|
334
|
+
rhs.bind_type(xsd_element.type)
|
|
335
|
+
|
|
336
|
+
# When a common case of value comparison is done, the inputs
|
|
337
|
+
# can be validated before the ORM query is constructed.
|
|
338
|
+
xsd_element.validate_comparison(rhs.raw_value, lookup=lookup, tag=tag)
|
|
339
|
+
|
|
299
340
|
return lookup
|
|
300
341
|
|
|
301
342
|
def build_compare_between(
|
|
@@ -303,10 +344,10 @@ class NonIdOperator(Operator):
|
|
|
303
344
|
compiler: CompiledQuery,
|
|
304
345
|
lhs: Expression,
|
|
305
346
|
lookup: str,
|
|
306
|
-
rhs: tuple[
|
|
347
|
+
rhs: tuple[Expression | ValueReference | gml.GM_Object, Expression | Measure],
|
|
307
348
|
) -> Q:
|
|
308
349
|
"""Use the value in comparison with 2 other values (e.g. between query)"""
|
|
309
|
-
if compiler.
|
|
350
|
+
if compiler.feature_types:
|
|
310
351
|
self.validate_comparison(compiler, lhs, lookup, rhs[0])
|
|
311
352
|
self.validate_comparison(compiler, lhs, lookup, rhs[1])
|
|
312
353
|
|
|
@@ -327,9 +368,21 @@ class SpatialOperator(NonIdOperator):
|
|
|
327
368
|
|
|
328
369
|
|
|
329
370
|
@dataclass
|
|
330
|
-
@tag_registry.
|
|
371
|
+
@tag_registry.register("Beyond")
|
|
372
|
+
@tag_registry.register("DWithin")
|
|
331
373
|
class DistanceOperator(SpatialOperator):
|
|
332
|
-
"""Comparing the distance to a geometry.
|
|
374
|
+
"""Comparing the distance to a geometry.
|
|
375
|
+
|
|
376
|
+
This parses and handles the syntax::
|
|
377
|
+
|
|
378
|
+
<fes:DWithin>
|
|
379
|
+
<fes:ValueReference>geometry</fes:ValueReference>
|
|
380
|
+
<gml:Point srsDimension="2">
|
|
381
|
+
<gml:pos>43.55749 1.525864</gml:pos>
|
|
382
|
+
</gml:Point>
|
|
383
|
+
<fes:Distance oum="m:>100</fes:Distance>
|
|
384
|
+
</fes:DWithin>
|
|
385
|
+
"""
|
|
333
386
|
|
|
334
387
|
allow_geometries = True # override static attribute
|
|
335
388
|
|
|
@@ -341,7 +394,7 @@ class DistanceOperator(SpatialOperator):
|
|
|
341
394
|
|
|
342
395
|
@classmethod
|
|
343
396
|
@expect_children(3, ValueReference, gml.GM_Object, Measure)
|
|
344
|
-
def from_xml(cls, element:
|
|
397
|
+
def from_xml(cls, element: NSElement):
|
|
345
398
|
geometries = gml.find_gml_nodes(element)
|
|
346
399
|
if not geometries:
|
|
347
400
|
raise ExternalParsingError(f"Missing gml element in <{element.tag}>")
|
|
@@ -349,10 +402,10 @@ class DistanceOperator(SpatialOperator):
|
|
|
349
402
|
raise ExternalParsingError(f"Multiple gml elements found in <{element.tag}>")
|
|
350
403
|
|
|
351
404
|
return cls(
|
|
352
|
-
valueReference=ValueReference.from_xml(
|
|
405
|
+
valueReference=ValueReference.from_xml(element.find(FES_VALUE_REFERENCE)),
|
|
353
406
|
operatorType=DistanceOperatorName.from_xml(element),
|
|
354
407
|
geometry=gml.parse_gml_node(geometries[0]),
|
|
355
|
-
distance=Measure.from_xml(
|
|
408
|
+
distance=Measure.from_xml(element.find(FES_DISTANCE)),
|
|
356
409
|
_source=element.tag,
|
|
357
410
|
)
|
|
358
411
|
|
|
@@ -366,9 +419,23 @@ class DistanceOperator(SpatialOperator):
|
|
|
366
419
|
|
|
367
420
|
|
|
368
421
|
@dataclass
|
|
369
|
-
@tag_registry.
|
|
422
|
+
@tag_registry.register(SpatialOperatorName) # <BBOX>, <Equals>, ...
|
|
370
423
|
class BinarySpatialOperator(SpatialOperator):
|
|
371
|
-
"""A comparison of geometries using 2 values, e.g. A Within B.
|
|
424
|
+
"""A comparison of geometries using 2 values, e.g. A Within B.
|
|
425
|
+
|
|
426
|
+
This parses and handles the syntax, and its variants::
|
|
427
|
+
|
|
428
|
+
<fes:BBOX>
|
|
429
|
+
<fes:ValueReference>Geometry</fes:ValueReference>
|
|
430
|
+
<gml:Envelope srsName="http://www.opengis.net/def/crs/epsg/0/4326">
|
|
431
|
+
<gml:lowerCorner>13.0983 31.5899</gml:lowerCorner>
|
|
432
|
+
<gml:upperCorner>35.5472 42.8143</gml:upperCorner>
|
|
433
|
+
</gml:Envelope>
|
|
434
|
+
</fes:BBOX>
|
|
435
|
+
|
|
436
|
+
It also handles the ``<fes:Equals>``, ``<fes:Within>``, ``<fes:Intersects>``, etc..
|
|
437
|
+
that exist in the :class:`SpatialOperatorName` enum.
|
|
438
|
+
"""
|
|
372
439
|
|
|
373
440
|
allow_geometries = True # override static attribute
|
|
374
441
|
|
|
@@ -378,7 +445,7 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
378
445
|
_source: str | None = field(compare=False, default=None)
|
|
379
446
|
|
|
380
447
|
@classmethod
|
|
381
|
-
def from_xml(cls, element:
|
|
448
|
+
def from_xml(cls, element: NSElement):
|
|
382
449
|
operator_type = SpatialOperatorName.from_xml(element)
|
|
383
450
|
if operator_type is SpatialOperatorName.BBOX and len(element) == 1:
|
|
384
451
|
# For BBOX, the geometry operator is optional
|
|
@@ -392,16 +459,15 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
392
459
|
return cls(
|
|
393
460
|
operatorType=operator_type,
|
|
394
461
|
operand1=ValueReference.from_xml(ref) if ref is not None else None,
|
|
395
|
-
operand2=tag_registry.
|
|
396
|
-
geo, allowed_types=SpatialDescription.__args__ # get_args() in 3.8
|
|
397
|
-
),
|
|
462
|
+
operand2=tag_registry.node_from_xml(geo, allowed_types=SpatialDescription.__args__),
|
|
398
463
|
_source=element.tag,
|
|
399
464
|
)
|
|
400
465
|
|
|
401
466
|
def build_query(self, compiler: CompiledQuery) -> Q:
|
|
402
467
|
operant1 = self.operand1
|
|
403
468
|
if operant1 is None:
|
|
404
|
-
|
|
469
|
+
# BBOX element points to the existing geometry element by default.
|
|
470
|
+
operant1 = _ResolvedValueReference(compiler.feature_types[0].main_geometry_element)
|
|
405
471
|
|
|
406
472
|
return self.build_compare(
|
|
407
473
|
compiler,
|
|
@@ -411,10 +477,61 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
411
477
|
)
|
|
412
478
|
|
|
413
479
|
|
|
480
|
+
class _ResolvedValueReference(ValueReference):
|
|
481
|
+
"""Internal ValueReference subclass that already knows which element it resolve to.
|
|
482
|
+
This avoids translating back and forth between an XPath path and the desired node.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
def __init__(self, geo_element: GeometryXsdElement):
|
|
486
|
+
self.xpath = f"(mocked {geo_element.orm_path})" # keep __str__ and __repr__ happy.
|
|
487
|
+
self.xpath_ns_aliases = {}
|
|
488
|
+
self.geo_element = geo_element
|
|
489
|
+
self.orm_path = geo_element.orm_path
|
|
490
|
+
|
|
491
|
+
def parse_xpath(
|
|
492
|
+
self, feature_types: list, ns_aliases: dict[str, str] | None = None
|
|
493
|
+
) -> XPathMatch:
|
|
494
|
+
return XPathMatch(feature_types[0], [self.geo_element], query="")
|
|
495
|
+
|
|
496
|
+
def build_lhs(self, compiler: CompiledQuery):
|
|
497
|
+
return self.orm_path
|
|
498
|
+
|
|
499
|
+
|
|
414
500
|
@dataclass
|
|
415
|
-
@tag_registry.
|
|
501
|
+
@tag_registry.register(TemporalOperatorName) # <After>, <Before>, ...
|
|
416
502
|
class TemporalOperator(NonIdOperator):
|
|
417
|
-
"""Comparisons with dates
|
|
503
|
+
"""Comparisons with dates.
|
|
504
|
+
|
|
505
|
+
For these operators, only the parsing is implemented.
|
|
506
|
+
These are not translated into ORM queries yet.
|
|
507
|
+
|
|
508
|
+
It supports a syntax such as::
|
|
509
|
+
|
|
510
|
+
<fes:TEquals>
|
|
511
|
+
<fes:ValueReference>SimpleTrajectory/gml:validTime/gml:TimeInstant</fes:ValueReference>
|
|
512
|
+
<gml:TimeInstant gml:id="TI1">
|
|
513
|
+
<gml:timePosition>2005-05-19T09:28:40Z</gml:timePosition>
|
|
514
|
+
</gml:TimeInstant>
|
|
515
|
+
</fes:TEquals>
|
|
516
|
+
|
|
517
|
+
or::
|
|
518
|
+
|
|
519
|
+
<fes:During>
|
|
520
|
+
<fes:ValueReference>SimpleTrajectory/gml:validTime/gml:TimeInstant</fes:ValueReference>
|
|
521
|
+
<gml:TimePeriod gml:id="TP1">
|
|
522
|
+
<gml:begin>
|
|
523
|
+
<gml:TimeInstant gml:id="TI1">
|
|
524
|
+
<gml:timePosition>2005-05-17T00:00:00Z</gml:timePosition>
|
|
525
|
+
</gml:TimeInstant>
|
|
526
|
+
</gml:begin>
|
|
527
|
+
<gml:end>
|
|
528
|
+
<gml:TimeInstant gml:id="TI2">
|
|
529
|
+
<gml:timePosition>2005-05-23T00:00:00Z</gml:timePosition>
|
|
530
|
+
</gml:TimeInstant>
|
|
531
|
+
</gml:end>
|
|
532
|
+
</gml:TimePeriod>
|
|
533
|
+
</fes:During>
|
|
534
|
+
"""
|
|
418
535
|
|
|
419
536
|
operatorType: TemporalOperatorName
|
|
420
537
|
operand1: ValueReference
|
|
@@ -422,20 +539,23 @@ class TemporalOperator(NonIdOperator):
|
|
|
422
539
|
_source: str | None = field(compare=False, default=None)
|
|
423
540
|
|
|
424
541
|
@classmethod
|
|
425
|
-
@expect_children(2, ValueReference, TemporalOperand)
|
|
426
|
-
def from_xml(cls, element:
|
|
542
|
+
@expect_children(2, ValueReference, *TemporalOperand.__args__)
|
|
543
|
+
def from_xml(cls, element: NSElement):
|
|
427
544
|
return cls(
|
|
428
545
|
operatorType=TemporalOperatorName.from_xml(element),
|
|
429
546
|
operand1=ValueReference.from_xml(element[0]),
|
|
430
|
-
operand2=tag_registry.
|
|
431
|
-
element[1], allowed_types=TemporalOperand.__args__
|
|
547
|
+
operand2=tag_registry.node_from_xml(
|
|
548
|
+
element[1], allowed_types=TemporalOperand.__args__
|
|
432
549
|
),
|
|
433
550
|
_source=element.tag,
|
|
434
551
|
)
|
|
435
552
|
|
|
436
553
|
|
|
437
554
|
class ComparisonOperator(NonIdOperator):
|
|
438
|
-
"""Base class for comparisons
|
|
555
|
+
"""Base class for comparisons.
|
|
556
|
+
This class name mirrors the fes-spec name,
|
|
557
|
+
and allows grouping various comparisons together.
|
|
558
|
+
"""
|
|
439
559
|
|
|
440
560
|
# Start counting fresh here, to collect the capabilities
|
|
441
561
|
# that are listed in the <fes20:ComparisonOperators> node:
|
|
@@ -443,9 +563,23 @@ class ComparisonOperator(NonIdOperator):
|
|
|
443
563
|
|
|
444
564
|
|
|
445
565
|
@dataclass
|
|
446
|
-
@tag_registry.
|
|
566
|
+
@tag_registry.register(BinaryComparisonName) # <PropertyIs...>
|
|
447
567
|
class BinaryComparisonOperator(ComparisonOperator):
|
|
448
|
-
"""A comparison between 2 values, e.g. A == B.
|
|
568
|
+
"""A comparison between 2 values, e.g. A == B.
|
|
569
|
+
|
|
570
|
+
This parses and handles the syntax::
|
|
571
|
+
|
|
572
|
+
<fes:PropertyIsEqualTo>
|
|
573
|
+
<fes:ValueReference>city/name</fes:ValueReference>
|
|
574
|
+
<fes:Literal>CloudCity</fes:Literal>
|
|
575
|
+
</fes:PropertyIsEqualTo>
|
|
576
|
+
|
|
577
|
+
...and all variations (``<fes:PropertyIsLessThan>``, etc...)
|
|
578
|
+
that are listed in the :class:`BinaryComparisonName` enum.
|
|
579
|
+
|
|
580
|
+
Note that both arguments are expressions, so these can be value references, literals
|
|
581
|
+
or functions. The ``<fes:Literal>`` element may hold a GML element as its value.
|
|
582
|
+
"""
|
|
449
583
|
|
|
450
584
|
operatorType: BinaryComparisonName
|
|
451
585
|
expression: tuple[Expression, Expression]
|
|
@@ -455,12 +589,12 @@ class BinaryComparisonOperator(ComparisonOperator):
|
|
|
455
589
|
|
|
456
590
|
@classmethod
|
|
457
591
|
@expect_children(2, Expression, Expression)
|
|
458
|
-
def from_xml(cls, element:
|
|
592
|
+
def from_xml(cls, element: NSElement):
|
|
459
593
|
return cls(
|
|
460
594
|
operatorType=BinaryComparisonName.from_xml(element),
|
|
461
595
|
expression=(
|
|
462
|
-
Expression.
|
|
463
|
-
Expression.
|
|
596
|
+
Expression.child_from_xml(element[0]),
|
|
597
|
+
Expression.child_from_xml(element[1]),
|
|
464
598
|
),
|
|
465
599
|
matchCase=element.get("matchCase", True),
|
|
466
600
|
matchAction=MatchAction(element.get("matchAction", default=MatchAction.Any)),
|
|
@@ -475,7 +609,19 @@ class BinaryComparisonOperator(ComparisonOperator):
|
|
|
475
609
|
@dataclass
|
|
476
610
|
@tag_registry.register("PropertyIsBetween")
|
|
477
611
|
class BetweenComparisonOperator(ComparisonOperator):
|
|
478
|
-
"""Check whether a value is between two elements.
|
|
612
|
+
"""Check whether a value is between two elements.
|
|
613
|
+
|
|
614
|
+
This parses and handles the syntax::
|
|
615
|
+
|
|
616
|
+
<fes:PropertyIsBetween>
|
|
617
|
+
<fes:ValueReference>DEPTH</fes:ValueReference>
|
|
618
|
+
<fes:LowerBoundary><fes:Literal>100</fes:Literal></fes:LowerBoundary>
|
|
619
|
+
<fes:UpperBoundary><fes:Literal>200</fes:Literal></fes:UpperBoundary>
|
|
620
|
+
</fes:PropertyIsBetween>
|
|
621
|
+
|
|
622
|
+
Note that both boundary arguments receive expressions, so these can be value
|
|
623
|
+
references, literals or functions!
|
|
624
|
+
"""
|
|
479
625
|
|
|
480
626
|
expression: Expression
|
|
481
627
|
lowerBoundary: Expression
|
|
@@ -484,17 +630,15 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
484
630
|
|
|
485
631
|
@classmethod
|
|
486
632
|
@expect_children(3, Expression, "LowerBoundary", "UpperBoundary")
|
|
487
|
-
def from_xml(cls, element:
|
|
488
|
-
if element[1].tag !=
|
|
489
|
-
FES20, "UpperBoundary"
|
|
490
|
-
):
|
|
633
|
+
def from_xml(cls, element: NSElement):
|
|
634
|
+
if (element[1].tag != FES_LOWER_BOUNDARY) or (element[2].tag != FES_UPPER_BOUNDARY):
|
|
491
635
|
raise ExternalParsingError(
|
|
492
636
|
f"{element.tag} should have 3 child nodes: "
|
|
493
637
|
f"(expression), <LowerBoundary>, <UpperBoundary>"
|
|
494
638
|
)
|
|
495
639
|
|
|
496
|
-
lower =
|
|
497
|
-
upper =
|
|
640
|
+
lower = element[1]
|
|
641
|
+
upper = element[2]
|
|
498
642
|
|
|
499
643
|
if len(lower) != 1:
|
|
500
644
|
raise ExternalParsingError(f"{lower.tag} should have 1 expression child node")
|
|
@@ -502,9 +646,9 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
502
646
|
raise ExternalParsingError(f"{upper.tag} should have 1 expression child node")
|
|
503
647
|
|
|
504
648
|
return cls(
|
|
505
|
-
expression=Expression.
|
|
506
|
-
lowerBoundary=Expression.
|
|
507
|
-
upperBoundary=Expression.
|
|
649
|
+
expression=Expression.child_from_xml(element[0]),
|
|
650
|
+
lowerBoundary=Expression.child_from_xml(lower[0]),
|
|
651
|
+
upperBoundary=Expression.child_from_xml(upper[0]),
|
|
508
652
|
_source=element.tag,
|
|
509
653
|
)
|
|
510
654
|
|
|
@@ -520,7 +664,15 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
520
664
|
@dataclass
|
|
521
665
|
@tag_registry.register("PropertyIsLike")
|
|
522
666
|
class LikeOperator(ComparisonOperator):
|
|
523
|
-
"""Perform wildcard matching.
|
|
667
|
+
"""Perform wildcard matching.
|
|
668
|
+
|
|
669
|
+
This parses and handles the syntax::
|
|
670
|
+
|
|
671
|
+
<fes:PropertyIsLike wildCard="*" singleChar="#" escapeChar="!">
|
|
672
|
+
<fes:ValueReference>last_name</fes:ValueReference>
|
|
673
|
+
<fes:Literal>John*</fes:Literal>
|
|
674
|
+
</fes:PropertyIsLike>
|
|
675
|
+
"""
|
|
524
676
|
|
|
525
677
|
expression: tuple[Expression, Expression]
|
|
526
678
|
wildCard: str
|
|
@@ -530,16 +682,16 @@ class LikeOperator(ComparisonOperator):
|
|
|
530
682
|
|
|
531
683
|
@classmethod
|
|
532
684
|
@expect_children(2, Expression, Expression)
|
|
533
|
-
def from_xml(cls, element:
|
|
685
|
+
def from_xml(cls, element: NSElement):
|
|
534
686
|
return cls(
|
|
535
687
|
expression=(
|
|
536
|
-
Expression.
|
|
537
|
-
Expression.
|
|
688
|
+
Expression.child_from_xml(element[0]),
|
|
689
|
+
Expression.child_from_xml(element[1]),
|
|
538
690
|
),
|
|
539
691
|
# These attributes are required by the WFS spec:
|
|
540
|
-
wildCard=
|
|
541
|
-
singleChar=
|
|
542
|
-
escapeChar=
|
|
692
|
+
wildCard=element.get_str_attribute("wildCard"),
|
|
693
|
+
singleChar=element.get_str_attribute("singleChar"),
|
|
694
|
+
escapeChar=element.get_str_attribute("escapeChar"),
|
|
543
695
|
_source=element.tag,
|
|
544
696
|
)
|
|
545
697
|
|
|
@@ -558,7 +710,9 @@ class LikeOperator(ComparisonOperator):
|
|
|
558
710
|
|
|
559
711
|
rhs = Literal(raw_value=value)
|
|
560
712
|
else:
|
|
561
|
-
raise ExternalParsingError(
|
|
713
|
+
raise ExternalParsingError(
|
|
714
|
+
f"Expected a literal value for the {self.xml_tags[0]} operator."
|
|
715
|
+
)
|
|
562
716
|
|
|
563
717
|
# Use the FesLike lookup
|
|
564
718
|
return self.build_compare(compiler, lhs=lhs, lookup="fes_like", rhs=rhs)
|
|
@@ -569,17 +723,29 @@ class LikeOperator(ComparisonOperator):
|
|
|
569
723
|
class NilOperator(ComparisonOperator):
|
|
570
724
|
"""Check whether the value evaluates to null/None.
|
|
571
725
|
If the WFS returned a property element with <tns:p xsi:nil='true'>, this returns true.
|
|
726
|
+
|
|
727
|
+
It parses and handles syntax such as::
|
|
728
|
+
|
|
729
|
+
<fes:PropertyIsNil>
|
|
730
|
+
<fes:ValueReference>city/name</fes:ValueReference>
|
|
731
|
+
</fes:PropertyIsNil>
|
|
732
|
+
|
|
733
|
+
Note that the provided argument can be any expression, not just a value reference.
|
|
734
|
+
Thus, it can also check whether a function returns null.
|
|
572
735
|
"""
|
|
573
736
|
|
|
574
737
|
expression: Expression | None
|
|
575
738
|
nilReason: str
|
|
576
739
|
_source: str | None = field(compare=False, default=None)
|
|
577
740
|
|
|
741
|
+
# Allow checking whether the geometry is null
|
|
742
|
+
allow_geometries: ClassVar[bool] = True
|
|
743
|
+
|
|
578
744
|
@classmethod
|
|
579
745
|
@expect_children(1, Expression)
|
|
580
|
-
def from_xml(cls, element:
|
|
746
|
+
def from_xml(cls, element: NSElement):
|
|
581
747
|
return cls(
|
|
582
|
-
expression=Expression.
|
|
748
|
+
expression=Expression.child_from_xml(element[0]) if element is not None else None,
|
|
583
749
|
nilReason=element.get("nilReason"),
|
|
584
750
|
_source=element.tag,
|
|
585
751
|
)
|
|
@@ -594,15 +760,26 @@ class NilOperator(ComparisonOperator):
|
|
|
594
760
|
class NullOperator(ComparisonOperator):
|
|
595
761
|
"""Check whether the property exists.
|
|
596
762
|
If the WFS would not return the property element <tns:p>, this returns true.
|
|
763
|
+
|
|
764
|
+
It parses and handles syntax such as::
|
|
765
|
+
|
|
766
|
+
<fes:PropertyIsNull>
|
|
767
|
+
<fes:ValueReference>city/name</fes:ValueReference>
|
|
768
|
+
</fes:PropertyIsNull>
|
|
769
|
+
|
|
770
|
+
Note that the provided argument can be any expression, not just a value reference.
|
|
597
771
|
"""
|
|
598
772
|
|
|
599
773
|
expression: Expression
|
|
600
774
|
_source: str | None = field(compare=False, default=None)
|
|
601
775
|
|
|
776
|
+
# Allow checking whether the geometry is null
|
|
777
|
+
allow_geometries: ClassVar[bool] = True
|
|
778
|
+
|
|
602
779
|
@classmethod
|
|
603
780
|
@expect_children(1, Expression)
|
|
604
|
-
def from_xml(cls, element:
|
|
605
|
-
return cls(expression=Expression.
|
|
781
|
+
def from_xml(cls, element: NSElement):
|
|
782
|
+
return cls(expression=Expression.child_from_xml(element[0]), _source=element.tag)
|
|
606
783
|
|
|
607
784
|
def build_query(self, compiler: CompiledQuery) -> Q:
|
|
608
785
|
# For now, the implementation is identical to PropertyIsNil.
|
|
@@ -613,14 +790,28 @@ class NullOperator(ComparisonOperator):
|
|
|
613
790
|
|
|
614
791
|
|
|
615
792
|
class LogicalOperator(NonIdOperator):
|
|
616
|
-
"""Base class for AND, OR, NOT comparisons"""
|
|
793
|
+
"""Base class in the fes-spec for AND, OR, NOT comparisons"""
|
|
617
794
|
|
|
618
795
|
|
|
619
796
|
@dataclass
|
|
620
797
|
@tag_registry.register("And")
|
|
621
798
|
@tag_registry.register("Or")
|
|
622
799
|
class BinaryLogicOperator(LogicalOperator):
|
|
623
|
-
"""Apply an 'AND' or 'OR' operator
|
|
800
|
+
"""Apply an 'AND' or 'OR' operator.
|
|
801
|
+
|
|
802
|
+
This parses and handles the syntax::
|
|
803
|
+
|
|
804
|
+
<fes:And>
|
|
805
|
+
<fes:PropertyIsGreaterThanOrEqualTo>
|
|
806
|
+
...
|
|
807
|
+
</fes:PropertyIsGreaterThanOrEqualTo>
|
|
808
|
+
<fes:BBOX>
|
|
809
|
+
...
|
|
810
|
+
</fes:BBOX>
|
|
811
|
+
</fes:And>
|
|
812
|
+
|
|
813
|
+
Any tag deriving from :class:`NonIdOperator` is allowed here.
|
|
814
|
+
"""
|
|
624
815
|
|
|
625
816
|
operands: list[NonIdOperator]
|
|
626
817
|
operatorType: BinaryLogicType
|
|
@@ -628,9 +819,9 @@ class BinaryLogicOperator(LogicalOperator):
|
|
|
628
819
|
|
|
629
820
|
@classmethod
|
|
630
821
|
@expect_children(2, NonIdOperator, NonIdOperator)
|
|
631
|
-
def from_xml(cls, element:
|
|
822
|
+
def from_xml(cls, element: NSElement):
|
|
632
823
|
return cls(
|
|
633
|
-
operands=[NonIdOperator.
|
|
824
|
+
operands=[NonIdOperator.child_from_xml(child) for child in element],
|
|
634
825
|
operatorType=BinaryLogicType.from_xml(element),
|
|
635
826
|
_source=element.tag,
|
|
636
827
|
)
|
|
@@ -644,7 +835,16 @@ class BinaryLogicOperator(LogicalOperator):
|
|
|
644
835
|
@dataclass
|
|
645
836
|
@tag_registry.register("Not")
|
|
646
837
|
class UnaryLogicOperator(LogicalOperator):
|
|
647
|
-
"""Apply a NOT operator
|
|
838
|
+
"""Apply a NOT operator.
|
|
839
|
+
|
|
840
|
+
This parses and handles the syntax::
|
|
841
|
+
|
|
842
|
+
<fes:Not>
|
|
843
|
+
<fes:PropertyIsNil>
|
|
844
|
+
<fes:ValueReference>city/name</fes:ValueReference>
|
|
845
|
+
</fes:PropertyIsNil>
|
|
846
|
+
</fes:Not>
|
|
847
|
+
"""
|
|
648
848
|
|
|
649
849
|
operands: NonIdOperator
|
|
650
850
|
operatorType: UnaryLogicType
|
|
@@ -652,9 +852,9 @@ class UnaryLogicOperator(LogicalOperator):
|
|
|
652
852
|
|
|
653
853
|
@classmethod
|
|
654
854
|
@expect_children(1, NonIdOperator)
|
|
655
|
-
def from_xml(cls, element:
|
|
855
|
+
def from_xml(cls, element: NSElement):
|
|
656
856
|
return cls(
|
|
657
|
-
operands=NonIdOperator.
|
|
857
|
+
operands=NonIdOperator.child_from_xml(element[0]),
|
|
658
858
|
operatorType=UnaryLogicType.from_xml(element),
|
|
659
859
|
_source=element.tag,
|
|
660
860
|
)
|
|
@@ -665,4 +865,8 @@ class UnaryLogicOperator(LogicalOperator):
|
|
|
665
865
|
|
|
666
866
|
|
|
667
867
|
class ExtensionOperator(NonIdOperator):
|
|
668
|
-
"""Base class for extensions to
|
|
868
|
+
"""Base class for extensions to FES 2.0.
|
|
869
|
+
|
|
870
|
+
It's fully allowed to introduce new operators on your own namespace.
|
|
871
|
+
These need to inherit from this class.
|
|
872
|
+
"""
|