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
pyopenapi_gen/ir.py
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
5
|
+
|
6
|
+
# Import NameSanitizer at the top for type hints and __post_init__ usage
|
7
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
8
|
+
|
9
|
+
# Import HTTPMethod as it's used by IROperation
|
10
|
+
from .http_types import HTTPMethod
|
11
|
+
|
12
|
+
# Forward declaration for IRSchema itself if needed for self-references in type hints
|
13
|
+
# class IRSchema:
|
14
|
+
# pass
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class IRSchema:
|
19
|
+
name: Optional[str] = None
|
20
|
+
type: Optional[str] = None # E.g., "object", "array", "string", or a reference to another schema name
|
21
|
+
format: Optional[str] = None
|
22
|
+
description: Optional[str] = None
|
23
|
+
required: List[str] = field(default_factory=list)
|
24
|
+
properties: Dict[str, IRSchema] = field(default_factory=dict)
|
25
|
+
items: Optional[IRSchema] = None # For type: "array"
|
26
|
+
enum: Optional[List[Any]] = None
|
27
|
+
default: Optional[Any] = None # Added default value
|
28
|
+
example: Optional[Any] = None # Added example value
|
29
|
+
additional_properties: Optional[Union[bool, IRSchema]] = None # True, False, or an IRSchema
|
30
|
+
is_nullable: bool = False
|
31
|
+
any_of: Optional[List[IRSchema]] = None
|
32
|
+
one_of: Optional[List[IRSchema]] = None
|
33
|
+
all_of: Optional[List[IRSchema]] = None # Store the list of IRSchema objects from allOf
|
34
|
+
title: Optional[str] = None # Added title
|
35
|
+
is_data_wrapper: bool = False # True if schema is a simple {{ "data": OtherSchema }} wrapper
|
36
|
+
|
37
|
+
# Internal generator flags/helpers
|
38
|
+
_from_unresolved_ref: bool = field(
|
39
|
+
default=False, repr=False
|
40
|
+
) # If this IRSchema is a placeholder for an unresolvable $ref
|
41
|
+
_refers_to_schema: Optional[IRSchema] = (
|
42
|
+
None # If this schema is a reference (e.g. a promoted property), this can link to the actual definition
|
43
|
+
)
|
44
|
+
_is_circular_ref: bool = field(default=False, repr=False) # If this IRSchema is part of a circular reference chain
|
45
|
+
_circular_ref_path: Optional[str] = field(default=None, repr=False) # Path of the circular reference
|
46
|
+
_max_depth_exceeded_marker: bool = field(
|
47
|
+
default=False, repr=False
|
48
|
+
) # If parsing this schema or its components exceeded max depth
|
49
|
+
_is_self_referential_stub: bool = field(default=False, repr=False) # If this is a placeholder for allowed self-ref
|
50
|
+
_is_name_derived: bool = field(
|
51
|
+
default=False, repr=False
|
52
|
+
) # True if the name was derived (e.g. for promoted inline objects)
|
53
|
+
_inline_name_resolution_path: Optional[str] = field(
|
54
|
+
default=None, repr=False
|
55
|
+
) # Path used for resolving inline names
|
56
|
+
|
57
|
+
# Fields for storing final, de-collided names for code generation
|
58
|
+
generation_name: Optional[str] = field(default=None, repr=True) # Final class/enum name
|
59
|
+
final_module_stem: Optional[str] = field(default=None, repr=True) # Final module filename stem
|
60
|
+
|
61
|
+
def __post_init__(self) -> None:
|
62
|
+
# Ensure name is always a valid Python identifier if set
|
63
|
+
# This must happen BEFORE type inference that might use the name (though current logic doesn't)
|
64
|
+
if self.name:
|
65
|
+
# Store original name if needed for specific logic before sanitization, though not currently used here.
|
66
|
+
# original_name = self.name
|
67
|
+
self.name = NameSanitizer.sanitize_class_name(self.name)
|
68
|
+
|
69
|
+
# Ensure that if type is a reference (string not matching basic types),
|
70
|
+
# other structural fields like properties/items/enum are usually None or empty.
|
71
|
+
basic_types = ["object", "array", "string", "integer", "number", "boolean", "null"]
|
72
|
+
if self.type and self.type not in basic_types:
|
73
|
+
# This schema acts as a reference by name to another schema.
|
74
|
+
# It shouldn't typically define its own structure beyond description/nullability.
|
75
|
+
pass
|
76
|
+
|
77
|
+
# The check for is_valid_python_identifier is somewhat redundant if sanitize_class_name works correctly,
|
78
|
+
# but can be kept as a safeguard or for logging if a raw name was problematic *before* sanitization.
|
79
|
+
if self.name and not NameSanitizer.is_valid_python_identifier(self.name):
|
80
|
+
pass # logger.warning or handle as needed elsewhere
|
81
|
+
|
82
|
+
# Ensure nested schemas are IRSchema instances
|
83
|
+
if isinstance(self.items, dict):
|
84
|
+
self.items = IRSchema(**self.items)
|
85
|
+
|
86
|
+
if isinstance(self.properties, dict):
|
87
|
+
new_props = {}
|
88
|
+
for k, v in self.properties.items():
|
89
|
+
if isinstance(v, dict):
|
90
|
+
new_props[k] = IRSchema(**v)
|
91
|
+
elif isinstance(v, IRSchema): # Already an IRSchema instance
|
92
|
+
new_props[k] = v
|
93
|
+
# else: it might be some other unexpected type, raise error or log
|
94
|
+
self.properties = new_props
|
95
|
+
|
96
|
+
if isinstance(self.additional_properties, dict):
|
97
|
+
self.additional_properties = IRSchema(**self.additional_properties)
|
98
|
+
|
99
|
+
for comp_list_attr in ["any_of", "one_of", "all_of"]:
|
100
|
+
comp_list = getattr(self, comp_list_attr)
|
101
|
+
if isinstance(comp_list, list):
|
102
|
+
new_comp_list = []
|
103
|
+
for item in comp_list:
|
104
|
+
if isinstance(item, dict):
|
105
|
+
new_comp_list.append(IRSchema(**item))
|
106
|
+
elif isinstance(item, IRSchema):
|
107
|
+
new_comp_list.append(item)
|
108
|
+
# else: item is some other type, could skip or raise
|
109
|
+
setattr(self, comp_list_attr, new_comp_list)
|
110
|
+
|
111
|
+
|
112
|
+
# NameSanitizer is now imported at the top
|
113
|
+
# from pyopenapi_gen.core.utils import NameSanitizer
|
114
|
+
|
115
|
+
|
116
|
+
@dataclass(slots=True)
|
117
|
+
class IRParameter:
|
118
|
+
name: str
|
119
|
+
param_in: str # Renamed from 'in' to avoid keyword clash, was in_: str in original __init__.py
|
120
|
+
required: bool
|
121
|
+
schema: IRSchema
|
122
|
+
description: Optional[str] = None
|
123
|
+
# example: Optional[Any] = None # This was in my latest ir.py but not __init__.py, keeping it from my version
|
124
|
+
|
125
|
+
|
126
|
+
# Adding other IR classes from the original __init__.py structure
|
127
|
+
@dataclass(slots=True)
|
128
|
+
class IRResponse:
|
129
|
+
status_code: str # can be "default" or specific status like "200"
|
130
|
+
description: Optional[str]
|
131
|
+
content: Dict[str, IRSchema] # media‑type → schema mapping
|
132
|
+
stream: bool = False # Indicates a binary or streaming response
|
133
|
+
stream_format: Optional[str] = None # Indicates the stream type
|
134
|
+
|
135
|
+
|
136
|
+
@dataclass(slots=True)
|
137
|
+
class IRRequestBody:
|
138
|
+
required: bool
|
139
|
+
content: Dict[str, IRSchema] # media‑type → schema mapping
|
140
|
+
description: Optional[str] = None
|
141
|
+
|
142
|
+
|
143
|
+
@dataclass(slots=True)
|
144
|
+
class IROperation:
|
145
|
+
operation_id: str
|
146
|
+
method: HTTPMethod # Enforced via enum for consistency
|
147
|
+
path: str # e.g. "/pets/{petId}"
|
148
|
+
summary: Optional[str]
|
149
|
+
description: Optional[str]
|
150
|
+
parameters: List[IRParameter] = field(default_factory=list)
|
151
|
+
request_body: Optional[IRRequestBody] = None
|
152
|
+
responses: List[IRResponse] = field(default_factory=list)
|
153
|
+
tags: List[str] = field(default_factory=list)
|
154
|
+
|
155
|
+
|
156
|
+
@dataclass(slots=True)
|
157
|
+
class IRSpec:
|
158
|
+
title: str
|
159
|
+
version: str
|
160
|
+
description: Optional[str] = None
|
161
|
+
schemas: Dict[str, IRSchema] = field(default_factory=dict)
|
162
|
+
operations: List[IROperation] = field(default_factory=list)
|
163
|
+
servers: List[str] = field(default_factory=list)
|
164
|
+
|
165
|
+
# self._raw_schema_node = None
|
166
|
+
|
167
|
+
# def __setattr__(self, name, value):
|
pyopenapi_gen/py.typed
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""
|
2
|
+
Unified type resolution service for OpenAPI schema to Python type conversion.
|
3
|
+
|
4
|
+
This package provides a clean, testable architecture for resolving OpenAPI schemas,
|
5
|
+
references, and operation responses to Python type strings.
|
6
|
+
|
7
|
+
Architecture:
|
8
|
+
- contracts/: Interfaces and protocols
|
9
|
+
- resolvers/: Core resolution logic
|
10
|
+
- services/: High-level orchestration
|
11
|
+
"""
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""Type resolution contracts and interfaces."""
|
2
|
+
|
3
|
+
from .protocols import ReferenceResolver, ResponseTypeResolver, SchemaTypeResolver, TypeContext
|
4
|
+
from .types import ResolvedType, TypeResolutionError
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"ReferenceResolver",
|
8
|
+
"SchemaTypeResolver",
|
9
|
+
"ResponseTypeResolver",
|
10
|
+
"TypeContext",
|
11
|
+
"ResolvedType",
|
12
|
+
"TypeResolutionError",
|
13
|
+
]
|
@@ -0,0 +1,106 @@
|
|
1
|
+
"""Protocols for type resolution components."""
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from typing import Dict, Optional, Protocol, runtime_checkable
|
5
|
+
|
6
|
+
from pyopenapi_gen import IROperation, IRResponse, IRSchema
|
7
|
+
|
8
|
+
from .types import ResolvedType
|
9
|
+
|
10
|
+
|
11
|
+
@runtime_checkable
|
12
|
+
class TypeContext(Protocol):
|
13
|
+
"""Context for type resolution operations."""
|
14
|
+
|
15
|
+
def add_import(self, module: str, name: str) -> None:
|
16
|
+
"""Add an import to the context."""
|
17
|
+
...
|
18
|
+
|
19
|
+
def add_conditional_import(self, condition: str, module: str, name: str) -> None:
|
20
|
+
"""Add a conditional import (e.g., TYPE_CHECKING)."""
|
21
|
+
...
|
22
|
+
|
23
|
+
|
24
|
+
class ReferenceResolver(ABC):
|
25
|
+
"""Resolves OpenAPI $ref references to target schemas."""
|
26
|
+
|
27
|
+
# Concrete implementations should have these attributes
|
28
|
+
schemas: Dict[str, IRSchema]
|
29
|
+
responses: Dict[str, IRResponse]
|
30
|
+
|
31
|
+
@abstractmethod
|
32
|
+
def resolve_ref(self, ref: str) -> Optional[IRSchema]:
|
33
|
+
"""
|
34
|
+
Resolve a $ref string to the target schema.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
ref: Reference string like "#/components/schemas/User"
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Target schema or None if not found
|
41
|
+
"""
|
42
|
+
pass
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
def resolve_response_ref(self, ref: str) -> Optional[IRResponse]:
|
46
|
+
"""
|
47
|
+
Resolve a response $ref to the target response.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
ref: Reference string like "#/components/responses/UserResponse"
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Target response or None if not found
|
54
|
+
"""
|
55
|
+
pass
|
56
|
+
|
57
|
+
|
58
|
+
class SchemaTypeResolver(ABC):
|
59
|
+
"""Resolves IRSchema objects to Python types."""
|
60
|
+
|
61
|
+
@abstractmethod
|
62
|
+
def resolve_schema(self, schema: IRSchema, context: TypeContext, required: bool = True) -> ResolvedType:
|
63
|
+
"""
|
64
|
+
Resolve a schema to a Python type.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
schema: The schema to resolve
|
68
|
+
context: Type resolution context
|
69
|
+
required: Whether the field is required
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Resolved Python type information
|
73
|
+
"""
|
74
|
+
pass
|
75
|
+
|
76
|
+
|
77
|
+
class ResponseTypeResolver(ABC):
|
78
|
+
"""Resolves operation responses to Python types."""
|
79
|
+
|
80
|
+
@abstractmethod
|
81
|
+
def resolve_operation_response(self, operation: IROperation, context: TypeContext) -> ResolvedType:
|
82
|
+
"""
|
83
|
+
Resolve an operation's primary response to a Python type.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
operation: The operation to resolve
|
87
|
+
context: Type resolution context
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
Resolved Python type information
|
91
|
+
"""
|
92
|
+
pass
|
93
|
+
|
94
|
+
@abstractmethod
|
95
|
+
def resolve_specific_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
|
96
|
+
"""
|
97
|
+
Resolve a specific response to a Python type.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
response: The response to resolve
|
101
|
+
context: Type resolution context
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Resolved Python type information
|
105
|
+
"""
|
106
|
+
pass
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""Core types for type resolution."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
|
7
|
+
class TypeResolutionError(Exception):
|
8
|
+
"""Raised when type resolution fails."""
|
9
|
+
|
10
|
+
pass
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class ResolvedType:
|
15
|
+
"""Result of type resolution."""
|
16
|
+
|
17
|
+
python_type: str
|
18
|
+
needs_import: bool = False
|
19
|
+
import_module: Optional[str] = None
|
20
|
+
import_name: Optional[str] = None
|
21
|
+
is_optional: bool = False
|
22
|
+
is_forward_ref: bool = False
|
23
|
+
was_unwrapped: bool = False
|
24
|
+
|
25
|
+
def __post_init__(self) -> None:
|
26
|
+
"""Validate resolved type data."""
|
27
|
+
if self.needs_import and not self.import_module:
|
28
|
+
raise ValueError("needs_import=True requires import_module")
|
29
|
+
if self.needs_import and not self.import_name:
|
30
|
+
raise ValueError("needs_import=True requires import_name")
|
@@ -0,0 +1,7 @@
|
|
1
|
+
"""Type resolution implementations."""
|
2
|
+
|
3
|
+
from .reference_resolver import OpenAPIReferenceResolver
|
4
|
+
from .response_resolver import OpenAPIResponseResolver
|
5
|
+
from .schema_resolver import OpenAPISchemaResolver
|
6
|
+
|
7
|
+
__all__ = ["OpenAPIReferenceResolver", "OpenAPISchemaResolver", "OpenAPIResponseResolver"]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""Reference resolver implementation."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Dict, Optional
|
5
|
+
|
6
|
+
from pyopenapi_gen import IRResponse, IRSchema
|
7
|
+
|
8
|
+
from ..contracts.protocols import ReferenceResolver
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class OpenAPIReferenceResolver(ReferenceResolver):
|
14
|
+
"""Resolves OpenAPI $ref references."""
|
15
|
+
|
16
|
+
def __init__(self, schemas: Dict[str, IRSchema], responses: Optional[Dict[str, IRResponse]] = None):
|
17
|
+
"""
|
18
|
+
Initialize reference resolver.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
schemas: Dictionary of schemas by name
|
22
|
+
responses: Dictionary of responses by name (optional)
|
23
|
+
"""
|
24
|
+
self.schemas = schemas
|
25
|
+
self.responses = responses or {}
|
26
|
+
|
27
|
+
def resolve_ref(self, ref: str) -> Optional[IRSchema]:
|
28
|
+
"""
|
29
|
+
Resolve a schema $ref to the target schema.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
ref: Reference string like "#/components/schemas/User"
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Target schema or None if not found
|
36
|
+
"""
|
37
|
+
if not ref.startswith("#/components/schemas/"):
|
38
|
+
logger.warning(f"Unsupported schema ref format: {ref}")
|
39
|
+
return None
|
40
|
+
|
41
|
+
schema_name = ref.split("/")[-1]
|
42
|
+
schema = self.schemas.get(schema_name)
|
43
|
+
|
44
|
+
if not schema:
|
45
|
+
logger.warning(f"Schema not found for ref: {ref}")
|
46
|
+
return None
|
47
|
+
|
48
|
+
return schema
|
49
|
+
|
50
|
+
def resolve_response_ref(self, ref: str) -> Optional[IRResponse]:
|
51
|
+
"""
|
52
|
+
Resolve a response $ref to the target response.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
ref: Reference string like "#/components/responses/UserResponse"
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
Target response or None if not found
|
59
|
+
"""
|
60
|
+
if not ref.startswith("#/components/responses/"):
|
61
|
+
logger.warning(f"Unsupported response ref format: {ref}")
|
62
|
+
return None
|
63
|
+
|
64
|
+
response_name = ref.split("/")[-1]
|
65
|
+
response = self.responses.get(response_name)
|
66
|
+
|
67
|
+
if not response:
|
68
|
+
logger.warning(f"Response not found for ref: {ref}")
|
69
|
+
return None
|
70
|
+
|
71
|
+
return response
|
@@ -0,0 +1,203 @@
|
|
1
|
+
"""Response type resolver implementation."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from pyopenapi_gen import IROperation, IRResponse, IRSchema
|
7
|
+
|
8
|
+
from ..contracts.protocols import ReferenceResolver, ResponseTypeResolver, SchemaTypeResolver, TypeContext
|
9
|
+
from ..contracts.types import ResolvedType
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class OpenAPIResponseResolver(ResponseTypeResolver):
|
15
|
+
"""Resolves operation responses to Python types."""
|
16
|
+
|
17
|
+
def __init__(self, ref_resolver: ReferenceResolver, schema_resolver: SchemaTypeResolver):
|
18
|
+
"""
|
19
|
+
Initialize response resolver.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
ref_resolver: Reference resolver for handling $ref
|
23
|
+
schema_resolver: Schema resolver for handling schemas
|
24
|
+
"""
|
25
|
+
self.ref_resolver = ref_resolver
|
26
|
+
self.schema_resolver = schema_resolver
|
27
|
+
|
28
|
+
def resolve_operation_response(self, operation: IROperation, context: TypeContext) -> ResolvedType:
|
29
|
+
"""
|
30
|
+
Resolve an operation's primary response to a Python type.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
operation: The operation to resolve
|
34
|
+
context: Type resolution context
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Resolved Python type information
|
38
|
+
"""
|
39
|
+
primary_response = self._get_primary_response(operation)
|
40
|
+
|
41
|
+
if not primary_response:
|
42
|
+
return ResolvedType(python_type="None")
|
43
|
+
|
44
|
+
return self.resolve_specific_response(primary_response, context)
|
45
|
+
|
46
|
+
def resolve_specific_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
|
47
|
+
"""
|
48
|
+
Resolve a specific response to a Python type.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
response: The response to resolve
|
52
|
+
context: Type resolution context
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Resolved Python type information
|
56
|
+
"""
|
57
|
+
# Handle response references
|
58
|
+
if hasattr(response, "ref") and response.ref:
|
59
|
+
return self._resolve_response_reference(response.ref, context)
|
60
|
+
|
61
|
+
# Handle responses without content (e.g., 204)
|
62
|
+
if not hasattr(response, "content") or not response.content:
|
63
|
+
return ResolvedType(python_type="None")
|
64
|
+
|
65
|
+
# Handle streaming responses
|
66
|
+
if hasattr(response, "stream") and response.stream:
|
67
|
+
return self._resolve_streaming_response(response, context)
|
68
|
+
|
69
|
+
# Get the content schema
|
70
|
+
schema = self._get_response_schema(response)
|
71
|
+
if not schema:
|
72
|
+
return ResolvedType(python_type="None")
|
73
|
+
|
74
|
+
# Resolve the schema
|
75
|
+
resolved = self.schema_resolver.resolve_schema(schema, context, required=True)
|
76
|
+
|
77
|
+
# Check for data unwrapping
|
78
|
+
unwrapped = self._try_unwrap_data_property(schema, context)
|
79
|
+
if unwrapped:
|
80
|
+
unwrapped.was_unwrapped = True
|
81
|
+
return unwrapped
|
82
|
+
|
83
|
+
return resolved
|
84
|
+
|
85
|
+
def _resolve_response_reference(self, ref: str, context: TypeContext) -> ResolvedType:
|
86
|
+
"""Resolve a response $ref."""
|
87
|
+
target_response = self.ref_resolver.resolve_response_ref(ref)
|
88
|
+
if not target_response:
|
89
|
+
logger.warning(f"Could not resolve response reference: {ref}")
|
90
|
+
return ResolvedType(python_type="None")
|
91
|
+
|
92
|
+
return self.resolve_specific_response(target_response, context)
|
93
|
+
|
94
|
+
def _get_primary_response(self, operation: IROperation) -> Optional[IRResponse]:
|
95
|
+
"""Get the primary success response from an operation."""
|
96
|
+
if not operation.responses:
|
97
|
+
return None
|
98
|
+
|
99
|
+
# Priority order: 200, 201, 202, 204, other 2xx, default
|
100
|
+
for code in ["200", "201", "202", "204"]:
|
101
|
+
for response in operation.responses:
|
102
|
+
if response.status_code == code:
|
103
|
+
return response
|
104
|
+
|
105
|
+
# Other 2xx responses
|
106
|
+
for response in operation.responses:
|
107
|
+
if response.status_code.startswith("2"):
|
108
|
+
return response
|
109
|
+
|
110
|
+
# Default response
|
111
|
+
for response in operation.responses:
|
112
|
+
if response.status_code == "default":
|
113
|
+
return response
|
114
|
+
|
115
|
+
# First response as fallback
|
116
|
+
return operation.responses[0] if operation.responses else None
|
117
|
+
|
118
|
+
def _get_response_schema(self, response: IRResponse) -> IRSchema | None:
|
119
|
+
"""Get the schema from a response's content."""
|
120
|
+
if not response.content:
|
121
|
+
return None
|
122
|
+
|
123
|
+
# Prefer application/json
|
124
|
+
content_types = list(response.content.keys())
|
125
|
+
content_type = None
|
126
|
+
|
127
|
+
if "application/json" in content_types:
|
128
|
+
content_type = "application/json"
|
129
|
+
elif any("json" in ct for ct in content_types):
|
130
|
+
content_type = next(ct for ct in content_types if "json" in ct)
|
131
|
+
elif content_types:
|
132
|
+
content_type = content_types[0]
|
133
|
+
|
134
|
+
if not content_type:
|
135
|
+
return None
|
136
|
+
|
137
|
+
return response.content.get(content_type)
|
138
|
+
|
139
|
+
def _try_unwrap_data_property(self, schema: IRSchema | None, context: TypeContext) -> Optional[ResolvedType]:
|
140
|
+
"""
|
141
|
+
Try to unwrap a 'data' property if the schema is a wrapper.
|
142
|
+
|
143
|
+
Returns unwrapped type or None if not applicable.
|
144
|
+
"""
|
145
|
+
if not schema or not hasattr(schema, "type") or schema.type != "object":
|
146
|
+
return None
|
147
|
+
|
148
|
+
properties = getattr(schema, "properties", None)
|
149
|
+
if not properties or len(properties) != 1:
|
150
|
+
return None
|
151
|
+
|
152
|
+
# Check for 'data' property
|
153
|
+
data_property = properties.get("data")
|
154
|
+
if not data_property:
|
155
|
+
return None
|
156
|
+
|
157
|
+
logger.info(f"Unwrapping 'data' property from response schema")
|
158
|
+
return self.schema_resolver.resolve_schema(data_property, context, required=True)
|
159
|
+
|
160
|
+
def _resolve_streaming_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
|
161
|
+
"""
|
162
|
+
Resolve a streaming response to an AsyncIterator type.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
response: The streaming response
|
166
|
+
context: Type resolution context
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
ResolvedType with AsyncIterator type
|
170
|
+
"""
|
171
|
+
# Add AsyncIterator import
|
172
|
+
context.add_import("typing", "AsyncIterator")
|
173
|
+
|
174
|
+
# Determine the item type for the stream
|
175
|
+
if not response.content:
|
176
|
+
# Binary stream with no specific content type
|
177
|
+
return ResolvedType(python_type="AsyncIterator[bytes]")
|
178
|
+
|
179
|
+
# Check for binary content types
|
180
|
+
content_types = list(response.content.keys())
|
181
|
+
is_binary = any(
|
182
|
+
ct in ["application/octet-stream", "application/pdf"] or ct.startswith(("image/", "audio/", "video/"))
|
183
|
+
for ct in content_types
|
184
|
+
)
|
185
|
+
|
186
|
+
if is_binary:
|
187
|
+
return ResolvedType(python_type="AsyncIterator[bytes]")
|
188
|
+
|
189
|
+
# For event streams (text/event-stream) or JSON streams
|
190
|
+
is_event_stream = any("event-stream" in ct for ct in content_types)
|
191
|
+
if is_event_stream:
|
192
|
+
context.add_import("typing", "Dict")
|
193
|
+
context.add_import("typing", "Any")
|
194
|
+
return ResolvedType(python_type="AsyncIterator[Dict[str, Any]]")
|
195
|
+
|
196
|
+
# For other streaming content, try to resolve the schema
|
197
|
+
schema = self._get_response_schema(response)
|
198
|
+
if schema:
|
199
|
+
resolved = self.schema_resolver.resolve_schema(schema, context, required=True)
|
200
|
+
return ResolvedType(python_type=f"AsyncIterator[{resolved.python_type}]")
|
201
|
+
|
202
|
+
# Default to bytes if we can't determine the type
|
203
|
+
return ResolvedType(python_type="AsyncIterator[bytes]")
|