django-gisserver 1.3.0__tar.gz → 1.4.0__tar.gz

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 (66) hide show
  1. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/CHANGES.md +12 -3
  2. {django-gisserver-1.3.0/django_gisserver.egg-info → django-gisserver-1.4.0}/PKG-INFO +2 -5
  3. {django-gisserver-1.3.0 → django-gisserver-1.4.0/django_gisserver.egg-info}/PKG-INFO +2 -5
  4. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/django_gisserver.egg-info/SOURCES.txt +2 -2
  5. django-gisserver-1.4.0/gisserver/__init__.py +1 -0
  6. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/conf.py +9 -12
  7. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/db.py +6 -10
  8. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/exceptions.py +1 -0
  9. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/features.py +18 -29
  10. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/geometries.py +11 -25
  11. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/operations/base.py +19 -40
  12. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/operations/wfs20.py +8 -20
  13. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/__init__.py +7 -2
  14. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/base.py +4 -13
  15. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/csv.py +13 -16
  16. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/geojson.py +14 -17
  17. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/gml32.py +309 -281
  18. django-gisserver-1.4.0/gisserver/output/gml32_lxml.py +612 -0
  19. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/results.py +105 -22
  20. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/utils.py +15 -5
  21. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/output/xmlschema.py +7 -8
  22. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/base.py +2 -4
  23. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/expressions.py +6 -13
  24. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/filters.py +6 -5
  25. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/functions.py +4 -4
  26. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/identifiers.py +1 -0
  27. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/operators.py +16 -43
  28. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/query.py +1 -3
  29. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/sorting.py +1 -3
  30. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/gml/__init__.py +1 -0
  31. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/gml/base.py +1 -0
  32. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/values.py +1 -3
  33. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/queries/__init__.py +1 -0
  34. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/queries/adhoc.py +3 -6
  35. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/queries/base.py +2 -6
  36. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/queries/stored.py +3 -6
  37. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/types.py +59 -46
  38. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/views.py +7 -10
  39. django-gisserver-1.4.0/pyproject.toml +96 -0
  40. django-gisserver-1.4.0/setup.cfg +4 -0
  41. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/setup.py +1 -1
  42. django-gisserver-1.3.0/gisserver/__init__.py +0 -1
  43. django-gisserver-1.3.0/gisserver/output/buffer.py +0 -64
  44. django-gisserver-1.3.0/setup.cfg +0 -39
  45. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/LICENSE +0 -0
  46. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/MANIFEST.in +0 -0
  47. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/README.md +0 -0
  48. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/django_gisserver.egg-info/dependency_links.txt +0 -0
  49. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/django_gisserver.egg-info/not-zip-safe +0 -0
  50. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/django_gisserver.egg-info/requires.txt +0 -0
  51. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/django_gisserver.egg-info/top_level.txt +0 -0
  52. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/operations/__init__.py +0 -0
  53. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/__init__.py +0 -0
  54. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/fes20/__init__.py +0 -0
  55. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/gml/geometries.py +0 -0
  56. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/parsers/tags.py +0 -0
  57. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/static/gisserver/index.css +0 -0
  58. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/index.html +0 -0
  59. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/service_description.html +0 -0
  60. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -0
  61. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
  62. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -0
  63. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
  64. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/feature_type.html +0 -0
  65. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templatetags/__init__.py +0 -0
  66. {django-gisserver-1.3.0 → django-gisserver-1.4.0}/gisserver/templatetags/gisserver_tags.py +0 -0
@@ -1,6 +1,15 @@
1
+ # 2024-07-01 (1.4.0)
2
+
3
+ * Added `GISSERVER_COUNT_NUMBER_MATCHED` setting to allow disabling "numberReturned" counting.
4
+ This will avoid an expensive PostgreSQL COUNT query, speeding up returning large sets.
5
+ * Added basic support for array fields in `GetPropertyValue`.
6
+ * Optimized GML rendering performance (around 15-20% faster on large datasets).
7
+ * Optimized overall rendering performance, which improved GeoJSON/CSV output too.
8
+ * Developers: updated pre-commit hooks
9
+ * Cleaned up leftover Python 3.7 compat code.
10
+
1
11
  # 2023-06-08 (1.3.0)
2
12
 
