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,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
@@ -11,16 +17,17 @@ from enum import Enum
11
17
  from django.db.models import Q
12
18
 
13
19
  from gisserver import conf
14
- from gisserver.exceptions import ExternalValueError
15
- from gisserver.parsers.base import BaseNode, tag_registry
16
- from gisserver.parsers.tags import expect_tag, get_attribute
20
+ from gisserver.exceptions import ExternalValueError, InvalidParameterValue
21
+ from gisserver.parsers.ast import AstNode, expect_no_children, expect_tag, tag_registry
17
22
  from gisserver.parsers.values import auto_cast, parse_iso_datetime
18
- from gisserver.types import FES20
23
+ from gisserver.parsers.xml import parse_qname, xmlns
19
24
 
20
25
  NoneType = type(None)
21
26
 
22
27
 
23
28
  class VersionActionTokens(Enum):
29
+ """Values for the 'version' attribute of the :class:`ResourceId` node."""
30
+
24
31
  FIRST = "FIRST"
25
32
  LAST = "LAST"
26
33
  ALL = "ALL"
@@ -28,13 +35,16 @@ class VersionActionTokens(Enum):
28
35
  PREVIOUS = "PREVIOUS"
29
36
 
30
37
 
31
- class Id(BaseNode):
32
- """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
+ """
33
43
 
34
- xml_ns = FES20
44
+ xml_ns = xmlns.fes20
35
45
 
36
- #: Tell which the type this ID belongs to, needs to be overwritten.
37
- type_name = ... # need to be defined by subclass!
46
+ def get_type_name(self):
47
+ raise NotImplementedError()
38
48
 
39
49
  def build_query(self, compiler) -> Q:
40
50
  raise NotImplementedError()
@@ -43,26 +53,47 @@ class Id(BaseNode):
43
53
  @dataclass
44
54
  @tag_registry.register("ResourceId")
45
55
  class ResourceId(Id):
46
- """The <fes:ResourceId> element."""
56
+ """The ``<fes:ResourceId>`` element.
57
+ This element allow queries to retrieve a resource by their identifier.
47
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`.
64
+ """
65
+
66
+ #: A raw "resource identifier". It typically includes the object name,
67
+ #: which is completely unrelated to XML namespacing.
48
68
  rid: str
69
+
70
+ #: Internal extra attribute, referencing the inferred typename from the :attr:`rid`.
71
+ type_name: str | None
72
+
73
+ #: Unused, this is part of additional conformance classes.
49
74
  version: int | datetime | VersionActionTokens | NoneType = None
50
75
  startTime: datetime | None = None
51
76
  endTime: datetime | None = None
52
77
 
78
+ def get_type_name(self):
79
+ """Implemented/override to expose the inferred type name."""
80
+ return self.type_name
81
+
53
82
  def __post_init__(self):
54
- try:
55
- self.type_name, self.id = self.rid.rsplit(".", 1)
56
- except ValueError:
57
- if conf.GISSERVER_WFS_STRICT_STANDARD:
58
- raise ExternalValueError("Expected typename.id format") from None
83
+ if conf.GISSERVER_WFS_STRICT_STANDARD and "." not in self.rid:
84
+ raise ExternalValueError("Expected typename.id format") from None
59
85
 
60
- # This should end in a 404 instead.
61
- self.type_name = None
62
- self.id = None
86
+ @classmethod
87
+ def from_string(cls, rid, ns_aliases: dict[str, str]):
88
+ # Like GeoServer, assume the "name" part of the "resource id" is a QName.
89
+ return cls(
90
+ rid=rid,
91
+ type_name=parse_qname(rid.rpartition(".")[0], ns_aliases),
92
+ )
63
93
 
64
94
  @classmethod
65
- @expect_tag(FES20, "ResourceId", leaf=True)
95
+ @expect_tag(xmlns.fes20, "ResourceId")
96
+ @expect_no_children
66
97
  def from_xml(cls, element):
67
98
  version = element.get("version")
68
99
  startTime = element.get("startTime")
@@ -71,24 +102,31 @@ class ResourceId(Id):
71
102
  if version:
