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