3
- ## Updates
4
13
  * Django 5 support added.
5
14
 
6
15
  ## Contributors
@@ -8,7 +17,7 @@ We would like to thank the following contributors
8
17
  for their work on this release.
9
18
 
10
19
  - [tomdtp](https://github.com/tomdtp)
11
-
20
+
12
21
  # 2023-06-08 (1.2.7)
13
22
 
14
23
  * WFS endpoints now accept a GML version number in their OUTPUTFORMAT.
@@ -42,7 +51,7 @@ for their work on this release.
42
51
  # 2022-04-13 (1.2.1)
43
52
 
44
53
  * Fixed regression for auto-correcting xmlns for `<Filter>` tags that have leading whitespace.
45
- * Fixed weird crashes when geometry field is not provided.
54
+ * Fixed weird crashes when geometry field is not provided.
46
55
  * Simplify `FeatureType.geometry_field` logic.
47
56
 
48
57
 
@@ -1,22 +1,21 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
7
7
  Author-email: opensource@edoburu.nl
8
8
  License: Mozilla Public License 2.0
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Programming Language :: Python
16
- Classifier: Programming Language :: Python :: 3.7
17
15
  Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Framework :: Django
21
20
  Classifier: Framework :: Django :: 3.2
22
21
  Classifier: Framework :: Django :: 4.0
@@ -157,5 +156,3 @@ software developed with public money is also publicly available.
157
156
 
158
157
  This package is initially developed by the City of Amsterdam, but the tools
159
158
  and concepts created in this project can be used in any city.
160
-
161
-
@@ -1,22 +1,21 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
7
7
  Author-email: opensource@edoburu.nl
8
8
  License: Mozilla Public License 2.0
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Programming Language :: Python
16
- Classifier: Programming Language :: Python :: 3.7
17
15
  Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Framework :: Django
21
20
  Classifier: Framework :: Django :: 3.2
22
21
  Classifier: Framework :: Django :: 4.0
@@ -157,5 +156,3 @@ software developed with public money is also publicly available.
157
156
 
158
157
  This package is initially developed by the City of Amsterdam, but the tools
159
158
  and concepts created in this project can be used in any city.
160
-
161
-
@@ -2,7 +2,7 @@ CHANGES.md
2
2
  LICENSE
3
3
  MANIFEST.in
4
4
  README.md
5
- setup.cfg
5
+ pyproject.toml
6
6
  setup.py
7
7
  django_gisserver.egg-info/PKG-INFO
8
8
  django_gisserver.egg-info/SOURCES.txt
@@ -23,10 +23,10 @@ gisserver/operations/base.py
23
23
  gisserver/operations/wfs20.py
24
24
  gisserver/output/__init__.py
25
25
  gisserver/output/base.py
26
- gisserver/output/buffer.py
27
26
  gisserver/output/csv.py
28
27
  gisserver/output/geojson.py
29
28
  gisserver/output/gml32.py
29
+ gisserver/output/gml32_lxml.py
30
30
  gisserver/output/results.py
31
31
  gisserver/output/utils.py
32
32
  gisserver/output/xmlschema.py
@@ -0,0 +1 @@
1
+ __version__ = "1.4.0" # follows PEP440
@@ -23,31 +23,28 @@ GISSERVER_DB_PRECISION = getattr(settings, "GISSERVER_DB_PRECISION", 15)
23
23
  # Otherwise, all database-supported SRID's are allowed.
24
24
  GISSERVER_SUPPORTED_CRS_ONLY = getattr(settings, "GISSERVER_SUPPORTED_CRS_ONLY", True)
25
25
 
26
+ # Whether the total results need to be counted.
27
+ # By disabling this, clients just need to fetch more pages
28
+ # 0 = No counting, 1 = all pages, 2 = only for the first page.
29
+ GISSERVER_COUNT_NUMBER_MATCHED = getattr(settings, "GISSERVER_COUNT_NUMBER_MATCHED", 1)
30
+
26
31
  # -- max page size
27
32
 
28
33
  # Allow tuning the page size without having to override code.
29
34
  # This corresponds with the "DefaultMaxFeatures" setting.
30
- GISSERVER_DEFAULT_MAX_PAGE_SIZE = getattr(
31
- settings, "GISSERVER_DEFAULT_MAX_PAGE_SIZE", 5000
32
- )
35
+ GISSERVER_DEFAULT_MAX_PAGE_SIZE = getattr(settings, "GISSERVER_DEFAULT_MAX_PAGE_SIZE", 5000)
33
36
 
34
37
  # CSV exports have a higher default page size, as these results can be streamed.
35
- GISSERVER_GEOJSON_MAX_PAGE_SIZE = getattr(
36
- settings, "GISSERVER_GEOJSON_MAX_PAGE_SIZE", math.inf
37
- )
38
+ GISSERVER_GEOJSON_MAX_PAGE_SIZE = getattr(settings, "GISSERVER_GEOJSON_MAX_PAGE_SIZE", math.inf)
38
39
  GISSERVER_CSV_MAX_PAGE_SIZE = getattr(settings, "GISSERVER_CSV_MAX_PAGE_SIZE", math.inf)
39
40
 
40
41
  # -- debugging
41
42
 
42
43
  # Whether to follow the WFS standards strictly (breaks CITE conformance testing)
43
- GISSERVER_WFS_STRICT_STANDARD = getattr(
44
- settings, "GISSERVER_WFS_STRICT_STANDARD", False
45
- )
44
+ GISSERVER_WFS_STRICT_STANDARD = getattr(settings, "GISSERVER_WFS_STRICT_STANDARD", False)
46
45
 
47
46
  # Whether to wrap filter errors in a nice response, or raise an exception
48
- GISSERVER_WRAP_FILTER_DB_ERRORS = getattr(
49
- settings, "GISSERVER_WRAP_FILTER_DB_ERRORS", True
50
- )
47
+ GISSERVER_WRAP_FILTER_DB_ERRORS = getattr(settings, "GISSERVER_WRAP_FILTER_DB_ERRORS", True)
51
48
 
52
49
 
53
50
  @receiver(setting_changed)
@@ -1,7 +1,8 @@
1
1
  """Internal module for additional GIS database functions."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
- from functools import reduce
5
+ from functools import lru_cache, reduce
5
6
  from typing import cast
6
7
 
7
8
  from django.contrib.gis.db.models import GeometryField, functions
@@ -114,6 +115,7 @@ def get_db_annotation(instance: models.Model, name: str, name_template: str):
114
115
  ) from e
115
116
 
116
117
 
118
+ @lru_cache
117
119
  def escape_xml_name(name: str, template="{name}") -> str:
118
120
  """Escape an XML name to be used as annotation name."""
119
121
  return template.format(name=name.replace(".", "_"))
@@ -132,19 +134,13 @@ def get_db_geometry_selects(
132
134
  }
133
135
 
134
136
 
135
- def _get_db_geometry_target(
136
- xsd_element: XsdElement, output_crs: CRS
137
- ) -> str | functions.Transform:
137
+ def _get_db_geometry_target(xsd_element: XsdElement, output_crs: CRS) -> str | functions.Transform:
138
138
  """Wrap the selection of a geometry field in a CRS Transform if needed."""
139
139
  field = cast(GeometryField, xsd_element.source)
140
- return conditional_transform(
141
- xsd_element.orm_path, field.srid, output_srid=output_crs.srid
142
- )
140
+ return conditional_transform(xsd_element.orm_path, field.srid, output_srid=output_crs.srid)
143
141
 
144
142
 
145
143
  def get_db_geometry_target(xpath_match: XPathMatch, output_crs: CRS):
146
144
  """Based on a resolved element, build the proper geometry field select clause."""
147
145
  field = cast(GeometryField, xpath_match.child.source)
148
- return conditional_transform(
149
- xpath_match.orm_path, field.srid, output_srid=output_crs.srid
150
- )
146
+ return conditional_transform(xpath_match.orm_path, field.srid, output_srid=output_crs.srid)
@@ -7,6 +7,7 @@ See:
7
7
  https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#35
8
8
  https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#411
9
9
  """
10
+
10
11
  from django.conf import settings
11
12
  from django.utils.html import format_html
12
13
 
@@ -13,14 +13,15 @@ The feature type classes (and field types) offer a flexible translation
13
13
  from attribute listings into a schema definition.
14
14
  For example, model relationships can be modelled to a different XML layout.
15
15
  """
16
+
16
17
  from __future__ import annotations
17
18
 
18
19
  import html
19
20
  import operator
20
21
  from collections import defaultdict
21
22
  from dataclasses import dataclass
22
- from functools import lru_cache, reduce
23
- from typing import List, Union
23
+ from functools import cached_property, lru_cache, reduce
24
+ from typing import Union
24
25
 
25
26
  from django.conf import settings
26
27
  from django.contrib.gis.db import models as gis_models
@@ -28,7 +29,6 @@ from django.contrib.gis.db.models import Extent, GeometryField
28
29
  from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
29
30
  from django.db import models
30
31
  from django.db.models.fields.related import ForeignObjectRel # Django 2.2 import
31
- from django.utils.functional import cached_property # py3.8: functools
32
32
 
33
33
  from gisserver.db import conditional_transform
34
34
  from gisserver.exceptions import ExternalValueError
@@ -138,9 +138,7 @@ def _get_model_fields(model, fields, parent=None):
138
138
  parent=parent,
139
139
  )
140
140
  for f in model._meta.get_fields()
141
- if not f.is_relation
142
- or f.many_to_one # ForeignKey
143
- or f.one_to_one # OneToOneField
141
+ if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey # OneToOneField
144
142
  ]
145
143
  else:
146
144
  # Only defined fields
@@ -291,7 +289,7 @@ class FeatureField:
291
289
 
292
290
 
293
291
  _FieldDefinition = Union[str, FeatureField]
294
- _FieldDefinitions = Union[_all_, List[_FieldDefinition]]
292
+ _FieldDefinitions = Union[_all_, list[_FieldDefinition]]
295
293
 
296
294
 
297
295
  class ComplexFeatureField(FeatureField):
@@ -455,15 +453,15 @@ class FeatureType:
455
453
  queryset: models.QuerySet,
456
454
  *,
457
455
  fields: _FieldDefinitions | None = None,
458
- display_field_name: str = None,
459
- geometry_field_name: str = None,
460
- name: str = None,
456
+ display_field_name: str | None = None,
457
+ geometry_field_name: str | None = None,
458
+ name: str | None = None,
461
459
  # WFS Metadata:
462
- title: str = None,
463
- abstract: str = None,
464
- keywords: list[str] = None,
465
- crs: CRS = None,
466
- other_crs: list[CRS] = None,
460
+ title: str | None = None,
461
+ abstract: str | None = None,
462
+ keywords: list[str] | None = None,
463
+ crs: CRS | None = None,
464
+ other_crs: list[CRS] | None = None,
467
465
  metadata_url: str | None = None,
468
466
  # Settings
469
467
  show_name_field: bool = True,
@@ -521,7 +519,6 @@ class FeatureType:
521
519
  """Hook that allows subclasses to reject access for datasets.
522
520
  It may raise a Django PermissionDenied error.
523
521
  """
524
- pass
525
522
 
526
523
  @cached_property
527
524
  def xml_name(self):
@@ -551,7 +548,7 @@ class FeatureType:
551
548
  This gives an object layout based on the XSD elements,
552
549
  that can be used for prefetching data.
553
550
  """
554
- models = dict()
551
+ models = {}
555
552
  fields = defaultdict(set)
556
553
  elements = defaultdict(list)
557
554
 
@@ -677,9 +674,7 @@ class FeatureType:
677
674
  # That that without .only(), use at least `self.queryset.all()` so a clone is returned.
678
675
  return self.queryset.only(*self._local_model_field_names)
679
676
 
680
- def get_related_queryset(
681
- self, feature_relation: FeatureRelation
682
- ) -> models.QuerySet:
677
+ def get_related_queryset(self, feature_relation: FeatureRelation) -> models.QuerySet:
683
678
  """Return the queryset that is used for prefetching related data."""
684
679
  if feature_relation.related_model is None:
685
680
  raise RuntimeError(
@@ -688,9 +683,7 @@ class FeatureType:
688
683
  )
689
684
  # Return a queryset that only retrieves the fields that are displayed.
690
685
  return self.filter_related_queryset(
691
- feature_relation.related_model.objects.only(
692
- *feature_relation._local_model_field_names
693
- )
686
+ feature_relation.related_model.objects.only(*feature_relation._local_model_field_names)
694
687
  )
695
688
 
696
689
  def filter_related_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
@@ -730,9 +723,7 @@ class FeatureType:
730
723
  return None
731
724
 
732
725
  # Perform the combining of geometries inside libgeos
733
- geometry = (
734
- geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
735
- )
726
+ geometry = geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
736
727
  if crs is not None and geometry.srid != crs.srid:
737
728
  crs.apply_to(geometry) # avoid clone
738
729
  return BoundingBox.from_geometry(geometry, crs=crs)
@@ -824,9 +815,7 @@ class FeatureType:
824
815
  f"XPath selectors with expanded syntax are not supported: {xpath}"
825
816
  )
826
817
  elif "(" in xpath:
827
- raise NotImplementedError(
828
- f"XPath selectors with functions are not supported: {xpath}"
829
- )
818
+ raise NotImplementedError(f"XPath selectors with functions are not supported: {xpath}")
830
819
 
831
820
  # Allow /app:ElementName/.. as "absolute" path.
832
821
  # Given our internal resolver logic, simple solution is to strip it.
@@ -3,6 +3,7 @@
3
3
  This includes the CRS parsing, coordinate transforms and bounding box object.
4
4
  The bounding box can be calculated within Python, or read from a database result.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
 
8
9
  import re
@@ -10,7 +11,7 @@ from dataclasses import dataclass, field
10
11
  from decimal import Decimal
11
12
  from functools import lru_cache
12
13
 
13
- from django.contrib.gis.gdal import AxisOrder, CoordTransform, SpatialReference
14
+ from django.contrib.gis.gdal import CoordTransform, SpatialReference
14
15
  from django.contrib.gis.geos import GEOSGeometry, Polygon
15
16
 
16
17
  from gisserver.exceptions import ExternalParsingError, ExternalValueError
@@ -101,9 +102,7 @@ class CRS:
101
102
  self.__dict__["has_custom_backend"] = self.backend is not None
102
103
 
103
104
  @classmethod
