Skip to content

Commit

Permalink
minor STAC API updates (#991)
Browse files Browse the repository at this point in the history
* minor STAC API updates
* add tests for required link properties
* STAC API: add bbox is geometry is null (#847)
* safeguard null geometry handling on STAC parsing (#847)
* update geometry/bbox handling
  • Loading branch information
tomkralidis authored Aug 4, 2024
1 parent 497c3ff commit fc5bda8
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 11 deletions.
10 changes: 5 additions & 5 deletions pycsw/core/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1769,7 +1769,8 @@ def _parse_oarec_record(context, repos, record):
if links:
_set(context, recobj, 'pycsw:Links', json.dumps(links))

_set(context, recobj, 'pycsw:BoundingBox', util.bbox2wktpolygon(util.geojson_geometry2bbox(record['geometry'])))
if record.get('geometry') is not None:
_set(context, recobj, 'pycsw:BoundingBox', util.bbox2wktpolygon(util.geojson_geometry2bbox(record['geometry'])))

if 'temporal' in record['properties'].get('extent', []):
_set(context, recobj, 'pycsw:TempExtent_begin', record['properties']['extent']['temporal']['interval'][0])
Expand All @@ -1784,6 +1785,7 @@ def _parse_stac_resource(context, repos, record):
recobj = repos.dataset()
keywords = []
links = []
bbox_wkt = None

stac_type = record.get('type', 'Feature')
if stac_type == 'Feature':
Expand All @@ -1793,7 +1795,8 @@ def _parse_stac_resource(context, repos, record):
stype = 'item'
title = record['properties'].get('title')
abstract = record['properties'].get('description')
bbox_wkt = util.bbox2wktpolygon(util.geojson_geometry2bbox(record['geometry']))
if record.get('geometry') is not None:
bbox_wkt = util.bbox2wktpolygon(util.geojson_geometry2bbox(record['geometry']))
elif stac_type == 'Collection':
LOGGER.debug('Parsing STAC Collection')
conformance = 'https://github.com/radiantearth/stac-spec/tree/master/collection-spec/collection-spec.md'
Expand All @@ -1804,8 +1807,6 @@ def _parse_stac_resource(context, repos, record):
if 'extent' in record and 'spatial' in record['extent']:
bbox_csv = ','.join(str(t) for t in record['extent']['spatial']['bbox'][0])
bbox_wkt = util.bbox2wktpolygon(bbox_csv)
else:
bbox_wkt = None
if 'extent' in record and 'temporal' in record['extent'] and 'interval' in record['extent']['temporal']:
_set(context, recobj, 'pycsw:TempExtent_begin', record['extent']['temporal']['interval'][0][0])
_set(context, recobj, 'pycsw:TempExtent_end', record['extent']['temporal']['interval'][0][1])
Expand All @@ -1816,7 +1817,6 @@ def _parse_stac_resource(context, repos, record):
stype = 'catalog'
title = record.get('title')
abstract = record.get('description')
bbox_wkt = None

_set(context, recobj, 'pycsw:Identifier', record['id'])
_set(context, recobj, 'pycsw:Typename', typename)
Expand Down
10 changes: 7 additions & 3 deletions pycsw/ogc/api/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,11 +1328,15 @@ def record2json(record, url, collection, mode='ogcapi-records'):
}

if mode == 'stac-api':
record_dict['properties']['datetime'] = to_rfc3339(record.date)
date_, date_type = to_rfc3339(record.date)
record_dict['properties']['datetime'] = date_

if None not in [record.time_begin, record.time_end]:
record_dict['properties']['start_datetime'] = to_rfc3339(record.time_begin)
record_dict['properties']['end_datetime'] = to_rfc3339(record.time_end)
start_date, start_date_type = to_rfc3339(record.time_begin)
end_date, end_date_type = to_rfc3339(record.time_end)

record_dict['properties']['start_datetime'] = start_date
record_dict['properties']['end_datetime'] = end_date

return record_dict

Expand Down
5 changes: 4 additions & 1 deletion pycsw/ogc/api/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ def json_serial(obj):
"""

if isinstance(obj, (datetime, date, time)):
return obj.isoformat() + 'Z'
if isinstance(obj, date):
return obj.strftime('%Y-%m-%d')
else:
return obj.isoformat() + 'Z'
elif isinstance(obj, bytes):
try:
LOGGER.debug('Returning as UTF-8 decoded bytes')
Expand Down
20 changes: 18 additions & 2 deletions pycsw/stac/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from pycsw.ogc.api.oapi import gen_oapi
from pycsw.ogc.api.records import API
from pycsw.core.pygeofilter_evaluate import to_filter
from pycsw.core.util import geojson_geometry2bbox

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -264,6 +265,9 @@ def collections(self, headers_, args):
'href': self.config['server']['url']
}]

response['numberMatched'] = len(response['collections'])
response['numberReturned'] = len(response['collections'])

return self.get_response(200, headers_, response)

def collection(self, headers_, args, collection='metadata:main'):
Expand Down Expand Up @@ -345,7 +349,7 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
headers, status, response = super().items(headers_, json_post_data, args, collection)

if collection not in self.get_all_collections():
msg = f'Invalid collection'
msg = 'Invalid collection'
LOGGER.exception(msg)
return self.get_exception(400, headers_, 'InvalidParameterValue', msg)

Expand Down Expand Up @@ -373,6 +377,18 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
else:
response2['features'].append(record)

if record.get('bbox') is None:
geometry = record.get('geometry')
if geometry is not None:
LOGGER.debug('Calculating bbox from geometry')
bbox = geojson_geometry2bbox(geometry)
record['bbox'] = [float(t) for t in bbox.split(',')]

for link in record['links']:
if link.get('rel') is None:
LOGGER.debug('Missing link relation; adding rel=related')
link['rel'] = 'related'

for count, value in enumerate(response2['links']):
if value['rel'] == 'alternate':
response2['links'].pop(count)
Expand Down Expand Up @@ -402,7 +418,7 @@ def item(self, headers_, args, collection, item):
headers, status, response = super().item(headers_, args, collection, item)

if collection not in self.get_all_collections():
msg = f'Invalid collection'
msg = 'Invalid collection'
LOGGER.exception(msg)
return self.get_exception(400, headers_, 'InvalidParameterValue', msg)

Expand Down
26 changes: 26 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 @@ -83,6 +83,19 @@ def test_conformance(config):
assert conformance in content['conformsTo']


def test_collections(config):
api = STACAPI(config)
headers, status, content = api.collections({}, {'f': 'json'})
content = json.loads(content)

assert headers['Content-Type'] == 'application/json'
assert status == 200
assert len(content['links']) == 3

assert len(content['collections']) == 1
assert len(content['collections']) == content['numberMatched']
assert len(content['collections']) == content['numberReturned']

def test_queryables(config):
api = STACAPI(config)
headers, status, content = api.queryables({}, {})
Expand Down Expand Up @@ -118,6 +131,15 @@ def test_items(config):
assert record['stac_version'] == '1.0.0'
assert record['collection'] == 'metadata:main'

for feature in content['features']:
if feature.get('geometry') is not None:
assert 'bbox' in feature
assert isinstance(feature['bbox'], list)

for link in feature['links']:
assert 'href' in link
assert 'rel' in link

# test GET KVP requests
content = json.loads(api.items({}, None, {'bbox': '-180,-90,180,90'})[2])
assert len(content['features']) == 3
Expand Down Expand Up @@ -194,5 +216,9 @@ def test_item(config):
assert content['stac_version'] == '1.0.0'
assert content['collection'] == 'metadata:main'

for link in content['links']:
assert 'href' in link
assert 'rel' in link

headers, status, content = api.item({}, {}, 'foo', item)
assert status == 400

0 comments on commit fc5bda8

Please sign in to comment.