django-gisserver 2.0__py3-none-any.whl → 2.1__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 (55) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/crs.py +401 -0
  6. gisserver/db.py +71 -5
  7. gisserver/exceptions.py +106 -2
  8. gisserver/extensions/functions.py +122 -28
  9. gisserver/extensions/queries.py +15 -10
  10. gisserver/features.py +44 -36
  11. gisserver/geometries.py +64 -306
  12. gisserver/management/commands/loadgeojson.py +41 -21
  13. gisserver/operations/base.py +11 -7
  14. gisserver/operations/wfs20.py +31 -93
  15. gisserver/output/__init__.py +6 -2
  16. gisserver/output/base.py +28 -13
  17. gisserver/output/csv.py +18 -6
  18. gisserver/output/geojson.py +7 -6
  19. gisserver/output/gml32.py +43 -23
  20. gisserver/output/results.py +25 -39
  21. gisserver/output/utils.py +9 -2
  22. gisserver/parsers/ast.py +171 -65
  23. gisserver/parsers/fes20/__init__.py +76 -4
  24. gisserver/parsers/fes20/expressions.py +97 -27
  25. gisserver/parsers/fes20/filters.py +9 -6
  26. gisserver/parsers/fes20/identifiers.py +27 -7
  27. gisserver/parsers/fes20/lookups.py +8 -6
  28. gisserver/parsers/fes20/operators.py +101 -49
  29. gisserver/parsers/fes20/sorting.py +14 -6
  30. gisserver/parsers/gml/__init__.py +10 -19
  31. gisserver/parsers/gml/base.py +32 -14
  32. gisserver/parsers/gml/geometries.py +48 -21
  33. gisserver/parsers/ows/kvp.py +10 -2
  34. gisserver/parsers/ows/requests.py +6 -4
  35. gisserver/parsers/query.py +6 -2
  36. gisserver/parsers/values.py +61 -4
  37. gisserver/parsers/wfs20/__init__.py +2 -0
  38. gisserver/parsers/wfs20/adhoc.py +25 -17
  39. gisserver/parsers/wfs20/base.py +12 -7
  40. gisserver/parsers/wfs20/projection.py +3 -3
  41. gisserver/parsers/wfs20/requests.py +1 -0
  42. gisserver/parsers/wfs20/stored.py +3 -2
  43. gisserver/parsers/xml.py +12 -0
  44. gisserver/projection.py +17 -7
  45. gisserver/static/gisserver/index.css +8 -3
  46. gisserver/templates/gisserver/base.html +12 -0
  47. gisserver/templates/gisserver/index.html +9 -15
  48. gisserver/templates/gisserver/service_description.html +12 -6
  49. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  50. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  51. gisserver/types.py +150 -81
  52. gisserver/views.py +47 -24
  53. django_gisserver-2.0.dist-info/RECORD +0 -66
  54. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  55. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
gisserver/parsers/ast.py CHANGED
@@ -3,17 +3,17 @@
3
3
  By transforming the XML Element nodes into Python objects, most logic naturally follows.
4
4
  For example, the FES filter syntax can be processed into objects that build an ORM query.
5
5
 
6
- Python classes can inherit :class:`BaseNode` and register themselves as the parser/handler
6
+ Python classes can inherit :class:`AstNode` and register themselves as the parser/handler
7
7
  for a given tag. Both normal Python classes and dataclass work,
8
- as long as it has an :meth:`BaseNode.from_xml` class method.
8
+ as long as it has an :meth:`AstNode.from_xml` class method.
9
9
  The custom `from_xml()` method should copy the XML data into local attributes.
10
10
 
11
11
  Next, when :meth:`TagRegistry.node_from_xml` is called,
12
12
  it will detect which class the XML Element refers to and initialize it using the ``from_xml()`` call.
13
- As convenience, calling :meth:`SomeNode.child_from_xml()` will also
14
- initialize the right subclass and initialize it.
13
+ As convenience, calling a :meth:`AstNode.child_from_xml`
14
+ on a subclass will also initialize the right subclass and initialize it.
15
15
 
16
- Since clients may not follow the desired XML schema, and make mistakes, one should avoid
16
+ Since clients may not follow the desired XML schema, and make mistakes, we should guard against
17
17
  creating an invalid Abstract Syntax Tree. When using :meth:`TagRegistry.node_from_xml`,
18
18
  the allowed child types can also be provided, preventing invalid child elements.
