Skip to content
Merged
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
11 changes: 9 additions & 2 deletions pycsw/core/pygeofilter_evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@

import logging

from shapely import box
from shapely.geometry import shape
from sqlalchemy import text

from pygeofilter import ast
from pygeofilter.values import Envelope
from pygeofilter.backends.evaluator import handle
from pygeofilter.backends.sqlalchemy import filters
from pygeofilter.backends.sqlalchemy.evaluate import SQLAlchemyFilterEvaluator
Expand All @@ -43,7 +45,8 @@


class PycswFilterEvaluator(SQLAlchemyFilterEvaluator):
def __init__(self, field_mapping=None, dbtype='sqlite', undefined_as_null=None):
def __init__(self, field_mapping=None, dbtype='sqlite',
undefined_as_null=None):
super().__init__(field_mapping, undefined_as_null=undefined_as_null)
self._pycsw_dbtype = dbtype

Expand All @@ -56,7 +59,11 @@ def intersects(self, node, lhs, rhs):
except AttributeError:
crs = 4326

wkt = shape(node.rhs.geometry).wkt
if isinstance(node.rhs, Envelope):
wkt = box(node.rhs.x1, node.rhs.x2,
node.rhs.y1, node.rhs.y2, ccw=False).wkt
else:
wkt = shape(node.rhs.geometry).wkt

