Skip to content

Commit 1e19fa3

Browse files
committed
✨ implement tool
1 parent d9eaa74 commit 1e19fa3

35 files changed

+4827
-41
lines changed

.editorconfig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# EditorConfig - https://editorconfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
end_of_line = lf
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
9+
10+
[*.py]
11+
indent_style = space
12+
indent_size = 4
13+
max_line_length = 88
14+
15+
[*.{json,yaml,yml,toml}]
16+
indent_style = space
17+
indent_size = 2
18+
19+
[*.md]
20+
trim_trailing_whitespace = false
21+
22+
[Makefile]
23+
indent_style = tab

.github/workflows/publish.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
build-and-publish:
10+
name: Build and publish
11+
runs-on: ubuntu-latest
12+
environment: pypi
13+
permissions:
14+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
15+
contents: read
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # Necessary for hatch-vcs to read tags
22+
23+
- name: Install uv
24+
uses: astral-sh/setup-uv@v5
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version-file: ".python-version"
30+
31+
- name: Build package
32+
run: uv build
33+
34+
- name: Publish to PyPI
35+
uses: pypa/gh-action-pypi-publish@release/v1

.pre-commit-config.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v5.0.0
4+
hooks:
5+
- id: check-yaml
6+
- id: end-of-file-fixer
7+
- id: trailing-whitespace
8+
- id: check-added-large-files
9+
- id: check-toml
10+
- id: check-merge-conflict
11+
- id: debug-statements
12+
13+
- repo: local
14+
hooks:
15+
- id: ruff
16+
name: ruff
17+
entry: uv run ruff check --fix
18+
language: system
19+
types: [python]
20+
require_serial: true
21+
- id: ruff-format
22+
name: ruff-format
23+
entry: uv run ruff format
24+
language: system
25+
types: [python]
26+
require_serial: true
27+
28+
- repo: local
29+
hooks:
30+
- id: mypy
31+
name: mypy
32+
entry: uv run mypy
33+
language: system
34+
types: [python]
35+
require_serial: true

Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.PHONY: install format lint test check clean
2+
3+
install:
4+
uv sync --extra preview --group dev --group test
5+
uv run pre-commit install
6+
7+
format:
8+
uv run ruff format openapi_burrito tests
9+
10+
lint:
11+
uv run ruff check --fix openapi_burrito tests
12+
uv run mypy openapi_burrito tests
13+
14+
test:
15+
uv run pytest --cov=openapi_burrito
16+
17+
# Run all checks (lint, typecheck, test)
18+
check: format lint test
19+
20+
clean:
21+
rm -rf .ruff_cache .pytest_cache .mypy_cache coverage.xml .coverage
22+
find . -type d -name "__pycache__" -exec rm -rf {} +

