pyopenapi-gen 0.8.3__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 (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,174 @@
1
+ """Resolves IRSchema to Python named types (classes, enums)."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Dict, Optional
6
+
7
+ from pyopenapi_gen import IRSchema
8
+ from pyopenapi_gen.context.render_context import RenderContext
9
+ from pyopenapi_gen.core.utils import NameSanitizer
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class NamedTypeResolver:
15
+ """Resolves IRSchema instances that refer to named models/enums."""
16
+
17
+ def __init__(self, context: RenderContext, all_schemas: Dict[str, IRSchema]):
18
+ self.context = context
19
+ self.all_schemas = all_schemas
20
+
21
+ def _is_self_reference(self, target_module_name: str, target_class_name: str) -> bool:
22
+ """Check if the target class is the same as the one currently being generated."""
23
+ if not self.context.current_file:
24
+ return False
25
+
26
+ # Extract current module name from the file path
27
+ current_file_name = self.context.current_file
28
+ current_module_name = os.path.splitext(os.path.basename(current_file_name))[0]
29
+
30
+ # For self-reference detection, we need to check if:
31
+ # 1. The target module name matches the current module name
32
+ # 2. The target class name is likely the class being defined in this file
33
+ #
34
+ # The target_class_name should match the class being generated in the current file
35
+ # For example, if we're in tree_node.py generating TreeNode class, and we're trying
36
+ # to reference TreeNode, then this is a self-reference
37
+ return current_module_name == target_module_name and self._class_being_generated_matches(target_class_name)
38
+
39
+ def _class_being_generated_matches(self, target_class_name: str) -> bool:
40
+ """Check if the target class name matches what's being generated in the current file."""
41
+ # This is a simple heuristic: if the file is tree_node.py, we expect TreeNode class
42
+ # More sophisticated logic could be added by tracking what class is currently being generated
43
+ if not self.context.current_file:
44
+ return False
45
+
46
+ current_file_name = self.context.current_file
47
+ current_module_name = os.path.splitext(os.path.basename(current_file_name))[0]
48
+
49
+ # Convert module name to expected class name (snake_case to PascalCase)
50
+ expected_class_name = self._module_name_to_class_name(current_module_name)
51
+
52
+ return target_class_name == expected_class_name
53
+
54
+ def _module_name_to_class_name(self, module_name: str) -> str:
55
+ """Convert module name (snake_case) to class name (PascalCase)."""
56
+ # Convert snake_case to PascalCase
57
+ # tree_node -> TreeNode
58
+ # message -> Message
59
+ parts = module_name.split("_")
60
+ return "".join(word.capitalize() for word in parts)
61
+
62
+ def resolve(self, schema: IRSchema, resolve_alias_target: bool = False) -> Optional[str]:
63
+ """
64
+ Resolves an IRSchema that refers to a named model/enum, or an inline named enum.
65
+
66
+ Args:
67
+ schema: The IRSchema to resolve.
68
+ resolve_alias_target: If true, the resolver should return the Python type string for the
69
+ *target* of an alias. If false, it should return the alias name itself.
70
+
71
+ Returns:
72
+ A Python type string for the resolved schema, e.g., "MyModel", "Optional[MyModel]".
73
+ """
74
+
75
+ if schema.name and schema.name in self.all_schemas:
76
+ # This schema is a REFERENCE to a globally defined schema (e.g., in components/schemas)
77
+ ref_schema = self.all_schemas[schema.name] # Get the actual definition
78
+ assert ref_schema.name is not None, f"Schema '{schema.name}' resolved to ref_schema with None name."
79
+
80
+ # NEW: Use generation_name and final_module_stem from the referenced schema
81
+ assert (
82
+ ref_schema.generation_name is not None
83
+ ), f"Referenced schema '{ref_schema.name}' must have generation_name set."
84
+ assert (
85
+ ref_schema.final_module_stem is not None
86
+ ), f"Referenced schema '{ref_schema.name}' must have final_module_stem set."
87
+
88
+ class_name_for_ref = ref_schema.generation_name
89
+ module_name_for_ref = ref_schema.final_module_stem
90
+
91
+ model_module_path_for_ref = (
92
+ f"{self.context.get_current_package_name_for_generated_code()}.models.{module_name_for_ref}"
93
+ )
94
+
95
+ key_to_check = model_module_path_for_ref
96
+ name_to_add = class_name_for_ref
97
+
98
+ if not resolve_alias_target:
99
+ # Check for self-reference: if we're generating the same class that we're trying to import
100
+ is_self_reference = self._is_self_reference(module_name_for_ref, class_name_for_ref)
101
+
102
+ if is_self_reference:
103
+ # For self-references, don't add import and return quoted type name
104
+ return f'"{name_to_add}"'
105
+ else:
106
+ # For external references, add import and return unquoted type name
107
+ self.context.add_import(logical_module=key_to_check, name=name_to_add)
108
+ return name_to_add
109
+ else:
110
+ # self.resolve_alias_target is TRUE. We are trying to find the *actual underlying type*
111
+ # of 'ref_schema' for use in an alias definition (e.g., MyStringAlias: TypeAlias = str).
112
+ # Check if ref_schema is structurally a simple alias (no properties, enum, composition)
113
+ is_structurally_simple_alias = not (
114
+ ref_schema.properties
115
+ or ref_schema.enum
116
+ or ref_schema.any_of
117
+ or ref_schema.one_of
118
+ or ref_schema.all_of
119
+ )
120
+
121
+ if is_structurally_simple_alias:
122
+ # It's an alias to a primitive, array, or simple object.
123
+ # We need to return the Python type of its target.
124
+ # For this, we delegate back to the main resolver, but on ref_schema's definition,
125
+ # and crucially, with resolve_alias_target=False for that sub-call to avoid loops
126
+ # and to get the structural type.
127
+ # Also, treat ref_schema as anonymous for this sub-resolution so it's purely structural.
128
+
129
+ # Construct a temporary schema that is like ref_schema but anonymous
130
+ # to force structural resolution by the main resolver.
131
+ # This is a bit of a workaround for not having direct access to other resolvers here.
132
+ # A better design might involve passing the main SchemaTypeResolver instance.
133
+ # For now, returning None effectively tells TypeHelper to do this.
134
+
135
+ return None # Signal to TypeHelper to resolve ref_schema structurally.
136
+ else:
137
+ # ref_schema is NOT structurally alias-like (e.g., it's a full object schema).
138
+ # If we are resolving an alias target, and the target is a full object schema,
139
+ # the "target type" IS that object schema's name.
140
+ # e.g. MyDataAlias = DataObject. Here, DataObject is the target.
141
+ # The AliasGenerator will then generate "MyDataAlias: TypeAlias = DataObject".
142
+ # It needs "DataObject" as the string.
143
+ # The import for DataObject will be handled by TypeHelper when generating that alias
144
+ # file itself, using the regular non-alias-target path.
145
+
146
+ self.context.add_import(logical_module=key_to_check, name=name_to_add)
147
+ return name_to_add # Return name_to_add
148
+
149
+ elif schema.enum:
150
+ # This is an INLINE enum definition (not a reference to a global enum)
151
+ enum_name: Optional[str] = None
152
+ if schema.name: # If the inline enum has a name, it will be generated as a named enum class
153
+ enum_name = NameSanitizer.sanitize_class_name(schema.name)
154
+ module_name = NameSanitizer.sanitize_module_name(schema.name)
155
+ model_module_path = f"{self.context.get_current_package_name_for_generated_code()}.models.{module_name}"
156
+ self.context.add_import(logical_module=model_module_path, name=enum_name)
157
+ return enum_name
158
+ else: # Inline anonymous enum, falls back to primitive type of its values
159
+ # (Handled by PrimitiveTypeResolver if this returns None or specific primitive)
160
+ # For now, this path might lead to PrimitiveTypeResolver via TypeHelper's main loop.
161
+ # Let's try to return the primitive type directly if possible.
162
+ primitive_type_of_enum = "str" # Default for enums if type not specified
163
+ if schema.type == "integer":
164
+ primitive_type_of_enum = "int"
165
+ elif schema.type == "number":
166
+ primitive_type_of_enum = "float"
167
+ # other types for enums are unusual.
168
+ return primitive_type_of_enum
169
+ else:
170
+ # Not a reference to a known schema, and not an inline enum.
171
+ # This could be an anonymous complex type, or an unresolved reference.
172
+ # Defer to other resolvers by returning None.
173
+
174
+ return None
@@ -0,0 +1,212 @@
1
+ """Resolves IRSchema to Python object types (classes, dicts)."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import TYPE_CHECKING, Dict, Optional
6
+
7
+ from pyopenapi_gen import IRSchema
8
+ from pyopenapi_gen.context.render_context import RenderContext
9
+ from pyopenapi_gen.core.utils import NameSanitizer
10
+
11
+ if TYPE_CHECKING:
12
+ from .resolver import SchemaTypeResolver # Avoid circular import
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ObjectTypeResolver:
18
+ """Resolves IRSchema instances of type 'object'."""
19
+
20
+ def __init__(self, context: RenderContext, all_schemas: Dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
21
+ self.context = context
22
+ self.all_schemas = all_schemas
23
+ self.main_resolver = main_resolver # For resolving nested types
24
+
25
+ def _promote_anonymous_object_schema_if_needed(
26
+ self,
27
+ schema_to_promote: IRSchema,
28
+ proposed_name_base: Optional[str],
29
+ ) -> Optional[str]:
30
+ """Gives a name to an anonymous object schema and registers it."""
31
+ if not proposed_name_base:
32
+ return None
33
+
34
+ class_name_base = NameSanitizer.sanitize_class_name(proposed_name_base)
35
+ # Suffix logic can be refined here if needed (e.g. Property vs Item)
36
+ potential_new_name = f"{class_name_base}Item"
37
+ counter = 1
38
+ final_new_name = potential_new_name
39
+ while final_new_name in self.all_schemas:
40
+ final_new_name = f"{potential_new_name}{counter}"
41
+ counter += 1
42
+ if counter > 10: # Safety break
43
+ logger.error(
44
+ f"[ObjectTypeResolver._promote] Could not find unique name "
45
+ f"for base '{potential_new_name}' after 10 tries."
46
+ )
47
+ return None
48
+
49
+ schema_to_promote.name = final_new_name # Assign the new name
50
+ # Set generation_name and final_module_stem after the new name is assigned
51
+ schema_to_promote.generation_name = NameSanitizer.sanitize_class_name(final_new_name)
52
+ schema_to_promote.final_module_stem = NameSanitizer.sanitize_module_name(final_new_name)
53
+
54
+ self.all_schemas[final_new_name] = schema_to_promote # Register in global schemas
55
+
56
+ # Add import for this newly named model
57
+ module_to_import_from = f"models.{NameSanitizer.sanitize_module_name(final_new_name)}"
58
+ self.context.add_import(module_to_import_from, final_new_name)
59
+ return final_new_name
60
+
61
+ def resolve(self, schema: IRSchema, parent_schema_name_for_anon_promotion: Optional[str] = None) -> Optional[str]:
62
+ """
63
+ Resolves an IRSchema of `type: "object"`.
64
+ Args:
65
+ schema: The IRSchema, expected to have `type: "object"`.
66
+ parent_schema_name_for_anon_promotion: Contextual name for promoting anonymous objects.
67
+ Returns:
68
+ A Python type string or None.
69
+ """
70
+ if schema.type == "object":
71
+ # Path A: additionalProperties is True (boolean)
72
+ if isinstance(schema.additional_properties, bool) and schema.additional_properties:
73
+ self.context.add_import("typing", "Dict")
74
+ self.context.add_import("typing", "Any")
75
+ return "Dict[str, Any]"
76
+
77
+ # Path B: additionalProperties is an IRSchema instance
78
+ if isinstance(schema.additional_properties, IRSchema):
79
+ ap_schema_instance = schema.additional_properties
80
+ is_ap_schema_defined = (
81
+ ap_schema_instance.type is not None
82
+ or ap_schema_instance.format is not None
83
+ or ap_schema_instance.properties
84
+ or ap_schema_instance.items
85
+ or ap_schema_instance.enum
86
+ or ap_schema_instance.any_of
87
+ or ap_schema_instance.one_of
88
+ or ap_schema_instance.all_of
89
+ )
90
+ if is_ap_schema_defined:
91
+ additional_prop_type = self.main_resolver.resolve(ap_schema_instance, required=True)
92
+ self.context.add_import("typing", "Dict")
93
+ return f"Dict[str, {additional_prop_type}]"
94
+
95
+ # Path C: additionalProperties is False, None, or empty IRSchema.
96
+ if schema.properties: # Object has its own properties
97
+ if not schema.name: # Anonymous object with properties
98
+ if parent_schema_name_for_anon_promotion:
99
+ promoted_name = self._promote_anonymous_object_schema_if_needed(
100
+ schema, parent_schema_name_for_anon_promotion
101
+ )
102
+ if promoted_name:
103
+ return promoted_name
104
+ # Fallback for unpromoted anonymous object with properties
105
+ logger.warning(
106
+ f"[ObjectTypeResolver] Anonymous object with properties not promoted. "
107
+ f"-> Dict[str, Any]. Schema: {schema}"
108
+ )
109
+ self.context.add_import("typing", "Dict")
110
+ self.context.add_import("typing", "Any")
111
+ return "Dict[str, Any]"
112
+ else: # Named object with properties
113
+ # If this named object is a component schema, ensure it's imported.
114
+ if schema.name and schema.name in self.all_schemas:
115
+ actual_schema_def = self.all_schemas[schema.name]
116
+ assert (
117
+ actual_schema_def.generation_name is not None
118
+ ), f"Actual schema '{actual_schema_def.name}' for '{schema.name}' must have generation_name."
119
+ assert (
120
+ actual_schema_def.final_module_stem is not None
121
+ ), f"Actual schema '{actual_schema_def.name}' for '{schema.name}' must have final_module_stem."
122
+
123
+ class_name_to_use = actual_schema_def.generation_name
124
+ module_stem_to_use = actual_schema_def.final_module_stem
125
+
126
+ base_model_path_part = f"models.{module_stem_to_use}"
127
+ model_module_path = base_model_path_part
128
+
129
+ if self.context.package_root_for_generated_code and self.context.overall_project_root:
130
+ abs_pkg_root = os.path.abspath(self.context.package_root_for_generated_code)
131
+ abs_overall_root = os.path.abspath(self.context.overall_project_root)
132
+ if abs_pkg_root.startswith(abs_overall_root) and abs_pkg_root != abs_overall_root:
133
+ rel_pkg_path = os.path.relpath(abs_pkg_root, abs_overall_root)
134
+ current_gen_pkg_dot_path = rel_pkg_path.replace(os.sep, ".")
135
+ model_module_path = f"{current_gen_pkg_dot_path}.{base_model_path_part}"
136
+ elif abs_pkg_root == abs_overall_root:
137
+ model_module_path = base_model_path_part
138
+ elif self.context.package_root_for_generated_code:
139
+ current_gen_pkg_name_from_basename = os.path.basename(
140
+ os.path.normpath(self.context.package_root_for_generated_code)
141
+ )
142
+ if current_gen_pkg_name_from_basename and current_gen_pkg_name_from_basename != ".":
143
+ model_module_path = f"{current_gen_pkg_name_from_basename}.{base_model_path_part}"
144
+
145
+ current_module_dot_path = self.context.get_current_module_dot_path()
146
+ if model_module_path != current_module_dot_path: # Avoid self-imports
147
+ self.context.add_import(model_module_path, class_name_to_use)
148
+ return class_name_to_use # Return the potentially de-collided name
149
+ else:
150
+ # This case should ideally not be hit if all named objects are in all_schemas
151
+ # Or, it's a named object that isn't a global component (e.g. inline named object for promotion)
152
+ # For safety, use schema.name if it's not in all_schemas (might be a freshly promoted name)
153
+ class_name_to_use = NameSanitizer.sanitize_class_name(schema.name)
154
+ logger.warning(
155
+ f"[ObjectTypeResolver] Named object '{schema.name}' not in all_schemas, "
156
+ f"using its own name '{class_name_to_use}'. "
157
+ f"This might occur for locally promoted anonymous objects."
158
+ )
159
+ return class_name_to_use
160
+ else: # Object has NO properties
161
+ if (
162
+ schema.name and schema.name in self.all_schemas
163
+ ): # Named object, no properties, AND it's a known component
164
+ actual_schema_def = self.all_schemas[schema.name]
165
+ assert actual_schema_def.generation_name is not None, (
166
+ f"Actual schema (no props) '{actual_schema_def.name}' "
167
+ f"for '{schema.name}' must have generation_name."
168
+ )
169
+ assert actual_schema_def.final_module_stem is not None, (
170
+ f"Actual schema (no props) '{actual_schema_def.name}' "
171
+ f"for '{schema.name}' must have final_module_stem."
172
+ )
173
+
174
+ class_name_to_use = actual_schema_def.generation_name
175
+ module_stem_to_use = actual_schema_def.final_module_stem
176
+
177
+ base_model_path_part = f"models.{module_stem_to_use}"
178
+ model_module_path = base_model_path_part
179
+
180
+ if self.context.package_root_for_generated_code and self.context.overall_project_root:
181
+ abs_pkg_root = os.path.abspath(self.context.package_root_for_generated_code)
182
+ abs_overall_root = os.path.abspath(self.context.overall_project_root)
183
+ if abs_pkg_root.startswith(abs_overall_root) and abs_pkg_root != abs_overall_root:
184
+ rel_pkg_path = os.path.relpath(abs_pkg_root, abs_overall_root)
185
+ current_gen_pkg_dot_path = rel_pkg_path.replace(os.sep, ".")
186
+ model_module_path = f"{current_gen_pkg_dot_path}.{base_model_path_part}"
187
+ elif abs_pkg_root == abs_overall_root:
188
+ model_module_path = base_model_path_part
189
+ elif self.context.package_root_for_generated_code:
190
+ current_gen_pkg_name_from_basename = os.path.basename(
191
+ os.path.normpath(self.context.package_root_for_generated_code)
192
+ )
193
+ if current_gen_pkg_name_from_basename and current_gen_pkg_name_from_basename != ".":
194
+ model_module_path = f"{current_gen_pkg_name_from_basename}.{base_model_path_part}"
195
+
196
+ current_module_dot_path = self.context.get_current_module_dot_path()
197
+ if model_module_path != current_module_dot_path: # Avoid self-imports
198
+ self.context.add_import(model_module_path, class_name_to_use)
199
+ return class_name_to_use # Return the potentially de-collided name
200
+ elif schema.name: # Named object, no properties, but NOT a known component
201
+ self.context.add_import("typing", "Dict")
202
+ self.context.add_import("typing", "Any")
203
+ return "Dict[str, Any]"
204
+ else: # Anonymous object, no properties
205
+ if schema.additional_properties is None: # Default OpenAPI behavior allows additional props
206
+ self.context.add_import("typing", "Dict")
207
+ self.context.add_import("typing", "Any")
208
+ return "Dict[str, Any]"
209
+ else: # additionalProperties was False or restrictive empty schema
210
+ self.context.add_import("typing", "Any")
211
+ return "Any"
212
+ return None
@@ -0,0 +1,57 @@
1
+ """Resolves IRSchema to Python primitive types."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from pyopenapi_gen import IRSchema
7
+ from pyopenapi_gen.context.render_context import RenderContext
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class PrimitiveTypeResolver:
13
+ """Resolves IRSchema to Python primitive type strings."""
14
+
15
+ def __init__(self, context: RenderContext):
16
+ self.context = context
17
+
18
+ def resolve(self, schema: IRSchema) -> Optional[str]:
19
+ """
20
+ Resolves an IRSchema to a Python primitive type string based on its 'type' and 'format'.
21
+
22
+ Handles standard OpenAPI types:
23
+ - integer -> "int"
24
+ - number -> "float"
25
+ - boolean -> "bool"
26
+ - string -> "str"
27
+ - string with format "date-time" -> "datetime" (imports `datetime.datetime`)
28
+ - string with format "date" -> "date" (imports `datetime.date`)
29
+ - string with format "binary" -> "bytes"
30
+ - null -> "None" (the string literal "None")
31
+
32
+ Args:
33
+ schema: The IRSchema to resolve.
34
+
35
+ Returns:
36
+ The Python primitive type string if the schema matches a known primitive type/format,
37
+ otherwise None.
38
+ """
39
+ primitive_type_map = {
40
+ "integer": "int",
41
+ "number": "float",
42
+ "boolean": "bool",
43
+ "string": "str",
44
+ }
45
+ if schema.type == "null":
46
+ return "None" # String literal "None"
47
+ if schema.type == "string" and schema.format == "date-time":
48
+ self.context.add_import("datetime", "datetime")
49
+ return "datetime"
50
+ if schema.type == "string" and schema.format == "date":
51
+ self.context.add_import("datetime", "date")
52
+ return "date"
53
+ if schema.type == "string" and schema.format == "binary":
54
+ return "bytes"
55
+ if schema.type in primitive_type_map:
56
+ return primitive_type_map[schema.type]
57
+ return None
@@ -0,0 +1,48 @@
1
+ """Orchestrates IRSchema to Python type resolution."""
2
+
3
+ import logging
4
+ from typing import Dict, Optional
5
+
6
+ from pyopenapi_gen import IRSchema
7
+ from pyopenapi_gen.context.render_context import RenderContext
8
+ from pyopenapi_gen.types.services.type_service import UnifiedTypeService
9
+
10
+ from .array_resolver import ArrayTypeResolver
11
+ from .composition_resolver import CompositionTypeResolver
12
+ from .finalizer import TypeFinalizer
13
+ from .named_resolver import NamedTypeResolver
14
+ from .object_resolver import ObjectTypeResolver
15
+ from .primitive_resolver import PrimitiveTypeResolver
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class SchemaTypeResolver:
21
+ """Orchestrates the resolution of IRSchema to Python type strings."""
22
+
23
+ def __init__(self, context: RenderContext, all_schemas: Dict[str, IRSchema]):
24
+ self.context = context
25
+ self.all_schemas = all_schemas
26
+
27
+ # Initialize specialized resolvers, passing self for circular dependencies if needed
28
+ self.primitive_resolver = PrimitiveTypeResolver(context)
29
+ self.named_resolver = NamedTypeResolver(context, all_schemas)
30
+ self.array_resolver = ArrayTypeResolver(context, all_schemas, self)
31
+ self.object_resolver = ObjectTypeResolver(context, all_schemas, self)
32
+ self.composition_resolver = CompositionTypeResolver(context, all_schemas, self)
33
+ self.finalizer = TypeFinalizer(context, self.all_schemas)
34
+
35
+ def resolve(
36
+ self,
37
+ schema: IRSchema,
38
+ required: bool = True,
39
+ resolve_alias_target: bool = False,
40
+ current_schema_context_name: Optional[str] = None,
41
+ ) -> str:
42
+ """
43
+ Determines the Python type string for a given IRSchema.
44
+ Now delegates to the UnifiedTypeService for consistent type resolution.
45
+ """
46
+ # Delegate to the unified type service for all type resolution
47
+ type_service = UnifiedTypeService(self.all_schemas)
48
+ return type_service.resolve_schema_type(schema, self.context, required)
@@ -0,0 +1,14 @@
1
+ """
2
+ Utility for extracting variable names from URL templates.
3
+ """
4
+
5
+ import re
6
+ from typing import Set
7
+
8
+
9
+ def extract_url_variables(url: str) -> Set[str]:
10
+ """
11
+ Extract all variable names (e.g., 'foo' from '{foo}') from a URL template.
12
+ Returns a set of variable names as strings.
13
+ """
14
+ return set(re.findall(r"{([^}]+)}", url))
@@ -0,0 +1,20 @@
1
+ from enum import Enum, unique
2
+
3
+
4
+ @unique
5
+ class HTTPMethod(str, Enum):
6
+ """Canonical HTTP method names supported by OpenAPI.
7
+
8
+ Implemented as `str` subclass to allow seamless usage anywhere a plain
9
+ string is expected (e.g., httpx, logging), while still providing strict
10
+ enumeration benefits.
11
+ """
12
+
13
+ GET = "GET"
14
+ POST = "POST"
15
+ PUT = "PUT"
16
+ PATCH = "PATCH"
17
+ DELETE = "DELETE"
18
+ OPTIONS = "OPTIONS"
19
+ HEAD = "HEAD"
20
+ TRACE = "TRACE"