genblaze-cli 0.3.0__tar.gz → 0.3.4__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.
@@ -54,7 +54,12 @@ ehthumbs.db
54
54
  !.env.example
55
55
  *.pem
56
56
  *.key
57
- credentials.json
57
+ credentials*
58
+ *.credentials
59
+ *_rsa
60
+ *.p12
61
+ *.pfx
62
+ *.keystore
58
63
 
59
64
  # Jupyter
60
65
  .ipynb_checkpoints/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: genblaze-cli
3
- Version: 0.3.0
3
+ Version: 0.3.4
4
4
  Summary: CLI for genblaze manifest operations
5
5
  Project-URL: Homepage, https://github.com/backblaze-labs/genblaze
6
6
  Project-URL: Documentation, https://github.com/backblaze-labs/genblaze#readme
@@ -20,7 +20,7 @@ Classifier: Topic :: Multimedia
20
20
  Classifier: Topic :: Software Development :: Libraries
21
21
  Requires-Python: >=3.11
22
22
  Requires-Dist: click>=8.0
23
- Requires-Dist: genblaze-core<0.4,>=0.2.0
23
+ Requires-Dist: genblaze-core<0.4,>=0.3.6
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pillow>=10.0; extra == 'dev'
26
26
  Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -31,11 +31,11 @@ Description-Content-Type: text/markdown
31
31
 
32
32
  **Command-line toolkit for inspecting, verifying, and indexing genblaze AI-generated-media provenance manifests.**
33
33
 
34
- `genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its SHA-256 hash, replay a run, or index manifests into Parquet for downstream analytics.
34
+ `genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its manifest hash and output sha256 coverage, replay a run, or index manifests into Parquet for downstream analytics.
35
35
 
36
36
  ## Why genblaze-cli
37
37
 
38
- - **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with.
38
+ - **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with and all output assets declare sha256.
39
39
  - **Works on any genblaze output** — PNG, JPEG, WebP, MP4, MP3, WAV with embedded manifests.
40
40
  - **Analytics-ready** — `genblaze index` emits partitioned Parquet tables (runs, steps, assets) for BI tools and data warehouses.
41
41
  - **Zero provider dependencies** — reads manifests; doesn't call any AI API.
@@ -57,7 +57,7 @@ genblaze --help
57
57
  genblaze extract video.mp4 # Extract embedded manifest → stdout (JSON)
58
58
  genblaze extract video.mp4 -o m.json # …or to a file
59
59
 
60
- genblaze verify video.mp4 # Verify the embedded manifest's SHA-256
60
+ genblaze verify video.mp4 # Verify manifest hash + output sha256 declarations
61
61
  genblaze verify manifest.json # Or verify a standalone manifest file
62
62
 
63
63
  genblaze replay manifest.json # Show what a replay would do (dry run)
@@ -65,7 +65,7 @@ genblaze replay manifest.json # Show what a replay would do (dry run
65
65
  genblaze index manifest.json -o data/ # Index into partitioned Parquet tables
66
66
  ```
67
67
 
68
- Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows.
68
+ Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows. The command does not fetch remote asset URLs; consumers that dereference `asset.url` must hash those bytes separately and compare them with the manifest's `asset.sha256`.
69
69
 
70
70
  ## Typical flow
71
71
 
@@ -3,11 +3,11 @@
3
3
 
4
4
  **Command-line toolkit for inspecting, verifying, and indexing genblaze AI-generated-media provenance manifests.**
5
5
 
6
- `genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its SHA-256 hash, replay a run, or index manifests into Parquet for downstream analytics.
6
+ `genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its manifest hash and output sha256 coverage, replay a run, or index manifests into Parquet for downstream analytics.
7
7
 
8
8
  ## Why genblaze-cli
9
9
 
10
- - **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with.
10
+ - **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with and all output assets declare sha256.
11
11
  - **Works on any genblaze output** — PNG, JPEG, WebP, MP4, MP3, WAV with embedded manifests.
12
12
  - **Analytics-ready** — `genblaze index` emits partitioned Parquet tables (runs, steps, assets) for BI tools and data warehouses.
13
13
  - **Zero provider dependencies** — reads manifests; doesn't call any AI API.
@@ -29,7 +29,7 @@ genblaze --help
29
29
  genblaze extract video.mp4 # Extract embedded manifest → stdout (JSON)
30
30
  genblaze extract video.mp4 -o m.json # …or to a file
31
31
 
32
- genblaze verify video.mp4 # Verify the embedded manifest's SHA-256
32
+ genblaze verify video.mp4 # Verify manifest hash + output sha256 declarations
33
33
  genblaze verify manifest.json # Or verify a standalone manifest file
34
34
 
35
35
  genblaze replay manifest.json # Show what a replay would do (dry run)
@@ -37,7 +37,7 @@ genblaze replay manifest.json # Show what a replay would do (dry run
37
37
  genblaze index manifest.json -o data/ # Index into partitioned Parquet tables
38
38
  ```