104
- def from_string(
105
- cls, uri: str | int, backend: SpatialReference | None = None
106
- ) -> CRS:
105
+ def from_string(cls, uri: str | int, backend: SpatialReference | None = None) -> CRS:
107
106
  """
108
107
  Parse an CRS (Coordinate Reference System) URI, which preferably follows the URN format
109
108
  as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_
@@ -146,17 +145,13 @@ class CRS:
146
145
  """Instantiate this class using an URN format."""
147
146
  urn_match = CRS_URN_REGEX.match(urn)
148
147
  if not urn_match:
149
- raise ExternalValueError(
150
- f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}"
151
- )
148
+ raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
152
149
 
153
150
  domain = urn_match.group("domain")
154
151
  authority = urn_match.group("authority").upper()
155
152
 
156
153
  if domain not in ("ogc", "opengis"):
157
- raise ExternalValueError(
158
- f"CRS URI [{urn}] contains unknown domain [{domain}]"
159
- )
154
+ raise ExternalValueError(f"CRS URI [{urn}] contains unknown domain [{domain}]")
160
155
 
161
156
  if authority == "EPSG":
162
157
  crsid = urn_match.group("id")
@@ -169,14 +164,10 @@ class CRS:
169
164
  elif authority == "OGC":
170
165
  crsid = urn_match.group("id").upper()
171
166
  if crsid != "CRS84":
172
- raise ExternalValueError(
173
- f"OGC CRS URI from [{urn}] contains unknown id [{id}]"
174
- )
167
+ raise ExternalValueError(f"OGC CRS URI from [{urn}] contains unknown id [{id}]")
175
168
  srid = 4326
176
169
  else:
177
- raise ExternalValueError(
178
- f"CRS URI [{urn}] contains unknown authority [{authority}]"
179
- )
170
+ raise ExternalValueError(f"CRS URI [{urn}] contains unknown authority [{authority}]")
180
171
 
181
172
  crs = cls(
182
173
  domain=domain,
@@ -253,9 +244,7 @@ class CRS:
253
244
  if self.origin:
254
245
  self.__dict__["backend"] = _get_spatial_reference(self.origin)
255
246
  else:
256
- self.__dict__["backend"] = _get_spatial_reference(
257
- self.srid, srs_type="epsg"
258
- )
247
+ self.__dict__["backend"] = _get_spatial_reference(self.srid, srs_type="epsg")
259
248
  return self.backend
260
249
 
261
250
  def apply_to(self, geometry: GEOSGeometry, clone=False) -> GEOSGeometry | None:
@@ -271,7 +260,7 @@ class CRS:
271
260
  if clone:
272
261
  return geometry.clone()
273
262
  else:
274
- return
263
+ return None
275
264
  else:
276
265
  # Convert using GDAL / proj
277
266
  transform = _get_coord_transform(geometry.srid, self._as_gdal())
@@ -300,8 +289,7 @@ class BoundingBox:
300
289
  bbox = bbox.split(",")
301
290
  if not (4 <= len(bbox) <= 5):
302
291
  raise ExternalParsingError(
303
- f"Input does not contain bounding box, "
304
- f"expected 4 or 5 values, not {bbox}."
292
+ f"Input does not contain bounding box, expected 4 or 5 values, not {bbox}."
305
293
  )
306
294
  return cls(
307
295
  Decimal(bbox[0]),
@@ -332,9 +320,7 @@ class BoundingBox:
332
320
  def __repr__(self):
333
321
  return f"BoundingBox({self.south}, {self.west}, {self.north}, {self.east})"
334
322
 
335
- def extend_to(
336
- self, lower_lon: float, lower_lat: float, upper_lon: float, upper_lat: float
337
- ):
323
+ def extend_to(self, lower_lon: float, lower_lat: float, upper_lon: float, upper_lat: float):
338
324
  """Expand the bounding box in-place"""
339
325
  self.south = min(self.south, lower_lon)
340
326
  self.west = min(self.west, lower_lat)
@@ -4,12 +4,14 @@ All operations extend from an WFSMethod class.
4
4
  This defines the parameters and output formats of the method.
5
5
  This introspection data is also parsed by the GetCapabilities call.
6
6
  """
