Tooling
CLI
snapapi path runs the file (or every .sapi under a directory).
snapapi examples/hello/hello.sapi
python -m snapapi examples/hello/hello.sapi
snapapi tests/
run (default)
| Flag | Meaning |
|---|---|
-k "Login or not Health" | Filter by name, description, and tags. and / or / not and parentheses. Adjacent words are AND. |
-m "smoke and not slow" | Filter by tag expression (and / or / not). |
--tag user | Run tests that have this tag (repeatable; all given tags must match). |
--exclude slow | Skip tests with this tag (repeatable). |
--name "Create User" | Run tests with this name (repeatable). |
--grep regex | Filter tests by name or description. |
-D TOKEN=secret / --variable | Set ${VAR} without an env file. Repeatable; overrides --profile. |
--env .env | Load KEY=VALUE pairs for ${VAR}. If omitted: <suite>.env, then .env beside the suite, then a single sibling *.env. |
--profile stage | Load environments/stage.env, .snapapi/stage.env, or stage.env. |
--timeout 10 | HTTP timeout in seconds (overrides suite TIMEOUT). |
-x / --exitfirst / --stop-on-failure | Stop each suite on the first failed test. Off by default: remaining tests keep running after a failure. |
--maxfail N | Stop after N failures. |
--lf / --last-failed | Re-run failures from .snapapi/last-run.json (file + suite + name, including Test [row]). |
--ff / --failed-first | Run last-failed tests first, then the rest. |
--collect-only / --co / --dry-run | Print selected tests without making HTTP calls. |
-q / -v | Quiet (summary only) or verbose. Repeatable. |
--durations N | Show the N slowest tests after the run (0 = all). |
--color yes|no|auto / --no-color | Force or disable ANSI color. |
--workers N | Run independent tests in parallel (threads in one process). SAVE is isolated per test; sibling SAVE or any DEPENDS falls back to sequential. DEPENDS also pulls named tests forward (file order otherwise). Plugin functions must be thread-safe: module-level mutable state is shared. SnapAPI variables are not. See CALL. |
--reruns N | Re-run failed tests up to N times (distinct from EXPECT RETRY). |
--include-skipped / --include-quarantine | Run tests marked SKIP or QUARANTINE. |
--mode record|replay|record-on-miss | VCR cassettes under .snapapi/cassettes/. |
--record-on-miss | With replay, hit the network and save when a cassette is missing. |
--vcr-match query,body,accept,authorization | Cassette identity fields (default: query, content-type, accept, body). |
--contract-strict | Fail when an OpenAPI path/method/schema is missing (default: skip/warn). |
--report json:file / junit: / html: | Write reports. Repeatable. |
--on-fail curl / --on-fail har:dir | Emit a redacted curl or HAR on failure. |
--no-dump | Do not print request/response on failure. |
--safe-url | Block private/metadata hosts. |
--allow-private-urls | Override --safe-url. |
--proxy URL | HTTP/HTTPS proxy. |
--insecure | Skip TLS certificate verification. |
--cert PATH / --cacert PATH | Client certificate and CA bundle. |
--listener PATH[:Class] | Python listener called after each test / suite (repeatable). |
--plugin PATH[:name] | Extra Python extension (repeatable). Prefer extensions/ or snapapi.yaml. See CALL. |
-V / --version | Print the SnapAPI version. |
lint
Parse and validate without making HTTP calls.
snapapi lint tests/
snapapi lint users.sapi --strict
--strict treats unused SAVE as errors. --env, --profile, and --plugin are available so interpolation can be checked. Exit 2 if any issue is an error.
fmt
snapapi fmt tests/
snapapi fmt users.sapi --check
Rewrites .sapi files. --check exits 1 if a file would change.
openapi
Generate GET/POST/PUT/PATCH/DELETE smoke tests from an OpenAPI spec.
snapapi openapi spec.yaml --base-url https://api.example.com -o smoke.sapi
https://api.example.com is a placeholder — put the origin from your spec.
Without -o, the suite is printed to stdout.
convert
Turn existing curl into a .sapi suite. Intent is mapped when possible (Authorization: Bearer → AUTH: bearer, query string → QUERY, JSON -d → BODY). Unsupported curl features print a review warning on stderr instead of silently inventing DSL.
snapapi convert 'curl https://api.example.com/users -H "Authorization: Bearer $TOKEN"'
snapapi convert request.sh -o users.sapi
snapapi convert --clipboard -o pasted.sapi
snapapi convert request.sh --suite Users --expect 200 -q
Pass a curl string, a shell file with one or more curl commands, or --clipboard. Without -o, the suite goes to stdout. Exit 2 on parse/usage errors.
history
snapapi history
snapapi history --failed --since 7d
Reads .snapapi/history.jsonl. --since accepts values like 7d, 24h, or 30m.
mock
snapapi mock tests/fixtures/mock.json --port 0
Serves routes from a JSON file and prints the URL. Routes may use path templates (/users/{id}), optional match.query / match.body subsets, and delay_ms. Exact paths win over templates.
watch
snapapi watch tests/ --interval 0.5
snapapi watch tests/ --tag users
Re-runs when .sapi or .snaptest files change (poll). Install pip install -e ".[watch]" for a watchdog observer. All run flags are accepted.
Listeners
A listener is a Python class (or module) with optional methods. SnapAPI calls the ones you implement. Listener exceptions are warnings; they do not change the suite result. Use listeners to push results to a TMS, Slack, or any webhook — SnapAPI stays tool-agnostic.
snapapi tests/ --listener reporters/jira.py:JiraReporter
snapapi tests/ --listener examples/print_listener.py:PrintListener --report html:report.html
export SNAPAPI_WEBHOOK_URL=https://example.com/hooks/snapapi
snapapi tests/ --listener examples/webhook_listener.py:WebhookListener
Contract
Implement only the hooks you need. Arguments:
class Reporter:
def start_suite(self, suite):
... # suite["name"], suite["source"]
def start_test(self, suite, name, tags):
...
def end_test(self, suite, test):
... # TestResult: name, status (passed|failed|skipped),
# tags, duration_ms, error, requests
# test.to_dict() → JSON-safe (redacted bodies/headers)
def end_suite(self, result):
... # SuiteResult: name, source, ok, passed, failed, skipped
# result.to_dict()
def report_written(self, kind, path):
... # kind is html, json, or junit
def close(self):
...
From Python, pass instances to run_suites(..., listeners=[Reporter()], close_listeners=True). The CLI writes reports, then calls report_written, then close.
See examples/print_listener.py (stdout) and examples/webhook_listener.py (POST JSON to SNAPAPI_WEBHOOK_URL). Map fields on the receiving side for Qase, Jira, Slack, or an internal collector.
Listeners wrap the run. They don’t compute a header on this POST. That’s CALL.
From Python
Call the same runner the CLI uses:
from snapapi.cli import run_suites
results = run_suites(["tests/foo.sapi"])
assert all(suite.ok for suite in results)
run_suites accepts the same options as snapapi run (tags, names, env file, timeout, workers, VCR mode, plugins=, and the rest) as keyword arguments. It returns a list of suite results.