39
39
 
40
- Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows.
40
+ Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows. The command does not fetch remote asset URLs; consumers that dereference `asset.url` must hash those bytes separately and compare them with the manifest's `asset.sha256`.
41
41
 
42
42
  ## Typical flow
43
43
 
@@ -0,0 +1,54 @@
1
+ """Extract command — print embedded manifest from a media file."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from genblaze_core.canonical.json import canonical_json
7
+ from genblaze_core.exceptions import ManifestError
8
+ from genblaze_core.models.manifest import Manifest
9
+
10
+ from genblaze_cli.manifest_io import extract_manifest
11
+
12
+
13
+ def _manifest_json_for_display(manifest: Manifest) -> str:
14
+ """Serialize for inspection without bypassing write-schema guards."""
15
+ try:
16
+ manifest.assert_writable_schema()
17
+ except ManifestError:
18
+ return canonical_json(manifest.model_dump(mode="python"))
19
+ return manifest.to_canonical_json()
20
+
21
+
22
+ @click.command()
23
+ @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
24
+ @click.option("--format", "fmt", type=click.Choice(["json", "summary"]), default="json")
25
+ @click.option(
26
+ "-o",
27
+ "--output",
28
+ type=click.Path(path_type=Path),
29
+ default=None,
30
+ help="Write extracted manifest JSON to this file instead of stdout.",
31
+ )
32
+ def extract(file: Path, fmt: str, output: Path | None) -> None:
33
+ """Extract and display the genblaze manifest from a media file."""
34
+ try:
35
+ manifest = extract_manifest(file)
36
+ if fmt == "json":
37
+ json_str = _manifest_json_for_display(manifest)
38
+ if output is not None:
39
+ output.write_text(json_str, encoding="utf-8")
40
+ else:
41
+ click.echo(json_str)
42
+ else:
43
+ report = manifest.verification_report()
44
+ click.echo(f"Run ID: {manifest.run.run_id}")
45
+ click.echo(f"Steps: {len(manifest.run.steps)}")
46
+ click.echo(f"Hash: {manifest.canonical_hash}")
47
+ click.echo(f"Hash OK: {report.hash_ok}")
48
+ click.echo(f"Output sha256: {len(report.unverified_sha256_ids)} missing or malformed")
49
+ # Itemize metadata too, so a False `Verified:` line always has a
50
+ # visible reason (report.ok folds in invalid_metadata_ids, #149).
51
+ click.echo(f"Output metadata: {len(report.invalid_metadata_ids)} out of spec")
52
+ click.echo(f"Verified: {report.ok} (asset bytes were not fetched or compared)")
53
+ except Exception as exc:
54
+ raise click.ClickException(f"{type(exc).__name__}: {exc}") from exc
@@ -7,7 +7,7 @@ import click
7
7
 
8
8
 
9
9
  @click.command()
10
- @click.argument("manifest_file", type=click.Path(exists=True, path_type=Path))
10
+ @click.argument("manifest_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
11
11
  @click.option(
12
12
  "--output",
13
13
  "-o",
@@ -17,12 +17,12 @@ import click
17
17
  )
18
18
  def index(manifest_file: Path, output: Path) -> None:
19
19
  """Write manifest data to a Parquet sink for querying."""
20
- from genblaze_core.models.manifest import Manifest
20
+ from genblaze_core.models.manifest import parse_manifest
21
21
  from genblaze_core.sinks.parquet import ParquetSink
22
22
 
23
23
  try:
24
24
  data = json.loads(manifest_file.read_text(encoding="utf-8"))
25
- manifest = Manifest.model_validate(data)
25
+ manifest = parse_manifest(data)
26
26
  except Exception as exc:
27
27
  raise click.ClickException(f"Failed to load manifest: {exc}") from exc
28
28
 
@@ -2,12 +2,16 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
+ from typing import TYPE_CHECKING
5
6
 
6
7
  import click
7
- from genblaze_core.models.enums import PromptVisibility
8
+ from genblaze_core.models.enums import PromptVisibility, RunStatus
8
9
 
10
+ if TYPE_CHECKING:
11
+ from genblaze_core.providers.base import BaseProvider
9
12
 
10
- def _load_provider(provider_name: str, allowed: tuple[str, ...] | None):
13
+
14
+ def _load_provider(provider_name: str, allowed: tuple[str, ...] | None) -> "BaseProvider":
11
15
  """Load a provider by name using entry point discovery.