7
+
7
8
  from __future__ import annotations
8
9
 
9
10
  import math
10
11
  import re
12
+ from collections.abc import Callable
11
13
  from dataclasses import dataclass, field
12
- from typing import Any, Callable
14
+ from typing import Any
13
15
 
14
16
  from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
15
17
  from django.http import HttpResponse
@@ -108,21 +110,15 @@ class Parameter:
108
110
  This method can be overwritten by a subclass if needed.
109
111
  """
110
112
  if self.allowed_values is not None and value not in self.allowed_values:
111
- msg = self.error_messages.get(
112
- "invalid", "Invalid value for {name}: {value}"
113
- )
114
- raise InvalidParameterValue(
115
- self.name, msg.format(name=self.name, value=value)
116
- )
113
+ msg = self.error_messages.get("invalid", "Invalid value for {name}: {value}")
114
+ raise InvalidParameterValue(self.name, msg.format(name=self.name, value=value))
117
115
 
118
116
 
119
117
  class UnsupportedParameter(Parameter):
120
118
  def value_from_query(self, KVP: dict):
121
119
  kvp_name = self.name.upper()
122
120
  if kvp_name in KVP:
123
- raise InvalidParameterValue(
124
- self.name, f"Support for {self.name} is not implemented!"
125
- )
121
+ raise InvalidParameterValue(self.name, f"Support for {self.name} is not implemented!")
126
122
  return None
127
123
 
128
124
 
@@ -262,9 +258,10 @@ class WFSMethod:
262
258
 
263
259
  def _parse_output_format(self, value) -> OutputFormat:
264
260
  """Select the proper OutputFormat object based on the input value"""
265
- # The str.replace is here to "allow application/gml+xml on the KVP",
266
- # but I'm not sure what that comment was supposed to mean.
267
- for v in [value, value.replace(" ", "+")]:
261
+ # When using ?OUTPUTFORMAT=application/gml+xml", it is actually an URL-encoded space
262
+ # character. Hence spaces are replaced back to a '+' character to allow such notation
263
+ # instead of forcing it to to be ?OUTPUTFORMAT=application/gml%2bxml".
264
+ for v in {value, value.replace(" ", "+")}:
268
265
  for o in self.output_formats:
269
266
  if o.matches(v):
270
267
  return o
@@ -290,9 +287,7 @@ class WFSMethod:
290
287
  tokens = iter(tokens)
291
288
  for prefix in tokens:
292
289
  if not prefix.startswith("xmlns("):
293
- raise InvalidParameterValue(
294
- "namespaces", f"Expected xmlns(...) format: {value}"
295
- )
290
+ raise InvalidParameterValue("namespaces", f"Expected xmlns(...) format: {value}")
296
291
  if prefix.endswith(")"):
297
292
  # xmlns(http://...)
298
293
  prefix = ""
@@ -313,9 +308,7 @@ class WFSMethod:
313
308
  def parse_request(self, KVP: dict) -> dict[str, Any]:
314
309
  """Parse the parameters of the request"""
315
310
  self.namespaces.update(self._parse_namespaces(KVP.get("NAMESPACES")))
316
- param_values = {
317
- param.name: param.value_from_query(KVP) for param in self.get_parameters()
318
- }
311
+ param_values = {param.name: param.value_from_query(KVP) for param in self.get_parameters()}
319
312
  param_values["NAMESPACES"] = self.namespaces
320
313
 
321
314
  for param in self.get_parameters():
@@ -331,7 +324,6 @@ class WFSMethod:
331
324
 
332
325
  def validate(self, **params):
333
326
  """Perform final request parameter validation before the method is called"""
334
- pass
335
327
 
336
328
  def __call__(self, **params):
337
329
  """Default call implementation: render an XML template."""
@@ -371,18 +363,12 @@ class WFSMethod:
371
363
  def _get_xml_template_name(self):
372
364
  """Generate the XML template name for this operation, and check it's file pattern"""
