django-gisserver 1.3.0__py3-none-any.whl → 1.4.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 (41) hide show
  1. {django_gisserver-1.3.0.dist-info → django_gisserver-1.4.0.dist-info}/METADATA +12 -15
  2. django_gisserver-1.4.0.dist-info/RECORD +54 -0
  3. {django_gisserver-1.3.0.dist-info → django_gisserver-1.4.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/conf.py +9 -12
  6. gisserver/db.py +6 -10
  7. gisserver/exceptions.py +1 -0
  8. gisserver/features.py +18 -29
  9. gisserver/geometries.py +11 -25
  10. gisserver/operations/base.py +19 -40
  11. gisserver/operations/wfs20.py +8 -20
  12. gisserver/output/__init__.py +7 -2
  13. gisserver/output/base.py +4 -13
  14. gisserver/output/csv.py +13 -16
  15. gisserver/output/geojson.py +14 -17
  16. gisserver/output/gml32.py +309 -281
  17. gisserver/output/gml32_lxml.py +612 -0
  18. gisserver/output/results.py +105 -22
  19. gisserver/output/utils.py +15 -5
  20. gisserver/output/xmlschema.py +7 -8
  21. gisserver/parsers/base.py +2 -4
  22. gisserver/parsers/fes20/expressions.py +6 -13
  23. gisserver/parsers/fes20/filters.py +6 -5
  24. gisserver/parsers/fes20/functions.py +4 -4
  25. gisserver/parsers/fes20/identifiers.py +1 -0
  26. gisserver/parsers/fes20/operators.py +16 -43
  27. gisserver/parsers/fes20/query.py +1 -3
  28. gisserver/parsers/fes20/sorting.py +1 -3
  29. gisserver/parsers/gml/__init__.py +1 -0
  30. gisserver/parsers/gml/base.py +1 -0
  31. gisserver/parsers/values.py +1 -3
  32. gisserver/queries/__init__.py +1 -0
  33. gisserver/queries/adhoc.py +3 -6
  34. gisserver/queries/base.py +2 -6
  35. gisserver/queries/stored.py +3 -6
  36. gisserver/types.py +59 -46
  37. gisserver/views.py +7 -10
  38. django_gisserver-1.3.0.dist-info/RECORD +0 -54
  39. gisserver/output/buffer.py +0 -64
  40. {django_gisserver-1.3.0.dist-info → django_gisserver-1.4.0.dist-info}/LICENSE +0 -0
  41. {django_gisserver-1.3.0.dist-info → django_gisserver-1.4.0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ Useful docs:
7
7
  * https://mapserver.org/development/rfc/ms-rfc-105.html
8
8
  * https://enonline.supermap.com/iExpress9D/API/WFS/WFS200/WFS_2.0.0_introduction.htm
9
9
  """
10
+
10
11
  import logging
11
12
  import math
12
13
  import re
@@ -46,12 +47,7 @@ class GetCapabilities(WFSMethod):
46
47
  """ "This operation returns map features, and available operations this WFS server supports."""
47
48
 
48
49
  output_formats = [
49
- OutputFormat(
50
- "application/gml+xml",
51
- version="3.2",
52
- renderer_class=output.gml32_value_renderer,
53
- title="GML",
54
- ),
50
+ OutputFormat("application/gml+xml", version="3.2", title="GML"), # for FME
55
51
  OutputFormat("text/xml"),
56
52
  ]
57
53
  xml_template_name = "get_capabilities.xml"
@@ -94,9 +90,7 @@ class GetCapabilities(WFSMethod):
94
90
  "AcceptVersions", "Can't provide both ACCEPTVERSIONS and VERSION"
95
91
  )
96
92
 
97
- matched_versions = set(accept_versions.split(",")).intersection(
98
- self.view.accept_versions
99
- )
93
+ matched_versions = set(accept_versions.split(",")).intersection(self.view.accept_versions)
100
94
  if not matched_versions:
101
95
  allowed = ", ".join(self.view.accept_versions)
102
96
  raise VersionNegotiationFailed(
@@ -156,7 +150,7 @@ class DescribeFeatureType(WFSTypeNamesMethod):
156
150
  "application/gml+xml",
157
151
  version="3.2",
158
152
  renderer_class=output.XMLSchemaRenderer,
159
- )
153
+ ),
160
154
  # OutputFormat("text/xml", subtype="gml/3.1.1"),
