Skip to content

API reference

Auto-generated from docstrings -- full signatures, parameter types, and defaults for the same Python API surface that page introduces narratively. Start there for how to use it; come here for the exact call signature.

Generator functions

pitloom.assemble.generate

generate(
    target: Path | str = ".",
    *,
    offline: bool | None = None,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
    extract_file_header: bool | None = None,
    content_type: bool | None = None,
    content_type_method: str | None = None,
    update_registry: bool | None = None,
) -> str

Smart unified entrypoint for generating SPDX 3 SBOMs across all target types.

Source code in pitloom/assemble/__init__.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def generate(
    target: Path | str = ".",
    *,
    offline: bool | None = None,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
    extract_file_header: bool | None = None,
    content_type: bool | None = None,
    content_type_method: str | None = None,
    update_registry: bool | None = None,
) -> str:
    """Smart unified entrypoint for generating SPDX 3 SBOMs across all target types."""
    target_str = str(target).strip()

    if target_str.lower() in ("env", "environment", "--env"):
        return generate_env_sbom(
            output_path=output_path,
            creation_metadata=creation_metadata,
            pretty=pretty,
            describe_relationship=describe_relationship,
            registry=registry,
            provenance=provenance,
            offline=offline,
            update_registry=update_registry,
        )

    if target_str.lower().endswith(".whl"):
        return generate_wheel_sbom(
            target_str,
            output_path=output_path,
            creation_metadata=creation_metadata,
            pretty=pretty,
            describe_relationship=describe_relationship,
            registry=registry,
            provenance=provenance,
            offline=offline,
            update_registry=update_registry,
        )

    if is_huggingface_source(target_str):
        return generate_model_sbom(
            target_str,
            offline=offline,
            output_path=output_path,
            creation_metadata=creation_metadata,
            pretty=pretty,
            describe_relationship=describe_relationship,
            registry=registry,
            provenance=provenance,
            enrich=enrich,
        )

    target_path = Path(target)
    if target_path.is_file():
        name_lower = target_path.name.lower()
        if any(
            name_lower.endswith(ext)
            for ext in (
                ".gguf",
                ".safetensors",
                ".onnx",
                ".pt",
                ".pth",
                ".h5",
                ".hdf5",
                ".keras",
                ".npy",
                ".npz",
                ".bin",
                ".ftz",
            )
        ):
            return generate_model_sbom(
                target_path,
                offline=offline,
                output_path=output_path,
                creation_metadata=creation_metadata,
                pretty=pretty,
                describe_relationship=describe_relationship,
                registry=registry,
                provenance=provenance,
                enrich=enrich,
            )

    return generate_project_sbom(
        target_path,
        output_path=output_path,
        creation_metadata=creation_metadata,
        pretty=pretty,
        describe_relationship=describe_relationship,
        registry=registry,
        provenance=provenance,
        enrich=enrich,
        extract_file_header=extract_file_header,
        content_type=content_type,
        content_type_method=content_type_method,
        offline=offline,
        update_registry=update_registry,
    )

pitloom.assemble.generate_project_sbom

generate_project_sbom(
    project_target: Path | str,
    *,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    project_metadata: ProjectMetadata | None = None,
    pitloom_config: PitloomConfig | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
    extract_file_header: bool | None = None,
    content_type: bool | None = None,
    content_type_method: str | None = None,
    offline: bool | None = None,
    update_registry: bool | None = None,
) -> str

Generate a Source SPDX 3 SBOM for a Python project or sdist archive.

Source code in pitloom/assemble/_generators.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def generate_project_sbom(
    project_target: Path | str,
    *,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    project_metadata: ProjectMetadata | None = None,
    pitloom_config: PitloomConfig | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
    extract_file_header: bool | None = None,
    content_type: bool | None = None,
    content_type_method: str | None = None,
    offline: bool | None = None,
    update_registry: bool | None = None,
) -> str:
    """Generate a Source SPDX 3 SBOM for a Python project or sdist archive."""
    target_path = Path(project_target)
    if project_metadata is None or pitloom_config is None:
        project_metadata, pitloom_config, _ = read_project(target_path)

    effective_pretty: bool = pitloom_config.pretty if pretty is None else pretty
    effective_describe: bool = bool(
        pitloom_config.describe_relationship
        if describe_relationship is None
        else describe_relationship
    )
    effective_provenance: ProvenanceConfig = provenance or pitloom_config.provenance
    effective_enrich_config: EnrichConfig = (
        dataclasses.replace(pitloom_config.enrich, local=enrich)
        if enrich is not None
        else pitloom_config.enrich
    )
    effective_extract_file_header: bool = (
        pitloom_config.extract_file_header
        if extract_file_header is None
        else extract_file_header
    )
    effective_content_type: bool = (
        pitloom_config.content_type.enabled if content_type is None else content_type
    )
    effective_content_type_method: str = (
        pitloom_config.content_type.method
        if content_type_method is None
        else content_type_method
    )
    _require_valid_content_type_method(effective_content_type_method)
    effective_offline: bool = pitloom_config.offline if offline is None else offline
    effective_update_registry: bool = (
        pitloom_config.update_registry if update_registry is None else update_registry
    )

    if target_path.is_file():
        merkle_root = None
        project_files = project_metadata.files
        search_root = target_path.parent
    else:
        merkle_root, project_files = get_wheel_files(
            target_path,
            scan_file_headers=effective_extract_file_header,
            detect_content_type=effective_content_type,
            content_type_method=effective_content_type_method,
            content_type_overrides=pitloom_config.content_type.overrides,
        )
        project_metadata.files = project_files
        search_root = target_path

    ai_models = (
        scan_project_for_ai_models(target_path, project_files)
        if target_path.is_dir()
        else []
    )

    enrichment_results_by_model = run_enrichers_for_models(
        ai_models, effective_enrich_config, target_path
    )

    resolved_registry = resolve_registry(
        search_root, registry if registry is not None else pitloom_config.ids_file
    )

    doc = DocumentModel(
        project=project_metadata,
        creation_metadata=creation_metadata or CreationMetadata(),
        ai_models=ai_models,
    )
    exporter = build(
        doc,
        merkle_root=merkle_root,
        sbom_type=spdx3_bindings.software_SbomType.source,
        registry=resolved_registry,
        provenance=effective_provenance,
        enrichment_results_by_model=enrichment_results_by_model,
        offline=effective_offline,
        content_type_method=effective_content_type_method,
    )

    if target_path.is_dir():
        merge_fragments(target_path, pitloom_config.fragments, exporter)

    _sync_registry(exporter, resolved_registry, effective_update_registry)

    sbom_json = exporter.to_json(
        pretty=effective_pretty,
        describe_relationship=effective_describe,
    )

    _write_output_file(sbom_json, output_path)

    return sbom_json

pitloom.assemble.generate_wheel_sbom

generate_wheel_sbom(
    wheel_path: Path | str,
    *,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    offline: bool | None = None,
    update_registry: bool | None = None,
) -> str

Generate an Analyzed SPDX 3 SBOM for a built Python wheel.

Source code in pitloom/assemble/_generators.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def generate_wheel_sbom(
    wheel_path: Path | str,
    *,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    offline: bool | None = None,
    update_registry: bool | None = None,
) -> str:
    """Generate an Analyzed SPDX 3 SBOM for a built Python wheel."""
    effective_pretty = False if pretty is None else pretty
    effective_describe = (
        False if describe_relationship is None else describe_relationship
    )
    effective_update_registry = True if update_registry is None else update_registry
    wheel_path_obj = Path(wheel_path)
    project_metadata, project_files = read_wheel(wheel_path_obj)
    phantom_deps = find_phantom_dependencies(project_files)

    cwd = Path.cwd()
    effective_offline = (
        _resolve_local_offline_default(cwd) if offline is None else offline
    )
    resolved_registry = resolve_registry(cwd, registry)

    doc = DocumentModel(
        project=project_metadata,
        creation_metadata=creation_metadata or CreationMetadata(),
        ai_models=[],
        phantom_dependencies=phantom_deps,
    )
    exporter = build(
        doc,
        merkle_root=None,
        sbom_type=spdx3_bindings.software_SbomType.analyzed,
        registry=resolved_registry,
        provenance=provenance,
        offline=effective_offline,
    )

    _sync_registry(exporter, resolved_registry, effective_update_registry)

    sbom_json = exporter.to_json(
        pretty=effective_pretty,
        describe_relationship=effective_describe,
    )

    _write_output_file(sbom_json, output_path)

    return sbom_json

pitloom.assemble.generate_model_sbom

generate_model_sbom(
    source: Path | str,
    *,
    offline: bool | None = None,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
) -> str

Generate an Analyzed SPDX 3 AIBOM for a local model file or HF repository.

