makeprov 0.4.7__tar.gz → 0.7.1__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.
Files changed (39) hide show
  1. makeprov-0.7.1/LICENSE +21 -0
  2. makeprov-0.7.1/PKG-INFO +455 -0
  3. makeprov-0.7.1/README.md +417 -0
  4. makeprov-0.7.1/pyproject.toml +55 -0
  5. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/__init__.py +5 -0
  6. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/config.py +59 -2
  7. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/context.jsonld +7 -0
  8. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/core.py +97 -5
  9. makeprov-0.7.1/src/makeprov/forges.py +106 -0
  10. makeprov-0.7.1/src/makeprov/forges.toml +35 -0
  11. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/paths.py +43 -3
  12. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/prov.py +338 -87
  13. makeprov-0.7.1/src/makeprov/refs.py +133 -0
  14. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/snakemake.py +148 -14
  15. makeprov-0.7.1/src/makeprov.egg-info/PKG-INFO +455 -0
  16. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov.egg-info/SOURCES.txt +7 -0
  17. makeprov-0.7.1/src/makeprov.egg-info/entry_points.txt +2 -0
  18. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov.egg-info/requires.txt +9 -6
  19. makeprov-0.7.1/tests/test_forges.py +109 -0
  20. makeprov-0.7.1/tests/test_jsonld_refs.py +33 -0
  21. {makeprov-0.4.7 → makeprov-0.7.1}/tests/test_makeprov.py +43 -0
  22. makeprov-0.7.1/tests/test_model_0_7.py +413 -0
  23. makeprov-0.7.1/tests/test_prov_context.py +118 -0
  24. makeprov-0.7.1/tests/test_snakemake.py +336 -0
  25. {makeprov-0.4.7 → makeprov-0.7.1}/tests/test_span_and_cached_download.py +54 -1
  26. makeprov-0.4.7/PKG-INFO +0 -252
  27. makeprov-0.4.7/README.md +0 -222
  28. makeprov-0.4.7/pyproject.toml +0 -51
  29. makeprov-0.4.7/src/makeprov.egg-info/PKG-INFO +0 -252
  30. makeprov-0.4.7/tests/test_jsonld_refs.py +0 -31
  31. makeprov-0.4.7/tests/test_prov_context.py +0 -39
  32. makeprov-0.4.7/tests/test_snakemake.py +0 -121
  33. {makeprov-0.4.7 → makeprov-0.7.1}/setup.cfg +0 -0
  34. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/rdfmixin.py +0 -0
  35. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov/span.py +0 -0
  36. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov.egg-info/dependency_links.txt +0 -0
  37. {makeprov-0.4.7 → makeprov-0.7.1}/src/makeprov.egg-info/top_level.txt +0 -0
  38. {makeprov-0.4.7 → makeprov-0.7.1}/tests/test_env_identity.py +0 -0
  39. {makeprov-0.4.7 → makeprov-0.7.1}/tests/test_prov_shacl.py +0 -0
