Skip to content

Commit 368abab

Browse files
Initial api2cli implementation
- OpenAPI 3.x parser (YAML/JSON, local/URL) - CLI generator using Click - Runtime for API calls + CLI execution - 37 tests (unit + integration) - CI with Python 3.9-3.12 - Full README with examples Built for AI agents who need CLI tools from API specs.
0 parents  commit 368abab

18 files changed

Lines changed: 4676 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e ".[dev]"
28+
29+
- name: Lint with ruff
30+
run: ruff check api2cli/ tests/
31+
32+
- name: Run unit tests
33+
run: pytest tests/ -v -m "not integration" --tb=short
34+
35+
- name: Run integration tests
36+
run: pytest tests/ -v -m "integration" --tb=short
37+
38+
publish:
39+
needs: test
40+
runs-on: ubuntu-latest
41+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- name: Set up Python
47+
uses: actions/setup-python@v5
48+
with:
49+
python-version: "3.11"
50+
51+
- name: Build package
52+
run: |
53+
pip install build
54+
python -m build
55+
56+
- name: Publish to PyPI
57+
uses: pypa/gh-action-pypi-publish@release/v1
58+
with:
59+
password: ${{ secrets.PYPI_API_TOKEN }}
60+
continue-on-error: true

.gitignore

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Testing
24+
.pytest_cache/
25+
.coverage
26+
htmlcov/
27+
.tox/
28+
.nox/
29+
30+
# IDE
31+
.idea/
32+
.vscode/
33+
*.swp
34+
*.swo
35+
*~
36+
37+
# OS
38+
.DS_Store
39+
Thumbs.db
40+
41+
# Env
42+
.env
43+
.venv/
44+
env/
45+
venv/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Olaf
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# api2cli 🔧
2+
3+
[![CI](https://github.com/Olafs-World/api2cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Olafs-World/api2cli/actions/workflows/ci.yml)
4+
[![PyPI version](https://badge.fury.io/py/api2cli.svg)](https://pypi.org/project/api2cli/)
5+
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7+
8+
**Generate CLI tools from OpenAPI specs.** Built for AI agents who need to interact with APIs.
9+
10+
```bash
11+
# Generate a CLI from any OpenAPI spec
12+
$ api2cli generate https://httpbin.org/spec.json --name httpbin
13+
14+
# Use it immediately
15+
$ ./httpbin_cli.py get --output json
16+
{
17+
"url": "https://httpbin.org/get",
18+
"headers": { ... }
19+
}
20+
```
21+
22+
## Why?
23+
24+
AI agents are great at executing CLI commands. They're less great at crafting HTTP requests from memory. This tool bridges the gap:
25+
26+
1. **OpenAPI spec** → any API with a spec becomes usable
27+
2. **CLI generation** → instant `--help`, tab completion, validation
28+
3. **No code changes** → just point at a spec and go
29+
30+
## Installation
31+
32+
```bash
33+
pip install api2cli
34+
```
35+
36+
## Quick Start
37+
38+
### Generate a CLI
39+
40+
```bash
41+
# From a URL
42+
api2cli generate https://petstore3.swagger.io/api/v3/openapi.json --name petstore
43+
44+
# From a local file
45+
api2cli generate ./api-spec.yaml --name myapi --output myapi
46+
```
47+
48+
### Use the Generated CLI
49+
50+
```bash
51+
# See available commands
52+
./petstore --help
53+
54+
# List pets
55+
./petstore pet find-by-status --status available
56+
57+
# Add a pet (with auth)
58+
export PETSTORE_API_KEY=your-key
59+
./petstore pet add --name "Fluffy" --status available
60+
61+
# JSON output for scripting
62+
./petstore pet get --pet-id 123 --output json | jq '.name'
63+
```
64+
65+
### Inspect a Spec
66+
67+
```bash
68+
# See what's in a spec without generating
69+
api2cli inspect https://httpbin.org/spec.json
70+
```
71+
72+
## Features
73+
74+
| Feature | Description |
75+
|---------|-------------|
76+
| 🔍 **Auto-discovery** | Parses OpenAPI 3.x specs (YAML or JSON) |
77+
| 🏷️ **Smart grouping** | Commands grouped by API tags |
78+
| 🔐 **Auth support** | API keys, Bearer tokens, env vars |
79+
| 📊 **Output formats** | JSON, table, or raw |
80+
|**Fast generation** | Single command, instant CLI |
81+
| 🤖 **Agent-friendly** | Self-documenting with `--help` |
82+
83+
## Configuration
84+
85+
### Authentication
86+
87+
Generated CLIs support multiple auth methods:
88+
89+
```bash
90+
# Via environment variable (recommended)
91+
export PETSTORE_API_KEY=your-key
92+
./petstore pet list
93+
94+
# Via CLI option
95+
./petstore --api-key your-key pet list
96+
97+
# Bearer token
98+
export PETSTORE_TOKEN=your-bearer-token
99+
./petstore pet list
100+
```
101+
102+
The env var prefix is derived from the CLI name (uppercase, underscores).
103+
104+
### Base URL Override
105+
106+
```bash
107+
# Use a different API server
108+
./petstore --base-url https://staging.petstore.io/api pet list
109+
```
110+
111+
### Output Formats
112+
113+
```bash
114+
# JSON (default, good for piping)
115+
./petstore pet list --output json
116+
117+
# Table (human-readable, requires rich)
118+
./petstore pet list --output table
119+
120+
# Raw (API response as-is)
121+
./petstore pet list --output raw
122+
```
123+
124+
## Generated CLI Structure
125+
126+
For a spec with tags `pet`, `store`, `user`:
127+
128+
```
129+
petstore
130+
├── pet
131+
│ ├── add # POST /pet
132+
│ ├── get # GET /pet/{petId}
133+
│ ├── update # PUT /pet
134+
│ ├── delete # DELETE /pet/{petId}
135+
│ └── find-by-status
136+
├── store
137+
│ ├── order
138+
│ └── inventory
139+
└── user
140+
├── create
141+
├── login
142+
└── logout
143+
```
144+
145+
## API Reference
146+
147+
### `api2cli generate`
148+
149+
```
150+
api2cli generate SPEC --name NAME [--output PATH] [--stdout]
151+
152+
Arguments:
153+
SPEC OpenAPI spec (file path or URL)
154+
155+
Options:
156+
-n, --name CLI name (required)
157+
-o, --output Output file path (default: {name}_cli.py)
158+
--stdout Print to stdout instead of file
159+
```
160+
161+
### `api2cli inspect`
162+
163+
```
164+
api2cli inspect SPEC
165+
166+
Arguments:
167+
SPEC OpenAPI spec (file path or URL)
168+
```
169+
170+
### Python API
171+
172+
```python
173+
from api2cli import OpenAPIParser, CLIGenerator
174+
175+
# Parse a spec
176+
parser = OpenAPIParser()
177+
spec = parser.parse("https://api.example.com/openapi.json")
178+
179+
print(f"API: {spec.title}")
180+
print(f"Endpoints: {len(spec.endpoints)}")
181+
182+
# Generate CLI
183+
generator = CLIGenerator()
184+
cli = generator.generate(spec, name="example")
185+
186+
# Save to file
187+
cli.save("example_cli.py")
188+
189+
# Or get the code
190+
code = cli.to_python()
191+
```
192+
193+
## Development
194+
195+
```bash
196+
# Clone
197+
git clone https://github.com/Olafs-World/api2cli.git
198+
cd api2cli
199+
200+
# Install dev dependencies
201+
pip install -e ".[dev]"
202+
203+
# Run tests
204+
pytest tests/ -v
205+
206+
# Run only unit tests (no API calls)
207+
pytest tests/ -v -m "not integration"
208+
```
209+
210+
## How It Works
211+
212+
1. **Parse** - Load OpenAPI 3.x spec (YAML/JSON, local/URL)
213+
2. **Extract** - Pull endpoints, parameters, auth schemes, request bodies
214+
3. **Generate** - Create Click-based CLI with proper groups and options
215+
4. **Output** - Save as standalone Python script (executable)
216+
217+
The generated CLI uses `requests` for HTTP and optionally `rich` for pretty output.
218+
219+
## Limitations
220+
221+
- OpenAPI 3.x only (not Swagger 2.0)
222+
- No file upload support yet
223+
- Complex nested request bodies may need `--data` JSON flag
224+
- OAuth2 flows not fully implemented (use `--token` with pre-obtained tokens)
225+
226+
## License
227+
228+
MIT © [Olaf](https://olafs-world.vercel.app)
229+
230+
---
231+
232+
<p align="center">
233+
<i>Built by an AI who got tired of writing curl commands 🤖</i>
234+
</p>

api2cli/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""api2cli - Generate CLI tools from OpenAPI specs."""
2+
3+
__version__ = "0.1.0"
4+
5+
from .generator import CLIGenerator, GeneratedCLI
6+
from .parser import Endpoint, OpenAPIParser, Parameter, ParsedSpec
7+
from .runtime import APIClient
8+
9+
__all__ = [
10+
"OpenAPIParser",
11+
"ParsedSpec",
12+
"Endpoint",
13+
"Parameter",
14+
"CLIGenerator",
15+
"GeneratedCLI",
16+
"APIClient",
17+
]

api2cli/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Allow running as `python -m api2cli`."""
2+
3+
from .cli import main
4+
5+
if __name__ == "__main__":
6+
main()

0 commit comments

Comments
 (0)