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
gisserver/views.py CHANGED
@@ -2,29 +2,43 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import re
6
- from urllib.parse import urlencode
7
+ from urllib.parse import unquote_plus, urlencode
7
8
 
8
9
  from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
9
10
  from django.core.exceptions import PermissionDenied as Django_PermissionDenied
10
11
  from django.shortcuts import render
11
12
  from django.views import View
13
+ from django.views.decorators.csrf import csrf_exempt
12
14
 
13
15
  from gisserver import conf
14
16
  from gisserver.exceptions import (
17
+ ExternalParsingError,
15
18
  InvalidParameterValue,
16
- MissingParameterValue,
17
19
  OperationNotSupported,
20
+ OperationParsingFailed,
21
+ OperationProcessingFailed,
18
22
  OWSException,
19
23
  PermissionDenied,
24
+ XmlElementNotSupported,
20
25
  )
21
26
  from gisserver.features import FeatureType, ServiceDescription
22
27
  from gisserver.operations import base, wfs20
28
+ from gisserver.parsers.ows import (
29
+ BaseOwsRequest,
30
+ KVPRequest,
31
+ resolve_kvp_parser_class,
32
+ resolve_xml_parser_class,
33
+ )
34
+ from gisserver.parsers.xml import parse_xml_from_string, split_ns
35
+
36
+ logger = logging.getLogger(__name__)
23
37
 
24
38
  SAFE_VERSION = re.compile(r"\A[0-9.]+\Z")
25
39
 
26
40
 
27
- class GISView(View):
41
+ class OWSView(View):
28
42
  """The base logic to implement OGC view like WFS.
29
43
 
30
44
  Each subclass defines 'accept_operations' with the desired RPC operations.
@@ -33,6 +47,9 @@ class GISView(View):
33
47
  #: Define the namespace to use in the XML
34
48
  xml_namespace = "http://example.org/gisserver"
35
49
 
50
+ #: Define namespace aliases to use, default is ``{"app": self.xml_namespace}``.
51
+ xml_namespace_aliases = None
52
+
36
53
  #: Default version to use
37
54
  version = "2.0.0"
38
55
 
@@ -54,6 +71,10 @@ class GISView(View):
54
71
  #: Whether to render GET HTML pages
55
72
  use_html_templates = True
56
73
 
74
+ #: The OWS request, which contains the complete parsed request.
75
+ ows_request: BaseOwsRequest = None
76
+
77
+ @csrf_exempt
57
78
  def dispatch(self, request, *args, **kwargs):
58
79
  """Render proper XML errors for exceptions on all request types."""
59
80
  try:
@@ -78,30 +99,114 @@ class GISView(View):
78
99
  else:
79
100
  return None
80
101
 
102
+ @classmethod
103
+ def get_xml_namespace_aliases(cls) -> dict[str, str]:
104
+ """Provide all namespaces aliases with a namespace.
105
+ This is most useful for parsing input.
106
+ The default is: ``{"app": cls.xml_namespace}``.
107
+ """
108
+ return cls.xml_namespace_aliases or {"app": cls.xml_namespace}
109
+
110
+ @classmethod
111
+ def get_xml_namespaces_to_prefixes(cls) -> dict[str, str]:
112
+ """Provide a mapping from namespace to prefix.
113
+ This is most useful for rendering output.
114
+ The default is: ``{cls.xml_namespace: "app"}``.
115
+ """
116
+ return {
117
+ xml_namespace: prefix
118
+ for prefix, xml_namespace in cls.get_xml_namespace_aliases().items()
119
+ }
120
+
81
121
  def get(self, request, *args, **kwargs):
82
122
  """Entry point to handle HTTP GET requests.
83
123
 
84
124
  This parses the 'SERVICE' and 'REQUEST' parameters,
85
- to call the proper operation.
125
+ to call the proper :class:`~gisserver.operations.base.WFSOperation`.
86
126
 
87
127
  All query parameters are handled as case-insensitive.
