django-gisserver 1.2.7__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.2.7.dist-info → django_gisserver-1.4.0.dist-info}/METADATA +12 -12
  2. django_gisserver-1.4.0.dist-info/RECORD +54 -0
  3. {django_gisserver-1.2.7.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 +15 -19
  15. gisserver/output/geojson.py +16 -20
  16. gisserver/output/gml32.py +310 -283
  17. gisserver/output/gml32_lxml.py +612 -0
  18. gisserver/output/results.py +107 -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.2.7.dist-info/RECORD +0 -54
  39. gisserver/output/buffer.py +0 -64
  40. {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/LICENSE +0 -0
  41. {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/top_level.txt +0 -0
@@ -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,12 +1,13 @@
1
1
  """Output rendering logic for GeoJSON."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import csv
5
- from datetime import datetime
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
9
- from django.utils.timezone import utc
10
11
 
11
12
  from gisserver import conf
12
13
  from gisserver.db import (
@@ -20,7 +21,6 @@ from gisserver.geometries import CRS
20
21
  from gisserver.types import XsdComplexType, XsdElement
21
22
 
22
23
  from .base import OutputRenderer
23
- from .buffer import StringBuffer
24
24
 
25
25
 
26
26
  class CSVRenderer(OutputRenderer):
@@ -32,6 +32,7 @@ class CSVRenderer(OutputRenderer):
32
32
  content_type = "text/csv; charset=utf-8"
33
33
  content_disposition = 'attachment; filename="{typenames} {page} {date}.csv"'
34
34
  max_page_size = conf.GISSERVER_CSV_MAX_PAGE_SIZE
35
+ chunk_size = 40_000
35
36
 
36
37
  #: The outputted CSV dialect. This can be a csv.Dialect subclass
37
38
  #: or one of the registered names like: "unix", "excel", "excel-tab"
@@ -67,7 +68,7 @@ class CSVRenderer(OutputRenderer):
67
68
  return queryset
68
69
 
69
70
  def render_stream(self):
70
- output = StringBuffer()
71
+ output = StringIO()
71
72
  writer = csv.writer(output, dialect=self.dialect)
72
73
 
73
74
  is_first_collection = True
@@ -79,11 +80,7 @@ class CSVRenderer(OutputRenderer):
79
80
  output.write("\n\n")
80
81
 
81
82
  # Write the header
82
- fields = [
83
- f
84
- for f in sub_collection.feature_type.xsd_type.elements
85
- if not f.is_many
86
- ]
83
+ fields = [f for f in sub_collection.feature_type.xsd_type.elements if not f.is_many]
87
84
  writer.writerow(self.get_header(fields))
88
85
 
89
86
  # By using .iterator(), the results are streamed with as little memory as
@@ -94,10 +91,13 @@ class CSVRenderer(OutputRenderer):
94
91
 
95
92
  # Only perform a 'yield' every once in a while,
96
93
  # as it goes back-and-forth for writing it to the client.
97
- if output.is_full():
98
- 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
99
99
 
100
- yield output.flush()
100
+ yield output.getvalue()
101
101
 
102
102
  def render_exception(self, exception: Exception):
103
103
  """Render the exception in a format that fits with the output."""
@@ -137,7 +137,7 @@ class CSVRenderer(OutputRenderer):
137
137
  # Array field
138
138
  append(",".join(map(str, value)))
139
139
  elif isinstance(value, datetime):
140
- append(str(value.astimezone(utc)))
140
+ append(str(value.astimezone(timezone.utc)))
141
141
  else:
142
142
  append(value)
143
143
  return values
@@ -160,15 +160,11 @@ class DBCSVRenderer(CSVRenderer):
160
160
  output_crs: CRS,
161
161
  **params,
162
162
  ) -> models.QuerySet:
163
- queryset = super().decorate_queryset(
164
- feature_type, queryset, output_crs, **params
165
- )
163
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
166
164
 
167
165
  # Instead of reading the binary geometry data,
168
166
  # ask the database to generate EWKT data directly.
169
- geo_selects = get_db_geometry_selects(
170
- feature_type.xsd_type.geometry_elements, output_crs
171
- )
167
+ geo_selects = get_db_geometry_selects(feature_type.xsd_type.geometry_elements, output_crs)
172
168
  if geo_selects:
173
169
  queryset = queryset.defer(*geo_selects.keys()).annotate(
174
170
  **build_db_annotations(geo_selects, "_as_ewkt_{name}", AsEWKT)
@@ -1,6 +1,8 @@
1
1
  """Output rendering logic for GeoJSON."""
2
- from datetime import datetime
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
@@ -8,7 +10,6 @@ from django.conf import settings
8
10
  from django.contrib.gis.db.models.functions import AsGeoJSON
9
11
  from django.db import models
10
12
  from django.utils.functional import Promise
11
- from django.utils.timezone import utc
12
13
 
13
14
  from gisserver import conf
14
15
  from gisserver.db import get_db_geometry_target
@@ -17,7 +18,6 @@ from gisserver.geometries import CRS
17
18
  from gisserver.types import XsdComplexType
18
19
 
19
20
  from .base import OutputRenderer
20
- from .buffer import BytesBuffer
21
21
 
22
22
 
23
23
  def _json_default(obj):
@@ -41,6 +41,7 @@ class GeoJsonRenderer(OutputRenderer):
41
41
  content_type = "application/geo+json; charset=utf-8"
42
42
  content_disposition = 'inline; filename="{typenames} {page} {date}.geojson"'
43
43
  max_page_size = conf.GISSERVER_GEOJSON_MAX_PAGE_SIZE
44
+ chunk_size = 40_000
44
45
 
45
46
  @classmethod
46
47
  def decorate_queryset(
@@ -50,9 +51,7 @@ class GeoJsonRenderer(OutputRenderer):
50
51
  output_crs: CRS,
51
52
  **params,
52
53
  ):
53
- queryset = super().decorate_queryset(
54
- feature_type, queryset, output_crs, **params
55
- )
54
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
56
55
 
57
56
  # Other geometries can be excluded as these are not rendered by 'properties'
58
57
  other_geometries = [
@@ -66,7 +65,7 @@ class GeoJsonRenderer(OutputRenderer):
66
65
  return queryset
67
66
 
68
67
  def render_stream(self):
69
- output = BytesBuffer()
68
+ output = BytesIO()
70
69
 
71
70
  # Generate the header from a Python dict,
72
71
  # but replace the last "}" into a comma, to allow writing more
@@ -97,8 +96,11 @@ class GeoJsonRenderer(OutputRenderer):
97
96
 
98
97
  # Only perform a 'yield' every once in a while,
99
98
  # as it goes back-and-forth for writing it to the client.
100
- if output.is_full():
101
- 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
102
104
 
103
105
  # Instead of performing an expensive .count() on the start of the page,
104
106
  # write this as a last field at the end of the response.
@@ -107,7 +109,7 @@ class GeoJsonRenderer(OutputRenderer):
107
109
  footer = self.get_footer()
108
110
  output.write(orjson.dumps(footer)[1:])
109
111
  output.write(b"\n")
110
- yield output.flush()
112
+ yield output.getvalue()
111
113
 
112
114
  def render_exception(self, exception: Exception):
113
115
  """Render the exception in a format that fits with the output."""
@@ -116,9 +118,7 @@ class GeoJsonRenderer(OutputRenderer):
116
118
  else:
117
119
  return f"/* {exception.__class__.__name__} during rendering! */\n"
118
120
 
119
- def render_feature(
120
- self, feature_type: FeatureType, instance: models.Model
121
- ) -> bytes:
121
+ def render_feature(self, feature_type: FeatureType, instance: models.Model) -> bytes:
122
122
  """Render the output of a single feature"""
123
123
 
124
124
  # Get all instance attributes:
@@ -147,7 +147,7 @@ class GeoJsonRenderer(OutputRenderer):
147
147
 
148
148
  def _format_geojson_value(self, value):
149
149
  if isinstance(value, datetime):
150
- return value.astimezone(utc)
150
+ return value.astimezone(timezone.utc)
151
151
  elif isinstance(value, models.Model):
152
152
  # ForeignKey, not defined as complex type.
153
153
  return str(value)
@@ -267,16 +267,12 @@ class DBGeoJsonRenderer(GeoJsonRenderer):
267
267
  """
268
268
 
269
269
  @classmethod
270
- def decorate_queryset(
271
- self, feature_type: FeatureType, queryset, output_crs, **params
272
- ):
270
+ def decorate_queryset(self, feature_type: FeatureType, queryset, output_crs, **params):
273
271
  """Update the queryset to let the database render the GML output.
274
272
  This is far more efficient then GeoDjango's logic, which performs a
275
273
  C-API call for every single coordinate of a geometry.
276
274
  """
277
- queryset = super().decorate_queryset(
278
- feature_type, queryset, output_crs, **params
279
- )
275
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
280
276
  # If desired, the entire FeatureCollection could be rendered
281
277
  # in PostgreSQL as well: https://postgis.net/docs/ST_AsGeoJSON.html
282
278
  if feature_type.geometry_fields: