runprov 0.1.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.
- runprov/__init__.py +189 -0
- runprov/__main__.py +1816 -0
- runprov/_atomic.py +180 -0
- runprov/_report.py +126 -0
- runprov/environment.py +409 -0
- runprov/export.py +258 -0
- runprov/hashing.py +639 -0
- runprov/project.py +703 -0
- runprov/prune.py +277 -0
- runprov/py.typed +0 -0
- runprov/run.py +3309 -0
- runprov/show.py +1089 -0
- runprov/sinks.py +309 -0
- runprov/terminal.py +512 -0
- runprov/verify.py +742 -0
- runprov/watch.py +277 -0
- runprov-0.1.0.dist-info/METADATA +142 -0
- runprov-0.1.0.dist-info/RECORD +21 -0
- runprov-0.1.0.dist-info/WHEEL +4 -0
- runprov-0.1.0.dist-info/entry_points.txt +2 -0
- runprov-0.1.0.dist-info/licenses/LICENSE +28 -0
runprov/__init__.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Copyright (c) 2026 Assistance Publique – Hôpitaux de Paris (AP-HP),
|
|
2
|
+
# Hôpital Henri-Mondor, and Taylor Thompson
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
# Licensed under the BSD 3-Clause License — see LICENSE.
|
|
5
|
+
r"""runprov — record what a script read, wrote and ran as, in a form that can be checked.
|
|
6
|
+
|
|
7
|
+
RAW, and that is load-bearing rather than stylistic: the example passes `sep="\t"`, and in a
|
|
8
|
+
normal docstring that is an actual tab by the time `help(runprov)` prints it — the reader is
|
|
9
|
+
shown `sep=" "` and cannot tell what to type. The example is the thing being got right
|
|
10
|
+
here, so it has to survive being rendered.
|
|
11
|
+
|
|
12
|
+
from runprov import Run, configure
|
|
13
|
+
|
|
14
|
+
configure(root=REPO) # history -> REPO/provenance/runs.jsonl, where the CLI looks
|
|
15
|
+
|
|
16
|
+
PROV = OUT.with_name("build_labels_provenance.json")
|
|
17
|
+
|
|
18
|
+
# provenance=PROV is what makes a crash record. Without it, __exit__ writes nothing.
|
|
19
|
+
with Run("build_labels", vars(args), provenance=PROV) as run:
|
|
20
|
+
df = pd.read_csv(run.input(INPUT), sep="\t") # registering IS how you open it
|
|
21
|
+
with open(run.output(OUT), "w", encoding="utf-8") as fh:
|
|
22
|
+
fh.write(run.header()) # the pin, inside the artifact
|
|
23
|
+
df.to_csv(fh, sep="\t", index=False)
|
|
24
|
+
run.note("n_rows", len(df))
|
|
25
|
+
|
|
26
|
+
`provenance=` on the CONSTRUCTOR is load-bearing and this docstring used to get it wrong —
|
|
27
|
+
it showed `run = Run(...)` with a closing `run.write(PROV)`, which is the shape README's
|
|
28
|
+
"Two shapes that record nothing" table lists first: a crash halfway records no sidecar, no
|
|
29
|
+
history line, and prints no warning. `with` alone does not fix it either, because `__exit__`
|
|
30
|
+
only writes when `provenance=` reached the constructor. That the package's own front page
|
|
31
|
+
taught the failure it exists to prevent is why `Run`'s class docstring states the rule
|
|
32
|
+
unhedged, and why a test now asserts this one does too.
|
|
33
|
+
|
|
34
|
+
Three properties, each of which exists because its absence caused a specific defect:
|
|
35
|
+
|
|
36
|
+
1. **Registration is the ergonomic path.** `input()` returns the path, so the natural way
|
|
37
|
+
to open a file is the recorded way.
|
|
38
|
+
2. **The pin lives in the artifact**, not only in a sidecar, so an artifact can say what
|
|
39
|
+
it was made from after the sidecar has been overwritten.
|
|
40
|
+
3. **The pin is deterministic.** No timestamps, no run id — those make every artifact
|
|
41
|
+
differ on every run, which produces a permanently red reproducibility check, which
|
|
42
|
+
trains everyone to ignore it.
|
|
43
|
+
|
|
44
|
+
Promoted out of `scripts/audit/_provenance.py` (ADR-028). That module is now a shim over
|
|
45
|
+
this package, so the audit and any new pipeline share one implementation rather than two
|
|
46
|
+
copies that agree until they do not.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import typing
|
|
50
|
+
|
|
51
|
+
from .hashing import content_digest, describe, sha256
|
|
52
|
+
from .project import (
|
|
53
|
+
DEFAULT_CODE_PATHS,
|
|
54
|
+
DEFAULT_TRACKED,
|
|
55
|
+
Project,
|
|
56
|
+
active,
|
|
57
|
+
configure,
|
|
58
|
+
detect_root,
|
|
59
|
+
is_configured,
|
|
60
|
+
)
|
|
61
|
+
from .run import HISTORY_SCHEMA, SCHEMA, START_SCHEMA, Run, Terminated
|
|
62
|
+
from .sinks import JsonlSink, RecordSink
|
|
63
|
+
|
|
64
|
+
# EVERY NAME HERE IS A PROMISE. It was 27, and the quickstart uses two of them; a previous
|
|
65
|
+
# review already called 24 too many to freeze. Five came out on 2026-08-19 (ledger L-24):
|
|
66
|
+
#
|
|
67
|
+
# VOLATILE, VOLATILE_JSON compiled regexes -- the MECHANISM of volatile-stamp stripping.
|
|
68
|
+
# Exporting them means never being able to change how it works.
|
|
69
|
+
# PIN_UNSAFE documentation rendered into a refusal message. Nothing branches
|
|
70
|
+
# on it -- proved by emptying it, behaviour unchanged.
|
|
71
|
+
# default_run_id, defaults `Project` already supplies; a caller passes a callable
|
|
72
|
+
# default_generation rather than reaching for these.
|
|
73
|
+
#
|
|
74
|
+
# They still EXIST at `runprov.run.PIN_UNSAFE`, `runprov.hashing.VOLATILE` and
|
|
75
|
+
# `runprov.project.default_run_id` -- they simply stop being promised. SCHEMA and
|
|
76
|
+
# HISTORY_SCHEMA stay (a consumer parsing records needs the version) and so do the two
|
|
77
|
+
# DEFAULT_* tuples (people extend them).
|
|
78
|
+
#
|
|
79
|
+
# FIVE MORE ON 2026-08-20 (ledger L-84 follow-up), for a different reason: not "this is
|
|
80
|
+
# mechanism" but "nobody was ever told to call this". Every one had ZERO references in
|
|
81
|
+
# README, GETTING-STARTED, WHY or any ADR -- checked, not assumed -- while the features they
|
|
82
|
+
# belong to are documented entirely as configuration and record fields:
|
|
83
|
+
#
|
|
84
|
+
# installed_packages, the environment snapshot is reached through
|
|
85
|
+
# write_snapshot `Project(env_snapshot_dir=...)` and the record it writes
|
|
86
|
+
# Capture terminal capture is reached through `terminal_log`
|
|
87
|
+
# MemorySink `sink=` takes one; the class is for tests and for people
|
|
88
|
+
# writing their own, neither of which needs a promise
|
|
89
|
+
# git 6 internal call sites and no documented caller. A bare `git`
|
|
90
|
+
# in an importing namespace also collides with GitPython's
|
|
91
|
+
# top-level module, and the contract -- swallow everything,
|
|
92
|
+
# return None, 20s timeout -- is provenance capture rather than
|
|
93
|
+
# a general-purpose runner. ADR-0003 left this open; this closes
|
|
94
|
+
# it by withdrawal rather than by rename.
|
|
95
|
+
#
|
|
96
|
+
# ONE ADDED ON 2026-08-22 (ledger A-30), and it is the only name ever promoted here:
|
|
97
|
+
# `START_SCHEMA`. Since the start line landed, `runs.jsonl` carries a third schema and the
|
|
98
|
+
# README prints it to external readers, so the promise existed as a copy-pasted string
|
|
99
|
+
# before it existed as a name. Every reader inside the package filters on it, and the
|
|
100
|
+
# package's own test says an outside one that does not "would count each completed run
|
|
101
|
+
# TWICE". That is verbatim the reason `HISTORY_SCHEMA` is promised.
|
|
102
|
+
#
|
|
103
|
+
# WHY THE QUIET ONES STAY, recorded because a promised name with no document reference has
|
|
104
|
+
# to justify itself — the criterion the L-84 withdrawal used, applied to what survived it
|
|
105
|
+
# (ledger A-31). A test requires a line here for every such name:
|
|
106
|
+
# `SCHEMA`, `HISTORY_SCHEMA`, `START_SCHEMA` — a consumer parsing records needs the
|
|
107
|
+
# version, and the third one decides whether a run is counted once or twice.
|
|
108
|
+
# `DEFAULT_CODE_PATHS`, `DEFAULT_TRACKED` — people extend them.
|
|
109
|
+
# `active` and `is_configured` — the pair a LIBRARY needs rather than a script:
|
|
110
|
+
# `is_configured()` to avoid re-configuring somebody else's project, `active()` to read
|
|
111
|
+
# what was resolved. There is no other documented spelling for either, which is why the
|
|
112
|
+
# L-84 criterion does not reach them: it is two-pronged, and its second prong is that the
|
|
113
|
+
# feature is documented as configuration and record fields instead. These have no such
|
|
114
|
+
# alternate route.
|
|
115
|
+
# `detect_root` — the root-detection RULE is documented behaviour, and a caller
|
|
116
|
+
# reproducing it by hand is exactly the drift this package exists to remove.
|
|
117
|
+
#
|
|
118
|
+
# 18 names. All ten withdrawn names still exist on their own modules.
|
|
119
|
+
__all__ = [
|
|
120
|
+
"DEFAULT_CODE_PATHS",
|
|
121
|
+
"DEFAULT_TRACKED",
|
|
122
|
+
"HISTORY_SCHEMA",
|
|
123
|
+
"SCHEMA",
|
|
124
|
+
"START_SCHEMA",
|
|
125
|
+
"JsonlSink",
|
|
126
|
+
"Project",
|
|
127
|
+
"RecordSink",
|
|
128
|
+
"Run",
|
|
129
|
+
"Terminated",
|
|
130
|
+
"active",
|
|
131
|
+
"configure",
|
|
132
|
+
"content_digest",
|
|
133
|
+
"describe",
|
|
134
|
+
"detect_root",
|
|
135
|
+
"is_configured",
|
|
136
|
+
"sha256",
|
|
137
|
+
"to_yaml",
|
|
138
|
+
]
|
|
139
|
+
__version__ = "0.1.0"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def to_yaml(records: typing.Any) -> str: # noqa: ANN401 - one record or an iterable of them
|
|
143
|
+
"""Render a record — or a whole history — as the transformation-log YAML shape.
|
|
144
|
+
|
|
145
|
+
The predecessor wrote a per-run `*_manifest_*.yml` with `yaml.safe_dump`, and every
|
|
146
|
+
script doing so carried its own `json_safe()` to make pandas and numpy scalars
|
|
147
|
+
dumpable. Both of those are covered here: the renderer needs **no pyyaml**, and the
|
|
148
|
+
record has already been through `_jsonable`, which resolves a `numpy.int64` to an
|
|
149
|
+
`int` rather than to the string `"6"`.
|
|
150
|
+
|
|
151
|
+
`python -m runprov log --format yaml` is this same function over a whole history. This
|
|
152
|
+
is it for one run, for a caller who wants the manifest as a file beside the artifact:
|
|
153
|
+
|
|
154
|
+
with Run("step", provenance=PROV) as run:
|
|
155
|
+
...
|
|
156
|
+
MANIFEST.write_text(runprov.to_yaml(run.record), encoding="utf-8")
|
|
157
|
+
|
|
158
|
+
AFTER the block, deliberately. `__exit__` is where outputs are hashed and the status
|
|
159
|
+
becomes known, so a manifest rendered inside the block describes a run that has not
|
|
160
|
+
finished — the same trap as `run.write()` instead of `provenance=`.
|
|
161
|
+
|
|
162
|
+
What it does NOT do is append `---` documents to a shared file. That is not an
|
|
163
|
+
oversight: the predecessor's writer appended `---` into a file that began as a list,
|
|
164
|
+
`yaml.safe_load_all` raises partway through the result, and nine
|
|
165
|
+
`fix_transformation_log_*.py` repair scripts exist because of it. The append-only
|
|
166
|
+
history is JSONL for exactly that reason, and this renders a VIEW of it.
|
|
167
|
+
|
|
168
|
+
THE ONLY `to_yaml` IN THE PACKAGE, since 2026-08-19. `runprov.show` carried a second
|
|
169
|
+
function of the same name and a different shape — it renders any nested structure as
|
|
170
|
+
indented YAML, this one renders RUN RECORDS in the transformation-log shape. Both were
|
|
171
|
+
importable and neither raised on the other's input, so the wrong import produced a
|
|
172
|
+
plausible file of the WRONG SHAPE rather than an error. It is now `show.render_yaml`
|
|
173
|
+
(ledger L-47).
|
|
174
|
+
|
|
175
|
+
This one also normalises with `_jsonable` first, which `render_yaml` does not — that is
|
|
176
|
+
what keeps a manifest written here byte-comparable with the sidecar written by
|
|
177
|
+
`run.write()`, and it is why the two disagreed: on a record holding a numpy-like scalar,
|
|
178
|
+
this renders `118` where the low-level one renders `"<scalar 118>"`.
|
|
179
|
+
"""
|
|
180
|
+
from .__main__ import _yaml # local: keeps the CLI module off the package import path
|
|
181
|
+
from .run import _jsonable
|
|
182
|
+
|
|
183
|
+
# THE SAME normalisation the sidecar and the history line go through. `_jsonable` runs
|
|
184
|
+
# at serialisation time, so a LIVE `run.record` still holds whatever the caller handed
|
|
185
|
+
# to `note()` -- and rendering that directly produced a manifest reading
|
|
186
|
+
# `n_exact_matches: "<scalar 118>"` beside a sidecar reading `118`, for one run. Two
|
|
187
|
+
# renderings of the same record must not disagree, which is the whole claim here.
|
|
188
|
+
rows = [records] if isinstance(records, dict) else list(records)
|
|
189
|
+
return _yaml([_jsonable(r) for r in rows])
|