pyopenapi-gen 0.13.0__py3-none-any.whl → 0.14.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. pyopenapi_gen/cli.py +3 -3
  2. pyopenapi_gen/context/import_collector.py +10 -10
  3. pyopenapi_gen/context/render_context.py +13 -13
  4. pyopenapi_gen/core/auth/plugins.py +7 -7
  5. pyopenapi_gen/core/http_status_codes.py +218 -0
  6. pyopenapi_gen/core/http_transport.py +19 -19
  7. pyopenapi_gen/core/loader/operations/parser.py +2 -2
  8. pyopenapi_gen/core/loader/operations/request_body.py +3 -3
  9. pyopenapi_gen/core/loader/parameters/parser.py +3 -3
  10. pyopenapi_gen/core/loader/responses/parser.py +2 -2
  11. pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
  12. pyopenapi_gen/core/pagination.py +3 -3
  13. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
  14. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
  15. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
  16. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
  17. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
  18. pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
  19. pyopenapi_gen/core/parsing/context.py +10 -10
  20. pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
  21. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
  22. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
  23. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
  24. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
  25. pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
  26. pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
  27. pyopenapi_gen/core/parsing/schema_parser.py +44 -25
  28. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
  29. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
  30. pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
  31. pyopenapi_gen/core/postprocess_manager.py +85 -12
  32. pyopenapi_gen/core/schemas.py +10 -10
  33. pyopenapi_gen/core/streaming_helpers.py +5 -7
  34. pyopenapi_gen/core/telemetry.py +4 -4
  35. pyopenapi_gen/core/utils.py +7 -7
  36. pyopenapi_gen/core/writers/code_writer.py +2 -2
  37. pyopenapi_gen/core/writers/documentation_writer.py +18 -18
  38. pyopenapi_gen/core/writers/line_writer.py +3 -3
  39. pyopenapi_gen/core/writers/python_construct_renderer.py +15 -11
  40. pyopenapi_gen/emit/models_emitter.py +2 -2
  41. pyopenapi_gen/emitters/core_emitter.py +3 -5
  42. pyopenapi_gen/emitters/endpoints_emitter.py +12 -12
  43. pyopenapi_gen/emitters/exceptions_emitter.py +153 -18
  44. pyopenapi_gen/emitters/models_emitter.py +6 -6
  45. pyopenapi_gen/generator/client_generator.py +10 -8
  46. pyopenapi_gen/helpers/endpoint_utils.py +16 -18
  47. pyopenapi_gen/helpers/type_cleaner.py +66 -53
  48. pyopenapi_gen/helpers/type_helper.py +7 -7
  49. pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
  50. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
  51. pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
  52. pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
  53. pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
  54. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
  55. pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
  56. pyopenapi_gen/ir.py +32 -34
  57. pyopenapi_gen/types/contracts/protocols.py +5 -5
  58. pyopenapi_gen/types/contracts/types.py +2 -3
  59. pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
  60. pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
  61. pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
  62. pyopenapi_gen/types/services/type_service.py +55 -9
  63. pyopenapi_gen/types/strategies/response_strategy.py +6 -7
  64. pyopenapi_gen/visit/client_visitor.py +5 -7
  65. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
  66. pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
  67. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +41 -19
  68. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
  69. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
  70. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
  71. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
  72. pyopenapi_gen/visit/exception_visitor.py +54 -16
  73. pyopenapi_gen/visit/model/alias_generator.py +1 -4
  74. pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
  75. pyopenapi_gen/visit/model/model_visitor.py +2 -3
  76. pyopenapi_gen/visit/visitor.py +3 -3
  77. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/METADATA +1 -1
  78. pyopenapi_gen-0.14.1.dist-info/RECORD +132 -0
  79. pyopenapi_gen-0.13.0.dist-info/RECORD +0 -131
  80. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/WHEEL +0 -0
  81. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
  82. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Dict, List, Optional, Tuple
3
+ from typing import List, Tuple
4
4
 
5
5
  from pyopenapi_gen import IROperation, IRParameter, IRRequestBody
6
6
  from pyopenapi_gen.context.render_context import RenderContext