88
128
  """
89
- # Convert to WFS key-value-pair format.
90
- self.KVP = {key.upper(): value for key, value in request.GET.items()}
129
+ # Parse GET parameters in Key-Value-Pair syntax format.
130
+ if logger.isEnabledFor(logging.DEBUG):
131
+ logger.debug(
132
+ "Parsing GET parameters:\n%s",
133
+ unquote_plus(
134
+ request.META["QUERY_STRING"].replace("\n&", "\n").replace("&", "\n")
135
+ ).rstrip(),
136
+ )
137
+ self.kvp = kvp = KVPRequest(request.GET, ns_aliases=self.get_xml_namespace_aliases())
138
+
139
+ # Get service (only raises error when value is missing and "default" parameter is not given)
140
+ defaults = {"default": self.default_service} if self.default_service else {}
141
+ service = kvp.get_str("service", **defaults).upper()
91
142
 
92
143
  # Perform early version parsing. The detailed validation happens by the operation
93
144
  # Parameter objects, but by performing an early check, templates can use that version.
94
- version = self.KVP.get("VERSION")
95
- if version and version in self.accept_versions:
96
- self.set_version(version)
145
+ version = kvp.get_str("version", default=None)
146
+ self.set_version(service, version)
97
147
 
98
148
  # Allow for a user-friendly opening page (hence the version check above)
99
149
  if self.use_html_templates and self.is_index_request():
100
- return self.render_index()
150
+ return self.render_index(service)
151
+
152
+ # Find the registered operation that handles the request
153
+ operation = kvp.get_str("request")
154
+ wfs_operation_cls = self.get_operation_class(service, operation)
155
+
156
+ # Parse the request syntax
157
+ request_cls = wfs_operation_cls.parser_class or resolve_kvp_parser_class(kvp)
158
+ self.ows_request = request_cls.from_kvp_request(kvp)
159
+
160
+ # Process the request!
161
+ return self.call_operation(wfs_operation_cls)
101
162
 
102
- # Normal WFS
103
- wfs_method_cls = self.get_operation_class()
104
- return self.call_operation(wfs_method_cls)
163
+ def post(self, request, *args, **kwargs):
164
+ """Entry point to handle HTTP POST requests.
165
+
166
+ This parses the XML to get the correct service and operation,
167
+ to call the proper :class:`~gisserver.operations.base.WFSOperation`.
168
+ """
169
+ # Parse the XML body
170
+ if logger.isEnabledFor(logging.DEBUG):
171
+ logger.debug("Parsing POST:\n%s", request.body.decode().rstrip())
172
+ try:
173
+ root = parse_xml_from_string(
174
+ request.body, extra_ns_aliases=self.get_xml_namespace_aliases()
175
+ )
176
+ except ExternalParsingError as e:
177
+ raise OperationParsingFailed(f"Unable to parse XML: {e}") from e
178
+
179
+ # Find the registered operation that handles the request
180
+ service = (
181
+ root.attrib.get("service", self.default_service)
182
+ if self.default_service
183
+ else root.get_str_attribute("service")
184
+ )
185
+
186
+ # Perform early version check. version can be omitted for GetCapabilities
187
+ self.set_version(service, root.attrib.get("version"))
188
+
189
+ # Find the registered operation that handles the request
190
+ namespace, operation = split_ns(root.tag)
191
+ wfs_operation_cls = self.get_operation_class(service, operation)
192
+
193
+ try:
194
+ # Parse the request syntax
195
+ request_cls = wfs_operation_cls.parser_class or resolve_xml_parser_class(root)
196
+ self.ows_request = request_cls.from_xml(root)
197
+
198
+ # Process the request!
199
+ return self.call_operation(wfs_operation_cls)
200
+ except XmlElementNotSupported as e:
201
+ # Unknown XML element, e.g. wrong namespace.
202
+ raise OperationNotSupported(str(e), locator=root.attrib.get("handle")) from e
203
+ except (OperationParsingFailed, OperationProcessingFailed) as e:
204
+ # The WFS spec dictates that these exceptions
205
+ # (and ResponseCacheExpired, CannotLockAllFeatures, FeaturesNotLocked which we don't raise)
206
+ # should report the 'handle' in the 'locator' argument of the exception when it was provided.
207
+ if self.ows_request.handle:
208
+ e.locator = self.ows_request.handle
209
+ raise
105
210
 
106
211
  def is_index_request(self):
107
212
  """Tell whether to index page should be shown."""
@@ -109,22 +214,29 @@ class GISView(View):
109
214
  # (misspelled?) parameters on the query string, this is considered to be an index page.
110
215
  # Minimal request has 2 parameters (SERVICE=WFS&REQUEST=GetCapabilities).
111
216
  # Some servers also allow a default of SERVICE, thus needing even less.
112
- return len(self.KVP) < 2 and {
113
- "REQUEST",
114
- "SERVICE",
115
- "VERSION",
116
- "ACCEPTVERSIONS",
117
- }.isdisjoint(self.KVP.keys())
118
-
119
- def render_index(self):
217
+ return (
218
+ self.request.method == "GET"
219
+ and len(self.request.GET) < 2
220
+ and {
221
+ "REQUEST",
222
+ "SERVICE",
223
+ "VERSION",
224
+ "ACCEPTVERSIONS",
225
+ }.isdisjoint(key.upper() for key in self.request.GET)
226
+ )
227
+
228
+ def render_index(self, service: str | None = None):
120
229
  """Render the index page."""
121
230
  # Not using the whole TemplateResponseMixin config with configurable parameters.
122
231
  # If this really needs more configuration, overriding is likely just as easy.
123
- return render(self.request, self.get_index_template_names(), self.get_index_context_data())
232
+ return render(
233
+ self.request,
234
+ self.get_index_template_names(service),
235
+ self.get_index_context_data(service=service),
236
+ )
124
237
 
125
- def get_index_context_data(self, **kwargs):
238
+ def get_index_context_data(self, service: str | None = None, **kwargs):
126
239
  """Provide the context data for the index page."""
127
- service = self.KVP.get("SERVICE", self.default_service)
128
240
  root_url = self.request.build_absolute_uri()
129
241
 
130
242
  # Allow passing extra vendor parameters to the links generated in the template
@@ -138,6 +250,7 @@ class GISView(View):
138
250
 
139
251
  return {
140
252
  "view": self,
253
+ "xml_namespaces": self.get_xml_namespaces_to_prefixes(),
141
254
  "service": service,
142
255
  "root_url": root_url,
143
256
  "base_query": base_query,
@@ -149,16 +262,11 @@ class GISView(View):
149
262
  **kwargs,
150
263
  }
151
264
 
152
- def get_index_template_names(self):
265
+ def get_index_template_names(self, service: str | None = None):
153
266
  """Get the index page template name.
