django-gisserver 1.5.0__py3-none-any.whl → 2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.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/db.py +56 -47
  8. gisserver/exceptions.py +26 -2
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +220 -156
  13. gisserver/geometries.py +32 -37
  14. gisserver/management/__init__.py +0 -0
  15. gisserver/management/commands/__init__.py +0 -0
  16. gisserver/management/commands/loadgeojson.py +291 -0
  17. gisserver/operations/base.py +122 -308
  18. gisserver/operations/wfs20.py +423 -337
  19. gisserver/output/__init__.py +9 -48
  20. gisserver/output/base.py +178 -139
  21. gisserver/output/csv.py +65 -74
  22. gisserver/output/geojson.py +34 -35
  23. gisserver/output/gml32.py +254 -246
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +52 -26
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -170
  28. gisserver/output/xmlschema.py +85 -46
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +13 -27
  32. gisserver/parsers/fes20/expressions.py +82 -38
  33. gisserver/parsers/fes20/filters.py +111 -43
  34. gisserver/parsers/fes20/identifiers.py +44 -26
  35. gisserver/parsers/fes20/lookups.py +144 -0
  36. gisserver/parsers/fes20/operators.py +331 -127
  37. gisserver/parsers/fes20/sorting.py +104 -33
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +5 -2
  40. gisserver/parsers/gml/geometries.py +69 -35
  41. gisserver/parsers/ows/__init__.py +25 -0
  42. gisserver/parsers/ows/kvp.py +190 -0
  43. gisserver/parsers/ows/requests.py +158 -0
  44. gisserver/parsers/query.py +175 -0
  45. gisserver/parsers/values.py +26 -0
  46. gisserver/parsers/wfs20/__init__.py +37 -0
  47. gisserver/parsers/wfs20/adhoc.py +245 -0
  48. gisserver/parsers/wfs20/base.py +143 -0
  49. gisserver/parsers/wfs20/projection.py +103 -0
  50. gisserver/parsers/wfs20/requests.py +482 -0
  51. gisserver/parsers/wfs20/stored.py +192 -0
  52. gisserver/parsers/xml.py +249 -0
  53. gisserver/projection.py +357 -0
  54. gisserver/static/gisserver/index.css +12 -1
  55. gisserver/templates/gisserver/index.html +1 -1
  56. gisserver/templates/gisserver/service_description.html +2 -2
  57. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +322 -259
  61. gisserver/views.py +198 -56
  62. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -285
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -37
  67. gisserver/queries/adhoc.py +0 -185
  68. gisserver/queries/base.py +0 -186
  69. gisserver/queries/projection.py +0 -240
  70. gisserver/queries/stored.py +0 -206
  71. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  72. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  73. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  74. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,482 @@
