A CLI tool to call REST APIs defined in OpenAPI 3.0 specs
Project description
papycli — Python CLI for OpenAPI 3.0 REST APIs
papycli is an interactive CLI that reads OpenAPI 3.0 specs and lets you call REST API endpoints directly from the terminal.
Features
- Auto-generates a CLI from any OpenAPI 3.0 spec
- Shell completion for bash and zsh
- Register and switch between multiple APIs
- Inspect API specs with
papycli spec - Validate request parameters before sending with
--check/--check-strict - Validate response body and status code against the OpenAPI spec with
--response-check - Automatically coerces
-pvalues to the correct JSON type (integer, number, boolean) based on the API spec - Log requests and responses to a file with
papycli config log - Extend request processing with request filter plugins (
papycli.request_filtersentry point) - Inspect and transform responses with response filter plugins (
papycli.response_filtersentry point)
Requirements
| Item | Notes |
|---|---|
| Python | 3.12 or later |
Installation
pip install papycli
Enable Shell Completion
bash:
# Add to ~/.bashrc or ~/.bash_profile
eval "$(papycli config completion-script bash)"
zsh:
# Add to ~/.zshrc
eval "$(papycli config completion-script zsh)"
Git Bash (Windows):
Git Bash uses MSYS path conversion, which can mangle the output of $() command substitution.
Disable it before running the eval command:
# Add to ~/.bashrc or ~/.bash_profile
export MSYS_NO_PATHCONV=1
eval "$(papycli config completion-script bash)"
Restart your shell or run source ~/.bashrc / source ~/.zshrc to apply.
Quick Start — Petstore Demo
This repository includes a demo using the Swagger Petstore.
1. Start the Petstore server
docker compose -f examples/docker-compose.yml up -d
The API will be available at http://localhost:8080/api/v3/.
2. Register the API
papycli config add examples/petstore-oas3.json
3. Try some commands
# List available endpoints
papycli summary
# GET /store/inventory
papycli get /store/inventory
# GET with a path parameter
papycli get /pet/99
# GET with a query parameter
papycli get /pet/findByStatus -q status available
# POST with body parameters
papycli post /pet -p name "My Dog" -p status available -p photoUrls "http://example.com/photo.jpg"
# POST with a raw JSON body
papycli post /pet -d '{"name": "My Dog", "status": "available", "photoUrls": ["http://example.com/photo.jpg"]}'
# Array parameter (repeat the same key)
papycli put /pet -p id 1 -p name "My Dog" -p photoUrls "http://example.com/a.jpg" -p photoUrls "http://example.com/b.jpg" -p status available
# Nested object (dot notation)
papycli put /pet -p id 1 -p name "My Dog" -p category.id 2 -p category.name "Dogs" -p photoUrls "http://example.com/photo.jpg" -p status available
# DELETE /pet/{petId}
papycli delete /pet/1
4. Tab completion
Once shell completion is enabled, tab completion is available:
$ papycli <TAB>
get post put patch delete config spec summary
$ papycli get <TAB>
/pet/findByStatus /pet/{petId} /store/inventory ...
$ papycli get /pet/findByStatus <TAB>
-q -p -H -d --summary --verbose --check --check-strict --response-check
$ papycli get /pet/findByStatus -q <TAB>
status
$ papycli get /pet/findByStatus -q status <TAB>
available pending sold
$ papycli post /pet -p <TAB>
name* photoUrls* status
$ papycli post /pet -p status <TAB>
available pending sold
Adding Your Own API
Step 1 — Run config add
papycli config add your-api-spec.json
This command will:
- Resolve all
$refreferences in the OpenAPI spec - Convert the spec to papycli's internal API definition format
- Save the result to
$PAPYCLI_CONF_DIR/apis/<name>.json - Create or update
$PAPYCLI_CONF_DIR/papycli.conf
The API name is derived from the filename (e.g. your-api-spec.json → your-api-spec).
Step 2 — Set the base URL
If the spec contains servers[0].url, it is used automatically. Otherwise, edit $PAPYCLI_CONF_DIR/papycli.conf and set the url field:
{
"default": "your-api-spec",
"your-api-spec": {
"openapispec": "your-api-spec.json",
"apidef": "your-api-spec.json",
"url": "https://your-api-base-url/"
}
}
Managing Multiple APIs
# Register multiple APIs
papycli config add petstore-oas3.json
papycli config add myapi.json
# Switch the active API
papycli config use myapi
# Remove a registered API
papycli config remove petstore-oas3
# Show registered APIs and the current default
papycli config list
Reference
# Configuration management commands
papycli config add <spec-file> Register an API from an OpenAPI spec file
papycli config remove <api-name> Remove a registered API
papycli config use <api-name> Switch the active API
papycli config list List registered APIs and current configuration
papycli config log Show the current log file path
papycli config log <path> Set the log file path
papycli config log --unset Disable logging
papycli config completion-script <bash|zsh> Print a shell completion script
# Inspection commands
papycli spec [resource] Show the raw internal API spec (filter by resource path)
papycli spec --full [resource] Output the stored OpenAPI spec (filter by resource path if given)
papycli summary [resource] List available endpoints (filter by resource prefix)
Required params marked with *, array params with []
papycli summary --csv Output endpoints in CSV format
# API call commands
papycli <method> <resource> [options]
Methods:
get | post | put | patch | delete
Options:
-H <header: value> Custom HTTP header (repeatable)
-q <name> <value> Query parameter (repeatable).
You can also embed query parameters directly in the
resource path: "/pet/findByStatus?status=available"
Inline parameters are sent before any -q parameters.
-p <name> <value> Body parameter (repeatable)
- Values are coerced to the correct JSON type
(integer, number, boolean) based on the API spec.
Strings are passed as-is.
- Repeat the same key to build a JSON array:
-p tags foo -p tags bar → {"tags":["foo","bar"]}
- Use dot notation to build a nested object:
-p category.id 1 -p category.name Dogs
→ {"category":{"id":"1","name":"Dogs"}}
-d <json> Raw JSON body (overrides -p)
--summary Show endpoint info without sending a request
--check Validate params before sending (warn on stderr, request is still sent)
--check-strict Validate params before sending (warn on stderr, abort with exit 1 on failure)
--response-check Validate response status code and body against the OpenAPI spec
(warn on stderr; violations do not affect exit code)
--verbose / -v Show HTTP status line
--version Show version
--help / -h Show help
Environment variables:
PAPYCLI_CONF_DIR Path to the config directory (default: ~/.papycli)
PAPYCLI_CUSTOM_HEADER Custom HTTP headers applied to every request.
Separate multiple headers with newlines:
export PAPYCLI_CUSTOM_HEADER=$'Authorization: Bearer token\nX-Tenant: acme'
Request Filter Plugins
You can intercept and transform outgoing requests by writing a request filter plugin.
A filter is a callable that receives a RequestContext and returns a modified RequestContext:
# my_plugin.py
from papycli.filters import RequestContext
def request_filter(ctx: RequestContext) -> RequestContext:
ctx.headers["X-Request-ID"] = "my-id"
return ctx
Register it in your package's pyproject.toml:
[project.entry-points."papycli.request_filters"]
my-filter = "my_plugin:request_filter"
Install the package and filters are applied automatically on every request, sorted by plugin name.
RequestContext fields:
| Field | Type | Description |
|---|---|---|
method |
str |
HTTP method (lowercase). Do not modify. |
url |
str |
Full URL with path parameters expanded. |
query_params |
list[tuple[str, str]] |
Query parameters. |
body |
dict | list | str | int | float | bool | None |
JSON request body. |
headers |
dict[str, str] |
Custom HTTP headers. |
Response Filter Plugins
You can inspect and transform incoming responses by writing a response filter plugin.
A filter is a callable that receives a ResponseContext and returns a modified ResponseContext, or None to suppress the response output and stop the filter chain:
# my_plugin.py
from papycli.filters import ResponseContext
def response_filter(ctx: ResponseContext) -> ResponseContext | None:
if isinstance(ctx.body, dict):
ctx.body["_status"] = ctx.status_code
return ctx
Returning None suppresses the response output entirely and prevents any subsequent filters from running — useful for silencing responses that match certain criteria.
Register it in your package's pyproject.toml:
[project.entry-points."papycli.response_filters"]
my-filter = "my_plugin:response_filter"
Install the package and the filters are applied automatically after every response, sorted by plugin name.
ResponseContext fields:
| Field | Type | Description |
|---|---|---|
method |
str |
HTTP method used for the request (lowercase). |
url |
str |
Full URL of the request. |
status_code |
int |
HTTP response status code. |
reason |
str |
HTTP response reason phrase (e.g. "OK", "Not Found"). |
headers |
dict[str, str] |
Response headers. |
body |
dict | list | str | int | float | bool | None |
Parsed response body. Modify this field to replace the response body. |
request_body |
dict | list | str | int | float | bool | None |
Request body sent to the server (read-only). None for requests without a body. |
Limitations
- Request bodies are
application/jsononly - Array parameters support scalar element types only (arrays of objects are not supported)
- Dot notation for nested objects supports one level of nesting only
- Pass auth headers via
-H "Authorization: Bearer token"or thePAPYCLI_CUSTOM_HEADERenv var
Development
git clone https://github.com/tmonj1/papycli.git
cd papycli
pip install -e ".[dev]"
Running Tests
These commands assume uv is installed. If you set up the environment with pip instead, omit the uv run prefix and run pytest directly.
Unit tests:
uv run pytest
Integration tests:
The integration tests require the papycli binary to be present in .venv/bin/. Run uv sync first:
uv sync
uv run pytest -m integration --override-ini addopts= tests/integration/
Run all tests at once:
uv sync
uv run pytest --override-ini addopts=
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file papycli-0.13.0.tar.gz.
File metadata
- Download URL: papycli-0.13.0.tar.gz
- Upload date:
- Size: 140.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7aff2501cde25812d41ab1ceab6f4cfab2504a9bf26868aa23799b70c0ded9ac
|
|
| MD5 |
730a3a193bb30a0330c9de23560814a8
|
|
| BLAKE2b-256 |
41ccbd3344c8ec54c1008b644c534f140a2a4cbd47ee19ac5d3bcee67f5410d9
|
Provenance
The following attestation bundles were made for papycli-0.13.0.tar.gz:
Publisher:
release-please.yml on tmonj1/papycli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
papycli-0.13.0.tar.gz -
Subject digest:
7aff2501cde25812d41ab1ceab6f4cfab2504a9bf26868aa23799b70c0ded9ac - Sigstore transparency entry: 1169762399
- Sigstore integration time:
-
Permalink:
tmonj1/papycli@de91b704827eff7550159e580ab9bc1d4e19a94f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tmonj1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@de91b704827eff7550159e580ab9bc1d4e19a94f -
Trigger Event:
push
-
Statement type:
File details
Details for the file papycli-0.13.0-py3-none-any.whl.
File metadata
- Download URL: papycli-0.13.0-py3-none-any.whl
- Upload date:
- Size: 42.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e47c5b87c4f03551febbb72be6c79678bd3cd7ceaeb96e5fe80a9b4ec10fb30f
|
|
| MD5 |
9345cb001d579c586492b3d7213beec9
|
|
| BLAKE2b-256 |
f9b09a19ce6d7444e2af792329da42c18526a9f7c74d53b549bf6a875aee4551
|
Provenance
The following attestation bundles were made for papycli-0.13.0-py3-none-any.whl:
Publisher:
release-please.yml on tmonj1/papycli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
papycli-0.13.0-py3-none-any.whl -
Subject digest:
e47c5b87c4f03551febbb72be6c79678bd3cd7ceaeb96e5fe80a9b4ec10fb30f - Sigstore transparency entry: 1169762459
- Sigstore integration time:
-
Permalink:
tmonj1/papycli@de91b704827eff7550159e580ab9bc1d4e19a94f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tmonj1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@de91b704827eff7550159e580ab9bc1d4e19a94f -
Trigger Event:
push
-
Statement type: