ledgerworks 0.2.0__py3-none-any.whl
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.
- ledgerworks/__init__.py +71 -0
- ledgerworks/artifact.py +396 -0
- ledgerworks/cli.py +439 -0
- ledgerworks/equivalence.py +554 -0
- ledgerworks/jobs.py +811 -0
- ledgerworks/jobs_cli.py +171 -0
- ledgerworks/layout.py +158 -0
- ledgerworks/ledger.py +908 -0
- ledgerworks/lineage.py +87 -0
- ledgerworks/meta.py +419 -0
- ledgerworks/periods.py +229 -0
- ledgerworks/plugins.py +49 -0
- ledgerworks/relink.py +460 -0
- ledgerworks/schema.sql +85 -0
- ledgerworks/verify.py +362 -0
- ledgerworks-0.2.0.dist-info/METADATA +184 -0
- ledgerworks-0.2.0.dist-info/RECORD +20 -0
- ledgerworks-0.2.0.dist-info/WHEEL +4 -0
- ledgerworks-0.2.0.dist-info/entry_points.txt +3 -0
- ledgerworks-0.2.0.dist-info/licenses/LICENSE +21 -0
ledgerworks/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""ledgerworks: run, record, verify and trace pipeline artifacts.
|
|
2
|
+
|
|
3
|
+
Every output of a pipeline is registered as an immutable artifact directory
|
|
4
|
+
with a ``meta.json`` sidecar (identity, hyperparameters, inputs, model, one
|
|
5
|
+
record per execution with its git commit, file hashes) and a SQLite index
|
|
6
|
+
that can always be rebuilt from those sidecars.
|
|
7
|
+
|
|
8
|
+
from ledgerworks import Ledger
|
|
9
|
+
|
|
10
|
+
ledger = Ledger("/data/ledger")
|
|
11
|
+
source = ledger.latest("documents")
|
|
12
|
+
|
|
13
|
+
with ledger.run(kind="document_embeddings", pipeline="my-project",
|
|
14
|
+
pipeline_version="v0.1.0", sources=[source],
|
|
15
|
+
verifier="document_embeddings") as run:
|
|
16
|
+
... # write outputs into run.out_dir
|
|
17
|
+
|
|
18
|
+
Optional pieces: ``ledgerworks.jobs`` (resumable long runs), ``ledgerworks.periods``
|
|
19
|
+
and ``ledgerworks.layout`` (calendar partitions), ``ledgerworks.relink`` and
|
|
20
|
+
``ledgerworks.equivalence`` (re-register outputs under new inputs, checked;
|
|
21
|
+
needs the ``relink`` extra).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ledgerworks.artifact import (
|
|
25
|
+
Artifact,
|
|
26
|
+
ModelCard,
|
|
27
|
+
RunMeta,
|
|
28
|
+
RunRecord,
|
|
29
|
+
utc_now_iso,
|
|
30
|
+
)
|
|
31
|
+
from ledgerworks.ledger import DEFAULT_LAYER, VALID_LAYERS, Ledger, LedgerError, RunHandle
|
|
32
|
+
from ledgerworks.meta import git_commit, hash_file, read_meta, run_environment, write_sidecars
|
|
33
|
+
from ledgerworks.verify import (
|
|
34
|
+
Finding,
|
|
35
|
+
Severity,
|
|
36
|
+
VerifyReport,
|
|
37
|
+
discover_verifiers,
|
|
38
|
+
verify,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "0.2.0"
|
|
42
|
+
|
|
43
|
+
# Names from the pre-release package, kept so older code keeps importing.
|
|
44
|
+
DatalakeIndex = Ledger
|
|
45
|
+
DatalakeError = LedgerError
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Artifact",
|
|
49
|
+
"DEFAULT_LAYER",
|
|
50
|
+
"DatalakeError",
|
|
51
|
+
"DatalakeIndex",
|
|
52
|
+
"Finding",
|
|
53
|
+
"Ledger",
|
|
54
|
+
"LedgerError",
|
|
55
|
+
"ModelCard",
|
|
56
|
+
"RunHandle",
|
|
57
|
+
"RunMeta",
|
|
58
|
+
"RunRecord",
|
|
59
|
+
"Severity",
|
|
60
|
+
"VALID_LAYERS",
|
|
61
|
+
"VerifyReport",
|
|
62
|
+
"__version__",
|
|
63
|
+
"discover_verifiers",
|
|
64
|
+
"git_commit",
|
|
65
|
+
"run_environment",
|
|
66
|
+
"hash_file",
|
|
67
|
+
"read_meta",
|
|
68
|
+
"utc_now_iso",
|
|
69
|
+
"verify",
|
|
70
|
+
"write_sidecars",
|
|
71
|
+
]
|
ledgerworks/artifact.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Ledger artifact metadata types.
|
|
2
|
+
|
|
3
|
+
Four dataclasses:
|
|
4
|
+
|
|
5
|
+
ModelCard -- describes a model used to produce an artifact. For models on
|
|
6
|
+
the HuggingFace Hub, prefer pulling the Hub's own metadata;
|
|
7
|
+
this class is the fallback for models whose weights live
|
|
8
|
+
locally and cannot be published.
|
|
9
|
+
|
|
10
|
+
RunRecord -- one execution that contributed to an artifact. Carries its
|
|
11
|
+
own git commit, timestamps, and the list of outputs it wrote.
|
|
12
|
+
An artifact may have several: a completed artifact that later
|
|
13
|
+
gains more data appends a new RunRecord rather than mutating
|
|
14
|
+
history, so each execution's code version stays auditable.
|
|
15
|
+
|
|
16
|
+
RunMeta -- the artifact as a whole: identity (kind, hyperparams, model),
|
|
17
|
+
lineage sources, a verifier reference, and the list of
|
|
18
|
+
RunRecords. Serialised to meta.json.
|
|
19
|
+
|
|
20
|
+
Artifact -- a registered, on-disk artifact: RunMeta plus its resolved path
|
|
21
|
+
and per-file hashes. Returned by Ledger lookups.
|
|
22
|
+
|
|
23
|
+
Identity
|
|
24
|
+
--------
|
|
25
|
+
An artifact's ID is a human-readable slug, deterministic given its identity
|
|
26
|
+
fields (kind, model, pipeline_version, hyperparams, creation date):
|
|
27
|
+
|
|
28
|
+
{kind}__{model_id}-{model_version}__{pipeline_version}__{param_slug}__{YYYYMMDDTHHMM}
|
|
29
|
+
|
|
30
|
+
Slugs sort sensibly, are greppable, and survive being read out loud. They are
|
|
31
|
+
NOT content hashes: two artifacts with identical parameters created in
|
|
32
|
+
different minutes get different IDs. Content identity lives in the file hashes.
|
|
33
|
+
Artifacts created before 2026-10-04 keep their date-only suffix ({YYYYMMDD}):
|
|
34
|
+
the minute applies to records carrying ``id_minute`` (every new one), so no
|
|
35
|
+
existing id changes.
|
|
36
|
+
|
|
37
|
+
The folder group (``{layer}/{group}/{kind}/{id}``, e.g. the data source an
|
|
38
|
+
artifact derives from) is NOT part of the ID: an artifact can be moved
|
|
39
|
+
between groups without changing identity.
|
|
40
|
+
|
|
41
|
+
Completeness
|
|
42
|
+
------------
|
|
43
|
+
An artifact is complete when its last RunRecord has partial=False. Any
|
|
44
|
+
artifact whose last record is still partial is a crashed or in-progress run:
|
|
45
|
+
downstream code must ignore it, and verification flags it.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import re
|
|
51
|
+
from dataclasses import asdict, dataclass, field
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Slug construction
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
_SLUG_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
|
|
61
|
+
_SEP = "__"
|
|
62
|
+
|
|
63
|
+
_MAX_PARAM_VALUE_CHARS = 24
|
|
64
|
+
_MAX_PARAM_SLUG_CHARS = 96
|
|
65
|
+
_DIGEST_CHARS = 8
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def slugify(value: str) -> str:
|
|
69
|
+
"""Reduce an arbitrary string to a slug-safe component."""
|
|
70
|
+
cleaned = _SLUG_SAFE.sub("-", str(value)).strip("-")
|
|
71
|
+
return cleaned or "none"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _short_digest(text: str) -> str:
|
|
75
|
+
"""Stable short digest for slug use (process-independent)."""
|
|
76
|
+
import hashlib
|
|
77
|
+
|
|
78
|
+
return hashlib.blake2b(text.encode(), digest_size=_DIGEST_CHARS // 2).hexdigest()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _format_param_value(value: Any) -> str:
|
|
82
|
+
"""Render a hyperparameter value compactly for a slug."""
|
|
83
|
+
if isinstance(value, bool):
|
|
84
|
+
return "true" if value else "false"
|
|
85
|
+
if isinstance(value, float):
|
|
86
|
+
return f"{value:g}"
|
|
87
|
+
return slugify(value)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def param_slug(hyperparams: dict[str, Any]) -> str:
|
|
91
|
+
"""Deterministic, length-bounded slug for a hyperparameter dict.
|
|
92
|
+
|
|
93
|
+
Keys are sorted so construction does not depend on insertion order. Values
|
|
94
|
+
too long to inline are digested; if the whole slug is still too long it
|
|
95
|
+
collapses to one digest of the canonical dict. Both fallbacks stay
|
|
96
|
+
deterministic. Full values are always recorded in meta.json regardless.
|
|
97
|
+
"""
|
|
98
|
+
if not hyperparams:
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
parts: list[str] = []
|
|
102
|
+
for key, value in sorted(hyperparams.items()):
|
|
103
|
+
rendered = _format_param_value(value)
|
|
104
|
+
if len(rendered) > _MAX_PARAM_VALUE_CHARS:
|
|
105
|
+
rendered = _short_digest(f"{key}={value!r}")
|
|
106
|
+
parts.append(f"{slugify(key)}{rendered}")
|
|
107
|
+
|
|
108
|
+
slug = "_".join(parts)
|
|
109
|
+
if len(slug) > _MAX_PARAM_SLUG_CHARS:
|
|
110
|
+
canonical = repr(sorted(hyperparams.items()))
|
|
111
|
+
slug = f"params{_short_digest(canonical)}"
|
|
112
|
+
return slug
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Time helper
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def utc_now_iso() -> str:
|
|
121
|
+
"""Current UTC time as ISO8601 with second precision."""
|
|
122
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# ModelCard
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class ModelCard:
|
|
132
|
+
"""Describes a model used to produce an artifact.
|
|
133
|
+
|
|
134
|
+
``backend`` names the inference engine that ran the model (``local``,
|
|
135
|
+
``tei``, ``embedx``) and ``serving`` holds everything that engine reported
|
|
136
|
+
about the model it served (a TEI ``/info`` snapshot, an embedx model
|
|
137
|
+
entry, the local device and dtype) plus the load-time compatibility-check
|
|
138
|
+
results. Both are ``None`` on cards written before they existed.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
model_id: str
|
|
142
|
+
version: str
|
|
143
|
+
repo: str
|
|
144
|
+
weights_public: bool
|
|
145
|
+
weights_sha256: str | None = None
|
|
146
|
+
commit: str | None = None
|
|
147
|
+
architecture: str | None = None
|
|
148
|
+
dim: int | None = None
|
|
149
|
+
pooling: str | None = None
|
|
150
|
+
trained_on: str | None = None
|
|
151
|
+
notes: str = ""
|
|
152
|
+
backend: str | None = None
|
|
153
|
+
serving: dict[str, Any] | None = None
|
|
154
|
+
|
|
155
|
+
_NO_SHA_WARNING = (
|
|
156
|
+
"[warning: private weights with no weights_sha256; "
|
|
157
|
+
"third parties cannot verify weight identity]"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def __post_init__(self) -> None:
|
|
161
|
+
# Idempotent: from_dict re-runs this on every load, so the warning must
|
|
162
|
+
# not be appended again when the stored notes already carry it.
|
|
163
|
+
if (
|
|
164
|
+
not self.weights_public
|
|
165
|
+
and not self.weights_sha256
|
|
166
|
+
and self._NO_SHA_WARNING not in self.notes
|
|
167
|
+
):
|
|
168
|
+
self.notes = ((self.notes + " " if self.notes else "") + self._NO_SHA_WARNING).strip()
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def slug(self) -> str:
|
|
172
|
+
return f"{slugify(self.model_id)}-{slugify(self.version)}"
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> dict[str, Any]:
|
|
175
|
+
return asdict(self)
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_dict(cls, data: dict[str, Any]) -> ModelCard:
|
|
179
|
+
known = {f for f in cls.__dataclass_fields__}
|
|
180
|
+
return cls(**{k: v for k, v in data.items() if k in known})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# RunRecord
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class RunRecord:
|
|
190
|
+
"""One execution that contributed outputs to an artifact.
|
|
191
|
+
|
|
192
|
+
`produced` lists the output file names this execution wrote (just this
|
|
193
|
+
run's contribution, not the whole artifact). The union of produced lists
|
|
194
|
+
across all records is what a verifier checks against the declared scope.
|
|
195
|
+
|
|
196
|
+
`partial` is True from the moment the record is created until the execution
|
|
197
|
+
completes cleanly. A record left partial is a crashed run.
|
|
198
|
+
|
|
199
|
+
`env` describes where the execution ran (see `meta.run_environment`); it is
|
|
200
|
+
empty on records written before it existed and on relinked artifacts.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
run_start: str
|
|
204
|
+
pipeline_version: str
|
|
205
|
+
pipeline_commit: str | None = None
|
|
206
|
+
run_end: str | None = None
|
|
207
|
+
partial: bool = True
|
|
208
|
+
produced: list[str] = field(default_factory=list)
|
|
209
|
+
notes: str = ""
|
|
210
|
+
env: dict[str, Any] = field(default_factory=dict)
|
|
211
|
+
|
|
212
|
+
def to_dict(self) -> dict[str, Any]:
|
|
213
|
+
return asdict(self)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def from_dict(cls, data: dict[str, Any]) -> RunRecord:
|
|
217
|
+
known = {f for f in cls.__dataclass_fields__}
|
|
218
|
+
return cls(**{k: v for k, v in data.items() if k in known})
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# RunMeta
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class RunMeta:
|
|
228
|
+
"""Reproducibility record for one artifact across all its executions."""
|
|
229
|
+
|
|
230
|
+
kind: str
|
|
231
|
+
pipeline: str
|
|
232
|
+
pipeline_version: str
|
|
233
|
+
pipeline_repo: str | None = None
|
|
234
|
+
hyperparams: dict[str, Any] = field(default_factory=dict)
|
|
235
|
+
sources: list[str] = field(default_factory=list)
|
|
236
|
+
model_card: ModelCard | None = None
|
|
237
|
+
verifier: str | None = None # entry-point name for content verification
|
|
238
|
+
runs: list[RunRecord] = field(default_factory=list)
|
|
239
|
+
deprecated: bool = False
|
|
240
|
+
deprecation_reason: str | None = None
|
|
241
|
+
notes: str = ""
|
|
242
|
+
# Frozen at creation so the artifact_id stays stable across executions.
|
|
243
|
+
created: str = ""
|
|
244
|
+
# Folder the artifact lives under, {layer}/{group}/{kind}/{id} (None: {layer}/{kind}/{id}).
|
|
245
|
+
# Not part of the identity: moving an artifact never changes its id.
|
|
246
|
+
group: str | None = None
|
|
247
|
+
# Set on an artifact registered by Ledger.relink: {"from": old id, "replace":
|
|
248
|
+
# {old parent: new parent}, "checks": [check results], "commit": git commit}.
|
|
249
|
+
relink: dict[str, Any] | None = None
|
|
250
|
+
# New artifacts carry the creation minute in their id (YYYYMMDDTHHMM); records
|
|
251
|
+
# written before this field existed keep the date-only id they were registered under.
|
|
252
|
+
id_minute: bool = False
|
|
253
|
+
|
|
254
|
+
def __post_init__(self) -> None:
|
|
255
|
+
if not self.created:
|
|
256
|
+
self.created = utc_now_iso()
|
|
257
|
+
self.id_minute = True
|
|
258
|
+
|
|
259
|
+
# -- aggregate state ---------------------------------------------------
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def partial(self) -> bool:
|
|
263
|
+
"""An artifact is complete only when its last run finished cleanly."""
|
|
264
|
+
if not self.runs:
|
|
265
|
+
return True
|
|
266
|
+
return self.runs[-1].partial
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def run_start(self) -> str:
|
|
270
|
+
"""Start of the first execution."""
|
|
271
|
+
return self.runs[0].run_start if self.runs else self.created
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def run_end(self) -> str | None:
|
|
275
|
+
"""End of the last execution, or None if still partial."""
|
|
276
|
+
if not self.runs:
|
|
277
|
+
return None
|
|
278
|
+
return self.runs[-1].run_end
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def pipeline_commit(self) -> str | None:
|
|
282
|
+
"""Commit of the last execution."""
|
|
283
|
+
return self.runs[-1].pipeline_commit if self.runs else None
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def produced(self) -> list[str]:
|
|
287
|
+
"""Union of all outputs across every execution, sorted and deduped."""
|
|
288
|
+
seen: set[str] = set()
|
|
289
|
+
for record in self.runs:
|
|
290
|
+
seen.update(record.produced)
|
|
291
|
+
return sorted(seen)
|
|
292
|
+
|
|
293
|
+
# -- identity ----------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def artifact_id(self) -> str:
|
|
297
|
+
parts: list[str] = [slugify(self.kind)]
|
|
298
|
+
if self.model_card is not None:
|
|
299
|
+
parts.append(self.model_card.slug)
|
|
300
|
+
parts.append(slugify(self.pipeline_version))
|
|
301
|
+
params = param_slug(self.hyperparams)
|
|
302
|
+
if params:
|
|
303
|
+
parts.append(params)
|
|
304
|
+
stamp = self.created[:16] if self.id_minute else self.created[:10]
|
|
305
|
+
parts.append(stamp.replace("-", "").replace(":", ""))
|
|
306
|
+
return _SEP.join(parts)
|
|
307
|
+
|
|
308
|
+
# -- serialisation -----------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def to_dict(self) -> dict[str, Any]:
|
|
311
|
+
return {
|
|
312
|
+
"kind": self.kind,
|
|
313
|
+
"pipeline": self.pipeline,
|
|
314
|
+
"pipeline_version": self.pipeline_version,
|
|
315
|
+
"pipeline_repo": self.pipeline_repo,
|
|
316
|
+
"hyperparams": dict(self.hyperparams),
|
|
317
|
+
"sources": list(self.sources),
|
|
318
|
+
"model_card": self.model_card.to_dict() if self.model_card else None,
|
|
319
|
+
"verifier": self.verifier,
|
|
320
|
+
"runs": [r.to_dict() for r in self.runs],
|
|
321
|
+
"deprecated": self.deprecated,
|
|
322
|
+
"deprecation_reason": self.deprecation_reason,
|
|
323
|
+
"notes": self.notes,
|
|
324
|
+
"created": self.created,
|
|
325
|
+
"group": self.group,
|
|
326
|
+
"relink": dict(self.relink) if self.relink is not None else None,
|
|
327
|
+
**({"id_minute": True} if self.id_minute else {}),
|
|
328
|
+
# Denormalised aggregates for human/tool convenience (never read back).
|
|
329
|
+
"partial": self.partial,
|
|
330
|
+
"run_start": self.run_start,
|
|
331
|
+
"run_end": self.run_end,
|
|
332
|
+
"pipeline_commit": self.pipeline_commit,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def from_dict(cls, data: dict[str, Any]) -> RunMeta:
|
|
337
|
+
payload = dict(data)
|
|
338
|
+
card = payload.pop("model_card", None)
|
|
339
|
+
runs_raw = payload.pop("runs", [])
|
|
340
|
+
for derived in ("partial", "run_start", "run_end", "pipeline_commit"):
|
|
341
|
+
payload.pop(derived, None)
|
|
342
|
+
known = {f for f in cls.__dataclass_fields__}
|
|
343
|
+
filtered = {k: v for k, v in payload.items() if k in known}
|
|
344
|
+
return cls(
|
|
345
|
+
**filtered,
|
|
346
|
+
model_card=ModelCard.from_dict(card) if card else None,
|
|
347
|
+
runs=[RunRecord.from_dict(r) for r in runs_raw],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# Artifact
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass
|
|
357
|
+
class Artifact:
|
|
358
|
+
"""A registered artifact on disk: its metadata, path, and file hashes."""
|
|
359
|
+
|
|
360
|
+
artifact_id: str
|
|
361
|
+
layer: str # 'raw' | 'derived' | 'output'
|
|
362
|
+
path: Path
|
|
363
|
+
meta: RunMeta
|
|
364
|
+
file_hashes: dict[str, str] = field(default_factory=dict)
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def kind(self) -> str:
|
|
368
|
+
return self.meta.kind
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def partial(self) -> bool:
|
|
372
|
+
return self.meta.partial
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def deprecated(self) -> bool:
|
|
376
|
+
return self.meta.deprecated
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def group(self) -> str | None:
|
|
380
|
+
return self.meta.group
|
|
381
|
+
|
|
382
|
+
def glob(self, pattern: str = "*.parquet") -> str:
|
|
383
|
+
"""A glob string ready to hand to DuckDB's read_parquet()."""
|
|
384
|
+
return str(self.path / pattern)
|
|
385
|
+
|
|
386
|
+
def files(self, pattern: str = "*.parquet") -> list[Path]:
|
|
387
|
+
return sorted(self.path.glob(pattern))
|
|
388
|
+
|
|
389
|
+
def __repr__(self) -> str:
|
|
390
|
+
flags = []
|
|
391
|
+
if self.partial:
|
|
392
|
+
flags.append("PARTIAL")
|
|
393
|
+
if self.deprecated:
|
|
394
|
+
flags.append("DEPRECATED")
|
|
395
|
+
suffix = f" [{','.join(flags)}]" if flags else ""
|
|
396
|
+
return f"<Artifact {self.artifact_id}{suffix}>"
|