data-annotations 2.8.1__tar.gz → 2.9.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 (37) hide show
  1. {data_annotations-2.8.1 → data_annotations-2.9.0}/PKG-INFO +12 -6
  2. {data_annotations-2.8.1 → data_annotations-2.9.0}/README.md +11 -5
  3. {data_annotations-2.8.1 → data_annotations-2.9.0}/pyproject.toml +1 -1
  4. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/annotations/answers.py +29 -21
  5. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/annotate/__init__.py +69 -13
  6. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/annotate/helpers.py +48 -12
  7. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/common.py +7 -0
  8. {data_annotations-2.8.1 → data_annotations-2.9.0}/LICENSE +0 -0
  9. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/__init__.py +0 -0
  10. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/_decorators.py +0 -0
  11. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/annotations/__init__.py +0 -0
  12. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/annotations/decorators.py +0 -0
  13. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/annotations/models.py +0 -0
  14. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/annotations/writers.py +0 -0
  15. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli.py +0 -0
  16. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/__init__.py +0 -0
  17. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/answers.py +0 -0
  18. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/prompts.py +0 -0
  19. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/provenance_commands.py +0 -0
  20. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/cli_app/publish.py +0 -0
  21. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/description/__init__.py +0 -0
  22. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/description/decorators.py +0 -0
  23. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/description/models.py +0 -0
  24. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/description/writers.py +0 -0
  25. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/__init__.py +0 -0
  26. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/decorators.py +0 -0
  27. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/git.py +0 -0
  28. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/models.py +0 -0
  29. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/recovery/__init__.py +0 -0
  30. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/recovery/chain.py +0 -0
  31. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/recovery/manifest.py +0 -0
  32. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/recovery/matching.py +0 -0
  33. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/recovery/sources.py +0 -0
  34. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/recovery/types.py +0 -0
  35. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/runtime.py +0 -0
  36. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/provenance/writers.py +0 -0
  37. {data_annotations-2.8.1 → data_annotations-2.9.0}/src/data_annotations/publish.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-annotations
3
- Version: 2.8.1
3
+ Version: 2.9.0
4
4
  Summary: Annotate data artifacts with provenance and descriptions
5
5
  Keywords: annotations,data,metadata,provenance,reproducibility
6
6
  Author: Rodrigo C. G. Pena
@@ -30,6 +30,11 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  # data-annotations
32
32
 
