django-gisserver 1.4.1__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 (73) hide show
  1. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.4.1.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 +63 -60
  8. gisserver/exceptions.py +47 -9
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +267 -240
  13. gisserver/geometries.py +34 -39
  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 +129 -305
  18. gisserver/operations/wfs20.py +428 -336
  19. gisserver/output/__init__.py +10 -48
  20. gisserver/output/base.py +198 -143
  21. gisserver/output/csv.py +81 -85
  22. gisserver/output/geojson.py +63 -72
  23. gisserver/output/gml32.py +310 -281
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +71 -30
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -154
  28. gisserver/output/xmlschema.py +86 -47
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +15 -11
  32. gisserver/parsers/fes20/expressions.py +89 -50
  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 +336 -128
  37. gisserver/parsers/fes20/sorting.py +107 -34
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +6 -3
  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 +11 -11
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +375 -258
  61. gisserver/views.py +206 -75
  62. django_gisserver-1.4.1.dist-info/RECORD +0 -53
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -275
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -34
  67. gisserver/queries/adhoc.py +0 -181
  68. gisserver/queries/base.py +0 -146
  69. gisserver/queries/stored.py +0 -205
  70. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  71. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  72. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  73. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/views.py CHANGED
@@ -2,30 +2,42 @@
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
- from django.http import HttpResponse
11
11
  from django.shortcuts import render
12
12
  from django.views import View
13
+ from django.views.decorators.csrf import csrf_exempt
13
14
 
14
15
  from gisserver import conf
15
16
  from gisserver.exceptions import (
17
+ ExternalParsingError,
16
18
  InvalidParameterValue,
17
- MissingParameterValue,
18
19
  OperationNotSupported,
20
+ OperationParsingFailed,
21
+ OperationProcessingFailed,
19
22
  OWSException,
20
23
  PermissionDenied,
21
24
  )
22
25
  from gisserver.features import FeatureType, ServiceDescription
23
26
  from gisserver.operations import base, wfs20
27
+ from gisserver.parsers.ows import (
28
+ BaseOwsRequest,
29
+ KVPRequest,
30
+ resolve_kvp_parser_class,
31
+ resolve_xml_parser_class,
32
+ )
33
+ from gisserver.parsers.xml import parse_xml_from_string, split_ns
34
+
35
+ logger = logging.getLogger(__name__)
24
36
 
25
37
  SAFE_VERSION = re.compile(r"\A[0-9.]+\Z")
26
38
 
27
39
 
28
- class GISView(View):
40
+ class OWSView(View):
29
41
  """The base logic to implement OGC view like WFS.
30
42
 
31
43
  Each subclass defines 'accept_operations' with the desired RPC operations.
@@ -34,6 +46,9 @@ class GISView(View):
34
46
  #: Define the namespace to use in the XML
35
47
  xml_namespace = "http://example.org/gisserver"
36
48
 
49
+ #: Define namespace aliases to use, default is {"app": self.xml_namespace}
50
+ xml_namespace_aliases = None
51
+
37
52
  #: Default version to use
38
53
  version = "2.0.0"
39
54
 
@@ -49,12 +64,16 @@ class GISView(View):
49
64
  #: Metadata of the service:
50
65
  service_description: ServiceDescription | None = None
51
66
 
52
- #: Template to render a HTML welcome page for non-OGC requests.
67
+ #: Template to render an HTML welcome page for non-OGC requests.
53
68
  index_template_name = None
54
69
 
55
70
  #: Whether to render GET HTML pages
56
71
  use_html_templates = True
57
72
 
73
+ #: The OWS request, which contains the complete parsed request.
74
+ ows_request: BaseOwsRequest = None
75
+
76
+ @csrf_exempt
58
77
  def dispatch(self, request, *args, **kwargs):
59
78
  """Render proper XML errors for exceptions on all request types."""
60
79
  try:
@@ -70,25 +89,32 @@ class GISView(View):
70
89
  When nothing is returned, the exception is raised instead.
71
90
  """
72
91
  if isinstance(exc, Django_PermissionDenied):
