|
19 | 19 | from openeo import BatchJob
|
20 | 20 | from openeo.api.process import Parameter
|
21 | 21 | from openeo.internal.graph_building import FlatGraphableMixin, PGNode
|
22 |
| -from openeo.metadata import _PYSTAC_1_9_EXTENSION_INTERFACE, TemporalDimension |
| 22 | +from openeo.metadata import ( |
| 23 | + _PYSTAC_1_9_EXTENSION_INTERFACE, |
| 24 | + Band, |
| 25 | + BandDimension, |
| 26 | + CubeMetadata, |
| 27 | + TemporalDimension, |
| 28 | +) |
23 | 29 | from openeo.rest import (
|
24 | 30 | CapabilitiesException,
|
25 | 31 | OpenEoApiError,
|
@@ -2737,6 +2743,21 @@ def test_load_result_filters(requests_mock):
|
2737 | 2743 |
|
2738 | 2744 |
|
2739 | 2745 | class TestLoadStac:
|
| 2746 | + @pytest.fixture |
| 2747 | + def build_stac_ref(self, tmp_path) -> typing.Callable[[dict], str]: |
| 2748 | + """ |
| 2749 | + Helper to dump (STAC) data to a temp file and return the path. |
| 2750 | + """ |
| 2751 | + # TODO #738 instead of working with local files: real request mocking of STAC resources compatible with pystac? |
| 2752 | + |
| 2753 | + def dump(data) -> str: |
| 2754 | + stac_path = tmp_path / "stac.json" |
| 2755 | + with stac_path.open("w", encoding="utf8") as f: |
| 2756 | + json.dump(data, fp=f) |
| 2757 | + return str(stac_path) |
| 2758 | + |
| 2759 | + return dump |
| 2760 | + |
2740 | 2761 | def test_basic(self, con120):
|
2741 | 2762 | cube = con120.load_stac("https://provider.test/dataset")
|
2742 | 2763 | assert cube.flat_graph() == {
|
@@ -2890,20 +2911,19 @@ def test_load_stac_from_job_empty_result(self, con120, requests_mock):
|
2890 | 2911 | }
|
2891 | 2912 |
|
2892 | 2913 | @pytest.mark.parametrize("temporal_dim", ["t", "datezz"])
|
2893 |
| - def test_load_stac_reduce_temporal(self, con120, tmp_path, temporal_dim): |
2894 |
| - stac_path = tmp_path / "stac.json" |
2895 |
| - stac_data = StacDummyBuilder.collection( |
2896 |
| - cube_dimensions={temporal_dim: {"type": "temporal", "extent": ["2024-01-01", "2024-04-04"]}} |
| 2914 | + def test_load_stac_reduce_temporal(self, con120, build_stac_ref, temporal_dim): |
| 2915 | + stac_ref = build_stac_ref( |
| 2916 | + StacDummyBuilder.collection( |
| 2917 | + cube_dimensions={temporal_dim: {"type": "temporal", "extent": ["2024-01-01", "2024-04-04"]}} |
| 2918 | + ) |
2897 | 2919 | )
|
2898 |
| - # TODO #738 real request mocking of STAC resources compatible with pystac? |
2899 |
| - stac_path.write_text(json.dumps(stac_data)) |
2900 | 2920 |
|
2901 |
| - cube = con120.load_stac(str(stac_path)) |
| 2921 | + cube = con120.load_stac(stac_ref) |
2902 | 2922 | reduced = cube.reduce_temporal("max")
|
2903 | 2923 | assert reduced.flat_graph() == {
|
2904 | 2924 | "loadstac1": {
|
2905 | 2925 | "process_id": "load_stac",
|
2906 |
| - "arguments": {"url": str(stac_path)}, |
| 2926 | + "arguments": {"url": stac_ref}, |
2907 | 2927 | },
|
2908 | 2928 | "reducedimension1": {
|
2909 | 2929 | "process_id": "reduce_dimension",
|
@@ -2957,66 +2977,174 @@ def test_load_stac_no_cube_extension_temporal_dimension(self, con120, tmp_path,
|
2957 | 2977 | cube = con120.load_stac(str(stac_path))
|
2958 | 2978 | assert cube.metadata.temporal_dimension == TemporalDimension(name="t", extent=dim_extent)
|
2959 | 2979 |
|
| 2980 | + def test_load_stac_default_band_handling(self, dummy_backend, build_stac_ref): |
| 2981 | + stac_ref = build_stac_ref( |
| 2982 | + StacDummyBuilder.collection( |
| 2983 | + # TODO #586 also cover STAC 1.1 style "bands" |
| 2984 | + summaries={"eo:bands": [{"name": "B01"}, {"name": "B02"}, {"name": "B03"}]} |
| 2985 | + ) |
| 2986 | + ) |
| 2987 | + |
| 2988 | + cube = dummy_backend.connection.load_stac(stac_ref) |
| 2989 | + assert cube.metadata.band_names == ["B01", "B02", "B03"] |
| 2990 | + |
| 2991 | + cube.execute() |
| 2992 | + assert dummy_backend.get_pg("load_stac")["arguments"] == { |
| 2993 | + "url": stac_ref, |
| 2994 | + } |
| 2995 | + |
2960 | 2996 | @pytest.mark.parametrize(
|
2961 | 2997 | "bands, expected_warning",
|
2962 | 2998 | [
|
2963 | 2999 | (
|
2964 | 3000 | ["B04"],
|
2965 |
| - "The specified bands ['B04'] are not a subset of the bands ['B01', 'B02', 'B03'] found in the STAC metadata (unknown bands: ['B04']). Using specified bands as is.", |
| 3001 | + "The specified bands ['B04'] in `load_stac` are not a subset of the bands ['B01', 'B02', 'B03'] found in the STAC metadata (unknown bands: ['B04']). Working with specified bands as is.", |
2966 | 3002 | ),
|
2967 | 3003 | (
|
2968 | 3004 | ["B03", "B04", "B05"],
|
2969 |
| - "The specified bands ['B03', 'B04', 'B05'] are not a subset of the bands ['B01', 'B02', 'B03'] found in the STAC metadata (unknown bands: ['B04', 'B05']). Using specified bands as is.", |
| 3005 | + "The specified bands ['B03', 'B04', 'B05'] in `load_stac` are not a subset of the bands ['B01', 'B02', 'B03'] found in the STAC metadata (unknown bands: ['B04', 'B05']). Working with specified bands as is.", |
2970 | 3006 | ),
|
2971 | 3007 | (["B03", "B02"], None),
|
2972 | 3008 | (["B01", "B02", "B03"], None),
|
2973 | 3009 | ],
|
2974 | 3010 | )
|
2975 |
| - def test_load_stac_band_filtering(self, con120, tmp_path, caplog, bands, expected_warning): |
2976 |
| - stac_path = tmp_path / "stac.json" |
2977 |
| - stac_data = StacDummyBuilder.collection( |
2978 |
| - summaries={"eo:bands": [{"name": "B01"}, {"name": "B02"}, {"name": "B03"}]} |
| 3011 | + def test_load_stac_band_filtering(self, dummy_backend, build_stac_ref, caplog, bands, expected_warning): |
| 3012 | + stac_ref = build_stac_ref( |
| 3013 | + StacDummyBuilder.collection( |
| 3014 | + # TODO #586 also cover STAC 1.1 style "bands" |
| 3015 | + summaries={"eo:bands": [{"name": "B01"}, {"name": "B02"}, {"name": "B03"}]} |
| 3016 | + ) |
2979 | 3017 | )
|
2980 |
| - # TODO #738 real request mocking of STAC resources compatible with pystac? |
2981 |
| - stac_path.write_text(json.dumps(stac_data)) |
2982 | 3018 |
|
2983 | 3019 | caplog.set_level(logging.WARNING)
|
2984 | 3020 | # Test with non-existing bands in the collection metadata
|
2985 |
| - cube = con120.load_stac(str(stac_path), bands=bands) |
| 3021 | + cube = dummy_backend.connection.load_stac(stac_ref, bands=bands) |
2986 | 3022 | assert cube.metadata.band_names == bands
|
2987 | 3023 | if expected_warning is None:
|
2988 | 3024 | assert caplog.text == ""
|
2989 | 3025 | else:
|
2990 | 3026 | assert expected_warning in caplog.text
|
2991 | 3027 |
|
2992 |
| - def test_load_stac_band_filtering_no_requested_bands(self, con120, tmp_path): |
2993 |
| - stac_path = tmp_path / "stac.json" |
2994 |
| - stac_data = StacDummyBuilder.collection( |
2995 |
| - summaries={"eo:bands": [{"name": "B01"}, {"name": "B02"}, {"name": "B03"}]} |
2996 |
| - ) |
2997 |
| - # TODO #738 real request mocking of STAC resources compatible with pystac? |
2998 |
| - stac_path.write_text(json.dumps(stac_data)) |
2999 |
| - |
3000 |
| - cube = con120.load_stac(str(stac_path)) |
3001 |
| - assert cube.metadata.band_names == ["B01", "B02", "B03"] |
| 3028 | + cube.execute() |
| 3029 | + assert dummy_backend.get_pg("load_stac")["arguments"] == { |
| 3030 | + "url": stac_ref, |
| 3031 | + "bands": bands, |
| 3032 | + } |
3002 | 3033 |
|
3003 |
| - def test_load_stac_band_filtering_no_metadata(self, con120, tmp_path, caplog): |
3004 |
| - stac_path = tmp_path / "stac.json" |
3005 |
| - stac_data = StacDummyBuilder.collection() |
3006 |
| - # TODO #738 real request mocking of STAC resources compatible with pystac? |
3007 |
| - stac_path.write_text(json.dumps(stac_data)) |
| 3034 | + def test_load_stac_band_filtering_no_band_metadata_default(self, dummy_backend, build_stac_ref, caplog): |
| 3035 | + stac_ref = build_stac_ref(StacDummyBuilder.collection()) |
3008 | 3036 |
|
3009 |
| - cube = con120.load_stac(str(stac_path)) |
| 3037 | + cube = dummy_backend.connection.load_stac(stac_ref) |
| 3038 | + # TODO #743: what should the default list of bands be? |
3010 | 3039 | assert cube.metadata.band_names == []
|
3011 | 3040 |
|
| 3041 | + cube.execute() |
| 3042 | + assert dummy_backend.get_pg("load_stac")["arguments"] == { |
| 3043 | + "url": stac_ref, |
| 3044 | + } |
| 3045 | + |
| 3046 | + @pytest.mark.parametrize( |
| 3047 | + ["bands", "has_band_dimension", "expected_pg_args", "expected_warning"], |
| 3048 | + [ |
| 3049 | + (None, False, {}, None), |
| 3050 | + ( |
| 3051 | + ["B02", "B03"], |
| 3052 | + True, |
| 3053 | + {"bands": ["B02", "B03"]}, |
| 3054 | + "Bands ['B02', 'B03'] were specified in `load_stac`, but no band dimension was detected in the STAC metadata. Working with band dimension and specified bands.", |
| 3055 | + ), |
| 3056 | + ], |
| 3057 | + ) |
| 3058 | + def test_load_stac_band_filtering_no_band_dimension( |
| 3059 | + self, dummy_backend, build_stac_ref, bands, has_band_dimension, expected_pg_args, expected_warning, caplog |
| 3060 | + ): |
| 3061 | + stac_ref = build_stac_ref(StacDummyBuilder.collection()) |
| 3062 | + |
| 3063 | + # This is a temporary mock.patch hack to make metadata_from_stac return metadata without a band dimension |
| 3064 | + # TODO #743: Do this properly through appropriate STAC metadata |
| 3065 | + from openeo.metadata import metadata_from_stac as orig_metadata_from_stac |
| 3066 | + |
| 3067 | + def metadata_from_stac(url: str): |
| 3068 | + metadata = orig_metadata_from_stac(url=url) |
| 3069 | + assert metadata.has_band_dimension() |
| 3070 | + metadata = metadata.drop_dimension("bands") |
| 3071 | + assert not metadata.has_band_dimension() |
| 3072 | + return metadata |
| 3073 | + |
| 3074 | + with mock.patch("openeo.rest.datacube.metadata_from_stac", new=metadata_from_stac): |
| 3075 | + cube = dummy_backend.connection.load_stac(stac_ref, bands=bands) |
| 3076 | + |
| 3077 | + assert cube.metadata.has_band_dimension() == has_band_dimension |
| 3078 | + |
| 3079 | + cube.execute() |
| 3080 | + assert dummy_backend.get_pg("load_stac")["arguments"] == { |
| 3081 | + **expected_pg_args, |
| 3082 | + "url": stac_ref, |
| 3083 | + } |
| 3084 | + |
| 3085 | + if expected_warning: |
| 3086 | + assert expected_warning in caplog.text |
| 3087 | + else: |
| 3088 | + assert not caplog.text |
| 3089 | + |
| 3090 | + def test_load_stac_band_filtering_no_band_metadata(self, dummy_backend, build_stac_ref, caplog): |
3012 | 3091 | caplog.set_level(logging.WARNING)
|
3013 |
| - cube = con120.load_stac(str(stac_path), bands=["B01", "B02"]) |
| 3092 | + stac_ref = build_stac_ref(StacDummyBuilder.collection()) |
| 3093 | + |
| 3094 | + cube = dummy_backend.connection.load_stac(stac_ref, bands=["B01", "B02"]) |
3014 | 3095 | assert cube.metadata.band_names == ["B01", "B02"]
|
3015 | 3096 | assert (
|
3016 |
| - "The specified bands ['B01', 'B02'] are not a subset of the bands [] found in the STAC metadata (unknown bands: ['B01', 'B02']). Using specified bands as is." |
| 3097 | + # TODO: better warning than confusing "not a subset of the bands []" ? |
| 3098 | + "The specified bands ['B01', 'B02'] in `load_stac` are not a subset of the bands [] found in the STAC metadata (unknown bands: ['B01', 'B02']). Working with specified bands as is." |
3017 | 3099 | in caplog.text
|
3018 | 3100 | )
|
3019 | 3101 |
|
| 3102 | + cube.execute() |
| 3103 | + assert dummy_backend.get_pg("load_stac")["arguments"] == { |
| 3104 | + "url": stac_ref, |
| 3105 | + "bands": ["B01", "B02"], |
| 3106 | + } |
| 3107 | + |
| 3108 | + @pytest.mark.parametrize( |
| 3109 | + ["bands", "expected_pg_args", "expected_warning"], |
| 3110 | + [ |
| 3111 | + (None, {}, None), |
| 3112 | + ( |
| 3113 | + ["B02", "B03"], |
| 3114 | + {"bands": ["B02", "B03"]}, |
| 3115 | + "The specified bands ['B02', 'B03'] in `load_stac` are not a subset of the bands ['Bz1', 'Bz2'] found in the STAC metadata (unknown bands: ['B02', 'B03']). Working with specified bands as is", |
| 3116 | + ), |
| 3117 | + ], |
| 3118 | + ) |
| 3119 | + def test_load_stac_band_filtering_custom_band_dimension( |
| 3120 | + self, dummy_backend, build_stac_ref, bands, expected_pg_args, expected_warning, caplog |
| 3121 | + ): |
| 3122 | + stac_ref = build_stac_ref(StacDummyBuilder.collection()) |
| 3123 | + |
| 3124 | + # This is a temporary mock.patch hack to make metadata_from_stac return metadata with a custom band dimension |
| 3125 | + # TODO #743: Do this properly through appropriate STAC metadata |
| 3126 | + from openeo.metadata import metadata_from_stac as orig_metadata_from_stac |
| 3127 | + |
| 3128 | + def metadata_from_stac(url: str): |
| 3129 | + return CubeMetadata(dimensions=[BandDimension(name="bandzz", bands=[Band("Bz1"), Band("Bz2")])]) |
| 3130 | + |
| 3131 | + with mock.patch("openeo.rest.datacube.metadata_from_stac", new=metadata_from_stac): |
| 3132 | + cube = dummy_backend.connection.load_stac(stac_ref, bands=bands) |
| 3133 | + |
| 3134 | + assert cube.metadata.has_band_dimension() |
| 3135 | + assert cube.metadata.band_dimension.name == "bandzz" |
| 3136 | + |
| 3137 | + cube.execute() |
| 3138 | + assert dummy_backend.get_pg("load_stac")["arguments"] == { |
| 3139 | + **expected_pg_args, |
| 3140 | + "url": stac_ref, |
| 3141 | + } |
| 3142 | + |
| 3143 | + if expected_warning: |
| 3144 | + assert expected_warning in caplog.text |
| 3145 | + else: |
| 3146 | + assert not caplog.text |
| 3147 | + |
3020 | 3148 |
|
3021 | 3149 | @pytest.mark.parametrize(
|
3022 | 3150 | "bands",
|
|
0 commit comments