373
365
  service = self.view.KVP["SERVICE"].lower()
374
- template_name = (
375
- f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
376
- )
366
+ template_name = f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
377
367
 
378
368
  # Since 'service' and 'version' are based on external input,
379
369
  # these values are double checked again to avoid remove file inclusion.
380
- if not RE_SAFE_FILENAME.match(service) or not RE_SAFE_FILENAME.match(
381
- self.view.version
382
- ):
383
- raise SuspiciousOperation(
384
- f"Refusing to render template name {template_name}"
385
- )
370
+ if not RE_SAFE_FILENAME.match(service) or not RE_SAFE_FILENAME.match(self.view.version):
371
+ raise SuspiciousOperation(f"Refusing to render template name {template_name}")
386
372
 
387
373
  return template_name
388
374
 
@@ -397,9 +383,7 @@ class WFSTypeNamesMethod(WFSMethod):
397
383
  # This is not delayed until _parse_type_names() as that wraps any TypeError
398
384
  # from view.get_feature_types() as an InvalidParameterValue exception.
399
385
  self.all_feature_types = self.view.get_feature_types()
400
- self.all_feature_types_by_name = _get_feature_types_by_name(
401
- self.all_feature_types
402
- )
386
+ self.all_feature_types_by_name = _get_feature_types_by_name(self.all_feature_types)
403
387
 
404
388
  def get_parameters(self):
405
389
  return super().get_parameters() + [
@@ -423,18 +407,13 @@ class WFSTypeNamesMethod(WFSMethod):
423
407
  "Parameter lists to perform multiple queries are not supported yet.",
424
408
  )
425
409
 
426
- return [
427
- self._parse_type_name(name, locator="typenames")
428
- for name in type_names.split(",")
429
- ]
410
+ return [self._parse_type_name(name, locator="typenames") for name in type_names.split(",")]
430
411
 
431
412
  def _parse_type_name(self, name, locator="typename") -> FeatureType:
432
413
  """Find the requested feature type for a type name"""
433
414
  app_prefix = self.namespaces[self.view.xml_namespace]
434
- if name.startswith(f"{app_prefix}:"):
435
- local_name = name[len(app_prefix) + 1 :] # strip our XML prefix
436
- else:
437
- local_name = name
415
+ # strip our XML prefix
416
+ local_name = name[len(app_prefix) + 1 :] if name.startswith(f"{app_prefix}:") else name
438
417
 
439
418
  try:
440
419
  return self.all_feature_types_by_name[local_name]
@@ -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 + [