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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/db.py +56 -47
  8. gisserver/exceptions.py +26 -2
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +220 -156
  13. gisserver/geometries.py +32 -37
  14. gisserver/management/__init__.py +0 -0
  15. gisserver/management/commands/__init__.py +0 -0
  16. gisserver/management/commands/loadgeojson.py +291 -0
  17. gisserver/operations/base.py +122 -308
  18. gisserver/operations/wfs20.py +423 -337
  19. gisserver/output/__init__.py +9 -48
  20. gisserver/output/base.py +178 -139
  21. gisserver/output/csv.py +65 -74
  22. gisserver/output/geojson.py +34 -35
  23. gisserver/output/gml32.py +254 -246
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +52 -26
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -170
  28. gisserver/output/xmlschema.py +85 -46
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +13 -27
  32. gisserver/parsers/fes20/expressions.py +82 -38
  33. gisserver/parsers/fes20/filters.py +111 -43
  34. gisserver/parsers/fes20/identifiers.py +44 -26
  35. gisserver/parsers/fes20/lookups.py +144 -0
  36. gisserver/parsers/fes20/operators.py +331 -127
  37. gisserver/parsers/fes20/sorting.py +104 -33
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +5 -2
  40. gisserver/parsers/gml/geometries.py +69 -35
  41. gisserver/parsers/ows/__init__.py +25 -0
  42. gisserver/parsers/ows/kvp.py +190 -0
  43. gisserver/parsers/ows/requests.py +158 -0
  44. gisserver/parsers/query.py +175 -0
  45. gisserver/parsers/values.py +26 -0
  46. gisserver/parsers/wfs20/__init__.py +37 -0
  47. gisserver/parsers/wfs20/adhoc.py +245 -0
  48. gisserver/parsers/wfs20/base.py +143 -0
  49. gisserver/parsers/wfs20/projection.py +103 -0
  50. gisserver/parsers/wfs20/requests.py +482 -0
  51. gisserver/parsers/wfs20/stored.py +192 -0
  52. gisserver/parsers/xml.py +249 -0
  53. gisserver/projection.py +357 -0
  54. gisserver/static/gisserver/index.css +12 -1
  55. gisserver/templates/gisserver/index.html +1 -1
  56. gisserver/templates/gisserver/service_description.html +2 -2
  57. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +322 -259
  61. gisserver/views.py +198 -56
  62. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -285
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -37
  67. gisserver/queries/adhoc.py +0 -185
  68. gisserver/queries/base.py +0 -186
  69. gisserver/queries/projection.py +0 -240
  70. gisserver/queries/stored.py +0 -206
  71. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  72. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  73. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  74. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,7 @@
2
2
 
3
3
  from .results import FeatureCollection, SimpleFeatureCollection # isort: skip (fixes import loops)
4
4
 
5
- from django.utils.functional import classproperty
6
-
7
- from gisserver import conf
8
-
9
- from .base import OutputRenderer
5
+ from .base import CollectionOutputRenderer, OutputRenderer
10
6
  from .csv import CSVRenderer, DBCSVRenderer
11
7
  from .geojson import DBGeoJsonRenderer, GeoJsonRenderer
