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
@@ -4,7 +4,13 @@ from dataclasses import dataclass
4
4
  from enum import Enum
5
5
 
6
6
  from gisserver.exceptions import InvalidParameterValue
7
+ from gisserver.parsers.ast import BaseNode, expect_children, expect_tag, tag_registry
7
8
  from gisserver.parsers.fes20 import ValueReference
9
+ from gisserver.parsers.ows import KVPRequest
10
+ from gisserver.parsers.query import CompiledQuery
11
+ from gisserver.parsers.xml import NSElement, xmlns
12
+
13
+ FES_SORT_ORDER = xmlns.fes20.qname("SortOrder")
8
14
 
9
15
 
10
16
  class SortOrder(Enum):
@@ -24,52 +30,117 @@ class SortOrder(Enum):
24
30
  "Expect ASC/DESC ordering direction", locator="sortby"
25
31
  ) from None
26
32
 
33
+ @classmethod
34
+ @expect_tag(xmlns.fes20, "SortOrder")
35
+ def from_xml(cls, element: NSElement):
36
+ return SortOrder.from_string(element.text)
37
+
27
38
 
28
39
  @dataclass
29
- class SortProperty:
30
- """This class name is based on the WFS spec."""
40
+ @tag_registry.register("SortProperty", xmlns.fes20)
41
+ class SortProperty(BaseNode):
42
+ """This class name is based on the WFS spec.
43
+
44
+ This parses and handles the syntax::
45
+
46
+ <fes:SortProperty>
47
+ <fes:ValueReference>name</fes:ValueReference>
48
+ <fes:SortOrder>ASC</fes:SortOrder>
49
+ </fes:SortProperty>
50
+ """
31
51
 
32
52
  value_reference: ValueReference
33
53
  sort_order: SortOrder = SortOrder.ASC
34
54
 
55
+ def __post_init__(self):
56
+ if "[" in self.value_reference.xpath:
57
+ raise InvalidParameterValue(
58
+ "Sorting with XPath attribute selectors is not supported.", locator="sortby"
59
+ )
60
+
61
+ @classmethod
62
+ @expect_tag(xmlns.fes20, "SortProperty")
63
+ @expect_children(1, ValueReference, "SortOrder")
64
+ def from_xml(cls, element: NSElement) -> SortProperty:
65
+ """Parse the incoming XML"""
66
+ sort_order = element.find(FES_SORT_ORDER)
67
+ return cls(
68
+ value_reference=ValueReference.from_xml(element[0]),
69
+ sort_order=(
70
+ SortOrder.from_xml(sort_order) if sort_order is not None else SortOrder.ASC
71
+ ),
72
+ )
73
+
74
+ @classmethod
75
+ def from_string(cls, value: str, ns_aliases: dict[str, str]) -> SortProperty:
76
+ """Parse the incoming GET parameter."""
77
+ xpath, _, direction = value.partition(" ")
78
+ return SortProperty(
79
+ value_reference=ValueReference(xpath, ns_aliases),
80
+ sort_order=SortOrder.from_string(direction) if direction else SortOrder.ASC,
81
+ )
82
+
83
+ def as_kvp(self):
84
+ """Translate the POST request into KVP GET parameters. This is needed for pagination."""
85
+ if self.sort_order == SortOrder.ASC:
86
+ return self.value_reference.xpath
87
+ else:
88
+ return f"{self.value_reference.xpath} {self.sort_order.name}"
89
+
35
90
 
36
91
  @dataclass
37
- class SortBy:
38
- """The sortBy clause."""
92
+ @tag_registry.register("SortBy", xmlns.fes20)
93
+ class SortBy(BaseNode):
94
+ """The sortBy clause.
95
+
96
+ This parses and handles the syntax::
97
+
98
+ <fes:SortBy>
99
+ <fes:SortProperty>
100
+ <fes:ValueReference>name</fes:ValueReference>
101
+ <fes:SortOrder>ASC</fes:SortOrder>
102
+ </fes:SortProperty>
103
+ </fes:SortBy>
104
+
105
+ It also supports the SORTBY parameter for GET requests.
106
+ """
39
107
 
40
108
  sort_properties: list[SortProperty]
41
109
 
42
110
  @classmethod