if self._pycsw_dbtype == 'postgresql+postgis+native':
return text(f"ST_Intersects({geometry}, 'SRID={crs};{wkt}')")
Expand Down
1 change: 1 addition & 0 deletions pycsw/ogc/pubsub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def publish_message(pubsub_client, action: str, collection: str = None,
"""
Publish broker message

:param pubsub_client: `paho.mqtt.client.Client` instance
:param action: `str` of action trigger name (create, update, delete)
:param collection: `str` of collection identifier
:param item: `str` of item identifier
Expand Down
83 changes: 73 additions & 10 deletions pycsw/stac/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
:returns: tuple of headers, status code, content
"""

cql_ops = []

if collection not in self.get_all_collections():
msg = 'Invalid collection'
LOGGER.exception(msg)
Expand All @@ -430,32 +432,93 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
LOGGER.debug('Empty JSON payload')
json_post_data = {}

if 'filter' in json_post_data:
LOGGER.debug('Detected filter query parameter')
json_post_data = json_post_data.pop('filter')

if 'bbox' in json_post_data:
LOGGER.debug('Detected bbox query parameter')
bbox_ = json_post_data.pop('bbox')
bbox_ = ','.join([str(b) for b in bbox_])
args['bbox'] = bbox_

cql_ops.append({
'op': 's_intersects', 'args': [{
'property': 'geometry'
},
{'bbox': json_post_data.pop('bbox')}]
})

if 'limit' in json_post_data:
LOGGER.debug('Detected limit query parameter')
args['limit'] = json_post_data.pop('limit')

if 'collections' in json_post_data:
LOGGER.debug('Detected limit query parameter')
args['collections'] = ','.join(json_post_data.pop('collections'))
LOGGER.debug('Detected collections query parameter')
cql_ops.append({
'op': 'in',
'args': [{
'property': 'collections',
},
json_post_data.pop('collections')
]
})

if 'ids' in json_post_data:
LOGGER.debug('Detected ids query parameter')
args['ids'] = ','.join(json_post_data.pop('ids'))
cql_ops.append({
'op': 'in',
'args': [{
'property': 'identifier',
},
json_post_data.pop('ids')
]
})

if 'intersects' in json_post_data:
LOGGER.debug('Detected intersects query parameter')
# TODO

if 'datetime' in json_post_data:
if '/' not in json_post_data['datetime']:
cql_ops.append({
'op': '=',
'args': [
{'property': 'date'},
json_post_data.pop('datetime')
]
})
else:
begin, end = json_post_data.pop('datetime').split('/')
if begin != '..':
cql_ops.append({
'op': '>=',
'args': [
{'property': 'time_begin'},
begin
]
})
if end != '..':
cql_ops.append({
'op': '<=',
'args': [
{'property': 'time_end'},
end
]
})

if 'filter' in json_post_data:
LOGGER.debug('Detected filter query parameter')
json_post_data = json_post_data.pop('filter')

if not json_post_data and not cql_ops:
LOGGER.debug('No JSON POST data or CQL ops')
elif not json_post_data and cql_ops:
LOGGER.debug('No JSON POST data left')
json_post_data = {
'op': 'and',
'args': cql_ops
}
else:
LOGGER.debug('JSON POST data is CQL2 JSON')
if cql_ops:
print("JJ2", json_post_data)
LOGGER.debug('Adding STAC API query parameters to CQL2 JSON')
json_post_data['args'].extend(cql_ops)

headers, status, response = super().items(headers_, json_post_data, args, collection)

response = json.loads(response)
Expand Down
179 changes: 179 additions & 0 deletions tests/functionaltests/suites/stac_api/test_stac_api_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ def test_items(config):
content = json.loads(api.items({}, {'bbox': [-180, -90, 180, 90]}, {})[2])
assert content['numberMatched'] == 31

content = json.loads(api.items({}, {'bbox': [-142, 42, -52, 84]}, {})[2])
assert content['numberMatched'] == 4

content = json.loads(api.items({},
{'bbox': [-180, -90, 180, 90], 'datetime': '2019-09-10T09:50:29.024000Z'}, # noqa
{})[2])
Expand Down Expand Up @@ -284,6 +287,107 @@ def test_items(config):
assert content['numberMatched'] == 1
assert content['features'][0]['properties']['view:off_nadir'] == 3.8

# test post CQL2 JSON requests
cql_json = {
'filter-lang': 'cql2-json',
'filter': {
'op': 'and',
'args': [{
'op': '=',
'args': [{
'property': 'parentidentifier'
},
'S2MSI1C']
}]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 12

cql_json = {
'filter-lang': 'cql2-json',
'filter': {
'op': 'and',
'args': [{
'op': '=',
'args': [{
'property': 'parentidentifier'
},
'S2MSI1C']
}]
}
}

content = json.loads(api.items({}, cql_json, {'limit': 1})[2])
assert content['numberMatched'] == 12
assert content['numberReturned'] == 1

cql_json = {
'filter-lang': 'cql2-json',
'limit': 1,
'filter': {
'op': 'and',
'args': [{
'op': '=',
'args': [{
'property': 'parentidentifier'
},
'S2MSI1C']
}]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 12
assert content['numberReturned'] == 1

cql_json = {
'bbox': [15,48,17,50],
'filter-lang': 'cql2-json',
'collections': ['S2MSI1Ci'],
'filter': {
'op': 'and',
'args': [
{
'op': '=',
'args': [
{
'property': 'identifier'
},
'S2B_MSIL1C_20190910T095029_N0500_R079_T33UWQ_20230429T151337.SAFE'
]
}
]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 0

cql_json = {
'bbox': [15,48,17,50],
'filter-lang': 'cql2-json',
'collections': ['S2MSI1C'],
'filter': {
'op': 'and',
'args': [
{
'op': '=',
'args': [
{
'property': 'identifier'
},
'S2B_MSIL1C_20190910T095029_N0500_R079_T33UWQ_20230429T151337.SAFE'
]
}
]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 1

cql_json = {
'filter-lang': 'cql2-json',
'filter': {
Expand Down Expand Up @@ -322,6 +426,30 @@ def test_items(config):
content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 0

cql_json = {
'filter-lang': 'cql2-json',
'filter': {
'op': 'and',
'args': [
{
'op': 'in',
'args': [
{
'property': 'collections'
},
[
'ARD_S3',
'sentinel-2-l2a'
]
]
}
]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 2

cql_json = {
'filter-lang': 'cql2-json',
'filter': {
Expand All @@ -346,6 +474,57 @@ def test_items(config):
content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 2

cql_json = {
'filter-lang': 'cql2-json',
'filter': {
'op': 'and',
'args': [{
'op': '=',
'args': [{
'property': 'identifier'
},
'S2B_MSIL2A_20190910T095029_N0500_R079_T33TXN_20230430T083712.SAFE' # noqa
]
}
]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 1

cql_json = {
"filter": {
"op": "and",
"args": [
{
"op": "=",
"args": [
{
"property": "parentidentifier"
},
"S2MSI2A"
]
},
{
"op": "in",
"args": [
{
"property": "title"
},
[
"S2B_MSIL2A_20190910T095029_N0500_R079_T33UXP_20230430T083712.SAFE", # noqa
"S2B_MSIL2A_20190910T095029_N0500_R079_T33UXQ_20230430T083712.SAFE" # noqa
]
]
}
]
}
}

content = json.loads(api.items({}, cql_json, {})[2])
assert content['numberMatched'] == 2

cql_json = {
'filter-lang': 'cql2-json',
'filter': {
Expand Down