12
16
 
13
17
  If allowed is set, reject providers not in the allowlist.
@@ -77,7 +81,7 @@ _UNREPLAYABLE_FIELDS = [
77
81
 
78
82
 
79
83
  @click.command()
80
- @click.argument("manifest_file", type=click.Path(exists=True, path_type=Path))
84
+ @click.argument("manifest_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
81
85
  @click.option("--dry-run/--no-dry-run", default=True, help="Preview without executing.")
82
86
  @click.option(
83
87
  "--allow-provider",
@@ -99,7 +103,6 @@ def replay(
99
103
  ) -> None:
100
104
  """Re-execute a pipeline from a manifest JSON file."""
101
105
  from genblaze_core.models.manifest import parse_manifest
102
- from genblaze_core.pipeline import Pipeline
103
106
 
104
107
  try:
105
108
  data = json.loads(manifest_file.read_text(encoding="utf-8"))
@@ -107,14 +110,25 @@ def replay(
107
110
  except Exception as exc:
108
111
  raise click.ClickException(f"Failed to load manifest: {exc}") from exc
109
112
 
110
- # Verify manifest hash integrity
111
- if not force and not manifest.verify():
113
+ # Verify manifest hash integrity before replay. Output sha256 coverage is
114
+ # reported separately because URL-only outputs can be metadata-valid but
115
+ # not sha256-covered.
116
+ report = manifest.verification_report()
117
+ if not force and not report.hash_ok:
112
118
  click.echo(
113
119
  "WARNING: Manifest hash does not match content. The file may have been modified.",
114
120
  err=True,
115
121
  )
116
122
  if not click.confirm("Continue anyway?"):
117
123
  raise click.Abort()
124
+ elif not force:
125
+ if report.unverified_sha256_ids:
126
+ click.echo(
127
+ "WARNING: Manifest hash matches, but output asset bytes are not "
128
+ f"bound for {len(report.unverified_sha256_ids)} asset(s) "
129
+ "missing or malformed sha256.",
130
+ err=True,
131
+ )
118
132
 
119
133
  run = manifest.run
120
134
  _print_summary(run, show_prompts=show_prompts)
@@ -132,9 +146,13 @@ def replay(
132
146
 
133
147
  # Safety: when no allowlist is provided, confirm each unique provider.
134
148
  # A hostile manifest could reference any installed provider and execute
135
- # paid API calls on the user's credentials without consent.
149
+ # paid API calls on the user's credentials without consent. Steps with no
150
+ # provider (INGEST/IMPORT, see the loop below) don't invoke one, so they're
151
+ # excluded here rather than flowing `None` into sorted() (issue #43).
136
152
  if allowed is None:
137
- unique_providers = sorted({step.provider for step in run.steps})
153
+ unique_providers = sorted(
154
+ {step.provider for step in run.steps if step.provider is not None}
155
+ )
138
156
  click.echo(
139
157
  "No --allow-provider allowlist set. Confirm each provider before execution:",
140
158
  err=True,
@@ -153,7 +171,9 @@ def replay(
153
171
  click.echo(" Detected chain mode from manifest step inputs.", err=True)
154
172
 
155
173
  # Cache providers by name to avoid re-instantiating
156
- providers: dict[str, object] = {}
174
+ from genblaze_core.pipeline import Pipeline
175
+
176
+ providers: dict[str, BaseProvider] = {}
157
177
  pipe = Pipeline(
158
178
  run.name,
159
179
  tenant_id=run.tenant_id,
@@ -165,6 +185,17 @@ def replay(
165
185
  _reserved = {"model", "prompt", "modality", "step_type", "seed", "negative_prompt"}
166
186
 
167
187
  for step_idx, step in enumerate(run.steps):
188
+ # A None provider (only valid for INGEST/IMPORT steps, see Step's
189
+ # own validator) has no upstream service to re-invoke here — replay
190
+ # only supports Pipeline.step()-shaped generative steps. Without this
191
+ # guard, None flowed into a dict[str, ...] key and _load_provider's
192
+ # str parameter, raising a confusing TypeError instead (issue #43).
193
+ if step.provider is None:
194
+ raise click.ClickException(
195
+ f"Step {step_idx + 1} (step_type={step.step_type}) has no provider; "
196
+ "replay does not support re-executing provider-less steps "
197
+ "(e.g. INGEST/IMPORT)."
198
+ )
168
199
  if step.provider not in providers:
169
200
  providers[step.provider] = _load_provider(step.provider, allowed)
170
201
 
@@ -202,10 +233,17 @@ def replay(
202
233
  )
203
234
 
204
235
  try:
205
- result = pipe.run()
236
+ result = pipe.run(raise_on_failure=False)
206
237
  click.echo()
238
+ if result.run.status in {RunStatus.FAILED, RunStatus.CANCELLED}:
239
+ click.echo(f"Replay failed. New run ID: {result.run.run_id}", err=True)
240
+ click.echo(f"Hash: {result.manifest.canonical_hash}", err=True)
241
+ click.echo(f"Status: {result.run.status}", err=True)
242
+ raise click.exceptions.Exit(1)
207
243
  click.echo(f"Replay complete. New run ID: {result.run.run_id}")
208
244
  click.echo(f"Hash: {result.manifest.canonical_hash}")
209
245
  click.echo(f"Status: {result.run.status}")
246
+ except click.exceptions.Exit:
247
+ raise
210
248
  except Exception as exc:
211
249
  raise click.ClickException(f"Replay failed: {exc}") from exc
@@ -0,0 +1,56 @@
1
+ """Verify command — check manifest hash and output sha256 coverage."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from genblaze_cli.manifest_io import extract_manifest
8
+
9
+
10
+ @click.command()
11
+ @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
12
+ @click.option(
13
+ "--hash-only",
14
+ is_flag=True,
15
+ help="Only verify canonical_hash; skip output sha256 and asset-metadata checks.",
16
+ )
17
+ def verify(file: Path, hash_only: bool) -> None:
18
+ """Verify an embedded, sidecar, or standalone genblaze manifest."""
19
+ try:
20
+ manifest = extract_manifest(file)
21
+ report = manifest.verification_report()
22
+ if not report.hash_ok:
23
+ click.echo("FAIL: manifest hash mismatch.", err=True)
24
+ raise click.exceptions.Exit(1)
25
+ if hash_only:
26
+ click.echo("OK: manifest hash verified. Asset bytes were not fetched or compared.")
27
+ return
28
+ if report.unverified_sha256_ids:
29
+ click.echo(
30
+ f"FAIL: {len(report.unverified_sha256_ids)} output asset(s) "
31
+ "missing or malformed sha256.",
32
+ err=True,
33
+ )
34
+ raise click.exceptions.Exit(1)
35
+ # Out-of-spec numeric/media_type metadata (e.g. width=0) is tolerated on
36
+ # load by parse_manifest() but fails verify() (#149); surface it here so
37
+ # the CLI verdict matches Manifest.verify()/report.ok instead of a stale
38
+ # "OK" that only checked sha256.
39
+ if report.invalid_metadata_ids:
40
+ click.echo(
41
+ f"FAIL: {len(report.invalid_metadata_ids)} output asset(s) "
42
+ "carry out-of-spec numeric/media_type metadata "
43
+ "(e.g. width=0, or a malformed media_type).",
44
+ err=True,
45
+ )
46
+ raise click.exceptions.Exit(1)
47
+ click.echo(
48
+ "OK: manifest hash verified; all output assets declare sha256 and "
49
+ "carry in-spec metadata. Asset bytes were not fetched or compared."
50
+ )
51
+ except click.exceptions.Exit:
52
+ raise
53
+ except Exception as exc:
54
+ # Prefix with exception type so "PermissionError: ..." and
55
+ # "EmbeddingError: ..." are distinguishable at a glance.
56
+ raise click.ClickException(f"{type(exc).__name__}: {exc}") from exc
@@ -9,7 +9,7 @@ from genblaze_cli.commands.verify import verify
9
9
 
10
10
 
11
11
  @click.group()
12
- @click.version_option(package_name="genblaze-cli")
12
+ @click.version_option(package_name="genblaze-cli", prog_name="genblaze-cli")
13
13
  def cli() -> None:
14
14
  """genblaze — media generation manifest toolkit."""
15
15
 
@@ -0,0 +1,42 @@
1
+ """Shared CLI manifest loading helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from genblaze_core._utils import MAX_MANIFEST_BYTES
9
+ from genblaze_core.exceptions import EmbeddingError
10
+ from genblaze_core.media import get_handler, guess_mime
11
+ from genblaze_core.media.sidecar import PointerSidecarError, SidecarHandler
12
+ from genblaze_core.models.manifest import Manifest, parse_manifest
13
+
14
+
15
+ def load_standalone_json_manifest(file: Path) -> Manifest:
16
+ size = file.stat().st_size
17
+ if size > MAX_MANIFEST_BYTES:
18
+ raise EmbeddingError(
19
+ f"Manifest JSON exceeds size limit: {size} > {MAX_MANIFEST_BYTES} bytes"
20
+ )
21
+ data = json.loads(file.read_text(encoding="utf-8"))
22
+ if "run" not in data and "manifest_uri" in data:
23
+ raise PointerSidecarError(
24
+ manifest_uri=data["manifest_uri"],
25
+ canonical_hash=data.get("canonical_hash", ""),
26
+ )
27
+ return parse_manifest(data)
28
+
29
+
30
+ def extract_manifest(file: Path) -> Manifest:
31
+ """Extract a manifest from standalone JSON, media, or sidecar files."""
32
+ if file.suffix.lower() == ".json":
33
+ return load_standalone_json_manifest(file)
34
+
35
+ mime = guess_mime(file)
36
+ handler = get_handler(mime)
37
+ if handler is not None:
38
+ try:
39
+ return handler.extract(file)
40
+ except EmbeddingError:
41
+ pass
42
+ return SidecarHandler().extract(file)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "genblaze-cli"
7
- version = "0.3.0"
7
+ version = "0.3.4"
8
8
  description = "CLI for genblaze manifest operations"
9
9
  authors = [{name = "Jeronimo De Leon", email = "jdeleon@backblaze.com"}]
10
10
  readme = "README.md"
@@ -23,7 +23,7 @@ classifiers = [
23
23
  keywords = ["genblaze", "ai", "media", "manifest", "provenance", "c2pa-ready", "genai", "pipeline", "cli", "command-line"]
24
24
 
25
25
  dependencies = [
26
- "genblaze-core>=0.2.0,<0.4",
26
+ "genblaze-core>=0.3.6,<0.4",
27
27
  "click>=8.0",
28
28
  ]
29
29
 
@@ -48,3 +48,8 @@ packages = ["genblaze_cli"]
48
48
 
49
49
  [tool.pytest.ini_options]
50
50
  testpaths = ["tests"]
51
+
52
+ [tool.deptry]
53
+ # Treat the `dev` extra as dev-only tooling so deptry does not flag pytest
54
+ # (and other test-only deps) as unused declared deps (DEP002).
55
+ optional_dependencies_dev_groups = ["dev"]