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.
Files changed (77) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/crs.py +401 -0
  8. gisserver/db.py +126 -51
  9. gisserver/exceptions.py +132 -4
  10. gisserver/extensions/__init__.py +4 -0
  11. gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
  12. gisserver/extensions/queries.py +266 -0
  13. gisserver/features.py +253 -181
  14. gisserver/geometries.py +64 -311
  15. gisserver/management/__init__.py +0 -0
  16. gisserver/management/commands/__init__.py +0 -0
  17. gisserver/management/commands/loadgeojson.py +311 -0
  18. gisserver/operations/base.py +130 -312
  19. gisserver/operations/wfs20.py +399 -375
  20. gisserver/output/__init__.py +14 -49
  21. gisserver/output/base.py +198 -144
  22. gisserver/output/csv.py +78 -75
  23. gisserver/output/geojson.py +37 -37
  24. gisserver/output/gml32.py +287 -259
  25. gisserver/output/iters.py +207 -0
  26. gisserver/output/results.py +73 -61
  27. gisserver/output/stored.py +143 -0
  28. gisserver/output/utils.py +81 -169
  29. gisserver/output/xmlschema.py +85 -46
  30. gisserver/parsers/__init__.py +10 -10
  31. gisserver/parsers/ast.py +426 -0
  32. gisserver/parsers/fes20/__init__.py +89 -31
  33. gisserver/parsers/fes20/expressions.py +172 -58
  34. gisserver/parsers/fes20/filters.py +116 -45
  35. gisserver/parsers/fes20/identifiers.py +66 -28
  36. gisserver/parsers/fes20/lookups.py +146 -0
  37. gisserver/parsers/fes20/operators.py +417 -161
  38. gisserver/parsers/fes20/sorting.py +113 -34
  39. gisserver/parsers/gml/__init__.py +17 -25
  40. gisserver/parsers/gml/base.py +36 -15
  41. gisserver/parsers/gml/geometries.py +105 -44
  42. gisserver/parsers/ows/__init__.py +25 -0
  43. gisserver/parsers/ows/kvp.py +198 -0
  44. gisserver/parsers/ows/requests.py +160 -0
  45. gisserver/parsers/query.py +179 -0
  46. gisserver/parsers/values.py +87 -4
  47. gisserver/parsers/wfs20/__init__.py +39 -0
  48. gisserver/parsers/wfs20/adhoc.py +253 -0
  49. gisserver/parsers/wfs20/base.py +148 -0
  50. gisserver/parsers/wfs20/projection.py +103 -0
  51. gisserver/parsers/wfs20/requests.py +483 -0
  52. gisserver/parsers/wfs20/stored.py +193 -0
  53. gisserver/parsers/xml.py +261 -0
  54. gisserver/projection.py +367 -0
  55. gisserver/static/gisserver/index.css +20 -4
  56. gisserver/templates/gisserver/base.html +12 -0
  57. gisserver/templates/gisserver/index.html +9 -15
  58. gisserver/templates/gisserver/service_description.html +12 -6
  59. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  60. gisserver/templates/gisserver/wfs/feature_field.html +3 -3
  61. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  62. gisserver/templatetags/gisserver_tags.py +20 -0
  63. gisserver/types.py +445 -313
  64. gisserver/views.py +227 -62
  65. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  66. gisserver/parsers/base.py +0 -149
  67. gisserver/parsers/fes20/query.py +0 -285
  68. gisserver/parsers/tags.py +0 -102
  69. gisserver/queries/__init__.py +0 -37
  70. gisserver/queries/adhoc.py +0 -185
  71. gisserver/queries/base.py +0 -186
  72. gisserver/queries/projection.py +0 -240
  73. gisserver/queries/stored.py +0 -206
  74. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  75. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  76. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  77. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
@@ -1,25 +1,45 @@
1
1
  """These classes map to the FES 2.0 specification for expressions.
2
2
  The class names are identical to those in the FES spec.
3
+
4
+ Inheritance structure:
5
+
6
+ * :class:`Expression`
7
+
8
+ * :class:`Function` for ``<fes:Function>``.
9
+ * :class:`Literal` for ``<fes:Literal>``.
10
+ * :class:`ValueReference` for ``<fes:ValueReference>``.
11
+ * :class:`BinaryOperator` for FES 1.0 compatibility.
12
+
13
+ The :class:`BinaryOperator` is included as expression
14
+ to handle FES 1.0 :class:`BinaryOperatorType` arithmetic tags:
15
+ ``<fes:Add>``, ``<fes:Sub>``, ``<fes:Mul>``, ``<fes:Div>``.
3
16
  """