Source code in pitloom/assemble/_model_generator.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def generate_model_sbom(
    source: Path | str,
    *,
    offline: bool | None = None,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
) -> str:
    """Generate an Analyzed SPDX 3 AIBOM for a local model file or HF repository."""
    effective_pretty = False if pretty is None else pretty
    effective_describe = (
        False if describe_relationship is None else describe_relationship
    )
    source_str = str(source)
    is_hf = is_huggingface_source(source_str)
    enrichment_results: list[EnrichmentResult] = []

    if is_hf:
        effective_offline = (
            _resolve_local_offline_default(Path.cwd()) if offline is None else offline
        )
        if effective_offline:
            raise ValueError(
                "Offline mode enabled: cannot fetch remote Hugging Face source "
                f"'{source_str}'"
            )
        model = read_huggingface(source_str)
        entity_spdx_id = None
    else:
        model_path = Path(source)
        model = read_ai_model(model_path)
        resolved_registry = resolve_registry(Path.cwd(), registry)
        entity_spdx_id = (
            resolved_registry.lookup_entity(model_path.stem, "ai_AIPackage")
            if resolved_registry is not None
            else None
        )

        model_dir = model_path.parent
        enrich_config = _resolve_model_enrich_config(model_dir)
        if enrich is not None:
            enrich_config = dataclasses.replace(enrich_config, local=enrich)
        enrichment_results = run_enrichers(model, enrich_config, model_dir)

    exporter = build_model(
        model,
        creation_metadata or CreationMetadata(),
        entity_spdx_id=entity_spdx_id,
        provenance=provenance,
        enrichment_results=enrichment_results,
    )

    sbom_json = exporter.to_json(
        pretty=effective_pretty,
        describe_relationship=effective_describe,
    )

    _write_output_file(sbom_json, output_path)

    return sbom_json

pitloom.assemble.generate_env_sbom

generate_env_sbom(
    *,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    offline: bool | None = None,
    update_registry: bool | None = None,
) -> str

Generate a Deployed SPDX 3 SBOM for the current installed environment.

Source code in pitloom/assemble/_generators.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def generate_env_sbom(
    *,
    output_path: Path | None = None,
    creation_metadata: CreationMetadata | None = None,
    pretty: bool | None = None,
    describe_relationship: bool | None = None,
    registry: str | Path | IdRegistry | None = None,
    provenance: ProvenanceConfig | None = None,
    offline: bool | None = None,
    update_registry: bool | None = None,
) -> str:
    """Generate a Deployed SPDX 3 SBOM for the current installed environment."""
    effective_pretty = False if pretty is None else pretty
    effective_describe = (
        False if describe_relationship is None else describe_relationship
    )
    effective_update_registry = True if update_registry is None else update_registry
    project_metadata, env_tree = read_environment()

    cwd = Path.cwd()
    effective_offline = (
        _resolve_local_offline_default(cwd) if offline is None else offline
    )
    resolved_registry = resolve_registry(cwd, registry)

    doc = DocumentModel(
        project=project_metadata,
        creation_metadata=creation_metadata or CreationMetadata(),
        ai_models=[],
    )
    exporter = build_deployed(
        doc,
        env_tree=env_tree,
        registry=resolved_registry,
        provenance=provenance,
        offline=effective_offline,
    )

    _sync_registry(exporter, resolved_registry, effective_update_registry)

    sbom_json = exporter.to_json(
        pretty=effective_pretty,
        describe_relationship=effective_describe,
    )

    _write_output_file(sbom_json, output_path)

    return sbom_json

Wheel embedding

pitloom.embed.embed_wheel_sbom

embed_wheel_sbom(
    wheel_path: Path | str,
    *,
    project_dir: Path | str | None = None,
    pitloom_config: PitloomConfig | None = None,
    sbom_path: Path | str | None = None,
    output_path: Path | str | None = None,
    sbom_basename: str | None = None,
    creation_metadata: CreationMetadata | None = None,
    registry: str | Path | IdRegistry | None = None,
    overrides: ConfigOverrides | None = None,
) -> tuple[Path, str, str, tuple[str, ...], bool]

Generate and embed a PEP 770 SBOM into a built Python wheel.

Source code in pitloom/embed.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def embed_wheel_sbom(
    wheel_path: Path | str,
    *,
    project_dir: Path | str | None = None,
    pitloom_config: PitloomConfig | None = None,
    sbom_path: Path | str | None = None,
    output_path: Path | str | None = None,
    sbom_basename: str | None = None,
    creation_metadata: CreationMetadata | None = None,
    registry: str | Path | IdRegistry | None = None,
    overrides: ConfigOverrides | None = None,
) -> tuple[Path, str, str, tuple[str, ...], bool]:
    """Generate and embed a PEP 770 SBOM into a built Python wheel."""
    wheel_obj = Path(wheel_path).resolve()
    wheel_metadata, _ = read_wheel(wheel_obj)
    eff_overrides = overrides if overrides is not None else ConfigOverrides()

    sbom_json, eff_basename = _generate_embed_sbom_json(
        wheel_metadata,
        project_dir=project_dir,
        pitloom_config=pitloom_config,
        sbom_path=sbom_path,
        sbom_basename=sbom_basename,
        creation_metadata=creation_metadata,
        registry=registry,
        overrides=eff_overrides,
    )
    target_filename = (
        f"{eff_basename.removesuffix(_SPDX3_JSON_EXT)}{_SPDX3_JSON_EXT}"
        if eff_basename
        else None
    )

    res_path, arcname, removed_arcnames, timestamp_floored = embed_sbom_in_wheel(
        wheel_obj, sbom_json, sbom_filename=target_filename
    )

    if output_path is not None:
        Path(output_path).write_text(sbom_json, encoding="utf-8")

    return res_path, arcname, sbom_json, removed_arcnames, timestamp_floored

