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.
- pyopenapi_gen/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +30 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- 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"
|