From a5539d1ce12532b4e73e63b11fcd1017c119535a Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Wed, 15 May 2024 18:49:19 +0200
Subject: [PATCH 1/8] fix issue of drop_non_raster with no proj:bbox: now
 looking for proj: or raster: properties

---
 simplestac/utils.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 410094e..20b9612 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -909,9 +909,10 @@ def apply_item(x, fun, name, output_dir, overwrite=False,
             x.add_asset(key=n, asset=asset)
     return x
 
-def drop_assets_without_proj(item, inplace=False):
+def drop_assets_without_proj(item, inplace=False, pattern="^proj:|^raster:"):
     """
-    Drops assets from the given item that do not have the "proj:bbox" field in their extra_fields.
+    Drops assets from the given item that do not have 
+    extra_fields with "proj:" or "raster:" prefix.
 
     Parameters:
         item (Item): The item from which to drop assets.
@@ -922,7 +923,12 @@ def drop_assets_without_proj(item, inplace=False):
     """
     if not inplace:
         item = item.clone()
-    item.assets = {k:v for k,v in item.assets.items() if "proj:bbox" in v.extra_fields}
+    
+    item.assets = {k:v for k,v in item.assets.items() if any([bool(re.search(pattern, p)) for p in v.extra_fields])}
+    
+    if len(item.assets) == 0:
+        logger.warning(f"Item {item.id} has no raster assets.")
+
     return item
 #######################################
 
-- 
GitLab


From 6b02f7c8d4eee51a8123333bd39d199f27d39762 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Wed, 15 May 2024 18:51:44 +0200
Subject: [PATCH 2/8] update changelog

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a17711..75902ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# v1.1.2
+- fix issue of drop_non_raster with no proj:bbox: now looking for any "proj:" or "raster:" properties
+
 # v1.1.1
 
 ## Add
-- 
GitLab


From 15959da85746d48e8e03f2969ae5fb673dbee16b Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Wed, 15 May 2024 19:19:37 +0200
Subject: [PATCH 3/8] add support for recursive item finding in
 build_item_collection

---
 simplestac/local.py | 46 +++++++++++++++++++++++++++++++++------------
 1 file changed, 34 insertions(+), 12 deletions(-)

diff --git a/simplestac/local.py b/simplestac/local.py
index 40576eb..baa8f4c 100644
--- a/simplestac/local.py
+++ b/simplestac/local.py
@@ -640,7 +640,35 @@ class MyStacItem(object):
         return item
 
 
+def get_item_dirs(input_dir, fmt):
+    """
+    Recursively retrieves item directories based on the input directory and format.
 
+    Parameters
+    ----------
+    input_dir : str
+        The input directory to search for item directories.
+    fmt : dict
+        The format containing the pattern for item directories.
+        See `collection_format`.
+    Returns
+    -------
+    list
+        A list of item directories found based on the format.
+    """    
+    item_dirs = []
+    if isinstance(input_dir, list):
+        for d in input_dir:
+            item_dirs.extend(get_item_dirs(d, fmt))
+        return item_dirs
+
+    input_dir = Path(input_dir).expand()
+    if re.match(fmt["item"]["pattern"], input_dir.name):
+        item_dirs.append(input_dir)
+    else:
+        item_dirs = get_item_dirs(input_dir.dirs(), fmt)
+    return item_dirs
+    
 def build_item_collection(input_dir,
                           fmt, 
                           item_parser=stac_item_parser,
@@ -680,19 +708,13 @@ def build_item_collection(input_dir,
     >>> col = build_item_collection(input_dir, fmt)
     """
     from simplestac.utils import ItemCollection # avoids circular import
-
-    if isinstance(input_dir, list):
-        items = []
-        for d in input_dir:
-            col = build_item_collection(d, fmt, **kwargs)
-            items.extend(col.items)
-        return ItemCollection(items, clone_items=False, **kwargs)
     
-    input_dir = Path(input_dir).expand()
-    if re.match(fmt["item"]["pattern"], input_dir.name):
-        item_dirs = [input_dir]
-    else:
-        item_dirs =  [d for d in input_dir.dirs() if re.match(fmt["item"]["pattern"], d.name)]
+    item_dirs = get_item_dirs(input_dir, fmt)
+    
+    if len(item_dirs) == 0:
+        logger.warning(f"No item found in {input_dir}")
+        return
+    
     items = []
     logger.info("Building item collection...")
     item_creator = MyStacItem(fmt, item_parser=item_parser, asset_parser=asset_parser)
-- 
GitLab


From 111884c730b9e9ded16810e7ffef7b9bcae02d69 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Wed, 15 May 2024 20:04:11 +0200
Subject: [PATCH 4/8] add parameter pattern to drop_non_raster and
 drop_asset_without_proj

---
 simplestac/utils.py | 62 ++++++++++++++++++++++++++++++++++-----------
 1 file changed, 47 insertions(+), 15 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 20b9612..1258ec9 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -62,11 +62,15 @@ S2_SEN2COR_BANDS = [f"B{i+1:02}" for i in range(12)]+["B8A"]
 class ExtendPystacClasses:
     """Add capacities to_xarray and filter to pystac Catalog, Collection, ItemCollection"""
 
-    def drop_non_raster(self, inplace=False):
-        """Drop non raster assets from each item in the collection.
+    def drop_non_raster(self, pattern="^proj:|^raster:", inplace=False):
+        """Drop non raster assets from each item in the collection,
+        based on pattern searched in asset extra_fields.
         
         Parameters
         ----------
+        pattern : str
+            The pattern to search for in asset extra_fields keys.
+            Defaults to "^proj:|^raster:".
         inplace : bool
             Whether to modify the collection in place. Defaults to False.
         
@@ -81,7 +85,7 @@ class ExtendPystacClasses:
             x = self.clone()
         
         for item in x.items: 
-            drop_assets_without_proj(item, inplace=True)
+            drop_assets_without_proj(item, pattern=pattern, inplace=True)
 
         if not inplace:
             return x
@@ -89,9 +93,31 @@ class ExtendPystacClasses:
     def to_xarray(self, xy_coords="center", bbox=None, geometry=None, gdal_env=DEFAULT_GDAL_ENV, **kwargs):
         """Returns a DASK xarray()
         
-        This is a proxy to stackstac.stac
+        This is a proxy to stackstac.stack
+
+        Parameters
+        ----------
+        xy_coords : str
+            Argument passed to stackstac.stack. Defaults to "center".
+        bbox : tuple, list
+            A bounding box to clip the xarray to, in the format (xmin, ymin, xmax, ymax).
+        geometry : shapely.geometry | geopandas.GeoSeries | geopandas.GeoDataFrame
+            A geometry to clip the xarray to.
+        gdal_env : stackstac.rio_reader.LayeredEnv
+            See stackstac.rio_reader.LayeredEnv. The default is DEFAULT_GDAL_ENV,
+            which is the same as stackstac.rio_reader.DEFAULT_GDAL_ENV with the addition
+            of GDAL_HTTP_MAX_RETRY=5 and GDAL_HTTP_RETRY_DELAY=1.
+        kwargs: dict
+            Additional keyword arguments passed to stackstac.stack.
         
-        Arguments are:
+        Returns
+        -------
+        dask.DataArray
+            
+
+        Notes
+        -----
+        The parameters available in stackstac.stack:
         assets=frozenset({'image/jp2', 'image/tiff', 'image/vnd.stac.geotiff', 'image/x.geotiff'}),
         epsg=None, resolution=None, bounds=None, bounds_latlon=None,
         snap_bounds=True, resampling=Resampling.nearest, chunksize=1024,
@@ -101,12 +127,10 @@ class ExtendPystacClasses:
         errors_as_nodata=(RasterioIOError('HTTP response code: 404'), ),
         reader=<class 'stackstac.rio_reader.AutoParallelRioReader'>
 
-        For details, see [stackstac.stac](https://stackstac.readthedocs.io/en/latest/api/main/stackstac.stack.html)
+        For details, see [stackstac.stack](https://stackstac.readthedocs.io/en/latest/api/main/stackstac.stack.html).
 
-        Notes:
-        ------
         Here, xy_coords="center" is the default to be consistent with rioxarray,
-        cf https://github.com/gjoseph92/stackstac/issues/207. Otherwise, stackstac.stac has
+        cf https://github.com/gjoseph92/stackstac/issues/207. Otherwise, stackstac.stack has
         xy_coords="topleft" as the default.
 
         Also, by default it is sorted by ascending datetime, see sortby_date.
@@ -909,17 +933,25 @@ def apply_item(x, fun, name, output_dir, overwrite=False,
             x.add_asset(key=n, asset=asset)
     return x
 
-def drop_assets_without_proj(item, inplace=False, pattern="^proj:|^raster:"):
+def drop_assets_without_proj(item, pattern="^proj:|^raster:", inplace=False):
     """
     Drops assets from the given item that do not have 
     extra_fields with "proj:" or "raster:" prefix.
 
-    Parameters:
-        item (Item): The item from which to drop assets.
-        inplace (bool, optional): If True, the assets will be dropped in place. Otherwise, a clone of the item will be created and modified.
+    Parameters
+    ----------
+    item: pystac.Item
+      The item from which to drop assets.
+    pattern: str, optional. 
+        The pattern to search for in asset extra_fields keys.
+    inplace: bool, optional
+        If True, the assets will be dropped in place.
+        Otherwise, a clone of the item will be created and modified.
 
-    Returns:
-        Item: The modified item with the dropped assets.
+    Returns
+    ------
+    pystac.Item
+        The modified item with the dropped assets.
     """
     if not inplace:
         item = item.clone()
-- 
GitLab


From e51da4d576a8e9aac1c7bcd7c119351c9f10bab9 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Wed, 15 May 2024 21:51:41 +0200
Subject: [PATCH 5/8] add tests and update changelog

---
 CHANGELOG.md         |  9 +++++-
 tests/conftest.py    | 24 +++++++++++++---
 tests/test_remote.py | 68 ++++++++++++++++----------------------------
 3 files changed, 52 insertions(+), 49 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75902ba..d1250eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # v1.1.2
-- fix issue of drop_non_raster with no proj:bbox: now looking for any "proj:" or "raster:" properties
+
+## Add
+- parameter `pattern` to `ItemCollection.drop_non_raster` and `drop_asset_without_proj`
+- support for recursive item search in `build_item_collection`
+
+## Fix
+- fix issue of drop_non_raster with no proj:bbox: now looking for any "proj:" or "raster:" properties.
+  A parameter `pattern` was added to 
 
 # v1.1.1
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 39b8819..30c7dd1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,8 +1,10 @@
-import pystac
-import pytest
-from path import Path
 import geopandas as gpd
 import logging
+from path import Path
+import planetary_computer as pc
+import pystac_client
+import pytest
+
 logging.basicConfig(level=logging.INFO)
 
 here = Path(__file__).parent
@@ -28,4 +30,18 @@ def roi(roi_file):
 @pytest.fixture(scope="session")
 def s2scene_pc_dir():
     scene_dir = here / "data" / "s2_scenes_pc"
-    yield scene_dir
\ No newline at end of file
+    yield scene_dir
+
+@pytest.fixture(scope="session")
+def pc_col(roi):
+    URL = "https://planetarycomputer.microsoft.com/api/stac/v1"
+    time_range = "2022-01-20/2022-01-31"
+    catalog = pystac_client.Client.open(URL, modifier=pc.sign_inplace)
+    search = catalog.search(
+        collections=["sentinel-2-l2a"],
+        bbox=roi.to_crs(4326).total_bounds,
+        datetime=time_range,
+        sortby="datetime",
+    )
+    col = search.item_collection()
+    yield col
\ No newline at end of file
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 91153a3..8a6c045 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -6,31 +6,13 @@ import numpy as np
 
 URL = "https://planetarycomputer.microsoft.com/api/stac/v1"
 
-def test_to_xarray(roi, s2scene_pc_dir):
-    time_range = "2022-01-01/2022-01-31"
-
-    catalog = pystac_client.Client.open(URL, modifier=pc.sign_inplace)
-    search = catalog.search(
-        collections=["sentinel-2-l2a"],
-        bbox=roi.to_crs(4326).total_bounds,
-        datetime=time_range,
-        sortby="datetime",
-    )
-    col = ItemCollection(search.item_collection())
+def test_to_xarray(pc_col, roi):
+    col = ItemCollection(pc_col)
     x = col.drop_non_raster().to_xarray()
     assert len(x.time) == len(col)
 
-def test_offset_harmonization(roi, s2scene_pc_dir):
-    time_range = "2022-01-20/2022-01-31"
-
-    catalog = pystac_client.Client.open(URL, modifier=pc.sign_inplace)
-    search = catalog.search(
-        collections=["sentinel-2-l2a"],
-        bbox=roi.to_crs(4326).total_bounds,
-        datetime=time_range,
-        sortby="datetime",
-    )
-    col = search.item_collection()
+def test_offset_harmonization(pc_col):
+    col = ItemCollection(pc_col)
     harmonize_sen2cor_offset(col, inplace=True)
     of0 = col[0].assets["B02"].extra_fields["raster:bands"][0]["offset"]
     ofN = col[-1].assets["B02"].extra_fields["raster:bands"][0]["offset"]
@@ -38,34 +20,31 @@ def test_offset_harmonization(roi, s2scene_pc_dir):
     assert of0 == 0
     assert ofN == -1000
 
-def test_drop_duplicates(roi, s2scene_pc_dir):
-    time_range = "2022-01-20/2022-01-31"
-    catalog = pystac_client.Client.open(URL, modifier=pc.sign_inplace)
-    search = catalog.search(
-        collections=["sentinel-2-l2a"],
-        bbox=roi.to_crs(4326).total_bounds,
-        datetime=time_range,
-        sortby="datetime",
-    )
-    col = search.item_collection()
+def test_drop_duplicates(pc_col):
+    col = ItemCollection(pc_col)
     col1 = ItemCollection(col.clone()+col.clone())
+    assert len(col1) == 2*len(col)
     col1.drop_duplicates(inplace=True)
     assert len(col1) == len(col)
 
-def test_write_assets(roi, s2scene_pc_dir):
+def test_drop_non_raster(pc_col):
+    col = ItemCollection(pc_col)
+    col1 = col.drop_non_raster()
+    assert "preview" in col[0].assets
+    assert "preview" not in col1[0].assets
 
-    s2scene_pc_dir.rmtree_p().mkdir_p()
-    time_range = "2016-01-01/2016-01-31"
+def test_filter(pc_col):
+    col = ItemCollection(pc_col)
+    col1 = col.filter(assets="B02")
+    assert "B03" in col[0].assets
+    assert "B03" not in col1[0].assets
 
-    catalog = pystac_client.Client.open(URL)
-    search = catalog.search(
-        collections=["sentinel-2-l2a"],
-        bbox=roi.to_crs(4326).total_bounds,
-        datetime=time_range,
-        query={"eo:cloud_cover": {"lt": 80}},
-    )
+def test_write_assets(pc_col, roi, s2scene_pc_dir):
+
+    s2scene_pc_dir.rmtree_p().mkdir_p()
 
-    col = ItemCollection(search.item_collection()).drop_non_raster()
+    col = ItemCollection(pc_col)
+    col.drop_non_raster(inplace=True)
     bbox = roi.to_crs(col.to_xarray().rio.crs).total_bounds
     encoding=dict(
         dtype="int16", 
@@ -86,6 +65,7 @@ def test_write_assets(roi, s2scene_pc_dir):
         new_col2 = write_assets(col, tempdir, geometry=roi.buffer(5), encoding=encoding, modifier=pc.sign_inplace)
         assert len(new_col2) == len(new_col)
         assert new_col2[0].bbox == new_col[0].bbox
-    
+
+
 
     
-- 
GitLab


From df915b3afb0268362fdbfb409b64234fd4c086f9 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Thu, 16 May 2024 01:50:36 +0200
Subject: [PATCH 6/8] add cllection_ready parameter to apply_items

---
 simplestac/utils.py | 41 ++++++++++++++++++++++++++++++++++-------
 tests/test_local.py | 43 ++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 72 insertions(+), 12 deletions(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index 1258ec9..e6427db 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -364,6 +364,7 @@ class ExtendPystacClasses:
     def apply_items(self, fun,
                     name,
                     output_dir,
+                    collection_ready=False,
                     overwrite=False,
                     inplace=False,
                     datetime=None,
@@ -386,6 +387,9 @@ class ExtendPystacClasses:
             This also serves as the file name suffix: "{item.id}_{name}.tif"
         output_dir : str
             The directory where the output will be saved. Created if it does not exist.
+        collection_ready : bool, optional
+            If True, the assets directory will be `output_dir / item.id`, ready for a pystac.Collection.
+            Defaults to False.
         overwrite : bool, optional
             Whether to overwrite existing files. Defaults to False.
         inplace : bool, optional
@@ -421,16 +425,18 @@ class ExtendPystacClasses:
 
         for item in tqdm(x.items, disable=not progress):
             apply_item(item, fun, name=name, output_dir=output_dir,
-                            overwrite=overwrite, copy=False, 
-                            bbox=bbox, geometry=geometry,
-                            writer_args=writer_args,
-                            **kwargs)
+                        collection_ready=collection_ready,
+                        overwrite=overwrite, copy=False, 
+                        bbox=bbox, geometry=geometry,
+                        writer_args=writer_args,
+                        **kwargs)
         if not inplace:
             return x
 
     def apply_rolling(self, fun, 
                       name, 
                       output_dir,
+                      collection_ready=False,
                       overwrite=False,
                       window=2,
                       inplace=False,
@@ -455,6 +461,8 @@ class ExtendPystacClasses:
             This also serves as the file name suffix: "{item.id}_{name}.tif"
         output_dir : str
             The directory where the output will be saved. Created if it does not exist.
+        collection_ready : bool, optional
+            If True, the assets directory will be `output_dir / item.id`, ready for a pystac.Collection.
         overwrite : bool, optional
             Whether to overwrite existing files. Defaults to False.
         inplace : bool, optional
@@ -512,15 +520,24 @@ class ExtendPystacClasses:
                 raise ValueError("Argument `writer_args` must have length 1 or the same length as `name`.")
         
         Nout = len(name)
-        
         output_dir = [Path(d).expand().mkdir_p() for d in output_dir] # make sure they exist 
+
         for i in tqdm(range(len(x.items)), disable=not progress):
             subitems = x.items[max((i-window+1),0):i+1]
+            item_id = x.items[i].id
+
             if center:
+                if window%2 == 0:
+                    raise ValueError("window must be odd if center=True")
                 subitems = x.items[max(i-window//2,0):i+(window-1)//2+1]
 
             subcol = self.__class__(subitems, clone_items=False)
-            raster_file = [d / f"{subitems[-1].id}_{n}.tif" for n, d in zip(name, output_dir)]
+            raster_dir = output_dir
+            if collection_ready:
+                raster_dir = [(d / item_id) for d in output_dir]
+            raster_file = [d / f"{item_id}_{n}.tif" for n, d in zip(name, raster_dir)]
+            
+
             if not overwrite and all([r.exists() for r in raster_file]):
                 logger.debug(f"File already exists, skipping computation: {raster_file}")
                 res = tuple([None]*Nout)
@@ -528,6 +545,8 @@ class ExtendPystacClasses:
                 # compute fun
                 with xr.set_options(keep_attrs=True):
                     res = fun(subcol.to_xarray(bbox=bbox, geometry=geometry), **kwargs)
+                if res is None:
+                    continue
                 if not isinstance(res, tuple):
                     res = (res,)
                 if len(res) != Nout:
@@ -538,6 +557,7 @@ class ExtendPystacClasses:
                     # write result
                     logger.debug("Writing: ", f)
                     r.name = n
+                    f.parent.mkdir_p()
                     write_raster(r, f, overwrite=overwrite, **wa)
                     
             for n, f in zip(name, raster_file):
@@ -801,7 +821,7 @@ def update_item_properties(x: pystac.Item, remove_item_props=DEFAULT_REMOVE_PROP
         for k in pop_props:
             x.properties.pop(k)
 
-def apply_item(x, fun, name, output_dir, overwrite=False,
+def apply_item(x, fun, name, output_dir, collection_ready=False, overwrite=False,
                copy=True, bbox=None, geometry=None, writer_args=None, **kwargs):
     """
     Applies a function to an item in a collection, 
@@ -818,6 +838,9 @@ def apply_item(x, fun, name, output_dir, overwrite=False,
         The name or names of the output raster file(s).
     output_dir : str or list of str
         The directory or directories to save the output raster file(s) to.
+    collection_ready : bool, optional
+        If True, the assets directory will be `output_dir / item.id`, ready for a pystac.Collection.
+        Defaults to `False`.
     overwrite : bool, optional
         Whether to overwrite existing raster files. Defaults to `False`.
     copy : bool, optional
@@ -895,6 +918,10 @@ def apply_item(x, fun, name, output_dir, overwrite=False,
     Nout = len(name)
     output_dir = [Path(d).expand().mkdir_p() for d in output_dir] 
     
+    # add item id level: output_dir / item.id
+    if collection_ready:
+        output_dir = [(d / x.id).mkdir_p() for d in output_dir]
+
     raster_file = [d / f"{x.id}_{n}.tif" for n, d in zip(name, output_dir)]
     if not overwrite and all([r.exists() for r in raster_file]):
         logger.debug(f"File already exists, skipping computation: {raster_file}")
diff --git a/tests/test_local.py b/tests/test_local.py
index 49e7263..df75cc7 100644
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -45,20 +45,36 @@ def test_datetime(s2scene_dir):
 
 def test_apply_items(s2scene_dir, roi):
     col = build_item_collection(s2scene_dir, collection_format())
-    NDVI_dir = s2scene_dir.parent / "NDVI"
-    NDVI_dir.rmtree_p()
+    output_dir = s2scene_dir.parent / "NDVI"
+    output_dir.rmtree_p()
     col.apply_items(
         apply_formula, 
         name="NDVI",
         geometry=roi.geometry,
         formula="((B08-B04)/(B08+B04))",
-        output_dir=NDVI_dir,
+        output_dir=output_dir,
         inplace=True)
     assert "NDVI" in col.items[-1].assets
-    assert len(NDVI_dir.files()) == len(col)
+    assert len(output_dir.files()) == len(col)
     # check if COG
     assert col.items[-1].assets["NDVI"].media_type == pystac.MediaType.COG
 
+    # with collection_ready
+    output_dir = s2scene_dir.parent / "S2-SSI"
+    output_dir.rmtree_p()
+    col.apply_items(
+        apply_formula, 
+        name="NDVI",
+        geometry=roi.geometry,
+        formula="((B08-B04)/(B08+B04))",
+        output_dir=output_dir,
+        collection_ready=True,
+        inplace=True)
+    assert "NDVI" in col.items[-1].assets
+    assert len(output_dir.dirs()) == len(col)
+
+
+
 def test_apply_rolling(s2scene_dir):
     col = build_item_collection(s2scene_dir, collection_format())
     output_dir = s2scene_dir.parent / "B07_diff"
@@ -76,7 +92,24 @@ def test_apply_rolling(s2scene_dir):
     assert "B07_diff" in col.items[-1].assets
     assert len(output_dir.files()) == (len(col)-1)
 
-  
+    # with collection_ready and multiple outputs
+    def band_diff(x, bands=["B07", "B08"]):
+        if len(x.time) > 1:
+            res = x.sel(band=bands).diff("time")
+            return tuple([res.sel(band=b) for b in bands])
+    output_dir = s2scene_dir.parent / "S2-diff"
+    output_dir.rmtree_p()
+    col.apply_rolling(
+        band_diff, 
+        name=["B07_diff", "B08_diff"],
+        output_dir=output_dir,
+        collection_ready=True,
+        inplace=True,
+        window=2)
+    assert "B07_diff" in col.items[-1].assets
+    assert len(output_dir.dirs()) == (len(col)-1)
+    assert len(list(output_dir.walkfiles())) == (len(col)-1)*2
+
 
 def test_apply_items_raster_args(s2scene_dir, roi):
     col = build_item_collection(s2scene_dir, collection_format())
-- 
GitLab


From 2578a5b265aab78f2f5b2283eda0607c912d9dc8 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Thu, 16 May 2024 01:55:47 +0200
Subject: [PATCH 7/8] add example to apply_formula

---
 simplestac/utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/simplestac/utils.py b/simplestac/utils.py
index e6427db..d53f9ca 100644
--- a/simplestac/utils.py
+++ b/simplestac/utils.py
@@ -1049,7 +1049,7 @@ def apply_formula(x, formula):
     x : xarray.DataArray
         It should have a 'band' dimension with the names that will be used by formula.
     formula : str
-        Formula, e.g. "B02>700", "CLM > 0", "SLC in [4,5]", "(B08-B06)/(B08+B06)"
+        Formula, e.g. "B02 > 600", "CLM > 0", "B02 > 600 | ~SLC in [4,5]", "(B08-B06)/(B08+B06)"
 
     Returns
     -------
-- 
GitLab


From e3cac735536844a71ff860e938e97c2e2847c122 Mon Sep 17 00:00:00 2001
From: Florian de Boissieu <fdeboiss@gmail.com>
Date: Thu, 16 May 2024 01:58:41 +0200
Subject: [PATCH 8/8] update changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1250eb..9c1f1bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Add
 - parameter `pattern` to `ItemCollection.drop_non_raster` and `drop_asset_without_proj`
 - support for recursive item search in `build_item_collection`
+- parameter `collection_ready` to `apply_item`, `apply_items`, `apply_rolling`
 
 ## Fix
 - fix issue of drop_non_raster with no proj:bbox: now looking for any "proj:" or "raster:" properties.
-- 
GitLab