pitloom.embed.embed_sbom_in_wheel

embed_sbom_in_wheel(
    wheel_path: Path | str,
    sbom_content: str | bytes,
    *,
    sbom_filename: str | None = None,
) -> tuple[Path, str, tuple[str, ...], bool]

Embed an SPDX 3 SBOM into a built wheel archive (PEP 770).

Source code in pitloom/_embed_wheel.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def embed_sbom_in_wheel(
    wheel_path: Path | str,
    sbom_content: str | bytes,
    *,
    sbom_filename: str | None = None,
) -> tuple[Path, str, tuple[str, ...], bool]:
    """Embed an SPDX 3 SBOM into a built wheel archive (PEP 770)."""
    wheel_obj = Path(wheel_path).resolve()
    if not wheel_obj.exists():
        raise FileNotFoundError(f"Wheel file not found: {wheel_obj}")

    sbom_bytes = (
        sbom_content.encode("utf-8") if isinstance(sbom_content, str) else sbom_content
    )
    if not sbom_bytes.strip():
        raise ValueError("SBOM content cannot be empty")

    orig_mode = wheel_obj.stat().st_mode if wheel_obj.exists() else None

    with zipfile.ZipFile(wheel_obj, "r") as original_zf:
        dist_info = _find_dist_info_prefix(original_zf, wheel_obj)
        plan = _plan_embed(original_zf, dist_info, sbom_filename, sbom_bytes)
        temp_path = _rewrite_wheel_archive(
            wheel_obj,
            original_zf,
            plan.sbom_arcname,
            sbom_bytes,
            plan.record_arcname,
            plan.new_record_bytes,
            plan.timestamp,
            plan.stale_arcnames,
        )

    try:
        os.replace(temp_path, wheel_obj)
        if orig_mode is not None:
            try:
                os.chmod(wheel_obj, orig_mode)
            except OSError:
                pass
    finally:
        if temp_path.exists():
            temp_path.unlink()

    return (
        wheel_obj,
        plan.sbom_arcname,
        tuple(sorted(plan.stale_arcnames)),
        plan.timestamp_floored,
    )

pitloom.embed.ConfigOverrides dataclass

ConfigOverrides(
    provenance: ProvenanceConfig | None = None,
    enrich: bool | None = None,
    extract_file_header: bool | None = None,
    content_type: bool | None = None,
    content_type_method: str | None = None,
    offline: bool | None = None,
)

Per-run overrides layered onto a project's [tool.pitloom] config.

Tracking decorator

loom.run is the Run class below (run = Run) -- use it as a decorator or a context manager, as shown on the Python API page.

pitloom.loom.Run

Run(
    output_file: str | Path,
    pretty: bool = False,
    creation_metadata: CreationMetadata | None = None,
    registry: str | Path | IdRegistry | None = None,
)

Context manager and decorator for capturing SPDX fragments.

Each Run is a single recording session that weaves metadata about a model and its datasets into an SBOM fragment.

Can be used as a context manager::

with loom.run("fragments/train.spdx3.json") as run:
    run.set_model("my-model")
    run.add_dataset("train.txt")
    run.add_validation_dataset("valid.txt")
    # ... training code ...
    run.set_model_hyperparameters({"lr": "0.1", "epoch": "5"})

Or as a function decorator::

@loom.run("fragments/preprocess.spdx3.json")
def preprocess():
    loom.add_input_dataset("rawdata/neg.txt")
    loom.add_output_dataset("data/train.txt",
                            data_preprocessing=["tokenization"])

The fragment's SPDX CreationInfo is configurable on par with the CLI and Hatchling build hook: pass a CreationMetadata to name a creator (a person, organization, or automated agent), or override the tool, timestamp, and comment. With none given, the fragment records the SoftwareAgent "Pitloom" (createdBy) and Tool "Pitloom" (createdUsing) of an unattended run::

loom.run(
    "fragments/train.spdx3.json",
    creation_metadata=CreationMetadata(
        creators=[Creator(name="Alice", type="person")]
    ),
)

Parameters:

Name Type Description Default
output_file str | Path

Path to write the SBOM fragment to.

required
pretty bool

Indent the JSON output with 2 spaces when True.

False
creation_metadata CreationMetadata | None

Creator, tool, timestamp, and comment overrides for the fragment's CreationInfo. See CreationMetadata for all fields. When None (default), the comment defaults to an auto-generated note identifying the loom SDK and its version, and the creator defaults to the SoftwareAgent "Pitloom".

None
registry str | Path | IdRegistry | None