72
103
  version = auto_cast(version)
73
104
 
105
+ rid = element.get_str_attribute("rid")
74
106
  return cls(
75
- rid=get_attribute(element, "rid"),
107
+ rid=rid,
108
+ type_name=element.parse_qname(rid.rpartition(".")[0]),
76
109
  version=version,
77
110
  startTime=parse_iso_datetime(startTime) if startTime else None,
78
111
  endTime=parse_iso_datetime(endTime) if endTime else None,
79
112
  )
80
113
 
81
- def build_query(self, compiler=None) -> Q:
114
+ def build_query(self, compiler) -> Q:
82
115
  """Render the SQL filter"""
83
116
  if self.startTime or self.endTime or self.version:
84
117
  raise NotImplementedError(
85
118
  "No support for <fes:ResourceId> startTime/endTime/version attributes"
86
119
  )
87
120
 
88
- lookup = Q(pk=self.id or self.rid)
89
- if compiler is not None:
90
- # When the
91
- # NOTE: type_name is currently read by the IdOperator that contains this object,
92
- # This code path only happens for stand-alone KVP invocation.
93
- compiler.add_lookups(lookup, type_name=self.type_name)
94
- return lookup
121
+ object_id = self.rid.rpartition(".")[2]
122
+
123
+ try:
124
+ # The 'ID' parameter is typed as string, but here we can check
125
+ # whether the database model needs an integer instead.
126
+ compiler.feature_types[0].model._meta.pk.get_prep_value(object_id)
127
+ except (TypeError, ValueError) as e:
128
+ raise InvalidParameterValue(
129
+ f"Invalid resourceId value: {e}", locator="resourceId"
130
+ ) from e
131
+
132
+ return Q(pk=object_id)
@@ -0,0 +1,146 @@
1
+ """Additional ORM lookups used by the fes-filter code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from django.contrib.gis.db.models.fields import BaseSpatialField
6
+ from django.contrib.gis.db.models.lookups import DWithinLookup
7
+ from django.db import models
8
+ from django.db.models import lookups
9
+
10
+ from gisserver.compat import ArrayField
11
+
12
+
13
+ @models.CharField.register_lookup
14
+ @models.TextField.register_lookup
15
+ @models.ForeignObject.register_lookup
16
+ class FesLike(lookups.Lookup):
17
+ """Allow ``fieldname__fes_like=...`` lookups in querysets."""
18
+
19
+ lookup_name = "fes_like"
20
+
21
+ def as_sql(self, compiler, connection):
22
+ """Generate the required SQL."""
23
+ # lhs = "table"."field"
24
+ # rhs = %s
25
+ # lhs_params = []
26
+ # lhs_params = ["prep-value"]
27
+ lhs, lhs_params = self.process_lhs(compiler, connection)
28
+ rhs, rhs_params = self.process_rhs(compiler, connection)
29
+ return f"{lhs} LIKE {rhs}", lhs_params + rhs_params
30
+
31
+ def get_db_prep_lookup(self, value, connection):
32
+ """This expects that the right-hand-side already has wildcard characters."""
33
+ return "%s", [value]
34
+
35
+
36
+ @models.Field.register_lookup
37
+ @models.ForeignObject.register_lookup
38
+ class FesNotEqual(lookups.Lookup):
39
+ """Allow ``fieldname__fes_notequal=...`` lookups in querysets."""
40
+
41
+ lookup_name = "fes_notequal"
42
+
43
+ def as_sql(self, compiler, connection):
44
+ """Generate the required SQL."""
45
+ lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
46
+ rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
47
+ return f"{lhs} != {rhs}", (lhs_params + rhs_params)
48
+
49
+
50
+ @BaseSpatialField.register_lookup
51
+ class FesBeyondLookup(DWithinLookup):
52
+ """Allow ``fieldname__fes_beyond=...`` lookups in querysets.
53
+
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``
58
+
59
+ See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
60
+ """
61
+
62
+ lookup_name = "fes_beyond"
63
+ sql_template = "NOT %(func)s(%(lhs)s, %(rhs)s, %(value)s)"
64
+
65
+ def get_rhs_op(self, connection, rhs):
66
+ # Allow the SQL $(func)s to be different from the ORM lookup name.
67
+ # This uses ST_DWithin() on PostGIS
68
+ return connection.ops.gis_operators["dwithin"]
69
+
70
+
71
+ if ArrayField is None:
72
+ ARRAY_LOOKUPS = {}
73
+ else:
74
+ # Comparisons with array fields go through a separate ORM lookup expression,
75
+ # so these can check whether ANY element matches in the array.
76
+ # This gives consistency between other repeated elements (e.g. M2M, reverse FK)
77
+ # where the whole object is returned when one of the sub-objects match.
78
+ ARRAY_LOOKUPS = {
79
+ "exact": "fes_anyexact",
80
+ "fes_notequal": "fes_anynotequal",
81
+ "fes_like": "fes_anylike",
82
+ "lt": "fes_anylt",
83
+ "lte": "fes_anylte",
84
+ "gt": "fes_anygt",
85
+ "gte": "fes_anygte",
86
+ }
87
+
88
+ class ArrayAnyMixin:
89
+ any_operators = {
90
+ "exact": "= ANY(%s)",
91
+ "ne": "!= ANY(%s)",
92
+ "gt": "< ANY(%s)",
93
+ "gte": "<= ANY(%s)",
94
+ "lt": "> ANY(%s)",
95
+ "lte": ">= ANY(%s)",
96
+ }
97
+
98
+ def as_sql(self, compiler, connection):
99
+ # For the ANY() comparison, the filter operands need to be reversed.
100
+ # So instead of "field < value", it becomes "value > ANY(field)
101
+ lhs_sql, lhs_params = self.process_lhs(compiler, connection)
102
+ rhs_sql, rhs_params = self.process_rhs(compiler, connection)
103
+ lhs_sql = self.get_rhs_op(connection, lhs_sql)
104
+ return f"{rhs_sql} {lhs_sql}", (rhs_params + lhs_params)
105
+
106
+ def get_rhs_op(self, connection, rhs):
107
+ return self.any_operators[self.lookup_name] % rhs
108
+
109
+ def _register_any_lookup(base: type[lookups.BuiltinLookup]):
110
+ """Register array lookups under a different name."""
111
+ cls = type(f"FesArrayAny{base.__name__}", (ArrayAnyMixin, base), {})
112
+ ArrayField.register_lookup(cls, lookup_name=f"fes_any{base.lookup_name}")
113
+
114
+ _register_any_lookup(lookups.Exact)
115
+ _register_any_lookup(lookups.Exact)
116
+ _register_any_lookup(lookups.GreaterThan)
117
+ _register_any_lookup(lookups.GreaterThanOrEqual)
118
+ _register_any_lookup(lookups.LessThan)
119
+ _register_any_lookup(lookups.LessThanOrEqual)
120
+
121
+ @ArrayField.register_lookup
122
+ class FesArrayAnyNotEqual(lookups.Lookup):
123
+ """Inequality test for a single item in the array"""
124
+
125
+ lookup_name = "fes_anynotequal"
126
+
127
+ def as_sql(self, compiler, connection):
128
+ """Generate the required SQL."""
129
+ lhs, lhs_params = self.process_lhs(compiler, connection)
130
+ rhs, rhs_params = self.process_rhs(compiler, connection)
131
+ return f"{rhs} != ANY({lhs})", (rhs_params + lhs_params)
132
+
133
+ @ArrayField.register_lookup
134
+ class FesArrayLike(FesLike):
135
+ """Allow like lookups for array fields."""
136
+
137
+ lookup_name = "fes_anylike"
138
+
139
+ def as_sql(self, compiler, connection):
140
+ """Generate the required SQL."""
141
+ lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
142
+ rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
143
+ return (
144
+ f"EXISTS(SELECT 1 FROM unnest({lhs}) AS item WHERE item LIKE {rhs})", # noqa: S608
145
+ (lhs_params + rhs_params),
146
+ )