73
- exc = PermissionDenied("typeNames", text=str(exc) or None)
74
- return HttpResponse(
75
- exc.as_xml().encode("utf-8"),
76
- content_type="text/xml; charset=utf-8",
77
- status=exc.status_code,
78
- reason=exc.reason,
79
- )
92
+ exc = PermissionDenied(str(exc) or None, locator="typeNames")
93
+ return exc.as_response()
80
94
  elif isinstance(exc, OWSException):
81
95
  # Wrap our XML-based exception into a response.
82
96
  exc.version = self.version # Use negotiated version
83
- return HttpResponse(
84
- exc.as_xml().encode("utf-8"),
85
- content_type="text/xml; charset=utf-8",
86
- status=exc.status_code,
87
- reason=exc.reason,
88
- )
97
+ return exc.as_response()
89
98
  else:
90
99
  return None
91
100
 
101
+ @classmethod
102
+ def get_xml_namespace_aliases(cls) -> dict[str, str]:
103
+ """Provide all namespaces aliases with a namespace.
104
+ This is most useful for parsing input.
105
+ """
106
+ return cls.xml_namespace_aliases or {"app": cls.xml_namespace}
107
+
108
+ @classmethod
109
+ def get_xml_namespaces_to_prefixes(cls) -> dict[str, str]:
110
+ """Provide a mapping from namespace to prefix.
111
+ This is most useful for rendering output.
112
+ """
113
+ return {
114
+ xml_namespace: prefix
115
+ for prefix, xml_namespace in cls.get_xml_namespace_aliases().items()
116
+ }
117
+
92
118
  def get(self, request, *args, **kwargs):
93
119
  """Entry point to handle HTTP GET requests.
94
120
 
@@ -97,22 +123,78 @@ class GISView(View):
97
123
 
98
124
  All query parameters are handled as case-insensitive.
99
125
  """
100
- # Convert to WFS key-value-pair format.
101
- self.KVP = {key.upper(): value for key, value in request.GET.items()}
126
+ # Parse GET parameters in Key-Value-Pair syntax format.
127
+ if logger.isEnabledFor(logging.DEBUG):
128
+ logger.debug(
129
+ "Parsing GET parameters:\n%s",
130
+ unquote_plus(request.META["QUERY_STRING"].replace("&", "\n")),
131
+ )
132
+ self.kvp = kvp = KVPRequest(request.GET, ns_aliases=self.get_xml_namespace_aliases())
133
+
134
+ # Get service (only raises error when value is missing and "default" parameter is not given)
135
+ defaults = {"default": self.default_service} if self.default_service else {}
136
+ service = kvp.get_str("service", **defaults).upper()
102
137
 
103
138
  # Perform early version parsing. The detailed validation happens by the operation
104
139
  # Parameter objects, but by performing an early check, templates can use that version.
105
- version = self.KVP.get("VERSION")
106
- if version and version in self.accept_versions:
107
- self.set_version(version)
140
+ version = kvp.get_str("version", default=None)
141
+ self.set_version(service, version)
108
142
 
109
- # Allow for an user-friendly opening page (hence the version check above)
143
+ # Allow for a user-friendly opening page (hence the version check above)
110
144
  if self.use_html_templates and self.is_index_request():
111
- return self.render_index()
145
+ return self.render_index(service)
146
+
147
+ # Find the registered operation that handles the request
148
+ operation = kvp.get_str("request")
149
+ wfs_operation_cls = self.get_operation_class(service, operation)
150
+
151
+ # Parse the request syntax
152
+ request_cls = wfs_operation_cls.parser_class or resolve_kvp_parser_class(kvp)
153
+ self.ows_request = request_cls.from_kvp_request(kvp)
154
+
155
+ # Process the request!
156
+ return self.call_operation(wfs_operation_cls)
157
+
158
+ def post(self, request, *args, **kwargs):
159
+ """Entry point to handle HTTP POST requests.
160
+
161
+ This parses the XML to get the correct service and operation,
162
+ to call the proper WFSMethod.
163
+ """
164
+ # Parse the XML body
165
+ if logger.isEnabledFor(logging.DEBUG):
166
+ logger.debug("Parsing POST:\n%s", request.body.decode().rstrip())
167
+ try:
168
+ root = parse_xml_from_string(
169
+ request.body, extra_ns_aliases=self.get_xml_namespace_aliases()
170
+ )
171
+ except ExternalParsingError as e:
172
+ raise OperationParsingFailed(f"Unable to parse XML: {e}") from e
173
+
174
+ # Find the registered operation that handles the request
175
+ service = (
176
+ root.attrib.get("service", self.default_service)
177
+ if self.default_service
178
+ else root.get_str_attribute("service")
179
+ )
180
+ operation = split_ns(root.tag)[1]
181
+ wfs_operation_cls = self.get_operation_class(service, operation)
112
182
 
