pyopenapi-gen 0.8.3__py3-none-any.whl → 0.8.6__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/cli.py +5 -22
- pyopenapi_gen/context/import_collector.py +8 -8
- pyopenapi_gen/core/loader/operations/parser.py +1 -1
- pyopenapi_gen/core/parsing/context.py +2 -1
- pyopenapi_gen/core/parsing/cycle_helpers.py +1 -1
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +4 -4
- pyopenapi_gen/core/parsing/schema_parser.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +1 -1
- pyopenapi_gen/core/postprocess_manager.py +39 -13
- pyopenapi_gen/core/schemas.py +101 -16
- pyopenapi_gen/core/utils.py +8 -3
- pyopenapi_gen/core/writers/python_construct_renderer.py +57 -9
- pyopenapi_gen/emitters/endpoints_emitter.py +1 -1
- pyopenapi_gen/helpers/endpoint_utils.py +4 -22
- pyopenapi_gen/helpers/type_cleaner.py +1 -1
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +1 -1
- pyopenapi_gen/helpers/type_resolution/finalizer.py +1 -1
- pyopenapi_gen/types/contracts/types.py +0 -1
- pyopenapi_gen/types/resolvers/response_resolver.py +5 -33
- pyopenapi_gen/types/resolvers/schema_resolver.py +2 -2
- pyopenapi_gen/types/services/type_service.py +0 -18
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +187 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +1 -20
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +5 -3
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +12 -6
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +352 -343
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +7 -4
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +4 -2
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +1 -1
- pyopenapi_gen/visit/model/dataclass_generator.py +32 -1
- pyopenapi_gen-0.8.6.dist-info/METADATA +383 -0
- {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.6.dist-info}/RECORD +36 -34
- pyopenapi_gen-0.8.3.dist-info/METADATA +0 -224
- {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.6.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.6.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.6.dist-info}/licenses/LICENSE +0 -0
@@ -10,8 +10,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
10
10
|
from pyopenapi_gen.core.utils import NameSanitizer
|
11
11
|
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
12
12
|
|
13
|
-
# Import necessary helpers from endpoint_utils
|
14
|
-
from pyopenapi_gen.helpers.endpoint_utils import get_param_type
|
13
|
+
# Import necessary helpers from endpoint_utils
|
14
|
+
from pyopenapi_gen.helpers.endpoint_utils import get_param_type
|
15
|
+
from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
|
15
16
|
|
16
17
|
if TYPE_CHECKING:
|
17
18
|
from pyopenapi_gen import IROperation
|
@@ -32,13 +33,15 @@ class EndpointMethodSignatureGenerator:
|
|
32
33
|
op: IROperation,
|
33
34
|
context: RenderContext,
|
34
35
|
ordered_params: List[Dict[str, Any]],
|
36
|
+
strategy: ResponseStrategy,
|
35
37
|
) -> None:
|
36
38
|
"""Writes the method signature to the provided CodeWriter."""
|
37
39
|
# Logic from EndpointMethodGenerator._write_method_signature
|
38
40
|
for p_info in ordered_params: # Renamed p to p_info to avoid conflict if IRParameter is named p
|
39
41
|
context.add_typing_imports_for_type(p_info["type"])
|
40
42
|
|
41
|
-
|
43
|
+
# Use strategy return type instead of computing it again
|
44
|
+
return_type = strategy.return_type
|
42
45
|
context.add_typing_imports_for_type(return_type)
|
43
46
|
|
44
47
|
# Check if AsyncIterator is in return_type or any parameter type
|
@@ -63,7 +66,7 @@ class EndpointMethodSignatureGenerator:
|
|
63
66
|
args = ["self"]
|
64
67
|
for p_orig in ordered_params:
|
65
68
|
p = p_orig.copy() # Work with a copy
|
66
|
-
arg_str = f"{NameSanitizer.sanitize_method_name(p[
|
69
|
+
arg_str = f"{NameSanitizer.sanitize_method_name(p["name"])}: {p["type"]}" # Ensure param name is sanitized
|
67
70
|
if not p.get("required", False):
|
68
71
|
# Default value handling: if default is None, it should be ' = None'
|
69
72
|
# If default is a string, it should be ' = "default_value"'
|
@@ -11,12 +11,12 @@ from typing import TYPE_CHECKING, Any, Dict, Optional # IO for multipart type h
|
|
11
11
|
from pyopenapi_gen.helpers.endpoint_utils import (
|
12
12
|
get_param_type,
|
13
13
|
get_request_body_type,
|
14
|
-
get_return_type_unified,
|
15
14
|
)
|
16
15
|
|
17
16
|
if TYPE_CHECKING:
|
18
17
|
from pyopenapi_gen import IROperation # IRParameter for op.parameters type hint
|
19
18
|
from pyopenapi_gen.context.render_context import RenderContext
|
19
|
+
from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
|
20
20
|
|
21
21
|
logger = logging.getLogger(__name__)
|
22
22
|
|
@@ -31,6 +31,7 @@ class EndpointImportAnalyzer:
|
|
31
31
|
self,
|
32
32
|
op: IROperation,
|
33
33
|
context: RenderContext,
|
34
|
+
response_strategy: ResponseStrategy,
|
34
35
|
) -> None:
|
35
36
|
"""Analyzes the operation and registers imports with the RenderContext."""
|
36
37
|
for param in op.parameters: # op.parameters are IRParameter objects
|
@@ -60,7 +61,8 @@ class EndpointImportAnalyzer:
|
|
60
61
|
if body_param_type:
|
61
62
|
context.add_typing_imports_for_type(body_param_type)
|
62
63
|
|
63
|
-
|
64
|
+
# Use the response strategy's return type for import analysis
|
65
|
+
return_type = response_strategy.return_type
|
64
66
|
context.add_typing_imports_for_type(return_type)
|
65
67
|
|
66
68
|
# Check for AsyncIterator in return type or parameter types
|
@@ -123,7 +123,7 @@ class EndpointParameterProcessor:
|
|
123
123
|
param_details_map[body_specific_param_info["name"]] = body_specific_param_info
|
124
124
|
else:
|
125
125
|
logger.warning(
|
126
|
-
f"Request body parameter name '{body_specific_param_info[
|
126
|
+
f"Request body parameter name '{body_specific_param_info["name"]}' "
|
127
127
|
f"for operation '{op.operation_id}'"
|
128
128
|
f"collides with an existing path/query/header parameter. Check OpenAPI spec."
|
129
129
|
)
|
@@ -4,7 +4,7 @@ Generates Python code for dataclasses from IRSchema objects.
|
|
4
4
|
|
5
5
|
import json
|
6
6
|
import logging
|
7
|
-
from typing import Dict, List, Optional, Tuple
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
8
8
|
|
9
9
|
from pyopenapi_gen import IRSchema
|
10
10
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -81,6 +81,22 @@ class DataclassGenerator:
|
|
81
81
|
)
|
82
82
|
return "None"
|
83
83
|
|
84
|
+
def _requires_field_mapping(self, api_field: str, python_field: str) -> bool:
|
85
|
+
"""Check if field mapping is required between API and Python field names."""
|
86
|
+
return api_field != python_field
|
87
|
+
|
88
|
+
def _generate_field_mappings(self, properties: Dict[str, Any], sanitized_names: Dict[str, str]) -> Dict[str, str]:
|
89
|
+
"""Generate field mappings for BaseSchema configuration."""
|
90
|
+
mappings = {}
|
91
|
+
for api_name, python_name in sanitized_names.items():
|
92
|
+
if api_name in properties and self._requires_field_mapping(api_name, python_name):
|
93
|
+
mappings[api_name] = python_name
|
94
|
+
return mappings
|
95
|
+
|
96
|
+
def _has_any_mappings(self, properties: Dict[str, Any], sanitized_names: Dict[str, str]) -> bool:
|
97
|
+
"""Check if any field mappings are needed."""
|
98
|
+
return bool(self._generate_field_mappings(properties, sanitized_names))
|
99
|
+
|
84
100
|
def generate(
|
85
101
|
self,
|
86
102
|
schema: IRSchema,
|
@@ -116,6 +132,7 @@ class DataclassGenerator:
|
|
116
132
|
|
117
133
|
class_name = base_name
|
118
134
|
fields_data: List[Tuple[str, str, Optional[str], Optional[str]]] = []
|
135
|
+
field_mappings: Dict[str, str] = {}
|
119
136
|
|
120
137
|
if schema.type == "array" and schema.items:
|
121
138
|
field_name_for_array_content = "items"
|
@@ -161,6 +178,10 @@ class DataclassGenerator:
|
|
161
178
|
# Sanitize the property name for use as a Python attribute
|
162
179
|
field_name = NameSanitizer.sanitize_method_name(prop_name)
|
163
180
|
|
181
|
+
# Track field mapping if the names differ
|
182
|
+
if self._requires_field_mapping(prop_name, field_name):
|
183
|
+
field_mappings[prop_name] = field_name
|
184
|
+
|
164
185
|
py_type = self.type_service.resolve_schema_type(prop_schema, context, required=is_required)
|
165
186
|
py_type = TypeFinalizer(context)._clean_type(py_type)
|
166
187
|
|
@@ -168,18 +189,28 @@ class DataclassGenerator:
|
|
168
189
|
if not is_required:
|
169
190
|
default_expr = self._get_field_default(prop_schema, context)
|
170
191
|
|
192
|
+
# Enhance field documentation for mapped fields
|
171
193
|
field_doc = prop_schema.description
|
194
|
+
if field_mappings.get(prop_name) == field_name and prop_name != field_name:
|
195
|
+
if field_doc:
|
196
|
+
field_doc = f"{field_doc} (maps from '{prop_name}')"
|
197
|
+
else:
|
198
|
+
field_doc = f"Maps from '{prop_name}'"
|
199
|
+
|
172
200
|
fields_data.append((field_name, py_type, default_expr, field_doc))
|
173
201
|
|
174
202
|
# logger.debug(
|
175
203
|
# f"DataclassGenerator: Preparing to render dataclass '{class_name}' with fields: {fields_data}."
|
176
204
|
# )
|
177
205
|
|
206
|
+
# Always use BaseSchema for better developer experience
|
207
|
+
# Only include field mappings if there are actual mappings needed
|
178
208
|
rendered_code = self.renderer.render_dataclass(
|
179
209
|
class_name=class_name,
|
180
210
|
fields=fields_data,
|
181
211
|
description=schema.description,
|
182
212
|
context=context,
|
213
|
+
field_mappings=field_mappings if field_mappings else None,
|
183
214
|
)
|
184
215
|
|
185
216
|
assert rendered_code.strip(), "Generated dataclass code cannot be empty."
|
@@ -0,0 +1,383 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pyopenapi-gen
|
3
|
+
Version: 0.8.6
|
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: click>=8.0.0
|
36
|
+
Requires-Dist: dataclass-wizard>=0.22.0
|
37
|
+
Requires-Dist: httpx>=0.24.0
|
38
|
+
Requires-Dist: openapi-core>=0.19
|
39
|
+
Requires-Dist: openapi-spec-validator>=0.7
|
40
|
+
Requires-Dist: pyyaml>=6.0
|
41
|
+
Requires-Dist: typer>=0.12.0
|
42
|
+
Provides-Extra: dev
|
43
|
+
Requires-Dist: bandit[toml]>=1.7.0; extra == 'dev'
|
44
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
45
|
+
Requires-Dist: dataclass-wizard>=0.22.0; extra == 'dev'
|
46
|
+
Requires-Dist: httpx>=0.24.0; extra == 'dev'
|
47
|
+
Requires-Dist: mypy>=1.7; extra == 'dev'
|
48
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == 'dev'
|
49
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
50
|
+
Requires-Dist: pytest-timeout>=2.1.0; extra == 'dev'
|
51
|
+
Requires-Dist: pytest-xdist>=3.0.0; extra == 'dev'
|
52
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
53
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
54
|
+
Requires-Dist: safety>=2.0.0; extra == 'dev'
|
55
|
+
Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
|
56
|
+
Requires-Dist: types-toml>=0.10.8; extra == 'dev'
|
57
|
+
Description-Content-Type: text/markdown
|
58
|
+
|
59
|
+
# PyOpenAPI Generator
|
60
|
+
|
61
|
+
[](https://python.org)
|
62
|
+
[](https://opensource.org/licenses/MIT)
|
63
|
+
[](https://github.com/psf/black)
|
64
|
+
[](https://github.com/astral-sh/ruff)
|
65
|
+
|
66
|
+
**Modern, enterprise-grade Python client generator for OpenAPI specifications.**
|
67
|
+
|
68
|
+
PyOpenAPI Generator creates async-first, strongly-typed Python clients from OpenAPI specs. Built for production use with advanced cycle detection, unified type resolution, and zero runtime dependencies.
|
69
|
+
|
70
|
+
## 🚀 Why PyOpenAPI Generator?
|
71
|
+
|
72
|
+
### Modern Python Architecture
|
73
|
+
- **Async-First**: Built for `async`/`await` with `httpx` for optimal performance
|
74
|
+
- **Type Safety**: Complete type hints, dataclass models, and mypy compatibility
|
75
|
+
- **Zero Dependencies**: Generated clients are completely self-contained
|
76
|
+
|
77
|
+
### Enterprise-Grade Reliability
|
78
|
+
- **Advanced Cycle Detection**: Handles complex schemas with circular references
|
79
|
+
- **Unified Type Resolution**: Consistent, testable type resolution across all components
|
80
|
+
- **Production Ready**: Comprehensive error handling and robust code generation
|
81
|
+
|
82
|
+
### Developer Experience
|
83
|
+
- **IDE Support**: Rich autocomplete and type checking in modern IDEs
|
84
|
+
- **Tag Organization**: Operations grouped by OpenAPI tags for intuitive navigation
|
85
|
+
- **Smart Features**: Auto-detected pagination, response unwrapping, and structured exceptions
|
86
|
+
|
87
|
+
## 📦 Installation
|
88
|
+
|
89
|
+
```bash
|
90
|
+
pip install pyopenapi-gen
|
91
|
+
```
|
92
|
+
|
93
|
+
Or with Poetry:
|
94
|
+
```bash
|
95
|
+
poetry add pyopenapi-gen
|
96
|
+
```
|
97
|
+
|
98
|
+
## ⚡ Quick Start
|
99
|
+
|
100
|
+
### 1. Generate Your First Client
|
101
|
+
```bash
|
102
|
+
pyopenapi-gen gen openapi.yaml \
|
103
|
+
--project-root . \
|
104
|
+
--output-package my_api_client
|
105
|
+
```
|
106
|
+
|
107
|
+
### 2. Use the Generated Client
|
108
|
+
```python
|
109
|
+
import asyncio
|
110
|
+
from my_api_client.client import APIClient
|
111
|
+
from my_api_client.core.config import ClientConfig
|
112
|
+
|
113
|
+
async def main():
|
114
|
+
config = ClientConfig(base_url="https://api.example.com")
|
115
|
+
async with APIClient(config) as client:
|
116
|
+
# Type-safe API calls with full IDE support
|
117
|
+
users = await client.users.list_users(page=1)
|
118
|
+
|
119
|
+
# Automatic pagination
|
120
|
+
async for user in client.users.list_users_paginated():
|
121
|
+
print(f"User: {user.name}")
|
122
|
+
|
123
|
+
asyncio.run(main())
|
124
|
+
```
|
125
|
+
|
126
|
+
## 🔧 Configuration Options
|
127
|
+
|
128
|
+
### Standalone Client (Default)
|
129
|
+
```bash
|
130
|
+
pyopenapi-gen gen openapi.yaml \
|
131
|
+
--project-root . \
|
132
|
+
--output-package my_api_client
|
133
|
+
```
|
134
|
+
Creates self-contained client with embedded core dependencies.
|
135
|
+
|
136
|
+
### Shared Core (Multiple Clients)
|
137
|
+
```bash
|
138
|
+
pyopenapi-gen gen openapi.yaml \
|
139
|
+
--project-root . \
|
140
|
+
--output-package clients.api_client \
|
141
|
+
--core-package clients.core
|
142
|
+
```
|
143
|
+
Multiple clients share a single core implementation.
|
144
|
+
|
145
|
+
### Additional Options
|
146
|
+
```bash
|
147
|
+
--force # Overwrite without prompting
|
148
|
+
--no-postprocess # Skip formatting and type checking
|
149
|
+
```
|
150
|
+
|
151
|
+
## ✨ Key Features
|
152
|
+
|
153
|
+
| Feature | Description |
|
154
|
+
|---------|-------------|
|
155
|
+
| 🔒 **Type Safety** | Complete type hints, dataclass models, and mypy compatibility |
|
156
|
+
| ⚡ **Async-First** | Built for modern Python `async`/`await` patterns with `httpx` |
|
157
|
+
| 🔌 **Pluggable Auth** | Bearer, API key, OAuth2, and custom authentication strategies |
|
158
|
+
| 🔄 **Smart Pagination** | Auto-detected cursor/page/offset patterns with async iteration |
|
159
|
+
| 📦 **Zero Dependencies** | Generated clients are completely self-contained |
|
160
|
+
| 🛡️ **Robust Parsing** | Advanced cycle detection and graceful handling of complex specs |
|
161
|
+
| 🎯 **Structured Errors** | Rich exception hierarchy with meaningful error messages |
|
162
|
+
| 🏷️ **Tag Organization** | Operations grouped by OpenAPI tags for intuitive navigation |
|
163
|
+
|
164
|
+
## Generated Client Structure
|
165
|
+
|
166
|
+
```
|
167
|
+
my_api_client/
|
168
|
+
├── client.py # Main APIClient with tag-grouped methods
|
169
|
+
├── core/ # Self-contained runtime dependencies
|
170
|
+
│ ├── config.py # Configuration management
|
171
|
+
│ ├── http_transport.py # HTTP client abstraction
|
172
|
+
│ ├── exceptions.py # Error hierarchy
|
173
|
+
│ └── auth/ # Authentication plugins
|
174
|
+
├── models/ # Dataclass models from schemas
|
175
|
+
│ └── user.py
|
176
|
+
├── endpoints/ # Operation methods grouped by tag
|
177
|
+
│ └── users.py
|
178
|
+
└── __init__.py
|
179
|
+
```
|
180
|
+
|
181
|
+
## 🔐 Authentication
|
182
|
+
|
183
|
+
PyOpenAPI Generator supports multiple authentication patterns out of the box:
|
184
|
+
|
185
|
+
### Bearer Token
|
186
|
+
```python
|
187
|
+
from my_api_client.core.auth.plugins import BearerAuth
|
188
|
+
|
189
|
+
config = ClientConfig(
|
190
|
+
base_url="https://api.example.com",
|
191
|
+
auth=BearerAuth("your-token")
|
192
|
+
)
|
193
|
+
```
|
194
|
+
|
195
|
+
### API Key (Header, Query, or Cookie)
|
196
|
+
```python
|
197
|
+
from my_api_client.core.auth.plugins import ApiKeyAuth
|
198
|
+
|
199
|
+
config = ClientConfig(
|
200
|
+
base_url="https://api.example.com",
|
201
|
+
auth=ApiKeyAuth("your-key", location="header", name="X-API-Key")
|
202
|
+
)
|
203
|
+
```
|
204
|
+
|
205
|
+
### OAuth2 with Refresh
|
206
|
+
```python
|
207
|
+
from my_api_client.core.auth.plugins import OAuth2Auth
|
208
|
+
|
209
|
+
def refresh_token():
|
210
|
+
# Your token refresh logic
|
211
|
+
return "new-token"
|
212
|
+
|
213
|
+
config = ClientConfig(
|
214
|
+
base_url="https://api.example.com",
|
215
|
+
auth=OAuth2Auth("initial-token", refresh_callback=refresh_token)
|
216
|
+
)
|
217
|
+
```
|
218
|
+
|
219
|
+
### Composite Authentication
|
220
|
+
```python
|
221
|
+
from my_api_client.core.auth.base import CompositeAuth
|
222
|
+
from my_api_client.core.auth.plugins import BearerAuth, HeadersAuth
|
223
|
+
|
224
|
+
config = ClientConfig(
|
225
|
+
base_url="https://api.example.com",
|
226
|
+
auth=CompositeAuth(
|
227
|
+
BearerAuth("token"),
|
228
|
+
HeadersAuth({"X-Custom-Header": "value"})
|
229
|
+
)
|
230
|
+
)
|
231
|
+
```
|
232
|
+
|
233
|
+
## 📊 Advanced Features
|
234
|
+
|
235
|
+
### Pagination Support
|
236
|
+
```python
|
237
|
+
# Manual pagination
|
238
|
+
page = 1
|
239
|
+
while True:
|
240
|
+
users = await client.users.list_users(page=page, limit=20)
|
241
|
+
if not users:
|
242
|
+
break
|
243
|
+
# Process users
|
244
|
+
page += 1
|
245
|
+
|
246
|
+
# Automatic pagination (if supported by the API)
|
247
|
+
async for user in client.users.list_users_paginated():
|
248
|
+
print(f"User: {user.name}")
|
249
|
+
```
|
250
|
+
|
251
|
+
### Error Handling
|
252
|
+
```python
|
253
|
+
try:
|
254
|
+
user = await client.users.get_user(user_id=123)
|
255
|
+
except client.exceptions.UserNotFoundError as e:
|
256
|
+
print(f"User not found: {e.detail}")
|
257
|
+
except client.exceptions.ClientError as e:
|
258
|
+
print(f"Client error: {e}")
|
259
|
+
except client.exceptions.ServerError as e:
|
260
|
+
print(f"Server error: {e}")
|
261
|
+
```
|
262
|
+
|
263
|
+
### Response Unwrapping
|
264
|
+
Many APIs wrap responses in a `data` field. PyOpenAPI Generator automatically detects and unwraps these patterns:
|
265
|
+
|
266
|
+
```python
|
267
|
+
# API returns: {"data": {"id": 1, "name": "John"}, "meta": {...}}
|
268
|
+
# Your code receives: User(id=1, name="John")
|
269
|
+
user = await client.users.get_user(user_id=1)
|
270
|
+
print(user.name) # "John"
|
271
|
+
```
|
272
|
+
|
273
|
+
## 🚧 Known Limitations
|
274
|
+
|
275
|
+
Some OpenAPI features have simplified implementations. Contributions welcome!
|
276
|
+
|
277
|
+
| Limitation | Current Behavior |
|
278
|
+
|------------|------------------|
|
279
|
+
| **Parameter Serialization** | Uses HTTP client defaults instead of OpenAPI `style`/`explode` |
|
280
|
+
| **Complex Multipart** | Basic file upload support; complex schemas simplified |
|
281
|
+
| **Response Headers** | Only response body returned, headers ignored |
|
282
|
+
| **Parameter Defaults** | Schema defaults not applied to method signatures |
|
283
|
+
|
284
|
+
> 💡 **Contributing**: See our [Contributing Guide](CONTRIBUTING.md) to help enhance OpenAPI specification coverage!
|
285
|
+
|
286
|
+
## 🏗️ Architecture
|
287
|
+
|
288
|
+
PyOpenAPI Generator uses a sophisticated three-stage pipeline designed for enterprise-grade reliability:
|
289
|
+
|
290
|
+
```mermaid
|
291
|
+
graph TD
|
292
|
+
A[OpenAPI Spec] --> B[Loading Stage]
|
293
|
+
B --> C[Intermediate Representation]
|
294
|
+
C --> D[Unified Type Resolution]
|
295
|
+
D --> E[Visiting Stage]
|
296
|
+
E --> F[Python Code AST]
|
297
|
+
F --> G[Emitting Stage]
|
298
|
+
G --> H[Generated Files]
|
299
|
+
H --> I[Post-Processing]
|
300
|
+
I --> J[Final Client Package]
|
301
|
+
|
302
|
+
subgraph "Key Components"
|
303
|
+
K[Schema Parser]
|
304
|
+
L[Cycle Detection]
|
305
|
+
M[Reference Resolution]
|
306
|
+
N[Type Service]
|
307
|
+
O[Code Emitters]
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
### Why This Architecture?
|
312
|
+
|
313
|
+
**Complex Schema Handling**: Modern OpenAPI specs contain circular references, deep nesting, and intricate type relationships. Our architecture handles these robustly.
|
314
|
+
|
315
|
+
**Production Ready**: Each stage has clear responsibilities and clean interfaces, enabling comprehensive testing and reliable code generation.
|
316
|
+
|
317
|
+
**Extensible**: Plugin-based authentication, customizable type resolution, and modular emitters make the system adaptable to various use cases.
|
318
|
+
|
319
|
+
## 📚 Documentation
|
320
|
+
|
321
|
+
- **[Architecture Guide](docs/architecture.md)** - Deep dive into the system design
|
322
|
+
- **[Type Resolution](docs/unified_type_resolution.md)** - How types are resolved and generated
|
323
|
+
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to the project
|
324
|
+
- **[API Reference](docs/)** - Complete API documentation
|
325
|
+
|
326
|
+
## 🤝 Contributing
|
327
|
+
|
328
|
+
We welcome contributions! PyOpenAPI Generator is designed to be extensible and maintainable.
|
329
|
+
|
330
|
+
### Quick Start for Contributors
|
331
|
+
```bash
|
332
|
+
# 1. Fork and clone the repository
|
333
|
+
git clone https://github.com/your-username/pyopenapi-gen.git
|
334
|
+
cd pyopenapi-gen
|
335
|
+
|
336
|
+
# 2. Set up development environment
|
337
|
+
source .venv/bin/activate # Activate virtual environment
|
338
|
+
poetry install --with dev
|
339
|
+
|
340
|
+
# 3. Run quality checks
|
341
|
+
make quality-fix # Auto-fix formatting and linting
|
342
|
+
make quality # Run all quality checks
|
343
|
+
make test # Run tests with coverage
|
344
|
+
```
|
345
|
+
|
346
|
+
### Development Workflow
|
347
|
+
```bash
|
348
|
+
# Essential commands for development
|
349
|
+
make quality-fix # Auto-fix formatting and linting issues
|
350
|
+
make quality # Run all quality checks (format, lint, typecheck, security)
|
351
|
+
make test # Run tests with 85% coverage requirement
|
352
|
+
make test-fast # Run tests, stop on first failure
|
353
|
+
|
354
|
+
# Individual quality commands
|
355
|
+
make format # Format code with Black
|
356
|
+
make lint-fix # Fix linting issues with Ruff
|
357
|
+
make typecheck # Type checking with mypy
|
358
|
+
make security # Security scanning with Bandit
|
359
|
+
```
|
360
|
+
|
361
|
+
See our [Contributing Guide](CONTRIBUTING.md) for detailed information on:
|
362
|
+
- 📋 Development setup and workflow
|
363
|
+
- 🧪 Testing guidelines and standards
|
364
|
+
- 📖 Documentation standards
|
365
|
+
- 🔄 Pull request process
|
366
|
+
- 🏗️ Architecture and design patterns
|
367
|
+
|
368
|
+
## 📄 License
|
369
|
+
|
370
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
371
|
+
|
372
|
+
Generated clients are self-contained and can be distributed under any license compatible with your project.
|
373
|
+
|
374
|
+
## 🙏 Acknowledgments
|
375
|
+
|
376
|
+
- Built with [httpx](https://www.python-httpx.org/) for modern async HTTP
|
377
|
+
- Type safety with [mypy](https://mypy.readthedocs.io/) strict mode
|
378
|
+
- Code quality with [Black](https://black.readthedocs.io/) and [Ruff](https://docs.astral.sh/ruff/)
|
379
|
+
- Visitor pattern for clean, maintainable code generation
|
380
|
+
|
381
|
+
---
|
382
|
+
|
383
|
+
**Made with ❤️ for the Python community**
|