diff --git a/README.md b/README.md index b26e8a7..155cc06 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,14 @@ collection and data object metadata to return combined results. assert irods.query_metadata(AVU("experiment_id", "experiment_1")) == [obj] +The default query operator is `=`. You can specify a different operator for each +AVU used to define a query by setting its `operator` attribute in any of the query +methods. + + assert DataObject.query_metadata(AVU("experiment_id", "experiment%", operator="like")) == [obj] + + assert irods.query_metadata(AVU("experiment_id", "experiment%", operator="like")) == [obj] + ### Pools `partisan` uses a small pool (the default is 4) of `BatonClient` instances to diff --git a/src/partisan/irods.py b/src/partisan/irods.py index 4b1ab9a..e9d01a8 100644 --- a/src/partisan/irods.py +++ b/src/partisan/irods.py @@ -67,6 +67,7 @@ class Baton: ATTRIBUTE = "attribute" VALUE = "value" UNITS = "units" + OPERATOR = "operator" COLL = "collection" OBJ = "data_object" @@ -828,7 +829,12 @@ class AVU(object): """The attribute history suffix""" def __init__( - self, attribute: Any, value: Any, units: str = None, namespace: str = None + self, + attribute: Any, + value: Any, + units: str = None, + namespace: str = None, + operator: str = None, ): """Create a new AVU instance. @@ -841,6 +847,9 @@ def __init__( namespace: The namespace (prefix) to be used for the attribute. Optional, but if supplied, must be the same as any existing namespace on the attribute string. + operator: The operator to use if the AVU is used as a query argument. + Optional, defaults to '='. Must be one of the available iRODS + query operators. Operators are not considered when comparing AVUs. """ if attribute is None: raise ValueError("AVU attribute may not be None") @@ -848,6 +857,8 @@ def __init__( raise ValueError("AVU value may not be None") if namespace is None: namespace = "" + if operator is None: + operator = "=" attr = str(attribute) value = str(value) @@ -888,6 +899,7 @@ def __init__( self._attribute = attr self._value = value self._units = units + self._operator = operator @classmethod def collate(cls, *avus: AVU) -> Dict[str : List[AVU]]: @@ -986,6 +998,11 @@ def units(self): """The units associated with the attribute. Units may be None.""" return self._units + @property + def operator(self): + """The operator associated with the AVU. The default is '='.""" + return self._operator + def with_namespace(self, namespace: str): """make a new copy of this AVU with the specified namespace. @@ -2526,6 +2543,8 @@ def default(self, o: Any) -> Any: enc = {Baton.ATTRIBUTE: o.attribute, Baton.VALUE: o.value} if o.units: enc[Baton.UNITS] = o.units + if o.operator: + enc[Baton.OPERATOR] = o.operator return enc if isinstance(o, Permission): diff --git a/tests/test_examples.py b/tests/test_examples.py index 6fc494c..c51b91f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -165,6 +165,18 @@ def test_data_object_examples(self, ont_gridion, tmp_path): # collection and data object metadata to return combined results. assert irods.query_metadata(AVU("experiment_id", "experiment_1")) == [obj] + # The default query operator is `=`. You can specify a different operator for + # each AVU used to define a query by setting its `operator` attribute in any of + # the query methods. + + assert DataObject.query_metadata( + AVU("experiment_id", "experiment%", operator="like") + ) == [obj] + + assert irods.query_metadata( + AVU("experiment_id", "experiment%", operator="like") + ) == [obj] + def test_pool_examples(self, ont_gridion): # partisan uses a small pool (the default is 4) of BatonClient instances to # serve requests. This pool is created automatically and is passed to the diff --git a/tests/test_irods.py b/tests/test_irods.py index c0cba26..e8a08b3 100644 --- a/tests/test_irods.py +++ b/tests/test_irods.py @@ -1152,6 +1152,9 @@ def test_query_meta_collection(self, annotated_collection, annotated_data_object assert [Collection(annotated_collection)] == Collection.query_metadata( AVU("attr1", "value1"), AVU("attr2", "value2"), AVU("attr3", "value3") ) + assert [Collection(annotated_collection)] == Collection.query_metadata( + AVU("attr1", "value%", operator="like") + ) @m.describe("Query DataObject namespace") @m.context("When a DataObject has metadata") @@ -1167,10 +1170,13 @@ def test_query_meta_data_object(self, annotated_collection, annotated_data_objec assert [DataObject(annotated_data_object)] == DataObject.query_metadata( AVU("attr1", "value1"), AVU("attr2", "value2"), AVU("attr3", "value3") ) + assert [DataObject(annotated_data_object)] == DataObject.query_metadata( + AVU("attr1", "value%", operator="like") + ) @m.describe("Query both DataObject and Collection namespaces") @m.context("When a DataObjects and Collections have metadata") - @m.it("Can be queried by that metadata, only returning everything") + @m.it("Can be queried by that metadata, returning everything") def test_query_meta_all(self, annotated_collection, annotated_data_object): assert [] == query_metadata(AVU("no_such_attr1", "no_such_value1")) assert [ @@ -1187,6 +1193,10 @@ def test_query_meta_all(self, annotated_collection, annotated_data_object): ] == query_metadata( AVU("attr1", "value1"), AVU("attr2", "value2"), AVU("attr3", "value3") ) + assert [ + Collection(annotated_collection), + DataObject(annotated_data_object), + ] == query_metadata(AVU("attr1", "value%", operator="like")) @m.describe("Test special paths (quotes, spaces)")