113
- # Normal WFS
114
- wfs_method_cls = self.get_operation_class()
115
- return self.call_operation(wfs_method_cls)
183
+ # Parse the request syntax
184
+ request_cls = wfs_operation_cls.parser_class or resolve_xml_parser_class(root)
185
+ try:
186
+ self.ows_request = request_cls.from_xml(root)
187
+ self.set_version(service, self.ows_request.version)
188
+
189
+ # Process the request!
190
+ return self.call_operation(wfs_operation_cls)
191
+ except (OperationParsingFailed, OperationProcessingFailed) as e:
192
+ # The WFS spec dictates that these exceptions
193
+ # (and ResponseCacheExpired, CannotLockAllFeatures, FeaturesNotLocked which we don't raise)
194
+ # should report the 'handle' in the 'locator' argument of the exception when it was provided.
195
+ if self.ows_request.handle:
196
+ e.locator = self.ows_request.handle
197
+ raise
116
198
 
117
199
  def is_index_request(self):
118
200
  """Tell whether to index page should be shown."""
@@ -120,22 +202,29 @@ class GISView(View):
120
202
  # (misspelled?) parameters on the query string, this is considered to be an index page.
121
203
  # Minimal request has 2 parameters (SERVICE=WFS&REQUEST=GetCapabilities).
122
204
  # Some servers also allow a default of SERVICE, thus needing even less.
123
- return len(self.KVP) < 2 and {
124
- "REQUEST",
125
- "SERVICE",
126
- "VERSION",
127
- "ACCEPTVERSIONS",
128
- }.isdisjoint(self.KVP.keys())
129
-
130
- def render_index(self):
205
+ return (
206
+ self.request.method == "GET"
207
+ and len(self.request.GET) < 2
208
+ and {
209
+ "REQUEST",
210
+ "SERVICE",
211
+ "VERSION",
212
+ "ACCEPTVERSIONS",
213
+ }.isdisjoint(key.upper() for key in self.request.GET)
214
+ )
215
+
216
+ def render_index(self, service: str | None = None):
131
217
  """Render the index page."""
132
218
  # Not using the whole TemplateResponseMixin config with configurable parameters.
133
219
  # If this really needs more configuration, overriding is likely just as easy.
134
- return render(self.request, self.get_index_template_names(), self.get_index_context_data())
220
+ return render(
221
+ self.request,
222
+ self.get_index_template_names(service),
223
+ self.get_index_context_data(service=service),
224
+ )
135
225
 
136
- def get_index_context_data(self, **kwargs):
226
+ def get_index_context_data(self, service: str | None = None, **kwargs):
137
227
  """Provide the context data for the index page."""
138
- service = self.KVP.get("SERVICE", self.default_service)
139
228
  root_url = self.request.build_absolute_uri()
140
229
 
141
230
  # Allow passing extra vendor parameters to the links generated in the template
@@ -149,6 +238,7 @@ class GISView(View):
149
238
 
150
239
  return {
151
240
  "view": self,
241
+ "xml_namespaces": self.get_xml_namespaces_to_prefixes(),
152
242
  "service": service,
153
243
  "root_url": root_url,
154
244
  "base_query": base_query,
@@ -160,16 +250,11 @@ class GISView(View):
160
250
  **kwargs,
161
251
  }
162
252
 
163
- def get_index_template_names(self):
253
+ def get_index_template_names(self, service: str | None = None):
164
254
  """Get the index page template name.
165
255
  If no template is configured, some reasonable defaults are selected.
166
256
  """
