django-gisserver 2.0__py3-none-any.whl → 2.1.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 (56) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/METADATA +27 -10
  2. django_gisserver-2.1.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/conf.py +23 -1
  6. gisserver/crs.py +452 -0
  7. gisserver/db.py +78 -6
  8. gisserver/exceptions.py +106 -2
  9. gisserver/extensions/functions.py +122 -28
  10. gisserver/extensions/queries.py +15 -10
  11. gisserver/features.py +46 -33
  12. gisserver/geometries.py +64 -306
  13. gisserver/management/commands/loadgeojson.py +41 -21
  14. gisserver/operations/base.py +11 -7
  15. gisserver/operations/wfs20.py +31 -93
  16. gisserver/output/__init__.py +6 -2
  17. gisserver/output/base.py +28 -13
  18. gisserver/output/csv.py +18 -6
  19. gisserver/output/geojson.py +7 -6
  20. gisserver/output/gml32.py +86 -27
  21. gisserver/output/results.py +25 -39
  22. gisserver/output/utils.py +9 -2
  23. gisserver/parsers/ast.py +177 -68
  24. gisserver/parsers/fes20/__init__.py +76 -4
  25. gisserver/parsers/fes20/expressions.py +97 -27
  26. gisserver/parsers/fes20/filters.py +9 -6
  27. gisserver/parsers/fes20/identifiers.py +27 -7
  28. gisserver/parsers/fes20/lookups.py +8 -6
  29. gisserver/parsers/fes20/operators.py +101 -49
  30. gisserver/parsers/fes20/sorting.py +14 -6
  31. gisserver/parsers/gml/__init__.py +10 -19
  32. gisserver/parsers/gml/base.py +32 -14
  33. gisserver/parsers/gml/geometries.py +54 -21
  34. gisserver/parsers/ows/kvp.py +10 -2
  35. gisserver/parsers/ows/requests.py +6 -4
  36. gisserver/parsers/query.py +6 -2
  37. gisserver/parsers/values.py +61 -4
  38. gisserver/parsers/wfs20/__init__.py +2 -0
  39. gisserver/parsers/wfs20/adhoc.py +28 -18
  40. gisserver/parsers/wfs20/base.py +12 -7
  41. gisserver/parsers/wfs20/projection.py +3 -3
  42. gisserver/parsers/wfs20/requests.py +1 -0
  43. gisserver/parsers/wfs20/stored.py +3 -2
  44. gisserver/parsers/xml.py +12 -0
  45. gisserver/projection.py +17 -7
  46. gisserver/static/gisserver/index.css +27 -6
  47. gisserver/templates/gisserver/base.html +15 -0
  48. gisserver/templates/gisserver/index.html +10 -16
  49. gisserver/templates/gisserver/service_description.html +12 -6
  50. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  51. gisserver/templates/gisserver/wfs/feature_type.html +44 -13
  52. gisserver/types.py +152 -82
  53. gisserver/views.py +47 -24
  54. django_gisserver-2.0.dist-info/RECORD +0 -66
  55. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info/licenses}/LICENSE +0 -0
  56. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,18 @@
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
@@ -13,6 +26,7 @@ from typing import Union
13
26
 
14
27
  from django.contrib.gis import geos
15
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
31
  from django.db.models import Q, Value
18
32
  from django.db.models.expressions import Combinable
@@ -20,7 +34,7 @@ from django.db.models.expressions import Combinable
20
34
  from gisserver.exceptions import ExternalParsingError
21
35
  from gisserver.extensions.functions import function_registry
