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
@@ -1,10 +1,8 @@
1
1
  """All supported output formats"""
2
2
 
3
- from django.utils.functional import classproperty
3
+ from .results import FeatureCollection, SimpleFeatureCollection # isort: skip (fixes import loops)
4
4
 
5
- from gisserver import conf
6
-
7
- from .base import OutputRenderer
5
+ from .base import CollectionOutputRenderer, OutputRenderer
8
6
  from .csv import CSVRenderer, DBCSVRenderer
9
7
  from .geojson import DBGeoJsonRenderer, GeoJsonRenderer
10
8
  from .gml32 import (
@@ -13,11 +11,13 @@ from .gml32 import (
13
11
  GML32Renderer,
14
12
  GML32ValueRenderer,
15
13
  )
16
- from .results import FeatureCollection, SimpleFeatureCollection
17
- from .xmlschema import XMLSchemaRenderer
14
+ from .stored import DescribeStoredQueriesRenderer, ListStoredQueriesRenderer
15
+ from .utils import to_qname
16
+ from .xmlschema import XmlSchemaRenderer
18
17
 
19
18
  __all__ = [
20
19
  "OutputRenderer",
20
+ "CollectionOutputRenderer",
21
21
  "FeatureCollection",
22
22
  "SimpleFeatureCollection",
23
23
  "DBCSVRenderer",
@@ -28,46 +28,8 @@ __all__ = [
28
28
  "GeoJsonRenderer",
29
29
  "GML32Renderer",
30
30
  "GML32ValueRenderer",
31
- "XMLSchemaRenderer",
32
- "geojson_renderer",
33
- "gml32_renderer",
34
- "gml32_value_renderer",
31
+ "XmlSchemaRenderer",
32
+ "ListStoredQueriesRenderer",
33
+ "DescribeStoredQueriesRenderer",
34
+ "to_qname",
35
35
  ]
36
-
37
-
38
- def select_renderer(native_renderer_class, db_renderer_class):
39
- """Dynamically select the preferred renderer.
40
- This allows changing the settings within the app.
41
- """
42
-
43
- class SelectRenderer:
44
- """A proxy to the actual rendering class.
45
- Its structure still allows accessing class-properties of the actual class.
46
- """
47
-
48
- @classproperty
49
- def real_class(self):
50
- if conf.GISSERVER_USE_DB_RENDERING:
51
- return db_renderer_class
52
- else:
53
- return native_renderer_class
54
-
55
- def __new__(cls, *args, **kwargs):
56
- # Return the actual class instead
57
- return cls.real_class(*args, **kwargs)
58
-
59
- @classmethod
60
- def decorate_collection(cls, *args, **kwargs):
61
- return cls.real_class.decorate_collection(*args, **kwargs)
62
-
63
- @classproperty
64
- def max_page_size(cls):
65
- return cls.real_class.max_page_size
66
-
67
- return SelectRenderer
68
-
69
-
70
- csv_renderer = select_renderer(CSVRenderer, DBCSVRenderer)
71
- gml32_renderer = select_renderer(GML32Renderer, DBGML32Renderer)
72
- gml32_value_renderer = select_renderer(GML32ValueRenderer, DBGML32ValueRenderer)
73
- geojson_renderer = select_renderer(GeoJsonRenderer, DBGeoJsonRenderer)
gisserver/output/base.py CHANGED
@@ -1,22 +1,141 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import math
5
+ import typing
6
+ from io import BytesIO, StringIO
4
7
 
5
8
  from django.conf import settings
6
9
  from django.db import models
7
10
  from django.http import HttpResponse, StreamingHttpResponse
8
- from django.utils.html import escape
11
+ from django.http.response import HttpResponseBase # Django 3.2 import location
9
12
 
10
- from gisserver import conf
11
- from gisserver.exceptions import InvalidParameterValue
12
- from gisserver.features import FeatureRelation, FeatureType
13
- from gisserver.geometries import CRS
14
- from gisserver.operations.base import WFSMethod
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
15
17
 
16
- from .results import FeatureCollection
18
+ from .utils import render_xmlns_attributes, to_qname
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from gisserver.operations.base import WFSOperation
24
+ from gisserver.projection import FeatureProjection, FeatureRelation
25
+
26
+ from .results import FeatureCollection
17
27
 
18
28
 
19
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):
20
139
  """Base class to create streaming responses.
21
140
 
22
141
  It receives the collected 'context' data of the WFSMethod.
@@ -26,188 +145,124 @@ class OutputRenderer:
26
145
  #: This value can be 'math.inf' to support endless pages by default.
27
146
  max_page_size = None
28
147
 
29
- #: Define the content type for rendering the output
30
- content_type = "application/octet-stream"
31
-
32
148
  #: An optional content-disposition header to output
33
149
  content_disposition = None
34
150
 
35
- def __init__(
36
- self,
37
- method: WFSMethod,
38
- source_query,
39
- collection: FeatureCollection,
40
- output_crs: CRS,
41
- ):
151
+ def __init__(self, operation: WFSOperation, collection: FeatureCollection):
42
152
  """
43
153
  Receive the collected data to render.
44
154
 
45
- :param method: The calling WFS Method (e.g. GetFeature class)
46
- :param source_query: The query that generated this output.
47
- :type source_query: gisserver.queries.QueryExpression
48
- :param collection: The collected data for rendering
49
- :param output_crs: The requested output projection.
155
+ :param operation: The calling WFS Method (e.g. GetFeature class)
156
+ :param collection: The collected data for rendering.
50
157
  """
51
- self.method = method
52
- self.source_query = source_query
158
+ super().__init__(operation)
53
159
  self.collection = collection
54
- self.output_crs = output_crs
55
- self.xml_srs_name = escape(str(self.output_crs))
56
-
57
- # Common elements for output rendering:
58
- self.server_url = method.view.server_url
59
- self.app_xml_namespace = method.view.xml_namespace
160
+ self.apply_projection()
60
161
 
61
- @classmethod
62
- def decorate_collection(cls, collection: FeatureCollection, output_crs: CRS, **params):
162
+ def apply_projection(self):
63
163
  """Perform presentation-layer logic enhancements on the queryset."""
64
- for sub_collection in collection.results:
65
- # Validate the presentation-level parameters for this feature:
66
- cls.validate(sub_collection.feature_type, **params)
67
-
68
- queryset = cls.decorate_queryset(
69
- sub_collection.feature_type,
70
- sub_collection.queryset,
71
- output_crs,
72
- **params,
164
+ for sub_collection in self.collection.results:
165
+ queryset = self.decorate_queryset(
166
+ projection=sub_collection.projection,
167
+ queryset=sub_collection.queryset,
73
168
  )
169
+ if sub_collection._result_cache is not None:
170
+ raise RuntimeError(
171
+ "SimpleFeatureCollection QuerySet was already processed in output rendering."
172
+ )
74
173
  if queryset is not None:
75
174
  sub_collection.queryset = queryset
76
175
 
77
- @classmethod
78
- def validate(cls, feature_type: FeatureType, **params):
79
- """Validate the presentation parameters"""
80
- crs = params["srsName"]
81
- if (
82
- conf.GISSERVER_SUPPORTED_CRS_ONLY
83
- and crs is not None
84
- and crs not in feature_type.supported_crs
85
- ):
86
- raise InvalidParameterValue(
87
- "srsName",
88
- f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
89
- )
90
-
91
- @classmethod
92
176
  def decorate_queryset(
93
- cls,
94
- feature_type: FeatureType,
95
- queryset: models.QuerySet,
96
- output_crs: CRS,
97
- **params,
177
+ self, projection: FeatureProjection, queryset: models.QuerySet
98
178
  ) -> models.QuerySet:
99
- """Apply presentation layer logic to the queryset."""
179
+ """Apply presentation layer logic to the queryset.
180
+
181
+ This allows fine-tuning the queryset for any special needs of the output rendering type.
182
+
183
+ :param feature_type: The feature that is being queried.
184
+ :param queryset: The constructed queryset so far.
185
+ :param output_crs: The projected output
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
+
100
192
  # Avoid fetching relations, fetch these within the same query,
101
- related = cls._get_prefetch_related(feature_type, output_crs)
102
- if related:
103
- queryset = queryset.prefetch_related(*related)
104
-
105
- # Also limit the queryset to the actual fields that are shown.
106
- # No need to request more data
107
- fields = [
108
- f.orm_field
109
- for f in feature_type.xsd_type.elements
110
- if not f.is_many or f.is_array # exclude M2M, but include ArrayField
111
- ]
112
- return queryset.only("pk", *fields)
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)
201
+
202
+ logger.debug(
203
+ "QuerySet for %s only retrieves: %r",
204
+ queryset.model._meta.label,
205
+ projection.only_fields,
206
+ )
207
+ return queryset.only("pk", *projection.only_fields)
113
208
 