167
- raw_service = self.KVP.get("SERVICE", self.default_service)
168
- if raw_service and raw_service in self.accept_operations:
169
- service = raw_service.lower()
170
- else:
171
- service = "default"
172
-
257
+ service = service.lower() if service and service in self.accept_operations else "default"
173
258
  if self.index_template_name:
174
259
  # Allow the same substitutions for a manually configured template.
175
260
  return [self.index_template_name.format(service=service, version=self.version)]
@@ -180,60 +265,58 @@ class GISView(View):
180
265
  "gisserver/index.html",
181
266
  ]
182
267
 
183
- def get_operation_class(self) -> type[base.WFSMethod]:
268
+ def get_operation_class(self, service: str, request: str) -> type[base.WFSOperation]:
184
269
  """Resolve the method that the client wants to call."""
185
270
  if not self.accept_operations:
186
271
  raise ImproperlyConfigured("View has no operations")
187
272
 
188
- # The service is always WFS
189
- service = self._get_required_arg("SERVICE", self.default_service).upper()
190
273
  try:
191
- operations = self.accept_operations[service]
274
+ operations = self.accept_operations[service.upper()]
192
275
  except KeyError:
193
276
  allowed = ", ".join(sorted(self.accept_operations.keys()))
194
277
  raise InvalidParameterValue(
195
- "service",
196
278
  f"'{service}' is an invalid service, supported are: {allowed}.",
279
+ locator="service",
197
280
  ) from None
198
281
 
199
282
  # Resolve the operation
200
- # In mapserver, the operation name is case insensitive.
201
- operation = self._get_required_arg("REQUEST").upper()
283
+ # In mapserver, the operation name is case-insensitive.
202
284
  uc_methods = {name.upper(): method for name, method in operations.items()}
203
-
204
285
  try:
205
- return uc_methods[operation]
286
+ return uc_methods[request.upper()]
206
287
  except KeyError:
207
288
  allowed = ", ".join(operations.keys())
208
289
  raise OperationNotSupported(
209
- "request",
210
- f"'{operation.lower()}' is not implemented, supported are: {allowed}.",
290
+ f"'{request}' is not implemented, supported are: {allowed}.",
291
+ locator="request",
211
292
  ) from None
212
293
 
213
- def call_operation(self, wfs_method_cls: type[base.WFSMethod]):
294
+ def call_operation(self, wfs_operation_cls: type[base.WFSOperation]):
214
295
  """Call the resolved method."""
215
- wfs_method = wfs_method_cls(self)
216
- param_values = wfs_method.parse_request(self.KVP)
217
- return wfs_method(**param_values) # goes into __call__()
296
+ self.request.ows_request = self.ows_request # for check_permissions()
218
297
 
219
- def _get_required_arg(self, argname, default=None):
220
- try:
221
- return self.KVP[argname]
222
- except KeyError:
223
- if default is not None:
224
- return default
225
- raise MissingParameterValue(argname.lower()) from None
298
+ wfs_operation = wfs_operation_cls(self, self.ows_request)
299
+ wfs_operation.validate_request(self.ows_request)
300
+ return wfs_operation.process_request(self.ows_request)
226
301
 
227
- def get_service_description(self, service: str) -> ServiceDescription:
302
+ def get_service_description(self, service: str | None = None) -> ServiceDescription:
228
303
  """Provide the (dynamically generated) service description."""
229
304
  return self.service_description or ServiceDescription(title="Unnamed")
230
305
 
231
- def set_version(self, version):
306
+ def set_version(self, service: str, version: str | None):
232
307
  """Enforce a particular version based on the request."""
308
+ if not version:
309
+ return
310
+
233
311
  if not SAFE_VERSION.match(version):
234
312
  # Make really sure we didn't mess things up, as this causes file includes.
235
313
  raise SuspiciousOperation("Invalid/insecure version number parsed")
236
314
 
315
+ if version not in self.accept_versions:
316
+ raise InvalidParameterValue(
317
+ f"{service} Server does not support VERSION {version}.", locator="version"
318
+ )
319
+
237
320
  # Enforce the requested version
238
321
  self.version = version
239
322
 
@@ -243,7 +326,7 @@ class GISView(View):
243
326
  return self.request.build_absolute_uri(self.request.path)