161
155
  ]
162
156
 
@@ -279,9 +273,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
279
273
  raise
280
274
  logger.exception("WFS request failed: %s\nParams: %r", str(e), params)
281
275
  msg = str(e)
282
- locator = (
283
- "srsName" if "Cannot find SRID" in msg else self._get_locator(**params)
284
- )
276
+ locator = "srsName" if "Cannot find SRID" in msg else self._get_locator(**params)
285
277
  raise InvalidParameterValue(locator, f"Invalid request: {msg}") from e
286
278
  except (TypeError, ValueError) as e:
287
279
  # TypeError/ValueError could reference a datatype mismatch in an
@@ -352,9 +344,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
352
344
  if not output_crs and collection.results:
353
345
  output_crs = collection.results[0].feature_type.crs
354
346
 
355
- outputFormat.renderer_class.decorate_collection(
356
- collection, output_crs, **params
357
- )
347
+ outputFormat.renderer_class.decorate_collection(collection, output_crs, **params)
358
348
 
359
349
  if stop != math.inf:
360
350
  if start > 0:
@@ -362,7 +352,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
362
352
  STARTINDEX=max(0, start - page_size),
363
353
  COUNT=page_size,
364
354
  )
365
- if stop < collection.number_matched:
355
+ if collection.has_next:
366
356
  # TODO: fix this when returning multiple typeNames:
367
357
  collection.next = self._replace_url_params(
368
358
  STARTINDEX=start + page_size,
@@ -479,9 +469,7 @@ class GetPropertyValue(BaseWFSGetDataMethod):
479
469
  renderer_class=output.gml32_value_renderer,
480
470
  title="GML",
481
471
  ),
482
- OutputFormat(
483
- "text/xml", subtype="gml/3.2", renderer_class=output.gml32_value_renderer
484
- ),
472
+ OutputFormat("text/xml", subtype="gml/3.2", renderer_class=output.gml32_value_renderer),
485
473
  ]
486
474
 
