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)

FlagMeaning
-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 userRun tests that have this tag (repeatable; all given tags must match).
--exclude slowSkip tests with this tag (repeatable).
--name "Create User"Run tests with this name (repeatable).
--grep regexFilter tests by name or description.
-D TOKEN=secret / --variableSet ${VAR} without an env file. Repeatable; overrides --profile.
--env .envLoad KEY=VALUE pairs for ${VAR}. If omitted: <suite>.env, then .env beside the suite, then a single sibling *.env.
--profile stageLoad environments/stage.env, .snapapi/stage.env, or stage.env.
--timeout 10HTTP timeout in seconds (overrides suite TIMEOUT).
-x / --exitfirst / --stop-on-failureStop each suite on the first failed test. Off by default: remaining tests keep running after a failure.
--maxfail NStop after N failures.
--lf / --last-failedRe-run failures from .snapapi/last-run.json (file + suite + name, including Test [row]).
--ff / --failed-firstRun last-failed tests first, then the rest.
--collect-only / --co / --dry-runPrint selected tests without making HTTP calls.
-q / -vQuiet (summary only) or verbose. Repeatable.
--durations NShow the N slowest tests after the run (0 = all).
--color yes|no|auto / --no-colorForce or disable ANSI color.
--workers NRun 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 NRe-run failed tests up to N times (distinct from EXPECT RETRY).
--include-skipped / --include-quarantineRun tests marked SKIP or QUARANTINE.
--mode record|replay|record-on-missVCR cassettes under .snapapi/cassettes/.
--record-on-missWith replay, hit the network and save when a cassette is missing.
--vcr-match query,body,accept,authorizationCassette identity fields (default: query, content-type, accept, body).
--contract-strictFail 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:dirEmit a redacted curl or HAR on failure.
--no-dumpDo not print request/response on failure.
--safe-urlBlock private/metadata hosts.
--allow-private-urlsOverride --safe-url.
--proxy URLHTTP/HTTPS proxy.
--insecureSkip TLS certificate verification.
--cert PATH / --cacert PATHClient 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 / --versionPrint 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.