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.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- 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.0.dist-info}/LICENSE +0 -0
- {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)
|