33
+ [![PyPI](https://img.shields.io/pypi/v/data-annotations?label=pypi)](https://pypi.org/project/data-annotations/)
34
+ [![Documentation](https://img.shields.io/badge/docs-latest-blue)](https://ceda-unibas.gitlab.io/tools/data-annotations/)
35
+ [![License](https://img.shields.io/pypi/l/data-annotations?label=license)](https://gitlab.com/ceda-unibas/tools/data-annotations/-/blob/main/LICENSE)
36
+ [![CI](https://gitlab.com/ceda-unibas/tools/data-annotations/badges/main/pipeline.svg)](https://gitlab.com/ceda-unibas/tools/data-annotations/-/pipelines)
37
+
33
38
  `data-annotations` is a Python package for attaching provenance and structured
34
39
  descriptions to the files and directories your workflows produce.
35
40
 
@@ -43,12 +48,13 @@ Optional Markdown README sidecars can be generated for human-readable summaries.
43
48
 
44
49
  ## Documentation
45
50
 
46
- The full documentation is organized as a [Diátaxis](https://diataxis.fr/) site:
47
- https://ceda-unibas.gitlab.io/tools/data-annotations/
51
+ The [full documentation](https://ceda-unibas.gitlab.io/tools/data-annotations/) is organized as a [Diátaxis](https://diataxis.fr/) site.
52
+
53
+ Other links:
48
54
 
49
- - Source: https://gitlab.com/ceda-unibas/tools/data-annotations
50
- - Changelog: https://gitlab.com/ceda-unibas/tools/data-annotations/-/blob/main/CHANGELOG.md
51
- - Work items: https://gitlab.com/ceda-unibas/tools/data-annotations/-/work_items
55
+ - [Source code](https://gitlab.com/ceda-unibas/tools/data-annotations)
56
+ - [Changelog](https://gitlab.com/ceda-unibas/tools/data-annotations/-/blob/main/CHANGELOG.md)
57
+ - [Work items](https://gitlab.com/ceda-unibas/tools/data-annotations/-/work_items)
52
58
 
53
59
  ## Installation
54
60
 
@@ -1,5 +1,10 @@
1
1
  # data-annotations
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/data-annotations?label=pypi)](https://pypi.org/project/data-annotations/)
4
+ [![Documentation](https://img.shields.io/badge/docs-latest-blue)](https://ceda-unibas.gitlab.io/tools/data-annotations/)
5
+ [![License](https://img.shields.io/pypi/l/data-annotations?label=license)](https://gitlab.com/ceda-unibas/tools/data-annotations/-/blob/main/LICENSE)
6
+ [![CI](https://gitlab.com/ceda-unibas/tools/data-annotations/badges/main/pipeline.svg)](https://gitlab.com/ceda-unibas/tools/data-annotations/-/pipelines)
7
+
3
8
  `data-annotations` is a Python package for attaching provenance and structured
4
9
  descriptions to the files and directories your workflows produce.
5
10
 
@@ -13,12 +18,13 @@ Optional Markdown README sidecars can be generated for human-readable summaries.
13
18
 
14
19
  ## Documentation
15
20
 
16
- The full documentation is organized as a [Diátaxis](https://diataxis.fr/) site:
17
- https://ceda-unibas.gitlab.io/tools/data-annotations/
21
+ The [full documentation](https://ceda-unibas.gitlab.io/tools/data-annotations/) is organized as a [Diátaxis](https://diataxis.fr/) site.
22
+
23
+ Other links:
18
24
 
19
- - Source: https://gitlab.com/ceda-unibas/tools/data-annotations
20
- - Changelog: https://gitlab.com/ceda-unibas/tools/data-annotations/-/blob/main/CHANGELOG.md
21
- - Work items: https://gitlab.com/ceda-unibas/tools/data-annotations/-/work_items
25
+ - [Source code](https://gitlab.com/ceda-unibas/tools/data-annotations)
26
+ - [Changelog](https://gitlab.com/ceda-unibas/tools/data-annotations/-/blob/main/CHANGELOG.md)
27
+ - [Work items](https://gitlab.com/ceda-unibas/tools/data-annotations/-/work_items)
22
28
 
23
29
  ## Installation
24
30
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "data-annotations"
3
- version = "2.8.1"
3
+ version = "2.9.0"
4
4
  description = "Annotate data artifacts with provenance and descriptions"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import re
3
3
  import shlex
4
- from collections.abc import Mapping
4
+ from collections.abc import Iterable, Mapping
5
5
  from pathlib import Path
6
6
  from typing import Any, Literal, TypeAlias
7
7
 
@@ -124,6 +124,32 @@ _EXPLICIT_PROVENANCE_OVERRIDE_FIELDS = {
124
124
  }
125
125
 
126
126
 
127
+ def _validate_runtime_inference_fields(values: Iterable[str]) -> list[str]:
128
+ normalized: list[str] = []
129
+ for value in values:
130
+ if value in _UNSUPPORTED_RUNTIME_INFERENCE_FIELDS:
131
+ raise ValueError(
132
+ "runtime inference is not supported for "
133
+ f"provenance.{value}; provide it explicitly"
134
+ )
135
+ if value not in _RUNTIME_INFERENCE_FIELDS:
136
+ allowed = sorted(_RUNTIME_INFERENCE_FIELDS)
137
+ raise ValueError(
138
+ f"unknown runtime inference field {value!r}; "
139
+ "expected one of: " + ", ".join(allowed)
140
+ )
141
+ if value not in normalized:
142
+ normalized.append(value)
143
+ return normalized
144
+
145
+
146
+ def _expand_runtime_inference_fields(values: Iterable[str]) -> set[str]:
147
+ fields: set[str] = set()
148
+ for field in values:
149
+ fields.update(_RUNTIME_INFERENCE_GROUPS.get(field, {field}))
150
+ return fields
151
+
152
+
127
153
  class ProvenanceAnswers(BaseModel):
128
154
  """Optional provenance overrides supplied through an answers payload."""
129
155
 
@@ -194,22 +220,7 @@ class ProvenanceAnswers(BaseModel):
194
220
  @field_validator("infer_from_runtime")
195
221
  @classmethod
196
222
  def _validate_runtime_inference_fields(cls, values: list[str]) -> list[str]:
197
- normalized: list[str] = []
198
- for value in values:
199
- if value in _UNSUPPORTED_RUNTIME_INFERENCE_FIELDS:
200
- raise ValueError(
201
- "runtime inference is not supported for "
202
- f"provenance.{value}; provide it explicitly"
203
- )
204
- if value not in _RUNTIME_INFERENCE_FIELDS:
205
- allowed = sorted(_RUNTIME_INFERENCE_FIELDS)
206
- raise ValueError(
207
- f"unknown runtime inference field {value!r}; "
208
- "expected one of: " + ", ".join(allowed)
209
- )
210
- if value not in normalized:
211
- normalized.append(value)
212
- return normalized
223
+ return _validate_runtime_inference_fields(values)
213
224
 
214
225
  @model_validator(mode="after")
215
226
  def _validate_runtime_inference_conflicts(self) -> "ProvenanceAnswers":
@@ -240,10 +251,7 @@ class ProvenanceAnswers(BaseModel):
240
251
  def runtime_inference_fields(self) -> set[str]:
241
252
  """Expand runtime inference groups into concrete provenance fields."""
242
253
 
243
- fields: set[str] = set()
244
- for field in self.infer_from_runtime:
245
- fields.update(_RUNTIME_INFERENCE_GROUPS.get(field, {field}))
246
- return fields
254
+ return _expand_runtime_inference_fields(self.infer_from_runtime)
247
255
 
248
256
 
249
257
  class BaseAnswers(BaseModel):
@@ -11,6 +11,7 @@ from .helpers import (
11
11
  _child_bundle_from_annotation,
12
12
  _child_bundles_from_answers,
13
13
  _collect_post_hoc_provenance_from_sources,
14
+ _documented_artifacts_from_discovered_files,
14
15
  _documented_artifact_groups_from_answers,
15
16
  _documented_artifacts_from_answers,
16
17
  _load_directory_answers,
@@ -38,6 +39,7 @@ from ..common import (
38
39
  InputValuesOption,
39
40
  MaxChecksumBytesOption,
40
41
  ParamValuesOption,
42
+ RuntimeInferenceValuesOption,
41
43
  ScriptOption,
42
44
  ScriptRepoPathOption,
43
45
  Sha256Option,
@@ -125,6 +127,7 @@ def annotate_file_command(
125
127
  source_path: SourcePathOption = None,
126
128
  source_revision: SourceRevisionOption = None,
127
129
  source_sha256: SourceSha256Option = None,
130
+ infer_from_runtime: RuntimeInferenceValuesOption = None,
128
131
  checksum_policy: ChecksumPolicyOption = "auto",
129
132
  max_checksum_bytes: MaxChecksumBytesOption = 10 * 1024**3,
130
133
  sha256: Sha256Option = None,
@@ -146,8 +149,6 @@ def annotate_file_command(
146
149
  file_answers.target if file_answers is not None else None,
147
150
  expected_kind="file",
148
151
  )
149
- if not is_interactive and file_answers is None:
150
- _error("--no-interactive requires --answers")
151
152
  if not is_interactive and file_answers is not None:
152
153
  try:
153
154
  answer_files.require_complete_file_answers(
@@ -215,6 +216,7 @@ def annotate_file_command(
215
216
  source_path=source_path,
216
217
  source_revision=source_revision,
217
218
  source_sha256=source_sha256,
219
+ infer_from_runtime=infer_from_runtime,
218
220
  )
219
221
 
220
222
  fields: list[FieldDefinition] = list(file_answers.fields) if file_answers else []
@@ -295,6 +297,7 @@ def annotate_directory_command(
295
297
  source_path: SourcePathOption = None,
296
298
  source_revision: SourceRevisionOption = None,
297
299
  source_sha256: SourceSha256Option = None,
300
+ infer_from_runtime: RuntimeInferenceValuesOption = None,
298
301
  recursive: bool = typer.Option(
299
302
  False,
300
303
  "--recursive",
@@ -326,6 +329,14 @@ def annotate_directory_command(
326
329
  "--group-kind",
327
330
  help="Artifact kind for the corresponding --group-selector.",
328
331
  ),
332
+ include_discovered: bool = typer.Option(
333
+ False,
334
+ "--include-discovered",
335
+ help=(
336
+ "Include discovered files and child bundles non-interactively using "
337
+ "sparse descriptions."
338
+ ),
339
+ ),
329
340
  checksum_policy: ChecksumPolicyOption = "auto",
330
341
  max_checksum_bytes: MaxChecksumBytesOption = 10 * 1024**3,
331
342
  checksum_values: ChecksumValuesOption = None,
@@ -346,17 +357,6 @@ def annotate_directory_command(
346
357
  directory_answers.target if directory_answers is not None else None,
347
358
  expected_kind="directory",
348
359
  )
349
- if not is_interactive and directory_answers is None:
350
- _error("--no-interactive requires --answers")
351
- if not is_interactive and directory_answers is not None:
352
- try:
353
- answer_files.require_complete_directory_answers(
354
- directory_answers,
355
- target=output_dir,
356
- )
357
- except answer_files.AnswersError as exc:
358
- _error(str(exc))
359
-
360
360
  discovered_files, discovered_child_bundle_annotations = _discover_directory_entries(
361
361
  output_dir,
362
362
  recursive=recursive,
@@ -382,6 +382,21 @@ def annotate_directory_command(
382
382
  or directory_answers.child_bundles
383
383
  )
384
384
  )
385
+ has_cli_inventory = include_discovered or bool(artifact_groups)
386
+ if not is_interactive:
387
+ if directory_answers is not None and not has_cli_inventory:
388
+ try:
389
+ answer_files.require_complete_directory_answers(
390
+ directory_answers,
391
+ target=output_dir,
392
+ )
393
+ except answer_files.AnswersError as exc:
394
+ _error(str(exc))
395
+ elif not has_answers_inventory and not has_cli_inventory:
396
+ _error(
397
+ "--no-interactive directory annotation requires "
398
+ "--include-discovered, --group-selector, or --answers inventory"
399
+ )
385
400
  if (
386
401
  not has_answers_inventory
387
402
  and not discovered_files
@@ -442,6 +457,7 @@ def annotate_directory_command(
442
457
  source_path=source_path,
443
458
  source_revision=source_revision,
444
459
  source_sha256=source_sha256,
460
+ infer_from_runtime=infer_from_runtime,
445
461
  )
446
462
 
447
463
  default_kind = _validate_artifact_kind(kind) if kind is not None else None
@@ -460,6 +476,46 @@ def annotate_directory_command(
460
476
  output_dir,
461
477
  directory_answers.child_bundles,
462
478
  )
479
+ if include_discovered:
480
+ excluded_relative_paths = {artifact.path for artifact in artifacts} | {
481
+ relative_path
482
+ for group in artifact_groups
483
+ for relative_path in group.paths
484
+ }
485
+ artifacts.extend(
486
+ _documented_artifacts_from_discovered_files(
487
+ output_dir,
488
+ discovered_files=discovered_files,
489
+ artifact_kind=default_kind or "other",
490
+ excluded_relative_paths=excluded_relative_paths,
491
+ )
492
+ )
493
+ known_child_annotation_paths = {
494
+ child_bundle.annotation_path for child_bundle in child_bundles
495
+ }
496
+ child_bundles.extend(
497
+ _child_bundle_from_annotation(output_dir, annotation_path)
498
+ for annotation_path in discovered_child_bundle_annotations
499
+ if annotation_path.relative_to(output_dir).as_posix()
500
+ not in known_child_annotation_paths
501
+ )
502
+ elif include_discovered:
503
+ grouped_relative_paths = {
504
+ relative_path for group in artifact_groups for relative_path in group.paths
505
+ }
506
+ artifacts = _documented_artifacts_from_discovered_files(
507
+ output_dir,
508
+ discovered_files=discovered_files,
509
+ artifact_kind=default_kind or "other",
510
+ excluded_relative_paths=grouped_relative_paths,
511
+ )
512
+ child_bundles = [
513
+ _child_bundle_from_annotation(output_dir, annotation_path)
514
+ for annotation_path in discovered_child_bundle_annotations
515
+ ]
516
+ elif not is_interactive:
517
+ artifacts = []
518
+ child_bundles = []
463
519
  else:
464
520
  artifacts = []
465
521
  grouped_relative_paths = {
@@ -3,6 +3,10 @@ from collections.abc import Mapping
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
+ from data_annotations.annotations.answers import (
7
+ _expand_runtime_inference_fields,
8
+ _validate_runtime_inference_fields,
9
+ )
6
10
  from data_annotations.annotations import (
7
11
  write_directory_annotation,
8
12
  write_file_annotation,
@@ -268,6 +272,7 @@ def _selected_git_tags(
268
272
  def _selected_source_code(
269
273
  *,
270
274
  answers: answer_files.BaseAnswers | None,
275
+ inferred_fields: set[str],
271
276
  source_kind: str | None,
272
277
  source_uri: str | None,
273
278
  source_download_uri: str | None,
@@ -284,10 +289,7 @@ def _selected_source_code(
284
289
  source_sha256,
285
290
  ]
286
291
  if not any(value is not None for value in source_values):
287
- if (
288
- answers is not None
289
- and "source_code" in answers.provenance.runtime_inference_fields()
290
- ):
292
+ if "source_code" in inferred_fields:
291
293
  return None
292
294
  return answers.provenance.source_code if answers is not None else None
293
295
 
@@ -308,19 +310,27 @@ def _selected_source_code(
308
310
  )
309
311
 
310
312
 
311
- def _runtime_inferred_fields(answers: answer_files.BaseAnswers | None) -> set[str]:
312
- if answers is None:
313
- return set()
314
- return answers.provenance.runtime_inference_fields()
313
+ def _runtime_inferred_fields(
314
+ answers: answer_files.BaseAnswers | None,
315
+ infer_from_runtime: list[str] | None,
316
+ ) -> set[str]:
317
+ selected_fields: list[str] = []
318
+ if answers is not None:
319
+ selected_fields.extend(answers.provenance.infer_from_runtime)
320
+ selected_fields.extend(infer_from_runtime or [])
321
+ try:
322
+ normalized = _validate_runtime_inference_fields(selected_fields)
323
+ except ValueError as exc:
324
+ _error(str(exc))
325
+ return _expand_runtime_inference_fields(normalized)
315
326
 
316
327
 
317
328
  def _filter_runtime_inferred_overrides(
318
329
  overrides: dict[str, Any],
319
330
  *,
320
- answers: answer_files.BaseAnswers | None,
331
+ inferred_fields: set[str],
321
332
  explicit_fields: set[str],
322
333
  ) -> dict[str, Any]:
323
- inferred_fields = _runtime_inferred_fields(answers)
324
334
  if not inferred_fields:
325
335
  return overrides
326
336
  if "source_code" in inferred_fields:
@@ -409,10 +419,12 @@ def _collect_post_hoc_provenance_from_sources(
409
419
  source_path: str | None,
410
420
  source_revision: str | None,
411
421
  source_sha256: str | None,
422
+ infer_from_runtime: list[str] | None,
412
423
  ) -> tuple[list[str], dict[str, Any], dict[str, Any]]:
413
424
  selected_inputs = _selected_inputs(input_values, answers)
414
425
  selected_params = _selected_params(param_values, answers)
415
426
  answer_command_tokens = _command_tokens_from_answers(answers)
427
+ inferred_fields = _runtime_inferred_fields(answers, infer_from_runtime)
416
428
  source_code_cli_values = {
417
429
  source_kind,
418
430
  source_uri,
@@ -451,6 +463,7 @@ def _collect_post_hoc_provenance_from_sources(
451
463
  selected_git_tags = _selected_git_tags(git_tags, answers)
452
464
  selected_source_code = _selected_source_code(
453
465
  answers=answers,
466
+ inferred_fields=inferred_fields,
454
467
  source_kind=source_kind,
455
468
  source_uri=source_uri,
456
469
  source_download_uri=source_download_uri,
@@ -505,7 +518,7 @@ def _collect_post_hoc_provenance_from_sources(
505
518
  params,
506
519
  _filter_runtime_inferred_overrides(
507
520
  overrides,
508
- answers=answers,
521
+ inferred_fields=inferred_fields,
509
522
  explicit_fields=explicit_override_fields,
510
523
  ),
511
524
  )
@@ -553,12 +566,35 @@ def _collect_post_hoc_provenance_from_sources(
553
566
  selected_params or {},
554
567
  _filter_runtime_inferred_overrides(
555
568
  overrides,
556
- answers=answers,
569
+ inferred_fields=inferred_fields,
557
570
  explicit_fields=explicit_override_fields,
558
571
  ),
559
572
  )
560
573
 
561
574
 
575
+ def _documented_artifacts_from_discovered_files(
576
+ output_dir: Path,
577
+ *,
578
+ discovered_files: list[Path],
579
+ artifact_kind: ArtifactKind,
580
+ excluded_relative_paths: set[str],
581
+ ) -> list[DocumentedArtifact]:
582
+ artifacts: list[DocumentedArtifact] = []
583
+ for discovered_file in discovered_files:
584
+ relative_path = discovered_file.relative_to(output_dir).as_posix()
585
+ if relative_path in excluded_relative_paths:
586
+ continue
587
+ artifacts.append(
588
+ DocumentedArtifact(
589
+ path=relative_path,
590
+ kind=artifact_kind,
591
+ title=relative_path,
592
+ summary=None,
593
+ )
594
+ )
595
+ return artifacts
596
+
597
+
562
598
  def _documented_artifacts_from_answers(
563
599
  artifacts: list[answer_files.DirectoryArtifactAnswers],
564
600
  ) -> list[DocumentedArtifact]:
@@ -109,6 +109,12 @@ SOURCE_SHA256_OPTION = typer.Option(
109
109
  "--source-sha256",
110
110
  help="SHA-256 checksum for downloaded source archives or files.",
111
111
  )
112
+ RUNTIME_INFERENCE_OPTION = typer.Option(
113
+ "--infer-from-runtime",
114
+ help=(
115
+ "Runtime provenance field or group to infer instead of overriding. Repeatable."
116
+ ),
117
+ )
112
118
  CHECKSUM_POLICY_OPTION = typer.Option(
113
119
  "--checksum-policy",
114
120
  help="Checksum behavior for local files: always, auto, or never.",
@@ -150,6 +156,7 @@ SourceDownloadUriOption = Annotated[str | None, SOURCE_DOWNLOAD_URI_OPTION]
150
156
  SourcePathOption = Annotated[str | None, SOURCE_PATH_OPTION]
151
157
  SourceRevisionOption = Annotated[str | None, SOURCE_REVISION_OPTION]
152
158
  SourceSha256Option = Annotated[str | None, SOURCE_SHA256_OPTION]
159
+ RuntimeInferenceValuesOption = Annotated[list[str] | None, RUNTIME_INFERENCE_OPTION]
153
160
  ChecksumPolicyOption = Annotated[str, CHECKSUM_POLICY_OPTION]
154
161
  MaxChecksumBytesOption = Annotated[int, MAX_CHECKSUM_BYTES_OPTION]
155
162
  Sha256Option = Annotated[str | None, SHA256_OPTION]