Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ jobs:
pip3 install -r requirements-pubsub.txt
pip3 install .
pip3 install GDAL==`gdal-config --version`
pip3 install --force-reinstall https://github.com/geopython/pygeofilter/archive/main.zip
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps wait for a new pygeofilter release?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Will keep this PR open until the next release of pygeofilter (thus removing the above).

- name: setup test data ⚙️
run: |
pybabel compile -d locale -l es
Expand Down
37 changes: 11 additions & 26 deletions docs/source/cql.rst → docs/source/cql2.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
.. _cql:
.. _cql2:

CQL support
===========
CQL2 support
============

OGC Common Query Language (`CQL2`_) is a generic language designed to provide enhanced query and subset/filtering to (primarily) feature and record data.
`OGC Common Query Language`_ (CQL2) is a generic language designed to provide enhanced query and subset/filtering to (primarily) feature and record data.

Providers
---------
Expand All @@ -14,7 +14,7 @@ for current provider support.
Limitations
-----------

Support of CQL is limited to `Basic CQL2 <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-core>`_ and thus it allows to query with the
Support is limited to `Basic CQL2 <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-core>`_ and thus it allows to query with the
following predicates:

* comparison predicates
Expand All @@ -24,9 +24,9 @@ following predicates:
Formats
-------

Supported providers leverage the CQL2 dialect with the JSON encoding `CQL-JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_.
Supported providers leverage the CQL2 dialect with the JSON encoding `CQL JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_.

PostgreSQL supports both `CQL2 JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_ and `CQL text <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-text>`_ dialects.
PostgreSQL supports both `CQL JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_ and `CQL Text <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-text>`_ dialects.

Queries
^^^^^^^
Expand Down Expand Up @@ -83,7 +83,7 @@ Or
]
}'

The same ``BETWEEN`` query using HTTP GET request formatted as CQL text and URL encoded as below:
The same ``BETWEEN`` query using HTTP GET request formatted as CQL2 text and URL encoded as below:

.. code-block:: bash

Expand All @@ -103,25 +103,10 @@ An ``EQUALS`` example for a specific property:
]
}'

A ``CROSSES`` example via an HTTP GET request. The CQL text is passed via the ``filter`` parameter.
A ``S_CROSSES`` example via an HTTP GET request. The CQL2 text is passed via the ``filter`` parameter.

.. code-block:: bash

curl "http://localhost:5000/collections/hot_osm_waterways/items?f=json&filter=CROSSES(foo_geom,%20LINESTRING(28%20-2,%2030%20-4))"
curl "http://localhost:5000/collections/hot_osm_waterways/items?f=json&filter=S_CROSSES(foo_geom,%20LINESTRING(28%20-2,%2030%20-4))"

A ``DWITHIN`` example via HTTP GET and using a custom CRS for the filter geometry:

.. code-block:: bash

curl "http://localhost:5000/collections/beni/items?filter=DWITHIN(geometry,POINT(1392921%205145517),100,meters)&filter-crs=http://www.opengis.net/def/crs/EPSG/0/3857"


The same example, but this time providing a geometry in EWKT format:

.. code-block:: bash

curl "http://localhost:5000/collections/beni/items?filter=DWITHIN(geometry,SRID=3857;POINT(1392921%205145517),100,meters)"

Note that the CQL text has been URL encoded. This is required in curl commands but when entering in a browser, plain text can be used e.g. ``CROSSES(foo_geom, LINESTRING(28 -2, 30 -4))``.

.. _`CQL2`: https://docs.ogc.org/is/21-065r2/21-065r2.html
.. _`OGC Common Query Language`: https://docs.ogc.org/is/21-065r2/21-065r2.html
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ reference documentation on all aspects of the project.
plugins
html-templating
crs
cql
cql2
language
development
ogc-compliance
Expand Down
8 changes: 4 additions & 4 deletions docs/source/publishing/ogcapi-features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ To publish an Elasticsearch index, the following are required in your index:
The ES provider also has the support for the CQL queries as indicated in the table above.

.. seealso::
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.

.. _ERDDAP Tabledap Service:

Expand Down Expand Up @@ -292,7 +292,7 @@ These are optional and if not specified, the default from the engine will be use
This provider has support for the CQL queries as indicated in the Provider table above.

.. seealso::
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.


OGR
Expand Down Expand Up @@ -432,7 +432,7 @@ To publish an OpenSearch index, the following are required in your index:
The OpenSearch provider also has the support for the CQL queries as indicated in the table above.

.. seealso::
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.

.. _Oracle:

Expand Down Expand Up @@ -730,7 +730,7 @@ block contains the necessary socket connection information.
This provider has support for the CQL queries as indicated in the Provider table above.

.. seealso::
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.

