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,141 +1,78 @@
1
1
  """The base protocol to implement an operation.
2
2
 
3
- All operations extend from an WFSMethod class.
4
- This defines the parameters and output formats of the method.
5
- This introspection data is also parsed by the GetCapabilities call.
3
+ The request itself is parsed by :mod:`gisserver.parsers.wfs20`, and handled here.
4
+ It can be seen as the "controller" that handles the actual request type.
5
+
6
+ All operations extend from an WFSOperation class.
7
+ This defines the metadata for the ``GetCapabilities`` call and possible output formats.
8
+
9
+ Each :class:`WFSOperation` can define a :attr:`~WFSOperation.parser_class`, or let it autodetect.
6
10
  """
7
11
 
8
12
  from __future__ import annotations
9
13
 
14
+ import logging
10
15
  import math
11
16
  import re
12
- from collections.abc import Callable
13
- from dataclasses import dataclass, field
14
- from typing import Any
17
+ import typing
18
+ from dataclasses import dataclass
19
+ from functools import cached_property
15
20
 
16
- from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
21
+ from django.core.exceptions import SuspiciousOperation
17
22
  from django.http import HttpResponse
18
23
  from django.template.loader import render_to_string
19
24
 
20
- from gisserver.exceptions import (
21
- ExternalParsingError,
22
- InvalidParameterValue,
23
- MissingParameterValue,
24
- OperationParsingFailed,
25
- )
25
+ from gisserver.exceptions import InvalidParameterValue
26
26
  from gisserver.features import FeatureType
27
+ from gisserver.output.base import OutputRenderer
28
+ from gisserver.parsers import ows
29
+ from gisserver.parsers.values import fix_type_name
27
30
 
31
+ if typing.TYPE_CHECKING:
32
+ from gisserver.views import WFSView
33
+
34
+ logger = logging.getLogger(__name__)
28
35
  NoneType = type(None)
36
+ R = typing.TypeVar("R", bound=OutputRenderer)
29
37
 
30
38
  RE_SAFE_FILENAME = re.compile(r"\A[A-Za-z0-9]+[A-Za-z0-9.]*") # no dot at the start.
31
39
 
40
+ __all__ = (
41
+ "Parameter",
42
+ "OutputFormat",
43
+ "WFSOperation",
44
+ "OutputFormatMixin",
45
+ "XmlTemplateMixin",
46
+ )
47
+
32
48
 
33
49
  @dataclass(frozen=True)
34
50
  class Parameter:
35
- """The definition of a parameter for an WFS method.
51
+ """The ``<ows:Parameter>`` tag to output in ``GetCapabilities``."""
36
52
 
37
- These parameters should be defined in the
38
- WFSMethod parameters attribute / get_parameters().
39
-
40
- These fields are used to parse the incoming request.
41
- The GetCapabilities method also reads the metadata of this value.
42
- """
43
-
44
- #: Name of the parameter (using the XML casing style)
53
+ #: The name of the parameter
45
54
  name: str
46
-
47
- #: Alias name (e.g. typenames/typename)
48
- alias: str | None = None
49
-
50
- #: Whether the parameter is required
51
- required: bool = False
52
-
53
- #: Optional parser callback to convert the parameter into a Python type
54
- parser: Callable[[str], Any] = None
55
-
56
- #: Whether to list this parameter in GetCapabilities
57
- in_capabilities: bool = False
58
-
59
- #: List of allowed values (also shown in GetCapabilities)
60
- allowed_values: list | tuple | set | NoneType = None
61
-
62
- #: Default value if it's not given
63
- default: Any = None
64
-
65
- #: Overridable dict for error messages
66
- error_messages: dict = field(default_factory=dict)
67
-
68
- def value_from_query(self, KVP: dict): # noqa: C901
69
- """Parse a request variable using the type definition.
70
-
71
- This uses the dataclass settings to parse the incoming request value.
72
- """
73
- # URL-based key-value-pair parameters use uppercase.
74
- kvp_name = self.name.upper()
75
- value = KVP.get(kvp_name)
76
- if not value and self.alias:
77
- value = KVP.get(self.alias.upper())
78
-
79
- # Check required field settings, both empty and missing value are treated the same.
80
- if not value:
81
- if not self.required:
82
- return self.default
83
- elif value is None:
84
- raise MissingParameterValue(f"Missing {kvp_name} parameter", locator=self.name)
85
- else:
86
- raise InvalidParameterValue(f"Empty {kvp_name} parameter", locator=self.name)
87
-
88
- # Allow conversion into a python object
89
- if self.parser is not None:
90
- try:
91
- value = self.parser(value)
92
- except ExternalParsingError as e:
93
- raise OperationParsingFailed(
94
- f"Unable to parse {kvp_name} argument: {e}",
95
- locator=self.name,
96
- ) from None
97
- except (TypeError, ValueError, NotImplementedError) as e:
98
- # TypeError/ValueError are raised by most handlers for unexpected data
99
- # The NotImplementedError can be raised by fes parsing.
100
- raise InvalidParameterValue(
101
- f"Invalid {kvp_name} argument: {e}",
102
- locator=self.name,
103
- ) from None
104
-
105
- # Validate against value choices
106
- self.validate_value(value)
107
-
108
- return value
109
-
110
- def validate_value(self, value):
111
- """Validate the parsed value.
112
- This method can be overwritten by a subclass if needed.
113
- """
114
- if self.allowed_values is not None and value not in self.allowed_values:
115
- msg = self.error_messages.get("invalid", "Invalid value for {name}: {value}")
116
- raise InvalidParameterValue(msg.format(name=self.name, value=value), locator=self.name)
117
-
118
-
119
- class UnsupportedParameter(Parameter):
120
- def value_from_query(self, KVP: dict):
121
- kvp_name = self.name.upper()
122
- if kvp_name in KVP:
123
- raise InvalidParameterValue(
124
- f"Support for {self.name} is not implemented!", locator=self.name
125
- )
126
- return None
55
+ #: The values to report in ``<ows:AllowedValues><ows:Value>``.
56
+ allowed_values: list[str]
127
57
 