A pitloom.ids.IdRegistry, a path to a registry JSON file, or None (default) to auto-discover loom-ids.json by walking up from the current working directory. Consulted read-only: datasets, the model, and the generating script all get the registered spdxId when one exists for them, so independently generated fragments can be unified at merge time without name-based matching.

None
Source code in pitloom/loom.py
88
89
90
91
92
93
94
95
96
97
98
99
def __init__(
    self,
    output_file: str | Path,
    pretty: bool = False,
    creation_metadata: CreationMetadata | None = None,
    registry: str | Path | IdRegistry | None = None,
):
    self.output_file = str(output_file)
    self.pretty = pretty
    self.creation_metadata = creation_metadata or CreationMetadata()
    self.registry = registry
    self.previous_run: _ActiveRun | None = None

pitloom.loom.set_model

set_model(
    name: str,
    model_type: str | None = None,
    hyperparameters: dict[str, str] | None = None,
    generated: bool | None = None,
) -> None

Set the name of the AI model being trained in the current run.

Source code in pitloom/loom.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def set_model(
    name: str,
    model_type: str | None = None,
    hyperparameters: dict[str, str] | None = None,
    generated: bool | None = None,
) -> None:
    """Set the name of the AI model being trained in the current run."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use `loom.set_model()` inside a "
            "`with pitloom.loom.run():` block or decorated function."
        )
    _active_run.set_model(
        name,
        model_type=model_type,
        hyperparameters=hyperparameters,
        generated=generated,
    )

pitloom.loom.use_model

use_model(
    name: str,
    model_type: str | None = None,
    hyperparameters: dict[str, str] | None = None,
) -> None

Explicitly declare an AI model consumed by the current run (for inference).

Source code in pitloom/loom.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def use_model(
    name: str,
    model_type: str | None = None,
    hyperparameters: dict[str, str] | None = None,
) -> None:
    """Explicitly declare an AI model consumed by the current run (for inference)."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use `loom.use_model()` inside a "
            "`with pitloom.loom.run():` block or decorated function."
        )
    _active_run.use_model(
        name,
        model_type=model_type,
        hyperparameters=hyperparameters,
    )

pitloom.loom.set_model_hyperparameters

set_model_hyperparameters(
    hyperparameters: dict[str, str],
) -> None

Update the active model with hyperparameters captured after training.

Source code in pitloom/loom.py
171
172
173
174
175
176
177
178
def set_model_hyperparameters(hyperparameters: dict[str, str]) -> None:
    """Update the active model with hyperparameters captured after training."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use `loom.set_model_hyperparameters()`"
            " inside a `with pitloom.loom.run():` block or decorated function."
        )
    _active_run.set_model_hyperparameters(hyperparameters)

pitloom.loom.add_dataset

add_dataset(name: str, dataset_type: str = 'text') -> None

Add a dataset utilized by the AI model in the current run.

Source code in pitloom/loom.py
181
182
183
184
185
186
187
188
def add_dataset(name: str, dataset_type: str = "text") -> None:
    """Add a dataset utilized by the AI model in the current run."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use `loom.add_dataset()` inside a "
            "`with pitloom.loom.run():` block or decorated function."
        )
    _active_run.add_dataset(name, dataset_type)

pitloom.loom.add_validation_dataset

add_validation_dataset(
    name: str, dataset_type: str = "text"
) -> None

Add a validation/test dataset in the current run.

Source code in pitloom/loom.py
191
192
193
194
195
196
197
198
199
def add_validation_dataset(name: str, dataset_type: str = "text") -> None:
    """Add a validation/test dataset in the current run."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use "
            "`loom.add_validation_dataset()` inside a "
            "`with pitloom.loom.run():` block or decorated function."
        )
    _active_run.add_validation_dataset(name, dataset_type)

pitloom.loom.add_input_dataset

add_input_dataset(
    name: str, dataset_type: str = "text"
) -> None

Declare a raw/source dataset consumed by a preprocessing step.

Source code in pitloom/loom.py
202
203
204
205
206
207
208
209
210
def add_input_dataset(name: str, dataset_type: str = "text") -> None:
    """Declare a raw/source dataset consumed by a preprocessing step."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use "
            "`loom.add_input_dataset()` inside a "
            "`with pitloom.loom.run():` block or decorated function."
        )
    _active_run.add_input_dataset(name, dataset_type)

pitloom.loom.add_output_dataset

add_output_dataset(
    name: str,
    dataset_type: str = "text",
    data_preprocessing: list[str] | None = None,
    input_datasets: list[str] | None = None,
) -> None

Declare a derived/processed dataset produced by a preprocessing step.

