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.
Files changed (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. 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