SQLiteGPKG
^^^^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion docs/source/publishing/ogcapi-records.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ To publish an Elasticsearch index, the following are required in your index:
The ES provider also has the support for the CQL queries as indicated in the table above.

.. seealso::
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.

TinyDBCatalogue
^^^^^^^^^^^^^^^
Expand Down
13 changes: 8 additions & 5 deletions pygeoapi/api/itemtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from typing import Any, Tuple, Union
import urllib.parse

from pygeofilter.parsers.ecql import parse as parse_ecql_text
from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
from pygeofilter.parsers.cql2_json import parse as parse_cql2_json
from pyproj.exceptions import CRSError

Expand Down Expand Up @@ -84,7 +84,10 @@
'http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables-query-parameters', # noqa
'http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/create-replace-delete', # noqa
'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/schemas',
'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/core-roles-features' # noqa
'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/core-roles-features', # noqa
'http://www.opengis.net/spec/cql2/1.0/conf/cql2-text',
'http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2'

]

CONFORMANCE_CLASSES_RECORDS = [
Expand Down Expand Up @@ -488,7 +491,7 @@ def get_collection_items(

if cql_text is not None:
try:
filter_ = parse_ecql_text(cql_text)
filter_ = parse_cql2_text(cql_text)
filter_ = modify_pygeofilter(
filter_,
filter_crs_uri=filter_crs_uri,
Expand Down Expand Up @@ -522,8 +525,8 @@ def get_collection_items(

LOGGER.debug('Processing filter-lang parameter')
filter_lang = request.params.get('filter-lang')
# Currently only cql-text is handled, but it is optional
if filter_lang not in [None, 'cql-json', 'cql-text']:
filter_langs = [None, 'cql-json', 'cql-text', 'cql2-text', 'cql2-json']
if filter_lang not in filter_langs:
msg = 'Invalid filter language'
return api.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
Expand Down
10 changes: 5 additions & 5 deletions pygeoapi/formatter/csv_.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Authors: Tom Kralidis <tomkralidis@gmail.com>
#
# Copyright (c) 2022 Tom Kralidis
# Copyright (c) 2026 Tom Kralidis
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
Expand Down Expand Up @@ -66,12 +66,12 @@ def write(self, options: dict = {}, data: dict = None) -> str:

:returns: string representation of format
"""
type = data.get('type', '')
LOGGER.debug(f'Formatting CSV from data type: {type}')
type_ = data.get('type', '')
LOGGER.debug(f'Formatting CSV from data type: {type_}')

if 'Feature' in type or 'features' in data:
if 'Feature' in type_ or 'features' in data:
return self._write_from_geojson(options, data)
elif 'Coverage' in type or 'coverages' in data:
elif 'Coverage' in type_ or 'coverages' in data:
return self._write_from_covjson(options, data)

def _write_from_geojson(
Expand Down
2 changes: 1 addition & 1 deletion tests/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ def test_conformance(config, api_):

assert isinstance(root, dict)
assert 'conformsTo' in root
assert len(root['conformsTo']) == 42
assert len(root['conformsTo']) == 44
assert 'http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs' \
in root['conformsTo']

Expand Down
70 changes: 11 additions & 59 deletions tests/other/test_crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Authors: Tom Kralidis <tomkralidis@gmail.com>
#
# Copyright (c) 2025 Tom Kralidis
# Copyright (c) 2026 Tom Kralidis
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
Expand Down Expand Up @@ -33,7 +33,7 @@
import pytest
from pyproj.exceptions import CRSError
import pygeofilter.ast
from pygeofilter.parsers.ecql import parse
from pygeofilter.parsers.cql2_text import parse
from pygeofilter.values import Geometry
from shapely.geometry import Point

Expand Down Expand Up @@ -201,7 +201,7 @@ def test_transform_bbox():

@pytest.mark.parametrize('original_filter, filter_crs, storage_crs, geometry_colum_name, expected', [ # noqa
pytest.param(
'INTERSECTS(geometry, POINT(1 1))',
'S_INTERSECTS(geometry, POINT(1 1))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
None,
Expand All @@ -212,7 +212,7 @@ def test_transform_bbox():
id='passthrough'
),
pytest.param(
'INTERSECTS(geometry, POINT(1 1))',
'S_INTERSECTS(geometry, POINT(1 1))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
'custom_geom_name',
Expand All @@ -223,7 +223,7 @@ def test_transform_bbox():
id='unnested-geometry-name'
),
pytest.param(
'some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))',
'some_attribute = 10 AND S_INTERSECTS(geometry, POINT(1 1))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
'custom_geom_name',
Expand All @@ -238,31 +238,7 @@ def test_transform_bbox():
id='nested-geometry-name'
),
pytest.param(
'(some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))) OR '
'DWITHIN(geometry, POINT(2 2), 10, meters)',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
None,
'custom_geom_name',
pygeofilter.ast.Or(
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (1, 1)})
),
),
pygeofilter.ast.DistanceWithin(
pygeofilter.ast.Attribute(name='custom_geom_name'),
Geometry({'type': 'Point', 'coordinates': (2, 2)}),
distance=10,
units='meters',
)
),
id='complex-filter-name'
),
pytest.param(
'INTERSECTS(geometry, POINT(12.512829 41.896698))',
'S_INTERSECTS(geometry, POINT(12.512829 41.896698))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
Expand All @@ -273,7 +249,7 @@ def test_transform_bbox():
id='unnested-geometry-transformed-coords'
),
pytest.param(
'some_attribute = 10 AND INTERSECTS(geometry, POINT(12.512829 41.896698))', # noqa
'some_attribute = 10 AND S_INTERSECTS(geometry, POINT(12.512829 41.896698))', # noqa
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
Expand All @@ -288,31 +264,7 @@ def test_transform_bbox():
id='nested-geometry-transformed-coords'
),
pytest.param(
'(some_attribute = 10 AND INTERSECTS(geometry, POINT(12.512829 41.896698))) OR ' # noqa
'DWITHIN(geometry, POINT(12 41), 10, meters)',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
pygeofilter.ast.Or(
pygeofilter.ast.And(
pygeofilter.ast.Equal(
pygeofilter.ast.Attribute(name='some_attribute'), 10),
pygeofilter.ast.GeometryIntersects(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2313682.387730346, 4641308.550187246)}) # noqa
),
),
pygeofilter.ast.DistanceWithin(
pygeofilter.ast.Attribute(name='geometry'),
Geometry({'type': 'Point', 'coordinates': (2267681.8892602, 4543101.513292163)}), # noqa
distance=10,
units='meters',
)
),
id='complex-filter-transformed-coords'
),
pytest.param(
'INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
'S_INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
Expand All @@ -323,7 +275,7 @@ def test_transform_bbox():
id='unnested-geometry-transformed-coords-explicit-input-crs-ewkt'
),
pytest.param(
'INTERSECTS(geometry, POINT(1392921 5145517))',
'S_INTERSECTS(geometry, POINT(1392921 5145517))',
'http://www.opengis.net/def/crs/EPSG/0/3857',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
Expand All @@ -334,7 +286,7 @@ def test_transform_bbox():
id='unnested-geometry-transformed-coords-explicit-input-crs-filter-crs'
),
pytest.param(
'INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
'S_INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
None,
Expand All @@ -345,7 +297,7 @@ def test_transform_bbox():
id='unnested-geometry-transformed-coords-ewkt-crs-overrides-filter-crs'
),
pytest.param(
'INTERSECTS(geometry, POINT(12.512829 41.896698))',
'S_INTERSECTS(geometry, POINT(12.512829 41.896698))',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/3004',
'custom_geom_name',
Expand Down
6 changes: 3 additions & 3 deletions tests/provider/test_postgresql_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
import pyproj
from shapely.geometry import shape as geojson_to_geom

from pygeofilter.parsers.ecql import parse
from pygeofilter.parsers.cql2_text import parse

from pygeoapi.api import API
from pygeoapi.api.itemtypes import (
Expand Down Expand Up @@ -410,7 +410,7 @@ def test_get_not_existing_item_raise_exception(config):
80835475, 80835478, 80835483, 80835486]),
("osm_id BETWEEN 80800000 AND 80900000 AND waterway = 'stream'",
[80835470]),
("osm_id BETWEEN 80800000 AND 80900000 AND waterway ILIKE 'sTrEam'",
("osm_id BETWEEN 80800000 AND 80900000 AND CASEI(waterway) LIKE CASEI('sTrEam')", # noqa
[80835470]),
("osm_id BETWEEN 80800000 AND 80900000 AND waterway LIKE 's%'",
[80835470]),
Expand All @@ -421,7 +421,7 @@ def test_get_not_existing_item_raise_exception(config):
("osm_id BETWEEN 80800000 AND 80900000 AND BBOX(foo_geom, 29, -2.8, 29.2, -2.9)", # noqa
[80827793, 80835470, 80835472, 80835483, 80835489]),
("osm_id BETWEEN 80800000 AND 80900000 AND "
"CROSSES(foo_geom, LINESTRING(29.091 -2.731, 29.253 -2.845))",
"S_CROSSES(foo_geom, LINESTRING(29.091 -2.731, 29.253 -2.845))",
[80835470, 80835472, 80835489])
])
def test_query_cql(config, cql, expected_ids):
Expand Down
Loading