154
267
  If no template is configured, some reasonable defaults are selected.
155
268
  """
156
- raw_service = self.KVP.get("SERVICE", self.default_service)
157
- if raw_service and raw_service in self.accept_operations:
158
- service = raw_service.lower()
159
- else:
160
- service = "default"
161
-
269
+ service = service.lower() if service and service in self.accept_operations else "default"
162
270
  if self.index_template_name:
163
271
  # Allow the same substitutions for a manually configured template.
164
272
  return [self.index_template_name.format(service=service, version=self.version)]
@@ -169,60 +277,65 @@ class GISView(View):
169
277
  "gisserver/index.html",
170
278
  ]
171
279
 
172
- def get_operation_class(self) -> type[base.WFSMethod]:
280
+ def get_operation_class(self, service: str, request: str) -> type[base.WFSOperation]:
173
281
  """Resolve the method that the client wants to call."""
174
282
  if not self.accept_operations:
175
283
  raise ImproperlyConfigured("View has no operations")
176
284
 
177
- # The service is always WFS
178
- service = self._get_required_arg("SERVICE", self.default_service).upper()
179
285
  try:
180
- operations = self.accept_operations[service]
286
+ operations = self.accept_operations[service.upper()]
181
287
  except KeyError:
182
288
  allowed = ", ".join(sorted(self.accept_operations.keys()))
183
289
  raise InvalidParameterValue(
184
- f"'{service}' is an invalid service, supported are: {allowed}.",
290
+ f"'{service}' is not supported, available are: {allowed}.",
185
291
  locator="service",
186
292
  ) from None
187
293
 
188
294
  # Resolve the operation
189
295
  # In mapserver, the operation name is case-insensitive.
190
- operation = self._get_required_arg("REQUEST").upper()
191
296
  uc_methods = {name.upper(): method for name, method in operations.items()}
192
-
193
297
  try:
194
- return uc_methods[operation]
298
+ return uc_methods[request.upper()]
195
299
  except KeyError:
196
300
  allowed = ", ".join(operations.keys())
197
301
  raise OperationNotSupported(
198
- f"'{operation.lower()}' is not implemented, supported are: {allowed}.",
302
+ f"'{request}' is not implemented, supported are: {allowed}.",
199
303
  locator="request",
200
304
  ) from None
201
305
 
202
- def call_operation(self, wfs_method_cls: type[base.WFSMethod]):
306
+ def call_operation(self, wfs_operation_cls: type[base.WFSOperation]):
203
307
  """Call the resolved method."""
204
- wfs_method = wfs_method_cls(self)
205
- param_values = wfs_method.parse_request(self.KVP)
206
- return wfs_method(**param_values) # goes into __call__()
308
+ self.request.ows_request = self.ows_request # for check_permissions()
207
309
 
208
- def _get_required_arg(self, argname, default=None):
209
- try:
210
- return self.KVP[argname]
211
- except KeyError:
212
- if default is not None:
213
- return default
214
- raise MissingParameterValue(locator=argname.lower()) from None
310
+ wfs_operation = wfs_operation_cls(self, self.ows_request)
311
+ wfs_operation.validate_request(self.ows_request)
312
+ return wfs_operation.process_request(self.ows_request)
215
313
 
216
- def get_service_description(self, service: str) -> ServiceDescription:
314
+ def get_service_description(self, service: str | None = None) -> ServiceDescription:
217
315
  """Provide the (dynamically generated) service description."""
218
316
  return self.service_description or ServiceDescription(title="Unnamed")
219
317
 
220
- def set_version(self, version):
318
+ def set_version(self, service: str, version: str | None):
221
319
  """Enforce a particular version based on the request."""
320
+ if service.upper() not in self.accept_operations:
321
+ allowed = ", ".join(sorted(self.accept_operations.keys()))
322
+ raise InvalidParameterValue(
323
+ f"'{service}' is not supported, available are: {allowed}.",
324
+ locator="service",
325
+ ) from None
326
+
327
+ if not version:
328
+ return
329
+
222
330
  if not SAFE_VERSION.match(version):
223
331
  # Make really sure we didn't mess things up, as this causes file includes.
224
332
  raise SuspiciousOperation("Invalid/insecure version number parsed")
225
333
 
334
+ if version not in self.accept_versions:
335
+ raise InvalidParameterValue(
336
+ f"This server does not support {service} version {version}.", locator="version"
337
+ )
338
+
226
339
  # Enforce the requested version
227
340
  self.version = version
228
341
 
@@ -232,7 +345,7 @@ class GISView(View):
232
345
  return self.request.build_absolute_uri(self.request.path)
233
346
 
234
347
 
235
- class WFSView(GISView):
348
+ class WFSView(OWSView):
236
349
  """A view for a single WFS server.
