mlflow-at1 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlflow-at1
3
+ Version: 0.1.0
4
+ Summary: MLflow artifact repository backed by verified, compressed, byte-exact AT-1 containers
5
+ Author: TinyFiles / AT-1
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://tinyfiles.io
8
+ Project-URL: Documentation, https://tinyfiles.io/docs/mlflow
9
+ Keywords: mlflow,artifacts,compression,provenance,verified,lossless
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: mlflow-skinny>=2.0
13
+ Provides-Extra: full
14
+ Requires-Dist: mlflow>=2.0; extra == "full"
15
+
16
+ # mlflow-at1 — verified, compressed MLflow artifacts
17
+
18
+ Store MLflow models, checkpoints, and datasets as **verified, byte-exact AT-1 containers** instead of raw
19
+ files. Every artifact is compressed losslessly (never-worse vs xz), carries an embedded SHA-256, and is
20
+ **byte-exact-verified on download** — a tampered or corrupted artifact is *refused*, not silently returned.
21
+
22
+ That makes an artifact in your MLflow store **provably the exact bytes you logged** — the reproducibility /
23
+ provenance hook that regulated AI (EU AI Act, NIST AI RMF, FDA 21 CFR Part 11) needs: "this model is the
24
+ exact model that was trained, untampered."
25
+
26
+ It's a **pure-glue plugin** — no engine code ships here. It registers the `at1://` artifact-repository
27
+ scheme and shells out to the `at1` CLI.
28
+
29
+ ## Install
30
+ ```bash
31
+ npm i -g @tinyfiles/cli # the at1 binary (the codec; no Python engine needed)
32
+ pip install mlflow-at1 # this plugin (registers the at1:// scheme)
33
+ # pip install mlflow-at1[full] # if you want full MLflow rather than mlflow-skinny
34
+ ```
35
+
36
+ ## Use
37
+ Point an experiment's **artifact location** at an `at1://` path; everything else is normal MLflow:
38
+ ```python
39
+ import mlflow
40
+ exp = mlflow.create_experiment("regulated-model", artifact_location="at1:///data/mlartifacts")
41
+ with mlflow.start_run(experiment_id=exp):
42
+ mlflow.log_artifact("model.bin") # -> stored as a verified, compressed AT-1 container
43
+ # mlflow.sklearn.log_model(...) etc. all route through AT-1 too
44
+
45
+ # later — download is a byte-exact, SHA-256-verified reconstruction:
46
+ path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path="model.bin")
47
+ ```
48
+
49
+ `at1://<path>` is a local/mounted filesystem base directory for the containers (S3/GCS backing is a
50
+ follow-on). Set `AT1_BIN` to point at a specific `at1` binary if it isn't on `PATH`.
51
+
52
+ ## What you get
53
+ - **Smaller** — lossless, never-worse than xz; typically a real reduction on weights/checkpoints/datasets.
54
+ - **Verified** — every container embeds a SHA-256; `download` refuses anything that doesn't reconstruct
55
+ byte-for-byte. Explicit proof per artifact: `repo.integrity("model.bin")` → `integrity OK (sha256 …)`.
56
+ - **Addressable** — one `.at1` per artifact; tabular artifacts stay queryable in place via the `at1` CLI.
57
+ - **Zero lock-in** — the container is an ordinary file with a tiny open decoder; you can always get the
58
+ exact original back, with or without this plugin.
59
+
60
+ ## Test
61
+ ```bash
62
+ pip install mlflow-skinny pytest && python -m pytest tests/ -q # needs the `at1` CLI on PATH
63
+ ```
64
+ 5 tests: log→download byte-exact, logical listing, directory trees, integrity + tamper-rejection, delete.
@@ -0,0 +1,49 @@
1
+ # mlflow-at1 — verified, compressed MLflow artifacts
2
+
3
+ Store MLflow models, checkpoints, and datasets as **verified, byte-exact AT-1 containers** instead of raw
4
+ files. Every artifact is compressed losslessly (never-worse vs xz), carries an embedded SHA-256, and is
5
+ **byte-exact-verified on download** — a tampered or corrupted artifact is *refused*, not silently returned.
6
+
7
+ That makes an artifact in your MLflow store **provably the exact bytes you logged** — the reproducibility /
8
+ provenance hook that regulated AI (EU AI Act, NIST AI RMF, FDA 21 CFR Part 11) needs: "this model is the
9
+ exact model that was trained, untampered."
10
+
11
+ It's a **pure-glue plugin** — no engine code ships here. It registers the `at1://` artifact-repository
12
+ scheme and shells out to the `at1` CLI.
13
+
14
+ ## Install
15
+ ```bash
16
+ npm i -g @tinyfiles/cli # the at1 binary (the codec; no Python engine needed)
17
+ pip install mlflow-at1 # this plugin (registers the at1:// scheme)
18
+ # pip install mlflow-at1[full] # if you want full MLflow rather than mlflow-skinny
19
+ ```
20
+
21
+ ## Use
22
+ Point an experiment's **artifact location** at an `at1://` path; everything else is normal MLflow:
23
+ ```python
24
+ import mlflow
25
+ exp = mlflow.create_experiment("regulated-model", artifact_location="at1:///data/mlartifacts")
26
+ with mlflow.start_run(experiment_id=exp):
27
+ mlflow.log_artifact("model.bin") # -> stored as a verified, compressed AT-1 container
28
+ # mlflow.sklearn.log_model(...) etc. all route through AT-1 too
29
+
30
+ # later — download is a byte-exact, SHA-256-verified reconstruction:
31
+ path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path="model.bin")
32
+ ```
33
+
34
+ `at1://<path>` is a local/mounted filesystem base directory for the containers (S3/GCS backing is a
35
+ follow-on). Set `AT1_BIN` to point at a specific `at1` binary if it isn't on `PATH`.
36
+
37
+ ## What you get
38
+ - **Smaller** — lossless, never-worse than xz; typically a real reduction on weights/checkpoints/datasets.
39
+ - **Verified** — every container embeds a SHA-256; `download` refuses anything that doesn't reconstruct
40
+ byte-for-byte. Explicit proof per artifact: `repo.integrity("model.bin")` → `integrity OK (sha256 …)`.
41
+ - **Addressable** — one `.at1` per artifact; tabular artifacts stay queryable in place via the `at1` CLI.
42
+ - **Zero lock-in** — the container is an ordinary file with a tiny open decoder; you can always get the
43
+ exact original back, with or without this plugin.
44
+
45
+ ## Test
46
+ ```bash
47
+ pip install mlflow-skinny pytest && python -m pytest tests/ -q # needs the `at1` CLI on PATH
48
+ ```
49
+ 5 tests: log→download byte-exact, logical listing, directory trees, integrity + tamper-rejection, delete.
@@ -0,0 +1,8 @@
1
+ """mlflow-at1 — store MLflow artifacts as verified, compressed, byte-exact AT-1 containers.
2
+
3
+ Registers the ``at1://`` artifact-repository scheme. See README for usage.
4
+ """
5
+ from mlflow_at1.at1_artifact_repo import AT1ArtifactRepository
6
+
7
+ __all__ = ["AT1ArtifactRepository"]
8
+ __version__ = "0.1.0"
@@ -0,0 +1,123 @@
1
+ """AT-1 artifact repository for MLflow — store models, checkpoints, and datasets as VERIFIED,
2
+ compressed, byte-exact AT-1 containers instead of raw files.
3
+
4
+ Register a tracking/artifact URI with the ``at1://`` scheme and MLflow routes every ``log_artifact`` /
5
+ model log through this repository: each file is compressed losslessly (never-worse vs xz), carries an
6
+ embedded SHA-256, and is **byte-exact-verified on download** (a tampered or corrupted artifact is refused,
7
+ not silently returned). So an artifact in your MLflow store is provably the exact bytes you logged — the
8
+ provenance hook regulated AI (EU AI Act / NIST AI RMF) needs.
9
+
10
+ Pure glue: it shells out to the ``at1`` CLI (``npm i -g @tinyfiles/cli``). No engine code lives here.
11
+ """
12
+ import os
13
+ import shutil
14
+ import posixpath
15
+ import subprocess
16
+
17
+ from mlflow.store.artifact.artifact_repo import ArtifactRepository
18
+ from mlflow.entities import FileInfo
19
+
20
+ _SUFFIX = ".at1"
21
+ _AT1_BIN = os.environ.get("AT1_BIN", "at1")
22
+
23
+
24
+ def _resolve_bin():
25
+ """Return the argv prefix to invoke the at1 CLI, handling a Windows .cmd/.bat launcher (npm shim)."""
26
+ resolved = shutil.which(_AT1_BIN) or _AT1_BIN
27
+ if os.name == "nt" and resolved.lower().endswith((".cmd", ".bat")):
28
+ return ["cmd", "/c", resolved]
29
+ return [resolved]
30
+
31
+
32
+ _BIN = _resolve_bin()
33
+
34
+
35
+ def _run(args):
36
+ r = subprocess.run(_BIN + args, capture_output=True, text=True)
37
+ if r.returncode != 0:
38
+ raise RuntimeError(f"AT-1 CLI failed: {' '.join(args)}\n{(r.stderr or r.stdout).strip()}")
39
+ return r
40
+
41
+
42
+ def _uri_to_base(artifact_uri):
43
+ """at1://<path> -> a local filesystem base directory for the .at1 containers."""
44
+ p = artifact_uri
45
+ for pre in ("at1://", "at1:"):
46
+ if p.startswith(pre):
47
+ p = p[len(pre):]
48
+ break
49
+ # Windows drive paths arrive as /C:/... -> strip the leading slash.
50
+ if os.name == "nt" and len(p) >= 3 and p[0] == "/" and p[2] == ":":
51
+ p = p[1:]
52
+ return os.path.normpath(p) if p else "."
53
+
54
+
55
+ class AT1ArtifactRepository(ArtifactRepository):
56
+ """MLflow ArtifactRepository backed by verified AT-1 containers (one ``.at1`` per artifact)."""
57
+
58
+ def __init__(self, artifact_uri, *args, **kwargs):
59
+ # MLflow passes tracking_uri/registry_uri kwargs in newer versions; forward what the installed
60
+ # base accepts and fall back to the legacy single-arg signature otherwise.
61
+ try:
62
+ super().__init__(artifact_uri, *args, **kwargs)
63
+ except TypeError:
64
+ super().__init__(artifact_uri)
65
+ self._base = _uri_to_base(artifact_uri)
66
+
67
+ # ---- write paths -------------------------------------------------------
68
+ def log_artifact(self, local_file, artifact_path=None):
69
+ dest_dir = self._base if not artifact_path else os.path.join(self._base, artifact_path)
70
+ os.makedirs(dest_dir, exist_ok=True)
71
+ out = os.path.join(dest_dir, os.path.basename(local_file) + _SUFFIX)
72
+ # 'auto' picks the best lossless codec for the file and is provably never-worse than xz.
73
+ _run(["compress", "auto", local_file, out])
74
+
75
+ def log_artifacts(self, local_dir, artifact_path=None):
76
+ local_dir = os.path.abspath(local_dir)
77
+ for root, _dirs, files in os.walk(local_dir):
78
+ rel = os.path.relpath(root, local_dir)
79
+ if rel == ".":
80
+ sub = artifact_path
81
+ else:
82
+ sub = posixpath.join(artifact_path, rel.replace(os.sep, "/")) if artifact_path else rel.replace(os.sep, "/")
83
+ for f in files:
84
+ self.log_artifact(os.path.join(root, f), sub)
85
+
86
+ # ---- read paths --------------------------------------------------------
87
+ def list_artifacts(self, path=None):
88
+ d = self._base if not path else os.path.join(self._base, path)
89
+ if not os.path.isdir(d):
90
+ return []
91
+ infos = []
92
+ for name in sorted(os.listdir(d)):
93
+ full = os.path.join(d, name)
94
+ rel = name if not path else posixpath.join(path, name)
95
+ if os.path.isdir(full):
96
+ infos.append(FileInfo(rel, True, None))
97
+ elif name.endswith(_SUFFIX):
98
+ # present the LOGICAL artifact name (without the .at1 container suffix). file_size is the
99
+ # on-disk compressed size; MLflow only uses it for display, and the byte-exact original is
100
+ # what download reconstructs.
101
+ infos.append(FileInfo(rel[: -len(_SUFFIX)], False, os.path.getsize(full)))
102
+ return infos
103
+
104
+ def _download_file(self, remote_file_path, local_path):
105
+ src = os.path.join(self._base, remote_file_path) + _SUFFIX
106
+ parent = os.path.dirname(local_path)
107
+ if parent:
108
+ os.makedirs(parent, exist_ok=True) # at1 decompress writes into an existing dir
109
+ # decompress re-checks the embedded SHA-256 and REFUSES a tampered/corrupt container, so a
110
+ # download is a verified, byte-exact reconstruction of exactly what was logged.
111
+ _run(["decompress", src, local_path])
112
+
113
+ def integrity(self, remote_file_path):
114
+ """Explicit integrity proof for one stored artifact (SHA-256, byte-for-byte). Returns the CLI line."""
115
+ src = os.path.join(self._base, remote_file_path) + _SUFFIX
116
+ return _run(["integrity", src]).stdout.strip()
117
+
118
+ def delete_artifacts(self, artifact_path=None):
119
+ d = self._base if not artifact_path else os.path.join(self._base, artifact_path)
120
+ if os.path.isdir(d):
121
+ shutil.rmtree(d, ignore_errors=True)
122
+ elif os.path.isfile(d + _SUFFIX):
123
+ os.remove(d + _SUFFIX)
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlflow-at1
3
+ Version: 0.1.0
4
+ Summary: MLflow artifact repository backed by verified, compressed, byte-exact AT-1 containers
5
+ Author: TinyFiles / AT-1
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://tinyfiles.io
8
+ Project-URL: Documentation, https://tinyfiles.io/docs/mlflow
9
+ Keywords: mlflow,artifacts,compression,provenance,verified,lossless
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: mlflow-skinny>=2.0
13
+ Provides-Extra: full
14
+ Requires-Dist: mlflow>=2.0; extra == "full"
15
+
16
+ # mlflow-at1 — verified, compressed MLflow artifacts
17
+
18
+ Store MLflow models, checkpoints, and datasets as **verified, byte-exact AT-1 containers** instead of raw
19
+ files. Every artifact is compressed losslessly (never-worse vs xz), carries an embedded SHA-256, and is
20
+ **byte-exact-verified on download** — a tampered or corrupted artifact is *refused*, not silently returned.
21
+
22
+ That makes an artifact in your MLflow store **provably the exact bytes you logged** — the reproducibility /
23
+ provenance hook that regulated AI (EU AI Act, NIST AI RMF, FDA 21 CFR Part 11) needs: "this model is the
24
+ exact model that was trained, untampered."
25
+
26
+ It's a **pure-glue plugin** — no engine code ships here. It registers the `at1://` artifact-repository
27
+ scheme and shells out to the `at1` CLI.
28
+
29
+ ## Install
30
+ ```bash
31
+ npm i -g @tinyfiles/cli # the at1 binary (the codec; no Python engine needed)
32
+ pip install mlflow-at1 # this plugin (registers the at1:// scheme)
33
+ # pip install mlflow-at1[full] # if you want full MLflow rather than mlflow-skinny
34
+ ```
35
+
36
+ ## Use
37
+ Point an experiment's **artifact location** at an `at1://` path; everything else is normal MLflow:
38
+ ```python
39
+ import mlflow
40
+ exp = mlflow.create_experiment("regulated-model", artifact_location="at1:///data/mlartifacts")
41
+ with mlflow.start_run(experiment_id=exp):
42
+ mlflow.log_artifact("model.bin") # -> stored as a verified, compressed AT-1 container
43
+ # mlflow.sklearn.log_model(...) etc. all route through AT-1 too
44
+
45
+ # later — download is a byte-exact, SHA-256-verified reconstruction:
46
+ path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path="model.bin")
47
+ ```
48
+
49
+ `at1://<path>` is a local/mounted filesystem base directory for the containers (S3/GCS backing is a
50
+ follow-on). Set `AT1_BIN` to point at a specific `at1` binary if it isn't on `PATH`.
51
+
52
+ ## What you get
53
+ - **Smaller** — lossless, never-worse than xz; typically a real reduction on weights/checkpoints/datasets.
54
+ - **Verified** — every container embeds a SHA-256; `download` refuses anything that doesn't reconstruct
55
+ byte-for-byte. Explicit proof per artifact: `repo.integrity("model.bin")` → `integrity OK (sha256 …)`.
56
+ - **Addressable** — one `.at1` per artifact; tabular artifacts stay queryable in place via the `at1` CLI.
57
+ - **Zero lock-in** — the container is an ordinary file with a tiny open decoder; you can always get the
58
+ exact original back, with or without this plugin.
59
+
60
+ ## Test
61
+ ```bash
62
+ pip install mlflow-skinny pytest && python -m pytest tests/ -q # needs the `at1` CLI on PATH
63
+ ```
64
+ 5 tests: log→download byte-exact, logical listing, directory trees, integrity + tamper-rejection, delete.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ mlflow_at1/__init__.py
4
+ mlflow_at1/at1_artifact_repo.py
5
+ mlflow_at1.egg-info/PKG-INFO
6
+ mlflow_at1.egg-info/SOURCES.txt
7
+ mlflow_at1.egg-info/dependency_links.txt
8
+ mlflow_at1.egg-info/entry_points.txt
9
+ mlflow_at1.egg-info/requires.txt
10
+ mlflow_at1.egg-info/top_level.txt
11
+ tests/test_at1_artifact_repo.py
@@ -0,0 +1,2 @@
1
+ [mlflow.artifact_repository]
2
+ at1 = mlflow_at1.at1_artifact_repo:AT1ArtifactRepository
@@ -0,0 +1,4 @@
1
+ mlflow-skinny>=2.0
2
+
3
+ [full]
4
+ mlflow>=2.0
@@ -0,0 +1 @@
1
+ mlflow_at1
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mlflow-at1"
7
+ version = "0.1.0"
8
+ description = "MLflow artifact repository backed by verified, compressed, byte-exact AT-1 containers"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "TinyFiles / AT-1" }]
13
+ keywords = ["mlflow", "artifacts", "compression", "provenance", "verified", "lossless"]
14
+ # Pure glue: depends on MLflow (the host) and the `at1` CLI on PATH (npm i -g @tinyfiles/cli).
15
+ # No engine code ships here, so this connector is Apache-2.0 and publishable. mlflow-skinny is the
16
+ # lightweight tracking/store/entities subset this plugin needs; `pip install mlflow-at1[full]` pulls full MLflow.
17
+ dependencies = ["mlflow-skinny>=2.0"]
18
+
19
+ [project.optional-dependencies]
20
+ full = ["mlflow>=2.0"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://tinyfiles.io"
24
+ Documentation = "https://tinyfiles.io/docs/mlflow"
25
+
26
+ # THE plugin hook: register the `at1://` scheme so MLflow routes artifacts through AT-1.
27
+ [project.entry-points."mlflow.artifact_repository"]
28
+ at1 = "mlflow_at1.at1_artifact_repo:AT1ArtifactRepository"
29
+
30
+ [tool.setuptools]
31
+ packages = ["mlflow_at1"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,85 @@
1
+ """Tests for the AT-1 MLflow artifact repository. Needs the `at1` CLI on PATH (npm i -g @tinyfiles/cli)
2
+ and mlflow-skinny. Skips cleanly if either is absent."""
3
+ import os
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
9
+
10
+ pytest.importorskip("mlflow")
11
+ from mlflow_at1.at1_artifact_repo import AT1ArtifactRepository, _BIN, _run # noqa: E402
12
+
13
+
14
+ def _at1_available():
15
+ try:
16
+ import subprocess
17
+ return subprocess.run(_BIN + ["--version"], capture_output=True).returncode == 0
18
+ except Exception:
19
+ return False
20
+
21
+
22
+ if not _at1_available():
23
+ pytest.skip("at1 CLI not on PATH (npm i -g @tinyfiles/cli)", allow_module_level=True)
24
+
25
+
26
+ def _repo(tmp_path):
27
+ return AT1ArtifactRepository(f"at1://{tmp_path.as_posix()}")
28
+
29
+
30
+ def _write(p, data):
31
+ p.write_bytes(data)
32
+ return str(p)
33
+
34
+
35
+ def test_log_download_byte_exact(tmp_path):
36
+ repo = _repo(tmp_path / "store")
37
+ src = _write(tmp_path / "model.bin", os.urandom(1000) + b"weights" * 4000)
38
+ repo.log_artifact(src)
39
+ # stored as a .at1 container, not the raw file
40
+ assert os.path.exists(os.path.join(repo._base, "model.bin.at1"))
41
+ out = tmp_path / "dl" / "model.bin"
42
+ repo._download_file("model.bin", str(out))
43
+ assert out.read_bytes() == open(src, "rb").read() # byte-exact reconstruction
44
+
45
+
46
+ def test_list_shows_logical_names(tmp_path):
47
+ repo = _repo(tmp_path / "store")
48
+ repo.log_artifact(_write(tmp_path / "a.pkl", b"A" * 5000))
49
+ repo.log_artifact(_write(tmp_path / "b.csv", b"id,v\n1,2\n3,4\n" * 500), artifact_path="data")
50
+ top = {fi.path: fi.is_dir for fi in repo.list_artifacts()}
51
+ assert "a.pkl" in top and top["a.pkl"] is False # logical name, no .at1 suffix
52
+ assert top.get("data") is True # subdir surfaces
53
+ assert {fi.path for fi in repo.list_artifacts("data")} == {"data/b.csv"}
54
+
55
+
56
+ def test_log_artifacts_directory_tree(tmp_path):
57
+ repo = _repo(tmp_path / "store")
58
+ d = tmp_path / "run"; (d / "sub").mkdir(parents=True)
59
+ _write(d / "top.bin", b"T" * 3000); _write(d / "sub" / "deep.bin", b"D" * 3000)
60
+ repo.log_artifacts(str(d), artifact_path="artifacts")
61
+ names = {fi.path for fi in repo.list_artifacts("artifacts")}
62
+ assert "artifacts/top.bin" in names
63
+ out = tmp_path / "deep.bin"
64
+ repo._download_file("artifacts/sub/deep.bin", str(out))
65
+ assert out.read_bytes() == b"D" * 3000
66
+
67
+
68
+ def test_integrity_and_tamper_rejection(tmp_path):
69
+ repo = _repo(tmp_path / "store")
70
+ repo.log_artifact(_write(tmp_path / "w.bin", os.urandom(500) + b"w" * 3000))
71
+ assert "integrity OK" in repo.integrity("w.bin")
72
+ # flip a byte inside the stored container -> download must REFUSE, not return wrong bytes
73
+ cont = os.path.join(repo._base, "w.bin.at1")
74
+ raw = bytearray(open(cont, "rb").read()); raw[len(raw) // 2] ^= 1
75
+ open(cont, "wb").write(raw)
76
+ with pytest.raises(RuntimeError):
77
+ repo._download_file("w.bin", str(tmp_path / "bad.out"))
78
+
79
+
80
+ def test_delete(tmp_path):
81
+ repo = _repo(tmp_path / "store")
82
+ repo.log_artifact(_write(tmp_path / "x.bin", b"X" * 4000))
83
+ assert repo.list_artifacts()
84
+ repo.delete_artifacts("x.bin")
85
+ assert not os.path.exists(os.path.join(repo._base, "x.bin.at1"))