Repository: datalad/datalad-next

Issue:
Establish `AnnexKey` class for any and all syntactic operations on keys
Something like this

```py
# BACKEND[-sNNNN][-mNNNN][-SNNNN-CNNNN]--NAME
_annexkey_regex = re.compile(
    '(?P<backend>[A-Z0-9]+)'
    '(|-s(?P<size>[0-9]+))'
    '(|-m(?P<mtime>[0-9]+))'
    '(|-S(?P<chunksize>[0-9]+)-C(?P<chunknumber>[0-9]+))'
    '--(?P<name>.*)$'
)


@dataclass
class AnnexKey:
    name: str
    backend: str
    size: int | None = None
    mtime: int | None = None
    chunksize: int | None = None
    chunknumber: int | None = None

    @classmethod
    def from_str(cls, key):
        key_matched = _annexkey_regex.match(key)
        if not key_matched:
            # without a sensible key there is no hope
            raise ValueError(f'{key!r} is not a valid git-annex key')
        return cls(**key_matched.groupdict())

    def __str__(self):
        return '{backend}{size}{mtime}{chunk}--{name}'.format(
            name=self.name,
            backend=self.backend,
            size=f'-s{self.size}' if self.size else '',
            mtime=f'-m{self.mtime}' if self.mtime else '',
            # if me reading of the spec is correct, the two chunk props
            # can only occur together
            chunk=f'-S{self.chunksize}-C{self.chunknumber}'
            if self.chunknumber else '',
        )
```

I am not sure where to put this. Maybe something like `datalad_next.models.annexkey`?

Comments:

Patch:
diff --git a/datalad_next/types/__init__.py b/datalad_next/types/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/datalad_next/types/annexkey.py b/datalad_next/types/annexkey.py
new file mode 100644
index 0000000..74bbcb6
--- /dev/null
+++ b/datalad_next/types/annexkey.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+import re
+
+
+# BACKEND[-sNNNN][-mNNNN][-SNNNN-CNNNN]--NAME
+_annexkey_regex = re.compile(
+    '(?P<backend>[A-Z0-9]+)'
+    '(|-s(?P<size>[0-9]+))'
+    '(|-m(?P<mtime>[0-9]+))'
+    '(|-S(?P<chunksize>[0-9]+)-C(?P<chunknumber>[0-9]+))'
+    '--(?P<name>.*)$'
+)
+
+
+@dataclass
+class AnnexKey:
+    """Representation of a git-annex key
+
+    https://git-annex.branchable.com/internals/key_format/
+    """
+    name: str
+    backend: str
+    size: int | None = None
+    mtime: int | None = None
+    chunksize: int | None = None
+    chunknumber: int | None = None
+
+    @classmethod
+    def from_str(cls, key: str):
+        key_matched = _annexkey_regex.match(key)
+        if not key_matched:
+            # without a sensible key there is no hope
+            raise ValueError(f'{key!r} is not a valid git-annex key')
+        return cls(**key_matched.groupdict())
+
+    def __str__(self) -> str:
+        return '{backend}{size}{mtime}{chunk}--{name}'.format(
+            name=self.name,
+            backend=self.backend,
+            size=f'-s{self.size}' if self.size else '',
+            mtime=f'-m{self.mtime}' if self.mtime else '',
+            # if me reading of the spec is correct, the two chunk props
+            # can only occur together
+            chunk=f'-S{self.chunksize}-C{self.chunknumber}'
+            if self.chunknumber else '',
+        )
diff --git a/docs/source/pyutils.rst b/docs/source/pyutils.rst
index ef8e24c..cff773d 100644
--- a/docs/source/pyutils.rst
+++ b/docs/source/pyutils.rst
@@ -23,3 +23,4 @@ Python utilities
    utils.multihash
    utils.requests_auth
    tests.fixtures
+   types.annexkey


Test patch:
diff --git a/datalad_next/types/tests/__init__.py b/datalad_next/types/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/datalad_next/types/tests/test_annexkey.py b/datalad_next/types/tests/test_annexkey.py
new file mode 100644
index 0000000..80c68c0
--- /dev/null
+++ b/datalad_next/types/tests/test_annexkey.py
@@ -0,0 +1,9 @@
+from ..annexkey import AnnexKey
+
+
+def test_annexkey():
+    for key in (
+        'MD5E-s792207360--985e680a221e47db05063a12b91d7d89.tar',
+    ):
+        # round-tripping for any key must give same outcome
+        assert key == str(AnnexKey.from_str(key))