128
58
 
129
- class OutputFormat:
59
+ class OutputFormat(typing.Generic[R]):
130
60
  """Declare an output format for the method.
131
61
 
132
- These formats should be used in the ``output_formats`` section of the WFSMethod.
62
+ These formats are used in the :meth:`get_output_formats` for
63
+ any :class:`WFSOperation` that implements :class:`OutputFormatMixin`.
64
+ This also connects the output format with a :attr:`renderer_class`.
133
65
  """
134
66
 
67
+ #: The class that performs the output rendering.
68
+ renderer_class: type[R] | None = None
69
+
135
70
  def __init__(
136
71
  self,
137
72
  content_type,
138
- renderer_class=None,
73
+ *,
74
+ subtype=None,
75
+ renderer_class: type[R] | None = None, # None is special case for GetCapabilities
139
76
  max_page_size=None,
140
77
  title=None,
141
78
  in_capabilities=True,
@@ -143,17 +80,16 @@ class OutputFormat:
143
80
  ):
144
81
  """
145
82
  :param content_type: The MIME-type used in the request to select this type.
83
+ :param subtype: Shorter alias for the MIME-type.
146
84
  :param renderer_class: The class that performs the output rendering.
147
- If it's not given, the operation renders it's output using an XML template.
148
85
  :param max_page_size: Used to override the ``max_page_size`` of the renderer_class.
149
86
  :param title: A human-friendly name for an HTML overview page.
150
87
  :param in_capabilities: Whether this format needs to be advertised in GetCapabilities.
151
88
  :param extra: Any additional key-value pairs for the definition.
152
- Could include ``subtype`` as a shorter alias for the MIME-type.
153
89
  """
154
90
  self.content_type = content_type
91
+ self.subtype = subtype
155
92
  self.extra = extra
156
- self.subtype = self.extra.get("subtype")
157
93
  self.renderer_class = renderer_class
158
94
  self.title = title
159
95
  self.in_capabilities = in_capabilities
@@ -189,177 +125,100 @@ class OutputFormat:
189
125
 
190
126
  def __str__(self):
191
127
  extra = "".join(f"; {name}={value}" for name, value in self.extra.items())
128
+ if self.subtype:
129
+ extra = f"; subtype={self.subtype}{extra}"
192
130
  return f"{self.content_type}{extra}"
193
131
 
194
132
  def __repr__(self):
195
133
  return f"<OutputFormat: {self}>"
196
134
 
197
135
 
198
- class WFSMethod:
136
+ class WFSOperation:
199
137
  """Basic interface to implement an WFS method.
200
138
 
201
139
  Each operation in this GIS-server extends from this base class. This class
202
140
  also exposes all requires metadata for the GetCapabilities request.
203
141
  """
204
142
 
205
- do_not_call_in_templates = True # avoid __call__ execution in Django templates
206
-
207
- #: List the supported parameters for this method, extended by get_parameters()
208
- parameters: list[Parameter] = []
209
-
210
- #: List the supported output formats for this method.
211
- output_formats: list[OutputFormat] = []
212
-
213
- #: Default template to use for rendering
214
- xml_template_name = None
143
+ #: Optionally, mention explicitly what the parser class should be used.
144
+ #: Otherwise, it's automatically resolved from the registered types.
145
+ parser_class: type[ows.BaseOwsRequest] = None
215
146
 
