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
gisserver/output/utils.py CHANGED
@@ -1,179 +1,84 @@
1
- from __future__ import annotations
1
+ """General utilities for outputting XML content"""
2
2
 
3
- from collections import defaultdict
4
- from collections.abc import Iterable
5
- from itertools import islice
6
- from typing import TypeVar
3
+ from datetime import date, datetime, time, timezone
4
+ from decimal import Decimal as D
7
5
 
8
- from django.db import connections, models
9
- from lru import LRU
6
+ from django.core.exceptions import ImproperlyConfigured
10
7
 
11
- M = TypeVar("M", bound=models.Model)
8
+ from gisserver.parsers.xml import xmlns
12
9
 
13
- DEFAULT_SQL_CHUNK_SIZE = 2000 # allow unit tests to alter this.
10
+ AUTO_STR = (int, float, D, date, time)
14
11
 
12
+ COMMON_NAMESPACES = xmlns.as_namespaces()
15
13
 
16
- class CountingIterator(Iterable[M]):
17
- """A simple iterator that counts how many results are given."""
18
14
 
19
- def __init__(self, iterator: Iterable[M], max_results=0):
20
- self._iterator = iterator
21
- self._number_returned = 0
22
- self._in_iterator = False
23
- self._max_results = max_results
24
- self._has_more = None
15
+ __all__ = (
16
+ "attr_escape",
17
+ "tag_escape",
18
+ "to_qname",
19
+ "render_xmlns_attributes",
20
+ "value_to_text",
21
+ "value_to_xml_string",
22
+ )
25
23
 
