From 697145226b0979a3ab915fda493459306391f9d6 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 28 Oct 2023 23:22:26 +0200 Subject: [PATCH 01/13] Updated test_filter --- oteapi_dlite/strategies/filter.py | 164 +++++++++++++++++++++++------- oteapi_dlite/strategies/parse.py | 2 +- requirements_dev.txt | 1 + setup.cfg | 2 +- tests/strategies/test_filter.py | 90 ++++++++++++---- 5 files changed, 202 insertions(+), 57 deletions(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 9c48a707..e5991966 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -1,63 +1,155 @@ -"""Trivial filter that adds an empty collection to the session.""" +"""Filter that removes all but specified instances in the collection.""" # pylint: disable=unused-argument -from typing import TYPE_CHECKING, Any, Dict +import re +from typing import TYPE_CHECKING -import dlite -from oteapi.datacache import DataCache -from oteapi.models import FilterConfig +from dlite.utils import get_referred_instances +from oteapi.models import AttrDict, FilterConfig from pydantic import Field from pydantic.dataclasses import dataclass from oteapi_dlite.models import DLiteSessionUpdate +from oteapi_dlite.utils import get_collection, update_collection if TYPE_CHECKING: - from typing import Optional + from typing import Any, Dict, Optional -@dataclass -class CreateCollectionStrategy: - """Trivial filter that adds an empty collection to the session. +class DLiteQueryConfig(AttrDict): + """Configuration for the DLite filter strategy. - **Registers strategies**: + First the `remove_label` and `remove_datamodel` configurations are + used to mark matching instances for removal. If neither + `remove_label` or `remove_datamodel` are given, all instances are + marked for removal. + + Then instances matching `keep_label` and `keep_datamodel` are unmarked + for removal. - - `("filterType", "dlite/create-collection")` + If `keep_referred` is true, any instance that is referred to by + an instance not marked for removeal is also unmarked for removal. + Finally, the instances that are still marked for removal are removed + from the collection. """ - filter_config: FilterConfig + remove_label: str = Field( + None, description="Regular expression matching labels to remove." + ) + remove_datamodel: str = Field( + None, + description="Regular expression matching datamodel URIs to remove.", + ) + keep_label: str = Field( + None, + description="Regular expression matching labels to keep." + "This configuration overrides `regexp_remove` and `datamodel_remove`. " + "Alias for the DLiteFilterStrategy `query` configuration.", + ) + keep_datamodel: str = Field( + None, + description="Regular expression matching datamodel URIs to keep in " + "collection. " + "This configuration overrides `regexp_remove` and `datamodel_remove`.", + ) + keep_referred: bool = Field( + True, + description="Whether to keep all instances in the collection that are " + "directly or indirectly referred to (via ref-types or collections) " + "by kept instances.", + ) + - # Find a better way to keep collections alive!!! - # Need to be `Any`, because otherwise `pydantic` complains. - collection_refs: Dict[str, Any] = Field( - {}, - description="A dictionary of DLite Collections.", +class DLiteFilterConfig(FilterConfig): + """DLite generate strategy config.""" + + configuration: DLiteQueryConfig = Field( + ..., description="DLite filter strategy-specific configuration." ) - def initialize( - self, session: "Optional[Dict[str, Any]]" = None - ) -> DLiteSessionUpdate: - """Initialize.""" - if session is None: - raise ValueError("Missing session") - if "collection_id" in session: - raise KeyError("`collection_id` already exists in session.") - coll = dlite.Collection() +@dataclass +class DLiteFilterStrategy: + """Filter that removes all but specified instances in the collection. + + The `query` configuration should be a regular expression matching labels + to keep in the collection. All other labels will be removed. - # Make sure that collection stays alive - # It will never be deallocated... - coll._incref() # pylint: disable=protected-access + **Registers strategies**: - # Store the collection in the data cache - cache = DataCache() - cache.add(value=coll.asjson(), key=coll.uuid) + - `("filterType", "dlite/filter")` - return DLiteSessionUpdate(collection_id=coll.uuid) + WARNING: This is a first simple implementation. The behaviour of this + strategy may change. + """ + + filter_config: DLiteFilterConfig + + def initialize( + self, + session: "Optional[Dict[str, Any]]" = None, + ) -> DLiteSessionUpdate: + """Initialize.""" + return DLiteSessionUpdate(collection_id=get_collection(session).uuid) def get( self, session: "Optional[Dict[str, Any]]" = None ) -> DLiteSessionUpdate: """Execute the strategy.""" - if session is None: - raise ValueError("Missing session") - return DLiteSessionUpdate(collection_id=session["collection_id"]) + # pylint: disable=too-many-branches + config = self.filter_config.configuration + + # Alias for query configuration + keep_label = ( + config.keep_label if config.keep_label else self.filter_config.query + ) + + instdict = {} # Map instance labels to metadata URI + coll = get_collection(session) + for s, p, o in coll.get_relations(): + if p == "_has-meta": + instdict[s] = o + + removal = set() # Labels marked for removal + + # 1: remove_label, remove_datamodel + if config.remove_label or config.remove_datamodel: + for label, uri in instdict.items(): + if config.remove_label and re.match(config.remove_label, label): + removal.add(label) + + if config.remove_datamodel and re.match( + config.remove_datamodel, uri + ): + removal.add(label) + else: + removal.update(instdict.keys()) + + # 2: keep_label, keep_datamodel + for label in set(removal): + if keep_label and re.match(keep_label, label): + removal.remove(label) + + uri = instdict[label] + if config.keep_datamodel and re.match(config.keep_datamodel, uri): + removal.remove(label) + + # 3: keep_referred + if config.keep_referred: + kept = set(instdict.keys()).difference(removal) + for label in kept: + removal.difference_update( + get_referred_instances(coll.get(label)) + ) + + print() + print("*** Removal:") + for label in removal: + print(" - ", label) + + # 4: remove from collection + for label in removal: + coll.remove(label) + + update_collection(coll) + return DLiteSessionUpdate(collection_id=get_collection(session).uuid) diff --git a/oteapi_dlite/strategies/parse.py b/oteapi_dlite/strategies/parse.py index cb93c16b..9b5945e3 100644 --- a/oteapi_dlite/strategies/parse.py +++ b/oteapi_dlite/strategies/parse.py @@ -118,7 +118,7 @@ def get( key = session["key"] else: raise ValueError( - "either `location` or `cacheconfig.accessKey` must be " + "either `location` or `datacache_config.accessKey` must be " "provided" ) diff --git a/requirements_dev.txt b/requirements_dev.txt index 1f0623aa..a277cf65 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ +invoke~=2.2 otelib>=0.3,<0.4.0 pre-commit~=3.5 pylint~=3.0 diff --git a/setup.cfg b/setup.cfg index 14c10d1f..b21d8503 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,4 +38,4 @@ oteapi.mapping = oteapi_dlite.mappings = oteapi_dlite.strategies.mapping:DLiteMappingStrategy oteapi.filter = - oteapi_dlite.dlite/create-collection = oteapi_dlite.strategies.filter:CreateCollectionStrategy + oteapi_dlite.dlite/filter = oteapi_dlite.strategies.filter:DLiteFilterStrategy diff --git a/tests/strategies/test_filter.py b/tests/strategies/test_filter.py index f28ee035..e0d16ca3 100644 --- a/tests/strategies/test_filter.py +++ b/tests/strategies/test_filter.py @@ -1,29 +1,81 @@ """Tests filter strategies.""" -from typing import TYPE_CHECKING +from pathlib import Path -if TYPE_CHECKING: - from oteapi.interfaces import IFilterStrategy +import dlite +from oteapi_dlite.strategies.filter import ( + DLiteFilterConfig, + DLiteFilterStrategy, +) +from oteapi_dlite.utils import get_meta -def test_create_collection() -> None: - """Test the create_collection filter.""" - import dlite +thisdir = Path(__file__).resolve().parent +entitydir = thisdir / ".." / "entities" +outdir = thisdir / ".." / "output" - from oteapi_dlite.strategies.filter import CreateCollectionStrategy +Image = get_meta("http://onto-ns.com/meta/1.0/Image") +image1 = Image([2, 2, 1]) +image2 = Image([2, 2, 1]) +image3 = Image([2, 2, 1]) +image4 = Image([2, 2, 1]) +innercoll = dlite.Collection() +innercoll.add("im1", image1) +innercoll.add("im2", image2) - config = {"filterType": "dlite/create_collection"} +coll = dlite.Collection() +coll.add("innercoll", innercoll) +coll.add("image1", image1) +coll.add("image2", image2) +coll.add("image3", image3) +coll.add("image4", image4) - session = {} - collfilter: "IFilterStrategy" = CreateCollectionStrategy(config) - session.update(collfilter.initialize(session)) +# Test 1 +config = DLiteFilterConfig( + filterType="dlite/filter", + query="^im", + configuration={}, +) +coll1 = coll.copy() +session = {"collection_id": coll1.uuid} - assert "collection_id" in session - coll_id = session["collection_id"] - coll = dlite.get_instance(coll_id) - assert isinstance(coll, dlite.Collection) +strategy = DLiteFilterStrategy(config) +session.update(strategy.initialize(session)) - collfilter = CreateCollectionStrategy(config) - session.update(collfilter.get(session)) - assert "collection_id" in session - assert session["collection_id"] == coll_id +strategy = DLiteFilterStrategy(config) +session.update(strategy.get(session)) + +assert set(coll1.get_labels()) == set( + [ + "image1", + "image2", + "image3", + "image4", + ] +) + + +# Test 2 +config = DLiteFilterConfig( + filterType="dlite/filter", + configuration={ + "remove_datamodel": Image.uri, + "keep_label": "(image2)|(image4)", + }, +) +coll2 = coll.copy() +session = {"collection_id": coll2.uuid} + +strategy = DLiteFilterStrategy(config) +session.update(strategy.initialize(session)) + +strategy = DLiteFilterStrategy(config) +session.update(strategy.get(session)) + +assert set(coll2.get_labels()) == set( + [ + "innercoll", + "image2", + "image4", + ] +) From bd2c144a3416e2d5a236762b15f061f619d59370 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 29 Oct 2023 00:05:53 +0200 Subject: [PATCH 02/13] Increased requirement on DLite to 0.4.3 ensure that we have dlite.utils.get_referred_instances() --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49b8bd59..4db1728b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -DLite-Python>=0.3.3,<1.0 +DLite-Python>=0.4.3,<1.0 numpy>=1.21,<2 oteapi-core>=0.1.2,<0.6.0 Pillow>=9.0.1,<11 From da0920cfef3cc99aa43dec2ebd8e76df5174b79f Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 29 Oct 2023 17:52:58 +0100 Subject: [PATCH 03/13] Added test for configuration `keep_referred` --- oteapi_dlite/strategies/filter.py | 28 +++++++++++++------------- tests/strategies/test_filter.py | 33 +++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index e5991966..5b1670b2 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -104,22 +104,23 @@ def get( config.keep_label if config.keep_label else self.filter_config.query ) - instdict = {} # Map instance labels to metadata URI + instdict = {} # Map instance labels to [uuid, metaURI] coll = get_collection(session) - for s, p, o in coll.get_relations(): - if p == "_has-meta": - instdict[s] = o + for s, _, o in coll.get_relations(p="_has-uuid"): + instdict[s] = [o] + for s, _, o in coll.get_relations(p="_has-meta"): + instdict[s].append(o) removal = set() # Labels marked for removal # 1: remove_label, remove_datamodel if config.remove_label or config.remove_datamodel: - for label, uri in instdict.items(): + for label, (uuid, metauri) in instdict.items(): if config.remove_label and re.match(config.remove_label, label): removal.add(label) if config.remove_datamodel and re.match( - config.remove_datamodel, uri + config.remove_datamodel, metauri ): removal.add(label) else: @@ -130,23 +131,22 @@ def get( if keep_label and re.match(keep_label, label): removal.remove(label) - uri = instdict[label] - if config.keep_datamodel and re.match(config.keep_datamodel, uri): + uuid, metauri = instdict[label] + if config.keep_datamodel and re.match( + config.keep_datamodel, metauri + ): removal.remove(label) # 3: keep_referred if config.keep_referred: + labels = {uuid: label for label, (uuid, _) in instdict.items()} kept = set(instdict.keys()).difference(removal) for label in kept: removal.difference_update( - get_referred_instances(coll.get(label)) + labels[inst.uuid] + for inst in get_referred_instances(coll.get(label)) ) - print() - print("*** Removal:") - for label in removal: - print(" - ", label) - # 4: remove from collection for label in removal: coll.remove(label) diff --git a/tests/strategies/test_filter.py b/tests/strategies/test_filter.py index e0d16ca3..62503fcd 100644 --- a/tests/strategies/test_filter.py +++ b/tests/strategies/test_filter.py @@ -30,7 +30,7 @@ coll.add("image4", image4) -# Test 1 +# Test simple use of query config = DLiteFilterConfig( filterType="dlite/filter", query="^im", @@ -55,12 +55,13 @@ ) -# Test 2 +# Test combining remove and keep config = DLiteFilterConfig( filterType="dlite/filter", configuration={ "remove_datamodel": Image.uri, "keep_label": "(image2)|(image4)", + "keep_referred": False, }, ) coll2 = coll.copy() @@ -79,3 +80,31 @@ "image4", ] ) + + +# Test with keep_referred=True +config = DLiteFilterConfig( + filterType="dlite/filter", + configuration={ + "remove_datamodel": Image.uri, + "keep_label": "(image2)|(image4)", + "keep_referred": True, + }, +) +coll3 = coll.copy() +session = {"collection_id": coll3.uuid} + +strategy = DLiteFilterStrategy(config) +session.update(strategy.initialize(session)) + +strategy = DLiteFilterStrategy(config) +session.update(strategy.get(session)) + +assert set(coll3.get_labels()) == set( + [ + "innercoll", + "image1", + "image2", + "image4", + ] +) From cd2e3d4ad574b020a726f859da94f7e1651de6a2 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 29 Oct 2023 18:02:41 +0100 Subject: [PATCH 04/13] For keep_referred configuration in filter, correctly handle the case when the referred instance is not in the collection of the session. --- oteapi_dlite/strategies/filter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 5b1670b2..c421b55b 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -145,6 +145,7 @@ def get( removal.difference_update( labels[inst.uuid] for inst in get_referred_instances(coll.get(label)) + if inst.uuid in labels ) # 4: remove from collection From b22e2bb79e4c4b03b5ba6d90dcd9d76c61bed979 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 10 Nov 2023 20:11:57 +0100 Subject: [PATCH 05/13] Update oteapi_dlite/strategies/filter.py Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com> --- oteapi_dlite/strategies/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index c421b55b..0466dd4b 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -27,7 +27,7 @@ class DLiteQueryConfig(AttrDict): for removal. If `keep_referred` is true, any instance that is referred to by - an instance not marked for removeal is also unmarked for removal. + an instance not marked for removal is also unmarked for removal. Finally, the instances that are still marked for removal are removed from the collection. From 8f4f56d329e1c7246634e953b703ce6be0eb56f1 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 10 Nov 2023 20:16:30 +0100 Subject: [PATCH 06/13] Update oteapi_dlite/strategies/filter.py Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com> --- oteapi_dlite/strategies/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 0466dd4b..29afaaf1 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -43,7 +43,7 @@ class DLiteQueryConfig(AttrDict): keep_label: str = Field( None, description="Regular expression matching labels to keep." - "This configuration overrides `regexp_remove` and `datamodel_remove`. " + "This configuration overrides `remove_label` and `remove_datamodel`. " "Alias for the DLiteFilterStrategy `query` configuration.", ) keep_datamodel: str = Field( From 038eab925148a2e33a2b42ac11c2e3551f3d75e6 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 10 Nov 2023 20:17:25 +0100 Subject: [PATCH 07/13] Update oteapi_dlite/strategies/filter.py Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com> --- oteapi_dlite/strategies/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 29afaaf1..6b277e4a 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -44,7 +44,7 @@ class DLiteQueryConfig(AttrDict): None, description="Regular expression matching labels to keep." "This configuration overrides `remove_label` and `remove_datamodel`. " - "Alias for the DLiteFilterStrategy `query` configuration.", + "Alias for the FilterStrategy `query` configuration, that is inherited from the oteapi-core Filter data model.", ) keep_datamodel: str = Field( None, From 379956a1b41707a99c39e10f2e1c50d531e22ea5 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 10 Nov 2023 20:17:44 +0100 Subject: [PATCH 08/13] Update oteapi_dlite/strategies/filter.py Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com> --- oteapi_dlite/strategies/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 6b277e4a..2ac13fa6 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -50,7 +50,7 @@ class DLiteQueryConfig(AttrDict): None, description="Regular expression matching datamodel URIs to keep in " "collection. " - "This configuration overrides `regexp_remove` and `datamodel_remove`.", + "This configuration overrides `remove_label` and `remove_datamodel`.", ) keep_referred: bool = Field( True, From 180829b3d940c7d90e8ce44734b172fd3a74af37 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 10 Nov 2023 20:35:39 +0100 Subject: [PATCH 09/13] Updated requirements as suggested by reviewer --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4db1728b..96c2153a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -DLite-Python>=0.4.3,<1.0 +DLite-Python>=0.4.5,<1.0 numpy>=1.21,<2 oteapi-core>=0.1.2,<0.6.0 Pillow>=9.0.1,<11 From d1d374c33ce6c34df725cfe2629c7fedab3a7de8 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 10 Nov 2023 23:51:54 +0100 Subject: [PATCH 10/13] Fixed too long line. --- oteapi_dlite/strategies/filter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 2ac13fa6..8bbe9951 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -44,7 +44,8 @@ class DLiteQueryConfig(AttrDict): None, description="Regular expression matching labels to keep." "This configuration overrides `remove_label` and `remove_datamodel`. " - "Alias for the FilterStrategy `query` configuration, that is inherited from the oteapi-core Filter data model.", + "Alias for the FilterStrategy `query` configuration, that is " + "inherited from the oteapi-core Filter data model.", ) keep_datamodel: str = Field( None, From 52cc6e0b0899ed74d5adc8185b8b2449b8e02c46 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 11 Nov 2023 16:21:27 +0100 Subject: [PATCH 11/13] Removed warning. --- oteapi_dlite/strategies/filter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/oteapi_dlite/strategies/filter.py b/oteapi_dlite/strategies/filter.py index 8bbe9951..7dc0eceb 100644 --- a/oteapi_dlite/strategies/filter.py +++ b/oteapi_dlite/strategies/filter.py @@ -80,8 +80,6 @@ class DLiteFilterStrategy: - `("filterType", "dlite/filter")` - WARNING: This is a first simple implementation. The behaviour of this - strategy may change. """ filter_config: DLiteFilterConfig From 0fe9d9e6ac67a3341cce1dbe60824ace9510f74d Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 23 Nov 2023 17:58:39 +0100 Subject: [PATCH 12/13] Update tests/strategies/test_filter.py Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com> --- tests/strategies/test_filter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/strategies/test_filter.py b/tests/strategies/test_filter.py index 62503fcd..fdb1a81f 100644 --- a/tests/strategies/test_filter.py +++ b/tests/strategies/test_filter.py @@ -31,6 +31,7 @@ # Test simple use of query +# Here keeping all instances with label containing "im" in the collection config = DLiteFilterConfig( filterType="dlite/filter", query="^im", From 6a7bb5591c895536cfe3a88f3fc0c3e311c8fc72 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 23 Nov 2023 19:51:07 +0100 Subject: [PATCH 13/13] Added extra test as suggested by reviewer --- tests/strategies/test_filter.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/strategies/test_filter.py b/tests/strategies/test_filter.py index 62503fcd..36b0273e 100644 --- a/tests/strategies/test_filter.py +++ b/tests/strategies/test_filter.py @@ -36,6 +36,32 @@ query="^im", configuration={}, ) +coll0 = coll.copy() +session = {"collection_id": coll0.uuid} + +strategy = DLiteFilterStrategy(config) +session.update(strategy.initialize(session)) + +strategy = DLiteFilterStrategy(config) +session.update(strategy.get(session)) + +assert set(coll0.get_labels()) == set( + [ + "image1", + "image2", + "image3", + "image4", + ] +) + + +# Same test as above, but use use `keep_label` instead of `query` +config = DLiteFilterConfig( + filterType="dlite/filter", + configuration={ + "keep_label": "^im", + }, +) coll1 = coll.copy() session = {"collection_id": coll1.uuid}