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
|
@@ -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,32 +82,36 @@ 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.
|
|
93
|
+
|
|
94
|
+
The values correspond with GeoDjango operators. So a ``BBOX`` query
|
|
95
|
+
will translate into ``geometry__intersects=Polygon(...)``.
|
|
96
|
+
"""
|
|
96
97
|
|
|
97
98
|
# (A Within B) implies that (B Contains A)
|
|
98
99
|
|
|
99
100
|
# BBOX can either be implemented using bboverlaps (more efficient), or the
|
|
100
101
|
# more correct "intersects" option (e.g. a line near the box would match otherwise).
|
|
101
102
|
BBOX = "intersects" # ISO version: "NOT DISJOINT"
|
|
102
|
-
Equals = "equals" # Test whether
|
|
103
|
+
Equals = "equals" # Test whether two geometries are topologically equal
|
|
103
104
|
Disjoint = "disjoint" # Tests whether two geometries are disjoint (do not interact)
|
|
104
105
|
Intersects = "intersects" # Tests whether two geometries intersect
|
|
105
|
-
Touches = "touches" # Tests whether two geometries touch
|
|
106
|
-
Crosses = "crosses" # Tests whether two geometries cross
|
|
107
|
-
Within = "within" # Tests
|
|
108
|
-
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).
|
|
109
110
|
Overlaps = "overlaps" # Test whether two geometries overlap
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
class TemporalOperatorName(TagNameEnum):
|
|
113
|
-
"""XML tag names
|
|
114
|
+
"""XML tag names mapped to datetime operators.
|
|
114
115
|
|
|
115
116
|
Explanation here: http://old.geotools.org/Temporal-Filters_211091519.html
|
|
116
117
|
and: https://github.com/geotools/geotools/wiki/temporal-filters
|
|
@@ -146,26 +147,44 @@ class UnaryLogicType(TagNameEnum):
|
|
|
146
147
|
|
|
147
148
|
@dataclass
|
|
148
149
|
class Measure(BaseNode):
|
|
149
|
-
"""
|
|
150
|
+
"""A measurement for a distance element.
|
|
151
|
+
|
|
152
|
+
This parses and handles the syntax::
|
|
150
153
|
|
|
151
|
-
|
|
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
|
|
152
165
|
|
|
153
166
|
value: Decimal
|
|
154
167
|
uom: str # Unit of measurement, fes20:UomSymbol | fes20:UomURI
|
|
155
168
|
|
|
156
169
|
@classmethod
|
|
157
|
-
@expect_tag(
|
|
158
|
-
def from_xml(cls, element:
|
|
159
|
-
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"))
|
|
160
173
|
|
|
161
174
|
def build_rhs(self, compiler) -> measure.Distance:
|
|
162
175
|
return measure.Distance(default_unit=self.uom, **{self.uom: self.value})
|
|
163
176
|
|
|
164
177
|
|
|
165
178
|
class Operator(BaseNode):
|
|
166
|
-
"""Abstract base class, as defined by FES spec.
|
|
179
|
+
"""Abstract base class, as defined by FES spec.
|
|
180
|
+
|
|
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
|
+
"""
|
|
167
186
|
|
|
168
|
-
xml_ns =
|
|
187
|
+
xml_ns = xmlns.fes20
|
|
169
188
|
|
|
170
189
|
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
171
190
|
raise NotImplementedError(f"Using {self.__class__.__name__} is not supported yet.")
|
|
@@ -177,8 +196,7 @@ class IdOperator(Operator):
|
|
|
177
196
|
|
|
178
197
|
id: list[Id]
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
def type_names(self) -> list[str]:
|
|
199
|
+
def get_type_names(self) -> list[str]:
|
|
182
200
|
"""Provide a list of all type names accessed by this operator"""
|
|
183
201
|
return [type_name for type_name in self.grouped_ids if type_name is not None]
|
|
184
202
|
|
|
@@ -188,7 +206,7 @@ class IdOperator(Operator):
|
|
|
188
206
|
ids = sorted(self.id, key=operator.attrgetter("rid"))
|
|
189
207
|
return {
|
|
190
208
|
type_name: list(items)
|
|
191
|
-
for type_name, items in groupby(ids, key=
|
|
209
|
+
for type_name, items in groupby(ids, key=lambda id: id.get_type_name())
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
def build_query(self, compiler):
|
|
@@ -203,8 +221,20 @@ class IdOperator(Operator):
|
|
|
203
221
|
compiler.mark_empty()
|
|
204
222
|
return
|
|
205
223
|
|
|
224
|
+
type_names = {ft.xml_name for ft in compiler.feature_types}
|
|
206
225
|
for type_name, items in self.grouped_ids.items():
|
|
207
|
-
|
|
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])
|
|
208
238
|
compiler.add_lookups(ids_subset, type_name=type_name)
|
|
209
239
|
|
|
210
240
|
|
|
@@ -213,9 +243,13 @@ class NonIdOperator(Operator):
|
|
|
213
243
|
|
|
214
244
|
This is used for nearly all operators,
|
|
215
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.
|
|
216
250
|
"""
|
|
217
251
|
|
|
218
|
-
_source = None
|
|
252
|
+
_source = None # often declared as non-comparing in dataclasses below.
|
|
219
253
|
allow_geometries = False
|
|
220
254
|
|
|
221
255
|
def build_compare(
|
|
@@ -234,7 +268,7 @@ class NonIdOperator(Operator):
|
|
|
234
268
|
if isinstance(lhs, Literal) and isinstance(rhs, ValueReference):
|
|
235
269
|
lhs, rhs = rhs, lhs
|
|
236
270
|
|
|
237
|
-
if compiler.
|
|
271
|
+
if compiler.feature_types:
|
|
238
272
|
lookup = self.validate_comparison(compiler, lhs, lookup, rhs)
|
|
239
273
|
|
|
240
274
|
# Build Django Q-object
|
|
@@ -251,47 +285,58 @@ class NonIdOperator(Operator):
|
|
|
251
285
|
compiler: CompiledQuery,
|
|
252
286
|
lhs: Expression,
|
|
253
287
|
lookup: str,
|
|
254
|
-
rhs: Expression | RhsTypes,
|
|
288
|
+
rhs: Expression | gml.GM_Object | RhsTypes,
|
|
255
289
|
):
|
|
256
290
|
"""Validate whether a given comparison is even possible.
|
|
257
|
-
|
|
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).
|
|
258
302
|
"""
|
|
259
303
|
if isinstance(lhs, ValueReference):
|
|
260
|
-
xsd_element =
|
|
304
|
+
xsd_element = lhs.parse_xpath(compiler.feature_types).child
|
|
261
305
|
tag = self._source if self._source is not None else None
|
|
262
306
|
|
|
263
307
|
# e.g. deny <PropertyIsLessThanOrEqualTo> against <gml:boundedBy>
|
|
264
|
-
if xsd_element.is_geometry and not self.allow_geometries:
|
|
308
|
+
if xsd_element.type.is_geometry and not self.allow_geometries:
|
|
265
309
|
raise OperationProcessingFailed(
|
|
266
|
-
"filter",
|
|
267
310
|
f"Operator '{tag}' does not support comparing"
|
|
268
311
|
f" geometry properties: '{xsd_element.xml_name}'.",
|
|
312
|
+
locator="filter",
|
|
269
313
|
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
270
314
|
)
|
|
271
315
|
|
|
272
|
-
if isinstance(rhs, Literal):
|
|
273
|
-
# Since the element is resolved, inform the Literal how to parse the value.
|
|
274
|
-
# This avoids various validation errors along the path.
|
|
275
|
-
rhs.bind_type(xsd_element.type)
|
|
276
|
-
|
|
277
|
-
# When a common case of value comparison is done, the inputs
|
|
278
|
-
# can be validated before the ORM query is constructed.
|
|
279
|
-
xsd_element.validate_comparison(rhs.raw_value, lookup=lookup, tag=tag)
|
|
280
|
-
|
|
281
316
|
# Checking scalar values against array fields will fail.
|
|
282
317
|
# However, to make the queries consistent with other unbounded types (i.e. M2M fields),
|
|
283
318
|
# it makes sense to return an object when *one* entry in the array matches.
|
|
284
319
|
if xsd_element.is_array:
|
|
285
320
|
try:
|
|
286
|
-
|
|
321
|
+
lookup = ARRAY_LOOKUPS[lookup]
|
|
287
322
|
except KeyError:
|
|
323
|
+
logger.debug("No array lookup known for %s", lookup)
|
|
288
324
|
raise OperationProcessingFailed(
|
|
289
|
-
"filter",
|
|
290
325
|
f"Operator '{tag}' is not supported for "
|
|
291
326
|
f"the '{xsd_element.name}' property.",
|
|
327
|
+
locator="filter",
|
|
292
328
|
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
293
329
|
) from None
|
|
294
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
|
+
|
|
295
340
|
return lookup
|
|
296
341
|
|
|
297
342
|
def build_compare_between(
|
|
@@ -299,10 +344,10 @@ class NonIdOperator(Operator):
|
|
|
299
344
|
compiler: CompiledQuery,
|
|
300
345
|
lhs: Expression,
|
|
301
346
|
lookup: str,
|
|
302
|
-
rhs: tuple[
|
|
347
|
+
rhs: tuple[Expression | ValueReference | gml.GM_Object, Expression | Measure],
|
|
303
348
|
) -> Q:
|
|
304
349
|
"""Use the value in comparison with 2 other values (e.g. between query)"""
|
|
305
|
-
if compiler.
|
|
350
|
+
if compiler.feature_types:
|
|
306
351
|
self.validate_comparison(compiler, lhs, lookup, rhs[0])
|
|
307
352
|
self.validate_comparison(compiler, lhs, lookup, rhs[1])
|
|
308
353
|
|
|
@@ -323,9 +368,21 @@ class SpatialOperator(NonIdOperator):
|
|
|
323
368
|
|
|
324
369
|
|
|
325
370
|
@dataclass
|
|
326
|
-
@tag_registry.
|
|
371
|
+
@tag_registry.register("Beyond")
|
|
372
|
+
@tag_registry.register("DWithin")
|
|
327
373
|
class DistanceOperator(SpatialOperator):
|
|
328
|
-
"""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
|
+
"""
|
|
329
386
|
|
|
330
387
|
allow_geometries = True # override static attribute
|
|
331
388
|
|
|
@@ -337,7 +394,7 @@ class DistanceOperator(SpatialOperator):
|
|
|
337
394
|
|
|
338
395
|
@classmethod
|
|
339
396
|
@expect_children(3, ValueReference, gml.GM_Object, Measure)
|
|
340
|
-
def from_xml(cls, element:
|
|
397
|
+
def from_xml(cls, element: NSElement):
|
|
341
398
|
geometries = gml.find_gml_nodes(element)
|
|
342
399
|
if not geometries:
|
|
343
400
|
raise ExternalParsingError(f"Missing gml element in <{element.tag}>")
|
|
@@ -345,10 +402,10 @@ class DistanceOperator(SpatialOperator):
|
|
|
345
402
|
raise ExternalParsingError(f"Multiple gml elements found in <{element.tag}>")
|
|
346
403
|
|
|
347
404
|
return cls(
|
|
348
|
-
valueReference=ValueReference.from_xml(
|
|
405
|
+
valueReference=ValueReference.from_xml(element.find(FES_VALUE_REFERENCE)),
|
|
349
406
|
operatorType=DistanceOperatorName.from_xml(element),
|
|
350
407
|
geometry=gml.parse_gml_node(geometries[0]),
|
|
351
|
-
distance=Measure.from_xml(
|
|
408
|
+
distance=Measure.from_xml(element.find(FES_DISTANCE)),
|
|
352
409
|
_source=element.tag,
|
|
353
410
|
)
|
|
354
411
|
|
|
@@ -362,9 +419,23 @@ class DistanceOperator(SpatialOperator):
|
|
|
362
419
|
|
|
363
420
|
|
|
364
421
|
@dataclass
|
|
365
|
-
@tag_registry.
|
|
422
|
+
@tag_registry.register(SpatialOperatorName) # <BBOX>, <Equals>, ...
|
|
366
423
|
class BinarySpatialOperator(SpatialOperator):
|
|
367
|
-
"""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
|
+
"""
|
|
368
439
|
|
|
369
440
|
allow_geometries = True # override static attribute
|
|
370
441
|
|
|
@@ -374,7 +445,7 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
374
445
|
_source: str | None = field(compare=False, default=None)
|
|
375
446
|
|
|
376
447
|
@classmethod
|
|
377
|
-
def from_xml(cls, element:
|
|
448
|
+
def from_xml(cls, element: NSElement):
|
|
378
449
|
operator_type = SpatialOperatorName.from_xml(element)
|
|
379
450
|
if operator_type is SpatialOperatorName.BBOX and len(element) == 1:
|
|
380
451
|
# For BBOX, the geometry operator is optional
|
|
@@ -388,16 +459,15 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
388
459
|
return cls(
|
|
389
460
|
operatorType=operator_type,
|
|
390
461
|
operand1=ValueReference.from_xml(ref) if ref is not None else None,
|
|
391
|
-
operand2=tag_registry.
|
|
392
|
-
geo, allowed_types=SpatialDescription.__args__ # get_args() in 3.8
|
|
393
|
-
),
|
|
462
|
+
operand2=tag_registry.node_from_xml(geo, allowed_types=SpatialDescription.__args__),
|
|
394
463
|
_source=element.tag,
|
|
395
464
|
)
|
|
396
465
|
|
|
397
466
|
def build_query(self, compiler: CompiledQuery) -> Q:
|
|
398
467
|
operant1 = self.operand1
|
|
399
468
|
if operant1 is None:
|
|
400
|
-
|
|
469
|
+
# BBOX element points to the existing geometry element by default.
|
|
470
|
+
operant1 = _ResolvedValueReference(compiler.feature_types[0].main_geometry_element)
|
|
401
471
|
|
|
402
472
|
return self.build_compare(
|
|
403
473
|
compiler,
|
|
@@ -407,10 +477,61 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
407
477
|
)
|
|
408
478
|
|
|
409
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
|
+
|
|
410
500
|
@dataclass
|
|
411
|
-
@tag_registry.
|
|
501
|
+
@tag_registry.register(TemporalOperatorName) # <After>, <Before>, ...
|
|
412
502
|
class TemporalOperator(NonIdOperator):
|
|
413
|
-
"""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
|
+
"""
|
|
414
535
|
|
|
415
536
|
operatorType: TemporalOperatorName
|
|
416
537
|
operand1: ValueReference
|
|
@@ -418,20 +539,23 @@ class TemporalOperator(NonIdOperator):
|
|
|
418
539
|
_source: str | None = field(compare=False, default=None)
|
|
419
540
|
|
|
420
541
|
@classmethod
|
|
421
|
-
@expect_children(2, ValueReference, TemporalOperand)
|
|
422
|
-
def from_xml(cls, element:
|
|
542
|
+
@expect_children(2, ValueReference, *TemporalOperand.__args__)
|
|
543
|
+
def from_xml(cls, element: NSElement):
|
|
423
544
|
return cls(
|
|
424
545
|
operatorType=TemporalOperatorName.from_xml(element),
|
|
425
546
|
operand1=ValueReference.from_xml(element[0]),
|
|
426
|
-
operand2=tag_registry.
|
|
427
|
-
element[1], allowed_types=TemporalOperand.__args__
|
|
547
|
+
operand2=tag_registry.node_from_xml(
|
|
548
|
+
element[1], allowed_types=TemporalOperand.__args__
|
|
428
549
|
),
|
|
429
550
|
_source=element.tag,
|
|
430
551
|
)
|
|
431
552
|
|
|
432
553
|
|
|
433
554
|
class ComparisonOperator(NonIdOperator):
|
|
434
|
-
"""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
|
+
"""
|
|
435
559
|
|
|
436
560
|
# Start counting fresh here, to collect the capabilities
|
|
437
561
|
# that are listed in the <fes20:ComparisonOperators> node:
|
|
@@ -439,9 +563,23 @@ class ComparisonOperator(NonIdOperator):
|
|
|
439
563
|
|
|
440
564
|
|
|
441
565
|
@dataclass
|
|
442
|
-
@tag_registry.
|
|
566
|
+
@tag_registry.register(BinaryComparisonName) # <PropertyIs...>
|
|
443
567
|
class BinaryComparisonOperator(ComparisonOperator):
|
|
444
|
-
"""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
|
+
"""
|
|
445
583
|
|
|
446
584
|
operatorType: BinaryComparisonName
|
|
447
585
|
expression: tuple[Expression, Expression]
|
|
@@ -451,12 +589,12 @@ class BinaryComparisonOperator(ComparisonOperator):
|
|
|
451
589
|
|
|
452
590
|
@classmethod
|
|
453
591
|
@expect_children(2, Expression, Expression)
|
|
454
|
-
def from_xml(cls, element:
|
|
592
|
+
def from_xml(cls, element: NSElement):
|
|
455
593
|
return cls(
|
|
456
594
|
operatorType=BinaryComparisonName.from_xml(element),
|
|
457
595
|
expression=(
|
|
458
|
-
Expression.
|
|
459
|
-
Expression.
|
|
596
|
+
Expression.child_from_xml(element[0]),
|
|
597
|
+
Expression.child_from_xml(element[1]),
|
|
460
598
|
),
|
|
461
599
|
matchCase=element.get("matchCase", True),
|
|
462
600
|
matchAction=MatchAction(element.get("matchAction", default=MatchAction.Any)),
|
|
@@ -471,7 +609,19 @@ class BinaryComparisonOperator(ComparisonOperator):
|
|
|
471
609
|
@dataclass
|
|
472
610
|
@tag_registry.register("PropertyIsBetween")
|
|
473
611
|
class BetweenComparisonOperator(ComparisonOperator):
|
|
474
|
-
"""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
|
+
"""
|
|
475
625
|
|
|
476
626
|
expression: Expression
|
|
477
627
|
lowerBoundary: Expression
|
|
@@ -480,17 +630,15 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
480
630
|
|
|
481
631
|
@classmethod
|
|
482
632
|
@expect_children(3, Expression, "LowerBoundary", "UpperBoundary")
|
|
483
|
-
def from_xml(cls, element:
|
|
484
|
-
if element[1].tag !=
|
|
485
|
-
FES20, "UpperBoundary"
|
|
486
|
-
):
|
|
633
|
+
def from_xml(cls, element: NSElement):
|
|
634
|
+
if (element[1].tag != FES_LOWER_BOUNDARY) or (element[2].tag != FES_UPPER_BOUNDARY):
|
|
487
635
|
raise ExternalParsingError(
|
|
488
636
|
f"{element.tag} should have 3 child nodes: "
|
|
489
637
|
f"(expression), <LowerBoundary>, <UpperBoundary>"
|
|
490
638
|
)
|
|
491
639
|
|
|
492
|
-
lower =
|
|
493
|
-
upper =
|
|
640
|
+
lower = element[1]
|
|
641
|
+
upper = element[2]
|
|
494
642
|
|
|
495
643
|
if len(lower) != 1:
|
|
496
644
|
raise ExternalParsingError(f"{lower.tag} should have 1 expression child node")
|
|
@@ -498,9 +646,9 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
498
646
|
raise ExternalParsingError(f"{upper.tag} should have 1 expression child node")
|
|
499
647
|
|
|
500
648
|
return cls(
|
|
501
|
-
expression=Expression.
|
|
502
|
-
lowerBoundary=Expression.
|
|
503
|
-
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]),
|
|
504
652
|
_source=element.tag,
|
|
505
653
|
)
|
|
506
654
|
|
|
@@ -516,7 +664,15 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
516
664
|
@dataclass
|
|
517
665
|
@tag_registry.register("PropertyIsLike")
|
|
518
666
|
class LikeOperator(ComparisonOperator):
|
|
519
|
-
"""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
|
+
"""
|
|
520
676
|
|
|
521
677
|
expression: tuple[Expression, Expression]
|
|
522
678
|
wildCard: str
|
|
@@ -526,16 +682,16 @@ class LikeOperator(ComparisonOperator):
|
|
|
526
682
|
|
|
527
683
|
@classmethod
|
|
528
684
|
@expect_children(2, Expression, Expression)
|
|
529
|
-
def from_xml(cls, element:
|
|
685
|
+
def from_xml(cls, element: NSElement):
|
|
530
686
|
return cls(
|
|
531
687
|
expression=(
|
|
532
|
-
Expression.
|
|
533
|
-
Expression.
|
|
688
|
+
Expression.child_from_xml(element[0]),
|
|
689
|
+
Expression.child_from_xml(element[1]),
|
|
534
690
|
),
|
|
535
691
|
# These attributes are required by the WFS spec:
|
|
536
|
-
wildCard=
|
|
537
|
-
singleChar=
|
|
538
|
-
escapeChar=
|
|
692
|
+
wildCard=element.get_str_attribute("wildCard"),
|
|
693
|
+
singleChar=element.get_str_attribute("singleChar"),
|
|
694
|
+
escapeChar=element.get_str_attribute("escapeChar"),
|
|
539
695
|
_source=element.tag,
|
|
540
696
|
)
|
|
541
697
|
|
|
@@ -554,7 +710,9 @@ class LikeOperator(ComparisonOperator):
|
|
|
554
710
|
|
|
555
711
|
rhs = Literal(raw_value=value)
|
|
556
712
|
else:
|
|
557
|
-
raise ExternalParsingError(
|
|
713
|
+
raise ExternalParsingError(
|
|
714
|
+
f"Expected a literal value for the {self.xml_tags[0]} operator."
|
|
715
|
+
)
|
|
558
716
|
|
|
559
717
|
# Use the FesLike lookup
|
|
560
718
|
return self.build_compare(compiler, lhs=lhs, lookup="fes_like", rhs=rhs)
|
|
@@ -565,17 +723,29 @@ class LikeOperator(ComparisonOperator):
|
|
|
565
723
|
class NilOperator(ComparisonOperator):
|
|
566
724
|
"""Check whether the value evaluates to null/None.
|
|
567
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.
|
|
568
735
|
"""
|
|
569
736
|
|
|
570
737
|
expression: Expression | None
|
|
571
738
|
nilReason: str
|
|
572
739
|
_source: str | None = field(compare=False, default=None)
|
|
573
740
|
|
|
741
|
+
# Allow checking whether the geometry is null
|
|
742
|
+
allow_geometries: ClassVar[bool] = True
|
|
743
|
+
|
|
574
744
|
@classmethod
|
|
575
745
|
@expect_children(1, Expression)
|
|
576
|
-
def from_xml(cls, element:
|
|
746
|
+
def from_xml(cls, element: NSElement):
|
|
577
747
|
return cls(
|
|
578
|
-
expression=Expression.
|
|
748
|
+
expression=Expression.child_from_xml(element[0]) if element is not None else None,
|
|
579
749
|
nilReason=element.get("nilReason"),
|
|
580
750
|
_source=element.tag,
|
|
581
751
|
)
|
|
@@ -590,15 +760,26 @@ class NilOperator(ComparisonOperator):
|
|
|
590
760
|
class NullOperator(ComparisonOperator):
|
|
591
761
|
"""Check whether the property exists.
|
|
592
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.
|
|
593
771
|
"""
|
|
594
772
|
|
|
595
773
|
expression: Expression
|
|
596
774
|
_source: str | None = field(compare=False, default=None)
|
|
597
775
|
|
|
776
|
+
# Allow checking whether the geometry is null
|
|
777
|
+
allow_geometries: ClassVar[bool] = True
|
|
778
|
+
|
|
598
779
|
@classmethod
|
|
599
780
|
@expect_children(1, Expression)
|
|
600
|
-
def from_xml(cls, element:
|
|
601
|
-
return cls(expression=Expression.
|
|
781
|
+
def from_xml(cls, element: NSElement):
|
|
782
|
+
return cls(expression=Expression.child_from_xml(element[0]), _source=element.tag)
|
|
602
783
|
|
|
603
784
|
def build_query(self, compiler: CompiledQuery) -> Q:
|
|
604
785
|
# For now, the implementation is identical to PropertyIsNil.
|
|
@@ -609,14 +790,28 @@ class NullOperator(ComparisonOperator):
|
|
|
609
790
|
|
|
610
791
|
|
|
611
792
|
class LogicalOperator(NonIdOperator):
|
|
612
|
-
"""Base class for AND, OR, NOT comparisons"""
|
|
793
|
+
"""Base class in the fes-spec for AND, OR, NOT comparisons"""
|
|
613
794
|
|
|
614
795
|
|
|
615
796
|
@dataclass
|
|
616
797
|
@tag_registry.register("And")
|
|
617
798
|
@tag_registry.register("Or")
|
|
618
799
|
class BinaryLogicOperator(LogicalOperator):
|
|
619
|
-
"""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
|
+
"""
|
|
620
815
|
|
|
621
816
|
operands: list[NonIdOperator]
|
|
622
817
|
operatorType: BinaryLogicType
|
|
@@ -624,9 +819,9 @@ class BinaryLogicOperator(LogicalOperator):
|
|
|
624
819
|
|
|
625
820
|
@classmethod
|
|
626
821
|
@expect_children(2, NonIdOperator, NonIdOperator)
|
|
627
|
-
def from_xml(cls, element:
|
|
822
|
+
def from_xml(cls, element: NSElement):
|
|
628
823
|
return cls(
|
|
629
|
-
operands=[NonIdOperator.
|
|
824
|
+
operands=[NonIdOperator.child_from_xml(child) for child in element],
|
|
630
825
|
operatorType=BinaryLogicType.from_xml(element),
|
|
631
826
|
_source=element.tag,
|
|
632
827
|
)
|
|
@@ -640,7 +835,16 @@ class BinaryLogicOperator(LogicalOperator):
|
|
|
640
835
|
@dataclass
|
|
641
836
|
@tag_registry.register("Not")
|
|
642
837
|
class UnaryLogicOperator(LogicalOperator):
|
|
643
|
-
"""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
|
+
"""
|
|
644
848
|
|
|
645
849
|
operands: NonIdOperator
|
|
646
850
|
operatorType: UnaryLogicType
|
|
@@ -648,9 +852,9 @@ class UnaryLogicOperator(LogicalOperator):
|
|
|
648
852
|
|
|
649
853
|
@classmethod
|
|
650
854
|
@expect_children(1, NonIdOperator)
|
|
651
|
-
def from_xml(cls, element:
|
|
855
|
+
def from_xml(cls, element: NSElement):
|
|
652
856
|
return cls(
|
|
653
|
-
operands=NonIdOperator.
|
|
857
|
+
operands=NonIdOperator.child_from_xml(element[0]),
|
|
654
858
|
operatorType=UnaryLogicType.from_xml(element),
|
|
655
859
|
_source=element.tag,
|
|
656
860
|
)
|
|
@@ -661,4 +865,8 @@ class UnaryLogicOperator(LogicalOperator):
|
|
|
661
865
|
|
|
662
866
|
|
|
663
867
|
class ExtensionOperator(NonIdOperator):
|
|
664
|
-
"""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
|
+
"""
|