43
- def from_string(cls, value: str):
44
- """Construct the SortBy object from a KVP "SORTBY" parameter."""
45
- props = []
46
- for field in value.split(","):
47
- if "[" in field:
48
- raise InvalidParameterValue(
49
- "sortby", "Sorting with XPath attribute selectors is not supported."
50
- )
51
-
52
- if " " in field:
53
- xpath, direction = field.split(" ", 1)
54
- props.append(
55
- SortProperty(
56
- value_reference=ValueReference(xpath),
57
- sort_order=SortOrder.from_string(direction),
58
- )
59
- )
60
- else:
61
- props.append(SortProperty(value_reference=ValueReference(field)))
62
-
63
- return cls(sort_properties=props)
64
-
65
- def build_ordering(self, feature_type=None) -> list[str]:
111
+ @expect_children(1)
112
+ def from_xml(cls, element: NSElement) -> SortBy:
113
+ """Parse the XML tag."""
114
+ return cls(
115
+ sort_properties=[
116
+ # The from_xml() validates that the child node is a <fes:SortProperty>
117
+ SortProperty.from_xml(child)
118
+ for child in element
119
+ ]
120
+ )
121
+
122
+ @classmethod
123
+ def from_kvp_request(cls, kvp: KVPRequest) -> SortBy | None:
124
+ """Construct the SortBy object from a KVP "SORTBY" parameter, and considering NAMESPACES."""
125
+ value = kvp.get_str("SortBy", default=None)
126
+ if value is None:
127
+ return None
128
+
129
+ return cls(
130
+ sort_properties=[
131
+ SortProperty.from_string(field, kvp.ns_aliases) for field in value.split(",")
132
+ ]
133
+ )
134
+
135
+ def as_kvp(self) -> str:
136
+ """Translate the POST request into KVP GET parameters. This is needed for pagination."""
137
+ return ",".join(sort_property.as_kvp() for sort_property in self.sort_properties)
138
+
139
+ def build_ordering(self, compiler: CompiledQuery):
66
140
  """Build the ordering for the Django ORM call."""
67
141
  ordering = []
68
142
  for prop in self.sort_properties:
69
- if feature_type is not None:
70
- orm_path = prop.value_reference.parse_xpath(feature_type).orm_path
71
- else:
72
- orm_path = prop.value_reference.xpath.replace("/", "__")
73
-
143
+ orm_path = prop.value_reference.parse_xpath(compiler.feature_types).orm_path
74
144
  ordering.append(f"{prop.sort_order.value}{orm_path}")
75
- return ordering
145
+
146
+ compiler.add_ordering(ordering)
@@ -5,45 +5,46 @@ These functions locate GML objects, and redirect to the proper parser.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from xml.etree.ElementTree import Element
9
-
10
- from defusedxml.ElementTree import fromstring
8
+ from typing import Union
11
9
 
12
10
  from gisserver.exceptions import ExternalParsingError
13
- from gisserver.parsers.base import tag_registry
11
+ from gisserver.parsers.ast import tag_registry
12
+ from gisserver.parsers.xml import NSElement, parse_xml_from_string
14
13
 
15
14
  from .base import AbstractGeometry, GM_Envelope, GM_Object, TM_Object
16
- from .geometries import is_gml_element # also do tag registration
15
+ from .geometries import GEOSGMLGeometry, is_gml_element # also do tag registration
17
16
 
18
17
  # All known root nodes as GML object:
19
- FES_GML_NODES = (GM_Object, GM_Envelope, TM_Object)
18
+ GmlRootNodes = Union[GM_Object, GM_Envelope, TM_Object]
20
19
 
21
20
  __all__ = [
22
21
  "GM_Object",
23
22
  "GM_Envelope",
24
23
  "TM_Object",
25
24
  "AbstractGeometry",
25
+ "GEOSGMLGeometry",
26
26
  "parse_gml",
27
27
  "parse_gml_node",
28
28
  "find_gml_nodes",
29
29
  ]
30
30
 
31
31
 
32
- def parse_gml(text: str | bytes) -> FES_GML_NODES:
32
+ def parse_gml(text: str | bytes) -> GmlRootNodes:
33
33
  """Parse an XML <gml:...> string."""
34
- root_element = fromstring(text)
34
+ root_element = parse_xml_from_string(text)
35
35
  return parse_gml_node(root_element)
36
36
 
37
37
 
38
- def parse_gml_node(element: Element) -> FES_GML_NODES:
38
+ def parse_gml_node(element: NSElement) -> GmlRootNodes:
39
39
  """Parse the element"""
40
40
  if not is_gml_element(element):