216
- #: Default content-type for render_xml()
217
- xml_content_type = "text/xml; charset=utf-8"
147
+ def __init__(self, view: WFSView, ows_request: ows.BaseOwsRequest):
148
+ self.view = view
149
+ self.ows_request = ows_request
218
150
 
219
- def __init__(self, view):
220
- self.view = view # an gisserver.views.GISView
221
- self.namespaces = {
222
- # Default namespaces for the incoming request:
223
- "http://www.w3.org/XML/1998/namespace": "xml",
224
- "http://www.opengis.net/wfs/2.0": "wfs",
225
- "http://www.opengis.net/gml/3.2": "gml",
226
- self.view.xml_namespace: "app",
227
- }
228
-
229
- def get_parameters(self):
230
- """Dynamically return the supported parameters for this method."""
231
- parameters = [
232
- # Always add SERVICE and VERSION as required parameters
233
- # Part of BaseRequest:
234
- Parameter(
235
- "service",
236
- required=not bool(self.view.default_service),
237
- in_capabilities=True,
238
- allowed_values=list(self.view.accept_operations.keys()),
239
- default=self.view.default_service, # can be None or e.g. "WFS"
240
- error_messages={"invalid": "Unsupported service type: {value}."},
241
- ),
242
- Parameter(
243
- "version",
244
- required=True, # Mandatory except for GetCapabilities
245
- allowed_values=self.view.accept_versions,
246
- error_messages={
247
- "invalid": "WFS Server does not support VERSION {value}.",
248
- },
249
- ),
250
- ] + self.parameters
251
-
252
- if self.output_formats:
253
- parameters += [
254
- # Part of StandardPresentationParameters:
255
- Parameter(
256
- "outputFormat",
257
- in_capabilities=True,
258
- allowed_values=self.output_formats,
259
- parser=self._parse_output_format,
260
- default=self.output_formats[0],
261
- )
262
- ]
151
+ def get_parameters(self) -> list[Parameter]:
152
+ """Parameters to advertise in the capabilities for this method."""
153
+ return [
154
+ # Always advertise SERVICE and VERSION as required parameters:
155
+ Parameter("service", allowed_values=list(self.view.accept_operations.keys())),
156
+ Parameter("version", allowed_values=self.view.accept_versions),
157
+ ]
263
158
 
264
- return parameters
159
+ def validate_request(self, ows_request: ows.BaseOwsRequest):
160
+ """Validate the request."""
265
161
 