1
+ """WFS 2.0 request objects for parsing.
2
+
3
+ These objects convert the various request formats into a uniform request object.
4
+ The object properties are based on the WFS specification and XSD type names.
5
+ By following these definitions closely, it naturally follows to support nearly
6
+ all possible request formats outside the common examples.
7
+
8
+ Examples:
9
+
10
+ * https://schemas.opengis.net/wfs/2.0/examples/
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+
18
+ from gisserver.exceptions import (
19
+ InvalidParameterValue,
20
+ MissingParameterValue,
21
+ OperationParsingFailed,
22
+ )
23
+ from gisserver.parsers import fes20
24
+ from gisserver.parsers.ast import expect_no_children, tag_registry
25
+ from gisserver.parsers.ows import BaseOwsRequest, KVPRequest
26
+ from gisserver.parsers.xml import NSElement, xmlns
27
+
28
+ from .adhoc import AdhocQuery
29
+ from .base import QueryExpression
30
+ from .projection import ResolveValue, parse_resolve_depth
31
+ from .stored import StoredQuery
32
+
33
+ OWS_GET_CAPABILITIES_ELEMENTS = {
34
+ # {"child-tag": ("item-tag", "python_name")}
35
+ xmlns.ows11.qname("AcceptVersions"): (xmlns.ows11.qname("Version"), "acceptVersions"),
36
+ xmlns.ows11.qname("Sections"): (xmlns.ows11.qname("Section"), "sections"),
37
+ xmlns.ows11.qname("AcceptFormats"): (xmlns.ows11.qname("OutputFormat"), "acceptFormats"),
38
+ xmlns.ows11.qname("AcceptLanguages"): (xmlns.ows11.qname("Language"), "acceptLanguages"),
39
+ }
40
+
41
+ # Fully qualified tag names
42
+ WFS_TYPE_NAME = xmlns.wfs20.qname("TypeName")
43
+ WFS_STORED_QUERY = xmlns.wfs20.qname("StoredQuery")
44
+
45
+
46
+ @dataclass
47
+ @tag_registry.register("GetCapabilities", xmlns.wfs20)
48
+ class GetCapabilities(BaseOwsRequest):
49
+ """Request parsing for GetCapabilities.
50
+
51
+ This parses and handles the syntax::
52
+
53
+ <wfs:GetCapabilities service="WFS" updateSequence="" version="">
54
+ <ows:AcceptVersions>
55
+ <ows:Version>...</ows:Version>
56
+ </ows:AcceptVersions>
57
+ <ows:Sections>
58
+ <ows:Section>...</ows:Section>
59
+ <ows:Section>...</ows:Section>
60
+ </ows:Sections>
61
+ <ows:AcceptFormats>
62
+ <ows:OutputFormat>...</ows:OutputFormat>
63
+ <ows:OutputFormat>...</ows:OutputFormat>
64
+ </ows:AcceptFormats>
65
+ <ows:AcceptLanguages>
66
+ <ows:Language>...</ows:Language>
67
+ <ows:Language>...</ows:Language>
68
+ </ows:AcceptLanguages>
69
+ </wfs:GetCapabilities>
70
+
71
+ And supports the GET syntax as well::
72
+
73
+ ?SERVICE=WFS&REQUEST=GetCapabilities&ACCEPTVERSIONS=2.0.0,1.1.0
74
+ """
75
+
76
+ updateSequence: str | None = None
77
+ acceptVersions: list[str] | None = None
78
+ # Sections can be: "ServiceIdentification", "ServiceProvider", "OperationsMetadata", "FeatureTypeList", "Filter_Capabilities"
79
+ sections: list[str] | None = None
80
+ acceptFormats: list[str] | None = None
81
+ acceptLanguages: list[str] | None = None
82
+
83
+ def __post_init__(self):
84
+ # Even GetCapabilities can still receive a version argument to fixate it.
85
+ if self.version and self.acceptVersions:
86
+ raise InvalidParameterValue(
87
+ "Can't provide both AcceptVersions and version", locator="AcceptVersions"
88
+ )
89
+
90
+ @classmethod
91
+ def from_xml(cls, element: NSElement):
92
+ """Parse the XML tag for the GetCapabilities."""
93
+ ows_kwargs = {}
94
+ for child in element:
95
+ pair = OWS_GET_CAPABILITIES_ELEMENTS.get(child.tag)
96
+ if pair is not None:
97
+ item_tag, arg_name = pair
98
+ ows_kwargs[arg_name] = [item.text for item in child.findall(item_tag)]
99
+
100
+ return cls(
101
+ # version is optional for this type, unlike all other methods
102
+ # so not calling **cls.base_xml_init_parameters()
103
+ service=element.get_str_attribute("service"),
104
+ version=element.attrib.get("version"),
105
+ handle=element.attrib.get("handle"),
106
+ updateSequence=element.attrib.get("updateSequence", None),
107
+ **ows_kwargs,
108
+ )
109
+
110
+ @classmethod
111
+ def from_kvp_request(cls, kvp: KVPRequest):
112
+ """Parse the KVP request format."""
113
+ return cls(
114
+ # version is optional for this type, unlike all other methods
115
+ # so not calling **cls.base_kvp_init_parameters()
116
+ service=kvp.get_str("SERVICE"),
117
+ version=kvp.get_str("VERSION", default=None),
118
+ handle=None,
119
+ acceptVersions=kvp.get_list("AcceptVersions", default=None),
120
+ acceptFormats=kvp.get_list("AcceptFormats", default=None),
121
+ acceptLanguages=kvp.get_list("AcceptLanguages", default=None),
122
+ sections=kvp.get_list("sections", default=None),
123
+ )
124
+
125
+
126
+ @dataclass
127
+ @tag_registry.register("DescribeFeatureType", xmlns.wfs20)
128
+ class DescribeFeatureType(BaseOwsRequest):
129
+ """The ``<wfs:DescribeFeatureType>`` element.
130
+
131
+ This parses the syntax::
132
+
133
+ <wfs:DescribeFeatureType version="2.0.0" service="WFS">
134
+ <wfs:TypeName>ns01:TreesA_1M</wfs:TypeName>
135
+ <wfs:TypeName>ns02:RoadL_1M</wfs:TypeName>
136
+ </wfs:DescribeFeatureType>
137
+
138
+ And supports the GET syntax as well::
139
+
140
+ ?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES=ns01:TreesA_1M,ns02:RoadL_1M
141
+ """
142
+
143
+ typeNames: list[str] | None = None
144
+ # WFS spec actually defines "application/gml+xml; version="3.2" as default output format value.
145
+ outputFormat: str | None = "XMLSCHEMA"
146
+
147
+ @classmethod
148
+ def from_xml(cls, element: NSElement):
149
+ """Parse the XML POST request."""
150
+ type_name_tags = element.findall(WFS_TYPE_NAME)
151
+ if any(not e.text for e in type_name_tags):
152
+ raise MissingParameterValue("Missing TypeName value", locator="TypeName")
153
+
154
+ return cls(
155
+ **cls.base_xml_init_parameters(element),
156
+ typeNames=[e.parse_qname(e.text) for e in type_name_tags] or None,
157
+ outputFormat=element.attrib.get("outputFormat", "XMLSCHEMA"),
158
+ )
159
+
160
+ @classmethod
161
+ def from_kvp_request(cls, kvp: KVPRequest):
162
+ """Parse the KVP GET request"""
163
+ type_names = (
164
+ # Check for empty values, don't check for missing values:
165
+ kvp.get_list("typeNames", alias="typename")
166
+ if ("typeNames" in kvp or "typename" in kvp)
167
+ else None
168
+ )
169
+
170
+ return cls(
171
+ **cls.base_kvp_init_parameters(kvp),
172
+ # TYPENAME is WFS 1.x, but some clients and the Cite test suite send it.
173
+ typeNames=(
174
+ [kvp.parse_qname(name) for name in type_names] if type_names is not None else None
175
+ ),
176
+ outputFormat=kvp.get_str("outputFormat", default="XMLSCHEMA"),
177
+ )
178
+
179
+
180
+ class ResultType(Enum):
181
+ """WFS result type format."""
182
+
183
+ hits = "HITS"
184
+ results = "RESULTS"
185
+
186
+ @classmethod
187
+ def _missing_(cls, value):
188
+ raise OperationParsingFailed(f"Invalid resultType value: {value}", locator="resultType")
189
+
190
+
191
+ @dataclass
192
+ class StandardPresentationParameters(BaseOwsRequest):
193
+ """Mixin that handles presentation parameters shared between different types.
194
+ This element mirrors the ``wfs:StandardPresentationParameters`` type.
195
+ """
196
+
197
+ count: int | None = None
198
+ outputFormat: str = "application/gml+xml; version=3.2"
199
+ resultType: ResultType = ResultType.results
200
+ startIndex: int = 0
201
+
202
+ @classmethod
203
+ def base_xml_init_parameters(cls, element: NSElement) -> dict:
204
+ """Parse the XML POST request."""
205
+ return dict(
206
+ **super().base_xml_init_parameters(element),
207
+ count=element.get_int_attribute("count"),
208
+ outputFormat=element.attrib.get("outputFormat", "application/gml+xml; version=3.2"),
209
+ resultType=ResultType[element.attrib.get("resultType", "results")],
210
+ startIndex=element.get_int_attribute("startIndex", 0),
211
+ )
212
+
213
+ @classmethod
214
+ def base_kvp_init_parameters(cls, kvp: KVPRequest) -> dict:
215
+ """Parse the KVP GET request."""
216
+ return dict(
217
+ **super().base_kvp_init_parameters(kvp),
218
+ # maxFeatures is WFS 1.x but some clients still send it.
219
+ count=kvp.get_int("count", alias="maxFeatures", default=None),
220
+ outputFormat=kvp.get_str("outputFormat", default="application/gml+xml; version=3.2"),
221
+ resultType=ResultType[kvp.get_str("resultType", default="results").lower()],
222
+ startIndex=kvp.get_int("startIndex", default=0),
223
+ )
224
+
225
+ def as_kvp(self) -> dict:
226
+ """Translate the POST request into KVP GET parameters. This is needed for pagination."""
227
+ params = super().as_kvp()
228
+ if self.outputFormat != "application/gml+xml; version=3.2":
229
+ params["OUTPUTFORMAT"] = self.outputFormat
230
+ if self.resultType != ResultType.results:
231
+ params["RESULTTYPE"] = self.resultType.value
232
+ if self.startIndex:
233
+ params["STARTINDEX"] = self.startIndex
234
+ if self.count is not None:
235
+ params["COUNT"] = self.count
236
+ return params
237
+
238
+
239
+ @dataclass
240
+ class StandardResolveParameters(BaseOwsRequest):
241
+ """Mixin that handles resolve parameters shared between different types.
242
+ This element mirrors the ``wfs:StandardResolveParameters`` type.
243
+ """
244
+
245
+ resolve: ResolveValue = ResolveValue.none
246
+ resolveDepth: int | None = None
247
+ resolveTimeout: int = 300
248
+
249
+ @classmethod
250
+ def base_xml_init_parameters(cls, element: NSElement) -> dict:
251
+ """Parse the XML POST request."""
252
+ return dict(
253
+ **super().base_xml_init_parameters(element),
254
+ resolve=ResolveValue[element.attrib.get("resolve", "none")],
255
+ resolveDepth=parse_resolve_depth(element.attrib.get("resolveDepth", None)),
256
+ resolveTimeout=element.get_int_attribute("resolveTimeout", 300),
257
+ )
258
+
259
+ @classmethod
260
+ def base_kvp_init_parameters(cls, kvp: KVPRequest) -> dict:
261
+ """Parse the KVP GET request."""
262
+ depth = kvp.get_str("resolveDepth", default="*")
263
+ return dict(
264
+ **super().base_kvp_init_parameters(kvp),
265
+ resolve=ResolveValue[kvp.get_str("resolve", default="none")],
266
+ resolveDepth=parse_resolve_depth(depth),
267
+ resolveTimeout=kvp.get_int("resolveTimeout", default=300),
268
+ )
269
+
270
+
271
+ @dataclass
272
+ class CommonQueryParameters(BaseOwsRequest):
273
+ """Internal mixin to deal with the query parameters"""
274
+
275
+ queries: list[QueryExpression] = None # need default for inheritance
276
+
277
+ @classmethod
278
+ def base_xml_init_parameters(cls, element: NSElement):
279
+ """Parse the XML POST request"""
280
+ return dict(
281
+ **super().base_xml_init_parameters(element),
282
+ queries=[
283
+ # This can instantiate an AdhocQuery or StoredQuery
284
+ QueryExpression.child_from_xml(child)
285
+ for child in element
286
+ ],
287
+ )
288
+
289
+ @classmethod
290
+ def base_kvp_init_parameters(cls, kvp: KVPRequest):
291
+ """Parse the KVP GET request"""
292
+ stored_query_id = kvp.get_str("STOREDQUERY_ID", default=None)
293
+ if stored_query_id:
294
+ queries = [
295
+ StoredQuery.from_kvp_request(sub_request)
296
+ for sub_request in kvp.split_parameter_lists()
297
+ ]
298
+ else:
299
+ queries = [
300
+ AdhocQuery.from_kvp_request(sub_request)
301
+ for sub_request in kvp.split_parameter_lists()
302
+ ]
303
+
304
+ return dict(
305
+ **super().base_kvp_init_parameters(kvp),
306
+ queries=queries,
307
+ )
308
+
309
+ def as_kvp(self) -> dict:
310
+ """Translate the POST request into KVP GET parameters. This is needed for pagination."""
311
+ if len(self.queries) > 1:
312
+ raise NotImplementedError()
313
+ return {
314
+ **super().as_kvp(),
315
+ **self.queries[0].as_kvp(),
316
+ }
317
+
318
+
319
+ @dataclass
320
+ @tag_registry.register("GetFeature", xmlns.wfs20)
321
+ class GetFeature(
322
+ StandardPresentationParameters,
323
+ StandardResolveParameters,
324
+ CommonQueryParameters,
325
+ BaseOwsRequest,
326
+ ):
327
+ """The ``<wfs:GetFeature>`` element.
328
+
329
+ This parses the syntax::
330
+
331
+ <wfs:GetFeature outputFormat="application/gml+xml; version=3.2">
332
+ <wfs:Query typeName="myns:InWaterA_1M">
333
+ <fes:Filter>
334
+ ...
335
+ </fes:Filter>
336
+ </wfs:Query>
337
+ </wfs:GetFeature>
338
+
339
+ And supports the KVP syntax::
340
+
341
+ ?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=myns:InWaterA_1M&FILTER=...
342
+
343
+ """
344
+
345
+
346
+ @dataclass
347
+ @tag_registry.register("GetPropertyValue", xmlns.wfs20)
348
+ class GetPropertyValue(
349
+ StandardPresentationParameters,
350
+ StandardResolveParameters,
351
+ CommonQueryParameters,
352
+ BaseOwsRequest,
353
+ ):
354
+ """The ``<wfs:GetPropertyValue>`` element.
355
+
356
+ This parses the syntax::
357
+
358
+ <wfs:GetPropertyValue valueReference="...">
359
+ <wfs:Query typeName="myns:InWaterA_1M">
360
+ <fes:Filter>
361
+ ...
362
+ </fes:Filter>
363
+ </wfs:Query>
364
+ </wfs:GetFeature>
365
+
366
+ As this is so similar to the :class:`GetFeature` syntax, these inherit from each other.
367
+
368
+ The KVP-syntax is also supported::
369
+
370
+ ?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetPropertyName&...&&VALUEREFERENCE=...
371
+ """
372
+
373
+ resolvePath: str | None = None
374
+ valueReference: fes20.ValueReference = None # need default for inheritance
375
+
376
+ def __post_init__(self):
377
+ if self.resolvePath:
378
+ raise InvalidParameterValue(
379
+ "Support for resolvePath is not implemented!", locator="resolvePath"
380
+ )
381
+ if len(self.queries) > 1:
382
+ raise InvalidParameterValue(
383
+ "GetPropertyValue only supports a single query", locator="filter"
384
+ )
385
+
386
+ @classmethod
387
+ def from_xml(cls, element: NSElement):
388
+ """Parse the XML POST request."""
389
+ return cls(
390
+ **cls.base_xml_init_parameters(element),
391
+ resolvePath=element.attrib.get("resolvePath", None),
392
+ valueReference=fes20.ValueReference(
393
+ xpath=element.get_str_attribute("valueReference"),
394
+ xpath_ns_aliases=element.ns_aliases,
395
+ ),
396
+ )
397
+
398
+ @classmethod
399
+ def from_kvp_request(cls, kvp: KVPRequest):
400
+ """Parse the KVP GET request."""
401
+ return cls(
402
+ **cls.base_kvp_init_parameters(kvp),
403
+ resolvePath=kvp.get_str("resolvePath", default=None),
404
+ valueReference=fes20.ValueReference(
405
+ xpath=kvp.get_str("valueReference"),
406
+ xpath_ns_aliases=kvp.ns_aliases,
407
+ ),
408
+ )
409
+
410
+ def as_kvp(self) -> dict:
411
+ """Translate the POST request into KVP GET parameters. This is needed for pagination."""
412
+ return {
413
+ **super().as_kvp(),
414
+ "VALUEREFERENCE": self.valueReference.xpath,
415
+ }
416
+
417
+
418
+ @tag_registry.register("ListStoredQueries", xmlns.wfs20)
419
+ class ListStoredQueries(BaseOwsRequest):
420
+ """The ``<wfs:ListStoredQueries>`` element."""
421
+
422
+ @classmethod
423
+ @expect_no_children
424
+ def from_xml(cls, element: NSElement):
425
+ return super().from_xml(element)
426
+
427
+
428
+ @dataclass
429
+ @tag_registry.register("DescribeStoredQueries", xmlns.wfs20)
430
+ class DescribeStoredQueries(BaseOwsRequest):
431
+ """The ``<wfs:DescribeStoredQueries>`` element.
432
+
433
+ This parses the syntax::
434
+
435
+ <wfs:DescribeStoredQueries>
436
+ <wfs:StoredQueryId>...</wfs:StoredQueryId>
437
+ <wfs:StoredQueryId>...</wfs:StoredQueryId>
438
+ </wfs:DescribeStoredQueries>
439
+
440
+ And the KVP syntax::
441
+
442
+ ?REQUEST=DescribeStoredQueries&STOREDQUERY_ID=...,...
443
+ """
444
+
445
+ storedQueryId: list[str] | None = None
446
+
447
+ @classmethod
448
+ def from_xml(cls, element: NSElement):
449
+ """Parse the XML POST request."""
450
+ id_tags = element.findall(WFS_STORED_QUERY)
451
+ if any(not e.text for e in id_tags):
452
+ raise MissingParameterValue("Missing StoredQuery value", locator="StoredQuery")
453
+
454
+ return cls(
455
+ **cls.base_xml_init_parameters(element),
456
+ storedQueryId=[child.text for child in id_tags] or None,
457
+ )
458
+
459
+ @classmethod
460
+ def from_kvp_request(cls, kvp: KVPRequest):
461
+ """Parse the KVP GET request."""
462
+ return cls(
463
+ **cls.base_kvp_init_parameters(kvp),
464
+ storedQueryId=kvp.get_list("STOREDQUERY_ID", default=None),
465
+ )
466
+
467
+
468
+ # @tag_registry.register("CreateStoredQuery", xmlns.wfs20)
469
+ # class CreateStoredQuery(BaseRequest):
470
+ # ...
471
+ #
472
+ # @tag_registry.register("LockFeature", xmlns.wfs20)
473
+ # class LockFeature(BaseRequest):
474
+ # ...
475
+ #
476
+ # @tag_registry.register("TransactionType", xmlns.wfs20)
477
+ # class TransactionType(BaseRequest):
478
+ # ...
479
+ #
480
+ # @tag_registry.register("DropStoredQuery", xmlns.wfs20)
481
+ # class DropStoredQuery(BaseRequest):
482
+ # ...
@@ -0,0 +1,192 @@
1
+ """Handle (stored)query objects.
2
+
3
+ These definitions follow the WFS spec.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import typing
9
+ from dataclasses import dataclass, field
10
+ from functools import cached_property
11
+ from typing import Any, ClassVar
12
+
13
+ from gisserver.exceptions import (
14
+ ExternalParsingError,
15
+ InvalidParameterValue,
16
+ MissingParameterValue,
17
+ )
18
+ from gisserver.extensions.queries import (
19
+ StoredQueryDescription,
20
+ StoredQueryImplementation,
21
+ stored_query_registry,
22
+ )
23
+ from gisserver.parsers.ast import tag_registry
24
+ from gisserver.parsers.ows import KVPRequest
25
+ from gisserver.parsers.query import CompiledQuery
26
+ from gisserver.parsers.xml import NSElement, xmlns
27
+ from gisserver.projection import FeatureProjection
28
+
29
+ from .base import QueryExpression
30
+
31
+ if typing.TYPE_CHECKING:
32
+ from gisserver.output import SimpleFeatureCollection
33
+
34
+ __all__ = ("StoredQuery",)
35
+
36
+ # Fully qualified tag names
37
+ WFS_PARAMETER = xmlns.wfs20.qname("Parameter")
38
+
39
+
40
+ @dataclass
41
+ @tag_registry.register("StoredQuery", xmlns.wfs)
42
+ class StoredQuery(QueryExpression):
43
+ """The ``<wfs:StoredQuery>`` element.
44
+
45
+ This loads a predefined query on the server.
46
+ A good description can be found at:
47
+ https://mapserver.org/ogc/wfs_server.html#stored-queries-wfs-2-0
48
+
49
+ This parses the following syntax::
50
+
51
+ <wfs:StoredQuery handle="" id="">
52
+ <wfs:Parameter name="">...</wfs:Parameter>
53
+ <wfs:Parameter name="">...</wfs:Parameter>
54
+ </wfs:StoredQuery>
55
+
56
+ and the KVP syntax::
57
+
58
+ ?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&STOREDQUERY_ID=...&{parameter}=...
59
+
60
+ Note that the base class logic (such as ``<wfs:PropertyName>`` elements) are still applicable.
61
+
62
+ This element resolves the stored query using the
63
+ :class:`~gisserver.projection.storage.StoredQueryRegistry`,
64
+ and passes the execution to this custom function.
65
+ """
66
+
67
+ query_locator: ClassVar[str] = "STOREDQUERY_ID"
68
+
69
+ id: str
70
+ parameters: dict[str, Any]
71
+ ns_aliases: dict[str, str] = field(compare=False, default_factory=dict)
72
+
73
+ @classmethod
74
+ def from_kvp_request(cls, kvp: KVPRequest):
75
+ """Parse the KVP request syntax. Any query parameters are additional parameters at the query string."""
76
+ query_id = kvp.get_str("STOREDQUERY_ID")
77
+ query_description = stored_query_registry.resolve_query(query_id)
78
+ raw_values = {
79
+ # Take the value when it's there. No validation is done yet,
80
+ # as those error messages are nicer in _parse_parameters()
81
+ name: kvp.get_str(name)
82
+ for name in query_description.parameters
83
+ if name in kvp
84
+ }
85
+
86
+ # Avoid unexpected behavior, check whether the client also sends adhoc query parameters
87
+ uc_args = [name.upper() for name in raw_values]
88
+ for name in ("filter", "bbox", "resourceID"):
89
+ uc_name = name.upper()
90
+ if uc_name not in uc_args and uc_name in kvp:
91
+ raise InvalidParameterValue(
92
+ "Stored query can't be combined with adhoc-query parameters", locator=name
93
+ )
94
+
95
+ return cls(
96
+ id=query_id,
97
+ parameters=cls._parse_parameters(query_description, raw_values),
98
+ ns_aliases=kvp.ns_aliases,
99
+ )
100
+
101
+ @classmethod
102
+ def from_xml(cls, element: NSElement):
103
+ """Read the XML element."""
104
+ query_id = element.get_str_attribute("id")
105
+ query_description = stored_query_registry.resolve_query(query_id)
106
+ raw_values = {
107
+ parameter.get_str_attribute("name"): parameter.text
108
+ for parameter in element.findall(WFS_PARAMETER)
109
+ }
110
+ return cls(
111
+ id=query_id,
112
+ parameters=cls._parse_parameters(query_description, raw_values),
113
+ ns_aliases=element.ns_aliases,
114
+ )
115
+
116
+ @classmethod
117
+ def _parse_parameters(
118
+ cls, query_description: StoredQueryDescription, raw_values: dict[str, str]
119
+ ) -> dict[str, Any]:
120
+ """Validate and translate the incoming parameter.
121
+ This transforms the raw values into their Python types.
122
+ It also validates whether ll expected parameters are provided.
123
+ """
124
+ values = {}
125
+ for name, xsd_type in query_description.parameters.items():
126
+ try:
127
+ raw_value = raw_values.pop(name)
128
+ except KeyError:
129
+ raise MissingParameterValue(
130
+ f"Stored query {query_description.id} requires an '{name}' parameter",
131
+ locator=name,
132
+ ) from None
133
+
134
+ try:
135
+ values[name] = xsd_type.to_python(raw_value)
136
+ except ExternalParsingError as e:
137
+ raise InvalidParameterValue(
138
+ f"Stored query {query_description.id} parameter '{name}' can't parse '{raw_value}' as {xsd_type}."
139
+ ) from e
140
+
141
+ # Anything left means more parameters were given
142
+ if raw_values:
143
+ names = ", ".join(raw_values)
144
+ raise InvalidParameterValue(
145
+ f"Stored query {query_description.id} does not support the parameter: '{names}'.",
146
+ locator=next(iter(raw_values)),
147
+ )
148
+
149
+ return values
150
+
151
+ @cached_property
152
+ def implementation(self) -> StoredQueryImplementation:
153
+ """Initialize the stored query from this request."""
154
+ query_description = stored_query_registry.resolve_query(self.id)
155
+ return query_description.implementation_class(
156
+ **self.parameters, ns_aliases=self.ns_aliases
157
+ )
158
+
159
+ def bind(self, *args, **kwargs):
160
+ """Associate this query with the application context."""
161
+ super().bind(*args, **kwargs)
162
+ self.implementation.bind(source_query=self, feature_types=self.feature_types)
163
+
164
+ def build_query(self, compiler: CompiledQuery):
165
+ """Forward queryset creation to the implementation class."""
166
+ return self.implementation.build_query(compiler)
167
+
168
+ def as_kvp(self):
169
+ # As this is such edge case, only support the minimal for CITE tests.
170
+ params = super().as_kvp()
171
+ params["STOREDQUERY_ID"] = self.id
172
+ for name, value in self.parameters.items():
173
+ params[name] = str(value) # should be raw value, but good enough for now.
174
+ return params
175
+
176
+ def get_type_names(self) -> list[str]:
177
+ """Tell which features are touched by the query."""
178
+ return self.implementation.get_type_names()
179
+
180
+ def get_projection(self) -> FeatureProjection:
181
+ """Tell how the <wfs:StoredQuery> output should be displayed."""
182
+ return FeatureProjection(
183
+ self.feature_types,
184
+ self.property_names,
185
+ value_reference=self.value_reference,
186
+ # In the spec, it's not possible to change the output CRS of a stored query:
187
+ # output_crs=self.srsName,
188
+ output_standalone=self.implementation.has_standalone_output, # for GetFeatureById
189
+ )
190
+
191
+ def finalize_results(self, result: SimpleFeatureCollection):
192
+ return self.implementation.finalize_results(result)