otterapi 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- otterapi-0.1.2/PKG-INFO +823 -0
- otterapi-0.1.2/README.md +793 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/codegen.py +6 -1
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/splitting.py +147 -7
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/config.py +9 -0
- otterapi-0.1.0/PKG-INFO +0 -709
- otterapi-0.1.0/README.md +0 -679
- {otterapi-0.1.0 → otterapi-0.1.2}/.gitignore +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/__main__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/cli.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/_body_builder.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/_features.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/_openapi_adapter.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/ast_utils.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/client.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/dataframes.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/endpoints.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/export.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/pagination.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_client_stub.py.tpl +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_concurrency.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_dataframe.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_export.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_models_mixin.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_pagination.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_retry.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/schema.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/types.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/utils.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/exceptions.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/constants.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v2/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v2/spec.json +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v2/v2.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3/spec.json +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3/v3.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_1/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_1/spec.json +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_1/v3_1.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_2/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_2/spec.json +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_2/v3_2.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/warnings.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/__init__.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/constraints/spec.yaml +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/dataframe/config.yaml +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/dataframe/spec.yaml +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/discriminator/spec.yaml +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/paginated/config.yaml +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/paginated/spec.yaml +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_ast_utils.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_body_builder.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_client_retry_lifecycle.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_codegen.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_codegen_structure.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_concurrency_runtime.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_config_errors.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_dataframe.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_discriminated_unions.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_exceptions.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_export.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_extra_forbid.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_feature_modules.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_html_repr.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_init_scaffold.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_not_schema.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_nullable_upgrade.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_openapi_adapter.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_openapi_support.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_openapi_upgrade.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_pagination.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_per_status_errors.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_public_api.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_regen_script.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_retry_runtime.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_runtime_guards.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_schema_constraints.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_smoke_generated_client.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_config.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_integration.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_resolver.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_tree.py +0 -0
- {otterapi-0.1.0 → otterapi-0.1.2}/pyproject.toml +0 -0
otterapi-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: otterapi
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A cute little companion that generates type-safe clients from OpenAPI documents.
|
|
5
|
+
Project-URL: Source, https://github.com/danplischke/otter
|
|
6
|
+
Author: Dan Plischke
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Operating System :: POSIX
|
|
12
|
+
Classifier: Operating System :: Unix
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx~=0.28.1
|
|
22
|
+
Requires-Dist: pydantic-settings~=2.11.0
|
|
23
|
+
Requires-Dist: pydantic[email]~=2.12.3
|
|
24
|
+
Requires-Dist: pyyaml~=6.0.3
|
|
25
|
+
Requires-Dist: typer~=0.20.0
|
|
26
|
+
Requires-Dist: universal-pathlib~=0.3.4
|
|
27
|
+
Provides-Extra: parquet
|
|
28
|
+
Requires-Dist: pyarrow>=15; extra == 'parquet'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# 🦦 OtterAPI
|
|
32
|
+
|
|
33
|
+
> *A cute and intelligent OpenAPI client generator that dives deep into your OpenAPIs*
|
|
34
|
+
|
|
35
|
+
**OtterAPI** is a sleek Python library that transforms OpenAPI specifications into clean, type-safe client code with Pydantic models and httpx-based HTTP clients.
|
|
36
|
+
|
|
37
|
+
## ✨ Features
|
|
38
|
+
|
|
39
|
+
- **Type-Safe Code Generation** - Generates Pydantic models and fully typed endpoint functions
|
|
40
|
+
- **Sync & Async Support** - Generate both synchronous and asynchronous API clients
|
|
41
|
+
- **OpenAPI 3.x Support** - Full support for OpenAPI 3.0, 3.1, and 3.2 specifications
|
|
42
|
+
- **Module Splitting** - Organize large APIs into multiple organized files
|
|
43
|
+
- **Pagination** - Auto-detect or configure offset, cursor, page, and link-header pagination
|
|
44
|
+
- **DataFrame Conversion** - Generate pandas/polars DataFrame methods for list endpoints
|
|
45
|
+
- **File Export** - Generate CSV, TSV, JSONL, and Parquet streaming helpers
|
|
46
|
+
- **Response Unwrapping** - Transparently unwrap envelope-style responses
|
|
47
|
+
- **Customizable Client** - Generated client class with configurable base URL, timeout, and headers
|
|
48
|
+
- **Environment Variable Support** - Use `${VAR}` or `${VAR:-default}` syntax in config files
|
|
49
|
+
|
|
50
|
+
## 🚀 Quick Start
|
|
51
|
+
|
|
52
|
+
### Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install otterapi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Basic Usage
|
|
59
|
+
|
|
60
|
+
1. Create an `otter.yml` configuration file:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
documents:
|
|
64
|
+
- source: https://petstore3.swagger.io/api/v3/openapi.json
|
|
65
|
+
output: petstore_client
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. Generate the client:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
otter generate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. Use the generated code:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from petstore_client import get_pet_by_id, aget_pet_by_id
|
|
78
|
+
|
|
79
|
+
# Synchronous usage
|
|
80
|
+
pet = get_pet_by_id(pet_id=123)
|
|
81
|
+
|
|
82
|
+
# Asynchronous usage
|
|
83
|
+
import asyncio
|
|
84
|
+
pet = asyncio.run(aget_pet_by_id(pet_id=123))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 📝 Configuration
|
|
90
|
+
|
|
91
|
+
### Config File Locations
|
|
92
|
+
|
|
93
|
+
OtterAPI looks for configuration in this order:
|
|
94
|
+
|
|
95
|
+
1. Path passed via `otter generate -c <path>`
|
|
96
|
+
2. `otter.yaml` or `otter.yml` in the current directory
|
|
97
|
+
3. `otter.json` in the current directory
|
|
98
|
+
4. `[tool.otterapi]` section in `pyproject.toml`
|
|
99
|
+
5. `OTTER_SOURCE` and `OTTER_OUTPUT` environment variables
|
|
100
|
+
|
|
101
|
+
### Config File Formats
|
|
102
|
+
|
|
103
|
+
**YAML (recommended):**
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
documents:
|
|
107
|
+
- source: https://api.example.com/openapi.json
|
|
108
|
+
output: ./client
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**pyproject.toml:**
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
[tool.otterapi]
|
|
115
|
+
[[tool.otterapi.documents]]
|
|
116
|
+
source = "https://api.example.com/openapi.json"
|
|
117
|
+
output = "./client"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**JSON:**
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"documents": [
|
|
125
|
+
{ "source": "https://api.example.com/openapi.json", "output": "./client" }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Top-Level Options
|
|
131
|
+
|
|
132
|
+
These sit at the root of your config file, outside of `documents:`.
|
|
133
|
+
|
|
134
|
+
| Option | Type | Default | Description |
|
|
135
|
+
|--------|------|---------|-------------|
|
|
136
|
+
| `documents` | list | — | List of OpenAPI documents to process (required) |
|
|
137
|
+
| `generate_endpoints` | bool | `true` | Whether to generate endpoint functions |
|
|
138
|
+
| `format_output` | bool | `true` | Format generated code with ruff/black |
|
|
139
|
+
| `validate_output` | bool | `true` | Validate generated code syntax after writing |
|
|
140
|
+
| `create_py_typed` | bool | `true` | Create `py.typed` marker files |
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
format_output: true
|
|
144
|
+
validate_output: true
|
|
145
|
+
create_py_typed: true
|
|
146
|
+
|
|
147
|
+
documents:
|
|
148
|
+
- source: https://api.example.com/openapi.json
|
|
149
|
+
output: ./client
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Document Options
|
|
153
|
+
|
|
154
|
+
Each entry under `documents:` supports these fields:
|
|
155
|
+
|
|
156
|
+
| Option | Type | Default | Description |
|
|
157
|
+
|--------|------|---------|-------------|
|
|
158
|
+
| `source` | string | — | URL or file path to the OpenAPI spec (required) |
|
|
159
|
+
| `output` | string | — | Output directory for generated code (required) |
|
|
160
|
+
| `base_url` | string | from spec | Override the base URL defined in the spec |
|
|
161
|
+
| `models_file` | string | `models.py` | Filename for generated models |
|
|
162
|
+
| `endpoints_file` | string | `endpoints.py` | Filename for generated endpoints (no-split mode) |
|
|
163
|
+
| `models_import_path` | string | `null` | Override the import path used for models in endpoints |
|
|
164
|
+
| `generate_async` | bool | `true` | Generate async endpoint functions |
|
|
165
|
+
| `generate_sync` | bool | `true` | Generate sync endpoint functions |
|
|
166
|
+
| `client_class_name` | string | from API title | Override the generated client class name |
|
|
167
|
+
| `include_paths` | list | `null` | Glob patterns — only matching paths are generated |
|
|
168
|
+
| `exclude_paths` | list | `null` | Glob patterns — matching paths are skipped (applied after `include_paths`) |
|
|
169
|
+
|
|
170
|
+
#### Path Filtering
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
documents:
|
|
174
|
+
- source: https://api.example.com/openapi.json
|
|
175
|
+
output: ./client
|
|
176
|
+
include_paths:
|
|
177
|
+
- /api/v2/** # only v2 endpoints
|
|
178
|
+
exclude_paths:
|
|
179
|
+
- /internal/* # skip internal endpoints
|
|
180
|
+
- /admin/** # skip admin endpoints
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Patterns follow standard glob syntax (`*` = single segment, `**` = any depth).
|
|
184
|
+
|
|
185
|
+
### Environment Variable Support
|
|
186
|
+
|
|
187
|
+
Any string value in a config file can reference environment variables:
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
documents:
|
|
191
|
+
- source: ${API_SPEC_URL}
|
|
192
|
+
output: ${OUTPUT_DIR:-./client}
|
|
193
|
+
base_url: ${BASE_URL:-https://api.example.com}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
You can also configure a single document entirely via environment variables (no config file needed):
|
|
197
|
+
|
|
198
|
+
| Variable | Description |
|
|
199
|
+
|----------|-------------|
|
|
200
|
+
| `OTTER_SOURCE` | Path or URL to the OpenAPI spec |
|
|
201
|
+
| `OTTER_OUTPUT` | Output directory |
|
|
202
|
+
| `OTTER_BASE_URL` | Base URL override |
|
|
203
|
+
| `OTTER_MODELS_FILE` | Models filename |
|
|
204
|
+
| `OTTER_ENDPOINTS_FILE` | Endpoints filename |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 📦 Module Splitting
|
|
209
|
+
|
|
210
|
+
For large APIs, OtterAPI can split generated code into multiple organized modules.
|
|
211
|
+
|
|
212
|
+
### Enabling Module Splitting
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
documents:
|
|
216
|
+
- source: https://api.example.com/openapi.json
|
|
217
|
+
output: ./client
|
|
218
|
+
module_split:
|
|
219
|
+
enabled: true
|
|
220
|
+
strategy: tag
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Splitting Strategies
|
|
224
|
+
|
|
225
|
+
#### `tag` — Split by OpenAPI Tags
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
module_split:
|
|
229
|
+
enabled: true
|
|
230
|
+
strategy: tag
|
|
231
|
+
min_endpoints: 1
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Endpoints tagged `["Users"]` go to `users.py`, `["Orders"]` to `orders.py`, etc.
|
|
235
|
+
|
|
236
|
+
#### `path` — Split by URL Path
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
module_split:
|
|
240
|
+
enabled: true
|
|
241
|
+
strategy: path
|
|
242
|
+
path_depth: 1
|
|
243
|
+
global_strip_prefixes:
|
|
244
|
+
- /api/v1
|
|
245
|
+
- /api/v2
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`/api/v1/users/123` → `users.py`, `/api/v1/orders/456` → `orders.py`.
|
|
249
|
+
|
|
250
|
+
#### `custom` — Explicit Module Mapping
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
module_split:
|
|
254
|
+
enabled: true
|
|
255
|
+
strategy: custom
|
|
256
|
+
module_map:
|
|
257
|
+
users:
|
|
258
|
+
- /users
|
|
259
|
+
- /users/*
|
|
260
|
+
- /users/**
|
|
261
|
+
orders:
|
|
262
|
+
- /orders/*
|
|
263
|
+
health:
|
|
264
|
+
- /health
|
|
265
|
+
- /ready
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### `hybrid` — Combined Strategy (Default)
|
|
269
|
+
|
|
270
|
+
Tries `module_map` first, then tags, then path:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
module_split:
|
|
274
|
+
enabled: true
|
|
275
|
+
strategy: hybrid
|
|
276
|
+
module_map:
|
|
277
|
+
health:
|
|
278
|
+
- /health
|
|
279
|
+
- /ready
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### `none` — All to Fallback
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
module_split:
|
|
286
|
+
enabled: true
|
|
287
|
+
strategy: none
|
|
288
|
+
fallback_module: api
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Module Map Patterns
|
|
292
|
+
|
|
293
|
+
| Pattern | Matches |
|
|
294
|
+
|---------|---------|
|
|
295
|
+
| `/users` | Exact path only |
|
|
296
|
+
| `/users/*` | One additional segment |
|
|
297
|
+
| `/users/**` | Any depth below `/users` |
|
|
298
|
+
| `/v?/users` | Single wildcard character |
|
|
299
|
+
|
|
300
|
+
### Nested Module Maps
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
module_split:
|
|
304
|
+
enabled: true
|
|
305
|
+
strategy: custom
|
|
306
|
+
module_map:
|
|
307
|
+
identity:
|
|
308
|
+
users:
|
|
309
|
+
- /users/*
|
|
310
|
+
auth:
|
|
311
|
+
- /auth/*
|
|
312
|
+
- /login
|
|
313
|
+
- /logout
|
|
314
|
+
billing:
|
|
315
|
+
invoices:
|
|
316
|
+
- /invoices/*
|
|
317
|
+
payments:
|
|
318
|
+
- /payments/*
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Advanced Module Definition
|
|
322
|
+
|
|
323
|
+
```yaml
|
|
324
|
+
module_split:
|
|
325
|
+
enabled: true
|
|
326
|
+
strategy: custom
|
|
327
|
+
module_map:
|
|
328
|
+
v2_api:
|
|
329
|
+
paths:
|
|
330
|
+
- /v2/**
|
|
331
|
+
strip_prefix: /v2
|
|
332
|
+
description: "API v2 endpoints"
|
|
333
|
+
file_name: v2.py # override the generated filename
|
|
334
|
+
modules:
|
|
335
|
+
users:
|
|
336
|
+
paths:
|
|
337
|
+
- /users/*
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Module Split Options Reference
|
|
341
|
+
|
|
342
|
+
| Option | Type | Default | Description |
|
|
343
|
+
|--------|------|---------|-------------|
|
|
344
|
+
| `enabled` | bool | `false` | Enable module splitting |
|
|
345
|
+
| `strategy` | string | `hybrid` | `none`, `path`, `tag`, `hybrid`, `custom` |
|
|
346
|
+
| `fallback_module` | string | `common` | Module for unmatched endpoints |
|
|
347
|
+
| `min_endpoints` | int | `2` | Minimum endpoints per module before consolidating into fallback |
|
|
348
|
+
| `flat_structure` | bool | `false` | Flat files instead of nested directories |
|
|
349
|
+
| `path_depth` | int | `1` | Path segments used for `path` strategy (1–5) |
|
|
350
|
+
| `global_strip_prefixes` | list | `/api`, `/api/v1`, `/api/v2`, `/api/v3` | Prefixes stripped from all paths before matching |
|
|
351
|
+
| `module_map` | object | `{}` | Custom module-to-path mappings |
|
|
352
|
+
| `split_models` | bool | `false` | Generate per-module model files instead of one shared `models.py` |
|
|
353
|
+
| `shared_models_module` | string | `_models` | Module name for shared models when `split_models` is `true` |
|
|
354
|
+
|
|
355
|
+
### Output Structure
|
|
356
|
+
|
|
357
|
+
**Flat (`flat_structure: false`, default):**
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
client/
|
|
361
|
+
├── __init__.py
|
|
362
|
+
├── models.py
|
|
363
|
+
├── _client.py
|
|
364
|
+
├── client.py
|
|
365
|
+
├── users.py
|
|
366
|
+
├── orders.py
|
|
367
|
+
└── health.py
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Nested (`flat_structure: false` with nested `module_map`):**
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
client/
|
|
374
|
+
├── __init__.py
|
|
375
|
+
├── models.py
|
|
376
|
+
├── _client.py
|
|
377
|
+
├── client.py
|
|
378
|
+
├── identity/
|
|
379
|
+
│ ├── __init__.py
|
|
380
|
+
│ ├── users.py
|
|
381
|
+
│ └── auth.py
|
|
382
|
+
└── billing/
|
|
383
|
+
├── __init__.py
|
|
384
|
+
└── invoices.py
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 📄 Pagination
|
|
390
|
+
|
|
391
|
+
OtterAPI generates iterator-based pagination methods alongside standard endpoint functions. Paginated endpoints get an `_iter` suffix variant that yields items one by one, handling page-fetching automatically.
|
|
392
|
+
|
|
393
|
+
### Enabling Pagination
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
documents:
|
|
397
|
+
- source: https://api.example.com/openapi.json
|
|
398
|
+
output: ./client
|
|
399
|
+
pagination:
|
|
400
|
+
enabled: true
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Auto-Detection
|
|
404
|
+
|
|
405
|
+
When `auto_detect: true` (the default), OtterAPI inspects each endpoint's parameters. If it finds a matching pair for any pagination style, it generates pagination methods automatically — no per-endpoint config needed.
|
|
406
|
+
|
|
407
|
+
```yaml
|
|
408
|
+
pagination:
|
|
409
|
+
enabled: true
|
|
410
|
+
auto_detect: true # default
|
|
411
|
+
default_style: offset # default; used when auto-detected
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Pagination Styles
|
|
415
|
+
|
|
416
|
+
| Style | Required Parameters | Description |
|
|
417
|
+
|-------|--------------------|-|
|
|
418
|
+
| `offset` | `offset` + `limit` | Offset/limit (e.g. `?offset=0&limit=100`) |
|
|
419
|
+
| `cursor` | `cursor` + `limit` | Cursor-based (e.g. `?cursor=abc&limit=100`) |
|
|
420
|
+
| `page` | `page` + `per_page` | Page number (e.g. `?page=2&per_page=50`) |
|
|
421
|
+
| `link` | — | RFC 5988 `Link` header |
|
|
422
|
+
|
|
423
|
+
### Global Pagination Options
|
|
424
|
+
|
|
425
|
+
| Option | Type | Default | Description |
|
|
426
|
+
|--------|------|---------|-------------|
|
|
427
|
+
| `enabled` | bool | `false` | Enable pagination generation |
|
|
428
|
+
| `auto_detect` | bool | `true` | Auto-detect pagination from parameter names |
|
|
429
|
+
| `default_style` | string | `offset` | Fallback style when auto-detecting |
|
|
430
|
+
| `default_page_size` | int | `100` | Default page size for iteration |
|
|
431
|
+
| `default_data_path` | string | `null` | Default JSON path to items array in response |
|
|
432
|
+
| `default_total_path` | string | `null` | Default JSON path to total count in response |
|
|
433
|
+
| `default_offset_param` | string | `offset` | Param name used for offset detection |
|
|
434
|
+
| `default_limit_param` | string | `limit` | Param name used for limit detection |
|
|
435
|
+
| `default_cursor_param` | string | `cursor` | Param name used for cursor detection |
|
|
436
|
+
| `default_page_param` | string | `page` | Param name used for page detection |
|
|
437
|
+
| `default_per_page_param` | string | `per_page` | Param name used for per-page detection |
|
|
438
|
+
| `endpoints` | object | `{}` | Per-endpoint overrides |
|
|
439
|
+
|
|
440
|
+
### Per-Endpoint Pagination Options
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
pagination:
|
|
444
|
+
enabled: true
|
|
445
|
+
endpoints:
|
|
446
|
+
list_users:
|
|
447
|
+
style: cursor
|
|
448
|
+
cursor_param: next_token
|
|
449
|
+
limit_param: max_results
|
|
450
|
+
data_path: data.users
|
|
451
|
+
total_path: meta.total
|
|
452
|
+
next_cursor_path: meta.next_token
|
|
453
|
+
default_page_size: 50
|
|
454
|
+
max_page_size: 200
|
|
455
|
+
get_orders:
|
|
456
|
+
enabled: false # disable pagination for this endpoint
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
| Option | Type | Default | Description |
|
|
460
|
+
|--------|------|---------|-------------|
|
|
461
|
+
| `enabled` | bool\|null | inherits | Override whether to generate pagination |
|
|
462
|
+
| `style` | string | inherits | `offset`, `cursor`, `page`, `link` |
|
|
463
|
+
| `offset_param` | string | inherits | Name of offset parameter |
|
|
464
|
+
| `limit_param` | string | inherits | Name of limit parameter |
|
|
465
|
+
| `cursor_param` | string | inherits | Name of cursor parameter |
|
|
466
|
+
| `page_param` | string | inherits | Name of page parameter |
|
|
467
|
+
| `per_page_param` | string | inherits | Name of per-page parameter |
|
|
468
|
+
| `data_path` | string | inherits | JSON path to items array in response |
|
|
469
|
+
| `total_path` | string | inherits | JSON path to total count |
|
|
470
|
+
| `next_cursor_path` | string | `null` | JSON path to next cursor value |
|
|
471
|
+
| `total_pages_path` | string | `null` | JSON path to total page count |
|
|
472
|
+
| `default_page_size` | int | inherits | Default page size |
|
|
473
|
+
| `max_page_size` | int | `null` | Maximum allowed page size |
|
|
474
|
+
|
|
475
|
+
### Usage
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
from client import list_users_iter, alist_users_iter
|
|
479
|
+
|
|
480
|
+
# Iterate all users (handles pagination automatically)
|
|
481
|
+
for user in list_users_iter():
|
|
482
|
+
print(user.name)
|
|
483
|
+
|
|
484
|
+
# Async variant
|
|
485
|
+
async for user in alist_users_iter():
|
|
486
|
+
print(user.name)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## 📊 DataFrame Conversion
|
|
492
|
+
|
|
493
|
+
When enabled, list-returning endpoints get additional methods that return pandas or polars DataFrames directly.
|
|
494
|
+
|
|
495
|
+
### Enabling DataFrame Methods
|
|
496
|
+
|
|
497
|
+
```yaml
|
|
498
|
+
documents:
|
|
499
|
+
- source: https://api.example.com/openapi.json
|
|
500
|
+
output: ./client
|
|
501
|
+
dataframe:
|
|
502
|
+
enabled: true
|
|
503
|
+
pandas: true # generate _df methods (default: true)
|
|
504
|
+
polars: true # generate _pl methods (default: false)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Generated Methods
|
|
508
|
+
|
|
509
|
+
| Original | Pandas | Polars |
|
|
510
|
+
|----------|--------|--------|
|
|
511
|
+
| `get_users()` | `get_users_df()` | `get_users_pl()` |
|
|
512
|
+
| `aget_users()` | `aget_users_df()` | `aget_users_pl()` |
|
|
513
|
+
|
|
514
|
+
### Usage
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
from client import list_pets_df, list_pets_pl
|
|
518
|
+
|
|
519
|
+
pdf = list_pets_df("available")
|
|
520
|
+
plf = list_pets_pl("available")
|
|
521
|
+
|
|
522
|
+
# Override the data extraction path at call time
|
|
523
|
+
df = list_pets_df("available", path="response.data.pets")
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Nested Response Paths
|
|
527
|
+
|
|
528
|
+
```yaml
|
|
529
|
+
dataframe:
|
|
530
|
+
enabled: true
|
|
531
|
+
pandas: true
|
|
532
|
+
default_path: data.items # default for all endpoints
|
|
533
|
+
endpoints:
|
|
534
|
+
get_users:
|
|
535
|
+
path: data.users # override for this endpoint
|
|
536
|
+
get_analytics:
|
|
537
|
+
path: response.events
|
|
538
|
+
polars: true
|
|
539
|
+
pandas: false
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### DataFrame Options
|
|
543
|
+
|
|
544
|
+
| Option | Type | Default | Description |
|
|
545
|
+
|--------|------|---------|-------------|
|
|
546
|
+
| `enabled` | bool | `false` | Enable DataFrame generation |
|
|
547
|
+
| `pandas` | bool | `true` | Generate `_df` (pandas) methods |
|
|
548
|
+
| `polars` | bool | `false` | Generate `_pl` (polars) methods |
|
|
549
|
+
| `default_path` | string | `null` | Default JSON path to extract data |
|
|
550
|
+
| `include_all` | bool | `true` | Generate for all list-returning endpoints |
|
|
551
|
+
| `endpoints` | object | `{}` | Per-endpoint overrides |
|
|
552
|
+
|
|
553
|
+
### Per-Endpoint DataFrame Options
|
|
554
|
+
|
|
555
|
+
| Option | Type | Default | Description |
|
|
556
|
+
|--------|------|---------|-------------|
|
|
557
|
+
| `enabled` | bool\|null | inherits | Override whether to generate methods |
|
|
558
|
+
| `path` | string | inherits | JSON path to extract data |
|
|
559
|
+
| `pandas` | bool\|null | inherits | Override pandas generation |
|
|
560
|
+
| `polars` | bool\|null | inherits | Override polars generation |
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## 💾 File Export
|
|
565
|
+
|
|
566
|
+
When enabled, list-returning endpoints get streaming export helpers for writing responses directly to files (local or remote via UPath).
|
|
567
|
+
|
|
568
|
+
### Enabling Export
|
|
569
|
+
|
|
570
|
+
```yaml
|
|
571
|
+
documents:
|
|
572
|
+
- source: https://api.example.com/openapi.json
|
|
573
|
+
output: ./client
|
|
574
|
+
export:
|
|
575
|
+
enabled: true
|
|
576
|
+
formats:
|
|
577
|
+
- csv
|
|
578
|
+
- jsonl
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Parquet support requires the `pyarrow` extra:
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
pip install otterapi[parquet]
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Export Options
|
|
588
|
+
|
|
589
|
+
| Option | Type | Default | Description |
|
|
590
|
+
|--------|------|---------|-------------|
|
|
591
|
+
| `enabled` | bool | `false` | Enable export helper generation |
|
|
592
|
+
| `formats` | list | `[csv, jsonl]` | Default formats: `csv`, `tsv`, `jsonl`, `parquet` |
|
|
593
|
+
| `default_path` | string | `null` | Default JSON path to extract list data |
|
|
594
|
+
| `include_all` | bool | `true` | Generate helpers for all list-returning endpoints |
|
|
595
|
+
| `batch_size` | int | `1000` | Batch size used when streaming pages to disk |
|
|
596
|
+
| `endpoints` | object | `{}` | Per-endpoint overrides |
|
|
597
|
+
|
|
598
|
+
### Per-Endpoint Export Options
|
|
599
|
+
|
|
600
|
+
```yaml
|
|
601
|
+
export:
|
|
602
|
+
enabled: true
|
|
603
|
+
formats: [csv, jsonl]
|
|
604
|
+
endpoints:
|
|
605
|
+
list_users:
|
|
606
|
+
formats: [parquet] # only parquet for this endpoint
|
|
607
|
+
path: data.users
|
|
608
|
+
get_events:
|
|
609
|
+
enabled: false # disable export for this endpoint
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
| Option | Type | Default | Description |
|
|
613
|
+
|--------|------|---------|-------------|
|
|
614
|
+
| `enabled` | bool\|null | inherits | Override whether to generate helpers |
|
|
615
|
+
| `path` | string | inherits | JSON path to extract data |
|
|
616
|
+
| `formats` | list | inherits | Override formats for this endpoint |
|
|
617
|
+
|
|
618
|
+
### Usage
|
|
619
|
+
|
|
620
|
+
```python
|
|
621
|
+
from client import export_list_users_csv, export_list_users_parquet
|
|
622
|
+
|
|
623
|
+
# Export directly to a file
|
|
624
|
+
export_list_users_csv("output/users.csv")
|
|
625
|
+
export_list_users_parquet("s3://my-bucket/users.parquet") # UPath remote targets work too
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 🔓 Response Unwrapping
|
|
631
|
+
|
|
632
|
+
For APIs that wrap all responses in an envelope (e.g. `{"data": {...}, "meta": {...}}`), response unwrapping makes endpoints return just the inner data automatically.
|
|
633
|
+
|
|
634
|
+
### Enabling Response Unwrap
|
|
635
|
+
|
|
636
|
+
```yaml
|
|
637
|
+
documents:
|
|
638
|
+
- source: https://api.example.com/openapi.json
|
|
639
|
+
output: ./client
|
|
640
|
+
response_unwrap:
|
|
641
|
+
enabled: true
|
|
642
|
+
data_path: data # default path (default: "data")
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Per-Endpoint Overrides
|
|
646
|
+
|
|
647
|
+
```yaml
|
|
648
|
+
response_unwrap:
|
|
649
|
+
enabled: true
|
|
650
|
+
data_path: data
|
|
651
|
+
endpoints:
|
|
652
|
+
get_user:
|
|
653
|
+
data_path: result.user # this endpoint uses a different path
|
|
654
|
+
get_raw_data:
|
|
655
|
+
enabled: false # don't unwrap this endpoint
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Response Unwrap Options
|
|
659
|
+
|
|
660
|
+
| Option | Type | Default | Description |
|
|
661
|
+
|--------|------|---------|-------------|
|
|
662
|
+
| `enabled` | bool | `false` | Enable response unwrapping |
|
|
663
|
+
| `data_path` | string | `data` | Default JSON path to extract from all responses |
|
|
664
|
+
| `endpoints` | object | `{}` | Per-endpoint overrides |
|
|
665
|
+
|
|
666
|
+
### Per-Endpoint Options
|
|
667
|
+
|
|
668
|
+
| Option | Type | Default | Description |
|
|
669
|
+
|--------|------|---------|-------------|
|
|
670
|
+
| `enabled` | bool\|null | inherits | Override whether to unwrap |
|
|
671
|
+
| `data_path` | string | inherits | JSON path for this endpoint |
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## 📖 Using Generated Code
|
|
676
|
+
|
|
677
|
+
### Direct Function Imports
|
|
678
|
+
|
|
679
|
+
```python
|
|
680
|
+
from client import get_user, create_user, list_orders
|
|
681
|
+
|
|
682
|
+
# Async versions are prefixed with 'a'
|
|
683
|
+
from client import aget_user, acreate_user, alist_orders
|
|
684
|
+
|
|
685
|
+
# Sync
|
|
686
|
+
user = get_user(user_id=123)
|
|
687
|
+
|
|
688
|
+
# Async
|
|
689
|
+
import asyncio
|
|
690
|
+
|
|
691
|
+
async def main():
|
|
692
|
+
user = await aget_user(user_id=123)
|
|
693
|
+
|
|
694
|
+
asyncio.run(main())
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Module-Specific Imports (with splitting)
|
|
698
|
+
|
|
699
|
+
```python
|
|
700
|
+
from client.users import get_user, create_user
|
|
701
|
+
from client.orders import list_orders, get_order
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Using the Client Class
|
|
705
|
+
|
|
706
|
+
```python
|
|
707
|
+
from client import Client
|
|
708
|
+
|
|
709
|
+
client = Client(
|
|
710
|
+
base_url='https://api.example.com',
|
|
711
|
+
timeout=30.0,
|
|
712
|
+
headers={'Authorization': 'Bearer your-token'},
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
user = client.get_user(user_id=123)
|
|
716
|
+
|
|
717
|
+
async def main():
|
|
718
|
+
user = await client.aget_user(user_id=123)
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Working with Models
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
from client.models import User, CreateUserRequest
|
|
725
|
+
|
|
726
|
+
new_user = CreateUserRequest(name='John Doe', email='john@example.com')
|
|
727
|
+
user = create_user(body=new_user)
|
|
728
|
+
print(user.id, user.email)
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## 🔧 CLI Reference
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
# Generate from default config (otter.yml, otter.yaml, or pyproject.toml)
|
|
737
|
+
otter generate
|
|
738
|
+
|
|
739
|
+
# Generate from specific config file
|
|
740
|
+
otter generate -c my-config.yml
|
|
741
|
+
|
|
742
|
+
# Initialize a new config file
|
|
743
|
+
otter init
|
|
744
|
+
|
|
745
|
+
# Validate configuration
|
|
746
|
+
otter validate
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## 🐍 Programmatic API
|
|
752
|
+
|
|
753
|
+
```python
|
|
754
|
+
from otterapi.codegen import Codegen
|
|
755
|
+
from otterapi.config import DocumentConfig, PaginationConfig, ExportConfig
|
|
756
|
+
|
|
757
|
+
config = DocumentConfig(
|
|
758
|
+
source='https://petstore3.swagger.io/api/v3/openapi.json',
|
|
759
|
+
output='./client',
|
|
760
|
+
pagination=PaginationConfig(enabled=True),
|
|
761
|
+
export=ExportConfig(enabled=True, formats=['csv', 'parquet']),
|
|
762
|
+
)
|
|
763
|
+
Codegen(config).generate()
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## ❗ Error Handling
|
|
769
|
+
|
|
770
|
+
Generated clients raise a typed exception hierarchy rooted at `BaseAPIError`:
|
|
771
|
+
|
|
772
|
+
```python
|
|
773
|
+
from my_client import (
|
|
774
|
+
Client, list_users,
|
|
775
|
+
BaseAPIError, # catches every API error
|
|
776
|
+
ClientError, # all 4xx
|
|
777
|
+
ServerError, # all 5xx
|
|
778
|
+
NotFoundError, # 404
|
|
779
|
+
RateLimitError, # 429
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
try:
|
|
783
|
+
users = list_users(client=Client())
|
|
784
|
+
except NotFoundError as e:
|
|
785
|
+
log.warning('not found: %s', e.detail)
|
|
786
|
+
except RateLimitError:
|
|
787
|
+
backoff_and_retry()
|
|
788
|
+
except ServerError:
|
|
789
|
+
page_oncall()
|
|
790
|
+
except BaseAPIError as e:
|
|
791
|
+
log.error('unexpected %d: %s', e.status_code, e.detail)
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Mapped status codes: 400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504. Other 4xx/5xx fall through to `ClientError`/`ServerError`. Subclass `BaseAPIError` in your `client.py` to customize error parsing.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## ♻️ Regenerating after spec changes
|
|
799
|
+
|
|
800
|
+
OtterAPI is **idempotent** for generated files:
|
|
801
|
+
|
|
802
|
+
- `models.py`, `endpoints.py`, `_client.py`, `_pagination.py`, `_export.py`, `_dataframe.py` are **rewritten on every run** — never edit them by hand.
|
|
803
|
+
- `client.py` is generated **once** and left alone — this is your customization seam.
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## 🛠 Development
|
|
808
|
+
|
|
809
|
+
```bash
|
|
810
|
+
git clone https://github.com/danplischke/otterapi.git
|
|
811
|
+
cd otterapi
|
|
812
|
+
uv sync
|
|
813
|
+
uv run pytest
|
|
814
|
+
uv run pytest --cov=otterapi
|
|
815
|
+
uv run ruff format .
|
|
816
|
+
uv run ruff check .
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## 📄 License
|
|
822
|
+
|
|
823
|
+
MIT License — see LICENSE for details.
|