12
8
  from .gml32 import (
@@ -15,10 +11,13 @@ from .gml32 import (
15
11
  GML32Renderer,
16
12
  GML32ValueRenderer,
17
13
  )
18
- from .xmlschema import XMLSchemaRenderer
14
+ from .stored import DescribeStoredQueriesRenderer, ListStoredQueriesRenderer
15
+ from .utils import to_qname
16
+ from .xmlschema import XmlSchemaRenderer
19
17
 
20
18
  __all__ = [
21
19
  "OutputRenderer",
20
+ "CollectionOutputRenderer",
22
21
  "FeatureCollection",
23
22
  "SimpleFeatureCollection",
24
23
  "DBCSVRenderer",
@@ -29,46 +28,8 @@ __all__ = [
29
28
  "GeoJsonRenderer",
30
29
  "GML32Renderer",
31
30
  "GML32ValueRenderer",
32
- "XMLSchemaRenderer",
33
- "geojson_renderer",
34
- "gml32_renderer",
35
- "gml32_value_renderer",
31
+ "XmlSchemaRenderer",
32
+ "ListStoredQueriesRenderer",
33
+ "DescribeStoredQueriesRenderer",
34
+ "to_qname",
36
35
  ]
37
-
38
-
39
- def select_renderer(native_renderer_class, db_renderer_class):
40
- """Dynamically select the preferred renderer.
41
- This allows changing the settings within the app.
42
- """
43
-
44
- class SelectRenderer:
45
- """A proxy to the actual rendering class.
46
- Its structure still allows accessing class-properties of the actual class.
47
- """
48
-
49
- @classproperty
50
- def real_class(self):
51
- if conf.GISSERVER_USE_DB_RENDERING:
52
- return db_renderer_class
53
- else:
54
- return native_renderer_class
55
-
56
- def __new__(cls, *args, **kwargs):
57
- # Return the actual class instead
58
- return cls.real_class(*args, **kwargs)
59
-
60
- @classmethod
61
- def decorate_collection(cls, *args, **kwargs):
62
- return cls.real_class.decorate_collection(*args, **kwargs)
63
-
64
- @classproperty
65
- def max_page_size(cls):
66
- return cls.real_class.max_page_size
67
-
68
- return SelectRenderer
69
-
70
-
71
- csv_renderer = select_renderer(CSVRenderer, DBCSVRenderer)
72
- gml32_renderer = select_renderer(GML32Renderer, DBGML32Renderer)
73
- gml32_value_renderer = select_renderer(GML32ValueRenderer, DBGML32ValueRenderer)
74
- geojson_renderer = select_renderer(GeoJsonRenderer, DBGeoJsonRenderer)
gisserver/output/base.py CHANGED
@@ -1,26 +1,141 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import math
4
5
  import typing
6
+ from io import BytesIO, StringIO
5
7
 
6
8
  from django.conf import settings
7
9
  from django.db import models
8
10
  from django.http import HttpResponse, StreamingHttpResponse
9
- from django.utils.html import escape
11
+ from django.http.response import HttpResponseBase # Django 3.2 import location
10
12
 
11
- from gisserver import conf
12
- from gisserver.exceptions import InvalidParameterValue
13
+ from gisserver.features import FeatureType
14
+ from gisserver.parsers.values import fix_type_name
15
+ from gisserver.parsers.xml import split_ns
16
+ from gisserver.types import XsdAnyType, XsdNode
17
+
18
+ from .utils import render_xmlns_attributes, to_qname
19
+
20
+ logger = logging.getLogger(__name__)
13
21
 
14
22
  if typing.TYPE_CHECKING:
15
- from gisserver.features import FeatureType
16
- from gisserver.geometries import CRS
17
- from gisserver.operations.base import WFSMethod
18
- from gisserver.queries import FeatureProjection, FeatureRelation, QueryExpression
23
+ from gisserver.operations.base import WFSOperation
24
+ from gisserver.projection import FeatureProjection, FeatureRelation
19
25
 
20
- from .results import FeatureCollection, SimpleFeatureCollection
26
+ from .results import FeatureCollection
21
27
 
22
28
 
23
29
  class OutputRenderer:
30
+ """Base class for rendering content.
31
+
32
+ Note most rendering logic will generally
33
+ need to use :class:`XmlOutputRenderer` or :class:`ConnectionOutputRenderer`
34
+ as their base class
35
+ """
36
+
37
+ #: Default content type for the HTTP response
38
+ content_type = "application/octet-stream"
39
+
40
+ def __init__(self, operation: WFSOperation):
41
+ """Base method for all output rendering."""
42
+ self.operation = operation
43
+
44
+ def get_response(self) -> HttpResponseBase:
45
+ """Render the output as regular or streaming response."""
46
+ stream = self.render_stream()
47
+ if isinstance(stream, (str, bytes, StringIO, BytesIO)):
48
+ # Not a real stream, output anyway as regular HTTP response.
49
+ return HttpResponse(
50
+ content=stream,
51
+ content_type=self.content_type,
52
+ headers=self.get_headers(),
53
+ )
54
+ else:
55
+ # An actual generator.
56
+ # Handover to WSGI server (starts streaming when reading the contents)
57
+ stream = self._trap_exceptions(stream)
58
+ return StreamingHttpResponse(
59
+ streaming_content=stream,
60
+ content_type=self.content_type,
61
+ headers=self.get_headers(),
62
+ )
63
+
64
+ def get_headers(self):
65
+ return {}
66
+
67
+ def _trap_exceptions(self, stream):
68
+ """Decorate the generator to show exceptions"""
69
+ try:
70
+ yield from stream
71
+ except Exception as e:
72
+ # Can't return 500 at this point,
73
+ # but can still tell the client what happened.
74
+ yield self.render_exception(e)
75
+ raise
76
+
77
+ def render_exception(self, exception: Exception):
78
+ """Inform the client that the stream processing was interrupted with an exception.
79
+ The exception can be rendered in the format fits with the output.
80
+
81
+ Purposefully, not much information is given, so avoid informing clients.
82
+ The actual exception is still raised and logged server-side.
83
+ """
84
+ if settings.DEBUG:
85
+ return f"{exception.__class__.__name__}: {exception}"
86
+ else:
87
+ return f"{exception.__class__.__name__} during rendering!"
88
+
89
+ def render_stream(self):
90
+ """Implement this in subclasses to implement a custom output format.
91
+
92
+ The implementation may return a ``str``/``bytes`` object, which becomes
93
+ a normal ``HttpResponse`` object **OR** return a generator
94
+ that emits chunks. Such generator is wrapped in a ``StreamingHttpResponse``.
95
+ """
96
+ raise NotImplementedError()
97
+
98
+
99
+ class XmlOutputRenderer(OutputRenderer):
100
+ """Base class/mixin for XML-based rendering.
101
+
102
+ This provides the logic to translate XML elements into QName aliases.
103
+ """
104
+
105
+ #: Default content type for the HTTP response
106
+ content_type = "text/xml; charset=utf-8"
107
+
108
+ #: Default extra namespaces to include in the xmlns="..." attributes, and use for to_qname().
109
+ xml_namespaces = {}
110
+
111
+ def __init__(self, operation: WFSOperation):
112
+ """Base method for all output rendering."""
113
+ super().__init__(operation)
114
+ self.app_namespaces = {
115
+ **self.xml_namespaces,
116
+ **operation.view.get_xml_namespaces_to_prefixes(),
117
+ }
118
+
119
+ def render_xmlns_attributes(self):
120
+ """Render XML Namespace declaration attributes"""
121
+ return render_xmlns_attributes(self.app_namespaces)
122
+
123
+ def to_qname(self, xsd_type: XsdNode | XsdAnyType, namespaces=None) -> str:
124
+ """Generate the aliased name for the element or type."""
125
+ return to_qname(xsd_type.namespace, xsd_type.name, namespaces or self.app_namespaces)
126
+
127
+ def feature_to_qname(self, feature_type: FeatureType | str) -> str:
128
+ """Convert the FeatureType name to a QName"""
129
+ if isinstance(feature_type, FeatureType):
130
+ return to_qname(feature_type.xml_namespace, feature_type.name, self.app_namespaces)
131
+ else:
132
+ # e.g. "return_type" for StoredQueryDescription/QueryExpressionText
133
+ type_name = fix_type_name(feature_type, self.operation.view.xml_namespace)
134
+ ns, localname = split_ns(type_name)
135
+ return to_qname(ns, localname, self.app_namespaces)
136
+
137
+
138
+ class CollectionOutputRenderer(OutputRenderer):
24
139
  """Base class to create streaming responses.
25
140
 
26
141
  It receives the collected 'context' data of the WFSMethod.
@@ -30,76 +145,36 @@ class OutputRenderer:
30
145
  #: This value can be 'math.inf' to support endless pages by default.
31
146
  max_page_size = None
32
147
 
33
- #: Define the content type for rendering the output
34
- content_type = "application/octet-stream"
35
-
36
148
  #: An optional content-disposition header to output
37
149
  content_disposition = None
38
150
 
39
- def __init__(
40
- self,
41
- method: WFSMethod,
42
- source_query: QueryExpression,
43
- collection: FeatureCollection,
44
- output_crs: CRS,
45
- ):
151
+ def __init__(self, operation: WFSOperation, collection: FeatureCollection):
46
152
  """
47
153
  Receive the collected data to render.
48
- These parameters are received from the ``get_context_data()`` method of the view.
49
154
 
50
- :param method: The calling WFS Method (e.g. GetFeature class)
51
- :param source_query: The query that generated this output.
155
+ :param operation: The calling WFS Method (e.g. GetFeature class)
52
156
  :param collection: The collected data for rendering.
53
- :param projection: Which fields to render.
54
- :param output_crs: The requested output projection.
55
157
  """
56
- self.method = method
57
- self.source_query = source_query
158
+ super().__init__(operation)
58
159
  self.collection = collection
59
- self.output_crs = output_crs
60
- self.xml_srs_name = escape(str(self.output_crs))
160
+ self.apply_projection()
61
161
 
62
- # Common elements for output rendering:
63
- self.server_url = method.view.server_url
64
- self.app_xml_namespace = method.view.xml_namespace
65
-
66
- @classmethod
67
- def decorate_collection(cls, collection: FeatureCollection, output_crs: CRS, **params):
162
+ def apply_projection(self):
68
163
  """Perform presentation-layer logic enhancements on the queryset."""
69
- for sub_collection in collection.results:
70
- # Validate the presentation-level parameters for this feature:
71
- cls.validate(sub_collection.feature_type, **params)
72
-
73
- queryset = cls.decorate_queryset(
74
- sub_collection.projection,
75
- sub_collection.queryset,
76
- output_crs,
77
- **params,
164
+ for sub_collection in self.collection.results:
165
+ queryset = self.decorate_queryset(
166
+ projection=sub_collection.projection,
167
+ queryset=sub_collection.queryset,
78
168
  )
169
+ if sub_collection._result_cache is not None:
170
+ raise RuntimeError(
171
+ "SimpleFeatureCollection QuerySet was already processed in output rendering."
172
+ )
79
173
  if queryset is not None:
80
174
  sub_collection.queryset = queryset
81
175
 
82
- @classmethod
83
- def validate(cls, feature_type: FeatureType, **params):
84
- """Validate the presentation parameters"""
85
- crs = params["srsName"]
86
- if (
87
- conf.GISSERVER_SUPPORTED_CRS_ONLY
88
- and crs is not None
89
- and crs not in feature_type.supported_crs
90
- ):
91
- raise InvalidParameterValue(
92
- f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
93
- locator="srsName",
94
- )
95
-
96
- @classmethod
97
176
  def decorate_queryset(
98
- cls,
99
- projection: FeatureProjection,
100
- queryset: models.QuerySet,
101
- output_crs: CRS,
102
- **params,
177
+ self, projection: FeatureProjection, queryset: models.QuerySet
103
178
  ) -> models.QuerySet:
104
179
  """Apply presentation layer logic to the queryset.
105
180
 
@@ -108,21 +183,30 @@ class OutputRenderer:
108
183
  :param feature_type: The feature that is being queried.
109
184
  :param queryset: The constructed queryset so far.
110
185
  :param output_crs: The projected output
111
- :param params: All remaining request parameters (e.g. KVP parameters).
112
186
  """
187
+ if queryset._result_cache is not None:
188
+ raise RuntimeError(
189
+ "QuerySet was already processed before passing to the output rendering."
190
+ )
191
+
113
192
  # Avoid fetching relations, fetch these within the same query,
114
- related = cls._get_prefetch_related(projection, output_crs)
115
- if related:
116
- queryset = queryset.prefetch_related(*related)
193
+ prefetches = self._get_prefetch_related(projection)
194
+ if prefetches:
195
+ logger.debug(
196
+ "QuerySet for %s prefetches: %r",
197
+ queryset.model._meta.label,
198
+ [p.prefetch_through for p in prefetches],
199
+ )
200
+ queryset = queryset.prefetch_related(*prefetches)
117
201
 
202
+ logger.debug(
203
+ "QuerySet for %s only retrieves: %r",
204
+ queryset.model._meta.label,
205
+ projection.only_fields,
206
+ )
118
207
  return queryset.only("pk", *projection.only_fields)
119
208
 
120
- @classmethod
121
- def _get_prefetch_related(
122
- cls,
123
- projection: FeatureProjection,
124
- output_crs: CRS,
125
- ) -> list[models.Prefetch]:
209
+ def _get_prefetch_related(self, projection: FeatureProjection) -> list[models.Prefetch]:
126
210
  """Summarize which fields read data from relations.
127
211
 
128
212
  This combines the input from flattened and complex fields,
@@ -134,17 +218,13 @@ class OutputRenderer:
134
218
  return [
135
219
  models.Prefetch(
136
220
  orm_relation.orm_path,
137
- queryset=cls.get_prefetch_queryset(projection, orm_relation, output_crs),
221
+ queryset=self.get_prefetch_queryset(projection, orm_relation),
138
222
  )
139
223
  for orm_relation in projection.orm_relations
140
224
  ]
141
225
 
142
- @classmethod
143
226
  def get_prefetch_queryset(
144
- cls,
145
- projection: FeatureProjection,
146
- feature_relation: FeatureRelation,
147
- output_crs: CRS,
227
+ self, projection: FeatureProjection, feature_relation: FeatureRelation
148
228
  ) -> models.QuerySet | None:
149
229
  """Generate a custom queryset that's used to prefetch a relation."""
150
230
  # Multiple elements could be referencing the same model, just take first that is filled in.
@@ -154,76 +234,35 @@ class OutputRenderer:
154
234
  # This will also apply .only() based on the projection's feature_relation
155
235
  return projection.feature_type.get_related_queryset(feature_relation)
156
236
 
157
- def get_response(self):
158
- """Render the output as streaming response."""
159
- stream = self.render_stream()
160
- if isinstance(stream, (str, bytes)):
161
- # Not a real stream, output anyway as regular HTTP response.
162
- response = HttpResponse(content=stream, content_type=self.content_type)
163
- else:
164
- # A actual generator.
165
- stream = self._trap_exceptions(stream)
166
- response = StreamingHttpResponse(
167
- streaming_content=stream,
168
- content_type=self.content_type,
169
- )
170
-
171
- # Add HTTP headers
172
- for name, value in self.get_headers().items():
173
- response[name] = value
174
-
175
- # Handover to WSGI server (starts streaming when reading the contents)
176
- return response
177
-
178
237
  def get_headers(self):
179
238
  """Return the response headers"""
180
239
  if self.content_disposition:
181
240
  # Offer a common quick content-disposition logic that works for all possible queries.
182
- sub_collection = self.collection.results[0]
183
- if sub_collection.stop == math.inf:
184
- page = f"{sub_collection.start}-end" if sub_collection.start else "all"
185
- elif sub_collection.stop:
186
- page = f"{sub_collection.start}-{sub_collection.stop - 1}"
187
- else:
188
- page = "results"
189
-
190
241
  return {
191
242
  "Content-Disposition": self.content_disposition.format(
192
- typenames="+".join(sub.feature_type.name for sub in self.collection.results),
193
- page=page,
194
- date=self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
195
- timestamp=self.collection.timestamp,
243
+ **self.get_content_disposition_kwargs()
196
244
  )
197
245
  }
198
246
 
199
247
  return {}
200
248
 
201
- def _trap_exceptions(self, stream):
202
- """Decorate the generator to show exceptions"""
203
- try:
204
- yield from stream
205
- except Exception as e:
206
- # Can't return 500 at this point,
207
- # but can still tell the client what happened.
208
- yield self.render_exception(e)
209
- raise
210
-
211
- def render_exception(self, exception: Exception):
212
- """Inform the client that the stream processing was interrupted with an exception.
213
- The exception can be rendered in the format fits with the output.
214
-
215
- Purposefully, not much information is given, so avoid informing clients.
216
- The actual exception is still raised and logged server-side.
217
- """
218
- if settings.DEBUG:
219
- return f"{exception.__class__.__name__}: {exception}"
249
+ def get_content_disposition_kwargs(self) -> dict:
250
+ """Offer a common quick content-disposition logic that works for all possible queries."""
251
+ sub_collection = self.collection.results[0]
252
+ if sub_collection.stop == math.inf:
253
+ page = f"{sub_collection.start}-end" if sub_collection.start else "all"
254
+ elif sub_collection.stop:
255
+ page = f"{sub_collection.start}-{sub_collection.stop - 1}"
220
256
  else:
221
- return f"{exception.__class__.__name__} during rendering!"
222
-
223
- def render_stream(self):
224
- """Implement this in subclasses to implement a custom output format."""
225
- raise NotImplementedError()
226
-
227
- def get_projection(self, sub_collection: SimpleFeatureCollection) -> FeatureProjection:
228
- """Provide the projection clause for the given sub-collection."""
229
- return self.source_query.get_projection(sub_collection.feature_type)
257
+ page = "results"
258
+
259
+ return {
260
+ "typenames": "+".join(
261
+ feature_type.name
262
+ for sub in self.collection.results
263
+ for feature_type in sub.feature_types
264
+ ),
265
+ "page": page,
266
+ "date": self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
267
+ "timestamp": self.collection.timestamp,
268
+ }