266
- def _parse_output_format(self, value) -> OutputFormat:
267
- """Select the proper OutputFormat object based on the input value"""
268
- # When using ?OUTPUTFORMAT=application/gml+xml", it is actually a URL-encoded space
269
- # character. Hence, spaces are replaced back to a '+' character to allow such notation
270
- # instead of forcing it to be ?OUTPUTFORMAT=application/gml%2bxml".
271
- for v in {value, value.replace(" ", "+")}:
272
- for o in self.output_formats:
273
- if o.matches(v):
274
- return o
275
- raise InvalidParameterValue(
276
- f"'{value}' is not a permitted output format for this operation.",
277
- locator="outputformat",
278
- ) from None
279
-
280
- def _parse_namespaces(self, value) -> dict[str, str]:
281
- """Parse the 'namespaces' definition.
162
+ def process_request(self, ows_request: ows.BaseOwsRequest):
163
+ """Default call implementation: render an XML template."""
164
+ raise NotImplementedError()
282
165
 
283
- The NAMESPACES parameter defines which namespaces are used in the KVP request.
284
- When this parameter is not given, the default namespaces are assumed.
166
+ @cached_property
167
+ def all_feature_types_by_name(self) -> dict[str, FeatureType]:
168
+ """Create a lookup for feature types by name.
169
+ This can be cached as the :class:`WFSOperation` is instantiated with each request.
285
170
  """
286
- if not value:
287
- return {}
288
-
289
- # example single value: xmlns(http://example.org)
290
- # or: namespaces=xmlns(xml,http://www.w3.org/...),xmlns(wfs,http://www.opengis.net/...)
291
- tokens = value.split(",")
292
-
293
- namespaces = {}
294
- tokens = iter(tokens)
295
- for prefix in tokens:
296
- if not prefix.startswith("xmlns("):
297
- raise InvalidParameterValue(
298
- f"Expected xmlns(...) format: {value}", locator="namespaces"
171
+ return {ft.xml_name: ft for ft in self.view.get_bound_feature_types()}
172
+
173
+ def resolve_feature_type(self, type_name: str, locator: str = "typeNames") -> FeatureType:
174
+ """Find the FeatureType defined in the application that corresponds with the XML type name."""
175
+ alt_type_name = fix_type_name(type_name, self.view.xml_namespace)
176
+ try:
177
+ return self.all_feature_types_by_name[alt_type_name]
178
+ except KeyError:
179
+ if alt_type_name:
180
+ logger.debug(
181
+ "Unable to locate '%s' (nor %s) in the server, options are: %r",
182
+ type_name,
183
+ alt_type_name,
184
+ list(self.all_feature_types_by_name),
299
185
  )
300
- if prefix.endswith(")"):
301
- # xmlns(http://...)
302
- prefix = ""
303
- uri = prefix[6:-1]
304
186
  else:
305
- uri = next(tokens, "")
306
- if not uri.endswith(")"):
307
- raise InvalidParameterValue(
308
- f"Expected xmlns(prefix,uri) format: {value}",
309
- locator="namespaces",
310
- )
311
- prefix = prefix[6:]
312
- uri = uri[:-1]
313
-
314
- namespaces[uri] = prefix
315
-
316
- return namespaces
317
-
318
- def parse_request(self, KVP: dict) -> dict[str, Any]:
319
- """Parse the parameters of the request"""
320
- self.namespaces.update(self._parse_namespaces(KVP.get("NAMESPACES")))
321
- param_values = {param.name: param.value_from_query(KVP) for param in self.get_parameters()}
322
- param_values["NAMESPACES"] = self.namespaces
323
-
324
- for param in self.get_parameters():
325
- param_values[param.name] = param.value_from_query(KVP)
326
-
327
- # Update version if requested.
328
- # This is stored on the view, so exceptions also use it.
329
- if param_values.get("version"):
330
- self.view.set_version(param_values["version"])
187
+ logger.debug(
188
+ "Unable to locate '%s' in the server, options are: %r",
189
+ type_name,
190
+ list(self.all_feature_types_by_name),
191
+ )
192
+ raise InvalidParameterValue(
193
+ f"Typename '{type_name}' doesn't exist in this server.",
194
+ locator=locator or "typeNames",
195
+ ) from None
331
196
 
332
- self.validate(**param_values)
333
- return param_values
334
197
 
335
- def validate(self, **params):
336
- """Perform final request parameter validation before the method is called"""
198
+ class XmlTemplateMixin:
199
+ """Mixin to support methods that render using a template."""
337
200
 
338
- def __call__(self, **params):
339
- """Default call implementation: render an XML template."""
340
- context = self.get_context_data(**params)
341
- if "outputFormat" in params:
342
- output_format: OutputFormat = params["outputFormat"]
201
+ #: Default template to use for rendering
202
+ #: This is resolved as :samp:`gisserver/{service}/{version}/{xml_template_name}`.
203
+ xml_template_name = None
343
204
 
344
- if output_format.renderer_class is not None:
345
- # Streaming HTTP responses, e.g. GML32/GeoJSON output:
346
- renderer = output_format.renderer_class(self, **context)
347
- return renderer.get_response()
205
+ #: The content-type to render.
206
+ xml_content_type = "text/xml; charset=utf-8"
348
207
 
349
- return self.render_xml(context, **params)
208
+ def process_request(self, ows_request: ows.BaseOwsRequest):
209
+ """Process the request by rendering a Django template."""
210
+ context = self.get_context_data()
211
+ return self.render_xml(context, ows_request)
350
212
 
351
- def get_context_data(self, **params):
213
+ def get_context_data(self):
352
214
  """Collect all arguments to use for rendering the XML template"""
353
215
  return {}
354
216
 
355
- def render_xml(self, context, **params):
356
- """Shortcut to render XML.
357
-
358
- This is the default method when the OutputFormat class doesn't have a renderer_class
359
- """
217
+ def render_xml(self, context, ows_request: ows.BaseOwsRequest):
218
+ """Render the response using a template."""
360
219
  return HttpResponse(
361
220
  render_to_string(
362
- self._get_xml_template_name(),
221
+ self._get_xml_template_name(ows_request),
363
222
  context={
364
223
  "view": self.view,
365
224
  "app_xml_namespace": self.view.xml_namespace,
@@ -370,9 +229,9 @@ class WFSMethod:
370
229
  content_type=self.xml_content_type,
371
230
  )
372
231
 
373
- def _get_xml_template_name(self):
232
+ def _get_xml_template_name(self, ows_request: ows.BaseOwsRequest) -> str:
374
233
  """Generate the XML template name for this operation, and check its file pattern"""
375
- service = self.view.KVP["SERVICE"].lower()
234
+ service = ows_request.service.lower()
376
235
  template_name = f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
377
236
 
378
237
  # Since 'service' and 'version' are based on external input,
@@ -383,66 +242,25 @@ class WFSMethod:
383
242
  return template_name
384
243
 
385
244
 
386
- class WFSTypeNamesMethod(WFSMethod):
387
- """A base method that also resolved the TYPENAMES parameter."""
388
-
389
- def __init__(self, *args, **kwargs):
390
- super().__init__(*args, **kwargs)
391
-
392
- # Retrieve the feature types directly, as these are needed during parsing.
393
- # This is not delayed until _parse_type_names() as that wraps any TypeError
394
- # from view.get_feature_types() as an InvalidParameterValue exception.
395
- self.all_feature_types = self.view.get_feature_types()
396
- self.all_feature_types_by_name = _get_feature_types_by_name(self.all_feature_types)
245
+ class OutputFormatMixin:
246
+ """Mixin to support methods that handle different output formats."""
397
247
 
398
- def get_parameters(self):
399
- return super().get_parameters() + [
400
- # QGis sends both TYPENAME (wfs 1.x) and TYPENAMES (wfs 2.0) for DescribeFeatureType
401
- # typeNames is not required when a ResourceID / GetFeatureById is specified.
402
- Parameter(
403
- "typeNames",
404
- alias="typeName", # WFS 1.0 name, but still needed for CITE tests
405
- required=False, # sometimes required, depends on other parameters
406
- parser=self._parse_type_names,
407
- ),
408
- ]
409
-
410
- def _parse_type_names(self, type_names) -> list[FeatureType]:
411
- """Find the requested feature types by name"""
412
- if "(" in type_names:
413
- # This allows to perform multiple queries in a single request:
414
- # TYPENAMES=(A)(B)&FILTER=(filter for A)(filter for B)
415
- raise OperationParsingFailed(
416
- "Parameter lists to perform multiple queries are not supported yet.",
417
- locator="typenames",
418
- )
419
-
420
- return [self._parse_type_name(name, locator="typenames") for name in type_names.split(",")]
421
-
422
- def _parse_type_name(self, name, locator="typename") -> FeatureType:
423
- """Find the requested feature type for a type name"""
424
- app_prefix = self.namespaces[self.view.xml_namespace]
425
- # strip our XML prefix
426
- local_name = name[len(app_prefix) + 1 :] if name.startswith(f"{app_prefix}:") else name
427
-
428
- try:
429
- return self.all_feature_types_by_name[local_name]
430
- except KeyError:
431
- raise InvalidParameterValue(
432
- f"Typename '{name}' doesn't exist in this server. "
433
- f"Please check the capabilities and reformulate your request.",
434
- locator=locator,
435
- ) from None
436
-
437
-
438
- def _get_feature_types_by_name(feature_types) -> dict[str, FeatureType]:
439
- """Create a lookup for feature types by name."""
440
- features_by_name = {ft.name: ft for ft in feature_types}
441
-
442
- # Check against bad configuration
443
- if len(features_by_name) != len(feature_types):
444
- all_names = [ft.name for ft in feature_types]
445
- duplicates = ", ".join(sorted({n for n in all_names if all_names.count(n) > 1}))
446
- raise ImproperlyConfigured(f"FeatureType names should be unique: {duplicates}")
248
+ def get_output_formats(self) -> list[OutputFormat]:
249
+ """List all output formats. This is exposed in GetCapabilities,
250
+ and used for internal rendering.
251
+ """
252
+ raise NotImplementedError()
447
253
 
448
- return features_by_name
254
+ def resolve_output_format(self, value, locator="outputFormat") -> OutputFormat:
255
+ """Select the proper OutputFormat object based on the input value"""
256
+ # When using ?OUTPUTFORMAT=application/gml+xml", it is actually a URL-encoded space
257
+ # character. Hence, spaces are replaced back to a '+' character to allow such notation
258
+ # instead of forcing it to be ?OUTPUTFORMAT=application/gml%2bxml".
259
+ for v in {value, value.replace(" ", "+")}:
260
+ for o in self.get_output_formats():
261
+ if o.matches(v):
262
+ return o
263
+ raise InvalidParameterValue(
264
+ f"'{value}' is not a permitted output format for this operation.",
265
+ locator=locator or "outputFormat",
266
+ ) from None