pyopenapi-gen 0.21.0__py3-none-any.whl → 0.22.0__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.

Potentially problematic release.


This version of pyopenapi-gen might be problematic. Click here for more details.

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