19
19
  Furthermore, to support the creation of ``from_xml()`` methods, the :func:`expect_tag`,
@@ -27,15 +27,17 @@ from __future__ import annotations
27
27
  from collections.abc import Iterable
28
28
  from enum import Enum
29
29
  from functools import wraps
30
- from itertools import chain
31
30
  from typing import TypeVar
32
31
  from xml.etree.ElementTree import QName
33
32
 
34
- from gisserver.exceptions import ExternalParsingError
33
+ from django.utils.functional import classproperty
34
+
35
+ from gisserver.exceptions import InvalidXmlElement, XmlElementNotSupported
36
+ from gisserver.parsers.xml import NSElement, xmlns
35
37
 
36
38
  __all__ = (
37
39
  "TagNameEnum",
38
- "BaseNode",
40
+ "AstNode",
39
41
  "TagRegistry",
40
42
  "tag_registry",
41
43
  "expect_children",
@@ -43,11 +45,9 @@ __all__ = (
43
45
  "expect_no_children",
44
46
  )
45
47
 
46
- from gisserver.parsers.xml import NSElement, xmlns
47
-
48
48
 
49
49
  class TagNameEnum(Enum):
50
- """An enumeration of XML tag names.
50
+ """An base clas for enumerations of XML tag names.
51
51
 
52
52
  All enumerations that represent tag names inherit from this.
53
53
  Each member name should be exactly the XML tag that it refers to.
@@ -55,7 +55,12 @@ class TagNameEnum(Enum):
55
55
 
56
56
  @classmethod
57
57
  def from_xml(cls, element: NSElement):
58
- """Cast the element tag name into the enum member"""
58
+ """Cast the element tag name into the enum member.
59
+
60
+ This translates the element name
61
+ such as``{http://www.opengis.net/fes/2.0}PropertyIsEqualTo``
62
+ into a ``PropertyIsEqualTo`` member.
63
+ """
59
64
  tag_name = element.tag
60
65
  if tag_name.startswith("{"):
61
66
  # Split the element tag into the namespace and local name.
@@ -74,7 +79,7 @@ class TagNameEnum(Enum):
74
79
  return f"{self.__class__.__name__}.{self.name}"
75
80
 
76
81
 
77
- class BaseNode:
82
+ class AstNode:
78
83
  """The base node for all classes that represent an XML tag.
79
84
 
80
85
  All subclasses of this class build an Abstract Syntax Tree (AST)
@@ -86,23 +91,29 @@ class BaseNode:
86
91
  """
87
92
 
88
93
  xml_ns: xmlns | str | None = None
89
- xml_tags = []
94
+
95
+ _xml_tags = []
96
+
97
+ @classproperty
98
+ def xml_name(cls) -> str:
99
+ """Tell the default tag by which this class is registered"""
100
+ return cls._xml_tags[0]
90
101
 
91
102
  def __init_subclass__(cls):
92
103
  # Each class level has a fresh list of supported child tags.
93
- cls.xml_tags = []
104
+ cls._xml_tags = []
94
105
 
95
106
  @classmethod
96
107
  def from_xml(cls, element: NSElement):
97
108
  """Initialize this Python class from the data of the corresponding XML tag.
98
- Each subclass overrides this to implement the XMl parsing of that particular XML tag.
109
+ Each subclass overrides this to implement the XML parsing of that particular XML tag.
99
110
  """
100
111
  raise NotImplementedError(
101
112
  f"{cls.__name__}.from_xml() is not implemented to parse <{element.tag}>"
102
113
  )
103
114
 
104
115
  @classmethod
105
- def child_from_xml(cls, element: NSElement) -> BaseNode:
116
+ def child_from_xml(cls, element: NSElement) -> AstNode:
106
117
  """Parse the element, returning the correct subclass of this tag.
107
118
 
108
119
  When ``Expression.child_from_xml(some_node)`` is given, it may
@@ -112,12 +123,23 @@ class BaseNode:
112
123
  return sub_class.from_xml(element)
113
124
 
114
125
  @classmethod
115
- def get_tag_names(cls) -> Iterable[str]:
126
+ def get_tag_names(cls) -> list[str]:
116
127
  """Provide all known XMl tags that this code can parse."""
117
- return chain.from_iterable(sub_type.xml_tags for sub_type in cls.__subclasses__())
128
+ try:
129
+ # Because a cached class property is hard to build
130
+ return _KNOWN_TAG_NAMES[cls]
131
+ except KeyError:
132
+ all_xml_tags = cls._xml_tags.copy()
133
+ for sub_cls in cls.__subclasses__():
134
+ all_xml_tags.extend(sub_cls.get_tag_names())
135
+ _KNOWN_TAG_NAMES[cls] = all_xml_tags
136
+ return all_xml_tags
137
+
118
138
 
139
+ _KNOWN_TAG_NAMES = {}
119
140
 
120
- BN = TypeVar("BN", bound=BaseNode)
141
+ BaseNode = AstNode # keep old name around
142
+ A = TypeVar("A", bound=AstNode)
121
143
 
122
144
 
123
145
  class TagRegistry:
@@ -126,7 +148,7 @@ class TagRegistry:
126
148
  The same class can be registered multiple times for different tag names.
127
149
  """
128
150
 
129
- parsers: dict[str, type[BaseNode]]
151
+ parsers: dict[str, type[AstNode]]
130
152
 
131
153
  def __init__(self):
132
154
  self.parsers = {}
@@ -141,9 +163,11 @@ class TagRegistry:
141
163
 
142
164
  Usage:
143
165
 
166
+ .. code-block:: python
167
+
144
168
  @dataclass
145
169
  @tag_registry.register()
146
- class SomeXmlTag(BaseNode):
170
+ class SomeXmlTag(AstNode):
147
171
  xml_ns = FES
148
172
 
149
173
  @classmethod
@@ -159,7 +183,7 @@ class TagRegistry:
159
183
  each member name is assumed to be an XML tag name.
160
184
  """
161
185
 
162
- def _dec(node_class: type[BaseNode]) -> type[BaseNode]:
186
+ def _dec(node_class: type[AstNode]) -> type[AstNode]:
163
187
  if tag is None or isinstance(tag, str):
164
188
  # Single tag name for the class.
165
189
  self._register_tag_parser(
@@ -170,7 +194,9 @@ class TagRegistry:
170
194
  # Note using __members__, not _member_names_.
171
195
  # The latter will skip aliased items (like BBOX/Within).
172
196
  for member_name in tag.__members__:
173
- self._register_tag_parser(node_class, tag=member_name, namespace=namespace)
197
+ self._register_tag_parser(
198
+ node_class, tag=member_name, namespace=namespace, hidden=hidden
199
+ )
174
200
  else:
175
201
  raise TypeError("tag type incorrect")
176
202
 
@@ -180,14 +206,14 @@ class TagRegistry:
180
206
 
181
207
  def _register_tag_parser(
182
208
  self,
183
- node_class: type[BaseNode],
209
+ node_class: type[AstNode],
184
210
  tag: str,
185
211
  namespace: xmlns | str | None = None,
186
212
  hidden: bool = False,
187
213
  ):
188
214
  """Register a Python (data) class as parser for an XML node."""
189
- if not issubclass(node_class, BaseNode):
190
- raise TypeError(f"{node_class} must be a subclass of BaseNode")
215
+ if not issubclass(node_class, AstNode):
216
+ raise TypeError(f"{node_class} must be a subclass of AstNode")
191
217
 
192
218
  if namespace is None and node_class.xml_ns is None:
193
219
  raise RuntimeError(
@@ -202,49 +228,46 @@ class TagRegistry:
202
228
 
203
229
  self.parsers[xml_name] = node_class # Track this parser to resolve the tag.
204
230
  if not hidden:
205
- node_class.xml_tags.append(xml_name) # Allow fetching all names later
231
+ node_class._xml_tags.append(xml_name) # Allow fetching all names later
206
232
 
207
- def node_from_xml(
208
- self, element: NSElement, allowed_types: tuple[type[BN]] | None = None
209
- ) -> BN:
210
- """Find the ``BaseNode`` subclass that corresponds to the given XML element,
233
+ def node_from_xml(self, element: NSElement, allowed_types: tuple[type[A]] | None = None) -> A:
234
+ """Find the ``AstNode`` subclass that corresponds to the given XML element,
211
235
  and initialize it with the element. This is a convenience shortcut.
212
236
  ``"""
213
237
  node_class = self.resolve_class(element, allowed_types)
214
238
  return node_class.from_xml(element)
215
239
 
216
240
  def resolve_class(
217
- self, element: NSElement, allowed_types: tuple[type[BN]] | None = None
218
- ) -> type[BN]:
219
- """Find the ``BaseNode`` subclass that corresponds to the given XML element."""
241
+ self, element: NSElement, allowed_types: tuple[type[A]] | None = None
242
+ ) -> type[A]:
243
+ """Find the ``AstNode`` subclass that corresponds to the given XML element."""
220
244
  try:
