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.
Files changed (89) hide show
  1. otterapi-0.1.2/PKG-INFO +823 -0
  2. otterapi-0.1.2/README.md +793 -0
  3. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/codegen.py +6 -1
  4. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/splitting.py +147 -7
  5. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/config.py +9 -0
  6. otterapi-0.1.0/PKG-INFO +0 -709
  7. otterapi-0.1.0/README.md +0 -679
  8. {otterapi-0.1.0 → otterapi-0.1.2}/.gitignore +0 -0
  9. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/__init__.py +0 -0
  10. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/__main__.py +0 -0
  11. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/cli.py +0 -0
  12. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/__init__.py +0 -0
  13. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/_body_builder.py +0 -0
  14. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/_features.py +0 -0
  15. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/_openapi_adapter.py +0 -0
  16. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/ast_utils.py +0 -0
  17. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/client.py +0 -0
  18. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/dataframes.py +0 -0
  19. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/endpoints.py +0 -0
  20. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/export.py +0 -0
  21. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/pagination.py +0 -0
  22. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/__init__.py +0 -0
  23. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_client_stub.py.tpl +0 -0
  24. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_concurrency.py +0 -0
  25. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_dataframe.py +0 -0
  26. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_export.py +0 -0
  27. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_models_mixin.py +0 -0
  28. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_pagination.py +0 -0
  29. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/runtime/_retry.py +0 -0
  30. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/schema.py +0 -0
  31. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/types.py +0 -0
  32. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/codegen/utils.py +0 -0
  33. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/exceptions.py +0 -0
  34. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/__init__.py +0 -0
  35. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/constants.py +0 -0
  36. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v2/__init__.py +0 -0
  37. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v2/spec.json +0 -0
  38. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v2/v2.py +0 -0
  39. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3/__init__.py +0 -0
  40. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3/spec.json +0 -0
  41. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3/v3.py +0 -0
  42. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_1/__init__.py +0 -0
  43. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_1/spec.json +0 -0
  44. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_1/v3_1.py +0 -0
  45. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_2/__init__.py +0 -0
  46. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_2/spec.json +0 -0
  47. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/v3_2/v3_2.py +0 -0
  48. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/openapi/warnings.py +0 -0
  49. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/__init__.py +0 -0
  50. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/__init__.py +0 -0
  51. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/constraints/spec.yaml +0 -0
  52. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/dataframe/config.yaml +0 -0
  53. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/dataframe/spec.yaml +0 -0
  54. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/discriminator/spec.yaml +0 -0
  55. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/paginated/config.yaml +0 -0
  56. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/fixtures/golden/paginated/spec.yaml +0 -0
  57. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_ast_utils.py +0 -0
  58. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_body_builder.py +0 -0
  59. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_client_retry_lifecycle.py +0 -0
  60. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_codegen.py +0 -0
  61. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_codegen_structure.py +0 -0
  62. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_concurrency_runtime.py +0 -0
  63. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_config_errors.py +0 -0
  64. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_dataframe.py +0 -0
  65. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_discriminated_unions.py +0 -0
  66. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_exceptions.py +0 -0
  67. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_export.py +0 -0
  68. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_extra_forbid.py +0 -0
  69. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_feature_modules.py +0 -0
  70. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_html_repr.py +0 -0
  71. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_init_scaffold.py +0 -0
  72. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_not_schema.py +0 -0
  73. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_nullable_upgrade.py +0 -0
  74. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_openapi_adapter.py +0 -0
  75. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_openapi_support.py +0 -0
  76. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_openapi_upgrade.py +0 -0
  77. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_pagination.py +0 -0
  78. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_per_status_errors.py +0 -0
  79. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_public_api.py +0 -0
  80. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_regen_script.py +0 -0
  81. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_retry_runtime.py +0 -0
  82. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_runtime_guards.py +0 -0
  83. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_schema_constraints.py +0 -0
  84. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_smoke_generated_client.py +0 -0
  85. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_config.py +0 -0
  86. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_integration.py +0 -0
  87. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_resolver.py +0 -0
  88. {otterapi-0.1.0 → otterapi-0.1.2}/otterapi/tests/test_splitting_tree.py +0 -0
  89. {otterapi-0.1.0 → otterapi-0.1.2}/pyproject.toml +0 -0
@@ -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.