pyopenapi-gen 2.7.2__py3-none-any.whl
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.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +28 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyopenapi-gen
|
|
3
|
+
Version: 2.7.2
|
|
4
|
+
Summary: Modern, async-first Python client generator for OpenAPI specifications with advanced cycle detection and unified type resolution
|
|
5
|
+
Project-URL: Homepage, https://github.com/your-org/pyopenapi-gen
|
|
6
|
+
Project-URL: Documentation, https://github.com/your-org/pyopenapi-gen/blob/main/README.md
|
|
7
|
+
Project-URL: Repository, https://github.com/your-org/pyopenapi-gen
|
|
8
|
+
Project-URL: Issues, https://github.com/your-org/pyopenapi-gen/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/your-org/pyopenapi-gen/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Bug Reports, https://github.com/your-org/pyopenapi-gen/issues
|
|
11
|
+
Project-URL: Source Code, https://github.com/your-org/pyopenapi-gen
|
|
12
|
+
Author-email: Mindhive Oy <contact@mindhive.fi>
|
|
13
|
+
Maintainer-email: Ville Venäläinen | Mindhive Oy <ville@mindhive.fi>
|
|
14
|
+
License: MIT
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Keywords: api,async,client,code-generation,enterprise,generator,http,openapi,python,rest,swagger,type-safe
|
|
17
|
+
Classifier: Development Status :: 4 - Beta
|
|
18
|
+
Classifier: Environment :: Console
|
|
19
|
+
Classifier: Framework :: AsyncIO
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
+
Classifier: Natural Language :: English
|
|
23
|
+
Classifier: Operating System :: MacOS
|
|
24
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
25
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
26
|
+
Classifier: Programming Language :: Python :: 3
|
|
27
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
29
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
30
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
31
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
32
|
+
Classifier: Topic :: System :: Networking
|
|
33
|
+
Classifier: Typing :: Typed
|
|
34
|
+
Requires-Python: <4.0.0,>=3.12
|
|
35
|
+
Requires-Dist: cattrs>=25.3.0
|
|
36
|
+
Requires-Dist: click>=8.0.0
|
|
37
|
+
Requires-Dist: dataclass-wizard<0.37.0,>=0.36.1
|
|
38
|
+
Requires-Dist: httpx>=0.24.0
|
|
39
|
+
Requires-Dist: openapi-core>=0.19
|
|
40
|
+
Requires-Dist: openapi-spec-validator>=0.7
|
|
41
|
+
Requires-Dist: pyyaml>=6.0
|
|
42
|
+
Requires-Dist: typer>=0.14.0
|
|
43
|
+
Requires-Dist: urllib3<3.0.0,>=2.6.0
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: bandit[toml]>=1.7.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
|
47
|
+
Requires-Dist: dataclass-wizard>=0.22.0; extra == 'dev'
|
|
48
|
+
Requires-Dist: httpx>=0.24.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: mypy>=1.7; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-timeout>=2.1.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest-xdist>=3.0.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
55
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
56
|
+
Requires-Dist: safety>=2.0.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
|
|
58
|
+
Requires-Dist: types-toml>=0.10.8; extra == 'dev'
|
|
59
|
+
Description-Content-Type: text/markdown
|
|
60
|
+
|
|
61
|
+
# PyOpenAPI Generator
|
|
62
|
+
|
|
63
|
+
[](https://python.org)
|
|
64
|
+
[](https://opensource.org/licenses/MIT)
|
|
65
|
+
[](https://github.com/psf/black)
|
|
66
|
+
[](https://github.com/astral-sh/ruff)
|
|
67
|
+
|
|
68
|
+
**Modern, enterprise-grade Python client generator for OpenAPI specifications.**
|
|
69
|
+
|
|
70
|
+
Generate async-first Python clients from OpenAPI specs with complete type safety, automatic field mapping, and zero runtime dependencies.
|
|
71
|
+
|
|
72
|
+
## Why PyOpenAPI Generator?
|
|
73
|
+
|
|
74
|
+
### Modern Python Architecture
|
|
75
|
+
|
|
76
|
+
- **Async-First**: All operations use `async`/`await` with `httpx` for high performance
|
|
77
|
+
- **Complete Type Safety**: Full type hints, dataclass models, and mypy strict mode compatibility
|
|
78
|
+
- **Truly Independent**: Generated clients require no runtime dependency on this package
|
|
79
|
+
|
|
80
|
+
### Enterprise-Grade Features
|
|
81
|
+
|
|
82
|
+
- **Complex Schema Handling**: Advanced cycle detection for circular references and deep nesting
|
|
83
|
+
- **Automatic Field Mapping**: Seamless conversion between API naming (snake_case, camelCase) and Python conventions
|
|
84
|
+
- **Pluggable Authentication**: Bearer tokens, API keys, OAuth2, custom auth, or combine multiple strategies
|
|
85
|
+
- **Streaming Support**: Built-in SSE and binary streaming for real-time data
|
|
86
|
+
|
|
87
|
+
### Superior Developer Experience
|
|
88
|
+
|
|
89
|
+
- **Rich IDE Support**: Full autocomplete, inline docs, and type checking in modern editors
|
|
90
|
+
- **Tag-Based Organization**: Operations automatically grouped by OpenAPI tags for intuitive navigation
|
|
91
|
+
- **Structured Exceptions**: Type-safe error handling with meaningful exception hierarchy
|
|
92
|
+
- **Easy Testing**: Auto-generated Protocol classes for each endpoint enable strict type-safe mocking
|
|
93
|
+
|
|
94
|
+
## Installation
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install pyopenapi-gen
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or with Poetry:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
poetry add pyopenapi-gen
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## ⚡ Quick Start
|
|
107
|
+
|
|
108
|
+
### 1. Generate Your First Client
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pyopenapi-gen openapi.yaml \
|
|
112
|
+
--project-root . \
|
|
113
|
+
--output-package my_api_client
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This creates a complete Python package at `./my_api_client/` with:
|
|
117
|
+
|
|
118
|
+
- Type-safe models from your schemas
|
|
119
|
+
- Async methods for all operations
|
|
120
|
+
- Built-in authentication support
|
|
121
|
+
- Complete independence from this generator
|
|
122
|
+
|
|
123
|
+
### 2. Use the Generated Client
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import asyncio
|
|
127
|
+
from my_api_client.client import APIClient
|
|
128
|
+
from my_api_client.core.config import ClientConfig
|
|
129
|
+
from my_api_client.core.http_transport import HttpxTransport
|
|
130
|
+
from my_api_client.core.auth.plugins import BearerAuth
|
|
131
|
+
|
|
132
|
+
async def main():
|
|
133
|
+
# Configure the client
|
|
134
|
+
config = ClientConfig(
|
|
135
|
+
base_url="https://api.example.com",
|
|
136
|
+
timeout=30.0
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Optional: Add authentication
|
|
140
|
+
auth = BearerAuth("your-api-token")
|
|
141
|
+
transport = HttpxTransport(
|
|
142
|
+
base_url=config.base_url,
|
|
143
|
+
timeout=config.timeout,
|
|
144
|
+
auth=auth
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Use as async context manager
|
|
148
|
+
async with APIClient(config, transport=transport) as client:
|
|
149
|
+
# Type-safe API calls with full IDE support
|
|
150
|
+
users = await client.users.list_users(limit=10)
|
|
151
|
+
|
|
152
|
+
# All operations are fully typed
|
|
153
|
+
user = await client.users.get_user(user_id=123)
|
|
154
|
+
print(f"User: {user.name}, Email: {user.email}")
|
|
155
|
+
|
|
156
|
+
asyncio.run(main())
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Using as a Library (Programmatic API)
|
|
160
|
+
|
|
161
|
+
The generator was designed to work both as a CLI tool and as a Python library. Programmatic usage enables integration with build systems, CI/CD pipelines, code generators, and custom tooling. You get the same powerful code generation capabilities with full Python API access.
|
|
162
|
+
|
|
163
|
+
### How to Use Programmatically
|
|
164
|
+
|
|
165
|
+
#### Basic Usage
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from pyopenapi_gen import generate_client
|
|
169
|
+
|
|
170
|
+
# Simple client generation
|
|
171
|
+
files = generate_client(
|
|
172
|
+
spec_path="input/openapi.yaml",
|
|
173
|
+
project_root=".",
|
|
174
|
+
output_package="pyapis.my_client"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
print(f"Generated {len(files)} files")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Advanced Usage with All Options
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from pyopenapi_gen import generate_client, GenerationError
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
files = generate_client(
|
|
187
|
+
spec_path="input/openapi.yaml",
|
|
188
|
+
project_root=".",
|
|
189
|
+
output_package="pyapis.my_client",
|
|
190
|
+
core_package="pyapis.core", # Optional shared core
|
|
191
|
+
force=True, # Overwrite without diff check
|
|
192
|
+
no_postprocess=False, # Run Black + mypy
|
|
193
|
+
verbose=True # Show progress
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Process generated files
|
|
197
|
+
for file_path in files:
|
|
198
|
+
print(f"Generated: {file_path}")
|
|
199
|
+
|
|
200
|
+
except GenerationError as e:
|
|
201
|
+
print(f"Generation failed: {e}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Multi-Client Generation Script
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from pyopenapi_gen import generate_client
|
|
208
|
+
from pathlib import Path
|
|
209
|
+
|
|
210
|
+
# Configuration for multiple clients
|
|
211
|
+
clients = [
|
|
212
|
+
{"spec": "api_v1.yaml", "package": "pyapis.client_v1"},
|
|
213
|
+
{"spec": "api_v2.yaml", "package": "pyapis.client_v2"},
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
# Shared core package
|
|
217
|
+
core_package = "pyapis.core"
|
|
218
|
+
|
|
219
|
+
# Generate all clients
|
|
220
|
+
for client_config in clients:
|
|
221
|
+
print(f"Generating {client_config['package']}...")
|
|
222
|
+
|
|
223
|
+
generate_client(
|
|
224
|
+
spec_path=client_config["spec"],
|
|
225
|
+
project_root=".",
|
|
226
|
+
output_package=client_config["package"],
|
|
227
|
+
core_package=core_package,
|
|
228
|
+
force=True,
|
|
229
|
+
verbose=True
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
print("All clients generated successfully!")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### API Reference
|
|
236
|
+
|
|
237
|
+
#### `generate_client()` Function
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
def generate_client(
|
|
241
|
+
spec_path: str,
|
|
242
|
+
project_root: str,
|
|
243
|
+
output_package: str,
|
|
244
|
+
core_package: str | None = None,
|
|
245
|
+
force: bool = False,
|
|
246
|
+
no_postprocess: bool = False,
|
|
247
|
+
verbose: bool = False,
|
|
248
|
+
) -> List[Path]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Parameters**:
|
|
252
|
+
|
|
253
|
+
- `spec_path`: Path to OpenAPI spec file (YAML or JSON)
|
|
254
|
+
- `project_root`: Root directory of your Python project
|
|
255
|
+
- `output_package`: Python package name (e.g., `'pyapis.my_client'`)
|
|
256
|
+
- `core_package`: Optional shared core package name (defaults to `{output_package}.core`)
|
|
257
|
+
- `force`: Skip diff check and overwrite existing output
|
|
258
|
+
- `no_postprocess`: Skip Black formatting and mypy type checking
|
|
259
|
+
- `verbose`: Print detailed progress information
|
|
260
|
+
|
|
261
|
+
**Returns**: List of `Path` objects for all generated files
|
|
262
|
+
|
|
263
|
+
**Raises**: `GenerationError` if generation fails
|
|
264
|
+
|
|
265
|
+
#### `ClientGenerator` Class (Advanced)
|
|
266
|
+
|
|
267
|
+
For advanced use cases requiring more control:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from pyopenapi_gen import ClientGenerator, GenerationError
|
|
271
|
+
from pathlib import Path
|
|
272
|
+
|
|
273
|
+
# Create generator with custom settings
|
|
274
|
+
generator = ClientGenerator(verbose=True)
|
|
275
|
+
|
|
276
|
+
# Generate with full control
|
|
277
|
+
try:
|
|
278
|
+
files = generator.generate(
|
|
279
|
+
spec_path="openapi.yaml",
|
|
280
|
+
project_root=Path("."),
|
|
281
|
+
output_package="pyapis.my_client",
|
|
282
|
+
core_package="pyapis.core",
|
|
283
|
+
force=False,
|
|
284
|
+
no_postprocess=False
|
|
285
|
+
)
|
|
286
|
+
except GenerationError as e:
|
|
287
|
+
print(f"Generation failed: {e}")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### `GenerationError` Exception
|
|
291
|
+
|
|
292
|
+
Raised when generation fails. Contains contextual information about the failure:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from pyopenapi_gen import generate_client, GenerationError
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
generate_client(
|
|
299
|
+
spec_path="invalid.yaml",
|
|
300
|
+
project_root=".",
|
|
301
|
+
output_package="test"
|
|
302
|
+
)
|
|
303
|
+
except GenerationError as e:
|
|
304
|
+
# Exception message includes context
|
|
305
|
+
print(f"Error: {e}")
|
|
306
|
+
# Typical causes:
|
|
307
|
+
# - Invalid OpenAPI specification
|
|
308
|
+
# - File I/O errors
|
|
309
|
+
# - Type checking failures
|
|
310
|
+
# - Invalid project structure
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Configuration Options
|
|
314
|
+
|
|
315
|
+
### Standalone Client (Default)
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
pyopenapi-gen openapi.yaml \
|
|
319
|
+
--project-root . \
|
|
320
|
+
--output-package my_api_client
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Creates self-contained client with embedded core dependencies.
|
|
324
|
+
|
|
325
|
+
### Shared Core (Multiple Clients)
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
pyopenapi-gen openapi.yaml \
|
|
329
|
+
--project-root . \
|
|
330
|
+
--output-package clients.api_client \
|
|
331
|
+
--core-package clients.core
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Multiple clients share a single core implementation.
|
|
335
|
+
|
|
336
|
+
### Additional Options
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
--force # Overwrite without prompting
|
|
340
|
+
--no-postprocess # Skip formatting and type checking
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Authentication
|
|
344
|
+
|
|
345
|
+
The generated clients support flexible authentication through the transport layer. Authentication plugins modify requests before they're sent.
|
|
346
|
+
|
|
347
|
+
### Bearer Token Authentication
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
from my_api_client.core.auth.plugins import BearerAuth
|
|
351
|
+
from my_api_client.core.http_transport import HttpxTransport
|
|
352
|
+
|
|
353
|
+
auth = BearerAuth("your-api-token")
|
|
354
|
+
transport = HttpxTransport(
|
|
355
|
+
base_url="https://api.example.com",
|
|
356
|
+
auth=auth
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
async with APIClient(config, transport=transport) as client:
|
|
360
|
+
# All requests automatically include: Authorization: Bearer your-api-token
|
|
361
|
+
users = await client.users.list_users()
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### API Key (Header, Query, or Cookie)
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
from my_api_client.core.auth.plugins import ApiKeyAuth
|
|
368
|
+
|
|
369
|
+
# API key in header
|
|
370
|
+
auth = ApiKeyAuth("your-key", location="header", name="X-API-Key")
|
|
371
|
+
|
|
372
|
+
# API key in query string
|
|
373
|
+
auth = ApiKeyAuth("your-key", location="query", name="api_key")
|
|
374
|
+
|
|
375
|
+
# API key in cookie
|
|
376
|
+
auth = ApiKeyAuth("your-key", location="cookie", name="session")
|
|
377
|
+
|
|
378
|
+
transport = HttpxTransport(
|
|
379
|
+
base_url="https://api.example.com",
|
|
380
|
+
auth=auth
|
|
381
|
+
)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### OAuth2 with Token Refresh
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from my_api_client.core.auth.plugins import OAuth2Auth
|
|
388
|
+
|
|
389
|
+
async def refresh_token(current_token: str) -> str:
|
|
390
|
+
# Your token refresh logic
|
|
391
|
+
# Call your auth server to get a new token
|
|
392
|
+
new_token = await get_new_token()
|
|
393
|
+
return new_token
|
|
394
|
+
|
|
395
|
+
auth = OAuth2Auth(
|
|
396
|
+
access_token="initial-token",
|
|
397
|
+
refresh_callback=refresh_token
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
transport = HttpxTransport(
|
|
401
|
+
base_url="https://api.example.com",
|
|
402
|
+
auth=auth
|
|
403
|
+
)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Composite Authentication (Multiple Auth Methods)
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
from my_api_client.core.auth.base import CompositeAuth
|
|
410
|
+
from my_api_client.core.auth.plugins import BearerAuth, HeadersAuth
|
|
411
|
+
|
|
412
|
+
# Combine multiple authentication methods
|
|
413
|
+
auth = CompositeAuth(
|
|
414
|
+
BearerAuth("api-token"),
|
|
415
|
+
HeadersAuth({"X-Client-ID": "my-app", "X-Version": "1.0"})
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
transport = HttpxTransport(
|
|
419
|
+
base_url="https://api.example.com",
|
|
420
|
+
auth=auth
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# All requests include both Authorization header and custom headers
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Custom Authentication
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
from typing import Any
|
|
430
|
+
from my_api_client.core.auth.base import BaseAuth
|
|
431
|
+
|
|
432
|
+
class CustomAuth(BaseAuth):
|
|
433
|
+
"""Your custom authentication logic"""
|
|
434
|
+
|
|
435
|
+
def __init__(self, api_key: str, secret: str):
|
|
436
|
+
self.api_key = api_key
|
|
437
|
+
self.secret = secret
|
|
438
|
+
|
|
439
|
+
async def authenticate_request(self, request_args: dict[str, Any]) -> dict[str, Any]:
|
|
440
|
+
# Add custom authentication logic
|
|
441
|
+
headers = dict(request_args.get("headers", {}))
|
|
442
|
+
headers["X-API-Key"] = self.api_key
|
|
443
|
+
headers["X-Signature"] = self._generate_signature()
|
|
444
|
+
request_args["headers"] = headers
|
|
445
|
+
return request_args
|
|
446
|
+
|
|
447
|
+
def _generate_signature(self) -> str:
|
|
448
|
+
# Your signature generation logic
|
|
449
|
+
return "signature"
|
|
450
|
+
|
|
451
|
+
auth = CustomAuth("key", "secret")
|
|
452
|
+
transport = HttpxTransport(base_url="https://api.example.com", auth=auth)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Advanced Features
|
|
456
|
+
|
|
457
|
+
### Error Handling
|
|
458
|
+
|
|
459
|
+
The generated client raises structured exceptions for all non-2xx responses:
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
from my_api_client.core.exceptions import HTTPError, ClientError, ServerError
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
user = await client.users.get_user(user_id=123)
|
|
466
|
+
print(f"Found user: {user.name}")
|
|
467
|
+
|
|
468
|
+
except ClientError as e:
|
|
469
|
+
# 4xx errors - client-side issues
|
|
470
|
+
print(f"Client error {e.status_code}: {e.response.text}")
|
|
471
|
+
if e.status_code == 404:
|
|
472
|
+
print("User not found")
|
|
473
|
+
elif e.status_code == 401:
|
|
474
|
+
print("Authentication required")
|
|
475
|
+
|
|
476
|
+
except ServerError as e:
|
|
477
|
+
# 5xx errors - server-side issues
|
|
478
|
+
print(f"Server error {e.status_code}: {e.response.text}")
|
|
479
|
+
|
|
480
|
+
except HTTPError as e:
|
|
481
|
+
# Catch-all for any HTTP errors
|
|
482
|
+
print(f"HTTP error {e.status_code}: {e.response.text}")
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Streaming Responses
|
|
486
|
+
|
|
487
|
+
For operations that return streaming data (like SSE or file downloads):
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
# Server-Sent Events (SSE)
|
|
491
|
+
async for event in client.events.stream_events():
|
|
492
|
+
print(f"Received event: {event}")
|
|
493
|
+
|
|
494
|
+
# Binary streaming (files, large downloads)
|
|
495
|
+
async with client.files.download_file(file_id=123) as response:
|
|
496
|
+
async for chunk in response:
|
|
497
|
+
# Process binary chunks
|
|
498
|
+
file.write(chunk)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Automatic Field Name Mapping
|
|
502
|
+
|
|
503
|
+
Generated models use cattrs with Meta class for seamless API ↔ Python field name conversion:
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
from my_api_client.models.user import User
|
|
507
|
+
|
|
508
|
+
# API returns camelCase: {"firstName": "John", "lastName": "Doe"}
|
|
509
|
+
# Python uses snake_case automatically
|
|
510
|
+
user_data = await client.users.get_user(user_id=1)
|
|
511
|
+
print(user_data.first_name) # "John" - automatically mapped
|
|
512
|
+
print(user_data.last_name) # "Doe"
|
|
513
|
+
|
|
514
|
+
# Serialization back to API format works automatically
|
|
515
|
+
new_user = User(first_name="Jane", last_name="Smith")
|
|
516
|
+
created = await client.users.create_user(user=new_user)
|
|
517
|
+
# Sends: {"firstName": "Jane", "lastName": "Smith"}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Type Safety and IDE Support
|
|
521
|
+
|
|
522
|
+
All generated code includes complete type hints:
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
# Your IDE provides autocomplete for all methods
|
|
526
|
+
client.users. # IDE shows: list_users(), get_user(), create_user(), etc.
|
|
527
|
+
|
|
528
|
+
# All parameters are typed
|
|
529
|
+
await client.users.create_user(
|
|
530
|
+
user=User( # IDE autocompletes User fields
|
|
531
|
+
name="John",
|
|
532
|
+
email="john@example.com",
|
|
533
|
+
age=30 # Type checking catches wrong types
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Return types are fully specified
|
|
538
|
+
user: User = await client.users.get_user(user_id=1)
|
|
539
|
+
# mypy validates the entire chain
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## 💼 Common Use Cases
|
|
543
|
+
|
|
544
|
+
### Microservice Communication
|
|
545
|
+
|
|
546
|
+
```python
|
|
547
|
+
# Generate clients for internal services
|
|
548
|
+
pyopenapi-gen services/user-api/openapi.yaml \
|
|
549
|
+
--project-root . \
|
|
550
|
+
--output-package myapp.clients.users
|
|
551
|
+
|
|
552
|
+
pyopenapi-gen services/order-api/openapi.yaml \
|
|
553
|
+
--project-root . \
|
|
554
|
+
--output-package myapp.clients.orders
|
|
555
|
+
|
|
556
|
+
# Use in your application
|
|
557
|
+
from myapp.clients.users.client import APIClient as UserClient
|
|
558
|
+
from myapp.clients.orders.client import APIClient as OrderClient
|
|
559
|
+
|
|
560
|
+
async def process_order(user_id: int, order_id: int):
|
|
561
|
+
async with UserClient(user_config) as user_client:
|
|
562
|
+
user = await user_client.users.get_user(user_id=user_id)
|
|
563
|
+
|
|
564
|
+
async with OrderClient(order_config) as order_client:
|
|
565
|
+
order = await order_client.orders.get_order(order_id=order_id)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### SDK Generation for Public APIs
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
# Generate a distributable SDK
|
|
572
|
+
pyopenapi-gen public-api.yaml \
|
|
573
|
+
--project-root sdk \
|
|
574
|
+
--output-package mycompany_sdk
|
|
575
|
+
|
|
576
|
+
# Package structure for distribution:
|
|
577
|
+
# sdk/
|
|
578
|
+
# mycompany_sdk/
|
|
579
|
+
# __init__.py
|
|
580
|
+
# client.py
|
|
581
|
+
# models/
|
|
582
|
+
# endpoints/
|
|
583
|
+
# core/
|
|
584
|
+
# setup.py
|
|
585
|
+
# README.md
|
|
586
|
+
|
|
587
|
+
# Users install: pip install mycompany-sdk
|
|
588
|
+
# Users use: from mycompany_sdk.client import APIClient
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Multi-Environment Setup
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
# Generate once, configure per environment
|
|
595
|
+
from my_api_client.client import APIClient
|
|
596
|
+
from my_api_client.core.config import ClientConfig
|
|
597
|
+
from my_api_client.core.http_transport import HttpxTransport
|
|
598
|
+
from my_api_client.core.auth.plugins import BearerAuth
|
|
599
|
+
|
|
600
|
+
# Development
|
|
601
|
+
dev_config = ClientConfig(base_url="https://dev-api.example.com")
|
|
602
|
+
dev_auth = BearerAuth(os.getenv("DEV_API_TOKEN"))
|
|
603
|
+
dev_transport = HttpxTransport(dev_config.base_url, auth=dev_auth)
|
|
604
|
+
|
|
605
|
+
# Production
|
|
606
|
+
prod_config = ClientConfig(base_url="https://api.example.com")
|
|
607
|
+
prod_auth = BearerAuth(os.getenv("PROD_API_TOKEN"))
|
|
608
|
+
prod_transport = HttpxTransport(prod_config.base_url, auth=prod_auth)
|
|
609
|
+
|
|
610
|
+
# Use the same client code with different configs
|
|
611
|
+
async with APIClient(dev_config, transport=dev_transport) as client:
|
|
612
|
+
users = await client.users.list_users()
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Testing with Mock Servers
|
|
616
|
+
|
|
617
|
+
```python
|
|
618
|
+
# Point generated client at mock server for testing
|
|
619
|
+
import pytest
|
|
620
|
+
from my_api_client.client import APIClient
|
|
621
|
+
from my_api_client.core.config import ClientConfig
|
|
622
|
+
|
|
623
|
+
@pytest.fixture
|
|
624
|
+
async def api_client(mock_server_url):
|
|
625
|
+
"""API client pointing to mock server"""
|
|
626
|
+
config = ClientConfig(base_url=mock_server_url)
|
|
627
|
+
async with APIClient(config) as client:
|
|
628
|
+
yield client
|
|
629
|
+
|
|
630
|
+
async def test_user_creation(api_client):
|
|
631
|
+
# Mock server returns predictable responses
|
|
632
|
+
user = await api_client.users.create_user(
|
|
633
|
+
user={"name": "Test User", "email": "test@example.com"}
|
|
634
|
+
)
|
|
635
|
+
assert user.name == "Test User"
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Testing and Mocking
|
|
639
|
+
|
|
640
|
+
### Protocol-Based Design for Strict Type Safety
|
|
641
|
+
|
|
642
|
+
The generator **automatically creates Protocol classes** for every endpoint client, enforcing strict type safety through explicit contracts. This enables easy testing with compile-time guarantees.
|
|
643
|
+
|
|
644
|
+
#### Generated Protocol Structure
|
|
645
|
+
|
|
646
|
+
For each OpenAPI tag, the generator creates:
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
# Generated automatically from your OpenAPI spec:
|
|
650
|
+
|
|
651
|
+
@runtime_checkable
|
|
652
|
+
class UsersClientProtocol(Protocol):
|
|
653
|
+
"""Protocol defining the interface of UsersClient for dependency injection."""
|
|
654
|
+
|
|
655
|
+
async def get_user(self, user_id: int) -> User: ...
|
|
656
|
+
async def list_users(self, limit: int = 10) -> list[User]: ...
|
|
657
|
+
async def create_user(self, user: User) -> User: ...
|
|
658
|
+
|
|
659
|
+
class UsersClient(UsersClientProtocol):
|
|
660
|
+
"""Real implementation - explicitly implements the protocol"""
|
|
661
|
+
|
|
662
|
+
def __init__(self, transport: HttpTransport, base_url: str) -> None:
|
|
663
|
+
self._transport = transport
|
|
664
|
+
self.base_url = base_url
|
|
665
|
+
|
|
666
|
+
async def get_user(self, user_id: int) -> User:
|
|
667
|
+
# Real HTTP implementation
|
|
668
|
+
...
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Key Point**: The real implementation **explicitly inherits from the Protocol**, ensuring mypy validates it implements all methods correctly!
|
|
672
|
+
|
|
673
|
+
### Creating Type-Safe Mocks
|
|
674
|
+
|
|
675
|
+
Your mocks **must explicitly inherit from the generated Protocol** to get compile-time safety:
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
import pytest
|
|
679
|
+
from my_api_client.endpoints.users import UsersClientProtocol
|
|
680
|
+
from my_api_client.endpoints.orders import OrdersClientProtocol
|
|
681
|
+
from my_api_client.models.user import User
|
|
682
|
+
from my_api_client.models.order import Order
|
|
683
|
+
|
|
684
|
+
class MockUsersClient(UsersClientProtocol):
|
|
685
|
+
"""
|
|
686
|
+
Mock implementation that explicitly inherits from the generated Protocol.
|
|
687
|
+
|
|
688
|
+
CRITICAL: If UsersClientProtocol changes (new method, different signature),
|
|
689
|
+
mypy will immediately flag this class as incomplete.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
def __init__(self):
|
|
693
|
+
self.calls: list[tuple[str, dict]] = [] # Track method calls
|
|
694
|
+
self.mock_data: dict[int, User] = {} # Store mock responses
|
|
695
|
+
|
|
696
|
+
async def get_user(self, user_id: int) -> User:
|
|
697
|
+
"""Mock implementation of get_user"""
|
|
698
|
+
self.calls.append(("get_user", {"user_id": user_id}))
|
|
699
|
+
|
|
700
|
+
# Return mock data
|
|
701
|
+
if user_id in self.mock_data:
|
|
702
|
+
return self.mock_data[user_id]
|
|
703
|
+
|
|
704
|
+
# Return default mock user
|
|
705
|
+
return User(
|
|
706
|
+
id=user_id,
|
|
707
|
+
name="Test User",
|
|
708
|
+
email=f"user{user_id}@example.com"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
async def list_users(self, limit: int = 10) -> list[User]:
|
|
712
|
+
"""Mock implementation of list_users"""
|
|
713
|
+
self.calls.append(("list_users", {"limit": limit}))
|
|
714
|
+
return [
|
|
715
|
+
User(id=1, name="User 1", email="user1@example.com"),
|
|
716
|
+
User(id=2, name="User 2", email="user2@example.com"),
|
|
717
|
+
][:limit]
|
|
718
|
+
|
|
719
|
+
async def create_user(self, user: User) -> User:
|
|
720
|
+
"""Mock implementation of create_user"""
|
|
721
|
+
self.calls.append(("create_user", {"user": user}))
|
|
722
|
+
user.id = 123
|
|
723
|
+
return user
|
|
724
|
+
|
|
725
|
+
class MockOrdersClient(OrdersClientProtocol):
|
|
726
|
+
"""Mock OrdersClient - explicitly implements the protocol"""
|
|
727
|
+
|
|
728
|
+
async def get_order(self, order_id: int) -> Order:
|
|
729
|
+
return Order(id=order_id, status="completed", total=99.99)
|
|
730
|
+
|
|
731
|
+
async def create_order(self, order: Order) -> Order:
|
|
732
|
+
order.id = 456
|
|
733
|
+
order.status = "pending"
|
|
734
|
+
return order
|
|
735
|
+
|
|
736
|
+
# Type checking ensures mocks match protocols at compile time!
|
|
737
|
+
# If you forget a method or have wrong signatures:
|
|
738
|
+
# mypy error: Cannot instantiate abstract class 'MockUsersClient' with abstract method 'new_method'
|
|
739
|
+
|
|
740
|
+
@pytest.fixture
|
|
741
|
+
def mock_users_client() -> UsersClientProtocol:
|
|
742
|
+
"""
|
|
743
|
+
Fixture providing a mock users client.
|
|
744
|
+
Return type annotation ensures type safety.
|
|
745
|
+
"""
|
|
746
|
+
return MockUsersClient()
|
|
747
|
+
|
|
748
|
+
@pytest.fixture
|
|
749
|
+
def mock_orders_client() -> OrdersClientProtocol:
|
|
750
|
+
"""Fixture providing a mock orders client"""
|
|
751
|
+
return MockOrdersClient()
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Using Mocked Endpoint Clients in Your Code
|
|
755
|
+
|
|
756
|
+
Now inject the mocks into your business logic:
|
|
757
|
+
|
|
758
|
+
```python
|
|
759
|
+
async def test_user_service_with_mocks(mock_users_client, mock_orders_client):
|
|
760
|
+
"""Test your business logic with mocked API clients"""
|
|
761
|
+
|
|
762
|
+
# Your business logic that depends on API clients
|
|
763
|
+
async def process_user_order(users_client, orders_client, user_id: int):
|
|
764
|
+
user = await users_client.get_user(user_id=user_id)
|
|
765
|
+
order = await orders_client.create_order(Order(user_id=user.id, items=[]))
|
|
766
|
+
return user, order
|
|
767
|
+
|
|
768
|
+
# Test with mocked clients
|
|
769
|
+
user, order = await process_user_order(
|
|
770
|
+
mock_users_client,
|
|
771
|
+
mock_orders_client,
|
|
772
|
+
user_id=123
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Assertions on business logic results
|
|
776
|
+
assert user.name == "Test User"
|
|
777
|
+
assert order.status == "pending"
|
|
778
|
+
|
|
779
|
+
# Verify interactions with the mock
|
|
780
|
+
assert len(mock_users_client.calls) == 1
|
|
781
|
+
assert mock_users_client.calls[0] == ("get_user", {"user_id": 123})
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Dependency Injection Pattern
|
|
785
|
+
|
|
786
|
+
Structure your code to accept **Protocol types**, not concrete implementations:
|
|
787
|
+
|
|
788
|
+
```python
|
|
789
|
+
from my_api_client.endpoints.users import UsersClientProtocol
|
|
790
|
+
from my_api_client.endpoints.orders import OrdersClientProtocol
|
|
791
|
+
|
|
792
|
+
class UserService:
|
|
793
|
+
"""
|
|
794
|
+
Service that depends on Protocol interfaces.
|
|
795
|
+
|
|
796
|
+
CRITICAL: Accept Protocol types, not concrete classes!
|
|
797
|
+
This allows injecting both real clients and mocks.
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
def __init__(
|
|
801
|
+
self,
|
|
802
|
+
users_client: UsersClientProtocol, # Protocol type!
|
|
803
|
+
orders_client: OrdersClientProtocol # Protocol type!
|
|
804
|
+
):
|
|
805
|
+
self.users = users_client
|
|
806
|
+
self.orders = orders_client
|
|
807
|
+
|
|
808
|
+
async def get_user_with_orders(self, user_id: int):
|
|
809
|
+
user = await self.users.get_user(user_id=user_id)
|
|
810
|
+
orders = await self.orders.list_orders(user_id=user_id)
|
|
811
|
+
return {"user": user, "orders": orders}
|
|
812
|
+
|
|
813
|
+
# In production: inject real clients (they implement the protocols)
|
|
814
|
+
from my_api_client.client import APIClient
|
|
815
|
+
from my_api_client.core.config import ClientConfig
|
|
816
|
+
|
|
817
|
+
config = ClientConfig(base_url="https://api.example.com")
|
|
818
|
+
async with APIClient(config) as client:
|
|
819
|
+
service = UserService(
|
|
820
|
+
users_client=client.users, # UsersClient implements UsersClientProtocol
|
|
821
|
+
orders_client=client.orders # OrdersClient implements OrdersClientProtocol
|
|
822
|
+
)
|
|
823
|
+
result = await service.get_user_with_orders(user_id=123)
|
|
824
|
+
|
|
825
|
+
# In tests: inject mocks (they also implement the protocols)
|
|
826
|
+
async def test_user_service(mock_users_client, mock_orders_client):
|
|
827
|
+
service = UserService(
|
|
828
|
+
users_client=mock_users_client, # MockUsersClient implements UsersClientProtocol
|
|
829
|
+
orders_client=mock_orders_client # MockOrdersClient implements OrdersClientProtocol
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
result = await service.get_user_with_orders(user_id=123)
|
|
833
|
+
|
|
834
|
+
assert result["user"].name == "Test User"
|
|
835
|
+
assert len(result["orders"]) > 0
|
|
836
|
+
|
|
837
|
+
# Verify mock was called correctly
|
|
838
|
+
assert ("get_user", {"user_id": 123}) in mock_users_client.calls
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Benefits of Generated Protocols
|
|
842
|
+
|
|
843
|
+
1. **Automatic Generation**: Protocols are generated from your OpenAPI spec - no manual writing
|
|
844
|
+
2. **Compile-Time Safety**: mypy catches missing/incorrect methods immediately
|
|
845
|
+
3. **Forced Updates**: When API changes, stale mocks break at compile time, not runtime
|
|
846
|
+
4. **Test at Right Level**: Mock business operations (get_user, create_order), not HTTP transport
|
|
847
|
+
5. **IDE Support**: Full autocomplete and inline errors for protocol implementations
|
|
848
|
+
6. **Refactoring Safety**: Rename operations? All implementations must update or fail type checking
|
|
849
|
+
7. **Documentation**: Protocol serves as explicit, enforced contract documentation
|
|
850
|
+
8. **No Runtime Overhead**: Protocols are pure type-checking, zero runtime cost
|
|
851
|
+
|
|
852
|
+
### Real-World Testing Example
|
|
853
|
+
|
|
854
|
+
Complete example showing protocol-based testing in action:
|
|
855
|
+
|
|
856
|
+
```python
|
|
857
|
+
# my_service.py
|
|
858
|
+
from my_api_client.endpoints.users import UsersClientProtocol
|
|
859
|
+
from my_api_client.models.user import User
|
|
860
|
+
|
|
861
|
+
class UserRegistrationService:
|
|
862
|
+
"""Business logic for user registration"""
|
|
863
|
+
|
|
864
|
+
def __init__(self, users_client: UsersClientProtocol):
|
|
865
|
+
self.users_client = users_client
|
|
866
|
+
|
|
867
|
+
async def register_user(self, name: str, email: str) -> User:
|
|
868
|
+
"""Register a new user with validation"""
|
|
869
|
+
# Business logic
|
|
870
|
+
if not email or "@" not in email:
|
|
871
|
+
raise ValueError("Invalid email")
|
|
872
|
+
|
|
873
|
+
# Use API client
|
|
874
|
+
user = User(name=name, email=email)
|
|
875
|
+
return await self.users_client.create_user(user=user)
|
|
876
|
+
|
|
877
|
+
# test_my_service.py
|
|
878
|
+
import pytest
|
|
879
|
+
from my_service import UserRegistrationService
|
|
880
|
+
from my_api_client.endpoints.users import UsersClientProtocol
|
|
881
|
+
from my_api_client.models.user import User
|
|
882
|
+
|
|
883
|
+
class MockUsersClient(UsersClientProtocol):
|
|
884
|
+
"""Type-safe mock for testing"""
|
|
885
|
+
|
|
886
|
+
def __init__(self):
|
|
887
|
+
self.created_users: list[User] = []
|
|
888
|
+
|
|
889
|
+
async def get_user(self, user_id: int) -> User:
|
|
890
|
+
return User(id=user_id, name="Test", email="test@example.com")
|
|
891
|
+
|
|
892
|
+
async def list_users(self, limit: int = 10) -> list[User]:
|
|
893
|
+
return []
|
|
894
|
+
|
|
895
|
+
async def create_user(self, user: User) -> User:
|
|
896
|
+
user.id = 123 # Simulate server assigning ID
|
|
897
|
+
self.created_users.append(user)
|
|
898
|
+
return user
|
|
899
|
+
|
|
900
|
+
@pytest.fixture
|
|
901
|
+
def mock_users_client() -> UsersClientProtocol:
|
|
902
|
+
return MockUsersClient()
|
|
903
|
+
|
|
904
|
+
async def test_register_user__valid_data__creates_user(mock_users_client):
|
|
905
|
+
"""
|
|
906
|
+
When: Registering with valid data
|
|
907
|
+
Then: User is created via API
|
|
908
|
+
"""
|
|
909
|
+
service = UserRegistrationService(mock_users_client)
|
|
910
|
+
|
|
911
|
+
user = await service.register_user(name="John", email="john@example.com")
|
|
912
|
+
|
|
913
|
+
assert user.id == 123
|
|
914
|
+
assert user.name == "John"
|
|
915
|
+
assert len(mock_users_client.created_users) == 1
|
|
916
|
+
|
|
917
|
+
async def test_register_user__invalid_email__raises_error(mock_users_client):
|
|
918
|
+
"""
|
|
919
|
+
When: Registering with invalid email
|
|
920
|
+
Then: ValueError is raised
|
|
921
|
+
"""
|
|
922
|
+
service = UserRegistrationService(mock_users_client)
|
|
923
|
+
|
|
924
|
+
with pytest.raises(ValueError, match="Invalid email"):
|
|
925
|
+
await service.register_user(name="John", email="invalid")
|
|
926
|
+
|
|
927
|
+
# If UsersClientProtocol changes (e.g., create_user signature changes):
|
|
928
|
+
# mypy error: Cannot instantiate abstract class 'MockUsersClient' with abstract method 'create_user'
|
|
929
|
+
# This forces you to update your mock, keeping tests in sync with API!
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### Auto-Generated Mock Helper Classes
|
|
933
|
+
|
|
934
|
+
The generator creates ready-to-use mock helper classes in the `mocks/` directory, providing a faster path to testable code.
|
|
935
|
+
|
|
936
|
+
#### Generated Mocks Structure
|
|
937
|
+
|
|
938
|
+
```
|
|
939
|
+
my_api_client/
|
|
940
|
+
├── mocks/
|
|
941
|
+
│ ├── __init__.py # Exports MockAPIClient and all endpoint mocks
|
|
942
|
+
│ ├── mock_client.py # MockAPIClient with auto-create pattern
|
|
943
|
+
│ └── endpoints/
|
|
944
|
+
│ ├── __init__.py # Exports MockUsersClient, MockOrdersClient, etc.
|
|
945
|
+
│ ├── mock_users.py # MockUsersClient helper
|
|
946
|
+
│ └── mock_orders.py # MockOrdersClient helper
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
#### Quick Start with Auto-Generated Mocks
|
|
950
|
+
|
|
951
|
+
Instead of manually creating mock classes, inherit from the generated helpers:
|
|
952
|
+
|
|
953
|
+
```python
|
|
954
|
+
from my_api_client.mocks import MockAPIClient, MockUsersClient
|
|
955
|
+
from my_api_client.models.user import User
|
|
956
|
+
|
|
957
|
+
# Option 1: Override specific methods
|
|
958
|
+
class TestUsersClient(MockUsersClient):
|
|
959
|
+
"""Inherit from generated mock, override only what you need"""
|
|
960
|
+
|
|
961
|
+
async def get_user(self, user_id: int) -> User:
|
|
962
|
+
return User(id=user_id, name="Test User", email="test@example.com")
|
|
963
|
+
|
|
964
|
+
# list_users and create_user will raise NotImplementedError with helpful messages
|
|
965
|
+
|
|
966
|
+
# Option 2: Use MockAPIClient with hybrid auto-create pattern
|
|
967
|
+
client = MockAPIClient(users=TestUsersClient())
|
|
968
|
+
|
|
969
|
+
# Access your custom mock
|
|
970
|
+
user = await client.users.get_user(user_id=123)
|
|
971
|
+
assert user.name == "Test User"
|
|
972
|
+
|
|
973
|
+
# Other endpoints auto-created with NotImplementedError stubs
|
|
974
|
+
# await client.orders.get_order(order_id=1) # Raises: NotImplementedError: Override MockOrdersClient.get_order()
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
#### Hybrid Auto-Create Pattern
|
|
978
|
+
|
|
979
|
+
`MockAPIClient` automatically creates mock instances for all endpoint clients you don't explicitly provide:
|
|
980
|
+
|
|
981
|
+
```python
|
|
982
|
+
from my_api_client.mocks import MockAPIClient, MockUsersClient, MockOrdersClient
|
|
983
|
+
from my_api_client.models.user import User
|
|
984
|
+
from my_api_client.models.order import Order
|
|
985
|
+
|
|
986
|
+
# Override only the clients you need for this test
|
|
987
|
+
class TestUsersClient(MockUsersClient):
|
|
988
|
+
async def get_user(self, user_id: int) -> User:
|
|
989
|
+
return User(id=user_id, name="Test User", email="test@example.com")
|
|
990
|
+
|
|
991
|
+
class TestOrdersClient(MockOrdersClient):
|
|
992
|
+
async def get_order(self, order_id: int) -> Order:
|
|
993
|
+
return Order(id=order_id, status="completed", total=99.99)
|
|
994
|
+
|
|
995
|
+
# Create client with partial overrides
|
|
996
|
+
client = MockAPIClient(
|
|
997
|
+
users=TestUsersClient(),
|
|
998
|
+
orders=TestOrdersClient()
|
|
999
|
+
# products, payments, etc. auto-created with NotImplementedError stubs
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Use your custom mocks
|
|
1003
|
+
user = await client.users.get_user(user_id=123)
|
|
1004
|
+
order = await client.orders.get_order(order_id=456)
|
|
1005
|
+
|
|
1006
|
+
# Unimplemented endpoints provide clear error messages
|
|
1007
|
+
# await client.products.list_products() # NotImplementedError: Override MockProductsClient.list_products()
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
#### NotImplementedError Guidance
|
|
1011
|
+
|
|
1012
|
+
Generated mock helpers raise `NotImplementedError` with helpful messages:
|
|
1013
|
+
|
|
1014
|
+
```python
|
|
1015
|
+
from my_api_client.mocks import MockUsersClient
|
|
1016
|
+
|
|
1017
|
+
mock = MockUsersClient()
|
|
1018
|
+
|
|
1019
|
+
# Attempting to call unimplemented method:
|
|
1020
|
+
await mock.get_user(user_id=123)
|
|
1021
|
+
# NotImplementedError: MockUsersClient.get_user() not implemented.
|
|
1022
|
+
# Override this method in your test:
|
|
1023
|
+
# class TestUsersClient(MockUsersClient):
|
|
1024
|
+
# async def get_user(self, user_id: int) -> User:
|
|
1025
|
+
# return User(...)
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
#### Comparison: Manual vs Auto-Generated
|
|
1029
|
+
|
|
1030
|
+
**Manual Protocol Implementation** (always available):
|
|
1031
|
+
|
|
1032
|
+
```python
|
|
1033
|
+
from my_api_client.endpoints.users import UsersClientProtocol
|
|
1034
|
+
|
|
1035
|
+
class MockUsersClient(UsersClientProtocol):
|
|
1036
|
+
"""Full control, implement all methods"""
|
|
1037
|
+
|
|
1038
|
+
async def get_user(self, user_id: int) -> User: ...
|
|
1039
|
+
async def list_users(self, limit: int = 10) -> list[User]: ...
|
|
1040
|
+
async def create_user(self, user: User) -> User: ...
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
**Auto-Generated Helper** (faster, less boilerplate):
|
|
1044
|
+
|
|
1045
|
+
```python
|
|
1046
|
+
from my_api_client.mocks import MockUsersClient
|
|
1047
|
+
|
|
1048
|
+
class TestUsersClient(MockUsersClient):
|
|
1049
|
+
"""Override only what you need"""
|
|
1050
|
+
|
|
1051
|
+
async def get_user(self, user_id: int) -> User:
|
|
1052
|
+
return User(id=user_id, name="Test User", email="test@example.com")
|
|
1053
|
+
|
|
1054
|
+
# Other methods inherited with NotImplementedError stubs
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
**Use auto-generated mocks when**:
|
|
1058
|
+
|
|
1059
|
+
- You want to quickly get started with testing
|
|
1060
|
+
- You only need to override specific methods
|
|
1061
|
+
- You prefer helpful NotImplementedError messages over abstract method errors
|
|
1062
|
+
|
|
1063
|
+
**Use manual Protocol implementation when**:
|
|
1064
|
+
|
|
1065
|
+
- You need complete control over all mock behavior
|
|
1066
|
+
- You're building reusable test fixtures
|
|
1067
|
+
- You want explicit tracking of all method calls
|
|
1068
|
+
|
|
1069
|
+
Both approaches are type-safe and provide compile-time validation!
|
|
1070
|
+
|
|
1071
|
+
## Supported OpenAPI Formats
|
|
1072
|
+
|
|
1073
|
+
The generator maps OpenAPI `format` specifiers to appropriate Python types with automatic serialisation/deserialisation:
|
|
1074
|
+
|
|
1075
|
+
| OpenAPI Format | Python Type | Notes |
|
|
1076
|
+
| ------------------ | ----------------------- | -------------------------------- |
|
|
1077
|
+
| `date-time` | `datetime.datetime` | ISO 8601 parsing |
|
|
1078
|
+
| `date` | `datetime.date` | ISO 8601 parsing |
|
|
1079
|
+
| `time` | `datetime.time` | ISO 8601 parsing |
|
|
1080
|
+
| `duration` | `datetime.timedelta` | ISO 8601 duration |
|
|
1081
|
+
| `uuid` | `uuid.UUID` | Standard UUID format |
|
|
1082
|
+
| `binary` | `bytes` | Raw binary data |
|
|
1083
|
+
| `byte` | `bytes` | Base64 encoded (auto-decoded) |
|
|
1084
|
+
| `ipv4` | `ipaddress.IPv4Address` | IPv4 address validation |
|
|
1085
|
+
| `ipv6` | `ipaddress.IPv6Address` | IPv6 address validation |
|
|
1086
|
+
| `uri` / `url` | `str` | No special handling |
|
|
1087
|
+
| `email` | `str` | No special handling |
|
|
1088
|
+
| `hostname` | `str` | No special handling |
|
|
1089
|
+
| `password` | `str` | No special handling |
|
|
1090
|
+
| `int32` / `int64` | `int` | Python int (unlimited precision) |
|
|
1091
|
+
| `float` / `double` | `float` | Python float |
|
|
1092
|
+
|
|
1093
|
+
> 💡 For `byte` format, the generated client automatically handles base64 encoding/decoding via cattrs hooks.
|
|
1094
|
+
|
|
1095
|
+
## Known Limitations
|
|
1096
|
+
|
|
1097
|
+
Some OpenAPI features have simplified implementations:
|
|
1098
|
+
|
|
1099
|
+
| Feature | Current Behavior | Workaround |
|
|
1100
|
+
| --------------------------- | --------------------------------------------------- | -------------------------------------------------- |
|
|
1101
|
+
| **Parameter Serialization** | Uses httpx defaults (not OpenAPI `style`/`explode`) | Manually format complex parameters |
|
|
1102
|
+
| **Response Headers** | Only body is returned, headers are ignored | Use custom transport to access full response |
|
|
1103
|
+
| **Multipart Forms** | Basic file upload only | Complex multipart schemas may need manual handling |
|
|
1104
|
+
| **Parameter Defaults** | Schema defaults not in method signatures | Pass defaults explicitly when calling |
|
|
1105
|
+
| **WebSockets** | Not currently supported | Use separate WebSocket library |
|
|
1106
|
+
|
|
1107
|
+
> 💡 These limitations rarely affect real-world usage. Most APIs work perfectly with the current implementation.
|
|
1108
|
+
|
|
1109
|
+
## Architecture
|
|
1110
|
+
|
|
1111
|
+
PyOpenAPI Generator uses a sophisticated three-stage pipeline designed for enterprise-grade reliability:
|
|
1112
|
+
|
|
1113
|
+
```mermaid
|
|
1114
|
+
graph TD
|
|
1115
|
+
A[OpenAPI Spec] --> B[Loading Stage]
|
|
1116
|
+
B --> C[Intermediate Representation]
|
|
1117
|
+
C --> D[Unified Type Resolution]
|
|
1118
|
+
D --> E[Visiting Stage]
|
|
1119
|
+
E --> F[Python Code AST]
|
|
1120
|
+
F --> G[Emitting Stage]
|
|
1121
|
+
G --> H[Generated Files]
|
|
1122
|
+
H --> I[Post-Processing]
|
|
1123
|
+
I --> J[Final Client Package]
|
|
1124
|
+
|
|
1125
|
+
subgraph "Key Components"
|
|
1126
|
+
K[Schema Parser]
|
|
1127
|
+
L[Cycle Detection]
|
|
1128
|
+
M[Reference Resolution]
|
|
1129
|
+
N[Type Service]
|
|
1130
|
+
O[Code Emitters]
|
|
1131
|
+
end
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
### Why This Architecture?
|
|
1135
|
+
|
|
1136
|
+
**Complex Schema Handling**: Modern OpenAPI specs contain circular references, deep nesting, and intricate type relationships. Our architecture handles these robustly.
|
|
1137
|
+
|
|
1138
|
+
**Production Ready**: Each stage has clear responsibilities and clean interfaces, enabling comprehensive testing and reliable code generation.
|
|
1139
|
+
|
|
1140
|
+
**Extensible**: Plugin-based authentication, customizable type resolution, and modular emitters make the system adaptable to various use cases.
|
|
1141
|
+
|
|
1142
|
+
## 📚 Documentation
|
|
1143
|
+
|
|
1144
|
+
- **[Architecture Guide](docs/architecture.md)** - Deep dive into the system design
|
|
1145
|
+
- **[Type Resolution](docs/unified_type_resolution.md)** - How types are resolved and generated
|
|
1146
|
+
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to the project
|
|
1147
|
+
- **[API Reference](docs/)** - Complete API documentation
|
|
1148
|
+
|
|
1149
|
+
## 🤝 Contributing
|
|
1150
|
+
|
|
1151
|
+
We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.
|
|
1152
|
+
|
|
1153
|
+
**For Contributors**: See our [Contributing Guide](CONTRIBUTING.md) for:
|
|
1154
|
+
|
|
1155
|
+
- Development setup and workflow
|
|
1156
|
+
- Testing requirements (85% coverage, mypy strict mode)
|
|
1157
|
+
- Code quality standards
|
|
1158
|
+
- Pull request process
|
|
1159
|
+
|
|
1160
|
+
**Quick Links**:
|
|
1161
|
+
|
|
1162
|
+
- [Architecture Documentation](docs/architecture.md) - System design and patterns
|
|
1163
|
+
- [Issue Tracker](https://github.com/mindhiveoy/pyopenapi_gen/issues) - Report bugs or request features
|
|
1164
|
+
|
|
1165
|
+
## 📄 License
|
|
1166
|
+
|
|
1167
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
1168
|
+
|
|
1169
|
+
Generated clients are self-contained and can be distributed under any license compatible with your project.
|