244
327
 
245
328
 
246
- class WFSView(GISView):
329
+ class WFSView(OWSView):
247
330
  """A view for a single WFS server.
248
331
 
249
332
  This view exposes multiple dataset,
@@ -257,6 +340,7 @@ class WFSView(GISView):
257
340
  max_page_size = conf.GISSERVER_DEFAULT_MAX_PAGE_SIZE
258
341
 
259
342
  #: Define the features (=tables) in this dataset.
343
+ #: For dynamic per-request logic, consider overwriting :meth:`get_feature_types` instead.
260
344
  feature_types: list[FeatureType] = []
261
345
 
262
346
  #: Internal configuration of all available RPC calls.
@@ -281,7 +365,7 @@ class WFSView(GISView):
281
365
  "ImplementsTransactionalWFS": False, # only reads
282
366
  "ImplementsLockingWFS": False, # only basic WFS
283
367
  "KVPEncoding": True, # HTTP GET support
284
- "XMLEncoding": False, # HTTP POST requests
368
+ "XMLEncoding": True, # HTTP POST requests
285
369
  "SOAPEncoding": False, # no SOAP requests
286
370
  "ImplementsInheritance": False,
287
371
  "ImplementsRemoteResolve": False,
@@ -316,20 +400,67 @@ class WFSView(GISView):
316
400
  }
317
401
 
318
402
  def get_feature_types(self) -> list[FeatureType]:
319
- """Return all available feature types this server exposes"""
403
+ """Return all available feature types this server exposes.
404
+
405
+ This method may be overwritten to provide feature types dynamically,
406
+ for example to give them different elements based on user permissions.
407
+ """
320
408
  return self.feature_types
321
409
 
410
+ def get_bound_feature_types(self) -> list[FeatureType]:
411
+ """Internal logic wrapping the FeatureType definitions provided by the developer.
412
+ This binds the XML namespace information from this view to the declared types.
413
+ """
414
+ feature_types = self.get_feature_types()
415
+ for feature_type in feature_types:
416
+ # Make sure the feature type can advertise itself with an XML namespace.
417
+ feature_type.bind_namespace(default_xml_namespace=self.xml_namespace)
418
+ return feature_types
419
+
322
420
  def get_index_context_data(self, **kwargs):
323
421
  """Add WFS specific metadata"""
324
- wfs_output_formats = self.accept_operations["WFS"]["GetFeature"].output_formats
422
+ get_feature_operation = self.accept_operations["WFS"]["GetFeature"]
423
+ operation = get_feature_operation(self, ows_request=None)
424
+
425
+ # Remove aliases
426
+ wfs_output_formats = []
427
+ seen = set()
428
+ for output_format in operation.get_output_formats():
429
+ if output_format.identifier not in seen:
430
+ wfs_output_formats.append(output_format)
431
+ seen.add(output_format.identifier)
325
432
 
326
433
  context = super().get_index_context_data(**kwargs)
327
434
  context.update(
328
435
  {
329
- "wfs_features": self.get_feature_types(),
436
+ "wfs_features": self.get_bound_feature_types(),
330
437
  "wfs_output_formats": wfs_output_formats,
331
438
  "wfs_filter_capabilities": self.wfs_filter_capabilities,
332
439
  "wfs_service_constraints": self.wfs_service_constraints,
333
440
  }
334
441
  )
335
442
  return context
443
+
444
+ def get_xml_schema_url(self, feature_types: list[FeatureType]) -> str:
445
+ """Return the XML schema URL for the given feature types.
446
+ This is used in the GML output rendering.
447
+ """
448
+ app_namespaces = self.get_xml_namespaces_to_prefixes()
449
+ type_names = ",".join(
450
+ f"{app_namespaces[ft.xml_namespace]}:{ft.name}" for ft in feature_types
451
+ )
452
+ return (
453
+ f"{self.server_url}?SERVICE=WFS&VERSION=2.0.0"
454
+ f"&REQUEST=DescribeFeatureType&TYPENAMES={type_names}"
455
+ )
456
+
457
+ def check_permissions(self, feature_type: FeatureType):
458
+ """Hook that allows subclasses to reject access for datasets.
459
+ It may raise a Django PermissionDenied error.
460
+
461
+ This can access: ``self.request`` (the Django HTTPRequest) and ``self.ows_request``.
462
+ The latter contains a parsed request object
463
+ from the :mod:`gisserver.parsers.wfs20` package,
464
+ such as the parsed :class:`~gisserver.parsers.wfs20.GetFeature`
465
+ or :class:`~gisserver.parsers.wfs20.GetPropertyValue` request.
466
+ """
@@ -1,53 +0,0 @@
1
- gisserver/__init__.py,sha256=ouhGKnkyYnTmywjuiv3RGvKgca9hYJc5FP0cJxrQDgo,40
2
- gisserver/conf.py,sha256=3LSfeDRTdJCLuopAOD14Y5m2iPhovreIaNI9zbaukpo,2343
3
- gisserver/db.py,sha256=iuj43NonqNdKkLrerdzTucy-Lu5NmqymuE0bkwR6XWs,5318
4
- gisserver/exceptions.py,sha256=TZVdmwKI90Ga-9l8A1H0u65V5yNvgg2TDgyN_LCQ2zI,4597
5
- gisserver/features.py,sha256=GFbi-ap4NjlJh2Ka6Ssh-OjgZClLzVZyBqYqmolAtfI,32171
6
- gisserver/geometries.py,sha256=VQVEFk0oRiqdxG28llb51aayf5m9cbyLljR3PrZfMTg,12400
7
- gisserver/types.py,sha256=vyHPkUXEADikTJfRq7Nodqt0BCo7YV91dNyZw6GIMO4,35087
8
- gisserver/views.py,sha256=A7TwUsG8NKdnjTbOXPbio1K-XRg6IIpy_xH__zwW8xI,13193
9
- gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- gisserver/operations/base.py,sha256=tIEvQvFRRX2esfeP48EmcwB27-MkShtjl6A1v9TJASE,16827
11
- gisserver/operations/wfs20.py,sha256=lYkEq4DZYTSxNdwdZBAGOb7vtN5qMnwxOp8OD1G3jR4,18426
12
- gisserver/output/__init__.py,sha256=EVzgFN-PqKZKwbGVgWBUbIzGIXo69HSDMQ37szgr6cw,2097
13
- gisserver/output/base.py,sha256=NYyV78JgNDV7uUmnk6HNbbLTkt_N60YKPh2z1o-AuzM,7791
14
- gisserver/output/csv.py,sha256=7UKnyhFyAH1yeOh719Ov3SkzoLF1Zzlp_0-wxRDU9bI,6222
15
- gisserver/output/geojson.py,sha256=KB4w6DU_Dn7ZNDM5INDEtz7zhOAN75v03lUayPDzGPE,11296
16
- gisserver/output/gml32.py,sha256=kaTljrRKIlxIshiJTtzmsWWJExzQSL4hEX_Q1i17k_4,30496
17
- gisserver/output/results.py,sha256=HOmeSKLb0Tzsd9AamuXEPZyRmoXxHj7fKBqIbk1J5Co,13301
18
- gisserver/output/utils.py,sha256=VR8nOAv1obhfumQy77RnpJpE3FopzuIJrkfosBq16w4,6569
19
- gisserver/output/xmlschema.py,sha256=ltCglrQ4YXJ_dpMrkXb06dFfHc5a_nGog7fb-K25aeQ,4579
20
- gisserver/parsers/__init__.py,sha256=g72D8EtR-QkhcemzY5-Qty-nhHPKza83Sz3KZm8bfq0,290
21
- gisserver/parsers/base.py,sha256=eK32lw004v-lU82JihnH5LF-3wSh8M-L4JSayDt2gJM,4835
22
- gisserver/parsers/tags.py,sha256=NyJhbt-U2zTewfhghH4LRslTaDK2UOq9Vgk83pWfJh0,3553
23
- gisserver/parsers/values.py,sha256=ypNPl4HduiVzexsL59ukZntlwXk5gHuSvQO6yhLeXD4,1016
24
- gisserver/parsers/fes20/__init__.py,sha256=l8OutRicZ8E2cpyFtJj6wTFNArYoFaLiXABGyZYyKvQ,624
25
- gisserver/parsers/fes20/expressions.py,sha256=kXfIpOP94WOzvT3PZyIKrZSawQJZUPYyJ1TF5MoKsUQ,9694
26
- gisserver/parsers/fes20/filters.py,sha256=HdWguZvAT2L4Xm-bNa4XPh7MEymqTB6fo8r47Z-Bjuw,3511
27
- gisserver/parsers/fes20/functions.py,sha256=Qn72GI5qLqDSbREmsGUDiAPN2u1eo4c8AFttJMDnJX4,8724
28
- gisserver/parsers/fes20/identifiers.py,sha256=vcPrMYcsemSlB9ziSUdVulhoI5PGFAh8vqeADBCWd60,2864
29
- gisserver/parsers/fes20/operators.py,sha256=MVyoo2Hq45Lyqjh1cX9P_Zy9ARceCs28xDNL5ql_Yd0,23206
30
- gisserver/parsers/fes20/query.py,sha256=IQU0k4ZG-sAjWEWvSXw6H_wYODwtCMsF8RrbVdik4KE,10069
31
- gisserver/parsers/fes20/sorting.py,sha256=y0QJxFE0OC-GsaqrD0ZCI4CI16roUZrSW3fAG7BOKwA,2146
32
- gisserver/parsers/gml/__init__.py,sha256=lNK0SXWkmTKdeJD9xdK6B-N-Y7wwGL4NRBI3dzoJDNI,1463
33
- gisserver/parsers/gml/base.py,sha256=cMyXAoc1mhLp3Dqf1f65gNBhjETQWyUx--AHPsPCwBU,1066
34
- gisserver/parsers/gml/geometries.py,sha256=cgV3WogA7LyC_H_NF7dGoGLvLdQRACG6AlThRkX_jhU,3171
35
- gisserver/queries/__init__.py,sha256=zBLHOWKcU7PvBj6NHfGqcdUKCFJdL_np3_Mdn3bx-nE,893
36
- gisserver/queries/adhoc.py,sha256=hhLh3xB0UU--2tZwRgOWzonrjHM8OxCF2YYbl7NsxhE,7087
37
- gisserver/queries/base.py,sha256=w0hXefxX516oN3qdPYvM7huTewqrwxJmAseHHJ_BOgk,5891
38
- gisserver/queries/stored.py,sha256=RasXxlHlMCFtpO3EjIeMH6XPaDNvWbqJP23itR_byuU,7060
39
- gisserver/static/gisserver/index.css,sha256=fiQuCMuiWUhhrNQD6bbUJaX5vH5OpG9qgPdELPB8cpI,163
40
- gisserver/templates/gisserver/index.html,sha256=yliku8jDqAOcenkbe1YnvJBnuS13MvzzsoDV8sQZ3GM,866
41
- gisserver/templates/gisserver/service_description.html,sha256=3EHLnQx27_nTDZFD2RH6E10EWnTN90s1DGuEj9PeBrg,993
42
- gisserver/templates/gisserver/wfs/feature_field.html,sha256=H5LsTAxGVGQqEpOZdEUTszQmFAZSpB9ggRJUsTdjYnw,538
43
- gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
44
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml,sha256=4Hxw1kCnxfWXTVPbHf8IW9_qiSeIEtnxHK6z6TS3-zA,1041
45
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=KKrI0YYQqZWtTOyOkm-ZHuNyfwipdx0i0N8p7fggPU8,8675
46
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml,sha256=CIBVQrKXmGMQ6BKbX4_0ru4lXtWnW5EIgHUbIi1V0Vc,636
47
- gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- gisserver/templatetags/gisserver_tags.py,sha256=RFGqEj-EQi9FrJiFJ7HHAQqlU4fDKVKTtZrPNwURXgA,204
49
- django_gisserver-1.4.1.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
50
- django_gisserver-1.4.1.dist-info/METADATA,sha256=SpfcYIXvQiqTubhTKOAlarsHoGcMj9XeZQQRx1CJrW0,5874
51
- django_gisserver-1.4.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
52
- django_gisserver-1.4.1.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
53
- django_gisserver-1.4.1.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 an 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 an "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()