Source code in pitloom/loom.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def add_output_dataset(
    name: str,
    dataset_type: str = "text",
    data_preprocessing: list[str] | None = None,
    input_datasets: list[str] | None = None,
) -> None:
    """Declare a derived/processed dataset produced by a preprocessing step."""
    if _active_run is None:
        raise RuntimeError(
            "No active loom.run() found. Please use "
            "`loom.add_output_dataset()` inside a "
            "`with pitloom.loom.run():` block or decorated function."
        )
    _active_run.add_output_dataset(
        name,
        dataset_type,
        data_preprocessing=data_preprocessing,
        input_datasets=input_datasets,
    )

Creation metadata

pitloom.core.creation.CreationMetadata dataclass

CreationMetadata(
    creators: list[Creator] = list(),
    tools: list[Tool] | None = None,
    creation_datetime: str | None = None,
    creation_comment: str | None = None,
    build_datetime: str | None = None,
)

Metadata describing who and what generated an SBOM.

Pitloom's own model for creation provenance -- distinct from, but mapping onto, SPDX 3 CreationInfo: each creator becomes an Agent in createdBy (Person, Organization, SoftwareAgent, or the generic Agent -- see Creator.type), and each tool becomes a Tool in createdUsing. When no creator is named, the assembler records the automated SoftwareAgent "Pitloom" in createdBy -- Pitloom acting on its own -- rather than inventing a Person.

Attributes:

Name Type Description
creators list[Creator]

Named creators, in order. When empty (default), no named creator is asserted and the assembler emits the SoftwareAgent "Pitloom" in createdBy instead. When multiple creators are supplied, each becomes its own Agent; the SPDX 3 suppliedBy of the main package (single-valued) is set to the first named creator.

tools list[Tool] | None

Creation tools, in order. None (default) means the default single Tool "Pitloom". An empty list suppresses createdUsing entirely (matches --no-creation-tool). A non-empty list emits one Tool per entry.

creation_datetime str | None

ISO 8601 string for the creation timestamp. Full ISO forms are accepted (e.g. offsets and fractional seconds). Pitloom preserves input precision internally and normalises to SPDX 3 DateTime (YYYY-MM-DDThh:mm:ssZ) only at export time. When None, the assembler falls back to SOURCE_DATE_EPOCH (reproducible-builds.org) if set, else the current UTC time -- see :func:resolve_source_date_epoch.

creation_comment str | None

Optional comment to include on the SPDX CreationInfo element. Callers (CLI, Hatchling build hook) set this to a static description of the invocation channel, e.g. "Generated via Pitloom CLI".

build_datetime str | None

ISO 8601 string for when the artifact was built (e.g. the moment the Hatchling hook fires). When set, the assembler records it as builtTime on the main software_Package element. When None (default), builtTime is omitted from the SBOM.

pitloom.core.creation.Creator dataclass

Creator(
    name: str,
    type: str = "person",
    email: str | None = None,
)

A single named creator, mapping onto an SPDX 3 Agent.

Attributes:

Name Type Description
name str

Display name of the person or organisation that initiated the SBOM generation.

type str

Agent subclass: "person" (default), "organization", "software-agent", or the generic "agent". All four are valid createdBy types per the SPDX 3 spec. Validated (and normalised to lower-case, stripped) in __post_init__.

email str | None

E-mail address of the creator. Recorded as an email external identifier on the creator Agent.

Raises:

Type Description
ValueError

If name is empty or whitespace-only, or if type (after normalisation) is not one of :data:VALID_CREATOR_TYPES.

pitloom.core.creation.Tool dataclass

Tool(name: str)

A single creation tool, mapping onto an SPDX 3 Tool.

Attributes:

Name Type Description
name str

Name of the tool. A tool literally named "Pitloom" gets a version summary appended automatically.

Raises:

Type Description
ValueError

If name is empty or whitespace-only.

Provenance configuration

pitloom.core.provenance.ProvenanceConfig dataclass

ProvenanceConfig(
    format: str = "both",
    schema: str = DEFAULT_PROVENANCE_SCHEMA,
    detail: str = "minimal",
    preserve_source_metadata: str = "auto",
)

Configuration settings for SPDX 3 metadata provenance annotations.

Attributes:

Name Type Description
format str

How to record metadata provenance ("annotation", "comment", "both").

schema str

Schema id for provenance Annotations.

detail str

Provenance detail level ("minimal", "full").

preserve_source_metadata str

How to preserve source metadata ("auto", "always", "never").

ID registry

pitloom.ids.IdRegistry

IdRegistry(
    namespace: str,
    files: dict[str, FileEntry] | None = None,
    entities: dict[str, EntityEntry] | None = None,
    path: Path | None = None,
)

A Loom ID registry: a stable file/entity -> SPDX ID registry, persisted as JSON.

