diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c67ae3..f5fcb24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: env: BASE_IMAGE: "${{ vars.DOCKER_ORG }}/geospaas:2.5.2-python${{ matrix.python_version }}" IMAGE_NAME: "${{ vars.DOCKER_ORG }}/geospaas_harvesting" - METANORM_VERSION: '4.2.0' + METANORM_VERSION: '4.2.2' GEOSPAAS_DB_HOST: 'db' GEOSPAAS_DB_USER: 'test' GEOSPAAS_DB_PASSWORD: "${{ secrets.GEOSPAAS_DB_PASSWORD }}" diff --git a/geospaas_harvesting/providers/earthdata_cmr.py b/geospaas_harvesting/providers/earthdata_cmr.py index 2a68fbc..55bf239 100644 --- a/geospaas_harvesting/providers/earthdata_cmr.py +++ b/geospaas_harvesting/providers/earthdata_cmr.py @@ -1,6 +1,7 @@ """Code for searching EarthData CMR (https://www.earthdata.nasa.gov/)""" import json +import shapely.errors from shapely.geometry import LineString, Point, Polygon import geospaas.catalog.managers as catalog_managers @@ -18,7 +19,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.search_url = 'https://cmr.earthdata.nasa.gov/search/granules.umm_json' self.search_parameters_parser.add_arguments([ - WKTArgument('location', required=False, geometry_types=(LineString, Point, Polygon)), + EarthDataSpatialArgument('location', required=False, geometry_types=(LineString, Point, Polygon)), StringArgument('short_name', required=True, description='Short name of the collection'), ChoiceArgument('downloadable', valid_options=['true', 'false'], default='true'), StringArgument('platform'), @@ -49,11 +50,34 @@ def _make_spatial_parameter(self, geometry): result = {'line': ','.join([f"{lon},{lat}" for lon, lat in points])} elif isinstance(geometry, Point): result = {'point': f"{geometry.xy[0][0]},{geometry.xy[1][0]}"} + elif isinstance(geometry, str): + name, value = geometry.split('=') + result = {name: value} else: raise ValueError(f"Unsupported geometry type {type(geometry)}") return result +class EarthDataSpatialArgument(WKTArgument): + """Argument that provides the specific spatial format required by + queries to the CMR API. + See https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#g-spatial + for valid parameters. + """ + def parse(self, value): + valid_prefixes = ('polygon', 'bounding_box', 'point', 'line', 'circle') + try: + return super().parse(value) + except (shapely.errors.ShapelyError, ValueError) as error: + if isinstance(value, str): + for prefix in valid_prefixes: + if value.startswith(f"{prefix}=") or value.startswith(f"{prefix}[]="): + return value + raise ValueError( + "location should be a geometry or a valid CMR spatial parameter" + ) from error + + class EarthDataCMRCrawler(HTTPPaginatedAPICrawler): """Crawler for the CMR Earthdata search API""" diff --git a/tests/providers/test_earthdata_cmr.py b/tests/providers/test_earthdata_cmr.py index b4a17df..f2ebaea 100644 --- a/tests/providers/test_earthdata_cmr.py +++ b/tests/providers/test_earthdata_cmr.py @@ -53,8 +53,39 @@ def test_make_spatial_parameter(self): self.assertEqual( provider._make_spatial_parameter(Point((1, 2))), {'point': '1.0,2.0'}) + self.assertEqual( + provider._make_spatial_parameter('bounding_box=-180,60,180,90'), + {'bounding_box': '-180,60,180,90'}) with self.assertRaises(ValueError): provider._make_spatial_parameter(MultiPoint(((1, 2), (3, 4)))) + with self.assertRaises(ValueError): + provider._make_spatial_parameter('polygon:1.0,2.0,2.0,3.0,3.0,4.0,1.0,2.0') + + +class EarthDataSpatialArgumentTestCase(unittest.TestCase): + """Tests for the Earthdata CMR spatial argument""" + def setUp(self): + self.argument = provider_earthdata_cmr.EarthDataSpatialArgument( + 'location', geometry_types=(Polygon,)) + + def test_parse_wkt(self): + """Test parsing a WKT geometry""" + self.assertEqual(self.argument.parse('POLYGON((1 2,2 3,3 4,1 2))'), + Polygon(((1, 2), (2, 3), (3, 4), (1, 2)))) + + def test_parse_raw_spatial_extent(self): + """Test parsing raw CMR spatial extent parameters""" + self.assertEqual(self.argument.parse('bounding_box=-180,60,180,90'), + 'bounding_box=-180,60,180,90') + self.assertEqual(self.argument.parse('polygon=1.0,2.0,2.0,3.0,3.0,4.0,1.0,2.0'), + 'polygon=1.0,2.0,2.0,3.0,3.0,4.0,1.0,2.0') + self.assertEqual(self.argument.parse('line=1.0,2.0,3.0,4.0'), 'line=1.0,2.0,3.0,4.0') + self.assertEqual(self.argument.parse('point=1.0,2.0'), 'point=1.0,2.0') + + with self.assertRaises(ValueError): + self.argument.parse('foo=1.0,2.0') + with self.assertRaises(ValueError): + self.argument.parse('point:1.0,2.0') class EarthdataCMRCrawlerTestCase(unittest.TestCase):