26
- def __iter__(self):
27
- # Count the number of returned items while reading them.
28
- # Tried using map(itemgetter(0), zip(model_iter, count_iter)) but that's not faster.
29
- self._in_iterator = True
30
- try:
31
- self._number_returned = 0
32
- for instance in self._iterator:
33
- if self._max_results and self._number_returned == self._max_results:
34
- self._has_more = True
35
- break
36
- self._number_returned += 1
37
- yield instance
38
- finally:
39
- if self._max_results and self._has_more is None:
40
- self._has_more = False # ignored the sentinel item
41
- self._in_iterator = False
42
-
43
- @property
44
- def number_returned(self) -> int:
45
- """Tell how many objects the iterator processed"""
46
- if self._in_iterator:
47
- raise RuntimeError("Can't read number of returned results during iteration")
48
- return self._number_returned
49
-
50
- @property
51
- def has_more(self) -> bool | None:
52
- return self._has_more
53
-
54
-
55
- class ChunkedQuerySetIterator(Iterable[M]):
56
- """An optimal strategy to perform ``prefetch_related()`` on large datasets.
57
-
58
- It fetches data from the queryset in chunks,
59
- and performs ``prefetch_related()`` behavior on each chunk.
60
-
61
- Django's ``QuerySet.prefetch_related()`` works by loading the whole queryset into memory,
62
- and performing an analysis of the related objects to fetch. When working on large datasets,
63
- this is very inefficient as more memory is consumed. Instead, ``QuerySet.iterator()``
64
- is preferred here as it returns instances while reading them. Nothing is stored in memory.
65
- Hence, both approaches are fundamentally incompatible. This class performs a
66
- mixed strategy: load a chunk, and perform prefetches for that particular batch.
67
-
68
- As extra performance benefit, a local cache avoids prefetching the same records
69
- again when the next chunk is analysed. It has a "least recently used" cache to avoid
70
- flooding the caches when foreign keys constantly point to different unique objects.
71
- """
72
-
73
- def __init__(self, queryset: models.QuerySet, chunk_size=None, sql_chunk_size=None):
74
- """
75
- :param queryset: The queryset to iterate over, that has ``prefetch_related()`` data.
76
- :param chunk_size: The size of each segment to analyse in-memory for related objects.
77
- :param sql_chunk_size: The size of each segment to fetch from the database,
78
- used when server-side cursors are not available. The default follows Django behavior.
79
- """
80
- self.queryset = queryset
81
- self.sql_chunk_size = sql_chunk_size or DEFAULT_SQL_CHUNK_SIZE
82
- self.chunk_size = chunk_size or self.sql_chunk_size
83
- self._fk_caches = defaultdict(lambda: LRU(self.chunk_size // 2))
84
- self._number_returned = 0
85
- self._in_iterator = False
86
-
87
- def __iter__(self):
88
- # Using iter() ensures the ModelIterable is resumed with the next chunk.
89
- self._number_returned = 0
90
- self._in_iterator = True
24
+
25
+ def tag_escape(s: str):
26
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
27
+
28
+
29
+ def attr_escape(s: str):
30
+ # Slightly faster then html.escape() as it doesn't replace single quotes.
31
+ # Having tried all possible variants, this code still outperforms other forms of escaping.
32
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
33
+
34
+
35
+ def value_to_xml_string(value):
36
+ # Simple scalar value
37
+ if isinstance(value, str): # most cases
38
+ return tag_escape(value)
39
+ elif isinstance(value, datetime):
40
+ return value.astimezone(timezone.utc).isoformat()
41
+ elif isinstance(value, bool):
42
+ return "true" if value else "false"
43
+ elif isinstance(value, AUTO_STR):
44
+ return value # no need for _tag_escape(), and f"{value}" works faster.
45
+ else:
46
+ return tag_escape(str(value))
47
+
48
+
49
+ def value_to_text(value):
50
+ # Simple scalar value, no XML escapes
51
+ if isinstance(value, str): # most cases
52
+ return value
53
+ elif isinstance(value, datetime):
54
+ return value.astimezone(timezone.utc).isoformat()
55
+ elif isinstance(value, bool):
56
+ return "true" if value else "false"
57
+ else:
58
+ return value # f"{value} works faster and produces the right format.
59
+
60
+
61
+ def render_xmlns_attributes(xml_namespaces: dict[str, str]):
62
+ """Render XML Namespace declaration attributes"""
63
+ return " ".join(
64
+ f'xmlns:{prefix}="{xml_namespace}"' if prefix else f'xmlns="{xml_namespace}"'
65
+ for xml_namespace, prefix in xml_namespaces.items()
66
+ )
67
+
68
+
69
+ def to_qname(namespace, localname, namespaces: dict[str, str]) -> str:
70
+ """Convert a fully qualified XML tag name to a prefixed short name."""
71
+ if namespace is None:
72
+ return localname
73
+
74
+ prefix = namespaces.get(namespace) # allow ""
75
+ if prefix is None:
91
76
  try:
92
- qs_iter = iter(self._get_queryset_iterator())
93
-
94
- # Keep fetching chunks
95
- while True:
96
- instances = list(islice(qs_iter, self.chunk_size))
97
- if not instances:
98
- break
99
-
100
- # Perform prefetches on this chunk:
101
- if self.queryset._prefetch_related_lookups:
102
- self._add_prefetches(instances)
103
-
104
- # And return to parent loop
105
- yield from instances
106
- self._number_returned += len(instances)
107
- finally:
108
- self._in_iterator = False
109
-
110
- def _get_queryset_iterator(self) -> Iterable:
111
- """The body of queryset.iterator(), while circumventing prefetching."""
112
- # The old code did return `self.queryset.iterator(chunk_size=self.sql_chunk_size)`
113
- # However, Django 4 supports using prefetch_related() with iterator() in that scenario.
114
- #
115
- # This code is the core of Django's QuerySet.iterator() that only produces the
116
- # old-style iteration, without any prefetches. Those are added by this class instead.
117
- use_chunked_fetch = not connections[self.queryset.db].settings_dict.get(
118
- "DISABLE_SERVER_SIDE_CURSORS"
119
- )
120
- iterable = self.queryset._iterable_class(
121
- self.queryset, chunked_fetch=use_chunked_fetch, chunk_size=self.sql_chunk_size
122
- )
123
-
124
- yield from iterable
125
-
126
- @property
127
- def number_returned(self) -> int:
128
- """Tell how many objects the iterator processed"""
129
- if self._in_iterator:
130
- raise RuntimeError("Can't read number of returned results during iteration")
131
- return self._number_returned
132
-
133
- def _add_prefetches(self, instances: list[M]):
134
- """Merge the prefetched objects for this batch with the model instances."""
135
- if self._fk_caches:
136
- # Make sure prefetch_related_objects() doesn't have
137
- # to fetch items again that infrequently changes.
138
- all_restored = self._restore_caches(instances)
139
- if all_restored:
140
- return
141
-
142
- # Reuse the Django machinery for retrieving missing sub objects.
143
- # and analyse the ForeignKey caches to allow faster prefetches next time
144
- models.prefetch_related_objects(instances, *self.queryset._prefetch_related_lookups)
145
- self._persist_prefetch_cache(instances)
146
-
147
- def _persist_prefetch_cache(self, instances):
148
- """Store the prefetched data so it can be applied to the next batch"""
149
- for instance in instances:
150
- for lookup, obj in instance._state.fields_cache.items():
151
- if obj is not None:
152
- cache = self._fk_caches[lookup]
153
- cache[obj.pk] = obj
154
-
155
- def _restore_caches(self, instances) -> bool:
156
- """Restore prefetched data to the new set of instances.
157
- This avoids unneeded prefetching of the same ForeignKey relation.
158
- """
159
- if not instances:
160
- return True
161
- if not self._fk_caches:
162
- return False
163
-
164
- all_restored = True
165
-
166
- for lookup, cache in self._fk_caches.items():
167
- field = instances[0]._meta.get_field(lookup)
168
- for instance in instances:
169
- id_value = getattr(instance, field.attname)
170
- if id_value is None:
171
- continue
172
-
173
- obj = cache.get(id_value, None)
174
- if obj is not None:
175
- instance._state.fields_cache[lookup] = obj
176
- else:
177
- all_restored = False
178
-
179
- return all_restored
77
+ prefix = COMMON_NAMESPACES[namespace]
78
+ except KeyError:
79
+ raise ImproperlyConfigured(
80
+ f"No XML namespace prefix defined for '{namespace}'.\n"
81
+ "This can be configured in 'WFSView.xml_namespace_aliases'."
82
+ ) from None
83
+
84
+ return f"{prefix}:{localname}" if prefix else localname
@@ -1,47 +1,62 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import typing
3
4
  from collections import deque
4
5
  from collections.abc import Iterable
5
6
  from io import StringIO
6
7
  from typing import cast
7
8
 
8
9
  from gisserver.features import FeatureType
9
- from gisserver.operations.base import WFSMethod
10
- from gisserver.types import XsdComplexType
10
+ from gisserver.parsers.xml import xmlns
11
+ from gisserver.types import XsdComplexType, XsdElement
11
12
 
12
- from .base import OutputRenderer
13
+ from .base import XmlOutputRenderer
14
+ from .utils import to_qname
13
15
 
16
+ if typing.TYPE_CHECKING:
17
+ from gisserver.operations.base import WFSOperation
14
18
 
15
- class XMLSchemaRenderer(OutputRenderer):
19
+
20
+ class XmlSchemaRenderer(XmlOutputRenderer):
16
21
  """Output rendering for DescribeFeatureType.
17
22
 
18
23
  This renders a valid XSD schema that describes the data type of the feature.
19
24
  """
20
25
 
21
26
  content_type = "application/gml+xml; version=3.2" # mandatory for WFS
27
+ xml_namespaces = {
28
+ # Default extra namespaces to include in the xmlns="..." attributes
29
+ "http://www.w3.org/2001/XMLSchema": "", # no xs:string, but "string"
30
+ "http://www.opengis.net/gml/3.2": "gml",
31
+ }
22
32
 
23
- def __init__(self, method: WFSMethod, feature_types: list[FeatureType]):
33
+ def __init__(self, operation: WFSOperation, feature_types: list[FeatureType]):
24
34
  """Overwritten method to handle the DescribeFeatureType context."""
25
- self.server_url = method.view.server_url
26
- self.app_xml_namespace = method.view.xml_namespace
35
+ super().__init__(operation)
27
36
  self.feature_types = feature_types
28
37
 
38
+ # For rendering type="..." fields, avoid xs: prefix.
39
+ self.type_namespaces = self.app_namespaces.copy()
40
+ self.type_namespaces[xmlns.xs.value] = "" # no "xs:string" but "string"
41
+
42
+ def get_headers(self):
43
+ """Make wget output slightly nicer."""
44
+ typenames = "+".join(feature_type.name for feature_type in self.feature_types)
45
+ return {"Content-Disposition": f'inline; filename="{typenames}.xsd"'}
46
+
29
47
  def render_stream(self):
30
- # For now, all features have the same XML namespace despite allowing
31
- # the prefixes to be different.
32
- xmlns_features = "\n ".join(
33
- f'xmlns:{p}="{self.app_xml_namespace}"'
34
- for p in sorted({feature_type.xml_prefix for feature_type in self.feature_types})
35
- )
48
+ # Render first with original app_namespaces
49
+ xmlns_attrib = self.render_xmlns_attributes()
50
+
51
+ # Allow targetNamespace to be unprefixed by self.node_to_qname().
52
+ target_namespace = self.feature_types[0].xml_namespace
53
+ self.app_namespaces[target_namespace] = "" # no "app:element" but "element"
36
54
 
37
- output = StringIO()
55
+ self.output = output = StringIO()
38
56
  output.write(
39
57
  f"""<?xml version='1.0' encoding="UTF-8" ?>
40
- <schema
41
- xmlns="http://www.w3.org/2001/XMLSchema"
42
- {xmlns_features}
43
- xmlns:gml="http://www.opengis.net/gml/3.2"
44
- targetNamespace="{self.app_xml_namespace}"
58
+ <schema {xmlns_attrib}
59
+ targetNamespace="{target_namespace}"
45
60
  elementFormDefault="qualified" version="0.1">
46
61
 
47
62
  """
@@ -50,7 +65,7 @@ class XMLSchemaRenderer(OutputRenderer):
50
65
  output.write("\n")
51
66
 
52
67
  for feature_type in self.feature_types:
53
- output.write(self.render_feature_type(feature_type))
68
+ self.write_feature_type(feature_type)
54
69
 
55
70
  output.write("</schema>\n")
56
71
  return output.getvalue()
@@ -61,45 +76,69 @@ class XMLSchemaRenderer(OutputRenderer):
61
76
  ' schemaLocation="http://schemas.opengis.net/gml/3.2.1/gml.xsd" />\n'
62
77
  )
63
78
 
64
- def render_feature_type(self, feature_type: FeatureType):
65
- output = StringIO()
79
+ def write_feature_type(self, feature_type: FeatureType):
66
80
  xsd_type: XsdComplexType = feature_type.xsd_type
67
81
 
68
82
  # This declares the that a top-level <app:featureName> is a class of a type.
69
- output.write(
70
- f' <element name="{feature_type.name}"'
71
- f' type="{xsd_type}" substitutionGroup="gml:AbstractFeature" />\n\n'
83
+ xsd_type_qname = self.to_qname(xsd_type, self.type_namespaces)
84
+ feature_qname = to_qname(
85
+ feature_type.xml_namespace, feature_type.name, self.app_namespaces
86
+ )
87
+ self.output.write(
88
+ f' <element name="{feature_qname}"'
89
+ f' type="{xsd_type_qname}" substitutionGroup="gml:AbstractFeature" />\n\n'
72
90
  )
73
91
 
74
92
  # Next, the complexType is rendered that defines the element contents.
75
93
  # Next, the complexType(s) are rendered that defines the element contents.
76
94
  # In case any fields are expanded (hence become subtypes), these are also included.
77
- output.write(self.render_complex_type(xsd_type))
95
+ self.write_complex_type(xsd_type)
78
96
  for complex_type in self._get_complex_types(xsd_type):
79
- output.write(self.render_complex_type(complex_type))
80
-
81
- return output.getvalue()
97
+ self.write_complex_type(complex_type)
82
98
 
83
- def render_complex_type(self, complex_type: XsdComplexType):
99
+ def write_complex_type(self, complex_type: XsdComplexType):
84
100
  """Write the definition of a single class."""
85
- output = StringIO()
86
- output.write(
87
- f' <complexType name="{complex_type.name}">\n'
88
- " <complexContent>\n"
89
- f' <extension base="{complex_type.base}">\n'
90
- " <sequence>\n"
91
- )
101
+ complex_qname = self.to_qname(complex_type)
102
+ self.output.write(f' <complexType name="{complex_qname}">\n')
92
103
 
93
- for xsd_element in complex_type.elements:
94
- output.write(f" {xsd_element}\n")
104
+ if complex_type.base is not None:
105
+ base_qname = self.to_qname(complex_type.base)
106
+ self.output.write(
107
+ f" <complexContent>\n" # extend base class
108
+ f' <extension base="{base_qname}">\n'
109
+ )
110
+ indent = " "
111
+ else:
112
+ indent = ""
95
113
 
96
- output.write(
97
- " </sequence>\n"
98
- " </extension>\n"
99
- " </complexContent>\n"
100
- " </complexType>\n\n"
101
- )
102
- return output.getvalue()
114
+ self.output.write(f" {indent}<sequence>\n")
115
+
116
+ for xsd_element in complex_type.elements:
117
+ self.output.write(f" {indent}{self.render_element(xsd_element)}\n")
118
+
119
+ self.output.write(f" {indent}</sequence>\n")
120
+ if complex_type.base is not None:
121
+ self.output.write(
122
+ " </extension>\n" # end extension
123
+ " </complexContent>\n"
124
+ )
125
+ self.output.write(" </complexType>\n\n")
126
+
127
+ def render_element(self, xsd_element: XsdElement):
128
+ """Staticmethod for unit testing."""
129
+ qname = self.to_qname(xsd_element)
130
+ type_qname = self.to_qname(xsd_element.type, self.type_namespaces)
131
+
132
+ attributes = [f'name="{qname}" type="{type_qname}"']
133
+ if xsd_element.min_occurs is not None:
134
+ attributes.append(f'minOccurs="{xsd_element.min_occurs}"')
135
+ if xsd_element.max_occurs is not None:
136
+ attributes.append(f'maxOccurs="{xsd_element.max_occurs}"')
137
+ if xsd_element.nillable:
138
+ str_bool = "true" if xsd_element.nillable else "false"
139
+ attributes.append(f'nillable="{str_bool}"')
140
+
141
+ return "<element {} />".format(" ".join(attributes))
103
142
 
104
143
  def _get_complex_types(self, root: XsdComplexType) -> Iterable[XsdComplexType]:
105
144
  """Find all fields that reference to complex types, including nested elements."""
@@ -1,13 +1,13 @@
1
- """All parser logic to process <fes:...> and <gml:...> tags."""
1
+ """All parser logic to process incoming XML data.
2
2
 
3
- from .fes20 import Filter
4
- from .fes20 import function_registry as fes_function_registry
5
- from .gml import parse_gml
3
+ This handles all tags, including:
6
4
 
7
- parse_fes = Filter.from_string
5
+ * ``<wfs:...>``
6
+ * ``<fes:...>``
7
+ * ``<gml:...>``
8
8
 
9
- __all__ = [
10
- "parse_fes",
11
- "parse_gml",
12
- "fes_function_registry",
13
- ]
9
+ Internally, the XML string is translated into an Abstract Syntax Tree (AST).
10
+ These objects contain to logic to process each bit of the XML tree.
11
+ The GET request parameters are translated into that same tree structure,
12
+ to have a uniform processing of the request.
13
+ """