Skip to main content

A CLI tool to call REST APIs defined in OpenAPI 3.0 specs

Project description

papycli — Turn Any OpenAPI 3.0 Spec into a CLI

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 -p values 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_filters entry point)
  • Inspect and transform responses with response filter plugins (papycli.response_filters entry 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/petstore/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/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:

  1. Resolve all $ref references in the OpenAPI spec
  2. Convert the spec to papycli's internal API definition format
  3. Save the result to $PAPYCLI_CONF_DIR/apis/<name>.json
  4. Create or update $PAPYCLI_CONF_DIR/papycli.conf

The API name is derived from the filename (e.g. your-api-spec.jsonyour-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

Creating a Named CLI for a Specific API

The --api option lets you target a specific registered API without switching the default:

papycli --api petstore-oas3 get /pet/1

Combined with a shell alias (or function) and a dedicated completion script, you can expose each API as a lightweight standalone CLI with full tab completion:

bash — add to ~/.bashrc:

eval "$(papycli config completion-script --api petstore-oas3 bash)"
alias petstore-oas3='papycli --api petstore-oas3'

zsh — add to ~/.zshrc:

eval "$(papycli config completion-script --api petstore-oas3 zsh)"
alias petstore-oas3='papycli --api petstore-oas3'

After sourcing your shell config, the alias acts as a standalone CLI:

$ petstore-oas3 <TAB>
  get  post  put  patch  delete  config  spec  summary

$ petstore-oas3 get <TAB>
  /pet/findByStatus  /pet/{petId}  /store/inventory  ...

Note (bash): If you prefer a shell function instead of an alias, note that bash function names cannot contain hyphens. In that case, use a hyphen-free API name (e.g. register as petstore and define petstore() { papycli --api petstore "$@"; }).


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
papycli config completion-script --api <api-name> <bash|zsh>  Print a completion script for the named CLI

# 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'
  PAPYCLI_DISABLE_DOTENV   Set to 1 to disable automatic .env file loading.
                             By default papycli loads .env from the current directory
                             and $PAPYCLI_CONF_DIR on startup. Disable this when running
                             in untrusted directories to prevent unintended env injection:
                             export PAPYCLI_DISABLE_DOTENV=1

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.
spec dict | None The matched operation spec entry from the API definition (read-only). None if not resolvable.

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.
schema dict | None The resolved OpenAPI Response Object for the matched status code (read-only). None if no matching definition exists.

Limitations

  • Request bodies are application/json only
  • Array parameters support scalar element types only (arrays of objects are not supported)

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 [--cov] [--cov-branch]

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

papycli-0.18.1.tar.gz (188.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

papycli-0.18.1-py3-none-any.whl (46.9 kB view details)

Uploaded Python 3

File details

Details for the file papycli-0.18.1.tar.gz.

File metadata

  • Download URL: papycli-0.18.1.tar.gz
  • Upload date:
  • Size: 188.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for papycli-0.18.1.tar.gz
Algorithm Hash digest
SHA256 033c8a777860ad4bec7a402a90635c9560ba4f7f71127dbf279f2194f6e2973b
MD5 97c7298fa8f9698847148cdf6ca38ff6
BLAKE2b-256 72ecb3619bd14056787e2764f5404628bccab25c958a046f978edd1f26fb5f1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for papycli-0.18.1.tar.gz:

Publisher: release-please.yml on tmonj1/papycli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file papycli-0.18.1-py3-none-any.whl.

File metadata

  • Download URL: papycli-0.18.1-py3-none-any.whl
  • Upload date:
  • Size: 46.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for papycli-0.18.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5f4f85bc268e54f085670b5bf304ed679fff9eeef9e5243abecf2b2d4d7bfa26
MD5 c48d3b25a03e3e35221cde06318b9c62
BLAKE2b-256 950185412559b1e44a32d3c6acb762b832cdf74eb8f55f84e0ac8bdbf0fb2ebb

See more details on using hashes here.

Provenance

The following attestation bundles were made for papycli-0.18.1-py3-none-any.whl:

Publisher: release-please.yml on tmonj1/papycli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page