makeprov-0.7.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benno Kruit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,455 @@
1
+ Metadata-Version: 2.4
2
+ Name: makeprov
3
+ Version: 0.7.1
4
+ Summary: A PROV/JSON-LD provenance tracking library for Python scripts, with an optional Snakemake bridge
5
+ Author-email: Benno Kruit <b.b.kruit@amsterdamumc.nl>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bennokr/makeprov
8
+ Project-URL: Documentation, https://bennokr.github.io/makeprov
9
+ Project-URL: Issue Tracker, https://github.com/bennokr/makeprov/issues
10
+ Keywords: provenance,prov,rdf,json-ld,snakemake
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: packaging>=21.3
20
+ Requires-Dist: parse>=1.20
21
+ Provides-Extra: rdf
22
+ Requires-Dist: rdflib>=6.0; extra == "rdf"
23
+ Requires-Dist: pyshacl>=0.20; extra == "rdf"
24
+ Provides-Extra: snakemake
25
+ Requires-Dist: snakemake; extra == "snakemake"
26
+ Provides-Extra: cli
27
+ Requires-Dist: defopt>=6; extra == "cli"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: makeprov[cli,rdf]; extra == "dev"
31
+ Provides-Extra: docs
32
+ Requires-Dist: sphinx>=7; extra == "docs"
33
+ Requires-Dist: myst-parser[linkify]; extra == "docs"
34
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
35
+ Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
36
+ Requires-Dist: makeprov[cli]; extra == "docs"
37
+ Dynamic: license-file
38
+
39
+ # makeprov: Pythonic Provenance Tracking
40
+
41
+ `makeprov` is a small library for recording W3C PROV/JSON-LD provenance
42
+ around Python functions that read and write files: which inputs produced
43
+ which outputs, when, with what code and environment. A decorator wraps a
44
+ function, tracks the files it declares as inputs/outputs, and writes a
45
+ provenance record after each call. A minimal `make`-style dependency
46
+ resolver and an optional Snakemake bridge are included, but the core
47
+ contract of the library is the provenance record — not workflow
48
+ orchestration, which tools like Snakemake already do well.
49
+
50
+ ## Features
51
+
52
+ - Decorator-based rules that infer dependencies from `InPath`/`OutPath`
53
+ parameters and write a PROV/JSON-LD record after every call.
54
+ - A clean `Plan → Run → Artifact` model: the script at a commit is a
55
+ `prov:Plan`, the runtime and the user are agents, and the two are tied
56
+ together by `prov:qualifiedAssociation`/`prov:hadPlan`.
57
+ - `ArtifactRef` lets a run cite external entities — a dataset IRI, an
58
+ object-store key, a model checkpoint — without makeprov copying their metadata.
59
+ - Provenance write failures are fatal by default (`ProvenanceConfig(strict=True)`),
60
+ so a rule can't silently "succeed" with no record of what it did.
61
+ - Resolve templated targets (``results/{sample}.txt``) via ``parse``-style patterns,
62
+ and a small dependency resolver (`build`/`build_all`) for chaining rules.
63
+ - Serialize provenance as JSON-LD, or as RDF/TriG when `rdflib` is installed
64
+ (`pip install "makeprov[rdf]"`).
65
+ - Optional Snakemake bridge that turns `--d3dag` and `--detailed-summary`
66
+ output into PROV JSON-LD artifacts ready for inclusion in Snakemake HTML reports.
67
+
68
+ ## The provenance model
69
+
70
+ makeprov keeps PROV's distinction between the *plan* (the recipe) and the
71
+ *agent* (whoever carried it out):
72
+
73
+ ```text
74
+ run.py @ git SHA a prov:Plan, schema:SoftwareSourceCode
75
+ CPython 3.11 a prov:Agent, prov:SoftwareAgent
76
+ you (opt-in) a prov:Agent, schema:Person
77
+
78
+ train-20260910T…-c2f6dc7b a prov:Activity
79
+ prov:used dataset-X, the Python environment
80
+ prov:wasAssociatedWith runtime, person
81
+ prov:qualifiedAssociation [ prov:agent person ; prov:hadPlan run.py ]
82
+
83
+ results/model.txt a prov:Entity
84
+ prov:wasGeneratedBy train-20260910T…-c2f6dc7b
85
+ dct:identifier sha256:…
86
+ ```
87
+
88
+ Keeping the plan and the agent apart is what makes the graph mappable onto
89
+ [Workflow Run RO-Crate](https://www.researchobject.org/workflow-run-crate/),
90
+ whose `instrument` (the software that was run) and `agent` (a Person or
91
+ Organization) are separate slots:
92
+
93
+ | makeprov / PROV-O | Process Run Crate |
94
+ | ------------------------ | ----------------- |
95
+ | `prov:Plan` | `instrument` |
96
+ | `prov:Activity` | `CreateAction` |
97
+ | `prov:used` | `object` |
98
+ | `prov:wasGeneratedBy` | `result` |
99
+ | `schema:Person` agent | `agent` |
100
+ | `startedAtTime`/`endedAtTime` | `startTime`/`endTime` |
101
+
102
+ The `schema:Person` agent is **off by default**: provenance documents are
103
+ routinely committed and published, and a name and email address are personal
104
+ data you should choose to publish rather than emit by accident. Turn it on with
105
+ `ProvenanceConfig(record_user=True)`, or `--record-user` on the Snakemake
106
+ bridge. Without it, the qualified association names the runtime as the
107
+ responsible agent.
108
+
109
+ Note that WRROC is Schema.org-native and defines no normative PROV-O mapping;
110
+ the table above is a practical alignment, not an OWL equivalence. makeprov's
111
+ own vocabulary stays `prov:`/`schema:` — RO-Crate and OpenLineage are intended
112
+ as adapters over this model rather than changes to it.
113
+
114
+ ### Planned structure vs. observed execution
115
+
116
+ By default a document is purely *retrospective*: it records the activities that
117
+ ran. A rule that was already up to date contributes nothing, because asserting
118
+ an execution that did not happen would be worse than saying nothing.
119
+
120
+ Set `emit_plan_graph = true` (or pass `--plan-graph`) to additionally emit the
121
+ *prospective* structure — each rule as a `prov:Plan` in its own right, linked to
122
+ the plans it depends on by `dct:requires`:
123
+
124
+ ```text
125
+ run.py#rule-transform a prov:Plan
126
+ dct:requires run.py#rule-extract
127
+ dct:source run.py
128
+ ```
129
+
130
+ The activity's `prov:hadPlan` then points at the specific rule rather than the
131
+ whole script. The Snakemake bridge does the same, collapsing the job DAG's
132
+ edges to rule-level `dct:requires` edges.
133
+
134
+ ### Forge profiles
135
+
136
+ When no `base_iri` is set, makeprov derives one from the git remote. The
137
+ supported hosts are declared in [`forges.toml`](src/makeprov/forges.toml) —
138
+ GitHub, GitLab, Bitbucket, Forgejo/Gitea/Codeberg and SourceHut — each giving
139
+ the permalink layout for that host:
140
+
141
+ ```toml
142
+ [[forge]]
143
+ name = "gitlab"
144
+ hosts = ["gitlab.com"]
145
+ blob = "{repo}/-/blob/{revision}/"
146
+ ```
147
+
148
+ Point `forge_profiles` at your own TOML file to add self-hosted instances;
149
+ entries there are matched first, so they can also override a built-in host.
150
+ SSH and scp-style remotes (`git@host:owner/repo.git`) are understood, and any
151
+ credentials embedded in a remote URL are stripped before it reaches a document.
152
+
153
+ ## Referencing things that aren't local files
154
+
155
+ `ArtifactRef` describes an entity a run consumed or produced. It is either
156
+ *local* (makeprov stats and hashes it) or *external* (makeprov records the IRI
157
+ and never touches the filesystem):
158
+
159
+ ```python
160
+ from makeprov import ArtifactRef, OutPath, rule
161
+
162
+ @rule()
163
+ def train(
164
+ dataset: ArtifactRef = ArtifactRef.external(
165
+ "https://example.org/datasets/train-v17",
166
+ types=("prov:Entity", "schema:Dataset"),
167
+ digest="sha256:...",
168
+ ),
169
+ model: OutPath = OutPath("models/m.pkl"),
170
+ ):
171
+ ...
172
+ ```
173
+
174
+ The external object keeps its own detailed metadata; makeprov only records that
175
+ this run used its stable IRI. External refs take no part in staleness checks,
176
+ since they have no local mtime to compare.
177
+
178
+ ## Installation
179
+
180
+ You can install the module directly from PyPI:
181
+
182
+ ```bash
183
+ pip install makeprov
184
+ ```
185
+
186
+ Optional extras add RDF/TriG export, CLI subcommand support, or the Snakemake bridge:
187
+
188
+ ```bash
189
+ pip install "makeprov[rdf]" # rdflib + pyshacl for RDF/TriG export
190
+ pip install "makeprov[cli]" # defopt, needed for makeprov.main()
191
+ pip install "makeprov[snakemake]" # the makeprov-snakemake bridge
192
+ ```
193
+
194
+ ## Usage
195
+
196
+ Here’s an example of how to use this package in your Python scripts:
197
+
198
+ ```python
199
+ from makeprov import rule, InPath, OutPath, build
200
+
201
+ @rule()
202
+ def process_data(
203
+ sample: int | None = None,
204
+ input_file: InPath = InPath('data/{sample:d}.txt'),
205
+ output_file: OutPath = OutPath('results/{sample:d}.txt')
206
+ ):
207
+ with input_file.open('r') as infile, output_file.open('w') as outfile:
208
+ data = infile.read()
209
+ outfile.write(data.upper())
210
+
211
+ if __name__ == '__main__':
212
+ # Build a specific templated target and its prerequisites
213
+ from makeprov import build
214
+ build('results/1.txt')
215
+
216
+ # Or expose rules via a command line interface
217
+ import defopt
218
+ defopt.run(process_data)
219
+ ```
220
+
221
+ You can execute `examples/example.py` via the CLI like so:
222
+
223
+ ```bash
224
+ python examples/example.py build-all
225
+
226
+ # Or set configuration through the CLI
227
+ python examples/example.py build-all --conf='{"base_iri": "http://mybaseiri.org/", "prov_dir": "my_prov_directory"}' --force --input_file input.txt --output_file final_output.txt
228
+
229
+ # Or set configuration through a TOML file
230
+ python examples/example.py build-all -c @my_config.toml
231
+
232
+ # Inspect dependency resolution without executing rules
233
+ python examples/example.py --explain results/1.txt
234
+ python examples/example.py --to-dot results/1.txt
235
+ ```
236
+
237
+ ### Complex CSV-to-RDF Workflow
238
+
239
+ For a more involved scenario, see [`examples/complex_example.py`](examples/complex_example.py). It creates multiple CSV files, aggregates their contents, and emits an RDF graph that is both serialized to disk and embedded into the provenance dataset because the function returns an `rdflib.Graph`.
240
+
241
+ ```python
242
+ @rule()
243
+ def export_totals_graph(
244
+ totals_csv: InPath = InPath("data/region_totals.csv"),
245
+ graph_ttl: OutPath = OutPath("data/region_totals.ttl"),
246
+ ) -> Graph:
247
+ graph = Graph()
248
+ graph.bind("sales", SALES)
249
+
250
+ with totals_csv.open("r", newline="") as handle:
251
+ for row in csv.DictReader(handle):
252
+ region_key = row["region"].lower().replace(" ", "-")
253
+ subject = SALES[f"region/{region_key}"]
254
+
255
+ graph.add((subject, RDF.type, SALES.RegionTotal))
256
+ graph.add((subject, SALES.regionName, Literal(row["region"])))
257
+ graph.add((subject, SALES.totalUnits, Literal(row["total_units"], datatype=XSD.integer)))
258
+ graph.add((subject, SALES.totalRevenue, Literal(row["total_revenue"], datatype=XSD.decimal)))
259
+
260
+ with graph_ttl.open("w") as handle:
261
+ handle.write(graph.serialize(format="turtle"))
262
+
263
+ return graph
264
+ ```
265
+
266
+ Run the entire workflow, including CSV generation and RDF export, with:
267
+
268
+ ```bash
269
+ python examples/complex_example.py build-sales-report
270
+ ```
271
+
272
+ ### Bundling nested provenance and directory outputs
273
+
274
+ Rules can merge the provenance from any rules they invoke by passing
275
+ ``merge=True`` to `makeprov.rule`. Pair this with
276
+ `makeprov.OutDir` to declare a directory and then materialize multiple
277
+ outputs beneath it while keeping them linked to a single provenance record. Use
278
+ `makeprov.InDir` for the same tracked-directory semantics on inputs. For nested
279
+ structures, call `subdir()` on an `OutDir`/`InDir` to auto-wrap subfolders
280
+ without manually constructing new instances.
281
+ See [`examples/merge_outdir_example.py`](examples/merge_outdir_example.py) for an example.
282
+
283
+ Merging is enabled by default: top-level runs start a provenance buffer and
284
+ flush it once the CLI finishes, so downstream rules end up in one document
285
+ unless you explicitly turn buffering off with `merge=False` on a rule or in the
286
+ global config. Nested merges append to their parent buffer rather than writing
287
+ multiple files.
288
+
289
+ ### Configured context and isolated sessions
290
+
291
+ `examples/context_demo_example.py` demonstrates pinning a base IRI, writing
292
+ provenance to a dedicated directory, and running rules inside an isolated
293
+ session so registries and buffers do not leak across runs:
294
+
295
+ ```bash
296
+ python examples/context_demo_example.py build-all
297
+ ```
298
+
299
+ ### Snakemake workflows
300
+
301
+ Install the `snakemake` extra (`pip install "makeprov[snakemake]"`) to get the
302
+ `makeprov-snakemake` command, which shells out to Snakemake and converts the
303
+ job DAG together with ``--detailed-summary`` metadata into a PROV document.
304
+ It mirrors the familiar configuration flags from `makeprov.config` and writes
305
+ JSON-LD by default. Note this is a best-effort bridge: it parses Snakemake's
306
+ human-oriented text output, so treat it as a convenience for reports rather
307
+ than an authoritative source of truth — it will raise rather than guess when
308
+ it can't unambiguously parse a filename (e.g. one containing whitespace).
309
+
310
+ ```bash
311
+ makeprov-snakemake --prov-path prov/snakemake -- --snakefile Snakefile --nolock
312
+ ```
313
+
314
+ Wire the resulting file into a report by marking it with Snakemake’s
315
+ `report()` helper:
316
+
317
+ ```python
318
+ rule provenance:
319
+ input:
320
+ "results/word_count.txt"
321
+ output:
322
+ "prov/snakemake.json"
323
+ shell:
324
+ (
325
+ "makeprov-snakemake "
326
+ "--prov-path prov/snakemake "
327
+ "--out-fmt json --context --frame provenance "
328
+ "-- "
329
+ "--snakefile {workflow.snakefile} --nolock {input}"
330
+ )
331
+ ```
332
+
333
+ Using the optional `--forceall-dag` flag ensures that the job-level dependency
334
+ edges in the provenance graph remain complete even when Snakemake skips nodes
335
+ that are already up to date.
336
+
337
+ ### Configuration
338
+
339
+ You can customize the provenance tracking with the following options:
340
+
341
+ - `base_iri` (str): Base IRI for new resources
342
+ - `prov_dir` (str): Directory for writing PROV `.json-ld` or `.trig` files
343
+ - `force` (bool): Force running of dependencies
344
+ - `dry_run` (bool): Only check workflow, don't run anything
345
+ - `strict` (bool, default `True`): Raise `makeprov.ProvenanceWriteError` if a
346
+ rule's provenance record fails to write, instead of only logging a
347
+ warning. A rule that produces a result but no provenance record is
348
+ treated as a failure by default; set `strict=False` to opt out per-rule
349
+ or globally.
350
+ - `run_id` (str | None): Adopt an externally supplied run identity, such as a
351
+ CI job id. When unset, each run gets a fresh unique id.
352
+ - `record_user` (bool, default `False`): Record the invoking user, taken from
353
+ `git config user.name`/`user.email`, as a `schema:Person` agent. Off by
354
+ default so personal data isn't published by accident.
355
+ CLI: `--record-user`.
356
+ - `emit_plan_graph` (bool, default `False`): Also emit prospective structure —
357
+ the rule dependency graph as `prov:Plan` nodes linked by `dct:requires`. Off
358
+ by default, so a document describes only what actually ran. CLI:
359
+ `--plan-graph`.
360
+ - `forge_profiles` (str | None): TOML file of extra forge profiles, for
361
+ self-hosted git hosts. CLI: `--forge-profiles`.
362
+
363
+ ### Upgrading to 0.7
364
+
365
+ 0.7 changes the provenance model. The decorator API is unchanged — existing
366
+ `@rule` functions using `InPath`/`OutPath` keep working — but the emitted
367
+ graph differs:
368
+
369
+ - The script is no longer a `prov:SoftwareAgent`. It is a `prov:Plan`, reached
370
+ from the activity via `prov:qualifiedAssociation`/`prov:hadPlan`. Consumers
371
+ that looked for `prov:wasAssociatedWith` to find the script should follow
372
+ `prov:hadPlan` instead.
373
+ - Outputs now carry a `sha256` content digest. Previously the digest was
374
+ computed and then discarded for outputs.
375
+ - Entity IRIs derived from a GitHub remote are pinned to the **commit** rather
376
+ than the branch, so an IRI no longer denotes different bytes after each push.
377
+ - Run identifiers include seconds and a random suffix. Minute-resolution ids
378
+ meant two runs of the same rule in one minute shared an activity IRI.
379
+ - A declared output that is missing after a successful run now raises
380
+ `UnresolvedArtifactError` instead of being dropped from the graph. Missing
381
+ *inputs* are recorded without content metadata and logged, rather than
382
+ disappearing.
383
+ - `Prov.create()` takes `list[ArtifactRef]` instead of `list[Path]`.
384
+ - The Snakemake bridge follows the same model: each rule is now a `prov:Plan`
385
+ at `<base>rule/<name>`, and each job activity carries a
386
+ `prov:qualifiedAssociation`. Its agent node gained `schema:SoftwareApplication`.
387
+ - The bridge no longer defaults to a `urn:snakemake:` namespace. That NID was
388
+ never IANA-registered, so it named no real namespace and collided across
389
+ unrelated workflows sharing rule names. It now shares the decorator API's
390
+ identifier policy (`makeprov.prov.resolve_iris`): an explicit `base_iri`,
391
+ else a commit-pinned base derived from a GitHub remote, else relative IRIs.
392
+ - `blob:` identifiers are only minted for files inside the repository. An
393
+ absolute path within the checkout is rewritten to its repo-relative form, and
394
+ a path outside it gets a `file:` URI instead of a `blob:` IRI that would
395
+ expand to a nonexistent location.
396
+ - Entities carry `schema:sha256` (bare hex) alongside the algorithm-qualified
397
+ `dct:identifier`, so consumers no longer have to parse a prefix.
398
+ - Activities state `prov:generated` as well as each entity's inverse
399
+ `prov:wasGeneratedBy`, mirroring `prov:used` and mapping onto RO-Crate's
400
+ `result`.
401
+ - A dirty working tree is recorded as `<sha>-dirty` (the `git describe --dirty`
402
+ convention) with a warning, instead of asserting a clean revision that does
403
+ not describe what ran.
404
+ - `prov:wasDerivedFrom` and `rdfs:seeAlso` on cached downloads are emitted as
405
+ node references rather than string literals, so the links are traversable.
406
+ `CachedDownload(..., sha256=...)` pins and verifies the cached copy.
407
+ - The base heuristic covers all hosts in `forges.toml`, not just GitHub, and
408
+ understands SSH remotes. Credentials embedded in a remote URL are stripped —
409
+ previously a remote like `https://user:token@github.com/o/r.git` would have
410
+ put the token into `@base` in every document.
411
+
412
+ ### Scoped spans and cached downloads
413
+
414
+ Use `makeprov.span(label, prov_path=None, frame=None, context=None)` as a
415
+ context manager or decorator to bracket a chunk of work in its own provenance
416
+ buffer. A span returns the merged `Prov` via `span.prov`, so nested spans can
417
+ emit labeled artifacts without manual slicing/merging:
418
+
419
+ ```python
420
+ from makeprov import span
421
+
422
+ with span("model-run", prov_path="prov/models/model1"):
423
+ run_model()
424
+ ```
425
+
426
+ For remote resources that are cached locally, wrap the path with
427
+ `CachedDownload`. It will lazily fetch on first access and record the source
428
+ URL (and optional headers) in the provenance:
429
+
430
+ ```python
431
+ from makeprov import CachedDownload, rule
432
+
433
+ @rule()
434
+ def fetch_data(meta_json=CachedDownload("https://example.org/meta.json", "cache/meta.json")):
435
+ with meta_json.open() as handle:
436
+ return handle.read()
437
+ ```
438
+
439
+ ## Documentation
440
+
441
+ Build the Sphinx docs (including autosummary API stubs) with the docs extra so
442
+ that the CLI dependencies needed for imports are available:
443
+
444
+ ```bash
445
+ pip install -e ".[docs]"
446
+ python docs/build.py
447
+ ```
448
+
449
+ ## Contributing
450
+
451
+ Contributions are welcome! Please open an issue or submit a pull request.
452
+
453
+ ## License
454
+
455
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.