Source code in pitloom/ids.py
62
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    namespace: str,
    files: dict[str, FileEntry] | None = None,
    entities: dict[str, EntityEntry] | None = None,
    path: Path | None = None,
) -> None:
    self.namespace = namespace
    self.files: dict[str, FileEntry] = files if files is not None else {}
    self.entities: dict[str, EntityEntry] = entities if entities is not None else {}
    self.path = path

find staticmethod

find(start: Path | None = None) -> IdRegistry | None

Walk upward from start (default: cwd) looking for loom-ids.json.

Source code in pitloom/ids.py
113
114
115
116
117
118
119
120
121
122
123
124
125
@staticmethod
def find(start: Path | None = None) -> IdRegistry | None:
    """Walk upward from *start* (default: cwd) looking for ``loom-ids.json``."""
    current = (start or Path.cwd()).resolve()
    for directory in (current, *current.parents):
        candidate = directory / DEFAULT_REGISTRY_FILENAME
        if candidate.is_file():
            try:
                return IdRegistry.load(candidate)
            except (ValueError, OSError) as exc:
                log.warning("Ignoring invalid registry %s: %s", candidate, exc)
                return None
    return None

generate

generate(paths: list[Path], project_root: Path) -> None

(Re-)index files under paths into this registry.

Source code in pitloom/ids.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def generate(self, paths: list[Path], project_root: Path) -> None:
    """(Re-)index files under *paths* into this registry."""
    # pylint: disable=import-outside-toplevel,cyclic-import
    from pitloom.extract.ai_model import AiModelFormat, detect_ai_model_format

    for file_path in _iter_files(paths, project_root):
        try:
            sha256 = _sha256_file(file_path)
        except OSError as exc:
            log.warning("Registry: could not read %s: %s", file_path, exc)
            continue
        rel_path = file_path.relative_to(project_root).as_posix()
        self.register_file(rel_path, sha256)

        fmt = detect_ai_model_format(file_path)
        if fmt != AiModelFormat.UNKNOWN:
            self.register_entity(file_path.stem, "ai_AIPackage")

harvest

harvest(object_set: SHACLObjectSet) -> tuple[int, int]

Harvest every named element in object_set into this registry.

Used both by :meth:import_sbom (after deserializing an existing SBOM from disk) and by SBOM generation itself, directly on a :class:~pitloom.export.spdx3_json.Spdx3JsonExporter's in-memory object set -- no serialize/reparse round trip needed there, since every element already carries its assigned spdxId.

Returns the number of (new_files, new_entities) added.

Source code in pitloom/ids.py
223
224
225
226
227
228
229
230
231
232
233
234
def harvest(self, object_set: spdx3.SHACLObjectSet) -> tuple[int, int]:
    """Harvest every named element in *object_set* into this registry.

    Used both by :meth:`import_sbom` (after deserializing an existing
    SBOM from disk) and by SBOM generation itself, directly on a
    :class:`~pitloom.export.spdx3_json.Spdx3JsonExporter`'s in-memory
    object set -- no serialize/reparse round trip needed there, since
    every element already carries its assigned ``spdxId``.

    Returns the number of ``(new_files, new_entities)`` added.
    """
    return self._harvest_sorted(_sorted_by_spdx_id(object_set))

import_sbom

import_sbom(sbom_path: Path) -> None

Harvest ids from an existing SPDX 3 JSON-LD SBOM into this registry.

Source code in pitloom/ids.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def import_sbom(self, sbom_path: Path) -> None:
    """Harvest ids from an existing SPDX 3 JSON-LD SBOM into this registry."""
    object_set = spdx3.SHACLObjectSet()
    with open(sbom_path, "rb") as f:
        spdx3.JSONLDDeserializer().read(f, object_set)

    sorted_objects = _sorted_by_spdx_id(object_set)

    if not self.files and not self.entities:
        for obj in sorted_objects:
            if isinstance(obj, spdx3.SpdxDocument) and obj.spdxId:
                self.namespace = obj.spdxId
                break

    self._harvest_sorted(sorted_objects)

load classmethod

load(path: Path) -> IdRegistry

Load a registry from path.

Source code in pitloom/ids.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@classmethod
def load(cls, path: Path) -> IdRegistry:
    """Load a registry from *path*."""
    if not path.exists():
        raise FileNotFoundError(f"Registry file not found: {path}")
    try:
        with open(path, encoding="utf-8") as f:
            data: dict[str, Any] = json.load(f)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Registry {path} is not valid JSON: {exc}") from exc

    namespace = data.get("namespace")
    if not isinstance(namespace, str) or not namespace:
        raise ValueError(f"Registry {path} is missing a valid 'namespace'")

    try:
        files = {
            str(rel_path): FileEntry(
                spdx_id=str(entry["spdxId"]), sha256=str(entry["sha256"])
            )
            for rel_path, entry in data.get("files", {}).items()
        }
        entities = {
            str(name): EntityEntry(
                type=str(entry["type"]), spdx_id=str(entry["spdxId"])
            )
            for name, entry in data.get("entities", {}).items()
        }
    except (KeyError, TypeError, AttributeError) as exc:
        raise ValueError(f"Registry {path} has a malformed entry: {exc}") from exc

    return cls(namespace=namespace, files=files, entities=entities, path=path)

lookup_entity

lookup_entity(name: str, type_name: str) -> str | None

Return the registered spdxId for the named entity of type_name.

Source code in pitloom/ids.py
134
135
136
137
138
139
def lookup_entity(self, name: str, type_name: str) -> str | None:
    """Return the registered ``spdxId`` for the named entity of *type_name*."""
    entry = self.entities.get(name)
    if entry is None or entry.type != type_name:
        return None
    return entry.spdx_id

lookup_file

lookup_file(path: str, sha256: str) -> str | None

Return the registered spdxId for path.

Source code in pitloom/ids.py
127
128
129
130
131
132
def lookup_file(self, path: str, sha256: str) -> str | None:
    """Return the registered ``spdxId`` for *path*."""
    entry = self.files.get(path)
    if entry is None or entry.sha256 != sha256:
        return None
    return entry.spdx_id

new classmethod

new(
    project_name: str, path: Path | None = None
) -> IdRegistry

Create a fresh, empty registry with a freshly minted namespace.

Source code in pitloom/ids.py
74
75
76
77
78
@classmethod
def new(cls, project_name: str, path: Path | None = None) -> IdRegistry:
    """Create a fresh, empty registry with a freshly minted namespace."""
    namespace = f"https://spdx.org/spdxdocs/{project_name}-{uuid4()}"
    return cls(namespace=namespace, path=path)

register_entity

register_entity(name: str, type_name: str) -> str

Register (or reuse) a named entity and return its spdxId.

Source code in pitloom/ids.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def register_entity(self, name: str, type_name: str) -> str:
    """Register (or reuse) a named entity and return its ``spdxId``."""
    existing = self.entities.get(name)
    if existing is not None:
        if existing.type != type_name:
            log.warning(
                "Registry: entity %r already registered as type %r; "
                "keeping its existing spdxId rather than minting a new "
                "one for type %r.",
                name,
                existing.type,
                type_name,
            )
        return existing.spdx_id
    spdx_id = self._mint_id(_type_id_prefix(type_name))
    self.entities[name] = EntityEntry(type=type_name, spdx_id=spdx_id)
    return spdx_id

register_file

register_file(path: str, sha256: str) -> str

Register (or refresh) a file entry and return its spdxId.

Source code in pitloom/ids.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def register_file(self, path: str, sha256: str) -> str:
    """Register (or refresh) a file entry and return its ``spdxId``."""
    existing = self.files.get(path)
    if existing is not None and existing.sha256 == sha256:
        return existing.spdx_id
    if existing is not None:
        log.info(
            "Registry: content changed for %s; minting a new spdxId (old: %s).",
            path,
            existing.spdx_id,
        )
    spdx_id = self._mint_id("File")
    self.files[path] = FileEntry(spdx_id=spdx_id, sha256=sha256)
    return spdx_id

save

save(path: Path | None = None) -> None

Write this registry as JSON to path.

Source code in pitloom/ids.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def save(self, path: Path | None = None) -> None:
    """Write this registry as JSON to *path*."""
    target = path or self.path
    if target is None:
        raise ValueError("No path given and registry has no default path")
    data = {
        "version": _REGISTRY_VERSION,
        "namespace": self.namespace,
        "files": {
            rel_path: {"spdxId": entry.spdx_id, "sha256": entry.sha256}
            for rel_path, entry in sorted(self.files.items())
        },
        "entities": {
            name: {"type": entry.type, "spdxId": entry.spdx_id}
            for name, entry in sorted(self.entities.items())
        },
    }
    target.parent.mkdir(parents=True, exist_ok=True)
    with open(target, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, sort_keys=True, ensure_ascii=False)
        f.write("\n")
    self.path = target

pitloom.ids.resolve_registry

resolve_registry(
    project_dir: Path,
    ids_file: str | Path | IdRegistry | None = None,
) -> IdRegistry | None

Resolve the registry a project build should consult.

Source code in pitloom/ids.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def resolve_registry(
    project_dir: Path,
    ids_file: str | Path | IdRegistry | None = None,
) -> IdRegistry | None:
    """Resolve the registry a project build should consult."""
    if isinstance(ids_file, IdRegistry):
        return ids_file
    if ids_file is not None:
        path = Path(ids_file)
        registry_path = path if path.is_absolute() else project_dir / path
        try:
            return IdRegistry.load(registry_path)
        except (FileNotFoundError, ValueError, OSError) as exc:
            log.warning("Could not load registry %s: %s", registry_path, exc)
            return None
    return IdRegistry.find(start=project_dir)