114
- @classmethod
115
- def _get_prefetch_related(
116
- cls, feature_type: FeatureType, output_crs: CRS
117
- ) -> list[models.Prefetch]:
209
+ def _get_prefetch_related(self, projection: FeatureProjection) -> list[models.Prefetch]:
118
210
  """Summarize which fields read data from relations.
119
211
 
120
212
  This combines the input from flattened and complex fields,
121
213
  in the unlikely case both variations are used in the same feature.
122
214
  """
215
+ # When PROPERTYNAME is used, determine all ORM paths (and levels) that this query will touch.
216
+ # The prefetch will only be applied when its field coincide with this list.
217
+
123
218
  return [
124
219
  models.Prefetch(
125
220
  orm_relation.orm_path,
126
- queryset=cls.get_prefetch_queryset(feature_type, orm_relation, output_crs),
221
+ queryset=self.get_prefetch_queryset(projection, orm_relation),
127
222
  )
128
- for orm_relation in feature_type.orm_relations
223
+ for orm_relation in projection.orm_relations
129
224
  ]
130
225
 
131
- @classmethod
132
226
  def get_prefetch_queryset(
133
- cls,
134
- feature_type: FeatureType,
135
- feature_relation: FeatureRelation,
136
- output_crs: CRS,
227
+ self, projection: FeatureProjection, feature_relation: FeatureRelation
137
228
  ) -> models.QuerySet | None:
138
229
  """Generate a custom queryset that's used to prefetch a relation."""
139
230
  # Multiple elements could be referencing the same model, just take first that is filled in.
140
231
  if feature_relation.related_model is None:
141
232
  return None
142
233
 
143
- return feature_type.get_related_queryset(feature_relation)
144
-
145
- def get_response(self):
146
- """Render the output as streaming response."""
147
- stream = self.render_stream()
148
- if isinstance(stream, (str, bytes)):
149
- # Not a real stream, output anyway as regular HTTP response.
150
- response = HttpResponse(content=stream, content_type=self.content_type)
151
- else:
152
- # A actual generator.
153
- stream = self._trap_exceptions(stream)
154
- response = StreamingHttpResponse(
155
- streaming_content=stream,
156
- content_type=self.content_type,
157
- )
158
-
159
- # Add HTTP headers
160
- for name, value in self.get_headers().items():
161
- response[name] = value
162
-
163
- # Handover to WSGI server (starts streaming when reading the contents)
164
- return response
234
+ # This will also apply .only() based on the projection's feature_relation
235
+ return projection.feature_type.get_related_queryset(feature_relation)
165
236
 
166
237
  def get_headers(self):
167
238
  """Return the response headers"""
168
239
  if self.content_disposition:
169
240
  # Offer a common quick content-disposition logic that works for all possible queries.
170
- sub_collection = self.collection.results[0]
171
- if sub_collection.stop == math.inf:
172
- page = f"{sub_collection.start}-end" if sub_collection.start else "all"
173
- elif sub_collection.stop:
174
- page = f"{sub_collection.start}-{sub_collection.stop - 1}"
175
- else:
176
- page = "results"
177
-
178
241
  return {
179
242
  "Content-Disposition": self.content_disposition.format(
180
- typenames="+".join(sub.feature_type.name for sub in self.collection.results),
181
- page=page,
182
- date=self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
183
- timestamp=self.collection.timestamp,
243
+ **self.get_content_disposition_kwargs()
184
244
  )
185
245
  }
186
246
 
187
247
  return {}
188
248
 
189
- def _trap_exceptions(self, stream):
190
- """Decorate the generator to show exceptions"""
191
- try:
192
- yield from stream
193
- except Exception as e:
194
- # Can't return 500 at this point,
195
- # but can still tell the client what happened.
196
- yield self.render_exception(e)
197
- raise
198
-
199
- def render_exception(self, exception: Exception):
200
- """Inform the client that the stream processing was interrupted with an exception.
201
- The exception can be rendered in the format fits with the output.
202
-
203
- Purposefully, not much information is given, so avoid informing clients.
204
- The actual exception is still raised and logged server-side.
205
- """
206
- if settings.DEBUG:
207
- return f"<!-- {exception.__class__.__name__}: {exception} -->\n"
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}"
208
256
  else:
209
- return f"<!-- {exception.__class__.__name__} during rendering! -->\n"
210
-
211
- def render_stream(self):
212
- """Implement this in subclasses to implement a custom output format."""
213
- raise NotImplementedError()
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
+ }