torch-deepaudioembedding 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. torch_dae/__init__.py +10 -0
  2. torch_dae/cards/__init__.py +59 -0
  3. torch_dae/cards/models.py +471 -0
  4. torch_dae/cards/validation.py +77 -0
  5. torch_dae/cli/__init__.py +1 -0
  6. torch_dae/cli/cards.py +45 -0
  7. torch_dae/cli/checkpoints.py +75 -0
  8. torch_dae/cli/environment.py +177 -0
  9. torch_dae/cli/main.py +20 -0
  10. torch_dae/cli/models.py +27 -0
  11. torch_dae/contracts.py +96 -0
  12. torch_dae/core/__init__.py +41 -0
  13. torch_dae/core/capabilities.py +61 -0
  14. torch_dae/core/checkpoint.py +1386 -0
  15. torch_dae/core/embeddings.py +62 -0
  16. torch_dae/core/errors.py +89 -0
  17. torch_dae/core/model.py +285 -0
  18. torch_dae/core/outputs.py +116 -0
  19. torch_dae/core/preprocessing.py +29 -0
  20. torch_dae/core/registry.py +174 -0
  21. torch_dae/environment/__init__.py +69 -0
  22. torch_dae/environment/fingerprint.py +159 -0
  23. torch_dae/environment/manager.py +1533 -0
  24. torch_dae/environment/materialization.py +14 -0
  25. torch_dae/environment/policy.py +25 -0
  26. torch_dae/environment/runtime.py +286 -0
  27. torch_dae/environment/sources.py +557 -0
  28. torch_dae/environment/specification.py +188 -0
  29. torch_dae/environment/subprocess.py +261 -0
  30. torch_dae/environment/verification.py +68 -0
  31. torch_dae/onboarding/__init__.py +87 -0
  32. torch_dae/onboarding/contracts.py +1336 -0
  33. torch_dae/onboarding/evaluation.py +423 -0
  34. torch_dae/onboarding/inspection.py +2387 -0
  35. torch_dae/onboarding/rendering.py +134 -0
  36. torch_dae/py.typed +0 -0
  37. torch_deepaudioembedding-0.1.0.dist-info/METADATA +332 -0
  38. torch_deepaudioembedding-0.1.0.dist-info/RECORD +42 -0
  39. torch_deepaudioembedding-0.1.0.dist-info/WHEEL +4 -0
  40. torch_deepaudioembedding-0.1.0.dist-info/entry_points.txt +2 -0
  41. torch_deepaudioembedding-0.1.0.dist-info/licenses/LICENSE +201 -0
  42. torch_deepaudioembedding-0.1.0.dist-info/licenses/NOTICE +2 -0