487
475
  parameters = BaseWFSGetDataMethod.parameters + [
@@ -1,4 +1,7 @@
1
1
  """All supported output formats"""
2
+
3
+ from django.utils.functional import classproperty
4
+
2
5
  from gisserver import conf
3
6
 
4
7
  from .base import OutputRenderer
@@ -13,8 +16,6 @@ from .gml32 import (
13
16
  from .results import FeatureCollection, SimpleFeatureCollection
14
17
  from .xmlschema import XMLSchemaRenderer
15
18
 
16
- from django.utils.functional import classproperty
17
-
18
19
  __all__ = [
19
20
  "OutputRenderer",
20
21
  "FeatureCollection",
@@ -40,6 +41,10 @@ def select_renderer(native_renderer_class, db_renderer_class):
40
41
  """
41
42
 
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
+
43
48
  @classproperty
44
49
  def real_class(self):
45
50
  if conf.GISSERVER_USE_DB_RENDERING:
gisserver/output/base.py CHANGED
@@ -59,9 +59,7 @@ class OutputRenderer:
59
59
  self.app_xml_namespace = method.view.xml_namespace
60
60
 
61
61
  @classmethod
62
- def decorate_collection(
63
- cls, collection: FeatureCollection, output_crs: CRS, **params
64
- ):
62
+ def decorate_collection(cls, collection: FeatureCollection, output_crs: CRS, **params):
65
63
  """Perform presentation-layer logic enhancements on the queryset."""
66
64
  for sub_collection in collection.results:
67
65
  # Validate the presentation-level parameters for this feature:
@@ -125,9 +123,7 @@ class OutputRenderer:
125
123
  return [
126
124
  models.Prefetch(
127
125
  orm_relation.orm_path,
128
- queryset=cls.get_prefetch_queryset(
129
- feature_type, orm_relation, output_crs
130
- ),
126
+ queryset=cls.get_prefetch_queryset(feature_type, orm_relation, output_crs),
131
127
  )
132
128
  for orm_relation in feature_type.orm_relations
133
129
  ]
@@ -173,10 +169,7 @@ class OutputRenderer:
173
169
  # Offer a common quick content-disposition logic that works for all possible queries.
174
170
  sub_collection = self.collection.results[0]
175
171
  if sub_collection.stop == math.inf:
176
- if sub_collection.start:
177
- page = f"{sub_collection.start}-end"
178
- else:
179
- page = "all"
172
+ page = f"{sub_collection.start}-end" if sub_collection.start else "all"
180
173
  elif sub_collection.stop:
181
174
  page = f"{sub_collection.start}-{sub_collection.stop - 1}"
182
175
  else:
@@ -184,9 +177,7 @@ class OutputRenderer:
184
177
 
185
178
  return {
186
179
  "Content-Disposition": self.content_disposition.format(
187
- typenames="+".join(
188
- sub.feature_type.name for sub in self.collection.results
189
- ),
180
+ typenames="+".join(sub.feature_type.name for sub in self.collection.results),
190
181
  page=page,
191
182
  date=self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
192
183
  timestamp=self.collection.timestamp,
gisserver/output/csv.py CHANGED
@@ -1,8 +1,10 @@
1
1
  """Output rendering logic for GeoJSON."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import csv
5
6
  from datetime import datetime, timezone
7
+ from io import StringIO
6
8
 
7
9
  from django.conf import settings
8
10
  from django.db import models
@@ -19,7 +21,6 @@ from gisserver.geometries import CRS
19
21
  from gisserver.types import XsdComplexType, XsdElement
20
22
 
21
23
  from .base import OutputRenderer
22
- from .buffer import StringBuffer
23
24
 
24
25
 
25
26
  class CSVRenderer(OutputRenderer):
@@ -31,6 +32,7 @@ class CSVRenderer(OutputRenderer):
31
32
  content_type = "text/csv; charset=utf-8"
32
33
  content_disposition = 'attachment; filename="{typenames} {page} {date}.csv"'
33
34
  max_page_size = conf.GISSERVER_CSV_MAX_PAGE_SIZE
35
+ chunk_size = 40_000
34
36
 
35
37
  #: The outputted CSV dialect. This can be a csv.Dialect subclass
36
38
  #: or one of the registered names like: "unix", "excel", "excel-tab"
@@ -66,7 +68,7 @@ class CSVRenderer(OutputRenderer):
66
68
  return queryset
67
69
 
68
70
  def render_stream(self):
69
- output = StringBuffer()
71
+ output = StringIO()
70
72
  writer = csv.writer(output, dialect=self.dialect)
71
73
 
72
74
  is_first_collection = True
@@ -78,11 +80,7 @@ class CSVRenderer(OutputRenderer):
78
80
  output.write("\n\n")
79
81
 
80
82
  # Write the header
81
- fields = [
82
- f
83
- for f in sub_collection.feature_type.xsd_type.elements
84
- if not f.is_many
85
- ]
83
+ fields = [f for f in sub_collection.feature_type.xsd_type.elements if not f.is_many]
86
84
  writer.writerow(self.get_header(fields))
87
85
 
88
86
  # By using .iterator(), the results are streamed with as little memory as
@@ -93,10 +91,13 @@ class CSVRenderer(OutputRenderer):
93
91
 
94
92
  # Only perform a 'yield' every once in a while,
95
93
  # as it goes back-and-forth for writing it to the client.
96
- if output.is_full():
97
- yield output.flush()
94
+ if output.tell() > self.chunk_size:
95
+ csv_chunk = output.getvalue()
96
+ output.seek(0)
97
+ output.truncate(0)
98
+ yield csv_chunk
98
99
 
99
- yield output.flush()
100
+ yield output.getvalue()
100
101
 
101
102
  def render_exception(self, exception: Exception):
102
103
  """Render the exception in a format that fits with the output."""
@@ -159,15 +160,11 @@ class DBCSVRenderer(CSVRenderer):
159
160
  output_crs: CRS,
160
161
  **params,
161
162
  ) -> models.QuerySet:
162
- queryset = super().decorate_queryset(
163
- feature_type, queryset, output_crs, **params
164
- )
163
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
165
164
 
166
165
  # Instead of reading the binary geometry data,
167
166
  # ask the database to generate EWKT data directly.
168
- geo_selects = get_db_geometry_selects(
169
- feature_type.xsd_type.geometry_elements, output_crs
170
- )
167
+ geo_selects = get_db_geometry_selects(feature_type.xsd_type.geometry_elements, output_crs)
171
168
  if geo_selects:
172
169
  queryset = queryset.defer(*geo_selects.keys()).annotate(
173
170
  **build_db_annotations(geo_selects, "_as_ewkt_{name}", AsEWKT)
@@ -1,6 +1,8 @@
1
1
  """Output rendering logic for GeoJSON."""
2
+
2
3
  from datetime import datetime, timezone
3
4
  from decimal import Decimal
5
+ from io import BytesIO
4
6
  from typing import cast
5
7
 
6
8
  import orjson
@@ -16,7 +18,6 @@ from gisserver.geometries import CRS
16
18
  from gisserver.types import XsdComplexType
17
19
 
18
20
  from .base import OutputRenderer
19
- from .buffer import BytesBuffer
20
21
 
21
22
 
22
23
  def _json_default(obj):
@@ -40,6 +41,7 @@ class GeoJsonRenderer(OutputRenderer):
40
41
  content_type = "application/geo+json; charset=utf-8"
41
42
  content_disposition = 'inline; filename="{typenames} {page} {date}.geojson"'
42
43
  max_page_size = conf.GISSERVER_GEOJSON_MAX_PAGE_SIZE
44
+ chunk_size = 40_000
43
45
 
44
46
  @classmethod
45
47
  def decorate_queryset(
@@ -49,9 +51,7 @@ class GeoJsonRenderer(OutputRenderer):
49
51
  output_crs: CRS,
50
52
  **params,
51
53
  ):
52
- queryset = super().decorate_queryset(
53
- feature_type, queryset, output_crs, **params
54
- )
54
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
55
55
 
56
56
  # Other geometries can be excluded as these are not rendered by 'properties'
57
57
  other_geometries = [
@@ -65,7 +65,7 @@ class GeoJsonRenderer(OutputRenderer):
65
65
  return queryset
66
66
 
67
67
  def render_stream(self):
68
- output = BytesBuffer()
68
+ output = BytesIO()
69
69
 
70
70
  # Generate the header from a Python dict,
71
71
  # but replace the last "}" into a comma, to allow writing more
@@ -96,8 +96,11 @@ class GeoJsonRenderer(OutputRenderer):
96
96
 
97
97
  # Only perform a 'yield' every once in a while,
98
98
  # as it goes back-and-forth for writing it to the client.
99
- if output.is_full():
100
- yield output.flush()
99
+ if output.tell() > self.chunk_size:
100
+ json_chunk = output.getvalue()
101
+ output.seek(0)
102
+ output.truncate(0)
103
+ yield json_chunk
101
104
 
102
105
  # Instead of performing an expensive .count() on the start of the page,
103
106
  # write this as a last field at the end of the response.
@@ -106,7 +109,7 @@ class GeoJsonRenderer(OutputRenderer):
106
109
  footer = self.get_footer()
107
110
  output.write(orjson.dumps(footer)[1:])
108
111
  output.write(b"\n")
109
- yield output.flush()
112
+ yield output.getvalue()
110
113
 
111
114
  def render_exception(self, exception: Exception):
112
115
  """Render the exception in a format that fits with the output."""
@@ -115,9 +118,7 @@ class GeoJsonRenderer(OutputRenderer):
115
118
  else:
116
119
  return f"/* {exception.__class__.__name__} during rendering! */\n"
117
120
 
118
- def render_feature(
119
- self, feature_type: FeatureType, instance: models.Model
120
- ) -> bytes:
121
+ def render_feature(self, feature_type: FeatureType, instance: models.Model) -> bytes:
121
122
  """Render the output of a single feature"""
122
123
 
123
124
  # Get all instance attributes:
@@ -266,16 +267,12 @@ class DBGeoJsonRenderer(GeoJsonRenderer):
266
267
  """
267
268
 
268
269
  @classmethod
269
- def decorate_queryset(
270
- self, feature_type: FeatureType, queryset, output_crs, **params
271
- ):
270
+ def decorate_queryset(self, feature_type: FeatureType, queryset, output_crs, **params):
272
271
  """Update the queryset to let the database render the GML output.
273
272
  This is far more efficient then GeoDjango's logic, which performs a
274
273
  C-API call for every single coordinate of a geometry.
275
274
  """
276
- queryset = super().decorate_queryset(
277
- feature_type, queryset, output_crs, **params
278
- )
275
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
279
276
  # If desired, the entire FeatureCollection could be rendered
280
277
  # in PostgreSQL as well: https://postgis.net/docs/ST_AsGeoJSON.html
281
278
  if feature_type.geometry_fields: