pyopenapi-gen 0.12.1__py3-none-any.whl → 0.14.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.
- pyopenapi_gen/core/http_status_codes.py +220 -0
- pyopenapi_gen/core/parsing/schema_parser.py +50 -5
- pyopenapi_gen/core/postprocess_manager.py +85 -12
- pyopenapi_gen/core/writers/python_construct_renderer.py +5 -1
- pyopenapi_gen/emitters/exceptions_emitter.py +150 -16
- pyopenapi_gen/generator/client_generator.py +4 -2
- pyopenapi_gen/types/resolvers/schema_resolver.py +67 -10
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +3 -2
- pyopenapi_gen/visit/exception_visitor.py +54 -16
- {pyopenapi_gen-0.12.1.dist-info → pyopenapi_gen-0.14.0.dist-info}/METADATA +1 -1
- {pyopenapi_gen-0.12.1.dist-info → pyopenapi_gen-0.14.0.dist-info}/RECORD +14 -13
- {pyopenapi_gen-0.12.1.dist-info → pyopenapi_gen-0.14.0.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.12.1.dist-info → pyopenapi_gen-0.14.0.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.12.1.dist-info → pyopenapi_gen-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,220 @@
|
|
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
|
+
from typing import Dict
|
12
|
+
|
13
|
+
# Standard HTTP status codes with human-readable names
|
14
|
+
HTTP_STATUS_CODES: Dict[int, str] = {
|
15
|
+
# 1xx Informational
|
16
|
+
100: "Continue",
|
17
|
+
101: "Switching Protocols",
|
18
|
+
102: "Processing",
|
19
|
+
103: "Early Hints",
|
20
|
+
# 2xx Success
|
21
|
+
200: "OK",
|
22
|
+
201: "Created",
|
23
|
+
202: "Accepted",
|
24
|
+
203: "Non-Authoritative Information",
|
25
|
+
204: "No Content",
|
26
|
+
205: "Reset Content",
|
27
|
+
206: "Partial Content",
|
28
|
+
207: "Multi-Status",
|
29
|
+
208: "Already Reported",
|
30
|
+
226: "IM Used",
|
31
|
+
# 3xx Redirection
|
32
|
+
300: "Multiple Choices",
|
33
|
+
301: "Moved Permanently",
|
34
|
+
302: "Found",
|
35
|
+
303: "See Other",
|
36
|
+
304: "Not Modified",
|
37
|
+
305: "Use Proxy",
|
38
|
+
307: "Temporary Redirect",
|
39
|
+
308: "Permanent Redirect",
|
40
|
+
# 4xx Client Error
|
41
|
+
400: "Bad Request",
|
42
|
+
401: "Unauthorised",
|
43
|
+
402: "Payment Required",
|
44
|
+
403: "Forbidden",
|
45
|
+
404: "Not Found",
|
46
|
+
405: "Method Not Allowed",
|
47
|
+
406: "Not Acceptable",
|
48
|
+
407: "Proxy Authentication Required",
|
49
|
+
408: "Request Timeout",
|
50
|
+
409: "Conflict",
|
51
|
+
410: "Gone",
|
52
|
+
411: "Length Required",
|
53
|
+
412: "Precondition Failed",
|
54
|
+
413: "Payload Too Large",
|
55
|
+
414: "URI Too Long",
|
56
|
+
415: "Unsupported Media Type",
|
57
|
+
416: "Range Not Satisfiable",
|
58
|
+
417: "Expectation Failed",
|
59
|
+
418: "I'm a teapot",
|
60
|
+
421: "Misdirected Request",
|
61
|
+
422: "Unprocessable Entity",
|
62
|
+
423: "Locked",
|
63
|
+
424: "Failed Dependency",
|
64
|
+
425: "Too Early",
|
65
|
+
426: "Upgrade Required",
|
66
|
+
428: "Precondition Required",
|
67
|
+
429: "Too Many Requests",
|
68
|
+
431: "Request Header Fields Too Large",
|
69
|
+
451: "Unavailable For Legal Reasons",
|
70
|
+
# 5xx Server Error
|
71
|
+
500: "Internal Server Error",
|
72
|
+
501: "Not Implemented",
|
73
|
+
502: "Bad Gateway",
|
74
|
+
503: "Service Unavailable",
|
75
|
+
504: "Gateway Timeout",
|
76
|
+
505: "HTTP Version Not Supported",
|
77
|
+
506: "Variant Also Negotiates",
|
78
|
+
507: "Insufficient Storage",
|
79
|
+
508: "Loop Detected",
|
80
|
+
510: "Not Extended",
|
81
|
+
511: "Network Authentication Required",
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
def get_status_name(code: int) -> str:
|
86
|
+
"""Get the human-readable name for an HTTP status code.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
code: HTTP status code (e.g., 404)
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Human-readable status name (e.g., "Not Found"), or "Unknown" if not found
|
93
|
+
"""
|
94
|
+
return HTTP_STATUS_CODES.get(code, "Unknown")
|
95
|
+
|
96
|
+
|
97
|
+
def is_error_code(code: int) -> bool:
|
98
|
+
"""Check if a status code represents an error (4xx or 5xx).
|
99
|
+
|
100
|
+
Args:
|
101
|
+
code: HTTP status code
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
True if the code is a client or server error, False otherwise
|
105
|
+
"""
|
106
|
+
return 400 <= code < 600
|
107
|
+
|
108
|
+
|
109
|
+
def is_client_error(code: int) -> bool:
|
110
|
+
"""Check if a status code represents a client error (4xx).
|
111
|
+
|
112
|
+
Args:
|
113
|
+
code: HTTP status code
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
True if the code is a client error, False otherwise
|
117
|
+
"""
|
118
|
+
return 400 <= code < 500
|
119
|
+
|
120
|
+
|
121
|
+
def is_server_error(code: int) -> bool:
|
122
|
+
"""Check if a status code represents a server error (5xx).
|
123
|
+
|
124
|
+
Args:
|
125
|
+
code: HTTP status code
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
True if the code is a server error, False otherwise
|
129
|
+
"""
|
130
|
+
return 500 <= code < 600
|
131
|
+
|
132
|
+
|
133
|
+
def is_success_code(code: int) -> bool:
|
134
|
+
"""Check if a status code represents success (2xx).
|
135
|
+
|
136
|
+
Args:
|
137
|
+
code: HTTP status code
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
True if the code is a success code, False otherwise
|
141
|
+
"""
|
142
|
+
return 200 <= code < 300
|
143
|
+
|
144
|
+
|
145
|
+
# Mapping of HTTP status codes to Python exception class names
|
146
|
+
# These are semantically meaningful names that Python developers expect
|
147
|
+
HTTP_EXCEPTION_NAMES: Dict[int, str] = {
|
148
|
+
# 4xx Client Errors
|
149
|
+
400: "BadRequestError",
|
150
|
+
401: "UnauthorisedError",
|
151
|
+
402: "PaymentRequiredError",
|
152
|
+
403: "ForbiddenError",
|
153
|
+
404: "NotFoundError",
|
154
|
+
405: "MethodNotAllowedError",
|
155
|
+
406: "NotAcceptableError",
|
156
|
+
407: "ProxyAuthenticationRequiredError",
|
157
|
+
408: "RequestTimeoutError",
|
158
|
+
409: "ConflictError",
|
159
|
+
410: "GoneError",
|
160
|
+
411: "LengthRequiredError",
|
161
|
+
412: "PreconditionFailedError",
|
162
|
+
413: "PayloadTooLargeError",
|
163
|
+
414: "UriTooLongError",
|
164
|
+
415: "UnsupportedMediaTypeError",
|
165
|
+
416: "RangeNotSatisfiableError",
|
166
|
+
417: "ExpectationFailedError",
|
167
|
+
418: "ImATeapotError",
|
168
|
+
421: "MisdirectedRequestError",
|
169
|
+
422: "UnprocessableEntityError",
|
170
|
+
423: "LockedError",
|
171
|
+
424: "FailedDependencyError",
|
172
|
+
425: "TooEarlyError",
|
173
|
+
426: "UpgradeRequiredError",
|
174
|
+
428: "PreconditionRequiredError",
|
175
|
+
429: "TooManyRequestsError",
|
176
|
+
431: "RequestHeaderFieldsTooLargeError",
|
177
|
+
451: "UnavailableForLegalReasonsError",
|
178
|
+
# 5xx Server Errors
|
179
|
+
500: "InternalServerError",
|
180
|
+
501: "NotImplementedError", # Note: Conflicts with Python built-in, will be handled
|
181
|
+
502: "BadGatewayError",
|
182
|
+
503: "ServiceUnavailableError",
|
183
|
+
504: "GatewayTimeoutError",
|
184
|
+
505: "HttpVersionNotSupportedError",
|
185
|
+
506: "VariantAlsoNegotiatesError",
|
186
|
+
507: "InsufficientStorageError",
|
187
|
+
508: "LoopDetectedError",
|
188
|
+
510: "NotExtendedError",
|
189
|
+
511: "NetworkAuthenticationRequiredError",
|
190
|
+
}
|
191
|
+
|
192
|
+
|
193
|
+
def get_exception_class_name(code: int) -> str:
|
194
|
+
"""Get the Python exception class name for an HTTP status code.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
code: HTTP status code (e.g., 404)
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
Python exception class name (e.g., "NotFoundError"), or "Error{code}" as fallback
|
201
|
+
|
202
|
+
Examples:
|
203
|
+
>>> get_exception_class_name(404)
|
204
|
+
'NotFoundError'
|
205
|
+
>>> get_exception_class_name(429)
|
206
|
+
'TooManyRequestsError'
|
207
|
+
>>> get_exception_class_name(999) # Unknown code
|
208
|
+
'Error999'
|
209
|
+
"""
|
210
|
+
# Check if we have a semantic name for this code
|
211
|
+
if code in HTTP_EXCEPTION_NAMES:
|
212
|
+
name = HTTP_EXCEPTION_NAMES[code]
|
213
|
+
# Handle Python keyword conflicts
|
214
|
+
if name == "NotImplementedError":
|
215
|
+
# Avoid conflict with Python's built-in NotImplementedError
|
216
|
+
return "HttpNotImplementedError"
|
217
|
+
return name
|
218
|
+
|
219
|
+
# Fallback to Error{code} for codes we don't have semantic names for
|
220
|
+
return f"Error{code}"
|
@@ -17,6 +17,8 @@ from .keywords.any_of_parser import _parse_any_of_schemas
|
|
17
17
|
from .keywords.one_of_parser import _parse_one_of_schemas
|
18
18
|
from .unified_cycle_detection import CycleAction
|
19
19
|
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
20
22
|
# Environment variables for configurable limits, with defaults
|
21
23
|
try:
|
22
24
|
MAX_CYCLES = int(os.environ.get("PYOPENAPI_MAX_CYCLES", "0")) # Default 0 means no explicit cycle count limit
|
@@ -27,8 +29,6 @@ try:
|
|
27
29
|
except ValueError:
|
28
30
|
ENV_MAX_DEPTH = 150 # Fallback to 150 if env var is invalid
|
29
31
|
|
30
|
-
logger = logging.getLogger(__name__)
|
31
|
-
|
32
32
|
|
33
33
|
def _resolve_ref(
|
34
34
|
ref_path_str: str,
|
@@ -417,7 +417,23 @@ def _parse_schema(
|
|
417
417
|
raw_type_field = schema_node.get("type")
|
418
418
|
|
419
419
|
if isinstance(raw_type_field, str):
|
420
|
-
|
420
|
+
# Handle non-standard type values that might appear
|
421
|
+
if raw_type_field in ["Any", "any"]:
|
422
|
+
# Convert 'Any' to None - will be handled as object later
|
423
|
+
extracted_type = None
|
424
|
+
logger.warning(
|
425
|
+
f"Schema{f' {schema_name}' if schema_name else ''} uses non-standard type 'Any'. "
|
426
|
+
"Converting to 'object'. Use standard OpenAPI types: string, number, integer, boolean, array, object."
|
427
|
+
)
|
428
|
+
elif raw_type_field == "None":
|
429
|
+
# Convert 'None' string to null handling
|
430
|
+
extracted_type = "null"
|
431
|
+
logger.warning(
|
432
|
+
f"Schema{f' {schema_name}' if schema_name else ''} uses type 'None'. "
|
433
|
+
'Converting to nullable object. Use \'type: ["object", "null"]\' for nullable types.'
|
434
|
+
)
|
435
|
+
else:
|
436
|
+
extracted_type = raw_type_field
|
421
437
|
elif isinstance(raw_type_field, list):
|
422
438
|
if "null" in raw_type_field:
|
423
439
|
is_nullable_from_type_field = True
|
@@ -446,10 +462,33 @@ def _parse_schema(
|
|
446
462
|
if props_from_comp or "allOf" in schema_node or "properties" in schema_node:
|
447
463
|
current_final_type = "object"
|
448
464
|
elif any_of_irs or one_of_irs:
|
465
|
+
# Keep None for composition types - they'll be handled by resolver
|
449
466
|
current_final_type = None
|
467
|
+
elif "enum" in schema_node:
|
468
|
+
# Enum without explicit type - infer from enum values
|
469
|
+
enum_values = schema_node.get("enum", [])
|
470
|
+
if enum_values:
|
471
|
+
first_val = enum_values[0]
|
472
|
+
if isinstance(first_val, str):
|
473
|
+
current_final_type = "string"
|
474
|
+
elif isinstance(first_val, (int, float)):
|
475
|
+
current_final_type = "number"
|
476
|
+
elif isinstance(first_val, bool):
|
477
|
+
current_final_type = "boolean"
|
478
|
+
else:
|
479
|
+
# Fallback to object for complex enum values
|
480
|
+
current_final_type = "object"
|
481
|
+
else:
|
482
|
+
current_final_type = "string" # Default for empty enums
|
483
|
+
else:
|
484
|
+
# No type specified and no clear indicators - default to object
|
485
|
+
# This is safer than 'Any' and matches OpenAPI spec defaults
|
486
|
+
current_final_type = "object"
|
450
487
|
|
451
488
|
if current_final_type == "null":
|
452
|
-
|
489
|
+
# Explicit null type - mark as nullable but use object type
|
490
|
+
is_nullable_overall = True
|
491
|
+
current_final_type = "object"
|
453
492
|
|
454
493
|
if current_final_type == "object":
|
455
494
|
# Properties from allOf have already been handled by _parse_composition_keywords
|
@@ -511,7 +550,13 @@ def _parse_schema(
|
|
511
550
|
else:
|
512
551
|
items_ir = actual_item_ir
|
513
552
|
else:
|
514
|
-
|
553
|
+
# Array without items specification - use object as safer default
|
554
|
+
# Log warning to help developers fix their specs
|
555
|
+
logger.warning(
|
556
|
+
f"Array type without 'items' specification found{f' in {schema_name}' if schema_name else ''}. "
|
557
|
+
"Using 'object' as item type. Consider adding 'items' to your OpenAPI spec for better type safety."
|
558
|
+
)
|
559
|
+
items_ir = IRSchema(type="object")
|
515
560
|
|
516
561
|
schema_ir_name_attr = NameSanitizer.sanitize_class_name(schema_name) if schema_name else None
|
517
562
|
|
@@ -32,13 +32,15 @@ class PostprocessManager:
|
|
32
32
|
# Ensure all targets are Path objects
|
33
33
|
target_paths = [Path(t) for t in targets]
|
34
34
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
35
|
+
# OPTIMISED: Run Ruff once on all files instead of per-file
|
36
|
+
# Collect all Python files
|
37
|
+
python_files = [p for p in target_paths if p.is_file() and p.suffix == ".py"]
|
38
|
+
|
39
|
+
if python_files:
|
40
|
+
# Run Ruff checks once on all files (much faster than per-file)
|
41
|
+
self.remove_unused_imports_bulk(python_files)
|
42
|
+
self.sort_imports_bulk(python_files)
|
43
|
+
self.format_code_bulk(python_files)
|
42
44
|
|
43
45
|
# Determine the package root directory(s) for Mypy
|
44
46
|
package_roots = set()
|
@@ -58,11 +60,82 @@ class PostprocessManager:
|
|
58
60
|
package_roots.add(target_path)
|
59
61
|
|
60
62
|
# Run Mypy on each identified package root
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
# TEMPORARILY DISABLED: Mypy is slow on large specs, disabled for faster iteration
|
64
|
+
# if package_roots:
|
65
|
+
# print(f"Running Mypy on package root(s): {package_roots}")
|
66
|
+
# for root_dir in package_roots:
|
67
|
+
# print(f"Running mypy on {root_dir}...")
|
68
|
+
# self.type_check(root_dir)
|
69
|
+
|
70
|
+
def remove_unused_imports_bulk(self, targets: List[Path]) -> None:
|
71
|
+
"""Remove unused imports from multiple targets using Ruff (bulk operation)."""
|
72
|
+
if not targets:
|
73
|
+
return
|
74
|
+
result = subprocess.run(
|
75
|
+
[
|
76
|
+
sys.executable,
|
77
|
+
"-m",
|
78
|
+
"ruff",
|
79
|
+
"check",
|
80
|
+
"--select=F401",
|
81
|
+
"--fix",
|
82
|
+
]
|
83
|
+
+ [str(t) for t in targets],
|
84
|
+
stdout=subprocess.PIPE,
|
85
|
+
stderr=subprocess.PIPE,
|
86
|
+
text=True,
|
87
|
+
)
|
88
|
+
if result.returncode != 0 or result.stderr:
|
89
|
+
if result.stdout:
|
90
|
+
_print_filtered_stdout(result.stdout)
|
91
|
+
if result.stderr:
|
92
|
+
print(result.stderr, file=sys.stderr)
|
93
|
+
|
94
|
+
def sort_imports_bulk(self, targets: List[Path]) -> None:
|
95
|
+
"""Sort imports in multiple targets using Ruff (bulk operation)."""
|
96
|
+
if not targets:
|
97
|
+
return
|
98
|
+
result = subprocess.run(
|
99
|
+
[
|
100
|
+
sys.executable,
|
101
|
+
"-m",
|
102
|
+
"ruff",
|
103
|
+
"check",
|
104
|
+
"--select=I",
|
105
|
+
"--fix",
|
106
|
+
]
|
107
|
+
+ [str(t) for t in targets],
|
108
|
+
stdout=subprocess.PIPE,
|
109
|
+
stderr=subprocess.PIPE,
|
110
|
+
text=True,
|
111
|
+
)
|
112
|
+
if result.returncode != 0 or result.stderr:
|
113
|
+
if result.stdout:
|
114
|
+
_print_filtered_stdout(result.stdout)
|
115
|
+
if result.stderr:
|
116
|
+
print(result.stderr, file=sys.stderr)
|
117
|
+
|
118
|
+
def format_code_bulk(self, targets: List[Path]) -> None:
|
119
|
+
"""Format code in multiple targets using Ruff (bulk operation)."""
|
120
|
+
if not targets:
|
121
|
+
return
|
122
|
+
result = subprocess.run(
|
123
|
+
[
|
124
|
+
sys.executable,
|
125
|
+
"-m",
|
126
|
+
"ruff",
|
127
|
+
"format",
|
128
|
+
]
|
129
|
+
+ [str(t) for t in targets],
|
130
|
+
stdout=subprocess.PIPE,
|
131
|
+
stderr=subprocess.PIPE,
|
132
|
+
text=True,
|
133
|
+
)
|
134
|
+
if result.returncode != 0 or result.stderr:
|
135
|
+
if result.stdout:
|
136
|
+
_print_filtered_stdout(result.stdout)
|
137
|
+
if result.stderr:
|
138
|
+
print(result.stderr, file=sys.stderr)
|
66
139
|
|
67
140
|
def remove_unused_imports(self, target: Union[str, Path]) -> None:
|
68
141
|
"""Remove unused imports from the target using Ruff."""
|
@@ -312,7 +312,11 @@ class PythonConstructRenderer:
|
|
312
312
|
has_content = True
|
313
313
|
if body_lines:
|
314
314
|
for line in body_lines:
|
315
|
-
|
315
|
+
# Handle empty lines without adding indentation (Ruff W293)
|
316
|
+
if line == "":
|
317
|
+
writer.writer.newline() # Just add a newline, no indent
|
318
|
+
else:
|
319
|
+
writer.write_line(line)
|
316
320
|
has_content = True
|
317
321
|
|
318
322
|
if not has_content:
|
@@ -1,4 +1,6 @@
|
|
1
|
+
import json
|
1
2
|
import os
|
3
|
+
from pathlib import Path
|
2
4
|
from typing import Optional
|
3
5
|
|
4
6
|
from pyopenapi_gen import IRSpec
|
@@ -6,29 +8,40 @@ from pyopenapi_gen.context.render_context import RenderContext
|
|
6
8
|
|
7
9
|
from ..visit.exception_visitor import ExceptionVisitor
|
8
10
|
|
9
|
-
# Template for spec-specific exception aliases
|
10
|
-
EXCEPTIONS_ALIASES_TEMPLATE = '''
|
11
|
-
from .exceptions import HTTPError, ClientError, ServerError
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
class Error{{ code }}({% if code < 500 %}ClientError{% else %}ServerError{% endif %}):
|
16
|
-
"""Exception alias for HTTP {{ code }} responses."""
|
17
|
-
pass
|
18
|
-
{% endfor %}
|
19
|
-
'''
|
12
|
+
class ExceptionsEmitter:
|
13
|
+
"""Generates spec-specific exception aliases with multi-client support.
|
20
14
|
|
15
|
+
This emitter handles two scenarios:
|
16
|
+
1. **Single client**: Generates exception_aliases.py directly in the core package
|
17
|
+
2. **Shared core**: Maintains a registry of all needed exception codes across clients
|
18
|
+
and regenerates the complete exception_aliases.py file
|
21
19
|
|
22
|
-
|
23
|
-
|
20
|
+
The registry file (.exception_registry.json) tracks which status codes are used by
|
21
|
+
which clients, ensuring that when multiple clients share a core package, all required
|
22
|
+
exceptions are available.
|
23
|
+
"""
|
24
24
|
|
25
25
|
def __init__(self, core_package_name: str = "core", overall_project_root: Optional[str] = None) -> None:
|
26
26
|
self.visitor = ExceptionVisitor()
|
27
27
|
self.core_package_name = core_package_name
|
28
28
|
self.overall_project_root = overall_project_root
|
29
29
|
|
30
|
-
def emit(
|
30
|
+
def emit(
|
31
|
+
self, spec: IRSpec, output_dir: str, client_package_name: Optional[str] = None
|
32
|
+
) -> tuple[list[str], list[str]]:
|
33
|
+
"""Generate exception aliases for the given spec.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
spec: IRSpec containing operations and responses
|
37
|
+
output_dir: Directory where exception_aliases.py will be written
|
38
|
+
client_package_name: Name of the client package (for registry tracking)
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
Tuple of (list of generated file paths, list of exception class names)
|
42
|
+
"""
|
31
43
|
file_path = os.path.join(output_dir, "exception_aliases.py")
|
44
|
+
registry_path = os.path.join(output_dir, ".exception_registry.json")
|
32
45
|
|
33
46
|
context = RenderContext(
|
34
47
|
package_root_for_generated_code=output_dir,
|
@@ -37,16 +50,137 @@ class ExceptionsEmitter:
|
|
37
50
|
)
|
38
51
|
context.set_current_file(file_path)
|
39
52
|
|
40
|
-
|
53
|
+
# Generate exception classes for this spec
|
54
|
+
generated_code, alias_names, status_codes = self.visitor.visit(spec, context)
|
55
|
+
|
56
|
+
# Update registry if we have a client package name (shared core scenario)
|
57
|
+
if client_package_name and self._is_shared_core(output_dir):
|
58
|
+
all_codes = self._update_registry(registry_path, client_package_name, status_codes)
|
59
|
+
# Regenerate with ALL codes from registry
|
60
|
+
generated_code, alias_names = self._generate_for_codes(all_codes, context)
|
61
|
+
|
41
62
|
generated_imports = context.render_imports()
|
42
63
|
|
43
|
-
# Add __all__ list
|
64
|
+
# Add __all__ list with proper spacing (2 blank lines after last class - Ruff E305)
|
44
65
|
if alias_names:
|
45
66
|
all_list_str = ", ".join([f'"{name}"' for name in alias_names])
|
46
|
-
all_assignment = f"\n\n__all__ = [{all_list_str}]\n"
|
67
|
+
all_assignment = f"\n\n\n__all__ = [{all_list_str}]\n"
|
47
68
|
generated_code += all_assignment
|
48
69
|
|
49
70
|
full_content = f"{generated_imports}\n\n{generated_code}"
|
50
71
|
with open(file_path, "w") as f:
|
51
72
|
f.write(full_content)
|
73
|
+
|
52
74
|
return [file_path], alias_names
|
75
|
+
|
76
|
+
def _is_shared_core(self, core_dir: str) -> bool:
|
77
|
+
"""Check if this core package is shared between multiple clients.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
core_dir: Path to the core package directory
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
True if the core package is outside the immediate client package
|
84
|
+
"""
|
85
|
+
# If overall_project_root is set and different from the core dir's parent,
|
86
|
+
# we're in a shared core scenario
|
87
|
+
if self.overall_project_root:
|
88
|
+
core_path = Path(core_dir).resolve()
|
89
|
+
project_root = Path(self.overall_project_root).resolve()
|
90
|
+
# Check if there are other client directories at the same level
|
91
|
+
parent_dir = core_path.parent
|
92
|
+
return parent_dir == project_root or parent_dir.parent == project_root
|
93
|
+
return False
|
94
|
+
|
95
|
+
def _update_registry(self, registry_path: str, client_name: str, status_codes: list[int]) -> list[int]:
|
96
|
+
"""Update the exception registry with this client's status codes.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
registry_path: Path to the .exception_registry.json file
|
100
|
+
client_name: Name of the client package
|
101
|
+
status_codes: List of status codes used by this client
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Complete list of all status codes across all clients
|
105
|
+
"""
|
106
|
+
registry = {}
|
107
|
+
if os.path.exists(registry_path):
|
108
|
+
with open(registry_path) as f:
|
109
|
+
registry = json.load(f)
|
110
|
+
|
111
|
+
# Update this client's codes
|
112
|
+
registry[client_name] = sorted(status_codes)
|
113
|
+
|
114
|
+
# Write back to registry
|
115
|
+
with open(registry_path, "w") as f:
|
116
|
+
json.dump(registry, f, indent=2, sort_keys=True)
|
117
|
+
|
118
|
+
# Return union of all codes
|
119
|
+
all_codes = set()
|
120
|
+
for codes in registry.values():
|
121
|
+
all_codes.update(codes)
|
122
|
+
|
123
|
+
return sorted(all_codes)
|
124
|
+
|
125
|
+
def _generate_for_codes(self, status_codes: list[int], context: RenderContext) -> tuple[str, list[str]]:
|
126
|
+
"""Generate exception classes for a specific list of status codes.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
status_codes: List of HTTP status codes to generate exceptions for
|
130
|
+
context: Render context for imports
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
Tuple of (generated_code, exception_class_names)
|
134
|
+
"""
|
135
|
+
from ..core.http_status_codes import (
|
136
|
+
get_exception_class_name,
|
137
|
+
get_status_name,
|
138
|
+
is_client_error,
|
139
|
+
is_server_error,
|
140
|
+
)
|
141
|
+
from ..core.writers.python_construct_renderer import PythonConstructRenderer
|
142
|
+
|
143
|
+
renderer = PythonConstructRenderer()
|
144
|
+
all_exception_code = []
|
145
|
+
generated_alias_names = []
|
146
|
+
|
147
|
+
for code in status_codes:
|
148
|
+
# Determine base class
|
149
|
+
if is_client_error(code):
|
150
|
+
base_class = "ClientError"
|
151
|
+
elif is_server_error(code):
|
152
|
+
base_class = "ServerError"
|
153
|
+
else:
|
154
|
+
continue
|
155
|
+
|
156
|
+
# Get human-readable exception class name (e.g., NotFoundError instead of Error404)
|
157
|
+
class_name = get_exception_class_name(code)
|
158
|
+
generated_alias_names.append(class_name)
|
159
|
+
|
160
|
+
# Get human-readable status name for documentation
|
161
|
+
status_name = get_status_name(code)
|
162
|
+
docstring = f"HTTP {code} {status_name}.\n\nRaised when the server responds with a {code} status code."
|
163
|
+
|
164
|
+
# Define the __init__ method body
|
165
|
+
init_method_body = [
|
166
|
+
"def __init__(self, response: Response) -> None:",
|
167
|
+
f' """Initialise {class_name} with the HTTP response.',
|
168
|
+
"", # Empty line without trailing whitespace (Ruff W293)
|
169
|
+
" Args:",
|
170
|
+
" response: The httpx Response object that triggered this exception",
|
171
|
+
' """',
|
172
|
+
" super().__init__(status_code=response.status_code, message=response.text, response=response)",
|
173
|
+
]
|
174
|
+
|
175
|
+
exception_code = renderer.render_class(
|
176
|
+
class_name=class_name,
|
177
|
+
base_classes=[base_class],
|
178
|
+
docstring=docstring,
|
179
|
+
body_lines=init_method_body,
|
180
|
+
context=context,
|
181
|
+
)
|
182
|
+
all_exception_code.append(exception_code)
|
183
|
+
|
184
|
+
# Join the generated class strings with 2 blank lines between classes (PEP 8 / Ruff E302)
|
185
|
+
final_code = "\n\n\n".join(all_exception_code)
|
186
|
+
return final_code, generated_alias_names
|
@@ -203,7 +203,7 @@ class ClientGenerator:
|
|
203
203
|
overall_project_root=str(tmp_project_root_for_diff), # Use temp project root for context
|
204
204
|
)
|
205
205
|
exception_files_list, exception_alias_names = exceptions_emitter.emit(
|
206
|
-
ir, str(tmp_core_dir_for_diff)
|
206
|
+
ir, str(tmp_core_dir_for_diff), client_package_name=output_package
|
207
207
|
) # Emit TO temp core dir
|
208
208
|
exception_files = [Path(p) for p in exception_files_list]
|
209
209
|
temp_generated_files += exception_files
|
@@ -374,7 +374,9 @@ class ClientGenerator:
|
|
374
374
|
core_package_name=resolved_core_package_fqn,
|
375
375
|
overall_project_root=str(project_root),
|
376
376
|
)
|
377
|
-
exception_files_list, exception_alias_names = exceptions_emitter.emit(
|
377
|
+
exception_files_list, exception_alias_names = exceptions_emitter.emit(
|
378
|
+
ir, str(core_dir), client_package_name=output_package
|
379
|
+
)
|
378
380
|
generated_files += [Path(p) for p in exception_files_list]
|
379
381
|
self._log_progress(f"Generated {len(exception_files_list)} exception files", "EMIT_EXCEPTIONS")
|
380
382
|
|
@@ -53,11 +53,12 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
53
53
|
|
54
54
|
# Handle composition types (any_of, all_of, one_of)
|
55
55
|
# These are processed for inline compositions or when generating the alias for a named composition
|
56
|
-
|
56
|
+
# Check for the attribute existence, not just truthiness, to handle empty lists
|
57
|
+
if hasattr(schema, "any_of") and schema.any_of is not None:
|
57
58
|
return self._resolve_any_of(schema, context, required, resolve_underlying)
|
58
|
-
elif hasattr(schema, "all_of") and schema.all_of:
|
59
|
+
elif hasattr(schema, "all_of") and schema.all_of is not None:
|
59
60
|
return self._resolve_all_of(schema, context, required, resolve_underlying)
|
60
|
-
elif hasattr(schema, "one_of") and schema.one_of:
|
61
|
+
elif hasattr(schema, "one_of") and schema.one_of is not None:
|
61
62
|
return self._resolve_one_of(schema, context, required, resolve_underlying)
|
62
63
|
|
63
64
|
# Handle named schemas without generation_name (fallback for references)
|
@@ -96,7 +97,52 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
96
97
|
elif schema_type == "null":
|
97
98
|
return self._resolve_null(context, required)
|
98
99
|
else:
|
99
|
-
|
100
|
+
# Gather detailed information about the problematic schema
|
101
|
+
schema_details = {
|
102
|
+
"type": schema_type,
|
103
|
+
"name": getattr(schema, "name", None),
|
104
|
+
"ref": getattr(schema, "ref", None),
|
105
|
+
"properties": list(getattr(schema, "properties", {}).keys()) if hasattr(schema, "properties") else None,
|
106
|
+
"enum": getattr(schema, "enum", None),
|
107
|
+
"description": getattr(schema, "description", None),
|
108
|
+
"generation_name": getattr(schema, "generation_name", None),
|
109
|
+
"is_nullable": getattr(schema, "is_nullable", None),
|
110
|
+
"any_of": len(getattr(schema, "any_of", []) or []) if hasattr(schema, "any_of") else 0,
|
111
|
+
"all_of": len(getattr(schema, "all_of", []) or []) if hasattr(schema, "all_of") else 0,
|
112
|
+
"one_of": len(getattr(schema, "one_of", []) or []) if hasattr(schema, "one_of") else 0,
|
113
|
+
}
|
114
|
+
|
115
|
+
# Remove None values for cleaner output
|
116
|
+
schema_details = {k: v for k, v in schema_details.items() if v is not None}
|
117
|
+
|
118
|
+
# Create detailed error message
|
119
|
+
error_msg = f"Unknown schema type '{schema_type}' encountered."
|
120
|
+
if schema_details.get("name"):
|
121
|
+
error_msg += f" Schema name: '{schema_details['name']}'."
|
122
|
+
if schema_details.get("ref"):
|
123
|
+
error_msg += f" Reference: '{schema_details['ref']}'."
|
124
|
+
|
125
|
+
# Log full details with actionable advice
|
126
|
+
logger.warning(f"{error_msg} Full details: {schema_details}")
|
127
|
+
|
128
|
+
# Provide specific guidance based on the unknown type
|
129
|
+
if schema_type == "Any":
|
130
|
+
logger.info(
|
131
|
+
"Schema type 'Any' will be mapped to typing.Any. Consider using a more specific type in your OpenAPI spec."
|
132
|
+
)
|
133
|
+
elif schema_type == "None" or schema_type is None:
|
134
|
+
logger.info(
|
135
|
+
"Schema type 'None' detected - likely an optional field or null type. This will be mapped to Optional[Any]."
|
136
|
+
)
|
137
|
+
return self._resolve_null(context, required)
|
138
|
+
elif schema_type and isinstance(schema_type, str):
|
139
|
+
# Unknown string type - provide helpful suggestions
|
140
|
+
logger.info(f"Unknown type '{schema_type}' - common issues:")
|
141
|
+
logger.info(" 1. Typo in type name (should be: string, integer, number, boolean, array, object)")
|
142
|
+
logger.info(" 2. Using a schema name as type (should use $ref instead)")
|
143
|
+
logger.info(" 3. Custom type not supported by OpenAPI (consider using allOf/oneOf/anyOf)")
|
144
|
+
logger.info(f" Location: Check your OpenAPI spec for schemas with type='{schema_type}'")
|
145
|
+
|
100
146
|
return self._resolve_any(context)
|
101
147
|
|
102
148
|
def _resolve_reference(
|
@@ -115,7 +161,12 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
115
161
|
module_stem = getattr(schema, "final_module_stem", None)
|
116
162
|
|
117
163
|
if not module_stem:
|
118
|
-
logger.warning(f"Named schema {schema.name} missing final_module_stem")
|
164
|
+
logger.warning(f"Named schema '{schema.name}' missing final_module_stem attribute.")
|
165
|
+
logger.info(f" This usually means the schema wasn't properly processed during parsing.")
|
166
|
+
logger.info(
|
167
|
+
f" Check if '{schema.name}' is defined in components/schemas or if it's an inline schema that should be promoted."
|
168
|
+
)
|
169
|
+
logger.info(f" The schema will be treated as 'Any' type for now.")
|
119
170
|
return ResolvedType(python_type=class_name or "Any", is_optional=not required)
|
120
171
|
|
121
172
|
# Check if we're trying to import from the same module (self-import)
|
@@ -237,6 +288,10 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
237
288
|
|
238
289
|
def _resolve_null(self, context: TypeContext, required: bool) -> ResolvedType:
|
239
290
|
"""Resolve null type."""
|
291
|
+
# For null types in schemas, we need to import Any for the Optional[Any] pattern
|
292
|
+
# But the type itself is None for union composition
|
293
|
+
if not required:
|
294
|
+
context.add_import("typing", "Any")
|
240
295
|
return ResolvedType(python_type="None", is_optional=not required)
|
241
296
|
|
242
297
|
def _resolve_array(
|
@@ -245,7 +300,11 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
245
300
|
"""Resolve array type."""
|
246
301
|
items_schema = getattr(schema, "items", None)
|
247
302
|
if not items_schema:
|
248
|
-
|
303
|
+
schema_name = getattr(schema, "name", "unnamed")
|
304
|
+
logger.warning(f"Array schema '{schema_name}' missing 'items' definition.")
|
305
|
+
logger.info(" Arrays in OpenAPI must define the type of items they contain.")
|
306
|
+
logger.info(' Example: { "type": "array", "items": { "type": "string" } }')
|
307
|
+
logger.info(" This will be mapped to List[Any] - consider fixing the OpenAPI spec.")
|
249
308
|
context.add_import("typing", "List")
|
250
309
|
context.add_import("typing", "Any")
|
251
310
|
return ResolvedType(python_type="List[Any]", is_optional=not required)
|
@@ -340,10 +399,8 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
340
399
|
if hasattr(sub_schema, "type") and sub_schema.type:
|
341
400
|
return self.resolve_schema(sub_schema, context, required, resolve_underlying)
|
342
401
|
|
343
|
-
# Fallback
|
344
|
-
|
345
|
-
return self.resolve_schema(schema.all_of[0], context, required, resolve_underlying)
|
346
|
-
|
402
|
+
# Fallback - if no schema has a concrete type, return Any
|
403
|
+
# Don't recurse into schemas with no type as that causes warnings
|
347
404
|
return self._resolve_any(context)
|
348
405
|
|
349
406
|
def _resolve_one_of(
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
7
7
|
import logging
|
8
8
|
from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict
|
9
9
|
|
10
|
+
from pyopenapi_gen.core.http_status_codes import get_exception_class_name
|
10
11
|
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
11
12
|
from pyopenapi_gen.helpers.endpoint_utils import (
|
12
13
|
_get_primary_response,
|
@@ -353,8 +354,8 @@ class EndpointResponseHandlerGenerator:
|
|
353
354
|
else:
|
354
355
|
writer.write_line("return None")
|
355
356
|
else:
|
356
|
-
# Error responses
|
357
|
-
error_class_name =
|
357
|
+
# Error responses - use human-readable exception names
|
358
|
+
error_class_name = get_exception_class_name(status_code_val)
|
358
359
|
context.add_import(f"{context.core_package_name}", error_class_name)
|
359
360
|
writer.write_line(f"raise {error_class_name}(response=response)")
|
360
361
|
|
@@ -1,40 +1,78 @@
|
|
1
1
|
from pyopenapi_gen import IRSpec
|
2
2
|
|
3
3
|
from ..context.render_context import RenderContext
|
4
|
+
from ..core.http_status_codes import (
|
5
|
+
get_exception_class_name,
|
6
|
+
get_status_name,
|
7
|
+
is_client_error,
|
8
|
+
is_error_code,
|
9
|
+
is_server_error,
|
10
|
+
)
|
4
11
|
from ..core.writers.python_construct_renderer import PythonConstructRenderer
|
5
12
|
|
6
13
|
|
7
14
|
class ExceptionVisitor:
|
8
|
-
"""Visitor for rendering exception alias classes from IRSpec.
|
15
|
+
"""Visitor for rendering exception alias classes from IRSpec.
|
16
|
+
|
17
|
+
This visitor generates exception classes only for error status codes (4xx and 5xx).
|
18
|
+
Success codes (2xx) are intentionally excluded as they represent successful responses.
|
19
|
+
"""
|
9
20
|
|
10
21
|
def __init__(self) -> None:
|
11
22
|
self.renderer = PythonConstructRenderer()
|
12
23
|
|
13
|
-
def visit(self, spec: IRSpec, context: RenderContext) -> tuple[str, list[str]]:
|
14
|
-
|
15
|
-
|
24
|
+
def visit(self, spec: IRSpec, context: RenderContext) -> tuple[str, list[str], list[int]]:
|
25
|
+
"""Generate exception classes from IRSpec.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
spec: The IRSpec containing operations and responses
|
29
|
+
context: Render context for imports and code generation
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
Tuple of (generated_code, exception_class_names, status_codes_list)
|
33
|
+
"""
|
34
|
+
# Register base exception imports (only the ones we actually use)
|
35
|
+
# Note: HTTPError is not used in exception_aliases.py, so we don't import it
|
36
|
+
context.add_import("httpx", "Response") # Third-party import first (Ruff I001)
|
16
37
|
context.add_import(f"{context.core_package_name}.exceptions", "ClientError")
|
17
38
|
context.add_import(f"{context.core_package_name}.exceptions", "ServerError")
|
18
|
-
context.add_import("httpx", "Response")
|
19
39
|
|
20
|
-
# Collect unique numeric status codes
|
21
|
-
|
22
|
-
|
23
|
-
|
40
|
+
# Collect unique numeric error status codes (4xx and 5xx only)
|
41
|
+
all_codes = {
|
42
|
+
int(resp.status_code) for op in spec.operations for resp in op.responses if resp.status_code.isdigit()
|
43
|
+
}
|
44
|
+
error_codes = sorted([code for code in all_codes if is_error_code(code)])
|
24
45
|
|
25
46
|
all_exception_code = []
|
26
47
|
generated_alias_names = []
|
27
48
|
|
28
49
|
# Use renderer to generate each exception class
|
29
|
-
for code in
|
30
|
-
|
31
|
-
|
50
|
+
for code in error_codes:
|
51
|
+
# Determine base class using helper functions
|
52
|
+
if is_client_error(code):
|
53
|
+
base_class = "ClientError"
|
54
|
+
elif is_server_error(code):
|
55
|
+
base_class = "ServerError"
|
56
|
+
else:
|
57
|
+
# Should not happen since we filtered to 4xx/5xx, but be defensive
|
58
|
+
continue
|
59
|
+
|
60
|
+
# Get human-readable exception class name (e.g., NotFoundError instead of Error404)
|
61
|
+
class_name = get_exception_class_name(code)
|
32
62
|
generated_alias_names.append(class_name)
|
33
|
-
|
63
|
+
|
64
|
+
# Get human-readable status name for documentation
|
65
|
+
status_name = get_status_name(code)
|
66
|
+
docstring = f"HTTP {code} {status_name}.\n\nRaised when the server responds with a {code} status code."
|
34
67
|
|
35
68
|
# Define the __init__ method body
|
36
69
|
init_method_body = [
|
37
70
|
"def __init__(self, response: Response) -> None:",
|
71
|
+
f' """Initialise {class_name} with the HTTP response.',
|
72
|
+
"", # Empty line without trailing whitespace (Ruff W293)
|
73
|
+
" Args:",
|
74
|
+
" response: The httpx Response object that triggered this exception",
|
75
|
+
' """',
|
38
76
|
" super().__init__(status_code=response.status_code, message=response.text, response=response)",
|
39
77
|
]
|
40
78
|
|
@@ -47,6 +85,6 @@ class ExceptionVisitor:
|
|
47
85
|
)
|
48
86
|
all_exception_code.append(exception_code)
|
49
87
|
|
50
|
-
# Join the generated class strings
|
51
|
-
final_code = "\n".join(all_exception_code)
|
52
|
-
return final_code, generated_alias_names
|
88
|
+
# Join the generated class strings with 2 blank lines between classes (PEP 8 / Ruff E302)
|
89
|
+
final_code = "\n\n\n".join(all_exception_code)
|
90
|
+
return final_code, generated_alias_names, error_codes
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pyopenapi-gen
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.14.0
|
4
4
|
Summary: Modern, async-first Python client generator for OpenAPI specifications with advanced cycle detection and unified type resolution
|
5
5
|
Project-URL: Homepage, https://github.com/your-org/pyopenapi-gen
|
6
6
|
Project-URL: Documentation, https://github.com/your-org/pyopenapi-gen/blob/main/README.md
|
@@ -11,9 +11,10 @@ pyopenapi_gen/context/render_context.py,sha256=AS08ha9WVjgRUsM1LFPjMCgrsHbczHH7c
|
|
11
11
|
pyopenapi_gen/core/CLAUDE.md,sha256=bz48K-PSrhxCq5ScmiLiU9kfpVVzSWRKOA9RdKk_pbg,6482
|
12
12
|
pyopenapi_gen/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
pyopenapi_gen/core/exceptions.py,sha256=HYFiYdmzsZUl46vB8M3B6Vpp6m8iqjUcKDWdL4yEKHo,498
|
14
|
+
pyopenapi_gen/core/http_status_codes.py,sha256=lIkKFlRkvVa6xkY4kTq5f7djwYmUmJwRo1kCX8m_cEs,6274
|
14
15
|
pyopenapi_gen/core/http_transport.py,sha256=77ZOTyl0_CLuDtSCOVDQoxHDQBnclJgz6f3Hs6cy7hY,9675
|
15
16
|
pyopenapi_gen/core/pagination.py,sha256=aeDOKo-Lu8mcSDqv0TlPXV9Ul-Nca76ZuKhQHKlsMUs,2301
|
16
|
-
pyopenapi_gen/core/postprocess_manager.py,sha256=
|
17
|
+
pyopenapi_gen/core/postprocess_manager.py,sha256=cia8FbDXbulk44ElT1CTlypu1oFjNM41y1gWy8-HSug,9362
|
17
18
|
pyopenapi_gen/core/schemas.py,sha256=FOE2e1vIl0vif_C34AehVznJG2W1hampPtJEfL80AxI,5535
|
18
19
|
pyopenapi_gen/core/streaming_helpers.py,sha256=XToNnm-EDAqiKh9ZS4GRxyastFkfSyNR0av-NDTZMPg,2706
|
19
20
|
pyopenapi_gen/core/telemetry.py,sha256=l6z972882MRzNOXU2leAvtnlYFLMSKKQ_oHz4qU5_n0,2225
|
@@ -37,7 +38,7 @@ pyopenapi_gen/core/parsing/__init__.py,sha256=RJsIR6cHaNoI4tBcpMlAa0JsY64vsHb9sP
|
|
37
38
|
pyopenapi_gen/core/parsing/context.py,sha256=8cM8mPItvDvJr8ZiukvdHBumlQl9hK1gUZL4BDpHaBk,8005
|
38
39
|
pyopenapi_gen/core/parsing/cycle_helpers.py,sha256=nG5ysNavL_6lpnHWFUZR9qraBxqOzuNfI6NgSEa8a5M,5939
|
39
40
|
pyopenapi_gen/core/parsing/schema_finalizer.py,sha256=qRTHUoVBQTgGmdfLuBuWxtWdj_SG71STGC3rn-tJvnA,6914
|
40
|
-
pyopenapi_gen/core/parsing/schema_parser.py,sha256=
|
41
|
+
pyopenapi_gen/core/parsing/schema_parser.py,sha256=2hOj1Xz8a2f2PF3oAAG-DwQse9WHt9DzvR6nhQ5dHJU,33148
|
41
42
|
pyopenapi_gen/core/parsing/unified_cycle_detection.py,sha256=3nplaCVh2dFwBPbmDc2kiU0SzTPXXktdQ5Rc0Q9Uu9s,10873
|
42
43
|
pyopenapi_gen/core/parsing/common/__init__.py,sha256=U3sHMO-l6S3Cm04CVOYmBCpqLEZvCylUI7yQfcTwxYU,27
|
43
44
|
pyopenapi_gen/core/parsing/common/type_parser.py,sha256=cK7xtxhoD43K2WjLP9TGip3As3akYeYW7L2XztXCecg,2562
|
@@ -63,7 +64,7 @@ pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py,sha256=4njv5ra
|
|
63
64
|
pyopenapi_gen/core/writers/code_writer.py,sha256=uWH5tRFIdT3RHsRV1haWQxESwhwMoM2G_CxnKB8uP88,4776
|
64
65
|
pyopenapi_gen/core/writers/documentation_writer.py,sha256=Vce-_kD4XDm3HfZb_ibSEKAu2fbTZCzzdojn9TPgFhU,8706
|
65
66
|
pyopenapi_gen/core/writers/line_writer.py,sha256=uhysxO6bh_9POUQHhoqYI4_savfAgjH4EcwBdNrVtPc,7759
|
66
|
-
pyopenapi_gen/core/writers/python_construct_renderer.py,sha256=
|
67
|
+
pyopenapi_gen/core/writers/python_construct_renderer.py,sha256=lKj5nR-ULeceapoP2EQQOfmJif7eKAyTuhkCgGd3cI4,12812
|
67
68
|
pyopenapi_gen/core_package_template/README.md,sha256=8YP-MS0KxphRbCGBf7kV3dYIFLU9piOJ3IMm3K_0hcI,1488
|
68
69
|
pyopenapi_gen/emit/models_emitter.py,sha256=Ty5yHGzvBDYa_qQwbyPNRLWPnHaWR_KLh6pYxT7uePY,7193
|
69
70
|
pyopenapi_gen/emitters/CLAUDE.md,sha256=iZYEZq1a1h033rxuh97cMpsKUElv72ysvTm3-QQUvrs,9323
|
@@ -71,10 +72,10 @@ pyopenapi_gen/emitters/client_emitter.py,sha256=kmMVnG-wAOJm7TUm0xOQ5YnSJfYxz1Sw
|
|
71
72
|
pyopenapi_gen/emitters/core_emitter.py,sha256=RcBsAYQ3ZKcWwtkzQmyHkL7VtDQjbIObFLXD9M_GdpI,8020
|
72
73
|
pyopenapi_gen/emitters/docs_emitter.py,sha256=aouKqhRdtVvYfGVsye_uqM80nONRy0SqN06cr1l3OgA,1137
|
73
74
|
pyopenapi_gen/emitters/endpoints_emitter.py,sha256=tzSLUzlZle2Lih_aZc4cJ-Y1ItjN5H_rABEWcDwECXA,9586
|
74
|
-
pyopenapi_gen/emitters/exceptions_emitter.py,sha256=
|
75
|
+
pyopenapi_gen/emitters/exceptions_emitter.py,sha256=3k3UTskmZyv_fQXyOudBnBHUvhvQTenjY7qa-KK1F48,7553
|
75
76
|
pyopenapi_gen/emitters/models_emitter.py,sha256=Gd0z2Xoze1XkVnajkOptW90ti7197wQ15I7vIITnULM,22243
|
76
77
|
pyopenapi_gen/generator/CLAUDE.md,sha256=BS9KkmLvk2WD-Io-_apoWjGNeMU4q4LKy4UOxYF9WxM,10870
|
77
|
-
pyopenapi_gen/generator/client_generator.py,sha256=
|
78
|
+
pyopenapi_gen/generator/client_generator.py,sha256=_L_MJ9fUG8roD_DhymIM0nsdqOOjlQF_JAkBTND9ttA,29435
|
78
79
|
pyopenapi_gen/helpers/CLAUDE.md,sha256=GyIJ0grp4SkD3plAUzyycW4nTUZf9ewtvvsdAGkmIZw,10609
|
79
80
|
pyopenapi_gen/helpers/__init__.py,sha256=m4jSQ1sDH6CesIcqIl_kox4LcDFabGxBpSIWVwbHK0M,39
|
80
81
|
pyopenapi_gen/helpers/endpoint_utils.py,sha256=bkRu6YddIPQQD3rZLbB8L5WYzG-2Bd_JgMbxMUYY2wY,22198
|
@@ -97,7 +98,7 @@ pyopenapi_gen/types/contracts/types.py,sha256=-Qvbx3N_14AaN-1BeyocrvsjiwXPn_eWQh
|
|
97
98
|
pyopenapi_gen/types/resolvers/__init__.py,sha256=_5kA49RvyOTyXgt0GbbOfHJcdQw2zHxvU9af8GGyNWc,295
|
98
99
|
pyopenapi_gen/types/resolvers/reference_resolver.py,sha256=qnaZeLmtyh4_NBMcKib58s6o5ycUJaattYt8F38_qIo,2053
|
99
100
|
pyopenapi_gen/types/resolvers/response_resolver.py,sha256=Kb1a2803lyoukoZy06ztPBlUw-A1lHiZ6NlJmsixxA8,6500
|
100
|
-
pyopenapi_gen/types/resolvers/schema_resolver.py,sha256=
|
101
|
+
pyopenapi_gen/types/resolvers/schema_resolver.py,sha256=n8pI9kUJo-vLjZAOPSvwpVmRoM7AzTa7cOMlidYJVXo,21779
|
101
102
|
pyopenapi_gen/types/services/__init__.py,sha256=inSUKmY_Vnuym6tC-AhvjCTj16GbkfxCGLESRr_uQPE,123
|
102
103
|
pyopenapi_gen/types/services/type_service.py,sha256=-LQj7oSx1mxb10Zi6DpawS8uyoUrUbnYhmUA0GuKZTc,4402
|
103
104
|
pyopenapi_gen/types/strategies/__init__.py,sha256=bju8_KEPNIow1-woMO-zJCgK_E0M6JnFq0NFsK1R4Ss,171
|
@@ -105,7 +106,7 @@ pyopenapi_gen/types/strategies/response_strategy.py,sha256=Y6E3O5xvCrJ2Y6IGn4BWl
|
|
105
106
|
pyopenapi_gen/visit/CLAUDE.md,sha256=Rq2e4S74TXv0ua2ZcCrO6cwCCccf3Yph44oVdj1yFPY,8297
|
106
107
|
pyopenapi_gen/visit/client_visitor.py,sha256=vpLCGF353XtBjfS7W69-1b1d79opTb1i6qBue6vSz5g,11152
|
107
108
|
pyopenapi_gen/visit/docs_visitor.py,sha256=hqgd4DAoy7T5Bap4mpH4R-nIZSyAWwFYmrIuNHM03Rg,1644
|
108
|
-
pyopenapi_gen/visit/exception_visitor.py,sha256=
|
109
|
+
pyopenapi_gen/visit/exception_visitor.py,sha256=D4LtLqdeS34kw6WbwhoWeMQzlh9uHqGNZjFtY0kq3Q4,3855
|
109
110
|
pyopenapi_gen/visit/visitor.py,sha256=PANoV9zXMUrefr0pBLXIKkDOaTIjQ2hyL82cidVBCLU,3645
|
110
111
|
pyopenapi_gen/visit/endpoint/__init__.py,sha256=DftIZSWp6Z8jKWoJE2VGKL4G_5cqwFXe9v-PALMmsGk,73
|
111
112
|
pyopenapi_gen/visit/endpoint/endpoint_visitor.py,sha256=1aS0i2D_Y-979_7aBd0W6IS2UVO6wMujsMMw8Qt8PRE,3574
|
@@ -113,7 +114,7 @@ pyopenapi_gen/visit/endpoint/generators/__init__.py,sha256=-X-GYnJZ9twiEBr_U0obW
|
|
113
114
|
pyopenapi_gen/visit/endpoint/generators/docstring_generator.py,sha256=U02qvuYtFElQNEtOHuTNXFl2NxUriIiuZMkmUsapOg4,5913
|
114
115
|
pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py,sha256=wUJ4_gaA1gRrFCHYFCObBIankxGQu0MNqiOSoZOZmoA,4352
|
115
116
|
pyopenapi_gen/visit/endpoint/generators/request_generator.py,sha256=OnkrkRk39_BrK9ZDvyWqJYLz1mocD2zY7j70yIpS0J4,5374
|
116
|
-
pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py,sha256=
|
117
|
+
pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py,sha256=OAD3_CDXiA0lXv7KvUjmp6QBBYxs569vmP-6K_SuGJc,22961
|
117
118
|
pyopenapi_gen/visit/endpoint/generators/signature_generator.py,sha256=CYtfsPMlTZN95g2WxrdnTloGx2RmqeNQRiyP9fOkUEQ,3892
|
118
119
|
pyopenapi_gen/visit/endpoint/generators/url_args_generator.py,sha256=EsmNuVSkGfUqrmV7-1GiLPzdN86V5UqLfs1SVY0jsf0,9590
|
119
120
|
pyopenapi_gen/visit/endpoint/processors/__init__.py,sha256=_6RqpOdDuDheArqDBi3ykhsaetACny88WUuuAJvr_ME,29
|
@@ -124,8 +125,8 @@ pyopenapi_gen/visit/model/alias_generator.py,sha256=TGL3AMq_PkBWFWeeXbNnA8hgO9hv
|
|
124
125
|
pyopenapi_gen/visit/model/dataclass_generator.py,sha256=nyTvBph6rtbJlCwTiDW_Y2UJmLLiA6D2QJUpA2xE0m8,10289
|
125
126
|
pyopenapi_gen/visit/model/enum_generator.py,sha256=AXqKUFuWUUjUF_6_HqBKY8vB5GYu35Pb2C2WPFrOw1k,10061
|
126
127
|
pyopenapi_gen/visit/model/model_visitor.py,sha256=4kAQSWsI4XumVYB3aAE7Ts_31hGfDlbytRalxyMFV3g,9510
|
127
|
-
pyopenapi_gen-0.
|
128
|
-
pyopenapi_gen-0.
|
129
|
-
pyopenapi_gen-0.
|
130
|
-
pyopenapi_gen-0.
|
131
|
-
pyopenapi_gen-0.
|
128
|
+
pyopenapi_gen-0.14.0.dist-info/METADATA,sha256=VQEURt-RzzvWvLDj0mDetS6zNnRNGx41B1pYO2wKJ0o,14025
|
129
|
+
pyopenapi_gen-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
130
|
+
pyopenapi_gen-0.14.0.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
|
131
|
+
pyopenapi_gen-0.14.0.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
|
132
|
+
pyopenapi_gen-0.14.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|