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.
@@ -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
- extracted_type = raw_type_field
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
- current_final_type = None
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
- items_ir = IRSchema(type="Any")
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
- # --- RE-ENABLE RUFF CHECKS ---
36
- for target_path in target_paths:
37
- if target_path.is_file() and target_path.suffix == ".py":
38
- self.remove_unused_imports(target_path)
39
- self.sort_imports(target_path)
40
- self.format_code(target_path)
41
- # --- END RE-ENABLE ---
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
- if package_roots:
62
- print(f"Running Mypy on package root(s): {package_roots}")
63
- for root_dir in package_roots:
64
- print(f"Running mypy on {root_dir}...")
65
- self.type_check(root_dir)
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
- writer.write_line(line)
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
- # Generated exception aliases for specific status codes
14
- {% for code in codes %}
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
- class ExceptionsEmitter:
23
- """Generates spec-specific exception aliases in exceptions.py using visitor/context."""
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(self, spec: IRSpec, output_dir: str) -> tuple[list[str], list[str]]:
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
- generated_code, alias_names = self.visitor.visit(spec, context)
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(ir, str(core_dir))
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
- if hasattr(schema, "any_of") and schema.any_of:
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
- logger.warning(f"Unknown schema type: {schema_type}")
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
- logger.warning("Array schema missing items")
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 to first schema
344
- if schema.all_of:
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 = f"Error{status_code_val}"
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
- # Register base exception imports
15
- context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
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
- codes = sorted(
22
- {int(resp.status_code) for op in spec.operations for resp in op.responses if resp.status_code.isdigit()}
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 codes:
30
- base_class = "ClientError" if code < 500 else "ServerError"
31
- class_name = f"Error{code}"
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
- docstring = f"Exception alias for HTTP {code} responses."
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.12.1
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=ky27ijbq6zEo43aqe-odz9CR3vFD_3XHhQR35XgMZo0,6879
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=W0kCV0ucq6Wybr7x1wZl-nMSRXVWAqI4T57ASSabZsM,30557
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=H2-dbUoc0wgI5Ll2OiRIRkfaDVBG_rFB4j44NXvF7Fk,12602
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=qPTIPXDyqSUtpmBIp-V4ap1uMHUPmYziCSN62t7qcAE,1918
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=MULKJY9SdRuYjt_R4XCXh3vJSW-92rsxOu-MVpIklho,29333
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=PsSF-DE8-GDXmvbAJz-tHlTCR-1UATGhT4dHf6kUDQQ,17988
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=6gWkjEWMYM35_rBNCgB-xdE0RvYwbj49wytAorLHK9k,2097
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=xbh4GQcA-fA98mOZxVSylLbZADse-7RLcLIbjnVAmlE,22834
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.12.1.dist-info/METADATA,sha256=JuNfuruc2Nc7du_GdSrJh9MCXDvEcjP8bTNk5QyzuaU,14025
128
- pyopenapi_gen-0.12.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
- pyopenapi_gen-0.12.1.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
130
- pyopenapi_gen-0.12.1.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
131
- pyopenapi_gen-0.12.1.dist-info/RECORD,,
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,,