pyopenapi-gen 2.7.2__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 +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -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/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -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 +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -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 +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -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 +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -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 +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -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 +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -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 +28 -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 +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Finalizes and cleans Python type strings."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pyopenapi_gen import IRSchema
|
|
6
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
7
|
+
from pyopenapi_gen.helpers.type_cleaner import TypeCleaner
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TypeFinalizer:
|
|
13
|
+
"""Handles final wrapping (Optional) and cleaning of type strings."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema] | None = None):
|
|
16
|
+
self.context = context
|
|
17
|
+
self.all_schemas = all_schemas if all_schemas is not None else {}
|
|
18
|
+
|
|
19
|
+
def finalize(self, py_type: str | None, schema: IRSchema, required: bool) -> str:
|
|
20
|
+
"""Wraps with Optional if needed, cleans the type string, and ensures typing imports."""
|
|
21
|
+
if py_type is None:
|
|
22
|
+
logger.warning(
|
|
23
|
+
f"[TypeFinalizer] Received None as py_type for schema "
|
|
24
|
+
f"'{schema.name or 'anonymous'}'. Defaulting to 'Any'."
|
|
25
|
+
)
|
|
26
|
+
self.context.add_import("typing", "Any")
|
|
27
|
+
py_type = "Any"
|
|
28
|
+
|
|
29
|
+
# CRITICAL: Clean BEFORE wrapping to prevent TypeCleaner from breaking "Union[X] | None" patterns
|
|
30
|
+
cleaned_type = self._clean_type(py_type)
|
|
31
|
+
optional_type = self._wrap_with_optional_if_needed(cleaned_type, schema, required)
|
|
32
|
+
|
|
33
|
+
# Ensure imports for common typing constructs that might have been introduced by cleaning or wrapping
|
|
34
|
+
final_type = optional_type # Use the wrapped type for import analysis
|
|
35
|
+
if "dict[" in final_type or final_type == "Dict":
|
|
36
|
+
self.context.add_import("typing", "Dict")
|
|
37
|
+
if "List[" in final_type or final_type == "List":
|
|
38
|
+
self.context.add_import("typing", "List")
|
|
39
|
+
if "Tuple[" in final_type or final_type == "Tuple": # Tuple might also appear bare
|
|
40
|
+
self.context.add_import("typing", "Tuple")
|
|
41
|
+
if "Union[" in final_type:
|
|
42
|
+
self.context.add_import("typing", "Union")
|
|
43
|
+
# Optional is now handled entirely by _wrap_with_optional_if_needed and not here
|
|
44
|
+
if final_type == "Any" or final_type == "Any | None": # Ensure Any is imported if it's the final type
|
|
45
|
+
self.context.add_import("typing", "Any")
|
|
46
|
+
|
|
47
|
+
return final_type
|
|
48
|
+
|
|
49
|
+
def _wrap_with_optional_if_needed(self, py_type: str, schema_being_wrapped: IRSchema, required: bool) -> str:
|
|
50
|
+
"""Wraps the Python type string with `... | None` if necessary.
|
|
51
|
+
|
|
52
|
+
Note: Modern Python 3.10+ uses X | None syntax exclusively.
|
|
53
|
+
Optional[X] should NEVER appear here - our unified type system generates X | None directly.
|
|
54
|
+
"""
|
|
55
|
+
is_considered_optional_by_usage = not required or schema_being_wrapped.is_nullable is True
|
|
56
|
+
|
|
57
|
+
if not is_considered_optional_by_usage:
|
|
58
|
+
return py_type # Not optional by usage, so don't wrap.
|
|
59
|
+
|
|
60
|
+
# At this point, usage implies optional. Now check if py_type inherently is.
|
|
61
|
+
|
|
62
|
+
if py_type == "Any":
|
|
63
|
+
# Modern Python 3.10+ doesn't need Optional import for | None syntax
|
|
64
|
+
return "Any | None" # Any is special, always wrap if usage is optional.
|
|
65
|
+
|
|
66
|
+
# SANITY CHECK: Unified type system should never produce Optional[X]
|
|
67
|
+
if py_type.startswith("Optional[") and py_type.endswith("]"):
|
|
68
|
+
logger.error(
|
|
69
|
+
f"❌ ARCHITECTURE VIOLATION: Received legacy Optional[X] type: {py_type}. "
|
|
70
|
+
f"This should NEVER happen - unified type system generates X | None directly. "
|
|
71
|
+
f"Schema: {schema_being_wrapped.name or 'anonymous'}. "
|
|
72
|
+
f"This indicates a bug in the type resolution pipeline."
|
|
73
|
+
)
|
|
74
|
+
# Defensive conversion (but this indicates a serious bug upstream)
|
|
75
|
+
inner_type = py_type[9:-1] # Remove "Optional[" and "]"
|
|
76
|
+
logger.warning(f"⚠️ Converted to modern syntax: {inner_type} | None")
|
|
77
|
+
return f"{inner_type} | None"
|
|
78
|
+
|
|
79
|
+
# If already has | None (modern style), don't add again
|
|
80
|
+
if " | None" in py_type or py_type.endswith("| None"):
|
|
81
|
+
return py_type # Already has | None union syntax.
|
|
82
|
+
|
|
83
|
+
is_union_with_none = "Union[" in py_type and (
|
|
84
|
+
", None]" in py_type or "[None," in py_type or ", None," in py_type or py_type == "Union[None]"
|
|
85
|
+
)
|
|
86
|
+
if is_union_with_none:
|
|
87
|
+
return py_type # Already a Union with None.
|
|
88
|
+
|
|
89
|
+
# New check: if py_type refers to a named schema that IS ITSELF nullable,
|
|
90
|
+
# its alias definition (if it's an alias) or its usage as a dataclass field type
|
|
91
|
+
# will effectively be Optional. So, if field usage is optional, we don't ADD another Optional layer.
|
|
92
|
+
if py_type in self.all_schemas: # Check if py_type is a known schema name
|
|
93
|
+
referenced_schema = self.all_schemas[py_type]
|
|
94
|
+
# If the schema being referenced is itself nullable, its definition (if alias)
|
|
95
|
+
# or its direct usage (if dataclass) will incorporate Optional via the resolver calling this finalizer.
|
|
96
|
+
# Thus, we avoid double-wrapping if the *usage* of this type is also optional.
|
|
97
|
+
if referenced_schema.is_nullable:
|
|
98
|
+
return py_type
|
|
99
|
+
|
|
100
|
+
# Wrap type with modern | None syntax (no Optional import needed in Python 3.10+)
|
|
101
|
+
return f"{py_type} | None"
|
|
102
|
+
|
|
103
|
+
def _clean_type(self, type_str: str) -> str:
|
|
104
|
+
"""Cleans a Python type string using TypeCleaner."""
|
|
105
|
+
return TypeCleaner.clean_type_parameters(type_str)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Resolves IRSchema to Python named types (classes, enums)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from pyopenapi_gen import IRSchema
|
|
7
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
8
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NamedTypeResolver:
|
|
14
|
+
"""Resolves IRSchema instances that refer to named models/enums."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema]):
|
|
17
|
+
self.context = context
|
|
18
|
+
self.all_schemas = all_schemas
|
|
19
|
+
|
|
20
|
+
def _is_self_reference(self, target_module_name: str, target_class_name: str) -> bool:
|
|
21
|
+
"""Check if the target class is the same as the one currently being generated."""
|
|
22
|
+
if not self.context.current_file:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
# Extract current module name from the file path
|
|
26
|
+
current_file_name = self.context.current_file
|
|
27
|
+
current_module_name = os.path.splitext(os.path.basename(current_file_name))[0]
|
|
28
|
+
|
|
29
|
+
# For self-reference detection, we need to check if:
|
|
30
|
+
# 1. The target module name matches the current module name
|
|
31
|
+
# 2. The target class name is likely the class being defined in this file
|
|
32
|
+
#
|
|
33
|
+
# The target_class_name should match the class being generated in the current file
|
|
34
|
+
# For example, if we're in tree_node.py generating TreeNode class, and we're trying
|
|
35
|
+
# to reference TreeNode, then this is a self-reference
|
|
36
|
+
return current_module_name == target_module_name and self._class_being_generated_matches(target_class_name)
|
|
37
|
+
|
|
38
|
+
def _class_being_generated_matches(self, target_class_name: str) -> bool:
|
|
39
|
+
"""Check if the target class name matches what's being generated in the current file."""
|
|
40
|
+
# This is a simple heuristic: if the file is tree_node.py, we expect TreeNode class
|
|
41
|
+
# More sophisticated logic could be added by tracking what class is currently being generated
|
|
42
|
+
if not self.context.current_file:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
current_file_name = self.context.current_file
|
|
46
|
+
current_module_name = os.path.splitext(os.path.basename(current_file_name))[0]
|
|
47
|
+
|
|
48
|
+
# Convert module name to expected class name (snake_case to PascalCase)
|
|
49
|
+
expected_class_name = self._module_name_to_class_name(current_module_name)
|
|
50
|
+
|
|
51
|
+
return target_class_name == expected_class_name
|
|
52
|
+
|
|
53
|
+
def _module_name_to_class_name(self, module_name: str) -> str:
|
|
54
|
+
"""Convert module name (snake_case) to class name (PascalCase)."""
|
|
55
|
+
# Convert snake_case to PascalCase
|
|
56
|
+
# tree_node -> TreeNode
|
|
57
|
+
# message -> Message
|
|
58
|
+
parts = module_name.split("_")
|
|
59
|
+
return "".join(word.capitalize() for word in parts)
|
|
60
|
+
|
|
61
|
+
def resolve(self, schema: IRSchema, resolve_alias_target: bool = False) -> str | None:
|
|
62
|
+
"""
|
|
63
|
+
Resolves an IRSchema that refers to a named model/enum, or an inline named enum.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
schema: The IRSchema to resolve.
|
|
67
|
+
resolve_alias_target: If true, the resolver should return the Python type string for the
|
|
68
|
+
*target* of an alias. If false, it should return the alias name itself.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A Python type string for the resolved schema, e.g., "MyModel", "MyModel | None".
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if schema.name and schema.name in self.all_schemas:
|
|
75
|
+
# This schema is a REFERENCE to a globally defined schema (e.g., in components/schemas)
|
|
76
|
+
ref_schema = self.all_schemas[schema.name] # Get the actual definition
|
|
77
|
+
if ref_schema.name is None:
|
|
78
|
+
raise RuntimeError(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
|
+
if ref_schema.generation_name is None:
|
|
82
|
+
raise RuntimeError(f"Referenced schema '{ref_schema.name}' must have generation_name set.")
|
|
83
|
+
if ref_schema.final_module_stem is None:
|
|
84
|
+
raise RuntimeError(f"Referenced schema '{ref_schema.name}' must have final_module_stem set.")
|
|
85
|
+
|
|
86
|
+
class_name_for_ref = ref_schema.generation_name
|
|
87
|
+
module_name_for_ref = ref_schema.final_module_stem
|
|
88
|
+
|
|
89
|
+
model_module_path_for_ref = (
|
|
90
|
+
f"{self.context.get_current_package_name_for_generated_code()}.models.{module_name_for_ref}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
key_to_check = model_module_path_for_ref
|
|
94
|
+
name_to_add = class_name_for_ref
|
|
95
|
+
|
|
96
|
+
if not resolve_alias_target:
|
|
97
|
+
# Check for self-reference: if we're generating the same class that we're trying to import
|
|
98
|
+
is_self_reference = self._is_self_reference(module_name_for_ref, class_name_for_ref)
|
|
99
|
+
|
|
100
|
+
if is_self_reference:
|
|
101
|
+
# For self-references, don't add import and return quoted type name
|
|
102
|
+
return f'"{name_to_add}"'
|
|
103
|
+
else:
|
|
104
|
+
# For external references, add import and return unquoted type name
|
|
105
|
+
self.context.add_import(logical_module=key_to_check, name=name_to_add)
|
|
106
|
+
return name_to_add
|
|
107
|
+
else:
|
|
108
|
+
# self.resolve_alias_target is TRUE. We are trying to find the *actual underlying type*
|
|
109
|
+
# of 'ref_schema' for use in an alias definition (e.g., MyStringAlias: TypeAlias = str).
|
|
110
|
+
# Check if ref_schema is structurally a simple alias (no properties, enum, composition)
|
|
111
|
+
is_structurally_simple_alias = not (
|
|
112
|
+
ref_schema.properties
|
|
113
|
+
or ref_schema.enum
|
|
114
|
+
or ref_schema.any_of
|
|
115
|
+
or ref_schema.one_of
|
|
116
|
+
or ref_schema.all_of
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if is_structurally_simple_alias:
|
|
120
|
+
# It's an alias to a primitive, array, or simple object.
|
|
121
|
+
# We need to return the Python type of its target.
|
|
122
|
+
# For this, we delegate back to the main resolver, but on ref_schema's definition,
|
|
123
|
+
# and crucially, with resolve_alias_target=False for that sub-call to avoid loops
|
|
124
|
+
# and to get the structural type.
|
|
125
|
+
# Also, treat ref_schema as anonymous for this sub-resolution so it's purely structural.
|
|
126
|
+
|
|
127
|
+
# Construct a temporary schema that is like ref_schema but anonymous
|
|
128
|
+
# to force structural resolution by the main resolver.
|
|
129
|
+
# This is a bit of a workaround for not having direct access to other resolvers here.
|
|
130
|
+
# A better design might involve passing the main SchemaTypeResolver instance.
|
|
131
|
+
# For now, returning None effectively tells TypeHelper to do this.
|
|
132
|
+
|
|
133
|
+
return None # Signal to TypeHelper to resolve ref_schema structurally.
|
|
134
|
+
else:
|
|
135
|
+
# ref_schema is NOT structurally alias-like (e.g., it's a full object schema).
|
|
136
|
+
# If we are resolving an alias target, and the target is a full object schema,
|
|
137
|
+
# the "target type" IS that object schema's name.
|
|
138
|
+
# e.g. MyDataAlias = DataObject. Here, DataObject is the target.
|
|
139
|
+
# The AliasGenerator will then generate "MyDataAlias: TypeAlias = DataObject".
|
|
140
|
+
# It needs "DataObject" as the string.
|
|
141
|
+
# The import for DataObject will be handled by TypeHelper when generating that alias
|
|
142
|
+
# file itself, using the regular non-alias-target path.
|
|
143
|
+
|
|
144
|
+
self.context.add_import(logical_module=key_to_check, name=name_to_add)
|
|
145
|
+
return name_to_add # Return name_to_add
|
|
146
|
+
|
|
147
|
+
elif schema.enum:
|
|
148
|
+
# This is an INLINE enum definition (not a reference to a global enum)
|
|
149
|
+
enum_name: str | None = None
|
|
150
|
+
if schema.name: # If the inline enum has a name, it will be generated as a named enum class
|
|
151
|
+
enum_name = NameSanitizer.sanitize_class_name(schema.name)
|
|
152
|
+
module_name = NameSanitizer.sanitize_module_name(schema.name)
|
|
153
|
+
model_module_path = f"{self.context.get_current_package_name_for_generated_code()}.models.{module_name}"
|
|
154
|
+
self.context.add_import(logical_module=model_module_path, name=enum_name)
|
|
155
|
+
return enum_name
|
|
156
|
+
else: # Inline anonymous enum, falls back to primitive type of its values
|
|
157
|
+
# (Handled by PrimitiveTypeResolver if this returns None or specific primitive)
|
|
158
|
+
# For now, this path might lead to PrimitiveTypeResolver via TypeHelper's main loop.
|
|
159
|
+
# Let's try to return the primitive type directly if possible.
|
|
160
|
+
primitive_type_of_enum = "str" # Default for enums if type not specified
|
|
161
|
+
if schema.type == "integer":
|
|
162
|
+
primitive_type_of_enum = "int"
|
|
163
|
+
elif schema.type == "number":
|
|
164
|
+
primitive_type_of_enum = "float"
|
|
165
|
+
# other types for enums are unusual.
|
|
166
|
+
return primitive_type_of_enum
|
|
167
|
+
else:
|
|
168
|
+
# Not a reference to a known schema, and not an inline enum.
|
|
169
|
+
# This could be an anonymous complex type, or an unresolved reference.
|
|
170
|
+
# Defer to other resolvers by returning None.
|
|
171
|
+
|
|
172
|
+
return None
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Resolves IRSchema to Python object types (classes, dicts)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
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: str | None,
|
|
29
|
+
) -> str | None:
|
|
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: str | None = None) -> str | None:
|
|
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
|
+
if actual_schema_def.generation_name is None:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"Actual schema '{actual_schema_def.name}' for '{schema.name}' must have generation_name."
|
|
119
|
+
)
|
|
120
|
+
if actual_schema_def.final_module_stem is None:
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
f"Actual schema '{actual_schema_def.name}' for '{schema.name}' must have final_module_stem."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
class_name_to_use = actual_schema_def.generation_name
|
|
126
|
+
module_stem_to_use = actual_schema_def.final_module_stem
|
|
127
|
+
|
|
128
|
+
base_model_path_part = f"models.{module_stem_to_use}"
|
|
129
|
+
model_module_path = base_model_path_part
|
|
130
|
+
|
|
131
|
+
if self.context.package_root_for_generated_code and self.context.overall_project_root:
|
|
132
|
+
abs_pkg_root = os.path.abspath(self.context.package_root_for_generated_code)
|
|
133
|
+
abs_overall_root = os.path.abspath(self.context.overall_project_root)
|
|
134
|
+
if abs_pkg_root.startswith(abs_overall_root) and abs_pkg_root != abs_overall_root:
|
|
135
|
+
rel_pkg_path = os.path.relpath(abs_pkg_root, abs_overall_root)
|
|
136
|
+
current_gen_pkg_dot_path = rel_pkg_path.replace(os.sep, ".")
|
|
137
|
+
model_module_path = f"{current_gen_pkg_dot_path}.{base_model_path_part}"
|
|
138
|
+
elif abs_pkg_root == abs_overall_root:
|
|
139
|
+
model_module_path = base_model_path_part
|
|
140
|
+
elif self.context.package_root_for_generated_code:
|
|
141
|
+
current_gen_pkg_name_from_basename = os.path.basename(
|
|
142
|
+
os.path.normpath(self.context.package_root_for_generated_code)
|
|
143
|
+
)
|
|
144
|
+
if current_gen_pkg_name_from_basename and current_gen_pkg_name_from_basename != ".":
|
|
145
|
+
model_module_path = f"{current_gen_pkg_name_from_basename}.{base_model_path_part}"
|
|
146
|
+
|
|
147
|
+
current_module_dot_path = self.context.get_current_module_dot_path()
|
|
148
|
+
if model_module_path != current_module_dot_path: # Avoid self-imports
|
|
149
|
+
self.context.add_import(model_module_path, class_name_to_use)
|
|
150
|
+
return class_name_to_use # Return the potentially de-collided name
|
|
151
|
+
else:
|
|
152
|
+
# This case should ideally not be hit if all named objects are in all_schemas
|
|
153
|
+
# Or, it's a named object that isn't a global component (e.g. inline named object for promotion)
|
|
154
|
+
# For safety, use schema.name if it's not in all_schemas (might be a freshly promoted name)
|
|
155
|
+
class_name_to_use = NameSanitizer.sanitize_class_name(schema.name)
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"[ObjectTypeResolver] Named object '{schema.name}' not in all_schemas, "
|
|
158
|
+
f"using its own name '{class_name_to_use}'. "
|
|
159
|
+
f"This might occur for locally promoted anonymous objects."
|
|
160
|
+
)
|
|
161
|
+
return class_name_to_use
|
|
162
|
+
else: # Object has NO properties
|
|
163
|
+
if (
|
|
164
|
+
schema.name and schema.name in self.all_schemas
|
|
165
|
+
): # Named object, no properties, AND it's a known component
|
|
166
|
+
actual_schema_def = self.all_schemas[schema.name]
|
|
167
|
+
if actual_schema_def.generation_name is None:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"Actual schema (no props) '{actual_schema_def.name}' "
|
|
170
|
+
f"for '{schema.name}' must have generation_name."
|
|
171
|
+
)
|
|
172
|
+
if actual_schema_def.final_module_stem is None:
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
f"Actual schema (no props) '{actual_schema_def.name}' "
|
|
175
|
+
f"for '{schema.name}' must have final_module_stem."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
class_name_to_use = actual_schema_def.generation_name
|
|
179
|
+
module_stem_to_use = actual_schema_def.final_module_stem
|
|
180
|
+
|
|
181
|
+
base_model_path_part = f"models.{module_stem_to_use}"
|
|
182
|
+
model_module_path = base_model_path_part
|
|
183
|
+
|
|
184
|
+
if self.context.package_root_for_generated_code and self.context.overall_project_root:
|
|
185
|
+
abs_pkg_root = os.path.abspath(self.context.package_root_for_generated_code)
|
|
186
|
+
abs_overall_root = os.path.abspath(self.context.overall_project_root)
|
|
187
|
+
if abs_pkg_root.startswith(abs_overall_root) and abs_pkg_root != abs_overall_root:
|
|
188
|
+
rel_pkg_path = os.path.relpath(abs_pkg_root, abs_overall_root)
|
|
189
|
+
current_gen_pkg_dot_path = rel_pkg_path.replace(os.sep, ".")
|
|
190
|
+
model_module_path = f"{current_gen_pkg_dot_path}.{base_model_path_part}"
|
|
191
|
+
elif abs_pkg_root == abs_overall_root:
|
|
192
|
+
model_module_path = base_model_path_part
|
|
193
|
+
elif self.context.package_root_for_generated_code:
|
|
194
|
+
current_gen_pkg_name_from_basename = os.path.basename(
|
|
195
|
+
os.path.normpath(self.context.package_root_for_generated_code)
|
|
196
|
+
)
|
|
197
|
+
if current_gen_pkg_name_from_basename and current_gen_pkg_name_from_basename != ".":
|
|
198
|
+
model_module_path = f"{current_gen_pkg_name_from_basename}.{base_model_path_part}"
|
|
199
|
+
|
|
200
|
+
current_module_dot_path = self.context.get_current_module_dot_path()
|
|
201
|
+
if model_module_path != current_module_dot_path: # Avoid self-imports
|
|
202
|
+
self.context.add_import(model_module_path, class_name_to_use)
|
|
203
|
+
return class_name_to_use # Return the potentially de-collided name
|
|
204
|
+
elif schema.name: # Named object, no properties, but NOT a known component
|
|
205
|
+
self.context.add_import("typing", "Dict")
|
|
206
|
+
self.context.add_import("typing", "Any")
|
|
207
|
+
return "dict[str, Any]"
|
|
208
|
+
else: # Anonymous object, no properties
|
|
209
|
+
if schema.additional_properties is None: # Default OpenAPI behavior allows additional props
|
|
210
|
+
self.context.add_import("typing", "Dict")
|
|
211
|
+
self.context.add_import("typing", "Any")
|
|
212
|
+
return "dict[str, Any]"
|
|
213
|
+
else: # additionalProperties was False or restrictive empty schema
|
|
214
|
+
self.context.add_import("typing", "Any")
|
|
215
|
+
return "Any"
|
|
216
|
+
return None
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Resolves IRSchema to Python primitive types."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pyopenapi_gen import IRSchema
|
|
6
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrimitiveTypeResolver:
|
|
12
|
+
"""Resolves IRSchema to Python primitive type strings."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, context: RenderContext):
|
|
15
|
+
self.context = context
|
|
16
|
+
|
|
17
|
+
def resolve(self, schema: IRSchema) -> str | None:
|
|
18
|
+
"""
|
|
19
|
+
Resolves an IRSchema to a Python primitive type string based on its 'type' and 'format'.
|
|
20
|
+
|
|
21
|
+
Handles standard OpenAPI types and formats:
|
|
22
|
+
- integer -> "int" (also int32, int64 formats)
|
|
23
|
+
- number -> "float" (also float, double formats)
|
|
24
|
+
- boolean -> "bool"
|
|
25
|
+
- string -> "str"
|
|
26
|
+
- string with format "date-time" -> "datetime" (imports `datetime.datetime`)
|
|
27
|
+
- string with format "date" -> "date" (imports `datetime.date`)
|
|
28
|
+
- string with format "time" -> "time" (imports `datetime.time`)
|
|
29
|
+
- string with format "duration" -> "timedelta" (imports `datetime.timedelta`)
|
|
30
|
+
- string with format "uuid" -> "UUID" (imports `uuid.UUID`)
|
|
31
|
+
- string with format "binary" or "byte" -> "bytes"
|
|
32
|
+
- string with format "ipv4" -> "IPv4Address" (imports `ipaddress.IPv4Address`)
|
|
33
|
+
- string with format "ipv6" -> "IPv6Address" (imports `ipaddress.IPv6Address`)
|
|
34
|
+
- string with format "uri", "url", "email", "hostname", "password" -> "str"
|
|
35
|
+
- null -> "None" (the string literal "None")
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
schema: The IRSchema to resolve.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The Python primitive type string if the schema matches a known primitive type/format,
|
|
42
|
+
otherwise None.
|
|
43
|
+
"""
|
|
44
|
+
if schema.type == "null":
|
|
45
|
+
return "None"
|
|
46
|
+
|
|
47
|
+
# Handle string formats first (before falling through to generic string)
|
|
48
|
+
if schema.type == "string" and schema.format:
|
|
49
|
+
return self._resolve_string_format(schema.format)
|
|
50
|
+
|
|
51
|
+
# Handle integer formats
|
|
52
|
+
if schema.type == "integer":
|
|
53
|
+
# int32 and int64 both map to Python int
|
|
54
|
+
return "int"
|
|
55
|
+
|
|
56
|
+
# Handle number formats
|
|
57
|
+
if schema.type == "number":
|
|
58
|
+
# float and double both map to Python float
|
|
59
|
+
return "float"
|
|
60
|
+
|
|
61
|
+
# Handle remaining primitive types
|
|
62
|
+
primitive_type_map = {
|
|
63
|
+
"boolean": "bool",
|
|
64
|
+
"string": "str",
|
|
65
|
+
}
|
|
66
|
+
if schema.type in primitive_type_map:
|
|
67
|
+
return primitive_type_map[schema.type]
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def _resolve_string_format(self, format_value: str) -> str:
|
|
72
|
+
"""Resolve string type with specific format to Python type."""
|
|
73
|
+
# Date/time formats
|
|
74
|
+
if format_value == "date-time":
|
|
75
|
+
self.context.add_import("datetime", "datetime")
|
|
76
|
+
return "datetime"
|
|
77
|
+
if format_value == "date":
|
|
78
|
+
self.context.add_import("datetime", "date")
|
|
79
|
+
return "date"
|
|
80
|
+
if format_value == "time":
|
|
81
|
+
self.context.add_import("datetime", "time")
|
|
82
|
+
return "time"
|
|
83
|
+
if format_value == "duration":
|
|
84
|
+
self.context.add_import("datetime", "timedelta")
|
|
85
|
+
return "timedelta"
|
|
86
|
+
|
|
87
|
+
# UUID format
|
|
88
|
+
if format_value == "uuid":
|
|
89
|
+
self.context.add_import("uuid", "UUID")
|
|
90
|
+
return "UUID"
|
|
91
|
+
|
|
92
|
+
# Binary formats
|
|
93
|
+
if format_value in ("binary", "byte"):
|
|
94
|
+
return "bytes"
|
|
95
|
+
|
|
96
|
+
# IP address formats
|
|
97
|
+
if format_value == "ipv4":
|
|
98
|
+
self.context.add_import("ipaddress", "IPv4Address")
|
|
99
|
+
return "IPv4Address"
|
|
100
|
+
if format_value == "ipv6":
|
|
101
|
+
self.context.add_import("ipaddress", "IPv6Address")
|
|
102
|
+
return "IPv6Address"
|
|
103
|
+
|
|
104
|
+
# String-based formats (no special Python type, just str)
|
|
105
|
+
if format_value in ("uri", "url", "email", "hostname", "password"):
|
|
106
|
+
return "str"
|
|
107
|
+
|
|
108
|
+
# Unknown format - fall back to str
|
|
109
|
+
return "str"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Orchestrates IRSchema to Python type resolution."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pyopenapi_gen import IRSchema
|
|
6
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
7
|
+
from pyopenapi_gen.types.services.type_service import UnifiedTypeService
|
|
8
|
+
|
|
9
|
+
from .array_resolver import ArrayTypeResolver
|
|
10
|
+
from .composition_resolver import CompositionTypeResolver
|
|
11
|
+
from .finalizer import TypeFinalizer
|
|
12
|
+
from .named_resolver import NamedTypeResolver
|
|
13
|
+
from .object_resolver import ObjectTypeResolver
|
|
14
|
+
from .primitive_resolver import PrimitiveTypeResolver
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SchemaTypeResolver:
|
|
20
|
+
"""Orchestrates the resolution of IRSchema to Python type strings."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema]):
|
|
23
|
+
self.context = context
|
|
24
|
+
self.all_schemas = all_schemas
|
|
25
|
+
|
|
26
|
+
# Initialize specialized resolvers, passing self for circular dependencies if needed
|
|
27
|
+
self.primitive_resolver = PrimitiveTypeResolver(context)
|
|
28
|
+
self.named_resolver = NamedTypeResolver(context, all_schemas)
|
|
29
|
+
self.array_resolver = ArrayTypeResolver(context, all_schemas, self)
|
|
30
|
+
self.object_resolver = ObjectTypeResolver(context, all_schemas, self)
|
|
31
|
+
self.composition_resolver = CompositionTypeResolver(context, all_schemas, self)
|
|
32
|
+
self.finalizer = TypeFinalizer(context, self.all_schemas)
|
|
33
|
+
|
|
34
|
+
def resolve(
|
|
35
|
+
self,
|
|
36
|
+
schema: IRSchema,
|
|
37
|
+
required: bool = True,
|
|
38
|
+
resolve_alias_target: bool = False,
|
|
39
|
+
current_schema_context_name: str | None = None,
|
|
40
|
+
) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Determines the Python type string for a given IRSchema.
|
|
43
|
+
Now delegates to the UnifiedTypeService for consistent type resolution.
|
|
44
|
+
"""
|
|
45
|
+
# Delegate to the unified type service for all type resolution
|
|
46
|
+
type_service = UnifiedTypeService(self.all_schemas)
|
|
47
|
+
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))
|