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.
Files changed (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,20 @@
1
+ class HTTPError(Exception):
2
+ """Base HTTP error with status code and message."""
3
+
4
+ def __init__(self, status_code: int, message: str, response: object | None = None) -> None:
5
+ super().__init__(f"{status_code}: {message}")
6
+ self.status_code = status_code
7
+ self.message = message
8
+ self.response = response
9
+
10
+
11
+ class ClientError(HTTPError):
12
+ """4XX client error responses."""
13
+
14
+ pass
15
+
16
+
17
+ class ServerError(HTTPError):
18
+ """5XX server error responses."""
19
+
20
+ pass
@@ -0,0 +1,218 @@
1
+ """HTTP status code definitions and human-readable names.
2
+
3
+ This module provides a registry of standard HTTP status codes with their
4
+ canonical names according to RFC specifications.
5
+
6
+ References:
7
+ - RFC 9110 (HTTP Semantics): https://www.rfc-editor.org/rfc/rfc9110.html
8
+ - IANA HTTP Status Code Registry: https://www.iana.org/assignments/http-status-codes/
9
+ """
10
+
11
+ # Standard HTTP status codes with human-readable names
12
+ HTTP_STATUS_CODES: dict[int, str] = {
13
+ # 1xx Informational
14
+ 100: "Continue",
15
+ 101: "Switching Protocols",
16
+ 102: "Processing",
17
+ 103: "Early Hints",
18
+ # 2xx Success
19
+ 200: "OK",
20
+ 201: "Created",
21
+ 202: "Accepted",
22
+ 203: "Non-Authoritative Information",
23
+ 204: "No Content",
24
+ 205: "Reset Content",
25
+ 206: "Partial Content",
26
+ 207: "Multi-Status",
27
+ 208: "Already Reported",
28
+ 226: "IM Used",
29
+ # 3xx Redirection
30
+ 300: "Multiple Choices",
31
+ 301: "Moved Permanently",
32
+ 302: "Found",
33
+ 303: "See Other",
34
+ 304: "Not Modified",
35
+ 305: "Use Proxy",
36
+ 307: "Temporary Redirect",
37
+ 308: "Permanent Redirect",
38
+ # 4xx Client Error
39
+ 400: "Bad Request",
40
+ 401: "Unauthorised",
41
+ 402: "Payment Required",
42
+ 403: "Forbidden",
43
+ 404: "Not Found",
44
+ 405: "Method Not Allowed",
45
+ 406: "Not Acceptable",
46
+ 407: "Proxy Authentication Required",
47
+ 408: "Request Timeout",
48
+ 409: "Conflict",
49
+ 410: "Gone",
50
+ 411: "Length Required",
51
+ 412: "Precondition Failed",
52
+ 413: "Payload Too Large",
53
+ 414: "URI Too Long",
54
+ 415: "Unsupported Media Type",
55
+ 416: "Range Not Satisfiable",
56
+ 417: "Expectation Failed",
57
+ 418: "I'm a teapot",
58
+ 421: "Misdirected Request",
59
+ 422: "Unprocessable Entity",
60
+ 423: "Locked",
61
+ 424: "Failed Dependency",
62
+ 425: "Too Early",
63
+ 426: "Upgrade Required",
64
+ 428: "Precondition Required",
65
+ 429: "Too Many Requests",
66
+ 431: "Request Header Fields Too Large",
67
+ 451: "Unavailable For Legal Reasons",
68
+ # 5xx Server Error
69
+ 500: "Internal Server Error",
70
+ 501: "Not Implemented",
71
+ 502: "Bad Gateway",
72
+ 503: "Service Unavailable",
73
+ 504: "Gateway Timeout",
74
+ 505: "HTTP Version Not Supported",
75
+ 506: "Variant Also Negotiates",
76
+ 507: "Insufficient Storage",
77
+ 508: "Loop Detected",
78
+ 510: "Not Extended",
79
+ 511: "Network Authentication Required",
80
+ }
81
+
82
+
83
+ def get_status_name(code: int) -> str:
84
+ """Get the human-readable name for an HTTP status code.
85
+
86
+ Args:
87
+ code: HTTP status code (e.g., 404)
88
+
89
+ Returns:
90
+ Human-readable status name (e.g., "Not Found"), or "Unknown" if not found
91
+ """
92
+ return HTTP_STATUS_CODES.get(code, "Unknown")
93
+
94
+
95
+ def is_error_code(code: int) -> bool:
96
+ """Check if a status code represents an error (4xx or 5xx).
97
+
98
+ Args:
99
+ code: HTTP status code
100
+
101
+ Returns:
102
+ True if the code is a client or server error, False otherwise
103
+ """
104
+ return 400 <= code < 600
105
+
106
+
107
+ def is_client_error(code: int) -> bool:
108
+ """Check if a status code represents a client error (4xx).
109
+
110
+ Args:
111
+ code: HTTP status code
112
+
113
+ Returns:
114
+ True if the code is a client error, False otherwise
115
+ """
116
+ return 400 <= code < 500
117
+
118
+
119
+ def is_server_error(code: int) -> bool:
120
+ """Check if a status code represents a server error (5xx).
121
+
122
+ Args:
123
+ code: HTTP status code
124
+
125
+ Returns:
126
+ True if the code is a server error, False otherwise
127
+ """
128
+ return 500 <= code < 600
129
+
130
+
131
+ def is_success_code(code: int) -> bool:
132
+ """Check if a status code represents success (2xx).
133
+
134
+ Args:
135
+ code: HTTP status code
136
+
137
+ Returns:
138
+ True if the code is a success code, False otherwise
139
+ """
140
+ return 200 <= code < 300
141
+
142
+
143
+ # Mapping of HTTP status codes to Python exception class names
144
+ # These are semantically meaningful names that Python developers expect
145
+ HTTP_EXCEPTION_NAMES: dict[int, str] = {
146
+ # 4xx Client Errors
147
+ 400: "BadRequestError",
148
+ 401: "UnauthorisedError",
149
+ 402: "PaymentRequiredError",
150
+ 403: "ForbiddenError",
151
+ 404: "NotFoundError",
152
+ 405: "MethodNotAllowedError",
153
+ 406: "NotAcceptableError",
154
+ 407: "ProxyAuthenticationRequiredError",
155
+ 408: "RequestTimeoutError",
156
+ 409: "ConflictError",
157
+ 410: "GoneError",
158
+ 411: "LengthRequiredError",
159
+ 412: "PreconditionFailedError",
160
+ 413: "PayloadTooLargeError",
161
+ 414: "UriTooLongError",
162
+ 415: "UnsupportedMediaTypeError",
163
+ 416: "RangeNotSatisfiableError",
164
+ 417: "ExpectationFailedError",
165
+ 418: "ImATeapotError",
166
+ 421: "MisdirectedRequestError",
167
+ 422: "UnprocessableEntityError",
168
+ 423: "LockedError",
169
+ 424: "FailedDependencyError",
170
+ 425: "TooEarlyError",
171
+ 426: "UpgradeRequiredError",
172
+ 428: "PreconditionRequiredError",
173
+ 429: "TooManyRequestsError",
174
+ 431: "RequestHeaderFieldsTooLargeError",
175
+ 451: "UnavailableForLegalReasonsError",
176
+ # 5xx Server Errors
177
+ 500: "InternalServerError",
178
+ 501: "NotImplementedError", # Note: Conflicts with Python built-in, will be handled
179
+ 502: "BadGatewayError",
180
+ 503: "ServiceUnavailableError",
181
+ 504: "GatewayTimeoutError",
182
+ 505: "HttpVersionNotSupportedError",
183
+ 506: "VariantAlsoNegotiatesError",
184
+ 507: "InsufficientStorageError",
185
+ 508: "LoopDetectedError",
186
+ 510: "NotExtendedError",
187
+ 511: "NetworkAuthenticationRequiredError",
188
+ }
189
+
190
+
191
+ def get_exception_class_name(code: int) -> str:
192
+ """Get the Python exception class name for an HTTP status code.
193
+
194
+ Args:
195
+ code: HTTP status code (e.g., 404)
196
+
197
+ Returns:
198
+ Python exception class name (e.g., "NotFoundError"), or "Error{code}" as fallback
199
+
200
+ Examples:
201
+ >>> get_exception_class_name(404)
202
+ 'NotFoundError'
203
+ >>> get_exception_class_name(429)
204
+ 'TooManyRequestsError'
205
+ >>> get_exception_class_name(999) # Unknown code
206
+ 'Error999'
207
+ """
208
+ # Check if we have a semantic name for this code
209
+ if code in HTTP_EXCEPTION_NAMES:
210
+ name = HTTP_EXCEPTION_NAMES[code]
211
+ # Handle Python keyword conflicts
212
+ if name == "NotImplementedError":
213
+ # Avoid conflict with Python's built-in NotImplementedError
214
+ return "HttpNotImplementedError"
215
+ return name
216
+
217
+ # Fallback to Error{code} for codes we don't have semantic names for
218
+ return f"Error{code}"
@@ -0,0 +1,222 @@
1
+ from typing import Any, Protocol
2
+
3
+ import httpx
4
+
5
+ from .auth.base import BaseAuth
6
+ from .exceptions import HTTPError
7
+
8
+
9
+ class HttpTransport(Protocol):
10
+ """
11
+ Defines the interface for an asynchronous HTTP transport layer.
12
+
13
+ This protocol allows different HTTP client implementations (like httpx, aiohttp)
14
+ to be used interchangeably by the generated API client. It requires
15
+ implementing classes to provide an async `request` method.
16
+
17
+ All implementations must:
18
+ - Provide a fully type-annotated async `request` method.
19
+ - Return an `httpx.Response` object for all requests.
20
+ - Be safe for use in async contexts.
21
+ - STRICT REQUIREMENT: Raise `HTTPError` for all HTTP responses with status codes < 200 or >= 300.
22
+ This ensures that only successful (2xx) responses are returned to the caller, and all error
23
+ or informational responses are handled via exceptions. Implementors must not return non-2xx
24
+ responses directly; instead, they must raise `HTTPError` with the status code and response body.
25
+ This contract guarantees consistent error handling for all generated clients.
26
+ """
27
+
28
+ async def request(
29
+ self,
30
+ method: str,
31
+ url: str,
32
+ **kwargs: Any,
33
+ ) -> httpx.Response:
34
+ """
35
+ Sends an asynchronous HTTP request.
36
+
37
+ Args:
38
+ method: The HTTP method (e.g., 'GET', 'POST').
39
+ url: The target URL for the request.
40
+ **kwargs: Additional keyword arguments for the HTTP client (e.g., headers, params, json, data).
41
+
42
+ Returns:
43
+ httpx.Response: The HTTP response object.
44
+
45
+ Raises:
46
+ HTTPError: For all HTTP responses with status codes < 200 or >= 300. Implementors MUST raise
47
+ this exception for any non-2xx response, passing the status code and response body.
48
+ Exception: Implementations may raise exceptions for network errors or invalid responses.
49
+
50
+ Protocol Contract:
51
+ - Only successful (2xx) responses are returned to the caller.
52
+ - All other responses (including redirects, client errors, and server errors) must result in
53
+ an HTTPError being raised. This ensures that error handling is consistent and explicit
54
+ across all generated clients and transport implementations.
55
+ """
56
+ raise NotImplementedError()
57
+
58
+ async def close(self) -> None:
59
+ """
60
+ Closes any resources held by the transport (e.g., HTTP connections).
61
+
62
+ All implementations must provide this method. If the transport does not hold resources,
63
+ this should be a no-op.
64
+ """
65
+ raise NotImplementedError()
66
+
67
+
68
+ class HttpxTransport:
69
+ """
70
+ A concrete implementation of the HttpTransport protocol using the `httpx` library.
71
+
72
+ This class provides the default asynchronous HTTP transport mechanism for the
73
+ generated API client. It wraps an `httpx.AsyncClient` instance to handle
74
+ request sending, connection pooling, and resource management.
75
+
76
+ Optionally supports authentication via any BaseAuth-compatible plugin, including CompositeAuth.
77
+
78
+ CONTRACT:
79
+ - This implementation strictly raises `HTTPError` for all HTTP responses with status codes < 200 or >= 300.
80
+ Only successful (2xx) responses are returned to the caller. This ensures that all error, informational,
81
+ and redirect responses are handled via exceptions, providing a consistent error handling model for the
82
+ generated client code.
83
+
84
+ Attributes:
85
+ _client (httpx.AsyncClient): Configured HTTPX async client for all requests.
86
+ _auth (BaseAuth | None): Optional authentication plugin for request signing (can be CompositeAuth).
87
+ _bearer_token (str | None): Optional bearer token for Authorization header.
88
+ _default_headers (dict[str, str] | None): Default headers to apply to all requests.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ base_url: str,
94
+ timeout: float | None = None,
95
+ auth: BaseAuth | None = None,
96
+ bearer_token: str | None = None,
97
+ default_headers: dict[str, str] | None = None,
98
+ verify_ssl: bool = True,
99
+ ) -> None:
100
+ """
101
+ Initializes the HttpxTransport.
102
+
103
+ Args:
104
+ base_url (str): The base URL for all API requests made through this transport.
105
+ timeout (float | None): The default timeout in seconds for requests. If None, httpx's default is used.
106
+ auth (BaseAuth | None): Optional authentication plugin for request signing (can be CompositeAuth).
107
+ bearer_token (str | None): Optional raw bearer token string for Authorization header.
108
+ default_headers (dict[str, str] | None): Default headers to apply to all requests.
109
+ verify_ssl (bool): Whether to verify SSL certificates. Defaults to True.
110
+ Set to False for local development with self-signed certificates.
111
+
112
+ Note:
113
+ If both auth and bearer_token are provided, auth takes precedence.
114
+ """
115
+ self._client: httpx.AsyncClient = httpx.AsyncClient(base_url=base_url, timeout=timeout, verify=verify_ssl)
116
+ self._auth: BaseAuth | None = auth
117
+ self._bearer_token: str | None = bearer_token
118
+ self._default_headers: dict[str, str] | None = default_headers
119
+
120
+ async def _prepare_headers(
121
+ self,
122
+ current_request_kwargs: dict[str, Any],
123
+ ) -> dict[str, str]:
124
+ """
125
+ Prepares headers for an HTTP request, incorporating default headers,
126
+ request-specific headers, and authentication.
127
+ """
128
+ # Initialize headers for the current request
129
+ prepared_headers: dict[str, str] = {}
130
+
131
+ # 1. Apply transport-level default headers
132
+ if self._default_headers:
133
+ prepared_headers.update(self._default_headers)
134
+
135
+ # 2. Merge headers passed specifically for this request (overriding transport defaults)
136
+ if "headers" in current_request_kwargs and isinstance(current_request_kwargs["headers"], dict):
137
+ prepared_headers.update(current_request_kwargs["headers"])
138
+
139
+ # 3. Apply authentication plugin or bearer token (which can further modify headers)
140
+ # We pass a temporary request_args dict containing only the headers to the auth plugin,
141
+ # as the auth plugin might expect other keys which are not relevant for header preparation.
142
+ # The auth plugin is expected to modify the 'headers' key in the passed dict.
143
+ temp_request_args_for_auth = {"headers": prepared_headers.copy()}
144
+
145
+ if self._auth is not None:
146
+ authenticated_args = await self._auth.authenticate_request(temp_request_args_for_auth)
147
+ # Ensure 'headers' key exists and is a dict after authentication
148
+ if "headers" in authenticated_args and isinstance(authenticated_args["headers"], dict):
149
+ prepared_headers = authenticated_args["headers"]
150
+ else:
151
+ # Handle cases where auth plugin might not return headers as expected
152
+ # This could be an error or a specific design of an auth plugin.
153
+ # For now, we assume it should always return a 'headers' dict.
154
+ # If not, we retain the headers we had before calling the auth plugin.
155
+ pass # Or raise an error, or log a warning.
156
+ elif self._bearer_token is not None:
157
+ # If no auth plugin, but bearer token is present, add/overwrite Authorization header.
158
+ prepared_headers["Authorization"] = f"Bearer {self._bearer_token}"
159
+
160
+ return prepared_headers
161
+
162
+ async def request(
163
+ self,
164
+ method: str,
165
+ url: str,
166
+ **kwargs: Any,
167
+ ) -> httpx.Response:
168
+ """
169
+ Sends an asynchronous HTTP request using the underlying httpx.AsyncClient.
170
+
171
+ Args:
172
+ method (str): The HTTP method (e.g., 'GET', 'POST').
173
+ url (str): The target URL path, relative to the `base_url` provided during initialization, or an absolute
174
+ URL.
175
+ **kwargs: Additional keyword arguments passed directly to `httpx.AsyncClient.request` (e.g., headers,
176
+ params, json, data).
177
+
178
+ Returns:
179
+ httpx.Response: The HTTP response object from the server.
180
+
181
+ Raises:
182
+ httpx.HTTPError: For network errors or invalid responses.
183
+ HTTPError: For non-2xx HTTP responses.
184
+ """
185
+ # Prepare request arguments, excluding headers initially
186
+ request_args: dict[str, Any] = {k: v for k, v in kwargs.items() if k != "headers"}
187
+
188
+ # This method handles default headers, request-specific headers, and authentication
189
+ prepared_headers = await self._prepare_headers(kwargs)
190
+ request_args["headers"] = prepared_headers
191
+
192
+ response = await self._client.request(method, url, **request_args)
193
+ if response.status_code < 200 or response.status_code >= 300:
194
+ raise HTTPError(status_code=response.status_code, message=response.text, response=response)
195
+ return response
196
+
197
+ async def close(self) -> None:
198
+ """
199
+ Closes the underlying httpx.AsyncClient and releases resources.
200
+
201
+ This should be called when the transport is no longer needed, typically
202
+ when the main API client is being shut down, to ensure proper cleanup
203
+ of network connections.
204
+ """
205
+ await self._client.aclose()
206
+
207
+ async def __aenter__(self) -> "HttpxTransport":
208
+ """
209
+ Enter the async context manager. Returns self.
210
+ """
211
+ return self
212
+
213
+ async def __aexit__(
214
+ self,
215
+ exc_type: type[BaseException] | None,
216
+ exc_val: BaseException | None,
217
+ exc_tb: object | None,
218
+ ) -> None:
219
+ """
220
+ Exit the async context manager. Calls close().
221
+ """
222
+ await self.close()
@@ -0,0 +1,12 @@
1
+ """Loader package to transform OpenAPI specs into Intermediate Representation.
2
+
3
+ This package contains the classes and functions to load OpenAPI specifications
4
+ and transform them into the internal IR dataclasses, which are then used for
5
+ code generation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .loader import SpecLoader, load_ir_from_spec
11
+
12
+ __all__ = ["SpecLoader", "load_ir_from_spec"]
@@ -0,0 +1,174 @@
1
+ """OpenAPI Spec Loader.
2
+
3
+ Provides the main SpecLoader class and utilities to transform a validated OpenAPI spec
4
+ into the internal IR dataclasses. This implementation covers a subset of the OpenAPI
5
+ surface, sufficient for the code emitter prototypes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ import warnings
13
+ from typing import Any, List, Mapping, cast
14
+
15
+ try:
16
+ # Use the newer validate() API if available to avoid deprecation warnings
17
+ from openapi_spec_validator import validate as validate_spec
18
+ except ImportError:
19
+ try:
20
+ from openapi_spec_validator import validate_spec # type: ignore
21
+ except ImportError: # pragma: no cover – optional in early bootstrapping
22
+ validate_spec = None # type: ignore[assignment]
23
+
24
+ from pyopenapi_gen import IRSpec
25
+ from pyopenapi_gen.core.loader.operations import parse_operations
26
+ from pyopenapi_gen.core.loader.schemas import build_schemas, extract_inline_enums
27
+
28
+ __all__ = ["SpecLoader", "load_ir_from_spec"]
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Check for cycle detection debug flags in environment
33
+ MAX_CYCLES = int(os.environ.get("PYOPENAPI_MAX_CYCLES", "0"))
34
+
35
+
36
+ class SpecLoader:
37
+ """Transforms a validated OpenAPI spec into IR dataclasses.
38
+
39
+ This class follows the Design by Contract principles and ensures that
40
+ all operations maintain proper invariants and verify their inputs/outputs.
41
+ """
42
+
43
+ def __init__(self, spec: Mapping[str, Any]):
44
+ """Initialize the spec loader with an OpenAPI spec.
45
+
46
+ Contracts:
47
+ Preconditions:
48
+ - spec is a valid OpenAPI spec mapping
49
+ - spec contains required OpenAPI fields
50
+ Postconditions:
51
+ - Instance is ready to load IR from the spec
52
+ """
53
+ if not isinstance(spec, Mapping):
54
+ raise ValueError("spec must be a Mapping")
55
+ if "openapi" not in spec:
56
+ raise ValueError("Missing 'openapi' field in the specification")
57
+ if "paths" not in spec:
58
+ raise ValueError("Missing 'paths' section in the specification")
59
+
60
+ self.spec = spec
61
+ self.info = spec.get("info", {})
62
+ self.title = self.info.get("title", "API Client")
63
+ self.version = self.info.get("version", "0.0.0")
64
+ self.description = self.info.get("description")
65
+ self.raw_components = spec.get("components", {})
66
+ self.raw_schemas = self.raw_components.get("schemas", {})
67
+ self.raw_parameters = self.raw_components.get("parameters", {})
68
+ self.raw_responses = self.raw_components.get("responses", {})
69
+ self.raw_request_bodies = self.raw_components.get("requestBodies", {})
70
+ self.paths = spec["paths"]
71
+ self.servers = [s.get("url") for s in spec.get("servers", []) if "url" in s]
72
+
73
+ def validate(self) -> List[str]:
74
+ """Validate the OpenAPI spec but continue on errors.
75
+
76
+ Contracts:
77
+ Postconditions:
78
+ - Returns a list of validation warnings
79
+ - The spec is validated using openapi-spec-validator if available
80
+ """
81
+ warnings_list = []
82
+
83
+ if validate_spec is not None:
84
+ try:
85
+ from typing import Hashable
86
+
87
+ validate_spec(cast(Mapping[Hashable, Any], self.spec))
88
+ except Exception as e:
89
+ warning_msg = f"OpenAPI spec validation error: {e}"
90
+ # Always collect the message
91
+ warnings_list.append(warning_msg)
92
+
93
+ # Heuristic: if this error originates from jsonschema or
94
+ # openapi_spec_validator, prefer logging over global warnings
95
+ # to avoid noisy test output while still surfacing the issue.
96
+ origin_module = getattr(e.__class__, "__module__", "")
97
+ if (
98
+ isinstance(e, RecursionError)
99
+ or origin_module.startswith("jsonschema")
100
+ or origin_module.startswith("openapi_spec_validator")
101
+ ):
102
+ logger.warning(warning_msg)
103
+ else:
104
+ # Preserve explicit warning behavior for unexpected failures
105
+ warnings.warn(warning_msg, UserWarning)
106
+
107
+ return warnings_list
108
+
109
+ def load_ir(self) -> IRSpec:
110
+ """Transform the spec into an IRSpec object.
111
+
112
+ Contracts:
113
+ Postconditions:
114
+ - Returns a fully populated IRSpec object
115
+ - All schemas are properly processed and named
116
+ - All operations are properly parsed and linked to schemas
117
+ """
118
+ # First validate the spec
119
+ self.validate()
120
+
121
+ # Build schemas and create context
122
+ context = build_schemas(self.raw_schemas, self.raw_components)
123
+
124
+ # Parse operations
125
+ operations = parse_operations(
126
+ self.paths,
127
+ self.raw_parameters,
128
+ self.raw_responses,
129
+ self.raw_request_bodies,
130
+ context,
131
+ )
132
+
133
+ # Extract inline enums and add them to the schemas map
134
+ schemas_dict = extract_inline_enums(context.parsed_schemas)
135
+
136
+ # Emit collected warnings after all parsing is done
137
+ for warning_msg in context.collected_warnings:
138
+ warnings.warn(warning_msg, UserWarning)
139
+
140
+ # Create and return the IR spec
141
+ ir_spec = IRSpec(
142
+ title=self.title,
143
+ version=self.version,
144
+ description=self.description,
145
+ schemas=schemas_dict,
146
+ operations=operations,
147
+ servers=self.servers,
148
+ )
149
+
150
+ # Post-condition check
151
+ if ir_spec.schemas != schemas_dict:
152
+ raise RuntimeError("Schemas mismatch in IRSpec")
153
+ if ir_spec.operations != operations:
154
+ raise RuntimeError("Operations mismatch in IRSpec")
155
+
156
+ return ir_spec
157
+
158
+
159
+ def load_ir_from_spec(spec: Mapping[str, Any]) -> IRSpec:
160
+ """Orchestrate the transformation of a spec dict into IRSpec.
161
+
162
+ This is a convenience function that creates a SpecLoader and calls load_ir().
163
+
164
+ Contracts:
165
+ Preconditions:
166
+ - spec is a valid OpenAPI spec mapping
167
+ Postconditions:
168
+ - Returns a fully populated IRSpec object
169
+ """
170
+ if not isinstance(spec, Mapping):
171
+ raise ValueError("spec must be a Mapping")
172
+
173
+ loader = SpecLoader(spec)
174
+ return loader.load_ir()
@@ -0,0 +1,12 @@
1
+ """Operation parsing utilities.
2
+
3
+ Functions to extract and transform operations from raw OpenAPI specifications.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .parser import parse_operations
9
+ from .post_processor import post_process_operation
10
+ from .request_body import parse_request_body
11
+
12
+ __all__ = ["parse_operations", "post_process_operation", "parse_request_body"]