4
17
 
5
18
  from __future__ import annotations
6
19
 
7
20
  import operator
8
- from dataclasses import dataclass
21
+ from dataclasses import dataclass, field
9
22
  from datetime import date, datetime
10
23
  from decimal import Decimal as D
11
24
  from functools import cached_property
12
25
  from typing import Union
13
- from xml.etree.ElementTree import Element
14
26
 
15
- from django.contrib.gis.geos import GEOSGeometry
27
+ from django.contrib.gis import geos
28
+ from django.contrib.gis.db import models as gis_models
29
+ from django.core.exceptions import ValidationError
16
30
  from django.db import models
17
- from django.db.models import Func, Q, Value
31
+ from django.db.models import Q, Value
18
32
  from django.db.models.expressions import Combinable
19
33
 
20
34
  from gisserver.exceptions import ExternalParsingError
21
- from gisserver.parsers.base import BaseNode, TagNameEnum, tag_registry
22
- from gisserver.parsers.fes20.functions import function_registry
35
+ from gisserver.extensions.functions import function_registry
36
+ from gisserver.parsers.ast import (
37
+ AstNode,
38
+ TagNameEnum,
39
+ expect_no_children,
40
+ expect_tag,
41
+ tag_registry,
42
+ )
23
43
  from gisserver.parsers.gml import (
24
44
  GM_Envelope,
25
45
  GM_Object,
@@ -27,12 +47,12 @@ from gisserver.parsers.gml import (
27
47
  is_gml_element,
28
48
  parse_gml_node,
29
49
  )
30
- from gisserver.parsers.tags import expect_tag, get_attribute
50
+ from gisserver.parsers.query import RhsTypes
31
51
  from gisserver.parsers.values import auto_cast
32
- from gisserver.types import FES20, ORMPath, XsdTypes, split_xml_name
52
+ from gisserver.parsers.xml import NSElement, parse_qname, xmlns
53
+ from gisserver.types import XPathMatch, XsdTypes
33
54
 
34
55
  NoneType = type(None)
35
- RhsTypes = Union[Combinable, Func, Q, GEOSGeometry, bool, int, str, date, datetime, tuple]
36
56
  ParsedValue = Union[int, str, date, D, datetime, GM_Object, GM_Envelope, TM_Object, NoneType]
37
57
 
38
58
  OUTPUT_FIELDS = {
@@ -43,6 +63,15 @@ OUTPUT_FIELDS = {
43
63
  datetime: models.DateTimeField(),
44
64
  float: models.FloatField(),
45
65
  D: models.DecimalField(),
66
+ geos.GEOSGeometry: gis_models.GeometryField(),
67
+ geos.Point: gis_models.PointField(),
68
+ geos.LineString: gis_models.LineStringField(),
69
+ geos.LinearRing: gis_models.LineStringField(),
70
+ geos.Polygon: gis_models.PolygonField(),
71
+ geos.MultiPoint: gis_models.MultiPointField(),
72
+ geos.MultiPolygon: gis_models.MultiPolygonField(),
73
+ geos.MultiLineString: gis_models.MultiLineStringField(),
74
+ geos.GeometryCollection: gis_models.GeometryCollectionField(),
46
75
  }
47
76
 
48
77
 
@@ -59,10 +88,20 @@ class BinaryOperatorType(TagNameEnum):
59
88
  Div = operator.truediv
60
89
 
61
90
 
62
- class Expression(BaseNode):
63
- """Abstract base class, as defined by FES spec."""
91
+ class Expression(AstNode):
92
+ """Abstract base class, as defined by FES spec.
93
+
94
+ The FES spec defines the following subclasses:
95
+
96
+ * :class:`ValueReference` (pointing to a field name)
97
+ * :class:`Literal` (a scalar value)
98
+ * :class:`Function` (a transformation for a value/field)
99
+
100
+ When code uses ``Expression.child_from_xml(element)``, the AST logic will
101
+ initialize the correct subclass for those elements.
102
+ """
64
103
 
65
- xml_ns = FES20
104
+ xml_ns = xmlns.fes20
66
105
 
67
106
  def build_lhs(self, compiler) -> str:
68
107
  """Get the expression as the left-hand-side of the equation.
@@ -86,7 +125,21 @@ class Expression(BaseNode):
86
125
  @dataclass(repr=False)
87
126
  @tag_registry.register("Literal")
88
127
  class Literal(Expression):
89
- """The <fes:Literal> element that holds a literal value"""
128
+ """The ``<fes:Literal>`` element that holds a literal value.
129
+
130
+ This can be a string value, possibly annotated with a type::
131
+
132
+ <fes:Literal type="xs:boolean">true</fes:Literal>
133
+
134
+ Following the spec, the value may also contain a complete geometry::
135
+
136
+ <fes:Literal>
137
+ <gml:Envelope xmlns:gml="http://www.opengis.net/gml/3.2" srsName="urn:ogc:def:crs:EPSG::4326">
138
+ <gml:lowerCorner>5.7 53.1</gml:lowerCorner>
139
+ <gml:upperCorner>6.1 53.5</gml:upperCorner>
140
+ </gml:Envelope>
141
+ </fes:Literal>
142
+ """
90
143
 
91
144
  # The XSD definition even defines a sequence of xsd:any as possible member!
92
145
  raw_value: NoneType | str | GM_Object | GM_Envelope | TM_Object
@@ -100,7 +153,9 @@ class Literal(Expression):
100
153
 
101
154
  @cached_property
102
155
  def value(self) -> ParsedValue: # officially <xsd:any>
103
- """Access the value of the element, cast to the appropriate data type."""
156
+ """Access the value of the element, cast to the appropriate data type.
157
+ :raises ExternalParsingError: When the value can't be converted to the proper type.
158
+ """
104
159
  if not isinstance(self.raw_value, str):
105
160
  return self.raw_value # GML element or None
106
161
  elif self.type:
@@ -112,19 +167,20 @@ class Literal(Expression):
112
167
 
113
168
  @cached_property
114
169
  def type(self) -> XsdTypes | None:
170
+ """Tell which datatype the literal holds.
171
+ This returns the type="..." value of the element.
172
+ """
115
173
  if not self.raw_type:
116
174
  return None
117
175
 
118
- xmlns, localname = split_xml_name(self.raw_type)
119
- if xmlns == "gml":
120
- return XsdTypes(self.raw_type)
121
- else:
122
- # No idea what XMLSchema was prefixed as (could be ns0 instead of xs:)
123
- return XsdTypes(localname)
176
+ # The raw type is already translated into a fully qualified XML name,
177
+ # which also allows defining types as "ns0:string", "xs:string" or "xsd:string".
178
+ # These are directly matched to the XsdTypes enum value.
179
+ return XsdTypes(self.raw_type)
124
180
 
125
181
  @classmethod
126
- @expect_tag(FES20, "Literal")
127
- def from_xml(cls, element: Element):
182
+ @expect_tag(xmlns.fes20, "Literal")
183
+ def from_xml(cls, element: NSElement):
128
184
  children = len(element)
129
185
  if not children:
130
186
  # Common case: value is raw text
@@ -137,7 +193,10 @@ class Literal(Expression):
137
193
  f"Unsupported child element for <Literal> element: {element[0].tag}."
138
194
  )
139
195
 
140
- return cls(raw_value=raw_value, raw_type=element.get("type"))
196
+ return cls(
197
+ raw_value=raw_value,
198
+ raw_type=parse_qname(element.attrib.get("type"), element.ns_aliases),
199
+ )
141
200
 
142
201
  def build_lhs(self, compiler) -> str:
143
202
  """Alias the value when it's used in the left-hand-side.
@@ -166,14 +225,23 @@ class Literal(Expression):
166
225
  @tag_registry.register("ValueReference")
167
226
  @tag_registry.register("PropertyName", hidden=True) # FES 1.0 name that old clients still use.
168
227
  class ValueReference(Expression):
169
- """The <fes:ValueReference> element that holds an XPath string.
228
+ """The ``<fes:ValueReference>`` element that holds an XPath string.
170
229
  In the fes XSD, this is declared as a subclass of xsd:string.
171
230
 
231
+ This parses the syntax like::
232
+
233
+ <fes:ValueReference>field-name</fes:ValueReference>
234
+ <fes:ValueReference>path/to/field-name</fes:ValueReference>
235
+ <fes:ValueReference>collection[@attr=value]/field-name</fes:ValueReference>
236
+
172
237
  The old WFS1/FES1 "PropertyName" is allowed as an alias.
173
238
  Various clients still send this, and mapserver/geoserver support this.
174
239
  """
175
240
 
241
+ #: The XPath value
176
242
  xpath: str
243
+ #: The known namespaces aliases at this point in the XML tree
244
+ xpath_ns_aliases: dict[str, str] | None = field(compare=False, default=None)
177
245
 
178
246
  def __str__(self):
179
247
  return self.xpath
@@ -182,53 +250,56 @@ class ValueReference(Expression):
182
250
  return f"ValueReference({self.xpath!r})"
183
251
 
184
252
  @classmethod
185
- @expect_tag(FES20, "ValueReference", "PropertyName", leaf=True)
186
- def from_xml(cls, element: Element):
187
- return cls(xpath=element.text)
253
+ @expect_tag(xmlns.fes20, "ValueReference", "PropertyName")
254
+ @expect_no_children
255
+ def from_xml(cls, element: NSElement):
256
+ return cls(xpath=element.text, xpath_ns_aliases=element.ns_aliases)
188
257
 
189
258
  def build_lhs(self, compiler) -> str:
190
- """Optimized LHS: there is no need to alias a field lookup through an annotation."""
191
- match = self.parse_xpath(compiler.feature_type)
259
+ """Use the field name in a left-hand-side expression."""
260
+ # Optimized from base class: fields don't need an alias lookup through annotations.
261
+ match = self.parse_xpath(compiler.feature_types)
192
262
  return match.build_lhs(compiler)
193
263
 
194
264
  def build_rhs(self, compiler) -> RhsTypes:
195
- """Return the value as F-expression"""
196
- match = self.parse_xpath(compiler.feature_type)
265
+ """Use the field name in a right-hand expression.
266
+ This generates an F-expression for the ORM."""
267
+ match = self.parse_xpath(compiler.feature_types)
197
268
  return match.build_rhs(compiler)
198
269
 
199
- def parse_xpath(self, feature_type=None) -> ORMPath:
270
+ def parse_xpath(self, feature_types: list) -> XPathMatch:
200
271
  """Convert the XPath into the required ORM query elements."""
201
- if feature_type is not None:
202
- # Can resolve against XSD paths, find the correct DB field name
203
- return feature_type.resolve_element(self.xpath)
204
- else:
205
- # Only used by unit testing (when feature_type is not given).
206
- parts = [word.strip() for word in self.xpath.split("/")]
207
- return ORMPath(orm_path="__".join(parts), orm_filters=None)
208
-
209
- @cached_property
210
- def element_name(self):
211
- """Tell which element this reference points to."""
212
- return self.xpath.rpartition("/")[2]
272
+ return feature_types[0].resolve_element(self.xpath, self.xpath_ns_aliases)
213
273
 
214
274
 
215
275
  @dataclass
216
276
  @tag_registry.register("Function")
217
277
  class Function(Expression):
218
- """The <fes:Function name="..."> element."""
278
+ """The ``<fes:Function name="...">`` element.
279
+
280
+ This parses the syntax such as::
281
+
282
+ <fes:Function name="Add">
283
+ <fes:ValueReference>field-name</fes:ValueReference>
284
+ <fes:Literal>2</fes:Literal>
285
+ </fes:Function>
286
+
287
+ Each argument of the function can be another :class:`Expression`,
288
+ such as a :class:`Function`, :class:`ValueReference` or :class:`Literal`.
289
+ """
219
290
 
220
291
  name: str # scoped name
221
292
  arguments: list[Expression] # xsd:element ref="fes20:expression"
222
293
 
223
294
  @classmethod
224
- @expect_tag(FES20, "Function")
225
- def from_xml(cls, element: Element):
295
+ @expect_tag(xmlns.fes20, "Function")
296
+ def from_xml(cls, element: NSElement):
226
297
  return cls(
227
- name=get_attribute(element, "name"),
228
- arguments=[Expression.from_child_xml(child) for child in element],
298
+ name=element.get_str_attribute("name"),
299
+ arguments=[Expression.child_from_xml(child) for child in element],
229
300
  )
230
301
 
231
- def build_rhs(self, compiler) -> RhsTypes:
302
+ def build_rhs(self, compiler) -> models.Func:
232
303
  """Build the SQL function object"""
233
304
  db_function = function_registry.resolve_function(self.name)
234
305
  args = [arg.build_rhs(compiler) for arg in self.arguments]
@@ -236,31 +307,74 @@ class Function(Expression):
236
307
 
237
308
 
238
309
  @dataclass
239
- @tag_registry.register_names(BinaryOperatorType)
310
+ @tag_registry.register(BinaryOperatorType)
240
311
  class BinaryOperator(Expression):
241
312
  """Support for FES 1.0 arithmetic operators.
242
313
 
314
+ This parses a syntax like::
315
+
316
+ <fes:Add>
317
+ <fes:ValueReference>field-name</fes:ValueReference>
318
+ <fes:Literal>2</fes:Literal>
319
+ </fes:Add>
320
+
321
+ The operator can be a ``<fes:Add>``, ``<fes:Sub>``, ``<fes:Mul>``, ``<fes:Div>``.
322
+
243
323
  These are no longer part of the FES 2.0 spec, but clients (like QGis)
244
- still assume the server supports these. Hence, these need to be included.
324
+ still assume the server use these. Hence, these need to be included.
245
325
  """
246
326
 
247
327
  _operatorType: BinaryOperatorType
248
328
  expression: tuple[Expression, Expression]
249
329
 
250
330
  @classmethod
251
- def from_xml(cls, element: Element):
331
+ def from_xml(cls, element: NSElement):
252
332
  return cls(
253
333
  _operatorType=BinaryOperatorType.from_xml(element),
254
334
  expression=(
255
- Expression.from_child_xml(element[0]),
256
- Expression.from_child_xml(element[1]),
335
+ Expression.child_from_xml(element[0]),
336
+ Expression.child_from_xml(element[1]),
257
337
  ),
258
338
  )
259
339
 
260
340
  def build_rhs(self, compiler) -> RhsTypes:
261
- value1 = _make_combinable(self.expression[0].build_rhs(compiler))
262
- value2 = _make_combinable(self.expression[1].build_rhs(compiler))
263
- return self._operatorType.value(value1, value2)
341
+ self.validate_arithmetic(compiler, self.expression[0], self.expression[1])
342
+ value1 = self.expression[0].build_rhs(compiler)
343
+ value2 = self.expression[1].build_rhs(compiler)
344
+
345
+ # Func() + 2 or 2 + Func() are automatically handled,
346
+ # but Q(..) + 2 or 2 + Q(..) aren't.
347
+ # When both are values, a direct calculation takes place in Python.
348
+ if isinstance(value1, Q):
349
+ value2 = _make_combinable(value2)
350
+ if isinstance(value2, Q):
351
+ value1 = _make_combinable(value1)
352
+
353
+ try:
354
+ # This calls somthing like: operator.add(F("field"), 2) or operator.add(1, 2)
355
+ return self._operatorType.value(value1, value2)
356
+ except (TypeError, ValueError, ArithmeticError) as e:
357
+ raise ValidationError(
358
+ f"Invalid data for the 'fes:{self._operatorType.name}' element: {value1} {value2}"
359
+ ) from e
360
+
361
+ def validate_arithmetic(self, compiler, lhs: Expression, rhs: Expression):
362
+ """Check whether values support arithmetic operators."""
363
+ if isinstance(lhs, Literal) and isinstance(rhs, ValueReference):
364
+ lhs, rhs = rhs, lhs
365
+
366
+ if isinstance(lhs, ValueReference):
367
+ xsd_element = lhs.parse_xpath(compiler.feature_types).child
368
+ if isinstance(rhs, Literal):
369
+ # Since the element is resolved, inform the Literal how to parse the value.
370
+ # This avoids various validation errors along the path.
371
+ rhs.bind_type(xsd_element.type)
372
+
373
+ # Validate the expressions against each other
374
+ # This raises an ValidationError when values can't be converted
375
+ xsd_element.to_python(rhs.raw_value)
376
+
377
+ return None
264
378
 
265
379
 
266
380
  def _make_combinable(value) -> Combinable | Q:
@@ -1,38 +1,115 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import AnyStr, Union
4
- from xml.etree.ElementTree import Element, QName
3
+ from dataclasses import dataclass, field
4
+ from typing import AnyStr, ClassVar, Union
5
5
 
6
- from defusedxml.ElementTree import ParseError, fromstring
6
+ from django.db.models import Q
7
7
 
8
- from gisserver.exceptions import ExternalParsingError
9
- from gisserver.parsers.base import tag_registry
10
- from gisserver.parsers.tags import expect_tag
11
- from gisserver.types import FES20, GML32
8
+ from gisserver.exceptions import InvalidParameterValue
9
+ from gisserver.parsers.ast import AstNode, expect_tag, tag_registry
10
+ from gisserver.parsers.gml import GEOSGMLGeometry
11
+ from gisserver.parsers.ows import KVPRequest
12
+ from gisserver.parsers.query import CompiledQuery
13
+ from gisserver.parsers.xml import NSElement, parse_xml_from_string, xmlns
12
14
 
13
- from . import expressions, identifiers, operators, query
15
+ from . import expressions, identifiers, operators
14
16
 
17
+ #: The FES element group that can be used as body for the :class:`Filter` element.
15
18
  FilterPredicates = Union[expressions.Function, operators.Operator]
16
19
 
20
+ # Fully qualified tag names
21
+ FES_RESOURCE_ID = xmlns.fes20.qname("ResourceId")
17
22
 
18
- class Filter:
19
- """The <fes:Filter> element.
20
23
 
21
- As this is a wrapper, it only contains a "predicate" element with the contents.
24
+ @dataclass
25
+ @tag_registry.register("Filter", xmlns.fes20)
26
+ class Filter(AstNode):
27
+ """The ``<fes:Filter>`` element.
28
+
29
+ This parses and handles the syntax::
30
+
31
+ <fes:Filter>
32
+ <fes:SomeOperator>
33
+ ...
34
+ </fes:SomeOperator>
35
+ </fes:Filter>
36
+
37
+ The :meth:`build_query` will convert the parsed tree
38
+ into a format that can build a Django ORM QuerySet.
39
+
40
+ .. seealso:: https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/filter_xsd.html#Filter
22
41
  """
23
42
 
24
- query_language = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
43
+ query_language: ClassVar[str] = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
25
44
 
45
+ #: The filter predicate (body)
26
46
  predicate: FilterPredicates
27
- source: AnyStr | None
28
47
 
29
- def __init__(self, predicate: FilterPredicates, source: AnyStr | None = None):
30
- self.predicate = predicate
31
- self.source = source
48
+ source: AnyStr | None = field(default=None, compare=False)
32
49
 
33
50
  @classmethod
34
- def from_string(cls, text: AnyStr) -> Filter:
35
- """Parse an XML <fes20:Filter> string.
51
+ def from_kvp_request(cls, kvp: KVPRequest) -> Filter | None:
52
+ """Parse the filter from the GET request."""
53
+
54
+ # Check filter language
55
+ filter_language = kvp.get_str("FILTER_LANGUAGE", default=cls.query_language)
56
+ if filter_language != cls.query_language:
57
+ raise InvalidParameterValue(
58
+ f"Invalid value for filterLanguage: {filter_language}", locator="filterLanguage"
59
+ )
60
+
61
+ # Parse filter
62
+ filter = kvp.get_custom(
63
+ "filter", default=None, parser=lambda x: cls.from_string(x, kvp.ns_aliases)
64
+ )
65
+
66
+ # Parse alternatives to filter
67
+ resource_ids = [
68
+ identifiers.ResourceId.from_string(rid, kvp.ns_aliases)
69
+ for rid in kvp.get_list("resourceID", default=[])
70
+ ]
71
+ bbox = kvp.get_custom("bbox", default=None, parser=GEOSGMLGeometry.from_bbox)
72
+
73
+ # Make sure the various query options are not mixed.
74
+ cls.validate_kvp_exclusions(filter, bbox, resource_ids)
75
+
76
+ if filter is None:
77
+ # See if the other KVP parameters still provide a basic filter.
78
+ # Instead of implementing these parameters separately in the AdhocQueryExpression,
79
+ # they are implemented by constructing the filter AST internally.
80
+ if resource_ids:
81
+ filter = Filter(predicate=operators.IdOperator(resource_ids))
82
+ elif bbox is not None:
83
+ filter = Filter(
84
+ predicate=operators.BinarySpatialOperator(
85
+ operatorType=operators.SpatialOperatorName.BBOX,
86
+ operand1=None,
87
+ operand2=bbox,
88
+ )
89
+ )
90
+
91
+ return filter
92
+
93
+ @classmethod
94
+ def validate_kvp_exclusions(
95
+ cls, filter: Filter | None, bbox: GEOSGMLGeometry | None, resource_ids: list
96
+ ):
97
+ """Validate mutually exclusive parameters"""
98
+ if filter is not None and (bbox is not None or resource_ids):
99
+ raise InvalidParameterValue(
100
+ "The FILTER parameter is mutually exclusive with BBOX and RESOURCEID",
101
+ locator="filter",
102
+ )
103
+
104
+ if resource_ids and (bbox is not None or filter is not None):
105
+ raise InvalidParameterValue(
106
+ "The RESOURCEID parameter is mutually exclusive with BBOX and FILTER",
107
+ locator="resourceId",
108
+ )
109
+
110
+ @classmethod
111
+ def from_string(cls, text: AnyStr, ns_aliases: dict[str, str] | None = None) -> Filter:
112
+ """Parse an XML ``<fes:Filter>`` string.
36
113
 
37
114
  This uses defusedxml by default, to avoid various XML injection attacks.
38
115
 
@@ -47,51 +124,45 @@ class Filter:
47
124
  if "xmlns" not in first_tag and (
48
125
  first_tag == "<Filter" or first_tag.startswith("<Filter ")
49
126
  ):
50
- text = f'{first_tag} xmlns="{FES20}" xmlns:gml="{GML32}"{text[end_first:]}'
127
+ text = f'{first_tag} xmlns="{xmlns.fes20}" xmlns:gml="{xmlns.gml32}"{text[end_first:]}'
51
128
 
52
- try:
53
- root_element = fromstring(text)
54
- except ParseError as e:
55
- # Offer consistent results for callers to check for invalid data.
56
- raise ExternalParsingError(str(e)) from e
129
+ root_element = parse_xml_from_string(text, extra_ns_aliases=ns_aliases)
57
130
  return Filter.from_xml(root_element, source=text)
58
131
 
59
132
  @classmethod
60
- @expect_tag(FES20, "Filter")
61
- def from_xml(cls, element: Element, source: AnyStr | None = None) -> Filter:
133
+ @expect_tag(xmlns.fes20, "Filter")
134
+ def from_xml(cls, element: NSElement, source: AnyStr | None = None) -> Filter:
62
135
  """Parse the <fes20:Filter> element."""
63
- if len(element) > 1 or element[0].tag == QName(FES20, "ResourceId"):
136
+ if len(element) > 1 or element[0].tag == FES_RESOURCE_ID:
64
137
  # fes20:ResourceId is the only element that may appear multiple times.
138
+ # Wrap it in an IdOperator so this class can have a single element as predicate.
65
139
  return Filter(
66
140
  predicate=operators.IdOperator(
67
- [identifiers.Id.from_child_xml(child) for child in element]
141
+ [identifiers.Id.child_from_xml(child) for child in element]
68
142
  ),
69
143
  source=source,
70
144
  )
71
145
  else:
72
146
  return Filter(
73
- predicate=tag_registry.from_child_xml(
74
- element[0], allowed_types=(expressions.Function, operators.Operator)
147
+ # Can be Function or Operator (e.g. BinaryComparisonOperator),
148
+ # but not Literal or ValueReference.
149
+ predicate=tag_registry.node_from_xml(
150
+ element[0], allowed_types=FilterPredicates.__args__
75
151
  ),
76
152
  source=source,
77
153
  )
78
154
 
79
- def compile_query(self, feature_type=None, using=None) -> query.CompiledQuery:
155
+ def build_query(self, compiler: CompiledQuery) -> Q | None:
80
156
  """Collect the data to perform a Django ORM query."""
81
- compiler = query.CompiledQuery(feature_type=feature_type, using=using)
82
-
83
157
  # Function, Operator, IdList
84
- q_object = self.predicate.build_query(compiler)
85
- if q_object is not None:
86
- compiler.add_lookups(q_object)
87
-
88
- return compiler
158
+ # The operators may add the logic themselves, or return a Q object.
159
+ return self.predicate.build_query(compiler)
89
160
 
90
- def __repr__(self):
91
- return f"Filter(predicate={self.predicate!r}, source={self.source})"
92
-
93
- def __eq__(self, other):
94
- if isinstance(other, Filter):
95
- return self.predicate == other.predicate
161
+ def get_resource_id_types(self) -> list[str] | None:
162
+ """When the filter predicate consists of ``<fes:ResourceId>`` elements, return those.
163
+ This can return an empty list in case a ``<fes:ResourceId>`` object doesn't define a type.
164
+ """
165
+ if isinstance(self.predicate, operators.IdOperator):
166
+ return self.predicate.get_type_names()
96
167
  else:
97
- return NotImplemented
168
+ return None