Running it in CI¶
validate exits 1 when it finds an error and 0 otherwise, which is the whole
integration story. Everything below is about where to put it.
As a gate in front of a batch¶
The case this was written for: a job that builds several hundred requests and submits them. Validate the whole set first, so a malformed field fails in a second rather than after two hundred paid renders.
#!/usr/bin/env bash
set -euo pipefail
fail=0
for f in requests/*.json; do
if ! h3check validate "$f" > "/tmp/$(basename "$f").report"; then
echo "--- $f"
cat "/tmp/$(basename "$f").report"
fail=1
fi
done
exit "$fail"
set -e would abort on the first failure, which is why the call is wrapped in if. You
want the whole list, not the first item.
GitHub Actions¶
name: validate render requests
on:
pull_request:
paths:
- 'requests/**.json'
jobs:
h3check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install h3check
- name: Validate
run: |
for f in requests/*.json; do
echo "::group::$f"
h3check validate "$f" || echo "::error file=$f::request rejected"
echo "::endgroup::"
done
pip install h3check pulls nothing else in — there are no dependencies, so the step costs
a second and needs no cache. If you would rather vendor than install, copy the module
and h3_spec.json into the same directory and call python3 vendor/h3check.py; the
spec is loaded relative to the module file, not the working directory.
pre-commit¶
- repo: local
hooks:
- id: h3check
name: h3check
entry: h3check validate
language: python
additional_dependencies: ["h3check"]
files: ^requests/.*\.json$
pre-commit passes matched filenames as arguments. validate takes exactly one path, so
this works as written only because the hook is invoked per file by default; if you set
pass_filenames: false you will need the loop from the first example.
Reading requests from stdin¶
When requests are generated rather than stored, skip the temporary file:
python3 build_request.py --shot 14 | h3check validate -
Exit code behaves identically. This is also the form to use inside a Python job, since the validator is importable:
import json, subprocess, sys
from h3check import validate
report = validate(request_dict)
if not report.ok:
print(report.render(), file=sys.stderr)
sys.exit(1)
validate() returns a Report dataclass with errors, warnings and notes lists, an
ok property, and a render() method that produces the indented text block the CLI
prints. Using the dataclass directly is the cleanest way to make your own decisions about
warnings.
Treating warnings as fatal¶
Warnings deliberately do not affect the exit code — see Errors, warnings and notes for why. If your pipeline disagrees, do not patch the tool; make the decision at the call site:
from h3check import validate
report = validate(request_dict)
if report.errors or report.warnings:
raise SystemExit(report.render())
From a shell, grep the output rather than the exit code:
out=$(h3check validate request.json) || true
printf '%s\n' "$out"
printf '%s' "$out" | grep -q 'WARNING' && exit 1
The two warnings that exist today — ref2va with no references, and an attempt to switch
audio off — are both cases where the request succeeds and quietly does something other
than what you meant, so a strict pipeline treating them as fatal is a defensible choice.
Cost estimation in the same job¶
cost always exits 0, so it composes freely:
h3check cost --duration 8 --resolution 2K | tail -1
# total: $1.0400
For a whole batch, import the function instead of shelling out per request:
from h3check import snap, cost
total = 0.0
for req in requests:
_, _, rendered = snap(req["duration"])
total += cost(rendered, req["resolution"])
print(f"batch total: ${total:.2f}")
Note snap() in that loop. Billing is per rendered second, and only an 8-second request
renders the length you asked for; summing the requested durations will under-count.