237
350
 
238
351
  This view exposes multiple dataset,
@@ -246,6 +359,7 @@ class WFSView(GISView):
246
359
  max_page_size = conf.GISSERVER_DEFAULT_MAX_PAGE_SIZE
247
360
 
248
361
  #: Define the features (=tables) in this dataset.
362
+ #: For dynamic per-request logic, consider overwriting :meth:`get_feature_types` instead.
249
363
  feature_types: list[FeatureType] = []
250
364
 
251
365
  #: Internal configuration of all available RPC calls.
@@ -270,7 +384,7 @@ class WFSView(GISView):
270
384
  "ImplementsTransactionalWFS": False, # only reads
271
385
  "ImplementsLockingWFS": False, # only basic WFS
272
386
  "KVPEncoding": True, # HTTP GET support
273
- "XMLEncoding": False, # HTTP POST requests
387
+ "XMLEncoding": True, # HTTP POST requests
274
388
  "SOAPEncoding": False, # no SOAP requests
275
389
  "ImplementsInheritance": False,
276
390
  "ImplementsRemoteResolve": False,
@@ -305,20 +419,71 @@ class WFSView(GISView):
305
419
  }
306
420
 
307
421
  def get_feature_types(self) -> list[FeatureType]:
308
- """Return all available feature types this server exposes"""
309
- return self.feature_types
422
+ """Return all available feature types this server exposes.
310
423
 
311
- def get_index_context_data(self, **kwargs):
312
- """Add WFS specific metadata"""
313
- wfs_output_formats = self.accept_operations["WFS"]["GetFeature"].output_formats
424
+ This method may be overwritten to provide feature types dynamically,
425
+ for example to give them different elements based on user permissions.
426
+ """
427
+ return self.feature_types
314
428
 
