Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

minor STAC API updates #991

Merged
merged 5 commits into from
Aug 4, 2024
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
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
Loading