torch_dae/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Root control-plane package for torch-dae.
2
+
3
+ repository foundation exposes contracts and registry helpers without importing PyTorch.
4
+ """
5
+
6
+ from torch_dae.core.registry import ModelCardRegistry
7
+
8
+ __all__ = ["ModelCardRegistry", "__version__"]
9
+
10
+ __version__ = "0.1.0"
@@ -0,0 +1,59 @@
1
+ """Stable, model-agnostic model-card contract namespace."""
2
+
3
+ from torch_dae.cards.models import (
4
+ BooleanCapability,
5
+ CapabilitiesSection,
6
+ DatasetRecord,
7
+ Datasets,
8
+ Description,
9
+ DeviceSupport,
10
+ EmbeddingsSection,
11
+ EvidenceRecord,
12
+ EvidenceStatus,
13
+ Identity,
14
+ IssueRecord,
15
+ IssueStatus,
16
+ MetricRecord,
17
+ ModelCard,
18
+ ModelCardLifecycle,
19
+ OutputComponent,
20
+ Outputs,
21
+ ProfilingSection,
22
+ ProfilingStatus,
23
+ RecommendedEnvironment,
24
+ ScientificReference,
25
+ SourceRecord,
26
+ Sources,
27
+ Tasks,
28
+ Usage,
29
+ WaveformInput,
30
+ )
31
+
32
+ __all__ = [
33
+ "BooleanCapability",
34
+ "CapabilitiesSection",
35
+ "DatasetRecord",
36
+ "Datasets",
37
+ "Description",
38
+ "DeviceSupport",
39
+ "EmbeddingsSection",
40
+ "EvidenceRecord",
41
+ "EvidenceStatus",
42
+ "Identity",
43
+ "IssueRecord",
44
+ "IssueStatus",
45
+ "MetricRecord",
46
+ "ModelCard",
47
+ "ModelCardLifecycle",
48
+ "OutputComponent",
49
+ "Outputs",
50
+ "ProfilingSection",
51
+ "ProfilingStatus",
52
+ "RecommendedEnvironment",
53
+ "ScientificReference",
54
+ "SourceRecord",
55
+ "Sources",
56
+ "Tasks",
57
+ "Usage",
58
+ "WaveformInput",
59
+ ]
@@ -0,0 +1,471 @@
1
+ """Strict model-card contracts for repository foundation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import Annotated, Literal
7
+
8
+ from pydantic import Field, HttpUrl, field_validator, model_validator
9
+
10
+ from torch_dae.contracts import (
11
+ GIT_REVISION_PATTERN,
12
+ REPO_RELATIVE_PATTERN,
13
+ CanonicalId,
14
+ StrictBaseModel,
15
+ ensure_repository_relative,
16
+ ensure_wrapper_entry_point,
17
+ )
18
+ from torch_dae.core.checkpoint import CheckpointSpec
19
+ from torch_dae.core.embeddings import EmbeddingSpec
20
+
21
+
22
+ class ModelCardLifecycle(StrEnum):
23
+ """Lifecycle states defined by project_spec.md."""
24
+
25
+ DRAFT = "draft"
26
+ ANALYZED = "analyzed"
27
+ ENVIRONMENT_RESOLVED = "environment_resolved"
28
+ CHECKPOINT_VERIFIED = "checkpoint_verified"
29
+ RUNTIME_VERIFIED = "runtime_verified"
30
+ PROFILED = "profiled"
31
+
32
+
33
+ class EvidenceStatus(StrEnum):
34
+ """Evidence provenance status."""
35
+
36
+ OFFICIALLY_REPORTED = "officially_reported"
37
+ OBSERVED = "observed"
38
+ INFERRED = "inferred"
39
+ UNRESOLVED = "unresolved"
40
+ NOT_REPORTED = "not_reported"
41
+ NOT_APPLICABLE = "not_applicable"
42
+
43
+
44
+ class IssueStatus(StrEnum):
45
+ """Issue lifecycle state."""
46
+
47
+ OPEN = "open"
48
+ RESOLVED = "resolved"
49
+ ACCEPTED = "accepted"
50
+ NOT_APPLICABLE = "not_applicable"
51
+
52
+
53
+ class ProfilingStatus(StrEnum):
54
+ """Profiling status reserved for later phases."""
55
+
56
+ NOT_PROFILED = "not_profiled"
57
+ PROFILED = "profiled"
58
+
59
+
60
+ class EvidenceRecord(StrictBaseModel):
61
+ """Concise evidence record for a material claim."""
62
+
63
+ evidence_id: CanonicalId
64
+ kind: str
65
+ status: EvidenceStatus
66
+ url: HttpUrl | None = None
67
+ revision: Annotated[str | None, Field(pattern=GIT_REVISION_PATTERN)] = None
68
+ path: str | None = None
69
+ symbol: str | None = None
70
+ description: str
71
+ rationale: str | None = None
72
+
73
+ @model_validator(mode="after")
74
+ def inferred_requires_rationale(self) -> EvidenceRecord:
75
+ if self.status == EvidenceStatus.INFERRED and not self.rationale:
76
+ raise ValueError("inferred evidence requires rationale")
77
+ return self
78
+
79
+ @field_validator("path")
80
+ @classmethod
81
+ def path_repository_relative(cls, value: str | None) -> str | None:
82
+ return ensure_repository_relative(value)
83
+
84
+
85
+ class IssueRecord(StrictBaseModel):
86
+ """Explicit issue record; lifecycle state is not used for every problem."""
87
+
88
+ issue_id: CanonicalId
89
+ kind: str
90
+ status: IssueStatus
91
+ description: str
92
+ impact: str
93
+
94
+
95
+ class Identity(StrictBaseModel):
96
+ """Checkpoint-specific card identity."""
97
+
98
+ model_name: str
99
+ model_family: str
100
+ variant: str
101
+ checkpoint_name: str
102
+ framework: Literal["pytorch"]
103
+ wrapper_entry_point: Annotated[
104
+ str,
105
+ Field(pattern=r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*:[A-Z][A-Za-z0-9_]*$"),
106
+ ]
107
+
108
+ @field_validator("wrapper_entry_point")
109
+ @classmethod
110
+ def validate_entry_point(cls, value: str) -> str:
111
+ return ensure_wrapper_entry_point(value)
112
+
113
+
114
+ class SourceRecord(StrictBaseModel):
115
+ """Repository, package, or asset source evidence."""
116
+
117
+ source_id: CanonicalId
118
+ kind: str
119
+ url: HttpUrl | None = None
120
+ package: str | None = None
121
+ revision: Annotated[str | None, Field(pattern=GIT_REVISION_PATTERN)] = None
122
+ path: Annotated[str | None, Field(pattern=REPO_RELATIVE_PATTERN)] = None
123
+ evidence_status: EvidenceStatus
124
+
125
+ @field_validator("path")
126
+ @classmethod
127
+ def path_repository_relative(cls, value: str | None) -> str | None:
128
+ return ensure_repository_relative(value)
129
+
130
+
131
+ class Sources(StrictBaseModel):
132
+ """Required source roles."""
133
+
134
+ official_repository: SourceRecord
135
+ implementation: SourceRecord
136
+ checkpoint: SourceRecord
137
+ wrapper: SourceRecord
138
+
139
+
140
+ class ScientificReference(StrictBaseModel):
141
+ """Publication metadata."""
142
+
143
+ title: str
144
+ doi: str | None = None
145
+ official_publication: HttpUrl | None = None
146
+ authors: tuple[str, ...]
147
+ year: int | None = Field(default=None, ge=1900, le=2100)
148
+
149
+
150
+ class Description(StrictBaseModel):
151
+ """Separated descriptive claims."""
152
+
153
+ architecture: str
154
+ preprocessing: str
155
+ training_objective: str
156
+ checkpoint_behavior: str
157
+ implementation: str
158
+
159
+
160
+ class Tasks(StrictBaseModel):
161
+ """Task roles separated by phase."""
162
+
163
+ pretraining: tuple[str, ...]
164
+ finetuning: tuple[str, ...]
165
+ official_evaluation: tuple[str, ...]
166
+ supported_inference: tuple[str, ...]
167
+
168
+
169
+ class DatasetRecord(StrictBaseModel):
170
+ """Dataset usage record."""
171
+
172
+ name: str
173
+ version: str | None = None
174
+ subset: str | None = None
175
+ split: str | None = None
176
+ role: str
177
+ source_status: EvidenceStatus
178
+ evidence_ids: tuple[str, ...] = ()
179
+
180
+
181
+ class Datasets(StrictBaseModel):
182
+ """Training, validation, and testing dataset partitions."""
183
+
184
+ training: tuple[DatasetRecord, ...]
185
+ validation: tuple[DatasetRecord, ...]
186
+ testing: tuple[DatasetRecord, ...]
187
+
188
+
189
+ class MetricRecord(StrictBaseModel):
190
+ """Reported metric record."""
191
+
192
+ task: str
193
+ dataset: str
194
+ split: str
195
+ metric: str
196
+ value: float | str | None
197
+ unit: str | None = None
198
+ protocol: str
199
+ checkpoint_specific: bool
200
+ source_status: EvidenceStatus
201
+ evidence_ids: tuple[str, ...] = ()
202
+
203
+
204
+ class RecommendedEnvironment(StrictBaseModel):
205
+ """Reference to committed environment artifacts."""
206
+
207
+ environment_id: CanonicalId
208
+ specification: Annotated[str, Field(pattern=REPO_RELATIVE_PATTERN)]
209
+ lockfile: Annotated[str, Field(pattern=REPO_RELATIVE_PATTERN)]
210
+ verified: bool
211
+
212
+ @field_validator("specification", "lockfile")
213
+ @classmethod
214
+ def paths_repository_relative(cls, value: str) -> str:
215
+ return ensure_repository_relative(value) or value
216
+
217
+
218
+ class Usage(StrictBaseModel):
219
+ """Usage references and commands."""
220
+
221
+ recommended_environment: RecommendedEnvironment
222
+ installation_commands: tuple[str, ...]
223
+ checkpoint_loading: tuple[str, ...]
224
+ smoke_test_command: str
225
+
226
+
227
+ class WaveformInput(StrictBaseModel):
228
+ """Canonical public waveform input contract."""
229
+
230
+ shape: Literal["B,C,T"]
231
+ sample_rate_hz: int = Field(gt=0)
232
+ dtype: str = "float32"
233
+ valid_lengths_shape: Literal["B"] | None = "B"
234
+ channels: str
235
+ resampling: str
236
+ padding: str
237
+ normalization: str
238
+
239
+
240
+ class OutputComponent(StrictBaseModel):
241
+ """Stable output component description."""
242
+
243
+ name: str
244
+ kind: str
245
+ semantic_kind: str
246
+ rank: int = Field(ge=0)
247
+ layout: str
248
+ dimensions: tuple[str, ...]
249
+ dtype: str | None = None
250
+ granularity: str | None = None
251
+ task_head_relation: str | None = None
252
+
253
+ @model_validator(mode="after")
254
+ def rank_matches_dimensions(self) -> OutputComponent:
255
+ if len(self.dimensions) != self.rank:
256
+ raise ValueError("output component rank must match dimensions length")
257
+ return self
258
+
259
+
260
+ class Outputs(StrictBaseModel):
261
+ """Forward and probability output declarations."""
262
+
263
+ primary: OutputComponent
264
+ components: tuple[OutputComponent, ...]
265
+ probability_output: OutputComponent | None = None
266
+
267
+
268
+ class EmbeddingsSection(StrictBaseModel):
269
+ """Embedding declarations with one selected default."""
270
+
271
+ default_embedding_id: CanonicalId
272
+ items: tuple[EmbeddingSpec, ...]
273
+
274
+ @model_validator(mode="after")
275
+ def validate_default(self) -> EmbeddingsSection:
276
+ ids = [item.embedding_id for item in self.items]
277
+ if len(ids) != len(set(ids)):
278
+ raise ValueError("embedding IDs must be unique")
279
+ defaults = [item for item in self.items if item.default]
280
+ if len(defaults) != 1:
281
+ raise ValueError("exactly one embedding must be marked default")
282
+ if self.default_embedding_id != defaults[0].embedding_id:
283
+ raise ValueError("default_embedding_id must refer to the declared default embedding")
284
+ if self.default_embedding_id not in {item.embedding_id for item in self.items}:
285
+ raise ValueError("default_embedding_id must refer to a declared embedding")
286
+ return self
287
+
288
+
289
+ class BooleanCapability(StrictBaseModel):
290
+ """Capability support plus optional reason."""
291
+
292
+ supported: bool
293
+ reason: str | None = None
294
+
295
+ @model_validator(mode="after")
296
+ def reason_for_unsupported(self) -> BooleanCapability:
297
+ if not self.supported and not self.reason:
298
+ raise ValueError("unsupported capabilities require a reason")
299
+ return self
300
+
301
+
302
+ class CapabilitiesSection(StrictBaseModel):
303
+ """Model-independent capability declarations."""
304
+
305
+ random_initialization: BooleanCapability
306
+ checkpoint_loading: BooleanCapability
307
+ probabilities: BooleanCapability
308
+ embeddings: BooleanCapability
309
+
310
+
311
+ class DeviceSupport(StrictBaseModel):
312
+ """Declared and observed device support."""
313
+
314
+ upstream_declared: tuple[str, ...]
315
+ locally_tested: tuple[str, ...]
316
+ known_limitations: tuple[str, ...]
317
+
318
+
319
+ class ProfilingSection(StrictBaseModel):
320
+ """Profiling placeholder or completed profiling reference."""
321
+
322
+ status: ProfilingStatus
323
+ report: Annotated[str | None, Field(pattern=REPO_RELATIVE_PATTERN)] = None
324
+
325
+ @field_validator("report")
326
+ @classmethod
327
+ def report_repository_relative(cls, value: str | None) -> str | None:
328
+ return ensure_repository_relative(value)
329
+
330
+ @model_validator(mode="after")
331
+ def profiled_requires_report(self) -> ProfilingSection:
332
+ if self.status == ProfilingStatus.PROFILED and self.report is None:
333
+ raise ValueError("profiled sections require a report reference")
334
+ return self
335
+
336
+
337
+ class ModelCard(StrictBaseModel):
338
+ """Validate the complete metadata for one checkpoint-specific integration.
339
+
340
+ A card represents exactly one model-family, variant, and checkpoint tuple. Validation enforces
341
+ lifecycle prerequisites, unique evidence identifiers, resolved evidence references, consistent
342
+ capabilities, repository-relative artifact paths, and the canonical waveform/output contracts.
343
+
344
+ Attributes
345
+ ----------
346
+ card_id
347
+ Stable canonical identifier for this exact integration tuple.
348
+ card_status
349
+ Current lifecycle state. Later states require verified environment, checkpoint, runtime,
350
+ and profiling evidence as applicable.
351
+ identity
352
+ Model, variant, checkpoint, framework, and public wrapper identity.
353
+ input
354
+ Canonical waveform contract using ``[B,C,T]``, sample rate in hertz, and optional ``[B]``
355
+ valid lengths.
356
+ embeddings
357
+ Declared embedding tensors, including exactly one default when embeddings are supported.
358
+ evidence
359
+ Provenance records supporting material scientific and technical claims.
360
+
361
+ Raises
362
+ ------
363
+ pydantic.ValidationError
364
+ If any field or cross-document lifecycle constraint is invalid.
365
+
366
+ Notes
367
+ -----
368
+ License fields are informational. Constructing a card never imports a model runtime, downloads
369
+ a checkpoint, or materializes an environment.
370
+ """
371
+
372
+ schema_version: Literal["1.0.0"]
373
+ card_id: CanonicalId
374
+ card_status: ModelCardLifecycle
375
+ identity: Identity
376
+ checkpoint: CheckpointSpec
377
+ sources: Sources
378
+ scientific_reference: ScientificReference
379
+ description: Description
380
+ tasks: Tasks
381
+ datasets: Datasets
382
+ reported_metrics: tuple[MetricRecord, ...]
383
+ usage: Usage
384
+ input: WaveformInput
385
+ outputs: Outputs
386
+ embeddings: EmbeddingsSection
387
+ capabilities: CapabilitiesSection
388
+ device_support: DeviceSupport
389
+ verification_report: Annotated[str | None, Field(pattern=REPO_RELATIVE_PATTERN)] = None
390
+ architectural_profiling: ProfilingSection
391
+ inference_profiling: ProfilingSection
392
+ energy_profiling: ProfilingSection
393
+ limitations: tuple[str, ...]
394
+ issues: tuple[IssueRecord, ...]
395
+ evidence: tuple[EvidenceRecord, ...]
396
+
397
+ @field_validator("verification_report")
398
+ @classmethod
399
+ def verification_report_repository_relative(cls, value: str | None) -> str | None:
400
+ return ensure_repository_relative(value)
401
+
402
+ @model_validator(mode="after")
403
+ def validate_lifecycle(self) -> ModelCard:
404
+ if (
405
+ self.card_status
406
+ in {
407
+ ModelCardLifecycle.ENVIRONMENT_RESOLVED,
408
+ ModelCardLifecycle.CHECKPOINT_VERIFIED,
409
+ ModelCardLifecycle.RUNTIME_VERIFIED,
410
+ ModelCardLifecycle.PROFILED,
411
+ }
412
+ and not self.usage.recommended_environment.verified
413
+ ):
414
+ raise ValueError("environment_resolved or later cards require verified environment")
415
+ if (
416
+ self.card_status
417
+ in {
418
+ ModelCardLifecycle.CHECKPOINT_VERIFIED,
419
+ ModelCardLifecycle.RUNTIME_VERIFIED,
420
+ ModelCardLifecycle.PROFILED,
421
+ }
422
+ and self.checkpoint.observed_sha256 is None
423
+ ):
424
+ raise ValueError(
425
+ "checkpoint_verified or later cards require observed checkpoint SHA-256"
426
+ )
427
+ if (
428
+ self.card_status
429
+ in {
430
+ ModelCardLifecycle.RUNTIME_VERIFIED,
431
+ ModelCardLifecycle.PROFILED,
432
+ }
433
+ and self.verification_report is None
434
+ ):
435
+ raise ValueError("runtime_verified and profiled cards require verification_report")
436
+ if self.card_status == ModelCardLifecycle.PROFILED:
437
+ for section in (
438
+ self.architectural_profiling,
439
+ self.inference_profiling,
440
+ self.energy_profiling,
441
+ ):
442
+ if section.status != ProfilingStatus.PROFILED:
443
+ raise ValueError(
444
+ "profiled cards require all profiling sections marked profiled"
445
+ )
446
+ return self
447
+
448
+ @model_validator(mode="after")
449
+ def validate_capabilities_and_evidence(self) -> ModelCard:
450
+ if self.capabilities.probabilities.supported != (
451
+ self.outputs.probability_output is not None
452
+ ):
453
+ raise ValueError("probability capability must agree with probability output")
454
+ if self.capabilities.embeddings.supported != bool(self.embeddings.items):
455
+ raise ValueError("embedding capability must agree with embedding declarations")
456
+
457
+ evidence_ids = [item.evidence_id for item in self.evidence]
458
+ if len(evidence_ids) != len(set(evidence_ids)):
459
+ raise ValueError("evidence IDs must be unique")
460
+ declared = set(evidence_ids)
461
+ references: list[str] = []
462
+ for dataset in self.datasets.training + self.datasets.validation + self.datasets.testing:
463
+ references.extend(dataset.evidence_ids)
464
+ for metric in self.reported_metrics:
465
+ references.extend(metric.evidence_ids)
466
+ for embedding in self.embeddings.items:
467
+ references.extend(embedding.evidence_ids)
468
+ missing = sorted(set(references) - declared)
469
+ if missing:
470
+ raise ValueError(f"evidence references are unresolved: {missing}")
471
+ return self
@@ -0,0 +1,77 @@
1
+ """Dual Pydantic and JSON Schema validation for model cards."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from jsonschema import Draft202012Validator, FormatChecker
10
+ from jsonschema.exceptions import ValidationError as JsonSchemaValidationError
11
+ from pydantic import ValidationError as PydanticValidationError
12
+
13
+ from torch_dae.cards.models import ModelCard
14
+
15
+
16
+ def load_json(path: Path) -> dict[str, Any]:
17
+ """Load a JSON object from disk."""
18
+
19
+ value = json.loads(path.read_text())
20
+ if not isinstance(value, dict):
21
+ raise ValueError(f"{path} did not contain a JSON object")
22
+ return value
23
+
24
+
25
+ def validate_model_card_pydantic(data: dict[str, Any]) -> ModelCard:
26
+ """Validate a card through Pydantic."""
27
+
28
+ return ModelCard.model_validate(data)
29
+
30
+
31
+ def validate_model_card_schema(data: dict[str, Any], schema_path: Path) -> None:
32
+ """Validate a card through JSON Schema with format checking."""
33
+
34
+ schema = load_json(schema_path)
35
+ Draft202012Validator(schema, format_checker=FormatChecker()).validate(data)
36
+
37
+
38
+ def assert_valid_pydantic(data: dict[str, Any]) -> ModelCard:
39
+ """Assert that Pydantic accepts a model card."""
40
+
41
+ return validate_model_card_pydantic(data)
42
+
43
+
44
+ def assert_invalid_pydantic(data: dict[str, Any]) -> None:
45
+ """Assert that Pydantic rejects a model card."""
46
+
47
+ try:
48
+ validate_model_card_pydantic(data)
49
+ except PydanticValidationError:
50
+ return
51
+ raise AssertionError("expected Pydantic validation to fail")
52
+
53
+
54
+ def assert_valid_json_schema(data: dict[str, Any], schema_path: Path) -> None:
55
+ """Assert that JSON Schema accepts a model card."""
56
+
57
+ validate_model_card_schema(data, schema_path)
58
+
59
+
60
+ def assert_invalid_json_schema(data: dict[str, Any], schema_path: Path) -> None:
61
+ """Assert that JSON Schema rejects a model card."""
62
+
63
+ try:
64
+ validate_model_card_schema(data, schema_path)
65
+ except JsonSchemaValidationError:
66
+ return
67
+ raise AssertionError("expected JSON Schema validation to fail")
68
+
69
+
70
+ def validate_model_card_path(path: Path, schema_path: Path | None = None) -> ModelCard:
71
+ """Validate a model-card file through both validators when a schema is available."""
72
+
73
+ data = load_json(path)
74
+ card = validate_model_card_pydantic(data)
75
+ if schema_path is not None:
76
+ validate_model_card_schema(data, schema_path)
77
+ return card
@@ -0,0 +1 @@
1
+ """CLI package."""
torch_dae/cli/cards.py ADDED
@@ -0,0 +1,45 @@
1
+ """Model-card CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from torch_dae.cards.validation import validate_model_card_path
10
+ from torch_dae.core.registry import ModelCardRegistry
11
+ from torch_dae.environment.manager import discover_repository_root
12
+
13
+ app = typer.Typer(no_args_is_help=True, help="Model-card commands.")
14
+
15
+
16
+ @app.command("list")
17
+ def list_cards() -> None:
18
+ """List discovered model cards."""
19
+
20
+ registry = ModelCardRegistry(discover_repository_root())
21
+ for card in registry.list_cards():
22
+ typer.echo(card.card_id)
23
+
24
+
25
+ @app.command("show")
26
+ def show(card_id: str) -> None:
27
+ """Show a model card as normalized JSON."""
28
+
29
+ registry = ModelCardRegistry(discover_repository_root())
30
+ typer.echo(registry.get_card(card_id).model_dump_json(indent=2))
31
+
32
+
33
+ @app.command("validate")
34
+ def validate(card_id_or_path: str) -> None:
35
+ """Validate a card id or JSON path."""
36
+
37
+ root = discover_repository_root()
38
+ candidate = Path(card_id_or_path)
39
+ if candidate.exists():
40
+ path = candidate
41
+ else:
42
+ registry = ModelCardRegistry(root)
43
+ path = registry.get_card_path(card_id_or_path)
44
+ validate_model_card_path(path, root / "schemas/model-card.schema.json")
45
+ typer.echo(f"valid: {path}")