41
41
  raise ExternalParsingError(f"Expected GML namespace for {element.tag}")
42
42
 
43
- return tag_registry.from_child_xml(element, allowed_types=FES_GML_NODES)
43
+ # All known root nodes as GML object:
44
+ return tag_registry.node_from_xml(element, allowed_types=GmlRootNodes.__args__)
44
45
 
45
46
 
46
- def find_gml_nodes(element: Element) -> list[Element]:
47
+ def find_gml_nodes(element: NSElement) -> list[NSElement]:
47
48
  """Find all gml elements in a node"""
48
49
  result = []
49
50
  for child in element:
@@ -4,7 +4,10 @@ See "Table D.2" in the GML 3.2.1 spec, showing how the UML names
4
4
  map to the GML implementations. These names are referenced by the FES spec.
5
5
  """
6
6
 
7
- from gisserver.parsers.base import BaseNode
7
+ from __future__ import annotations
8
+
9
+ from gisserver.parsers.ast import BaseNode
10
+ from gisserver.parsers.query import CompiledQuery
8
11
 
9
12
 
10
13
  class AbstractGeometry(BaseNode):
@@ -13,7 +16,7 @@ class AbstractGeometry(BaseNode):
13
16
  <gml:AbstractGeometry> implements the ISO 19107 GM_Object.
14
17
  """
15
18
 
16
- def build_rhs(self, compiler):
19
+ def build_rhs(self, compiler: CompiledQuery):
17
20
  # Allow the value to be used in a binary operator
18
21
  raise NotImplementedError()
19
22
 
@@ -4,14 +4,15 @@ Overview of GML 3.2 changes: https://mapserver.org/el/development/rfc/ms-rfc-105
4
4
  """
5
5
 
6
6
  from dataclasses import dataclass
7
- from xml.etree.ElementTree import Element, tostring
7
+ from xml.etree.ElementTree import tostring
8
8
 
9
- from django.contrib.gis.geos import GEOSGeometry
9
+ from django.contrib.gis.geos import GEOSGeometry, Polygon
10
10
 
11
+ from gisserver.exceptions import ExternalParsingError
11
12
  from gisserver.geometries import CRS
12
- from gisserver.parsers.base import tag_registry
13
- from gisserver.parsers.tags import get_attribute
14
- from gisserver.types import GML21, GML32
13
+ from gisserver.parsers.ast import tag_registry
14
+ from gisserver.parsers.query import CompiledQuery
15
+ from gisserver.parsers.xml import NSElement, xmlns
15
16
 
16
17
  from .base import AbstractGeometry, TM_Object
17
18
 
@@ -24,18 +25,20 @@ def is_gml_element(element) -> bool:
24
25
 
25
26
 
26
27
  @dataclass(repr=False)
27
- @tag_registry.register("Polygon", GML21)
28
- @tag_registry.register("LineString", GML32)
29
- @tag_registry.register("LinearRing", GML32)
30
- @tag_registry.register("MultiLineString", GML32)
31
- @tag_registry.register("MultiPoint", GML32)
32
- @tag_registry.register("MultiPolygon", GML32)
33
- @tag_registry.register("MultiSurface", GML32)
34
- @tag_registry.register("Point", GML32)
35
- @tag_registry.register("Polygon", GML32)
36
- @tag_registry.register("Envelope", GML32)
28
+ @tag_registry.register("Polygon", xmlns.gml21)
29
+ @tag_registry.register("LineString", xmlns.gml32)
30
+ @tag_registry.register("LinearRing", xmlns.gml32)
31
+ @tag_registry.register("MultiLineString", xmlns.gml32)
32
+ @tag_registry.register("MultiPoint", xmlns.gml32)
33
+ @tag_registry.register("MultiPolygon", xmlns.gml32)
34
+ @tag_registry.register("MultiSurface", xmlns.gml32)
35
+ @tag_registry.register("Point", xmlns.gml32)
36
+ @tag_registry.register("Polygon", xmlns.gml32)
37
+ @tag_registry.register("Envelope", xmlns.gml32)
37
38
  class GEOSGMLGeometry(AbstractGeometry):
38
- """Convert the incoming GML into a Django GEOSGeometry"""
39
+ """Convert the incoming GML into a Django GEOSGeometry.
40
+ This tag parses all ``<gml:...>`` geometry elements within the query.
41
+ """
39
42
 
40
43
  # Not implemented:
41
44
  # - Curve
@@ -43,20 +46,42 @@ class GEOSGMLGeometry(AbstractGeometry):
43
46
  # - MultiGeometry
44
47
  # - Surface
45
48
 
46
- xml_ns = ...
47
-
48
49
  srs: CRS
49
50
  geos_data: GEOSGeometry
50
51
 
51
52
  @classmethod
52
- def from_xml(cls, element: Element):
53
+ def from_bbox(cls, bbox_value: str):
54
+ """Parse the bounding box from an input string.
55
+
56
+ It can either be 4 coordinates, or 4 coordinates with a special reference system.
57
+ """
58
+ bbox = bbox_value.split(",")
59
+ if not (4 <= len(bbox) <= 5):
60
+ raise ExternalParsingError(
61
+ f"Input does not contain bounding box, expected 4 or 5 values, not {bbox}."
62
+ )
63
+
64
+ polygon = Polygon.from_bbox(
65
+ (float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3]))
66
+ )
67
+ if len(bbox) == 5:
68
+ crs = CRS.from_string(bbox[4])
69
+ polygon.srid = crs.srid
70
+ else:
71
+ crs = None # will be resolved
72
+
73
+ # Wrap in an element that the filter can use.
74
+ return cls(srs=crs, geos_data=polygon)
75
+
76
+ @classmethod
77
+ def from_xml(cls, element: NSElement):
53
78
  """Push the whole <gml:...> element into the GEOS parser.
54
79
  This avoids having to support the whole GEOS logic.
55
80
 
56
81
  GML is a complex beast with many different forms for the same thing:
57
82
  http://erouault.blogspot.com/2014/04/gml-madness.html
58
83
  """
59
- srs = CRS.from_string(get_attribute(element, "srsName"))
84
+ srs = CRS.from_string(element.get_str_attribute("srsName"))
60
85
 
61
86
  # Push the whole <gml:...> element into the GEOS parser.
62
87
  # This avoids having to support the whole GEOS logic.
@@ -77,24 +102,33 @@ class GEOSGMLGeometry(AbstractGeometry):
77
102
  def json(self):
78
103
  return self.geos_data.json
79
104
 
80
- def build_rhs(self, compiler):
105
+ def build_rhs(self, compiler: CompiledQuery):
106
+ # Perform final validation during the construction of the query.
107
+ if self.srs is None:
108
+ # When the feature type is known, apply its default CRS.
109
+ # This is not possible in XML parsing, but may happen for BBOX parsing.
110
+ self.srs = compiler.feature_types[0].crs
111
+ self.geos_data.srid = self.srs.srid # assign default CRS to geometry
112
+ elif compiler.feature_types: # for unit tests
113
+ self.srs = compiler.feature_types[0].resolve_crs(self.srs, locator="bbox")
114
+
81
115
  return self.geos_data
82
116
 
83
117
 
84
- @tag_registry.register("After", GML32)
85
- @tag_registry.register("Before", GML32)
86
- @tag_registry.register("Begins", GML32)
87
- @tag_registry.register("BegunBy", GML32)
88
- @tag_registry.register("TContains", GML32)
89
- @tag_registry.register("TEquals", GML32)
90
- @tag_registry.register("TOverlaps", GML32)
91
- @tag_registry.register("During", GML32)
92
- @tag_registry.register("Meets", GML32)
93
- @tag_registry.register("OverlappedBy", GML32)
94
- @tag_registry.register("MetBy", GML32)
95
- @tag_registry.register("EndedBy", GML32)
96
- @tag_registry.register("AnyInteracts", GML32)
118
+ @tag_registry.register("After")
119
+ @tag_registry.register("Before")
120
+ @tag_registry.register("Begins")
121
+ @tag_registry.register("BegunBy")
122
+ @tag_registry.register("TContains")
123
+ @tag_registry.register("TEquals")
124
+ @tag_registry.register("TOverlaps")
125
+ @tag_registry.register("During")
126
+ @tag_registry.register("Meets")
127
+ @tag_registry.register("OverlappedBy")
128
+ @tag_registry.register("MetBy")
129
+ @tag_registry.register("EndedBy")
130
+ @tag_registry.register("AnyInteracts")
97
131
  class TM_GeometricPrimitive(TM_Object):
98
132
  """Not implemented: the whole GML temporal logic"""
99
133
 
100
- xml_ns = GML32
134
+ xml_ns = xmlns.gml32
@@ -0,0 +1,25 @@
1
+ """Generic Open Web Services (OWS) protocol bits to handle incoming requests.
2
+
3
+ This is the common logic between WFS, WMS and other protocols.
4
+
5
+ These translate both request-syntax formats in the same internal objects
6
+ that the rest of the controller/view logic can use.
7
+ """
8
+
9
+ from .kvp import KVPRequest
10
+ from .requests import (
11
+ BaseOwsRequest,
12
+ parse_get_request,
13
+ parse_post_request,
14
+ resolve_kvp_parser_class,
15
+ resolve_xml_parser_class,
16
+ )
17
+
18
+ __all__ = (
19
+ "KVPRequest",
20
+ "BaseOwsRequest",
21
+ "resolve_kvp_parser_class",
22
+ "resolve_xml_parser_class",
23
+ "parse_get_request",
24
+ "parse_post_request",
25
+ )
@@ -0,0 +1,190 @@
1
+ """Parsing the Key-Value-Pair (KVP) request format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import copy
6
+
7
+ from gisserver.exceptions import (
8
+ InvalidParameterValue,
9
+ MissingParameterValue,
10
+ OperationParsingFailed,
11
+ wrap_parser_errors,
12
+ )
13
+ from gisserver.parsers.xml import parse_qname, xmlns
14
+
15
+ REQUIRED = ... # sentinel value
16
+
17
+
18
+ class KVPRequest:
19
+ """The Key-Value-Pair (KVP) request format.
20
+
21
+ This handles parameters from the HTTP GET request.
22
+ It includes notation format support for certain parameters,
23
+ such as comma-separated lists, and parenthesis-grouping notations.
24
+
25
+ Some basic validation is performed, allowing to convert the data into Python types.
26
+ """
27
+
28
+ def __init__(self, query_string: dict[str, str], ns_aliases: dict[str, str] | None = None):
29
+ # The parameters are case-insensitive.
30
+ self.params = {name.upper(): value for name, value in query_string.items()}
31
+
32
+ # Make sure most common namespaces are known for resolving them.
33
+ self.ns_aliases = {
34
+ **xmlns.as_ns_aliases(), # some defaults in case these are missing in the request
35
+ **(ns_aliases or {}), # our local application namespaces
36
+ **parse_kvp_namespaces(self.params.get("NAMESPACES")), # extra in the request
37
+ }
38
+
39
+ def __contains__(self, name: str) -> bool:
40
+ """Tell whether a parameter is present."""
41
+ return name.upper() in self.params
42
+
43
+ def get_custom(self, name: str, *, alias: str | None = None, default=REQUIRED, parser=None):
44
+ """Retrieve a value by name.
45
+ This performs basic validation, similar to what the XML parsing does.
46
+
47
+ Any parsing errors or validation checks are raised as WFS Exceptions,
48
+ meaning the client will get the appropriate response.
49
+
50
+ :param name: The name of the parameter, typically given in its XML notation format (camelCase).
51
+ :param alias: An older WFS 1 to try for compatibility (e.g. TYPENAMES/TYPENAME, COUNT/MAXFEATURES)
52
+ :param default: The default value to return. If not provided, the parameter is required.
53
+ :param parser: A custom Python function or type to convert the value with.
54
+ """
55
+ kvp_name = name.upper()
56
+ value = self.params.get(kvp_name)
57
+ if not value and alias:
58
+ value = self.params.get(alias.upper())
59
+
60
+ # Check required field settings, both empty and missing value are treated the same.
61
+ if not value:
62
+ if default is REQUIRED:
63
+ if value is None:
64
+ raise MissingParameterValue(
65
+ f"Missing required '{name}' parameter.", locator=name
66
+ )
67
+ else:
68
+ raise InvalidParameterValue(f"Empty '{kvp_name}' parameter", locator=name)
69
+ return default
70
+
71
+ # Allow conversion into a python object
72
+ if parser is not None:
73
+ with wrap_parser_errors(kvp_name, locator=name):
74
+ return parser(value)
75
+ else:
76
+ return value
77
+
78
+ def get_str(
79
+ self, name: str, *, alias: str | None = None, default: str | None = REQUIRED
80
+ ) -> str | None:
81
+ """Retrieve a string value from the request."""
82
+ return self.get_custom(name, alias=alias, default=default)
83
+
84
+ def get_int(
85
+ self, name: str, *, alias: str | None = None, default: int | None = REQUIRED
86
+ ) -> int | None:
87
+ """Retrieve an integer value from the request."""
88
+ return self.get_custom(name, alias=alias, default=default, parser=int)
89
+
90
+ def get_list(
91
+ self, name, *, alias: str | None = None, default: list | None = REQUIRED
92
+ ) -> list[str] | None:
93
+ """Retrieve a comma-separated list value from the request."""
94
+ return self.get_custom(
95
+ name,
96
+ alias=alias,
97
+ default=default,
98
+ parser=lambda x: x.split(","),
99
+ )
100
+
101
+ def parse_qname(self, value) -> str:
102
+ """Convert the value to an XML fully qualified name."""
103
+ return parse_qname(value, self.ns_aliases)
104
+
105
+ def split_parameter_lists(self) -> list[KVPRequest]:
106
+ """Split the parameter lists into individual requests.
107
+
108
+ This translates a request such as::
109
+
110
+ TYPENAMES=(ns1:F1,ns2:F2)(ns1:F1,ns1:F1)
111
+ &ALIASES=(A,B)(C,D)
112
+ &FILTER=(<Filter>… for A,B …</Filter>)(<Filter>…for C,D…</Filter>)
113
+
114
+ into separate pairs:
115
+
116
+ TYPENAMES=ns1:F1,ns2:F2&ALIASES=A,B&FILTER=<Filter>…for A,B…</Filter>
117
+ TYPENAMES=ns1:F1,ns1:F1&ALIASES=C,D&FILTER=<Filter>…for C,D…</Filter>
118
+
119
+ It's both possible have some query parameters split and some shared.
120
+ For example to have two different bounding boxes:
121
+
122
+ TYPENAMES=(INWATER_1M)(BuiltUpA_1M)&BBOX=(40.9821,...)(40.5874,...)
123
+
124
+ or have a single bounding box for both queries::
125
+
126
+ TYPENAMES=(INWATER_1M)(BuiltUpA_1M)&BBOX=40.9821,23.4948,41.0257,23.5525
127
+ """
128
+ pairs = {
129
+ name: value[1:-1].split(")(")
130
+ for name, value in self.params.items()
131
+ if value.startswith("(") and value.endswith(")")
132
+ }
133
+ if not pairs:
134
+ return [self]
135
+
136
+ pair_sizes = {len(value) for value in pairs.values()}
137
+ if len(pair_sizes) > 1:
138
+ keys = sorted(pairs)
139
+ raise OperationParsingFailed(
140
+ f"Inconsistent pairs between: {', '.join(keys)}", locator=keys[0]
141
+ )
142
+
143
+ # Produce variations of the same request object
144
+ pair_size = next(iter(pair_sizes))
145
+ variants = []
146
+ for i in range(pair_size):
147
+ updates = {key: value[i] for key, value in pairs.items()}
148
+ variant = copy(self)
149
+ variant.params = {**self.params, **updates}
150
+ variants.append(variant)
151
+ return variants
152
+
153
+
154
+ def parse_kvp_namespaces(value) -> dict[str, str]:
155
+ """Parse the 'NAMESPACES' parameter format to lookups.
156
+
157
+ The NAMESPACES parameter defines which namespaces are used in the KVP request.
158
+ When this parameter is not given, the default namespaces are assumed.
159
+ """
160
+ if not value:
161
+ return {}
162
+
163
+ # example single value: xmlns(http://example.org)
164
+ # or: NAMESPACES=xmlns(xml,http://www.w3.org/...),xmlns(wfs,http://www.opengis.net/...)
165
+ tokens = value.split(",")
166
+
167
+ namespaces = {}
168
+ tokens = iter(tokens)
169
+ for prefix in tokens:
170
+ if not prefix.startswith("xmlns("):
171
+ raise InvalidParameterValue(
172
+ f"Expected xmlns(...) format: {value}", locator="namespaces"
173
+ )
174
+ if prefix.endswith(")"):
175
+ # xmlns(http://...)
176
+ uri = prefix[6:-1]
177
+ prefix = ""
178
+ else:
179
+ uri = next(tokens, "")
180
+ if not uri.endswith(")"):
181
+ raise InvalidParameterValue(
182
+ f"Expected xmlns(prefix,uri) format: {value}",
183
+ locator="namespaces",
184
+ )
185
+ prefix = prefix[6:]
186
+ uri = uri[:-1]
187
+
188
+ namespaces[prefix] = uri
189
+
190
+ return namespaces