22
36
  from gisserver.parsers.ast import (
23
- BaseNode,
37
+ AstNode,
24
38
  TagNameEnum,
25
39
  expect_no_children,
26
40
  expect_tag,
@@ -36,7 +50,7 @@ from gisserver.parsers.gml import (
36
50
  from gisserver.parsers.query import RhsTypes
37
51
  from gisserver.parsers.values import auto_cast
38
52
  from gisserver.parsers.xml import NSElement, parse_qname, xmlns
39
- from gisserver.types import ORMPath, XsdTypes
53
+ from gisserver.types import XPathMatch, XsdTypes
40
54
 
41
55
  NoneType = type(None)
42
56
  ParsedValue = Union[int, str, date, D, datetime, GM_Object, GM_Envelope, TM_Object, NoneType]
@@ -74,10 +88,11 @@ class BinaryOperatorType(TagNameEnum):
74
88
  Div = operator.truediv
75
89
 
76
90
 
77
- class Expression(BaseNode):
91
+ class Expression(AstNode):
78
92
  """Abstract base class, as defined by FES spec.
79
93
 
80
94
  The FES spec defines the following subclasses:
95
+
81
96
  * :class:`ValueReference` (pointing to a field name)
82
97
  * :class:`Literal` (a scalar value)
83
98
  * :class:`Function` (a transformation for a value/field)
@@ -110,13 +125,13 @@ class Expression(BaseNode):
110
125
  @dataclass(repr=False)
111
126
  @tag_registry.register("Literal")
112
127
  class Literal(Expression):
113
- """The <fes:Literal> element that holds a literal value.
128
+ """The ``<fes:Literal>`` element that holds a literal value.
114
129
 
115
130
  This can be a string value, possibly annotated with a type::
116
131
 
117
132
  <fes:Literal type="xs:boolean">true</fes:Literal>
118
133
 
119
- Following the spec, the value may also contain a complete geometry:
134
+ Following the spec, the value may also contain a complete geometry::
120
135
 
121
136
  <fes:Literal>
122
137
  <gml:Envelope xmlns:gml="http://www.opengis.net/gml/3.2" srsName="urn:ogc:def:crs:EPSG::4326">
@@ -138,7 +153,9 @@ class Literal(Expression):
138
153
 
139
154
  @cached_property
140
155
  def value(self) -> ParsedValue: # officially <xsd:any>
141
- """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
+ """
142
159
  if not isinstance(self.raw_value, str):
143
160
  return self.raw_value # GML element or None
144
161
  elif self.type:
@@ -208,14 +225,22 @@ class Literal(Expression):
208
225
  @tag_registry.register("ValueReference")
209
226
  @tag_registry.register("PropertyName", hidden=True) # FES 1.0 name that old clients still use.
210
227
  class ValueReference(Expression):
211
- """The <fes:ValueReference> element that holds an XPath string.
228
+ """The ``<fes:ValueReference>`` element that holds an XPath string.
212
229
  In the fes XSD, this is declared as a subclass of xsd:string.
213
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
+
214
237
  The old WFS1/FES1 "PropertyName" is allowed as an alias.
215
238
  Various clients still send this, and mapserver/geoserver support this.
216
239
  """
217
240
 
241
+ #: The XPath value
218
242
  xpath: str
243
+ #: The known namespaces aliases at this point in the XML tree
219
244
  xpath_ns_aliases: dict[str, str] | None = field(compare=False, default=None)
220
245
 
221
246
  def __str__(self):
@@ -231,35 +256,37 @@ class ValueReference(Expression):
231
256
  return cls(xpath=element.text, xpath_ns_aliases=element.ns_aliases)
232
257
 
233
258
  def build_lhs(self, compiler) -> str:
234
- """Optimized LHS: there is no need to alias a field lookup through an annotation."""
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.
235
261
  match = self.parse_xpath(compiler.feature_types)
236
262
  return match.build_lhs(compiler)
237
263
 
238
264
  def build_rhs(self, compiler) -> RhsTypes:
239
- """Return the value as F-expression"""
265
+ """Use the field name in a right-hand expression.
266
+ This generates an F-expression for the ORM."""
240
267
  match = self.parse_xpath(compiler.feature_types)
241
268
  return match.build_rhs(compiler)
242
269
 
243
- def parse_xpath(self, feature_types: list) -> ORMPath:
270
+ def parse_xpath(self, feature_types: list) -> XPathMatch:
244
271
  """Convert the XPath into the required ORM query elements."""
245
- if feature_types:
246
- # Can resolve against XSD paths, find the correct DB field name
247
- return feature_types[0].resolve_element(self.xpath, self.xpath_ns_aliases)
248
- else:
249
- # Only used by unit testing (when feature_type is not given).
250
- parts = [word.strip() for word in self.xpath.split("/")]
251
- return ORMPath(orm_path="__".join(parts), orm_filters=None)
252
-
253
- @cached_property
254
- def element_name(self):
255
- """Tell which element this reference points to."""
256
- return self.xpath.rpartition("/")[2]
272
+ return feature_types[0].resolve_element(self.xpath, self.xpath_ns_aliases)
257
273
 
258
274
 
259
275
  @dataclass
260
276
  @tag_registry.register("Function")
261
277
  class Function(Expression):
262
- """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
+ """
263
290
 
264
291
  name: str # scoped name
265
292
  arguments: list[Expression] # xsd:element ref="fes20:expression"
@@ -284,8 +311,17 @@ class Function(Expression):
284
311
  class BinaryOperator(Expression):
285
312
  """Support for FES 1.0 arithmetic operators.
286
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
+
287
323
  These are no longer part of the FES 2.0 spec, but clients (like QGis)
288
- still assume the server supports these. Hence, these need to be included.
324
+ still assume the server use these. Hence, these need to be included.
289
325
  """
290
326
 
291
327
  _operatorType: BinaryOperatorType
@@ -302,9 +338,43 @@ class BinaryOperator(Expression):
302
338
  )
303
339
 
304
340
  def build_rhs(self, compiler) -> RhsTypes:
305
- value1 = _make_combinable(self.expression[0].build_rhs(compiler))
306
- value2 = _make_combinable(self.expression[1].build_rhs(compiler))
307
- 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
308
378
 
309
379
 
310
380
  def _make_combinable(value) -> Combinable | Q:
@@ -6,7 +6,7 @@ from typing import AnyStr, ClassVar, Union
6
6
  from django.db.models import Q
7
7
 
8
8
  from gisserver.exceptions import InvalidParameterValue
9
- from gisserver.parsers.ast import BaseNode, expect_tag, tag_registry
9
+ from gisserver.parsers.ast import AstNode, expect_tag, tag_registry
10
10
  from gisserver.parsers.gml import GEOSGMLGeometry
11
11
  from gisserver.parsers.ows import KVPRequest
12
12
  from gisserver.parsers.query import CompiledQuery
@@ -14,6 +14,7 @@ from gisserver.parsers.xml import NSElement, parse_xml_from_string, xmlns
14
14
 
15
15
  from . import expressions, identifiers, operators
16
16
 
17
+ #: The FES element group that can be used as body for the :class:`Filter` element.
17
18
  FilterPredicates = Union[expressions.Function, operators.Operator]
18
19
 
19
20
  # Fully qualified tag names
@@ -22,8 +23,8 @@ FES_RESOURCE_ID = xmlns.fes20.qname("ResourceId")
22
23
 
23
24
  @dataclass
24
25
  @tag_registry.register("Filter", xmlns.fes20)
25
- class Filter(BaseNode):
26
- """The <fes:Filter> element.
26
+ class Filter(AstNode):
27
+ """The ``<fes:Filter>`` element.
27
28
 
28
29
  This parses and handles the syntax::
29
30
 
@@ -41,7 +42,9 @@ class Filter(BaseNode):
41
42
 
42
43
  query_language: ClassVar[str] = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
43
44
 
45
+ #: The filter predicate (body)
44
46
  predicate: FilterPredicates
47
+
45
48
  source: AnyStr | None = field(default=None, compare=False)
46
49
 
47
50
  @classmethod
@@ -106,7 +109,7 @@ class Filter(BaseNode):
106
109
 
107
110
  @classmethod
108
111
  def from_string(cls, text: AnyStr, ns_aliases: dict[str, str] | None = None) -> Filter:
109
- """Parse an XML <fes20:Filter> string.
112
+ """Parse an XML ``<fes:Filter>`` string.
110
113
 
111
114
  This uses defusedxml by default, to avoid various XML injection attacks.
112
115
 
@@ -156,8 +159,8 @@ class Filter(BaseNode):
156
159
  return self.predicate.build_query(compiler)
157
160
 
158
161
  def get_resource_id_types(self) -> list[str] | None:
159
- """When the filter parses ResourceId objects, return those.
160
- This can return an empty list in case a ResourceId object doesn't define a type.
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.
161
164
  """
162
165
  if isinstance(self.predicate, operators.IdOperator):
163
166
  return self.predicate.get_type_names()
@@ -1,5 +1,11 @@
1
1
  """These classes map to the FES 2.0 specification for identifiers.
2
2
  The class names are identical to those in the FES spec.
3
+
4
+ Inheritance structure:
5
+
6
+ * :class:`Id`
7
+
8
+ * :class:`ResourceId`
3
9
  """
4
10
 
5
11
  from __future__ import annotations
@@ -12,7 +18,7 @@ from django.db.models import Q
12
18
 
13
19
  from gisserver import conf
14
20
  from gisserver.exceptions import ExternalValueError, InvalidParameterValue
15
- from gisserver.parsers.ast import BaseNode, expect_no_children, expect_tag, tag_registry
21
+ from gisserver.parsers.ast import AstNode, expect_no_children, expect_tag, tag_registry
16
22
  from gisserver.parsers.values import auto_cast, parse_iso_datetime
17
23
  from gisserver.parsers.xml import parse_qname, xmlns
18
24
 
@@ -20,7 +26,7 @@ NoneType = type(None)
20
26
 
21
27
 
22
28
  class VersionActionTokens(Enum):
23
- """Values for the 'version' attribute of the ResourceId node."""
29
+ """Values for the 'version' attribute of the :class:`ResourceId` node."""
24
30
 
25
31
  FIRST = "FIRST"
26
32
  LAST = "LAST"
@@ -29,8 +35,11 @@ class VersionActionTokens(Enum):
29
35
  PREVIOUS = "PREVIOUS"
30
36
 
31
37
 
32
- class Id(BaseNode):
33
- """Abstract base class, as defined by FES spec."""
38
+ class Id(AstNode):
39
+ """Abstract base class, as defined by FES spec.
40
+ Any custom identifier-element needs to extend from this node.
41
+ By default, the :class:`ResourceId` element is supported.
42
+ """
34
43
 
35
44
  xml_ns = xmlns.fes20
36
45
 
@@ -44,19 +53,30 @@ class Id(BaseNode):
44
53
  @dataclass
45
54
  @tag_registry.register("ResourceId")
46
55
  class ResourceId(Id):
47
- """The <fes:ResourceId> element.
56
+ """The ``<fes:ResourceId>`` element.
48
57
  This element allow queries to retrieve a resource by their identifier.
58
+
59
+ This parses the syntax::
60
+
61
+ <fes:ResourceId rid="typename.123" />
62
+
63
+ This element is placed inside a :class:`~gisserver.parsers.fes20.filters.Filter`.
49
64
  """
50
65
 
51
- # A raw "resource identifier". Needs to encode the object name somehow,
52
- # and it's completely unrelated to XML namespacing.
66
+ #: A raw "resource identifier". It typically includes the object name,
67
+ #: which is completely unrelated to XML namespacing.
53
68
  rid: str
69
+
70
+ #: Internal extra attribute, referencing the inferred typename from the :attr:`rid`.
54
71
  type_name: str | None
72
+
73
+ #: Unused, this is part of additional conformance classes.
55
74
  version: int | datetime | VersionActionTokens | NoneType = None
56
75
  startTime: datetime | None = None
57
76
  endTime: datetime | None = None
58
77
 
59
78
  def get_type_name(self):
79
+ """Implemented/override to expose the inferred type name."""
60
80
  return self.type_name
61
81
 
62
82
  def __post_init__(self):
@@ -14,7 +14,7 @@ from gisserver.compat import ArrayField
14
14
  @models.TextField.register_lookup
15
15
  @models.ForeignObject.register_lookup
16
16
  class FesLike(lookups.Lookup):
17
- """Allow fieldname__fes_like=... lookups in querysets."""
17
+ """Allow ``fieldname__fes_like=...`` lookups in querysets."""
18
18
 
19
19
  lookup_name = "fes_like"
20
20
 
@@ -36,7 +36,7 @@ class FesLike(lookups.Lookup):
36
36
  @models.Field.register_lookup
37
37
  @models.ForeignObject.register_lookup
38
38
  class FesNotEqual(lookups.Lookup):
39
- """Allow fieldname__fes_notequal=... lookups in querysets."""
39
+ """Allow ``fieldname__fes_notequal=...`` lookups in querysets."""
40
40
 
41
41
  lookup_name = "fes_notequal"
42
42
 
@@ -49,10 +49,12 @@ class FesNotEqual(lookups.Lookup):
49
49
 
50
50
  @BaseSpatialField.register_lookup
51
51
  class FesBeyondLookup(DWithinLookup):
52
- """Based on the FES 2.0.3 corrigendum:
52
+ """Allow ``fieldname__fes_beyond=...`` lookups in querysets.
53
53
 
54
- DWithin(A,B,d) = Distance(A,B) < d
55
- Beyond(A,B,d) = Distance(A,B) > d
54
+ Based on the FES 2.0.3 corrigendum:
55
+
56
+ * ``DWithin(A,B,d) = Distance(A,B) < d``
57
+ * ``Beyond(A,B,d) = Distance(A,B) > d``
56
58
 
57
59
  See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
58
60
  """
@@ -130,7 +132,7 @@ else:
130
132
 
131
133
  @ArrayField.register_lookup
132
134
  class FesArrayLike(FesLike):
133
- """Allow fieldname__fes_like=... lookups in querysets."""
135
+ """Allow like lookups for array fields."""
134
136
 
135
137
  lookup_name = "fes_anylike"
136
138