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