README.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
> [!WARNING]
2+
> **Early Development**: This project is under active development. APIs may
3+
> change.
4+
5+
---
6+
7+
<div align="center">
8+
9+
<img src="docs/static/logo.png" alt="openapi-burrito logo" width="128" />
10+
11+
# openapi-burrito
12+
13+
**Wrap your OpenAPI specs in type-safe Python clients**
14+
15+
[![PyPI version](https://img.shields.io/pypi/v/openapi-burrito?style=flat&logo=pypi&logoColor=white&color=3775A9)](https://pypi.org/project/openapi-burrito/)
16+
[![Python](https://img.shields.io/pypi/pyversions/openapi-burrito?style=flat&logo=python&logoColor=white)](https://pypi.org/project/openapi-burrito/)
17+
[![License](https://img.shields.io/github/license/simon-lund/openapi-burrito?style=flat&color=green)](LICENSE)
18+
[![OpenAPI](https://img.shields.io/badge/OpenAPI-3.0+-6BA539?style=flat&logo=openapiinitiative&logoColor=white)](https://www.openapis.org/)
19+
20+
</div>
21+
22+
## Table of Contents
23+
24+
- [Quick Start](#quick-start)
25+
- [Features](#features)
26+
- [Installation](#installation)
27+
- [Documentation](#documentation)
28+
- [Examples](#examples)
29+
- [Star History](#star-history)
30+
31+
## Quick Start
32+
33+
```bash
34+
# Install
35+
uv tool install openapi-burrito
36+
37+
# Generate
38+
openapi-burrito generate openapi.json -o ./my_client
39+
```
40+
41+
```python
42+
from my_client import Client
43+
44+
api = Client(base_url="https://api.example.com")
45+
46+
# Path-first API: type-checked paths and snake_case parameters
47+
res = api.GET("/users/{user_id}", user_id=123)
48+
49+
if res.is_success:
50+
print(res.data)
51+
else:
52+
print(f"Error {res.status_code}: {res.error}")
53+
```
54+
55+
## Features
56+
57+
- **Path-First API** - Call endpoints by path literal
58+
(`api.GET("/users/{user_id}")`), with full IDE autocomplete for paths and
59+
parameters
60+
- **Type-Safe** - `TypedDict` models and `@overload` signatures
61+
- **Zero Runtime** - Generated code is yours, no runtime dependency on this tool
62+
- **httpx-Based** - Async support, connection pooling, all httpx features
63+
- **Middleware System** - Logging, retry, auth via composable middleware
64+
- **Snake Case Params** - Path parameters auto-converted to Python style
65+
(`{userId}``{user_id}`)
66+
67+
## Installation
68+
69+
### For Users
70+
71+
```bash
72+
# As a CLI tool (recommended)
73+
uv tool install openapi-burrito
74+
75+
# With preview server support (Swagger UI, Redoc)
76+
uv tool install openapi-burrito[preview]
77+
```
78+
79+
### For Developers
80+
81+
```bash
82+
# Clone and install all dev dependencies
83+
git clone https://github.com/simon-lund/openapi-burrito.git
84+
cd openapi-burrito
85+
make install
86+
87+
# Run linting and type checks
88+
make lint
89+
90+
# Run tests
91+
make test
92+
```
93+
94+
## Security
95+
96+
This generator sanitizes identifiers and string literals to prevent code
97+
injection from malformed OpenAPI specs. However, **always review untrusted specs
98+
before generating**.
99+
100+
### Parser Safety Audit
101+
102+
All fields output by the parser are validated/sanitized:
103+
104+
| Field | Validation | Notes |
105+
| ------------------------------------- | ---------------------- | ------------------------------------- |
106+
| Model/param names | `sanitize(mode="id")` | Converted to valid Python identifiers |
107+
| Paths | `sanitize(mode="str")` | String-escaped for literals |
108+
| Descriptions/docs | `sanitize(mode="doc")` | Docstring-escaped |
109+
| `type` strings | Type translator | Built from validated schema types |
110+
| `method` | `HTTPMethod` enum | Only known HTTP methods allowed |
111+
| `in` (param location) | Enum check | Only `path\|query\|header\|cookie` |
112+
| `required`, `read_only`, `write_only` | `bool()` cast | Forced to boolean |
113+
| `default` | `repr()` | Python string representation |
114+
115+
A malicious spec could attempt injection like:
116+
117+
```yaml
118+
components:
119+
schemas:
120+
"User:\n pass\nimport os; os.system('rm -rf /') # ":
121+
type: object
122+
```
123+
124+
While this generator escapes such payloads, the safest approach is to only
125+
generate clients from trusted sources.
126+
127+
See
128+
[CVE-2020-15142](https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-9x4c-63pf-525f)
129+
for an example of this vulnerability class in other generators.
130+
131+
## Documentation
132+
133+
| Guide | Description |
134+
| ------------------------------------------- | ---------------------------------------------- |
135+
| [Introduction](docs/01-introduction.md) | Installation and basic usage |
136+
| [Authentication](docs/02-authentication.md) | API keys, tokens, OAuth patterns |
137+
| [Middleware](docs/03-middleware.md) | Logging, retry, custom handling |
138+
| [Type System](docs/04-type-system.md) | `UNSET`, `Unknown`, `NotRequired`, limitations |
139+
| [CLI Reference](docs/05-cli-reference.md) | `generate` and `preview` commands |
140+
| [Contributing](docs/06-contributing.md) | Development setup and guidelines |
141+
142+
## Examples
143+
144+
See the [`examples/`](examples/) directory:
145+
146+
- **[Petstore](examples/petstore/)** - Classic Swagger Petstore API
147+
- **[Artifacts MMO](examples/artifactsmmo/)** - Game API with complex schemas
148+
149+
## Star History
150+
151+
[![Star History Chart](https://api.star-history.com/svg?repos=klementine/openapi-burrito&type=date&legend=top-left)](https://www.star-history.com/#klementine/openapi-burrito&type=date&legend=top-left)

docs/01-introduction.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Introduction
2+
3+
This guide covers installation and basic usage of `openapi-burrito`.
4+
5+
## Installation
6+
7+
We recommend using [uv](https://docs.astral.sh/uv/) for fast, isolated
8+
installation:
9+
10+
```bash
11+
uv tool install openapi-burrito
12+
```
13+
14+
## Generate a Client
15+
16+
Point the CLI to your OpenAPI spec (local file or URL) and specify an output
17+
directory:
18+
19+
```bash
20+
openapi-burrito generate openapi.json -o ./my_client
21+
```
22+
23+
This creates a self-contained Python package with:
24+
25+
| File | Description |
26+
| ---------------- | --------------------------------------------------------------- |
27+
| `client.py` | Main client with typed `@overload` signatures for each endpoint |
28+
| `models.py` | All API schemas as `TypedDict` definitions |
29+
| `pyproject.toml` | Dependencies (`httpx`) |
30+
| `__init__.py` | Package exports |
31+
32+
## Basic Usage
33+
34+
```python
35+
from my_client import Client
36+
37+
# Initialize with base URL
38+
api = Client(base_url="https://api.example.com")
39+
40+
# GET request with path parameter (note: snake_case)
41+
res = api.GET("/users/{user_id}", user_id=123)
42+
43+
if res.is_success:
44+
print(res.data)
45+
else:
46+
print(f"Error: {res.error}")
47+
48+
# POST request with JSON body
49+
res = api.POST("/users", json={"name": "Alice", "email": "alice@example.com"})
50+
```
51+
52+
> **Note**: Path parameters are automatically converted to snake_case. OpenAPI's
53+
> `{userId}` becomes `{user_id}` in the generated client.
54+
55+
## httpx Configuration
56+
57+
The generated client wraps [httpx](https://www.python-httpx.org/), so all
58+
`httpx.Client` constructor arguments are supported:
59+
60+
```python
61+
client = Client(
62+
base_url="https://api.example.com",
63+
timeout=30.0,
64+
headers={"User-Agent": "MyApp/1.0"},
65+
proxies={"https://": "http://localhost:8080"},
66+
verify=False, # Disable SSL verification (dev only)
67+
)
68+
```
69+
70+
See [httpx documentation](https://www.python-httpx.org/) for all available
71+
options.
72+
73+
## Next Steps
74+
75+
- [CLI Reference](cli-reference.md) - All commands and options
76+
- [Authentication](authentication.md) - API keys, tokens, OAuth
77+
- [Middleware](middleware.md) - Logging, retry, custom handling
78+
- [Type System](type-system.md) - Understanding generated types

0 commit comments

Comments
 (0)