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,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"
|
pyopenapi_gen/ir.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, List, 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: str | None = None
|
|
20
|
+
type: str | None = None # E.g., "object", "array", "string", or a reference to another schema name
|
|
21
|
+
format: str | None = None
|
|
22
|
+
description: str | None = None
|
|
23
|
+
required: List[str] = field(default_factory=list)
|
|
24
|
+
properties: dict[str, IRSchema] = field(default_factory=dict)
|
|
25
|
+
items: IRSchema | None = None # For type: "array"
|
|
26
|
+
enum: List[Any] | None = None
|
|
27
|
+
default: Any | None = None # Added default value
|
|
28
|
+
example: Any | None = None # Added example value
|
|
29
|
+
additional_properties: Union[bool, IRSchema] | None = None # True, False, or an IRSchema
|
|
30
|
+
is_nullable: bool = False
|
|
31
|
+
any_of: List[IRSchema] | None = None
|
|
32
|
+
one_of: List[IRSchema] | None = None
|
|
33
|
+
all_of: List[IRSchema] | None = None # Store the list of IRSchema objects from allOf
|
|
34
|
+
title: str | None = 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: IRSchema | None = (
|
|
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: str | None = 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: str | None = field(default=None, repr=False) # Path used for resolving inline names
|
|
54
|
+
|
|
55
|
+
# Fields for storing final, de-collided names for code generation
|
|
56
|
+
generation_name: str | None = field(default=None, repr=True) # Final class/enum name
|
|
57
|
+
final_module_stem: str | None = field(default=None, repr=True) # Final module filename stem
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
# Ensure name is always a valid Python identifier if set
|
|
61
|
+
# This must happen BEFORE type inference that might use the name (though current logic doesn't)
|
|
62
|
+
if self.name:
|
|
63
|
+
# Store original name if needed for specific logic before sanitization, though not currently used here.
|
|
64
|
+
# original_name = self.name
|
|
65
|
+
self.name = NameSanitizer.sanitize_class_name(self.name)
|
|
66
|
+
|
|
67
|
+
# Ensure that if type is a reference (string not matching basic types),
|
|
68
|
+
# other structural fields like properties/items/enum are usually None or empty.
|
|
69
|
+
basic_types = ["object", "array", "string", "integer", "number", "boolean", "null"]
|
|
70
|
+
if self.type and self.type not in basic_types:
|
|
71
|
+
# This schema acts as a reference by name to another schema.
|
|
72
|
+
# It shouldn't typically define its own structure beyond description/nullability.
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# The check for is_valid_python_identifier is somewhat redundant if sanitize_class_name works correctly,
|
|
76
|
+
# but can be kept as a safeguard or for logging if a raw name was problematic *before* sanitization.
|
|
77
|
+
if self.name and not NameSanitizer.is_valid_python_identifier(self.name):
|
|
78
|
+
pass # logger.warning or handle as needed elsewhere
|
|
79
|
+
|
|
80
|
+
# Ensure nested schemas are IRSchema instances
|
|
81
|
+
if isinstance(self.items, dict):
|
|
82
|
+
self.items = IRSchema(**self.items)
|
|
83
|
+
|
|
84
|
+
if isinstance(self.properties, dict):
|
|
85
|
+
new_props = {}
|
|
86
|
+
for k, v in self.properties.items():
|
|
87
|
+
if isinstance(v, dict):
|
|
88
|
+
new_props[k] = IRSchema(**v)
|
|
89
|
+
elif isinstance(v, IRSchema): # Already an IRSchema instance
|
|
90
|
+
new_props[k] = v
|
|
91
|
+
# else: it might be some other unexpected type, raise error or log
|
|
92
|
+
self.properties = new_props
|
|
93
|
+
|
|
94
|
+
if isinstance(self.additional_properties, dict):
|
|
95
|
+
self.additional_properties = IRSchema(**self.additional_properties)
|
|
96
|
+
|
|
97
|
+
for comp_list_attr in ["any_of", "one_of", "all_of"]:
|
|
98
|
+
comp_list = getattr(self, comp_list_attr)
|
|
99
|
+
if isinstance(comp_list, list):
|
|
100
|
+
new_comp_list = []
|
|
101
|
+
for item in comp_list:
|
|
102
|
+
if isinstance(item, dict):
|
|
103
|
+
new_comp_list.append(IRSchema(**item))
|
|
104
|
+
elif isinstance(item, IRSchema):
|
|
105
|
+
new_comp_list.append(item)
|
|
106
|
+
# else: item is some other type, could skip or raise
|
|
107
|
+
setattr(self, comp_list_attr, new_comp_list)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# NameSanitizer is now imported at the top
|
|
111
|
+
# from pyopenapi_gen.core.utils import NameSanitizer
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(slots=True)
|
|
115
|
+
class IRParameter:
|
|
116
|
+
name: str
|
|
117
|
+
param_in: str # Renamed from 'in' to avoid keyword clash, was in_: str in original __init__.py
|
|
118
|
+
required: bool
|
|
119
|
+
schema: IRSchema
|
|
120
|
+
description: str | None = None
|
|
121
|
+
# example: Any | None = None # This was in my latest ir.py but not __init__.py, keeping it from my version
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Adding other IR classes from the original __init__.py structure
|
|
125
|
+
@dataclass(slots=True)
|
|
126
|
+
class IRResponse:
|
|
127
|
+
status_code: str # can be "default" or specific status like "200"
|
|
128
|
+
description: str | None
|
|
129
|
+
content: dict[str, IRSchema] # media‑type → schema mapping
|
|
130
|
+
stream: bool = False # Indicates a binary or streaming response
|
|
131
|
+
stream_format: str | None = None # Indicates the stream type
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(slots=True)
|
|
135
|
+
class IRRequestBody:
|
|
136
|
+
required: bool
|
|
137
|
+
content: dict[str, IRSchema] # media‑type → schema mapping
|
|
138
|
+
description: str | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(slots=True)
|
|
142
|
+
class IROperation:
|
|
143
|
+
operation_id: str
|
|
144
|
+
method: HTTPMethod # Enforced via enum for consistency
|
|
145
|
+
path: str # e.g. "/pets/{petId}"
|
|
146
|
+
summary: str | None
|
|
147
|
+
description: str | None
|
|
148
|
+
parameters: List[IRParameter] = field(default_factory=list)
|
|
149
|
+
request_body: IRRequestBody | None = None
|
|
150
|
+
responses: List[IRResponse] = field(default_factory=list)
|
|
151
|
+
tags: List[str] = field(default_factory=list)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(slots=True)
|
|
155
|
+
class IRSpec:
|
|
156
|
+
title: str
|
|
157
|
+
version: str
|
|
158
|
+
description: str | None = None
|
|
159
|
+
schemas: dict[str, IRSchema] = field(default_factory=dict)
|
|
160
|
+
operations: List[IROperation] = field(default_factory=list)
|
|
161
|
+
servers: List[str] = field(default_factory=list)
|
|
162
|
+
|
|
163
|
+
# self._raw_schema_node = None
|
|
164
|
+
|
|
165
|
+
# def __setattr__(self, name, value):
|
pyopenapi_gen/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# types/ - Unified Type Resolution System
|
|
2
|
+
|
|
3
|
+
## Why This Folder?
|
|
4
|
+
Central, testable type resolution replacing scattered type conversion logic. Single source of truth for OpenAPI → Python type mappings with dependency injection architecture.
|
|
5
|
+
|
|
6
|
+
## Key Dependencies
|
|
7
|
+
- **Input**: `IRSchema`, `IRResponse`, `IROperation` from `../../ir.py`
|
|
8
|
+
- **Output**: Python type strings (`str`, `List[User]`, `Optional[Dict[str, Any]]`)
|
|
9
|
+
- **Context**: `RenderContext` from `../../context/render_context.py`
|
|
10
|
+
|
|
11
|
+
## Essential Patterns
|
|
12
|
+
|
|
13
|
+
### 1. Service → Resolvers → Contracts
|
|
14
|
+
```python
|
|
15
|
+
# Main entry point (services/)
|
|
16
|
+
UnifiedTypeService → SchemaResolver/ResponseResolver → TypeContext protocol
|
|
17
|
+
|
|
18
|
+
# Usage pattern
|
|
19
|
+
type_service = UnifiedTypeService(schemas, responses)
|
|
20
|
+
python_type = type_service.resolve_schema_type(schema, context, required=True)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Protocol-Based Dependency Injection
|
|
24
|
+
```python
|
|
25
|
+
# contracts/protocols.py - Define interfaces
|
|
26
|
+
class TypeContext(Protocol):
|
|
27
|
+
def add_import(self, import_str: str) -> None: ...
|
|
28
|
+
|
|
29
|
+
# resolvers/ - Use protocols, not concrete types
|
|
30
|
+
def resolve_type(schema: IRSchema, context: TypeContext) -> str:
|
|
31
|
+
# Implementation uses context protocol
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. Error Handling
|
|
35
|
+
```python
|
|
36
|
+
from .contracts.types import TypeResolutionError
|
|
37
|
+
|
|
38
|
+
# Always wrap resolution failures
|
|
39
|
+
try:
|
|
40
|
+
return resolve_complex_type(schema)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise TypeResolutionError(f"Failed to resolve {schema.name}: {e}")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Critical Implementation Details
|
|
46
|
+
|
|
47
|
+
### Schema Type Resolution Priority
|
|
48
|
+
1. **Enum**: `schema.enum` → `UserStatusEnum`
|
|
49
|
+
2. **Named Reference**: `schema.type` as schema name → `User`
|
|
50
|
+
3. **Primitive**: `schema.type` → `str`, `int`, `bool`
|
|
51
|
+
4. **Array**: `schema.type="array"` → `List[ItemType]`
|
|
52
|
+
5. **Object**: `schema.type="object"` → `Dict[str, Any]` or dataclass
|
|
53
|
+
6. **Composition**: `allOf`/`oneOf`/`anyOf` → `Union[...]`
|
|
54
|
+
|
|
55
|
+
### Forward Reference Handling
|
|
56
|
+
```python
|
|
57
|
+
# For circular dependencies
|
|
58
|
+
if schema.name in context.forward_refs:
|
|
59
|
+
return f'"{schema.name}"' # String annotation
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Response Unwrapping Logic
|
|
63
|
+
```python
|
|
64
|
+
# Detect wrapper responses with single 'data' field
|
|
65
|
+
if (response.schema.type == "object" and
|
|
66
|
+
"data" in response.schema.properties and
|
|
67
|
+
len(response.schema.properties) == 1):
|
|
68
|
+
return resolve_schema_type(response.schema.properties["data"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Dependencies on Other Systems
|
|
72
|
+
|
|
73
|
+
### From core/
|
|
74
|
+
- `IRSchema`, `IRResponse`, `IROperation` definitions
|
|
75
|
+
- Parsing context for cycle detection state
|
|
76
|
+
|
|
77
|
+
### From context/
|
|
78
|
+
- `RenderContext` for import management and rendering state
|
|
79
|
+
- Import collection and deduplication
|
|
80
|
+
|
|
81
|
+
### From helpers/ (Legacy)
|
|
82
|
+
- `TypeHelper` delegates to `UnifiedTypeService`
|
|
83
|
+
- Maintains backward compatibility during transition
|
|
84
|
+
|
|
85
|
+
## Testing Requirements
|
|
86
|
+
|
|
87
|
+
### Unit Test Pattern
|
|
88
|
+
```python
|
|
89
|
+
def test_resolve_schema_type__string_schema__returns_str():
|
|
90
|
+
# Arrange
|
|
91
|
+
schema = IRSchema(type="string")
|
|
92
|
+
mock_context = Mock(spec=TypeContext)
|
|
93
|
+
resolver = OpenAPISchemaResolver({})
|
|
94
|
+
|
|
95
|
+
# Act
|
|
96
|
+
result = resolver.resolve_type(schema, mock_context)
|
|
97
|
+
|
|
98
|
+
# Assert
|
|
99
|
+
assert result == "str"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Integration Test Pattern
|
|
103
|
+
```python
|
|
104
|
+
def test_type_service__complex_schema__resolves_correctly():
|
|
105
|
+
# Test with real schemas and context
|
|
106
|
+
schemas = {"User": IRSchema(...)}
|
|
107
|
+
responses = {"UserResponse": IRResponse(...)}
|
|
108
|
+
service = UnifiedTypeService(schemas, responses)
|
|
109
|
+
# Test actual resolution
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Common Pitfalls
|
|
113
|
+
|
|
114
|
+
1. **Context Mutation**: Always pass context, never mutate globally
|
|
115
|
+
2. **Missing Imports**: Resolver must call `context.add_import()` for complex types
|
|
116
|
+
3. **Circular Dependencies**: Check `context.forward_refs` before resolution
|
|
117
|
+
4. **Error Swallowing**: Wrap exceptions in `TypeResolutionError`
|
|
118
|
+
|
|
119
|
+
## Extension Points
|
|
120
|
+
|
|
121
|
+
### Adding New Resolvers
|
|
122
|
+
```python
|
|
123
|
+
# Create new resolver implementing protocols
|
|
124
|
+
class CustomResolver:
|
|
125
|
+
def resolve_type(self, schema: IRSchema, context: TypeContext) -> str:
|
|
126
|
+
# Custom logic
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
# Register in UnifiedTypeService
|
|
130
|
+
service.register_resolver(CustomResolver())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### New Response Strategies
|
|
134
|
+
```python
|
|
135
|
+
# strategies/response_strategy.py
|
|
136
|
+
class CustomResponseStrategy:
|
|
137
|
+
def should_unwrap(self, response: IRResponse) -> bool:
|
|
138
|
+
# Custom unwrapping logic
|
|
139
|
+
pass
|
|
140
|
+
```
|
|
@@ -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 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) -> IRSchema | None:
|
|
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) -> IRResponse | None:
|
|
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,28 @@
|
|
|
1
|
+
"""Core types for type resolution."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TypeResolutionError(Exception):
|
|
7
|
+
"""Raised when type resolution fails."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ResolvedType:
|
|
14
|
+
"""Result of type resolution."""
|
|
15
|
+
|
|
16
|
+
python_type: str
|
|
17
|
+
needs_import: bool = False
|
|
18
|
+
import_module: str | None = None
|
|
19
|
+
import_name: str | None = None
|
|
20
|
+
is_optional: bool = False
|
|
21
|
+
is_forward_ref: bool = False
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
"""Validate resolved type data."""
|
|
25
|
+
if self.needs_import and not self.import_module:
|
|
26
|
+
raise ValueError("needs_import=True requires import_module")
|
|
27
|
+
if self.needs_import and not self.import_name:
|
|
28
|
+
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
|
|
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: Dict[str, IRResponse] | None = 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) -> IRSchema | None:
|
|
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) -> IRResponse | None:
|
|
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
|