Python API CLI Clients¶
Use this skill as a conceptual reference for command-line applications that operate remote services. Preserve established project conventions, but make the command surface predictable for people, scripts, CI jobs, and shell composition.
Design Priorities¶
A good API CLI should be:
- Task-oriented: commands reflect user goals rather than HTTP endpoints or internal service classes.
- Predictable: names, flags, defaults, output, errors, and exit statuses behave consistently.
- Composable: successful data goes to stdout, diagnostics go to stderr, and machine-readable output is stable.
- Safe: destructive operations are explicit, retries respect operation semantics, and secrets never enter output or logs.
- Layered: command parsing, application behavior, API resources, transport, authentication, and persistence have distinct owners.
- Inspectable: users can discover commands and effective configuration without reading source code.
Use the Command Line Interface Guidelines as the general human-interface baseline. Prefer the target project's established command framework and HTTP library over introducing replacements without a concrete need.
Command Model¶
Design the command tree around a small, consistent grammar:
mycli <resource> <action> [arguments] [options]
mycli projects list --owner alice
mycli projects get PROJECT_ID
mycli projects create --name NAME
mycli projects delete PROJECT_ID
- Use nouns for resource groups and familiar verbs for actions.
- Keep equivalent operations parallel across resources:
list,get,create,update, anddeleteshould not change meaning by command group. - Prefer explicit positional arguments for primary identities and named options for modifiers.
- Reserve global options for behavior that applies consistently across commands, such as profile, endpoint, output format, verbosity, and non-interactive mode.
- Give every command useful
--helpoutput with a one-line purpose, argument meaning, defaults, and behavior-changing caveats. - Avoid mirroring every server endpoint. Combine low-level calls when one user task requires them, and omit endpoints that do not form a coherent CLI operation.
Do not make users memorize hidden context. When a command depends on an active account, project, region, or profile, make that context discoverable and overridable.
Responsibility Boundaries¶
Keep the command layer thin and dependencies directional:
entry point and bootstrap
└── command groups
└── application services
└── API client and resources
└── authenticated HTTP transport
└── HTTP library
configuration ───────────────┘
credentials ──> authentication
renderers <──── command results
| Layer | Owns | Avoid |
|---|---|---|
| Entry point | Dependency construction, top-level exception mapping, process exit | Business logic and API calls |
| Command | Parsing, prompts, presentation, command-specific orchestration | Raw HTTP details and credential refresh |
| Application service | Multi-request use cases and domain decisions | Terminal formatting |
| API resource | Endpoint paths, request parameters, and response models | CLI prompts and global process state |
| Transport | Base URL, headers, serialization, timeouts, retries, and response decoding | Resource-specific business rules |
| Authentication | Credential acquisition, storage, and renewal | API resource behavior |
| Renderer | Human and machine-readable output | Network calls and state mutation |
Keep framework objects at the command boundary. Core operations should accept ordinary typed values and return structured results so they can be tested without invoking a subprocess.
Input And Interaction¶
- Accept flags for every value that automation may need to provide. Prompts are an interactive convenience, not the only input path.
- Prompt only when stdin and stderr are attached to a terminal and the user has not selected non-interactive mode.
- In non-interactive mode, fail quickly with a specific missing-input error instead of waiting for input.
- Read large request bodies from a file or stdin; avoid forcing structured documents into shell-escaped arguments.
- Distinguish an omitted option from an explicit empty value when the API supports partial updates.
- Validate syntax locally, but let the service remain authoritative for remote identities, permissions, and business rules.
- Support
--before pass-through values or positional arguments that can begin with a hyphen.
For destructive or difficult-to-reverse operations, state the target precisely and require confirmation in interactive sessions. Provide an explicit option such as --yes for automation; never silently infer consent merely because input is non-interactive.
Output Contract¶
Treat output as a public interface.
- Write requested results to stdout and diagnostics, progress, warnings, and errors to stderr.
- Make the default human output concise and scannable. Do not print the same result as both prose and a table.
- Provide one stable machine-readable format, usually
--output jsonor--json, for commands whose results are useful in automation. - Serialize machine output from typed result models rather than scraping human-formatted strings.
- Keep machine-readable stdout clean: no progress bars, update notices, color codes, or explanatory prefixes.
- Disable color and animated progress when the output stream is not a terminal or when the user requests it.
- Use a pager only for interactive human output, and provide a consistent way to disable it.
- Document whether list commands emit one aggregate value or a stream of records; do not switch shapes based on result count.
When adding fields, preserve existing machine-readable fields where practical. Treat renaming, removing, or changing the type of a field as a compatibility decision.
Configuration Model¶
Use one documented precedence order:
- Resolve configuration once near startup and pass a validated settings object inward.
- Keep endpoint, profile, timeout, output mode, and similar behavior visible through a config or diagnostics command.
- Show provenance when troubleshooting precedence, but redact secret values.
- Store configuration in platform-appropriate user directories rather than the current working directory unless project-local configuration is intentional.
- Keep credentials behind a separate storage abstraction. A convenient config file is not automatically an acceptable secret store.
- Validate incompatible options together and report the conflict in the user's vocabulary.
HTTP And API Behavior¶
Centralize remote-call behavior in the transport or API client:
- Set explicit connect, read, write, and pool timeouts appropriate to the service.
- Send a useful user agent containing the CLI name and version.
- Map service errors into a small application error taxonomy before they reach commands.
- Retry only transient failures, honor
Retry-After, cap attempts and elapsed time, and add jitter where concurrent clients may synchronize. - Automatically retry state-changing requests only when they are demonstrably replay-safe, such as through an idempotency key accepted by the service.
- Preserve server request or correlation IDs in verbose diagnostics without exposing sensitive response data.
- Keep pagination in the API layer. Let commands choose whether to fetch one page, stream pages, or collect all results based on output and memory requirements.
- Make cancellation responsive between requests and during long-running operations.
Do not leak raw HTTP-library exceptions as the normal user interface. Preserve the original exception as the cause for debugging while presenting a stable CLI-level error.
Authentication¶
Choose authentication from the service contract and execution context. Keep credential acquisition and renewal out of command handlers and API resources.
For OAuth-protected APIs, load OAuth 2.0 for installed CLI clients. It covers public clients, Authorization Code with PKCE, loopback callbacks, device authorization, Authlib and HTTPX2 boundaries, protected token storage, synchronized refresh, scopes, discovery, and bounded authentication retries.
For API keys or static tokens:
- Accept them through an explicit credential provider such as an OS credential store, environment variable, or CI secret integration.
- Define precedence when more than one provider is configured.
- Never place credentials in command arguments by default because process listings and shell history may expose them.
- Redact credentials and credential-like headers from errors, debug logs, traces, and support bundles.
For unattended workloads, use a service identity and grant intended for machines. Do not reuse a person's interactive credentials as automation identity.
Errors And Exit Status¶
Keep a small documented taxonomy and map it once at the entry point:
| Category | User-facing behavior | Exit-status requirement |
|---|---|---|
| Usage or validation | Explain the invalid input and show the nearest help hint | Stable nonzero status distinct from remote failure |
| Authentication | Explain whether login or credential repair is required | Stable nonzero status |
| Authorization | Identify the denied operation without claiming credentials are expired | Stable nonzero status |
| Not found or conflict | Name the target and preserve actionable server context | Stable nonzero status if scripts branch on it |
| Rate limit or transient service failure | Explain retryability and any known retry time | Stable nonzero status |
| Unexpected failure | Concise message plus opt-in diagnostic detail | Generic nonzero status |
- Return zero only when the requested operation completed according to its contract.
- Do not require scripts to parse prose to distinguish common failure categories.
- Keep normal errors concise. Put tracebacks, request details, and internal context behind an explicit debug or verbose mode.
- Handle interruption without a traceback by default and use the platform's conventional interrupted-process status.
- Preserve partial-success information for batch operations and define whether partial success is a failing exit status.
State-Changing Operations¶
- Display or return the identity of the affected resource.
- Support a dry-run or plan mode when the service can accurately predict a consequential change.
- Use idempotency keys for retried creates or actions when the API supports them.
- Do not claim rollback if the remote API cannot provide it.
- For batch changes, define ordering, concurrency limits, stop/continue behavior, and partial-failure reporting.
- Keep local caches disposable unless their contents are explicitly part of the user contract.
Concurrency And Async Boundaries¶
Use concurrency only where it improves a measured workflow such as independent page or resource retrieval. Bound concurrent requests to respect service and local limits.
Choose one owner for the event loop. Command handlers may call an async application boundary, but lower layers should not invoke nested event-loop runners. Keep synchronous and asynchronous APIs separate or adapt them in one explicit place.
Suggested Package Shape¶
Adapt this shape to the project's size and existing conventions:
src/mycli/
├── __main__.py
├── cli.py
├── config.py
├── errors.py
├── output.py
├── api/
│ ├── client.py
│ ├── transport.py
│ └── resources/
└── auth/
Small clients can combine modules while preserving the conceptual boundaries. Split code when a boundary has distinct dependencies, state, tests, or change cadence, not merely to reproduce the example tree.
Design Sequence¶
- Inventory the user tasks, execution environments, API capabilities, and existing project conventions.
- Define the command grammar, required inputs, destructive-operation policy, output modes, and exit-status contract before wiring endpoints.
- Define typed configuration and its precedence, including credential providers and active context.
- Establish API resource, transport, authentication, and error boundaries.
- Implement one vertical command path through parsing, service behavior, transport, rendering, and error mapping.
- Verify the path both in-process and as an installed subprocess before repeating the pattern.
- Add concurrency, retries, caching, rich presentation, and convenience prompts only where requirements justify them.
Testing Strategy¶
- Unit-test command-independent services, API resources, renderers, configuration resolution, and error mapping directly.
- Test command invocation with isolated environment variables, config directories, stdin, stdout, and stderr.
- Assert exit status, stdout, and stderr independently.
- Cover human and machine-readable output, including empty and multi-page results.
- Use a mock transport for timeouts, malformed responses, pagination, rate limits, transient retries, and permanent failures.
- Verify interactive confirmation and non-interactive refusal for destructive operations.
- Test redaction with realistic secret shapes in headers, URLs, response bodies, and nested exceptions.
- Add live-service tests only for behavior a local fake cannot represent, using isolated accounts and CI-managed credentials.
- Build and install the distribution in a clean environment to verify the console entry point and runtime dependencies.
Reference Map¶
| Topic | Load when | Reference |
|---|---|---|
| Python library selection | Choosing or comparing a parser framework, terminal output, TUI, configuration, HTTP, authentication, testing, or packaging stack | Python CLI library selection |
| OAuth for installed applications | The API uses OAuth, OIDC discovery, refresh tokens, loopback callbacks, or device authorization | OAuth 2.0 for installed CLI clients |
Completion Checks¶
- Commands model recognizable user tasks with consistent names and options.
- Interactive conveniences have explicit non-interactive equivalents.
- Stdout, stderr, machine output, and exit statuses form a stable automation contract.
- Configuration has one visible precedence order and credentials use an appropriate protected source.
- Commands do not own raw HTTP, authentication lifecycle, or terminal-independent business logic.
- Timeouts, retries, pagination, cancellation, and state-changing request safety are explicit.
- Errors are actionable, categorized, redacted, and mapped at one process boundary.
- Destructive and batch operations define confirmation, idempotency, and partial-failure behavior.
- Tests exercise installed command behavior as well as isolated application and transport logic.
- Help text and diagnostics let users discover the command surface and effective non-secret configuration.