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.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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) ->
|
|
32
|
+
def parse_gml(text: str | bytes) -> GmlRootNodes:
|
|
33
33
|
"""Parse an XML <gml:...> string."""
|
|
34
|
-
root_element =
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
gisserver/parsers/gml/base.py
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
13
|
-
from gisserver.parsers.
|
|
14
|
-
from gisserver.
|
|
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",
|
|
28
|
-
@tag_registry.register("LineString",
|
|
29
|
-
@tag_registry.register("LinearRing",
|
|
30
|
-
@tag_registry.register("MultiLineString",
|
|
31
|
-
@tag_registry.register("MultiPoint",
|
|
32
|
-
@tag_registry.register("MultiPolygon",
|
|
33
|
-
@tag_registry.register("MultiSurface",
|
|
34
|
-
@tag_registry.register("Point",
|
|
35
|
-
@tag_registry.register("Polygon",
|
|
36
|
-
@tag_registry.register("Envelope",
|
|
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
|
|
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(
|
|
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"
|
|
85
|
-
@tag_registry.register("Before"
|
|
86
|
-
@tag_registry.register("Begins"
|
|
87
|
-
@tag_registry.register("BegunBy"
|
|
88
|
-
@tag_registry.register("TContains"
|
|
89
|
-
@tag_registry.register("TEquals"
|
|
90
|
-
@tag_registry.register("TOverlaps"
|
|
91
|
-
@tag_registry.register("During"
|
|
92
|
-
@tag_registry.register("Meets"
|
|
93
|
-
@tag_registry.register("OverlappedBy"
|
|
94
|
-
@tag_registry.register("MetBy"
|
|
95
|
-
@tag_registry.register("EndedBy"
|
|
96
|
-
@tag_registry.register("AnyInteracts"
|
|
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 =
|
|
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
|