Skip to content

Commit

Permalink
Add support for irods:: style AVUs
Browse files Browse the repository at this point in the history
These are added by iRODS' own internal processes and are exposed
amongst user AVUs.
  • Loading branch information
kjsanger committed Oct 12, 2023
1 parent f61d115 commit 06e4826
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 22 deletions.
59 changes: 38 additions & 21 deletions src/partisan/irods.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import atexit
import json
import re
import subprocess
import threading
from abc import abstractmethod
Expand Down Expand Up @@ -818,10 +819,17 @@ class AVU(object):
SEPARATOR = ":"
"""The attribute namespace separator"""

IRODS_NAMESPACE = "irods"

IRODS_SEPARATOR = "::"
"""The attribute namespace separator used by iRODS' system AVUs"""

HISTORY_SUFFIX = "_history"
"""The attribute history suffix"""

def __init__(self, attribute: Any, value: Any, units=None, namespace=None):
def __init__(
self, attribute: Any, value: Any, units: str = None, namespace: str = None
):
"""Create a new AVU instance.
Args:
Expand All @@ -841,14 +849,33 @@ def __init__(self, attribute: Any, value: Any, units=None, namespace=None):
if namespace is None:
namespace = ""

if namespace.find(AVU.SEPARATOR) >= 0:
attr = str(attribute)
value = str(value)

if re.match(r"\s+$", attr):
raise ValueError("AVU attribute may not be entirely whitespace")
if re.match(r"\s+$", value):
raise ValueError("AVU value may not be entirely whitespace")
if re.match(r"\s+$", namespace):
raise ValueError("AVU namespace may not be entirely whitespace")

# Handle iRODS' own namespaced AVUs
if attr.startswith(AVU.IRODS_NAMESPACE) and (
attr.find(AVU.IRODS_SEPARATOR) == len(AVU.IRODS_NAMESPACE)
):
self._separator = AVU.IRODS_SEPARATOR
# Handle all other AVUs, namespaced or not
else:
self._separator = AVU.SEPARATOR

if namespace and namespace.find(self._separator) >= 0:
raise ValueError(
f"AVU namespace contained a separator '{AVU.SEPARATOR}': '{namespace}'"
f"AVU namespace contained a separator '{self._separator}': "
f"'{namespace}'"
)

attr = str(attribute)
if attr.find(AVU.SEPARATOR) >= 0:
ns, at = attr.split(AVU.SEPARATOR, maxsplit=1)
if attr.find(self._separator) >= 0:
ns, at = attr.split(self._separator, maxsplit=1)
if namespace and ns != namespace:
raise ValueError(
f"AVU attribute namespace '{ns}' did not match "
Expand All @@ -859,7 +886,7 @@ def __init__(self, attribute: Any, value: Any, units=None, namespace=None):

self._namespace = namespace
self._attribute = attr
self._value = str(value)
self._value = value
self._units = units

@classmethod
Expand Down Expand Up @@ -942,9 +969,10 @@ def without_namespace(self):
@property
def attribute(self):
"""The attribute, including namespace, if any. The namespace an attribute are
separated by AVU.SEPARATOR."""
separated by AVU.SEPARATOR (or AVU.IRODS_SEPARATOR in the case of iRODS'
internal AVUs)."""
if self._namespace:
return f"{self._namespace}{AVU.SEPARATOR}{self._attribute}"
return f"{self._namespace}{self._separator}{self._attribute}"
else:
return self.without_namespace

Expand Down Expand Up @@ -2519,21 +2547,10 @@ def as_baton(d: Dict) -> Any:

# Match an AVU sub-document
if Baton.ATTRIBUTE in d:
attr = str(d[Baton.ATTRIBUTE])
attr = d[Baton.ATTRIBUTE]
value = d[Baton.VALUE]
units = d.get(Baton.UNITS, None)

if attr.find(AVU.SEPARATOR) >= 0: # Has namespace
(ns, _, bare_attr) = attr.partition(AVU.SEPARATOR)

# This accepts an attribute with a namespace that is the empty
# string i.e. ":foo" or is whitespace i.e. " :foo" and discards
# the namespace.
if not ns.strip():
ns = None

return AVU(bare_attr, value, units, namespace=ns)

return AVU(attr, value, units)

# Match an access permission sub-document
Expand Down
24 changes: 23 additions & 1 deletion tests/test_irods.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,13 @@ def test_create_with_namespaced_attribute(self):
assert AVU("x:a", 1).attribute == "x:a"
assert AVU("x:a", 1).without_namespace == "a"

with pytest.raises(ValueError, match="AVU namespace contained a separator"):
with pytest.raises(ValueError, match="may not be entirely whitespace"):
AVU(" ", 1)

with pytest.raises(ValueError, match="may not be entirely whitespace"):
AVU("a", 1, namespace=" ")

with pytest.raises(ValueError, match="namespace contained a separator"):
AVU("a", 1, namespace="x:")

with pytest.raises(ValueError, match="did not match the declared namespace"):
Expand Down Expand Up @@ -223,6 +229,19 @@ def test_create_with_namespaced_attribute(self):
assert AVU("x:::a", 1).attribute == "x:::a"
assert AVU("x:::a", 1).without_namespace == "::a"

# We can handle iRODS' own AVUs that it adds sometimes
assert AVU("irods::a", 1).namespace == AVU.IRODS_NAMESPACE
assert AVU("irods::a", 1).attribute == "irods::a"
assert AVU("irods::a", 1).without_namespace == "a"

# Even if they should have extra colons
assert AVU("irods:::a", 1).namespace == AVU.IRODS_NAMESPACE
assert AVU("irods:::a", 1).attribute == "irods:::a"
assert AVU("irods:::a", 1).without_namespace == ":a"

with pytest.raises(ValueError, match="did not match the declared namespace"):
AVU("irods::a", 1, namespace="x")

@m.describe("Comparison")
def test_compare_avus_equal(self):
assert AVU("a", 1) == AVU("a", 1)
Expand Down Expand Up @@ -792,6 +811,9 @@ def test_get_checksum(self, simple_data_object):
assert obj.checksum() == "39a4aa291ca849d601e4e5b8ed627a04"

@m.it("Can have its checksum verified as good")
@pytest.mark.skipif(
irods_version() <= (4, 2, 10), reason="requires iRODS server >4.2.10"
)
def test_verify_checksum_good(self, simple_data_object):
obj = DataObject(simple_data_object)

Expand Down

0 comments on commit 06e4826

Please sign in to comment.