429
+ def get_bound_feature_types(self) -> list[FeatureType]:
430
+ """Internal logic wrapping the FeatureType definitions provided by the developer.
431
+ This binds the XML namespace information from this view to the declared types.
432
+ """
433
+ feature_types = self.get_feature_types()
434
+ for feature_type in feature_types:
435
+ # Make sure the feature type can advertise itself with an XML namespace.
436
+ feature_type.bind_namespace(default_xml_namespace=self.xml_namespace)
437
+ return feature_types
438
+
439
+ def get_index_context_data(self, **kwargs) -> dict:
440
+ """Get the context data for the index template"""
315
441
  context = super().get_index_context_data(**kwargs)
316
442
  context.update(
317
443
  {
318
- "wfs_features": self.get_feature_types(),
319
- "wfs_output_formats": wfs_output_formats,
444
+ "GISSERVER_SUPPORTED_CRS_ONLY": conf.GISSERVER_SUPPORTED_CRS_ONLY,
445
+ "wfs_features": self.get_bound_feature_types(),
446
+ "wfs_output_formats": self._get_wfs_output_formats(),
320
447
  "wfs_filter_capabilities": self.wfs_filter_capabilities,
321
448
  "wfs_service_constraints": self.wfs_service_constraints,
322
449
  }
323
450
  )
324
451
  return context