221
245
  node_class = self.parsers[element.tag]
222
246
  except KeyError:
223
- msg = f"Unsupported tag: <{element.tag}>"
247
+ msg = f"Unsupported tag: <{element.qname}>"
224
248
  if "{" not in element.tag:
225
249
  msg = f"{msg} without an XML namespace"
226
250
  if allowed_types:
227
- # Show better exception message
228
- types = ", ".join(chain.from_iterable(c.get_tag_names() for c in allowed_types))
229
- msg = f"{msg}, expected one of: {types}"
251
+ allowed = _tag_names_to_text(_get_allowed_tag_names(*allowed_types), element)
252
+ msg = f"{msg}, expected one of: {allowed}."
230
253
 
231
- raise ExternalParsingError(msg) from None
254
+ raise XmlElementNotSupported(msg) from None
232
255
 
233
256
  # Check whether the resolved class is indeed a valid option here.
234
257
  if allowed_types is not None and not issubclass(node_class, allowed_types):
235
258
  types = ", ".join(c.__name__ for c in allowed_types)
236
- raise ExternalParsingError(
237
- f"Unexpected {node_class.__name__} for <{element.tag}> node, "
259
+ raise InvalidXmlElement(
260
+ f"Unexpected {node_class.__name__} for <{element.qname}> node, "
238
261
  f"expected one of: {types}"
239
262
  )
240
263
 
241
264
  return node_class
242
265
 
243
- def get_parser_class(self, xml_qname) -> type[BaseNode]:
266
+ def get_parser_class(self, xml_qname) -> type[AstNode]:
244
267
  """Provide the parser class for a given XML Qualified name."""
245
268
  return self.parsers[xml_qname]
246
269
 
247
- def find_subclasses(self, node_type: type[BN]) -> list[type[BN]]:
270
+ def find_subclasses(self, node_type: type[A]) -> list[type[A]]:
248
271
  """Find all registered parsers for a given node."""
249
272
  return {
250
273
  tag: node_class
@@ -254,7 +277,19 @@ class TagRegistry:
254
277
 
255
278
 
256
279
  def expect_tag(namespace: xmlns | str, *tag_names: str):
257
- """Validate whether a given tag is need."""
280
+ """Decorator for ``from_xml()`` methods that validate whether a given tag is provided.
281
+
282
+ For example:
283
+
284
+ .. code-block:: python
285
+
286
+ @classmethod
287
+ @expect_tag(xmlns.fes20, "Literal")
288
+ def from_xml(cls, element):
289
+ ...
290
+
291
+ This guard is needed when nodes are passed directly to a ``from_xml()`` method.
292
+ """
258
293
  valid_tags = {QName(namespace, name).text for name in tag_names}
259
294
  expect0 = QName(namespace, tag_names[0]).text
260
295
 
@@ -262,8 +297,9 @@ def expect_tag(namespace: xmlns | str, *tag_names: str):
262
297
  @wraps(func)
263
298
  def _expect_tag_decorator(cls, element: NSElement, *args, **kwargs):
264
299
  if element.tag not in valid_tags:
265
- raise ExternalParsingError(
266
- f"{cls.__name__} parser expects an <{expect0}> node, got <{element.tag}>"
300
+ raise InvalidXmlElement(
301
+ f"{cls.__name__} parser expects an <{_replace_common_ns(expect0, element)}> node,"
302
+ f" got <{element.qname}>"
267
303
  )
268
304
  return func(cls, element, *args, **kwargs)
269
305
 
@@ -273,13 +309,25 @@ def expect_tag(namespace: xmlns | str, *tag_names: str):
273
309
 
274
310
 
275
311
  def expect_no_children(from_xml_func):
276
- """Validate that the XML tag has no child nodes."""
312
+ """Decorator for ``from_xml()`` methods that validate that the XML tag has no child nodes.
313
+
314
+ For example:
315
+
316
+ .. code-block:: python
317
+
318
+ @classmethod
319
+ @expect_tag(xmlns.fes20, "ResourceId")
320
+ @expect_no_children
321
+ def from_xml(cls, element):
322
+ ...
323
+ """
277
324
 
278
325
  @wraps(from_xml_func)
279
326
  def _expect_no_children_decorator(cls, element: NSElement, *args, **kwargs):
280
327
  if len(element):
281
- raise ExternalParsingError(
282
- f"Unsupported child element for {element.tag} element: {element[0].tag}."
328
+ raise InvalidXmlElement(
329
+ f"Element <{element.qname}> does not support child elements,"
330
+ f" found <{element[0].qname}>."
283
331
  )
284
332
 
285
333
  return from_xml_func(cls, element, *args, **kwargs)
@@ -287,28 +335,53 @@ def expect_no_children(from_xml_func):
287
335
  return _expect_no_children_decorator
288
336
 
289
337
 
290
- def expect_children(min_child_nodes, *expect_types: str | type[BaseNode]):
291
- """Validate whether an element has enough children to continue parsing."""
292
- known_tag_names = set()
293
- for child_type in expect_types:
294
- if isinstance(child_type, type) and issubclass(child_type, BaseNode):
295
- known_tag_names.update(child_type.get_tag_names())
296
- elif isinstance(child_type, str):
297
- known_tag_names.add(child_type)
298
- else:
338
+ def expect_children( # noqa: C901
339
+ min_child_nodes, *expect_types: str | type[AstNode], silent_allowed: tuple[str] = ()
340
+ ):
341
+ """Decorator for ``from_xml()`` methods to validate whether an element has the expected children.
342
+
343
+ For example:
344
+
345
+ .. code-block:: python
346
+
347
+ @classmethod
348
+ @expect_children(2, Expression)
349
+ def from_xml(cls, element):
350
+ ...
351
+ """
352
+ # Validate arguments early
353
+ for child_type in expect_types + silent_allowed:
354
+ if isinstance(child_type, str):
355
+ if not child_type.startswith("{"):
356
+ raise ValueError(
357
+ f"String arguments to @expect_children() should be"
358
+ f" fully qualified XML namespaces, not {child_type!r}"
359
+ )
360
+ elif not isinstance(child_type, type) or not issubclass(child_type, AstNode):
299
361
  raise TypeError(f"Unexpected {child_type!r}")
300
- known_tag_names = sorted(known_tag_names)
362
+
363
+ def _get_allowed(known_tag_names, element):
364
+ return _tag_names_to_text(sorted(set(known_tag_names) - set(silent_allowed)), element)
301
365
 
302
366
  def _wrapper(func):
303
367
  @wraps(func)
304
368
  def _expect_children_decorator(cls, element: NSElement, *args, **kwargs):
369
+ known_tag_names = _get_allowed_tag_names(*expect_types, *silent_allowed)
370
+
305
371
  if len(element) < min_child_nodes:
306
- type_names = ", ".join(known_tag_names)
307
- suffix = f" (possible tags: {type_names})" if type_names else ""
308
- raise ExternalParsingError(
309
- f"<{element.tag}> should have {min_child_nodes} child nodes, "
310
- f"got {len(element)}{suffix}"
372
+ allowed = _get_allowed(known_tag_names, element)
373
+ raise InvalidXmlElement(
374
+ f"<{element.qname}> should have {min_child_nodes} child nodes,"
375
+ f" got only {len(element)}."
376
+ f" Allowed types are: {allowed}."
311
377
  )
378
+ for child in element:
379
+ if child.tag not in known_tag_names:
380
+ allowed = _get_allowed(known_tag_names, element)
381
+ raise InvalidXmlElement(
382
+ f"<{element.qname}> does not support a <{child.qname}> child node."
383
+ f" Allowed types are: {allowed}."
384
+ )
312
385
 
313
386
  return func(cls, element, *args, **kwargs)
314
387
 
@@ -317,4 +390,37 @@ def expect_children(min_child_nodes, *expect_types: str | type[BaseNode]):
317
390
  return _wrapper
318
391
 
319
392
 
393
+ def _get_allowed_tag_names(*expect_types: type[AstNode] | str) -> list[str]:
394
+ # Resolve arguments later, as get_tag_names() depends on __subclasses__()
395
+ # which may not be completely known at this point.
396
+ tag_names = []
397
+ for child_type in expect_types:
398
+ if isinstance(child_type, type) and issubclass(child_type, AstNode):
399
+ tag_names.extend(child_type.get_tag_names())
400
+ elif isinstance(child_type, str):
401
+ if not child_type.startswith("{"):
402
+ raise ValueError(
403
+ f"String arguments should be fully qualified XML namespaces, not {child_type!r}"
404
+ )
405
+ tag_names.append(child_type)
406
+ else:
407
+ raise TypeError(f"Unexpected {child_type!r}")
408
+ return tag_names
409
+
410
+
411
+ def _tag_names_to_text(tag_names: Iterable[str], user_element: NSElement) -> str:
412
+ body = _replace_common_ns(">, <".join(tag_names), user_element)
413
+ return f"<{body}>"
414
+
415
+
416
+ def _replace_common_ns(text: str, user_element: NSElement):
417
+ """In error messages, replace the full XML names with QName/prefixed versions.
418
+ The chosen prefixes reference the tags that the client submitted in their XML body.
419
+ """
420
+ for prefix, ns in user_element.ns_aliases.items():
421
+ text = text.replace(f"{{{ns}}}", f"{prefix}:")
422
+ return text
423
+
424
+
425
+ #: The tag registry to register new parsing classes at.
320
426
  tag_registry = TagRegistry()
@@ -13,16 +13,88 @@ can be very helpful to see which options each object type should support.
13
13
  from __future__ import annotations
14
14
 
15
15
  from . import lookups # noqa: F401 (need to register ORM lookups)
16
- from .expressions import ValueReference
16
+ from .expressions import (
17
+ BinaryOperator,
18
+ BinaryOperatorType,
19
+ Expression,
20
+ Function,
21
+ Literal,
22
+ ValueReference,
23
+ )
17
24
  from .filters import Filter
18
- from .identifiers import ResourceId
19
- from .operators import IdOperator
20
- from .sorting import SortBy
25
+ from .identifiers import Id, ResourceId, VersionActionTokens
26
+ from .operators import (
27
+ BetweenComparisonOperator,
28
+ BinaryComparisonName,
29
+ BinaryComparisonOperator,
30
+ BinaryLogicOperator,
31
+ BinaryLogicType,
32
+ BinarySpatialOperator,
33
+ ComparisonOperator,
34
+ DistanceOperator,
35
+ DistanceOperatorName,
36
+ ExtensionOperator,
37
+ IdOperator,
38
+ LikeOperator,
39
+ LogicalOperator,
40
+ MatchAction,
41
+ Measure,
42
+ NilOperator,
43
+ NonIdOperator,
44
+ NullOperator,
45
+ Operator,
46
+ SpatialOperator,
47
+ SpatialOperatorName,
48
+ TemporalOperator,
49
+ TemporalOperatorName,
50
+ UnaryLogicOperator,
51
+ UnaryLogicType,
52
+ )
53
+ from .sorting import SortBy, SortOrder, SortProperty
21
54
 
22
55
  __all__ = [
23
56
  "Filter",
57
+ # Expressions
58
+ "Expression",
59
+ "Function",
60
+ "Literal",
24
61
  "ValueReference",
62
+ # WFS 1.0 expressions
63
+ "BinaryOperator",
64
+ "BinaryOperatorType",
65
+ # Identifiers
66
+ "Id",
25
67
  "ResourceId",
68
+ "VersionActionTokens",
69
+ # Operators
70
+ "BetweenComparisonOperator",
71
+ "BinaryComparisonName",
72
+ "BinaryComparisonOperator",
73
+ "BinaryLogicOperator",
74
+ "BinaryLogicType",
75
+ "BinarySpatialOperator",
76
+ "ComparisonOperator",
77
+ "DistanceOperator",
78
+ "DistanceOperatorName",
79
+ "ExtensionOperator",
26
80
  "IdOperator",
81
+ "LikeOperator",
82
+ "LogicalOperator",
83
+ "MatchAction",
84
+ "Measure",
85
+ "NilOperator",
86
+ "NonIdOperator",
87
+ "NullOperator",
88
+ "Operator",
89
+ "SpatialOperator",
90
+ "SpatialOperatorName",
91
+ "TemporalOperator",
92
+ "TemporalOperatorName",
93
+ "UnaryLogicOperator",
94
+ "UnaryLogicType",
95
+ # Sorting
96
+ "SortBy",
97
+ "SortOrder",
98
+ "SortProperty",
27
99
  "SortBy",
28
100
  ]