@@ -17,7 +17,7 @@ PARAM_TYPE_MAPPING = {
17
17
  "boolean": "bool",
18
18
  "string": "str",
19
19
  "array": "List",
20
- "object": "Dict[str, Any]",
20
+ "object": "dict[str, Any]",
21
21
  }
22
22
  # Format-specific overrides
23
23
  PARAM_FORMAT_MAPPING = {
@@ -47,7 +47,7 @@ def schema_to_type(schema: IRParameter) -> str:
47
47
  # Array handling
48
48
  elif s.type == "array" and s.items:
49
49
  # For array items, we recursively call schema_to_type.
50
- # The nullability of the item_type itself (e.g. List[Optional[int]])
50
+ # The nullability of the item_type itself (e.g. List[int | None])
51
51
  # will be handled by the recursive call based on s.items.is_nullable.
52
52
  item_schema_as_param = IRParameter(name="_item", param_in="_internal", required=False, schema=s.items)
53
53
  item_type_str = schema_to_type(item_schema_as_param)
@@ -70,8 +70,8 @@ def schema_to_type(schema: IRParameter) -> str:
70
70
  # 2. Apply nullability based on IRSchema's is_nullable field
71
71
  # This s.is_nullable should be the source of truth from the IR after parsing.
72
72
  if s.is_nullable:
73
- # Ensure "Any" also gets wrapped, e.g. Optional[Any]
74
- py_type = f"Optional[{py_type}]"
73
+ # Ensure "Any" also gets wrapped, e.g. Any | None
74
+ py_type = f"{py_type} | None"
75
75
 
76
76
  return py_type
77
77
 
@@ -82,7 +82,7 @@ def _get_request_body_type(body: IRRequestBody) -> str:
82
82
  if "json" in mt.lower():
83
83
  return schema_to_type(IRParameter(name="body", param_in="body", required=body.required, schema=sch))
84
84
  # Fallback to generic dict
85
- return "Dict[str, Any]"
85
+ return "dict[str, Any]"
86
86
 
87
87
 
88
88
  def _deduplicate_tag_clients(client_classes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
@@ -106,7 +106,7 @@ class EndpointsEmitter:
106
106
  def __init__(self, context: RenderContext) -> None:
107
107
  self.context = context
108
108
  self.formatter = Formatter()
109
- self.visitor: Optional[EndpointVisitor] = None
109
+ self.visitor: EndpointVisitor | None = None
110
110
 
111
111
  def _deduplicate_operation_ids(self, operations: List[IROperation]) -> None:
112
112
  """
@@ -115,7 +115,7 @@ class EndpointsEmitter:
115
115
  Args:
116
116
  operations: List of operations for a single tag.
117
117
  """
118
- seen_methods: Dict[str, int] = {}
118
+ seen_methods: dict[str, int] = {}
119
119
  for op in operations:
120
120
  method_name = NameSanitizer.sanitize_method_name(op.operation_id)
121
121
  if method_name in seen_methods:
@@ -144,7 +144,7 @@ class EndpointsEmitter:
144
144
  self.context.file_manager.write_file(str(file_path), content)
145
145
 
146
146
  # Ensure parsed_schemas is at least an empty dict if None,
147
- # as EndpointVisitor expects Dict[str, IRSchema]
147
+ # as EndpointVisitor expects dict[str, IRSchema]
148
148
  current_parsed_schemas = self.context.parsed_schemas
149
149
  if current_parsed_schemas is None:
150
150
  logger.warning(
@@ -156,8 +156,8 @@ class EndpointsEmitter:
156
156
  if self.visitor is None:
157
157
  self.visitor = EndpointVisitor(current_parsed_schemas) # Pass the (potentially defaulted) dict
158
158
 
159
- tag_key_to_ops: Dict[str, List[IROperation]] = {}
160
- tag_key_to_candidates: Dict[str, List[str]] = {}
159
+ tag_key_to_ops: dict[str, List[IROperation]] = {}
160
+ tag_key_to_candidates: dict[str, List[str]] = {}
161
161
  for op in operations:
162
162
  tags = op.tags or [DEFAULT_TAG]
163
163
  for tag in tags:
@@ -175,7 +175,7 @@ class EndpointsEmitter:
175
175
  upper = sum(1 for c in t if c.isupper())
176
176
  return (is_pascal, word_count, upper, t)
177
177
 
178
- tag_map: Dict[str, str] = {}
178
+ tag_map: dict[str, str] = {}
179
179
  for key, candidates in tag_key_to_candidates.items():
180
180
  best_tag_for_key = DEFAULT_TAG # Default if no candidates somehow
181
181
  if candidates:
@@ -1,34 +1,46 @@
1
+ import json
1
2
  import os
2
- from typing import Optional
3
+ from pathlib import Path
3
4
 
4
5
  from pyopenapi_gen import IRSpec
5
6
  from pyopenapi_gen.context.render_context import RenderContext
6
7
 
7
8
  from ..visit.exception_visitor import ExceptionVisitor
8
9
 
9
- # Template for spec-specific exception aliases
10
- EXCEPTIONS_ALIASES_TEMPLATE = '''
11
- from .exceptions import HTTPError, ClientError, ServerError
12
10
 
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
- '''
11
+ class ExceptionsEmitter:
12
+ """Generates spec-specific exception aliases with multi-client support.
20
13
 
14
+ This emitter handles two scenarios:
15
+ 1. **Single client**: Generates exception_aliases.py directly in the core package
16
+ 2. **Shared core**: Maintains a registry of all needed exception codes across clients
17
+ and regenerates the complete exception_aliases.py file
21
18
 
22
- class ExceptionsEmitter:
23
- """Generates spec-specific exception aliases in exceptions.py using visitor/context."""
19
+ The registry file (.exception_registry.json) tracks which status codes are used by
20
+ which clients, ensuring that when multiple clients share a core package, all required
21
+ exceptions are available.
22
+ """
24
23
 
25
- def __init__(self, core_package_name: str = "core", overall_project_root: Optional[str] = None) -> None:
24
+ def __init__(self, core_package_name: str = "core", overall_project_root: str | None = None) -> None:
26
25
  self.visitor = ExceptionVisitor()
27
26
  self.core_package_name = core_package_name
28
27
  self.overall_project_root = overall_project_root
29
28
 
30
- def emit(self, spec: IRSpec, output_dir: str) -> tuple[list[str], list[str]]:
29
+ def emit(
30
+ self, spec: IRSpec, output_dir: str, client_package_name: str | None = None
31
+ ) -> tuple[list[str], list[str]]:
32
+ """Generate exception aliases for the given spec.
33
+
34
+ Args:
35
+ spec: IRSpec containing operations and responses
36
+ output_dir: Directory where exception_aliases.py will be written
37
+ client_package_name: Name of the client package (for registry tracking)
38
+
39
+ Returns:
40
+ Tuple of (list of generated file paths, list of exception class names)
41
+ """
31
42
  file_path = os.path.join(output_dir, "exception_aliases.py")
43
+ registry_path = os.path.join(output_dir, ".exception_registry.json")
32
44
 
33
45
  context = RenderContext(
34
46
  package_root_for_generated_code=output_dir,
@@ -37,16 +49,139 @@ class ExceptionsEmitter:
37
49
  )
38
50
  context.set_current_file(file_path)
39
51
 
40
- generated_code, alias_names = self.visitor.visit(spec, context)
52
+ # Generate exception classes for this spec
53
+ generated_code, alias_names, status_codes = self.visitor.visit(spec, context)
54
+
55
+ # Update registry if we have a client package name (shared core scenario)
56
+ if client_package_name and self._is_shared_core(output_dir):
57
+ all_codes = self._update_registry(registry_path, client_package_name, status_codes)
58
+ # Regenerate with ALL codes from registry
59
+ generated_code, alias_names = self._generate_for_codes(all_codes, context)
60
+
41
61
  generated_imports = context.render_imports()
42
62
 
43
- # Add __all__ list
63
+ alias_names.sort()
64
+
65
+ # Add __all__ list with proper spacing (2 blank lines after last class - Ruff E305)
44
66
  if alias_names:
45
67
  all_list_str = ", ".join([f'"{name}"' for name in alias_names])
46
- all_assignment = f"\n\n__all__ = [{all_list_str}]\n"
68
+ all_assignment = f"\n\n\n__all__ = [{all_list_str}]\n"
47
69
  generated_code += all_assignment
48
70
 
49
71
  full_content = f"{generated_imports}\n\n{generated_code}"
50
72
  with open(file_path, "w") as f:
51
73
  f.write(full_content)
74
+
52
75
  return [file_path], alias_names
76
+
77
+ def _is_shared_core(self, core_dir: str) -> bool:
78
+ """Check if this core package is shared between multiple clients.
79
+
80
+ Args:
81
+ core_dir: Path to the core package directory
82
+
83
+ Returns:
84
+ True if the core package is outside the immediate client package
85
+ """
86
+ # If overall_project_root is set and different from the core dir's parent,
87
+ # we're in a shared core scenario
88
+ if self.overall_project_root:
89
+ core_path = Path(core_dir).resolve()
90
+ project_root = Path(self.overall_project_root).resolve()
91
+ # Check if there are other client directories at the same level
92
+ parent_dir = core_path.parent
93
+ return parent_dir == project_root or parent_dir.parent == project_root
94
+ return False
95
+
96
+ def _update_registry(self, registry_path: str, client_name: str, status_codes: list[int]) -> list[int]:
97
+ """Update the exception registry with this client's status codes.
98
+
99
+ Args:
100
+ registry_path: Path to the .exception_registry.json file
101
+ client_name: Name of the client package
102
+ status_codes: List of status codes used by this client
103
+
104
+ Returns:
105
+ Complete list of all status codes across all clients
106
+ """
107
+ registry = {}
108
+ if os.path.exists(registry_path):
109
+ with open(registry_path) as f:
110
+ registry = json.load(f)
111
+
112
+ # Update this client's codes
113
+ registry[client_name] = sorted(status_codes)
114
+
115
+ # Write back to registry
116
+ with open(registry_path, "w") as f:
117
+ json.dump(registry, f, indent=2, sort_keys=True)
118
+
119
+ # Return union of all codes
120
+ all_codes = set()
121
+ for codes in registry.values():
122
+ all_codes.update(codes)
123
+
124
+ return sorted(all_codes)
125
+
126
+ def _generate_for_codes(self, status_codes: list[int], context: RenderContext) -> tuple[str, list[str]]:
127
+ """Generate exception classes for a specific list of status codes.
128
+
129
+ Args:
130
+ status_codes: List of HTTP status codes to generate exceptions for
131
+ context: Render context for imports
132
+
133
+ Returns:
134
+ Tuple of (generated_code, exception_class_names)
135
+ """
136
+ from ..core.http_status_codes import (
137
+ get_exception_class_name,
138
+ get_status_name,
139
+ is_client_error,
140
+ is_server_error,
141
+ )
142
+ from ..core.writers.python_construct_renderer import PythonConstructRenderer
143
+
144
+ renderer = PythonConstructRenderer()
145
+ all_exception_code = []
146
+ generated_alias_names = []
147
+
148
+ for code in status_codes:
149
+ # Determine base class
150
+ if is_client_error(code):
151
+ base_class = "ClientError"
152
+ elif is_server_error(code):
153
+ base_class = "ServerError"
154
+ else:
155
+ continue
156
+
157
+ # Get human-readable exception class name (e.g., NotFoundError instead of Error404)
158
+ class_name = get_exception_class_name(code)
159
+ generated_alias_names.append(class_name)
160
+
161
+ # Get human-readable status name for documentation
162
+ status_name = get_status_name(code)
163
+ docstring = f"HTTP {code} {status_name}.\n\nRaised when the server responds with a {code} status code."
164
+
165
+ # Define the __init__ method body
166
+ init_method_body = [
167
+ "def __init__(self, response: Response) -> None:",
168
+ f' """Initialise {class_name} with the HTTP response.',
169
+ "", # Empty line without trailing whitespace (Ruff W293)
170
+ " Args:",
171
+ " response: The httpx Response object that triggered this exception",
172
+ ' """',
173
+ " super().__init__(status_code=response.status_code, message=response.text, response=response)",
174
+ ]
175
+
176
+ exception_code = renderer.render_class(
177
+ class_name=class_name,
178
+ base_classes=[base_class],
179
+ docstring=docstring,
180
+ body_lines=init_method_body,
181
+ context=context,
182
+ )
183
+ all_exception_code.append(exception_code)
184
+
185
+ # Join the generated class strings with 2 blank lines between classes (PEP 8 / Ruff E302)
186
+ final_code = "\n\n\n".join(all_exception_code)
187
+ return final_code, generated_alias_names
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Dict, List, Optional, Set
3
+ from typing import List, Set
4
4
 
5
5
  from pyopenapi_gen import IRSchema, IRSpec
6
6
  from pyopenapi_gen.context.render_context import RenderContext
@@ -22,15 +22,15 @@ class ModelsEmitter:
22
22
  Handles creation of __init__.py and py.typed files.
23
23
  """
24
24
 
25
- def __init__(self, context: RenderContext, parsed_schemas: Dict[str, IRSchema]):
25
+ def __init__(self, context: RenderContext, parsed_schemas: dict[str, IRSchema]):
26
26
  self.context: RenderContext = context
27
27
  # Store a reference to the schemas that were passed in.
28
28
  # These schemas will have their .generation_name and .final_module_stem updated.
29
- self.parsed_schemas: Dict[str, IRSchema] = parsed_schemas
29
+ self.parsed_schemas: dict[str, IRSchema] = parsed_schemas
30
30
  self.import_collector = self.context.import_collector
31
31
  self.writer = CodeWriter()
32
32
 
33
- def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> Optional[str]:
33
+ def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
34
34
  """Generates a single Python file for a given IRSchema."""
35
35
  if not schema_ir.name: # Original name, used for logging/initial identification
36
36
  logger.warning(f"Skipping model generation for schema without an original name: {schema_ir}")
@@ -168,7 +168,7 @@ class ModelsEmitter:
168
168
  generated_content = init_writer.get_code()
169
169
  return generated_content
170
170
 
171
- def emit(self, spec: IRSpec, output_root: str) -> Dict[str, List[str]]:
171
+ def emit(self, spec: IRSpec, output_root: str) -> dict[str, List[str]]:
172
172
  """Emits all model files derived from IR schemas.
173
173
 
174
174
  Contracts:
@@ -352,7 +352,7 @@ class ModelsEmitter:
352
352
 
353
353
  # Fetch the schema_ir object using the key from all_schemas_for_generation
354
354
  # This ensures we are working with the potentially newly created & named schemas.
355
- current_schema_ir_obj: Optional[IRSchema] = all_schemas_for_generation.get(schema_key)
355
+ current_schema_ir_obj: IRSchema | None = all_schemas_for_generation.get(schema_key)
356
356
 
357
357
  if not current_schema_ir_obj:
358
358
  logger.warning(f"Schema key '{schema_key}' from all_schemas_for_generation not found. Skipping.")
@@ -9,7 +9,7 @@ import tempfile
9
9
  import time
10
10
  from datetime import datetime
11
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Optional
12
+ from typing import Any, List
13
13
 
14
14
  from pyopenapi_gen.context.render_context import RenderContext
15
15
  from pyopenapi_gen.core.loader.loader import load_ir_from_spec
@@ -47,9 +47,9 @@ class ClientGenerator:
47
47
  """
48
48
  self.verbose = verbose
49
49
  self.start_time = time.time()
50
- self.timings: Dict[str, float] = {}
50
+ self.timings: dict[str, float] = {}
51
51
 
52
- def _log_progress(self, message: str, stage: Optional[str] = None) -> None:
52
+ def _log_progress(self, message: str, stage: str | None = None) -> None:
53
53
  """
54
54
  Log a progress message with timestamp.
55
55
 
@@ -89,7 +89,7 @@ class ClientGenerator:
89
89
  output_package: str,
90
90
  force: bool = False,
91
91
  no_postprocess: bool = False,
92
- core_package: Optional[str] = None,
92
+ core_package: str | None = None,
93
93
  ) -> List[Path]:
94
94
  """
95
95
  Generate the client code from the OpenAPI spec.
@@ -99,10 +99,10 @@ class ClientGenerator:
99
99
  project_root (Path): Path to the root of the Python project (absolute or relative).
100
100
  output_package (str): Python package path for the generated client (e.g., 'pyapis.my_api_client').
101
101
  force (bool): Overwrite output without diff check.
102
- name (Optional[str]): Custom client package name (not used).
102
+ name (str | None): Custom client package name (not used).
103
103
  docs (bool): Kept for interface compatibility.
104
104
  telemetry (bool): Kept for interface compatibility.
105
- auth (Optional[str]): Kept for interface compatibility.
105
+ auth (str | None): Kept for interface compatibility.
106
106
  no_postprocess (bool): Skip post-processing (type checking, etc.).
107
107
  core_package (str): Python package path for the core package.
108
108
 
@@ -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
 
@@ -5,7 +5,7 @@ Used by EndpointVisitor and related emitters.
5
5
 
6
6
  import logging
7
7
  import re
8
- from typing import Any, Dict, List, Optional
8
+ from typing import Any, List
9
9
 
10
10
  from pyopenapi_gen import IROperation, IRParameter, IRRequestBody, IRResponse, IRSchema
11
11
  from pyopenapi_gen.context.render_context import RenderContext
@@ -17,7 +17,7 @@ from ..types.services.type_service import UnifiedTypeService
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
- def get_params(op: IROperation, context: RenderContext, schemas: Dict[str, IRSchema]) -> List[Dict[str, Any]]:
20
+ def get_params(op: IROperation, context: RenderContext, schemas: dict[str, IRSchema]) -> List[dict[str, Any]]:
21
21
  """
22
22
  Returns a list of dicts with name, type, default, and required for template rendering.
23
23
  Requires the full schema dictionary for type resolution.
@@ -37,7 +37,7 @@ def get_params(op: IROperation, context: RenderContext, schemas: Dict[str, IRSch
37
37
  return params
38
38
 
39
39
 
40
- def get_param_type(param: IRParameter, context: RenderContext, schemas: Dict[str, IRSchema]) -> str:
40
+ def get_param_type(param: IRParameter, context: RenderContext, schemas: dict[str, IRSchema]) -> str:
41
41
  """Returns the Python type hint for a parameter, resolving references using the schemas dict."""
42
42
  # Use unified service for type resolution
43
43
  type_service = UnifiedTypeService(schemas)
@@ -59,7 +59,7 @@ def get_param_type(param: IRParameter, context: RenderContext, schemas: Dict[str
59
59
  return py_type
60
60
 
61
61
 
62
- def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas: Dict[str, IRSchema]) -> str:
62
+ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas: dict[str, IRSchema]) -> str:
63
63
  """Returns the Python type hint for a request body, resolving references using the schemas dict."""
64
64
  # Prefer application/json schema if available
65
65
  json_schema = body.content.get("application/json")
@@ -70,11 +70,11 @@ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
70
70
  if py_type.startswith(".") and not py_type.startswith(".."):
71
71
  py_type = "models" + py_type
72
72
 
73
- # If the resolved type is 'Any' for a JSON body, default to Dict[str, Any]
73
+ # If the resolved type is 'Any' for a JSON body, default to dict[str, Any]
74
74
  if py_type == "Any":
75
75
  context.add_import("typing", "Dict")
76
76
  # context.add_import("typing", "Any") # Already added by the fallback or TypeHelper
77
- return "Dict[str, Any]"
77
+ return "dict[str, Any]"
78
78
  return py_type
79
79
  # Fallback for other content types (e.g., octet-stream)
80
80
  # TODO: Handle other types more specifically if needed
@@ -89,7 +89,7 @@ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
89
89
  def get_return_type(
90
90
  op: IROperation,
91
91
  context: RenderContext,
92
- schemas: Dict[str, IRSchema],
92
+ schemas: dict[str, IRSchema],
93
93
  ) -> tuple[str | None, bool]:
94
94
  """
95
95
  DEPRECATED: Use get_return_type_unified instead.
@@ -136,7 +136,7 @@ def get_return_type(
136
136
  return (py_type, False)
137
137
 
138
138
 
139
- def _get_primary_response(op: IROperation) -> Optional[IRResponse]:
139
+ def _get_primary_response(op: IROperation) -> IRResponse | None:
140
140
  """Helper to find the best primary success response."""
141
141
  resp = None
142
142
  # Prioritize 200, 201, 202, 204
@@ -158,7 +158,7 @@ def _get_primary_response(op: IROperation) -> Optional[IRResponse]:
158
158
  return None
159
159
 
160
160
 
161
- def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[Optional[IRSchema], Optional[str]]:
161
+ def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[IRSchema | None, str | None]:
162
162
  """Helper to get the schema and content type from a response."""
163
163
  if not resp.content:
164
164
  return None, None
@@ -179,7 +179,7 @@ def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[Optional[IR
179
179
  return resp.content.get(mt), mt
180
180
 
181
181
 
182
- def _find_resource_schema(update_schema_name: str, schemas: Dict[str, IRSchema]) -> Optional[IRSchema]:
182
+ def _find_resource_schema(update_schema_name: str, schemas: dict[str, IRSchema]) -> IRSchema | None:
183
183
  """
184
184
  Given an update schema name (e.g. 'TenantUpdate'), try to find the corresponding
185
185
  resource schema (e.g. 'Tenant') in the schemas dictionary.
@@ -205,7 +205,7 @@ def _find_resource_schema(update_schema_name: str, schemas: Dict[str, IRSchema])
205
205
  return None
206
206
 
207
207
 
208
- def _infer_type_from_path(path: str, schemas: Dict[str, IRSchema]) -> Optional[IRSchema]:
208
+ def _infer_type_from_path(path: str, schemas: dict[str, IRSchema]) -> IRSchema | None:
209
209
  """
210
210
  Infers a response type from a path. This is used when a response schema is not specified.
211
211
 
@@ -350,8 +350,8 @@ def merge_params_with_model_fields(
350
350
  op: IROperation,
351
351
  model_schema: IRSchema,
352
352
  context: RenderContext,
353
- schemas: Dict[str, IRSchema],
354
- ) -> List[Dict[str, Any]]:
353
+ schemas: dict[str, IRSchema],
354
+ ) -> List[dict[str, Any]]:
355
355
  """
356
356
  Merge endpoint parameters with required model fields for function signatures.
357
357
  - Ensures all required model fields are present as parameters (without duplication).
@@ -484,7 +484,7 @@ def get_type_for_specific_response(
484
484
  ctx.add_import("typing", "AsyncIterator")
485
485
  ctx.add_import("typing", "Dict")
486
486
  ctx.add_import("typing", "Any")
487
- return "AsyncIterator[Dict[str, Any]]"
487
+ return "AsyncIterator[dict[str, Any]]"
488
488
 
489
489
  return final_py_type
490
490
 
@@ -504,9 +504,7 @@ def _is_binary_stream_content(resp_ir: IRResponse) -> bool:
504
504
  )
505
505
 
506
506
 
507
- def _get_item_type_from_schema(
508
- resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext
509
- ) -> Optional[str]:
507
+ def _get_item_type_from_schema(resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext) -> str | None:
510
508
  """Extract item type from schema for streaming responses."""
511
509
  schema, _ = _get_response_schema_and_content_type(resp_ir)
512
510
  if not schema:
@@ -528,7 +526,7 @@ def get_python_type_for_response_body(resp_ir: IRResponse, all_schemas: dict[str
528
526
  return type_service.resolve_schema_type(schema, ctx, required=True)
529
527
 
530
528
 
531
- def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) -> Optional[IRSchema]:
529
+ def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) -> IRSchema | None:
532
530
  """Get the schema from a response object."""
533
531
  schema, _ = _get_response_schema_and_content_type(resp_ir)
534
532
  return schema