CLI ยท commit a16563e

restbook

Plain-text HTTP request files runner with chaining, assertions & reports

Download restbook.zip
Quickstart
unzip restbook.zip && cd restbook
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

restbook

Keep your API requests in version-controlled .http files and run them from the terminal and CI. restbook understands the format popularised by IDE REST clients and adds what CI needs: environments, request chaining (use the token from login in later requests), a small safe assertion language, concurrency, curl/HAR conversion and JUnit/JSON/Markdown reports with meaningful exit codes.

Features

Install

cd restbook
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate     # provides the `restbook` command

Requires Python 3.12+.

Quickstart

# terminal 1: the bundled demo API
python examples/api/server.py 8184

# terminal 2
cd examples/requests
restbook run auth.http notes.http features.http --env dev -j 4

Usage

Writing a request file

@contentType = application/json

### Login
# @name login
POST {{baseUrl}}/login
Content-Type: {{contentType}}

{"username": "{{username}}", "password": "{{password}}"}

?? status == 200
?? body.$.token matches /^[0-9a-f]{32}$/

### Who am I
# @name me
GET {{baseUrl}}/me
Authorization: Bearer {{login.response.body.$.token}}

?? status == 200
?? body.$.name == {{username}}

Reference: docs/file-format.md, docs/assertions.md.

Running

$ restbook run auth.http --env dev --only login,me
PASS login POST http://127.0.0.1:8184/login  200 2 ms 101 B
  ok   status == 200
  ok   body.$.token matches /^[0-9a-f]{32}$/
  ok   body.$.user.roles.length == 2
  ok   body.$.user.roles[0] == "admin"
  ok   header set-cookie contains session=
{
  "token": "7f036c0481fec956a3aa7d6ada59e5d5",
  "user": {
    "name": "demo",
    "roles": [
      "admin",
      "editor"
    ]
  }
}
PASS me GET http://127.0.0.1:8184/me  200 1 ms 16 B
  ok   status == 200
  ok   body.$.name == demo
{
  "name": "demo"
}

2 passed, 0 failed, 0 skipped in 2 ms

--only accepts comma-separated names, tags and globs (--only 'login,users*,crud'). Requests that a selected request depends on are added automatically.

All three example files, four at a time:

$ restbook run auth.http notes.http features.http --env dev --body-lines 0 -j 4 --no-save | tail -3
  ok   duration < 3000

24 passed, 0 failed, 1 skipped in 433 ms

Failures and exit codes

$ restbook run health.http --no-save
FAIL h GET http://127.0.0.1:8184/health  200 1 ms 16 B
  fail status == 201 (line 4)  (got 200)
  ok   body.$.status == "ok"
...
0 passed, 1 failed, 0 skipped in 1 ms
$ echo $?
1
Exit code Meaning
0 every request passed (skipped ones do not count as failures)
1 at least one failed assertion or request error (connection, timeout, unresolved reference)
2 usage, parse, environment or dependency-cycle error; nothing was sent

Parse and dependency errors point at the problem:

$ restbook run bad.http
parse error: bad.http:2: invalid header line 'bad header' (expected 'Name: value'; a blank line must precede the body)
$ restbook run cy.http
error: dependency cycle: a -> b -> a

Dry run (secrets masked)

$ restbook run notes.http --env dev --only create --dry-run
### login
POST http://127.0.0.1:8184/login
Content-Type: application/json

{"username": "demo", "password": "****"}

### create
POST http://127.0.0.1:8184/notes
Authorization: ****
Content-Type: application/json

{
  "title": "Release checklist 990",
  "body": "created at 2026-10-16T16:22:17.602Z, due 2026-10-17T16:22:17.602Z",
  "tags": ["ops", "7fce37dd-fa58-46cd-8231-4796198b7c68"]
}

curl export and import

$ restbook curl notes.http login --env dev --mask
curl -X POST http://127.0.0.1:8184/login -L -H 'Content-Type: application/json' --data-raw '{"username": "demo", "password": "****"}'

$ restbook import-curl "curl -X POST https://api.example.com/v1/items -H 'Content-Type: application/json' -H 'Authorization: Bearer abc' -d '{\"name\":\"x\"}' -L"
### post-v1-items
POST https://api.example.com/v1/items
Content-Type: application/json
Authorization: Bearer abc

{"name":"x"}

$ restbook import-har capture.har -o requests.http

Responses to earlier requests cannot be known without sending, so restbook curl leaves {{login.response...}} placeholders in place.

Reports

restbook run notes.http --env dev --junit report.xml --json report.json --markdown report.md

Responses are saved as .restbook/responses/<name>.json (change with --out-dir, disable with --no-save), containing the masked request and the full response with parsed JSON body.

run options

Option Purpose
--env NAME environment from http-client.env.json (+ private file)
--var K=V override a variable (beats file and env values)
--only LIST names, tags or globs
-j N run independent requests concurrently
--dry-run print resolved requests, send nothing
--insecure, --proxy URL TLS verification off, proxy
--retries N retry transport errors and 502/503/504 with backoff
--timeout S, --no-redirect defaults; per-request @timeout / @no-redirect win
--junit F, --json F, --markdown F reports
--body-lines N response lines shown (0 hides bodies)

Configuration

There is no global config file. Behaviour comes from the command line, the .http file and the environment files described in docs/file-format.md. The example http-client.private.env.json contains only the demo API's password; real projects should git-ignore it.

Architecture

Module (src/restbook/) Role
parser.py .http text to ParsedFile/Request, line-accurate errors
scope.py variable resolution, response references, dependency graph, cycle detection, topological order
dynamic.py $uuid, $timestamp, $randomInt, $datetime
jsonpath.py JSONPath subset
assertions.py assertion DSL parse and evaluation, JSON Schema check
build.py resolved request: bodies, multipart, GraphQL, file includes
envfiles.py, masking.py environment files; secret masking
runner.py selection, httpx execution, retries, concurrency
interop.py curl export/import, HAR import, .http rendering
report.py JUnit, JSON, Markdown, saved responses
cli.py click commands

Design notes with worked examples: docs/design.md. Demo API: examples/api/server.py (stdlib: login/token, notes CRUD, pagination, slow endpoint, redirect, cookies, form, multipart upload, GraphQL echo).

Tests

.venv/bin/pytest -q

Covers the parser (including error line numbers), variables, dynamic values, dependency ordering and cycles, JSONPath, assertions and schemas, multipart/file includes, curl/HAR round trips, end-to-end runs against the demo API (sequential and concurrent), JUnit validity and CLI exit codes.

Limitations