diff --git a/CHANGES.md b/CHANGES.md index 899046848..536cb0741 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### titiler.core * fix `TerrainRGB` algorithm name (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/804) +* add more tests for `RescalingParams` and `HistogramParams` dependencies ## 0.18.0 (2024-03-22) diff --git a/src/titiler/application/tests/fixtures/item.json b/src/titiler/application/tests/fixtures/item.json index f8e447265..8a20a9ed5 100644 --- a/src/titiler/application/tests/fixtures/item.json +++ b/src/titiler/application/tests/fixtures/item.json @@ -1,18 +1,10 @@ { "type": "Feature", - "stac_version": "1.0.0-beta.1", - "stac_extensions": [ - "eo", - "view", - "proj" - ], + "stac_version": "1.0.0", "id": "S2A_34SGA_20200318_0_L2A", - "bbox": [ - 23.293255090449595, - 31.505183020453355, - 24.296453548295318, - 32.51147809805106 - ], + "properties": { + "datetime": "2020-03-18T09:11:33Z" + }, "geometry": { "type": "Polygon", "coordinates": [ @@ -40,91 +32,31 @@ ] ] }, - "properties": { - "datetime": "2020-03-18T09:11:33Z", - "platform": "sentinel-2a", - "constellation": "sentinel-2", - "instruments": [ - "msi" - ], - "gsd": 10, - "data_coverage": 73.85, - "view:off_nadir": 0, - "eo:cloud_cover": 89.84, - "proj:epsg": 32634, - "sentinel:latitude_band": "S", - "sentinel:grid_square": "GA", - "sentinel:sequence": "0", - "sentinel:product_id": "S2A_MSIL2A_20200318T085701_N0214_R007_T34SGA_20200318T115254", - "created": "2020-05-12T21:03:26.671Z", - "updated": "2020-05-12T21:03:26.671Z" - }, - "collection": "sentinel-s2-l2a-cogs", + "links": [ + { + "rel": "self", + "href": "https://myurl.com/item.json", + "type": "application/json" + } + ], "assets": { "B01": { - "title": "Band 1 (coastal)", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B01.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 1 (coastal)" }, "B09": { - "title": "Band 9", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B09.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 9" } }, - "links": [ - { - "rel": "self", - "href": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_34SGA_20200318_0_L2A/S2A_34SGA_20200318_0_L2A.json", - "type": "application/json" - }, - { - "rel": "parent", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "collection", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "root", - "href": "https://myurl.com/v0/" - }, - { - "title": "Source STAC Item", - "rel": "derived_from", - "href": "https://myurl.com/collections/sentinel-s2-l2a/items/S2A_34SGA_20200318_0_L2A", - "type": "application/json" - } - ] + "bbox": [ + 23.293255090449595, + 31.505183020453355, + 24.296453548295318, + 32.51147809805106 + ], + "stac_extensions": [], + "collection": "sentinel-s2-l2a-cogs" } diff --git a/src/titiler/core/tests/fixtures/item.json b/src/titiler/core/tests/fixtures/item.json index f8e447265..8a20a9ed5 100644 --- a/src/titiler/core/tests/fixtures/item.json +++ b/src/titiler/core/tests/fixtures/item.json @@ -1,18 +1,10 @@ { "type": "Feature", - "stac_version": "1.0.0-beta.1", - "stac_extensions": [ - "eo", - "view", - "proj" - ], + "stac_version": "1.0.0", "id": "S2A_34SGA_20200318_0_L2A", - "bbox": [ - 23.293255090449595, - 31.505183020453355, - 24.296453548295318, - 32.51147809805106 - ], + "properties": { + "datetime": "2020-03-18T09:11:33Z" + }, "geometry": { "type": "Polygon", "coordinates": [ @@ -40,91 +32,31 @@ ] ] }, - "properties": { - "datetime": "2020-03-18T09:11:33Z", - "platform": "sentinel-2a", - "constellation": "sentinel-2", - "instruments": [ - "msi" - ], - "gsd": 10, - "data_coverage": 73.85, - "view:off_nadir": 0, - "eo:cloud_cover": 89.84, - "proj:epsg": 32634, - "sentinel:latitude_band": "S", - "sentinel:grid_square": "GA", - "sentinel:sequence": "0", - "sentinel:product_id": "S2A_MSIL2A_20200318T085701_N0214_R007_T34SGA_20200318T115254", - "created": "2020-05-12T21:03:26.671Z", - "updated": "2020-05-12T21:03:26.671Z" - }, - "collection": "sentinel-s2-l2a-cogs", + "links": [ + { + "rel": "self", + "href": "https://myurl.com/item.json", + "type": "application/json" + } + ], "assets": { "B01": { - "title": "Band 1 (coastal)", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B01.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 1 (coastal)" }, "B09": { - "title": "Band 9", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B09.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 9" } }, - "links": [ - { - "rel": "self", - "href": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_34SGA_20200318_0_L2A/S2A_34SGA_20200318_0_L2A.json", - "type": "application/json" - }, - { - "rel": "parent", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "collection", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "root", - "href": "https://myurl.com/v0/" - }, - { - "title": "Source STAC Item", - "rel": "derived_from", - "href": "https://myurl.com/collections/sentinel-s2-l2a/items/S2A_34SGA_20200318_0_L2A", - "type": "application/json" - } - ] + "bbox": [ + 23.293255090449595, + 31.505183020453355, + 24.296453548295318, + 32.51147809805106 + ], + "stac_extensions": [], + "collection": "sentinel-s2-l2a-cogs" } diff --git a/src/titiler/core/tests/test_dependencies.py b/src/titiler/core/tests/test_dependencies.py index 4f1e8214a..9971d0c7e 100644 --- a/src/titiler/core/tests/test_dependencies.py +++ b/src/titiler/core/tests/test_dependencies.py @@ -246,6 +246,18 @@ def _assets_bidx(params=Depends(dependencies.AssetsBidxParams)): response = client.get("/third?assets=data&assets=image") assert response.json()["assets"] == ["data", "image"] + response = client.get( + "/third", + params=( + ("assets", "data"), + ("assets", "image"), + ), + ) + assert response.json()["assets"] == ["data", "image"] + + response = client.get("/third", params={"assets": ["data", "image"]}) + assert response.json()["assets"] == ["data", "image"] + response = client.get("/third") assert not response.json()["assets"] @@ -255,12 +267,37 @@ def _assets_bidx(params=Depends(dependencies.AssetsBidxParams)): assert response.json()["assets"] == ["data", "image"] assert response.json()["asset_indexes"] == {"data": [1, 2, 3], "image": [1]} + response = client.get( + "/third", + params=( + ("assets", "data"), + ("assets", "image"), + ("asset_bidx", "data|1,2,3"), + ("asset_bidx", "image|1"), + ), + ) + + assert response.json()["assets"] == ["data", "image"] + assert response.json()["asset_indexes"] == {"data": [1, 2, 3], "image": [1]} + response = client.get( "/third?assets=data&assets=image&asset_expression=data|b1/b2&asset_expression=image|b1*b2" ) assert response.json()["assets"] == ["data", "image"] assert response.json()["asset_expression"] == {"data": "b1/b2", "image": "b1*b2"} + response = client.get( + "/third", + params=( + ("assets", "data"), + ("assets", "image"), + ("asset_expression", "data|b1/b2"), + ("asset_expression", "image|b1*b2"), + ), + ) + assert response.json()["assets"] == ["data", "image"] + assert response.json()["asset_expression"] == {"data": "b1/b2", "image": "b1*b2"} + def test_bands(): """test bands deps.""" @@ -457,3 +494,103 @@ def _endpoint(algorithm=Depends(PostProcessParams)): assert response.json()["azimuth"] == 30 assert response.json()["buffer"] == 4 assert response.json()["input_nbands"] == 1 + + +def test_rescale_params(): + """test RescalingParams dependency.""" + app = FastAPI() + + @app.get("/") + def main(rescale=Depends(dependencies.RescalingParams)): + """return rescale.""" + return rescale + + client = TestClient(app) + + response = client.get("/", params={"rescale": "0,1"}) + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get("/?rescale=0,1") + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get("/?rescale=0,1&rescale=2,3") + assert response.status_code == 200 + assert response.json() == [[0, 1], [2, 3]] + + with pytest.raises(AssertionError): + client.get("/", params={"rescale": [0, 1]}) + + response = client.get("/", params={"rescale": [[0, 1]]}) + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get( + "/", + params=( + ("rescale", [0, 1]), + ("rescale", [0, 1]), + ), + ) + assert response.status_code == 200 + assert response.json() == [[0, 1], [0, 1]] + + response = client.get( + "/", + params=( + ("rescale", "0,1"), + ("rescale", "0,1"), + ), + ) + assert response.status_code == 200 + assert response.json() == [[0, 1], [0, 1]] + + response = client.get("/", params={"rescale": [[0, 1], [2, 3]]}) + assert response.status_code == 200 + assert response.json() == [[0, 1], [2, 3]] + + +def test_histogram_params(): + """Test HistogramParams dependency.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.HistogramParams)): + """return rescale.""" + return params + + client = TestClient(app) + + response = client.get( + "/", + params={"histogram_bins": "8"}, + ) + assert response.status_code == 200 + assert response.json()["bins"] == 8 + + response = client.get( + "/", + params={"histogram_bins": "8,9"}, + ) + assert response.status_code == 200 + assert response.json()["bins"] == [8.0, 9.0] + + response = client.get( + "/", + ) + assert response.status_code == 200 + assert response.json()["bins"] == 10 + + response = client.get( + "/", + params={"histogram_range": "8,9"}, + ) + assert response.status_code == 200 + assert response.json()["range"] == [8.0, 9.0] + + with pytest.raises(AssertionError): + client.get( + "/", + params={"histogram_range": "8"}, + ) diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index fea94b47c..4568b1077 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -146,6 +146,23 @@ class AssetsParams(DefaultDependency): ] = None +def parse_asset_indexes( + asset_indexes: Union[Sequence[str], Dict[str, Sequence[int]]], +) -> Dict[str, Sequence[int]]: + """parse asset indexes parameters.""" + return { + idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) + for idx in asset_indexes + } + + +def parse_asset_expression( + asset_expression: Union[Sequence[str], Dict[str, str]], +) -> Dict[str, str]: + """parse asset expression parameters.""" + return {idx.split("|")[0]: idx.split("|")[1] for idx in asset_expression} + + @dataclass class AssetsBidxExprParams(AssetsParams, BidxParams): """Assets, Expression and Asset's band Indexes parameters.""" @@ -199,10 +216,7 @@ def __post_init__(self): ) if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + self.asset_indexes = parse_asset_indexes(self.asset_indexes) if self.asset_indexes and self.indexes: warnings.warn( @@ -218,10 +232,7 @@ class AssetsBidxExprParamsOptional(AssetsBidxExprParams): def __post_init__(self): """Post Init.""" if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + self.asset_indexes = parse_asset_indexes(self.asset_indexes) if self.asset_indexes and self.indexes: warnings.warn( @@ -274,15 +285,10 @@ class AssetsBidxParams(AssetsParams, BidxParams): def __post_init__(self): """Post Init.""" if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + self.asset_indexes = parse_asset_indexes(self.asset_indexes) if self.asset_expression: - self.asset_expression: Dict[str, str] = { # type: ignore - idx.split("|")[0]: idx.split("|")[1] for idx in self.asset_expression - } + self.asset_expression = parse_asset_expression(self.asset_expression) if self.asset_indexes and self.indexes: warnings.warn( @@ -430,7 +436,20 @@ def RescalingParams( ) -> Optional[RescaleType]: """Min/Max data Rescaling""" if rescale: - return [tuple(map(float, r.replace(" ", "").split(","))) for r in rescale] + rescale_array = [] + for r in rescale: + parsed = tuple( + map( + float, + r.replace(" ", "").replace("[", "").replace("]", "").split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + return rescale_array return None @@ -528,7 +547,12 @@ def __post_init__(self): self.bins = 10 if self.range: - self.range = list(map(float, self.range.split(","))) # type: ignore + parsed = list(map(float, self.range.split(","))) + assert ( + len(parsed) == 2 + ), f"Invalid histogram_range values: {self.range}, should be of form 'min,max'" + + self.range = parsed # type: ignore def CoordCRSParams(