452
+
453
+ def _get_wfs_output_formats(self) -> list[base.OutputFormat]:
454
+ """Find the output formats of the ``GetFeature`` operation for the HTML index."""
455
+ get_feature_operation = self.accept_operations["WFS"]["GetFeature"]
456
+ operation = get_feature_operation(self, ows_request=None)
457
+
458
+ # Get output formats, remove duplicates (e.g. GeoJSON/geojson alias)
459
+ wfs_output_formats = []
460
+ seen = set()
461
+ for output_format in operation.get_output_formats():
462
+ if output_format.identifier not in seen:
463
+ wfs_output_formats.append(output_format)
464
+ seen.add(output_format.identifier)
465
+ return wfs_output_formats
466
+
467
+ def get_xml_schema_url(self, feature_types: list[FeatureType]) -> str:
468
+ """Return the XML schema URL for the given feature types.
469
+ This is used in the GML output rendering.
470
+ """
471
+ app_namespaces = self.get_xml_namespaces_to_prefixes()
472
+ type_names = ",".join(
473
+ f"{app_namespaces[ft.xml_namespace]}:{ft.name}" for ft in feature_types
474
+ )
475
+ return (
476
+ f"{self.server_url}?SERVICE=WFS&VERSION=2.0.0"
477
+ f"&REQUEST=DescribeFeatureType&TYPENAMES={type_names}"
478
+ )
479
+
480
+ def check_permissions(self, feature_type: FeatureType):
481
+ """Hook that allows subclasses to reject access for datasets.
482
+ It may raise a Django PermissionDenied error.
483
+
484
+ This can access: ``self.request`` (the Django HTTPRequest) and ``self.ows_request``.
485
+ The latter contains a parsed request object
486
+ from the :mod:`gisserver.parsers.wfs20` package,
487
+ such as the parsed :class:`~gisserver.parsers.wfs20.GetFeature`
488
+ or :class:`~gisserver.parsers.wfs20.GetPropertyValue` request.
489
+ """
@@ -1,54 +0,0 @@
1
- gisserver/__init__.py,sha256=XtaIlx7Gik8RqIFDc1G0JAUfcmeNyTWjev1E1Oym_ms,40
2
- gisserver/conf.py,sha256=3LSfeDRTdJCLuopAOD14Y5m2iPhovreIaNI9zbaukpo,2343
3
- gisserver/db.py,sha256=-d6VQRkRWMSWKkA_6cVWhLzSI1cKmRoVktqA6hVwvuM,4928
4
- gisserver/exceptions.py,sha256=wRsiKWhZtA8NmfMNncRtMeFgWDMdOWRp9Ey96iFAwMI,5194
5
- gisserver/features.py,sha256=b3hEDs90xwEsANcRgipekZZrp4ucxRWwGeUutk2gRTw,31821
6
- gisserver/geometries.py,sha256=HLZ3d8E5M2sfs2Kzz_GE3pZdynn-rYXPpuU9GVh0vsg,12398
7
- gisserver/types.py,sha256=bsl3PezhGcHpUAtuV4AhdBmFLmtZZpDCT_HqpdVyLtM,36936
8
- gisserver/views.py,sha256=xMHKezn4F1xZ2r78l9IzUlL5M3qZ78fjRJJtSzA8-Ww,12809
9
- gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- gisserver/operations/base.py,sha256=Ls5i6ulMJqyhqu6o969rJ-mezW-T6OW5mr8oZ3BTBH8,17222
11
- gisserver/operations/wfs20.py,sha256=bQeMJ4T6AisSBMABYa3li1u53w0zUxs02qBwjS4QbOw,18742
12
- gisserver/output/__init__.py,sha256=C6hXIVIxBd2o_1Y7VhIU9vCg7pswI2tXsd74wgMdU4o,2134
13
- gisserver/output/base.py,sha256=GygubFyhPO33fQw3jIuOerEH8yjbk0DJ6NhQdFkT7-g,8645
14
- gisserver/output/csv.py,sha256=Mm7zYzUs9ffw9fqcKY9NT7qbMX-MzSFnqBg5I1wdRU0,6459
15
- gisserver/output/geojson.py,sha256=bhRtNdMi1tMcSFBxDTNL8oysAMVhoolctKjZhxIncN8,11297
16
- gisserver/output/gml32.py,sha256=KbhcBBVp4sEhvKs5e6fP4_GKKL6Kr2yyYaDLWePBQxs,31293
17
- gisserver/output/results.py,sha256=VD-fJgWrMTwGaEGG_BSVhDv_m60dldBY6yAcTpRICT4,13949
18
- gisserver/output/utils.py,sha256=FIlkHymA43RLZAfB6-trM3_IQKbIVb_4OUp5fxRbVGY,7379
19
- gisserver/output/xmlschema.py,sha256=TdTMyP2_r98wkeffWAwrfm3l9XAmivu1KcYWNpgQKR4,4578
20
- gisserver/parsers/__init__.py,sha256=g72D8EtR-QkhcemzY5-Qty-nhHPKza83Sz3KZm8bfq0,290
21
- gisserver/parsers/base.py,sha256=9DE73wMphp7At3mGW0xwGU_TWPm55x8JEh4odpdRhlo,4833
22
- gisserver/parsers/tags.py,sha256=NyJhbt-U2zTewfhghH4LRslTaDK2UOq9Vgk83pWfJh0,3553
23
- gisserver/parsers/values.py,sha256=ypNPl4HduiVzexsL59ukZntlwXk5gHuSvQO6yhLeXD4,1016
24
- gisserver/parsers/fes20/__init__.py,sha256=72RuFxAQp9hW0pgFUDyfQ0WsStNNk6J_y3NH-zYaPOA,1140
25
- gisserver/parsers/fes20/expressions.py,sha256=A1-8JsGahXJPWLuTSVNAf0oSuyLfDMnFKM7viIt35Y8,9573
26
- gisserver/parsers/fes20/filters.py,sha256=HdWguZvAT2L4Xm-bNa4XPh7MEymqTB6fo8r47Z-Bjuw,3511
27
- gisserver/parsers/fes20/functions.py,sha256=mutPlbjOlg6R-9nNe8goaNdsAlZPkuckYlhMsQyp6NE,8723
28
- gisserver/parsers/fes20/identifiers.py,sha256=vcPrMYcsemSlB9ziSUdVulhoI5PGFAh8vqeADBCWd60,2864
29
- gisserver/parsers/fes20/operators.py,sha256=oQezU553Mts1DoExlH7_U29XXXdk6V84ihlBOozRzLg,23360
30
- gisserver/parsers/fes20/query.py,sha256=_OxElAgeo34-00pNY5Apc6TSUKokSG9VWSMmIplN7AA,10638
31
- gisserver/parsers/fes20/sorting.py,sha256=Uj8gsbBXumihWkrcj_pRPscwZAScUJSngAyjKri-Z8Y,2184
32
- gisserver/parsers/gml/__init__.py,sha256=lNK0SXWkmTKdeJD9xdK6B-N-Y7wwGL4NRBI3dzoJDNI,1463
33
- gisserver/parsers/gml/base.py,sha256=xWC5FrJyMjq7h5zmKwEvtRF0-CGVbCtvnO8Isvnf4mE,1065
34
- gisserver/parsers/gml/geometries.py,sha256=cgV3WogA7LyC_H_NF7dGoGLvLdQRACG6AlThRkX_jhU,3171
35
- gisserver/queries/__init__.py,sha256=yntEYRfaZ1Yjnr8lMvJt9Erh31WktikIf1af4TTuX74,1000
36
- gisserver/queries/adhoc.py,sha256=Hhcxwt5C2OHqtLc-3QUqx8MZEnhoqkKWjBp56e6aljc,7428
37
- gisserver/queries/base.py,sha256=EVJiwcLBjxGXJ8k012gGEiZ1q_gQZokSwdMl4H1SPus,7857
38
- gisserver/queries/projection.py,sha256=KEz9UcZGVkYq7XTm_5nx8w5TnbSqht7rOGEuTGeJwm8,9922
39
- gisserver/queries/stored.py,sha256=G258R9HVX3OooM5mhQF_N_JrFHLjM2y5iusrzPcN9TU,7125
40
- gisserver/static/gisserver/index.css,sha256=fiQuCMuiWUhhrNQD6bbUJaX5vH5OpG9qgPdELPB8cpI,163
41
- gisserver/templates/gisserver/index.html,sha256=yliku8jDqAOcenkbe1YnvJBnuS13MvzzsoDV8sQZ3GM,866
42
- gisserver/templates/gisserver/service_description.html,sha256=3EHLnQx27_nTDZFD2RH6E10EWnTN90s1DGuEj9PeBrg,993
43
- gisserver/templates/gisserver/wfs/feature_field.html,sha256=H5LsTAxGVGQqEpOZdEUTszQmFAZSpB9ggRJUsTdjYnw,538
44
- gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
45
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml,sha256=4Hxw1kCnxfWXTVPbHf8IW9_qiSeIEtnxHK6z6TS3-zA,1041
46
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=Ej6ZXf-nGiPjiKdhNK5vAsX2iiiS12knjD0VaSzSltM,8717
47
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml,sha256=CIBVQrKXmGMQ6BKbX4_0ru4lXtWnW5EIgHUbIi1V0Vc,636
48
- gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
- gisserver/templatetags/gisserver_tags.py,sha256=RFGqEj-EQi9FrJiFJ7HHAQqlU4fDKVKTtZrPNwURXgA,204
50
- django_gisserver-1.5.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
51
- django_gisserver-1.5.0.dist-info/METADATA,sha256=IWlSvP8d_4wsCVq4NWAE0T7Q8QZGqFVMvWUyLss85aY,5859
52
- django_gisserver-1.5.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
53
- django_gisserver-1.5.0.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
54
- django_gisserver-1.5.0.dist-info/RECORD,,
gisserver/parsers/base.py DELETED
@@ -1,149 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from enum import Enum
4
- from xml.etree.ElementTree import Element, QName
5
-
6
- from gisserver.exceptions import ExternalParsingError
7
-
8
- from .tags import split_ns
9
-
10
-
11
- class TagNameEnum(Enum):
12
- """An enumeration of tag names.
13
-
14
- All enumerations that represent tag names inherit from this.
15
- Each member name should be exactly the XML tag that it refers to.
16
- """
17
-
18
- @classmethod
19
- def from_xml(cls, element: Element):
20
- """Cast the element tag name into the enum member"""
21
- ns, localname = split_ns(element.tag)
22
- return cls[localname]
23
-
24
- @classmethod
25
- def _missing_(cls, value):
26
- raise NotImplementedError(f"<{value}> is not registered as valid {cls.__name__}")
27
-
28
- def __repr__(self):
29
- # Make repr(filter) easier to copy-paste
30
- return f"{self.__class__.__name__}.{self.name}"
31
-
32
-
33
- class BaseNode:
34
- """The base node for all classes that represent an XML tag."""
35
-
36
- xml_ns = None
37
- xml_tags = []
38
-
39
- def __init_subclass__(cls):
40
- cls.xml_tags = []
41
-
42
- @classmethod
43
- def from_xml(cls, element: Element):
44
- raise NotImplementedError(
45
- f"{cls.__name__}.from_xml() is not implemented to parse <{element.tag}>"
46
- )
47
-
48
- @classmethod
49
- def from_child_xml(cls, element: Element) -> BaseNode:
50
- """By default, the node attempts to locate a child-class from the registry.
51
- The individual tags should override `from_xml()` to return their own type.
52
- """
53
- return tag_registry.from_child_xml(element, allowed_types=(cls,))
54
-
55
-
56
- class TagRegistry:
57
- """Registration of all classes that can parse XML nodes.
58
-
59
- The same class can be registered multiple times for different tag names.
60
- """
61
-
62
- parsers: dict[str, type[BaseNode]]
63
-
64
- def __init__(self):
65
- self.parsers = {}
66
-
67
- def register(self, name=None, namespace=None, hidden=False):
68
- """Decorator to register a class as XML node parser.
69
-
70
- This registers the decorated class as the designated parser
71
- for a specific XML tag.
72
-
73
- Usage:
74
-
75
- @dataclass
76
- @tag_registry.register()
77
- class SomeXmlTag(BaseNode):
78
- xml_ns = FES
79
-
80
- @classmethod
81
- def from_xml(cls, element: Element):
82
- return cls(
83
- ...
84
- )
85
-
86
- """
87
-
88
- def _dec(sub_class: type[BaseNode]) -> type[BaseNode]:
89
- if sub_class.xml_ns is None:
90
- raise RuntimeError(f"{sub_class.__name__}.xml_ns should be set")
91
-
92
- localname = name or sub_class.__name__
93
- qname = QName(namespace or sub_class.xml_ns, localname)
94
-
95
- self.parsers[qname] = sub_class # Track this parser to resolve the tag.
96
- if not hidden:
97
- sub_class.xml_tags.append(name) # Allow fetching all names later
98
- return sub_class # allow decorator usage
99
-
100
- return _dec
101
-
102
- def register_names(self, names: type[TagNameEnum], namespace=None):
103
- """Decorator to register the 'tag_class' as the
104
- parser-backend for all member-names in this enum.
105
- """
106
-
107
- def _dec(sub_class: type[BaseNode]):
108
- # Looping over _member_names_ will skip aliased items (like BBOX/Within)
109
- for member_name in names.__members__:
110
- self.register(name=member_name, namespace=namespace)(sub_class)
111
- return sub_class
112
-
113
- return _dec
114
-
115
- def from_child_xml(self, element: Element, allowed_types=None) -> BaseNode:
116
- """Convert the element into a Python class.
117
-
118
- This locates the parser from the registered tag.
119
- It's assumed that the tag has a "from_xml()" method too.
120
- """
121
- try:
122
- real_cls = self.resolve_class(element.tag)
123
- except ExternalParsingError as e:
124
- if allowed_types:
125
- # Show better exception message
126
- types = ", ".join(c.__name__ for c in allowed_types)
127
- raise ExternalParsingError(f"{e}, expected one of: {types}") from None
128
-
129
- raise
130
-
131
- # Check whether the resolved class is indeed a valid option here.
132
- if allowed_types is not None and not issubclass(real_cls, allowed_types):
133
- types = ", ".join(c.__name__ for c in allowed_types)
134
- raise ExternalParsingError(
135
- f"Unexpected {real_cls.__name__} for <{element.tag}> node, "
136
- f"expected one of: {types}"
137
- )
138
-
139
- return real_cls.from_xml(element)
140
-
141
- def resolve_class(self, tag_name) -> type[BaseNode]:
142
- # Resolve the dataclass using the tag name
143
- try:
144
- return self.parsers[tag_name]
145
- except KeyError:
146
- raise ExternalParsingError(f"Unsupported tag: <{tag_name}>") from None
147
-
148
-
149
- tag_registry = TagRegistry()