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,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generator for creating mock method implementations.
|
|
3
|
+
|
|
4
|
+
This module generates mock methods that raise NotImplementedError,
|
|
5
|
+
allowing users to create test doubles by subclassing and overriding
|
|
6
|
+
only the methods they need.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ....context.render_context import RenderContext
|
|
12
|
+
from ....core.utils import NameSanitizer
|
|
13
|
+
from ....core.writers.code_writer import CodeWriter
|
|
14
|
+
from ....ir import IROperation
|
|
15
|
+
from .endpoint_method_generator import EndpointMethodGenerator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MockGenerator:
|
|
19
|
+
"""
|
|
20
|
+
Generates mock method implementations for testing.
|
|
21
|
+
|
|
22
|
+
Mock methods preserve the exact signature of the real implementation
|
|
23
|
+
but raise NotImplementedError with helpful error messages instead
|
|
24
|
+
of performing actual operations.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
|
28
|
+
self.schemas = schemas or {}
|
|
29
|
+
self.method_generator = EndpointMethodGenerator(self.schemas)
|
|
30
|
+
|
|
31
|
+
def generate(self, op: IROperation, context: RenderContext) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Generate a mock method that raises NotImplementedError.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
op: The operation to generate a mock for
|
|
37
|
+
context: Render context for import tracking
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Complete mock method code as string
|
|
41
|
+
"""
|
|
42
|
+
# Generate the full method using EndpointMethodGenerator
|
|
43
|
+
full_method = self.method_generator.generate(op, context)
|
|
44
|
+
|
|
45
|
+
# Parse and transform it to a mock implementation
|
|
46
|
+
return self._transform_to_mock(full_method, op)
|
|
47
|
+
|
|
48
|
+
def _transform_to_mock(self, full_method_code: str, op: IROperation) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Transform a full method implementation into a mock that raises NotImplementedError.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
full_method_code: Complete method code from EndpointMethodGenerator
|
|
54
|
+
op: The operation (for generating error messages)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Mock method code with NotImplementedError body
|
|
58
|
+
"""
|
|
59
|
+
lines = full_method_code.split("\n")
|
|
60
|
+
writer = CodeWriter()
|
|
61
|
+
|
|
62
|
+
i = 0
|
|
63
|
+
while i < len(lines):
|
|
64
|
+
line = lines[i]
|
|
65
|
+
stripped = line.strip()
|
|
66
|
+
|
|
67
|
+
# Handle @overload decorator - keep it
|
|
68
|
+
if stripped.startswith("@overload"):
|
|
69
|
+
writer.write_line(stripped)
|
|
70
|
+
i += 1
|
|
71
|
+
|
|
72
|
+
# Copy overload signature until we hit `: ...`
|
|
73
|
+
while i < len(lines):
|
|
74
|
+
sig_line = lines[i]
|
|
75
|
+
sig_stripped = sig_line.strip()
|
|
76
|
+
writer.write_line(sig_stripped)
|
|
77
|
+
|
|
78
|
+
if sig_stripped.endswith(": ..."):
|
|
79
|
+
writer.write_line("") # Blank line after overload
|
|
80
|
+
i += 1
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
i += 1
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Handle method definition (async def or def)
|
|
87
|
+
if (stripped.startswith("async def ") or stripped.startswith("def ")) and "(" in stripped:
|
|
88
|
+
# Determine if this is an async generator
|
|
89
|
+
is_async_generator = False
|
|
90
|
+
|
|
91
|
+
# Collect signature lines to check return type
|
|
92
|
+
signature_lines = []
|
|
93
|
+
temp_i = i
|
|
94
|
+
while temp_i < len(lines):
|
|
95
|
+
sig_stripped = lines[temp_i].strip()
|
|
96
|
+
signature_lines.append(sig_stripped)
|
|
97
|
+
if sig_stripped.endswith(":") and not sig_stripped.endswith(","):
|
|
98
|
+
# Check if AsyncIterator in return type
|
|
99
|
+
full_sig = " ".join(signature_lines)
|
|
100
|
+
is_async_generator = "AsyncIterator" in full_sig
|
|
101
|
+
break
|
|
102
|
+
temp_i += 1
|
|
103
|
+
|
|
104
|
+
# Write signature lines
|
|
105
|
+
for sig in signature_lines:
|
|
106
|
+
writer.write_line(sig)
|
|
107
|
+
|
|
108
|
+
# Write mock body
|
|
109
|
+
writer.indent()
|
|
110
|
+
|
|
111
|
+
# Docstring
|
|
112
|
+
writer.write_line('"""')
|
|
113
|
+
writer.write_line("Mock implementation that raises NotImplementedError.")
|
|
114
|
+
writer.write_line("")
|
|
115
|
+
writer.write_line("Override this method in your test subclass to provide")
|
|
116
|
+
writer.write_line("the behavior needed for your test scenario.")
|
|
117
|
+
writer.write_line('"""')
|
|
118
|
+
|
|
119
|
+
# Error message
|
|
120
|
+
method_name = NameSanitizer.sanitize_method_name(op.operation_id)
|
|
121
|
+
tag = op.tags[0] if op.tags else "Client"
|
|
122
|
+
class_name = f"Mock{NameSanitizer.sanitize_class_name(tag)}Client"
|
|
123
|
+
error_msg = (
|
|
124
|
+
f'"{class_name}.{method_name}() not implemented. ' f'Override this method in your test subclass."'
|
|
125
|
+
)
|
|
126
|
+
writer.write_line(f"raise NotImplementedError({error_msg})")
|
|
127
|
+
|
|
128
|
+
# For async generators, add unreachable yield for type checker
|
|
129
|
+
if is_async_generator:
|
|
130
|
+
writer.write_line("yield # pragma: no cover")
|
|
131
|
+
|
|
132
|
+
writer.dedent()
|
|
133
|
+
|
|
134
|
+
# Skip the rest of this method implementation in the original code
|
|
135
|
+
i = len(lines) # Exit the loop
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
i += 1
|
|
139
|
+
|
|
140
|
+
return writer.get_code()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Generator for @overload signatures when operations have multiple content types."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyopenapi_gen import IROperation
|
|
7
|
+
|
|
8
|
+
from ....context.render_context import RenderContext
|
|
9
|
+
from ....core.utils import NameSanitizer
|
|
10
|
+
from ....core.writers.code_writer import CodeWriter
|
|
11
|
+
from ..processors.parameter_processor import EndpointParameterProcessor
|
|
12
|
+
from .docstring_generator import EndpointDocstringGenerator
|
|
13
|
+
from .signature_generator import EndpointMethodSignatureGenerator
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OverloadMethodGenerator:
|
|
19
|
+
"""
|
|
20
|
+
Generates @overload signatures for operations with multiple content types.
|
|
21
|
+
|
|
22
|
+
When an operation's request body accepts multiple content types
|
|
23
|
+
(e.g., application/json and multipart/form-data), this generator creates
|
|
24
|
+
type-safe @overload signatures following PEP 484.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
|
28
|
+
self.schemas = schemas or {}
|
|
29
|
+
self.parameter_processor = EndpointParameterProcessor(self.schemas)
|
|
30
|
+
self.signature_generator = EndpointMethodSignatureGenerator(self.schemas)
|
|
31
|
+
self.docstring_generator = EndpointDocstringGenerator(self.schemas)
|
|
32
|
+
|
|
33
|
+
def has_multiple_content_types(self, op: IROperation) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Check if operation request body has multiple content types.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
op: The operation to check
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if operation has request body with multiple content types
|
|
42
|
+
"""
|
|
43
|
+
if not op.request_body:
|
|
44
|
+
return False
|
|
45
|
+
return len(op.request_body.content) > 1
|
|
46
|
+
|
|
47
|
+
def generate_overload_signatures(
|
|
48
|
+
self, op: IROperation, context: RenderContext, response_strategy: Any
|
|
49
|
+
) -> list[str]:
|
|
50
|
+
"""
|
|
51
|
+
Generate @overload signatures for each content type.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
op: The operation with multiple content types
|
|
55
|
+
context: Render context for import tracking
|
|
56
|
+
response_strategy: Response strategy for return type resolution
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of @overload signature code strings
|
|
60
|
+
"""
|
|
61
|
+
if not self.has_multiple_content_types(op):
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
# Ensure typing.overload is imported
|
|
65
|
+
context.add_import("typing", "overload")
|
|
66
|
+
context.add_import("typing", "Literal")
|
|
67
|
+
context.add_import("typing", "IO")
|
|
68
|
+
context.add_import("typing", "Any")
|
|
69
|
+
|
|
70
|
+
overload_signatures = []
|
|
71
|
+
|
|
72
|
+
# Type narrowing: request_body is guaranteed to exist by has_multiple_content_types check
|
|
73
|
+
assert (
|
|
74
|
+
op.request_body is not None
|
|
75
|
+
), "request_body should not be None after has_multiple_content_types check" # nosec B101 - Type narrowing for mypy, validated by has_multiple_content_types
|
|
76
|
+
|
|
77
|
+
for content_type, schema in op.request_body.content.items():
|
|
78
|
+
signature_code = self._generate_single_overload(op, content_type, schema, context, response_strategy)
|
|
79
|
+
overload_signatures.append(signature_code)
|
|
80
|
+
|
|
81
|
+
return overload_signatures
|
|
82
|
+
|
|
83
|
+
def _generate_single_overload(
|
|
84
|
+
self,
|
|
85
|
+
op: IROperation,
|
|
86
|
+
content_type: str,
|
|
87
|
+
schema: Any,
|
|
88
|
+
context: RenderContext,
|
|
89
|
+
response_strategy: Any,
|
|
90
|
+
) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Generate a single @overload signature for one content type.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
op: The operation
|
|
96
|
+
content_type: The media type (e.g., "application/json")
|
|
97
|
+
schema: The schema for this content type
|
|
98
|
+
context: Render context
|
|
99
|
+
response_strategy: Response strategy for return type
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
@overload signature code as string
|
|
103
|
+
"""
|
|
104
|
+
writer = CodeWriter()
|
|
105
|
+
|
|
106
|
+
# Write @overload decorator
|
|
107
|
+
writer.write_line("@overload")
|
|
108
|
+
|
|
109
|
+
# Determine parameter name and type based on content type
|
|
110
|
+
param_info = self._get_content_type_param_info(content_type, schema, context)
|
|
111
|
+
|
|
112
|
+
# Build parameter list from operation parameters directly
|
|
113
|
+
param_parts = ["self"]
|
|
114
|
+
|
|
115
|
+
# Add path, query, and header parameters from operation
|
|
116
|
+
if op.parameters:
|
|
117
|
+
from ....types.services.type_service import UnifiedTypeService
|
|
118
|
+
|
|
119
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
120
|
+
|
|
121
|
+
for param in op.parameters:
|
|
122
|
+
if param.param_in in ("path", "query", "header"):
|
|
123
|
+
param_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
|
|
124
|
+
sanitized_name = NameSanitizer.sanitize_method_name(param.name)
|
|
125
|
+
param_parts.append(f"{sanitized_name}: {param_type}")
|
|
126
|
+
|
|
127
|
+
# Add keyword-only separator
|
|
128
|
+
param_parts.append("*")
|
|
129
|
+
|
|
130
|
+
# Add content-type-specific parameter
|
|
131
|
+
param_parts.append(f"{param_info['name']}: {param_info['type']}")
|
|
132
|
+
|
|
133
|
+
# Add content_type parameter with Literal type
|
|
134
|
+
param_parts.append(f'content_type: Literal["{content_type}"] = "{content_type}"')
|
|
135
|
+
|
|
136
|
+
# Get return type from response strategy
|
|
137
|
+
return_type = response_strategy.return_type
|
|
138
|
+
|
|
139
|
+
# Sanitize method name to snake_case
|
|
140
|
+
method_name = NameSanitizer.sanitize_method_name(op.operation_id)
|
|
141
|
+
|
|
142
|
+
# Write signature
|
|
143
|
+
params_str = ",\n ".join(param_parts)
|
|
144
|
+
writer.write_line(f"async def {method_name}(")
|
|
145
|
+
writer.indent()
|
|
146
|
+
writer.write_line(params_str)
|
|
147
|
+
writer.dedent()
|
|
148
|
+
writer.write_line(f") -> {return_type}: ...")
|
|
149
|
+
|
|
150
|
+
return writer.get_code()
|
|
151
|
+
|
|
152
|
+
def _get_content_type_param_info(self, content_type: str, schema: Any, context: RenderContext) -> dict[str, str]:
|
|
153
|
+
"""
|
|
154
|
+
Get parameter name and type hint for a content type.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
content_type: Media type string
|
|
158
|
+
schema: Schema for this content type
|
|
159
|
+
context: Render context for type resolution
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary with 'name' and 'type' keys
|
|
163
|
+
"""
|
|
164
|
+
from ....types.services.type_service import UnifiedTypeService
|
|
165
|
+
|
|
166
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
167
|
+
|
|
168
|
+
# Map content types to parameter names and base types
|
|
169
|
+
if content_type == "application/json":
|
|
170
|
+
# For JSON, resolve the actual schema type
|
|
171
|
+
type_hint = type_service.resolve_schema_type(schema, context, required=True)
|
|
172
|
+
return {"name": "body", "type": type_hint}
|
|
173
|
+
|
|
174
|
+
elif content_type == "multipart/form-data":
|
|
175
|
+
# For multipart, always use files dict
|
|
176
|
+
return {"name": "files", "type": "dict[str, IO[Any]]"}
|
|
177
|
+
|
|
178
|
+
elif content_type == "application/x-www-form-urlencoded":
|
|
179
|
+
# For form data, use dict
|
|
180
|
+
return {"name": "data", "type": "dict[str, str]"}
|
|
181
|
+
|
|
182
|
+
else:
|
|
183
|
+
# Default: use body with Any type
|
|
184
|
+
logger.warning(f"Unknown content type {content_type}, using body: Any")
|
|
185
|
+
return {"name": "body", "type": "Any"}
|
|
186
|
+
|
|
187
|
+
def generate_implementation_signature(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Generate the actual implementation method signature with optional parameters.
|
|
190
|
+
|
|
191
|
+
This signature accepts all possible content-type parameters as optional,
|
|
192
|
+
and includes runtime dispatch logic.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
op: The operation
|
|
196
|
+
context: Render context
|
|
197
|
+
response_strategy: Response strategy for return type
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Implementation method signature code
|
|
201
|
+
"""
|
|
202
|
+
writer = CodeWriter()
|
|
203
|
+
|
|
204
|
+
# Build parameter list
|
|
205
|
+
param_parts = ["self"]
|
|
206
|
+
|
|
207
|
+
# Add path, query, and header parameters from operation
|
|
208
|
+
if op.parameters:
|
|
209
|
+
from ....types.services.type_service import UnifiedTypeService
|
|
210
|
+
|
|
211
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
212
|
+
|
|
213
|
+
for param in op.parameters:
|
|
214
|
+
if param.param_in in ("path", "query", "header"):
|
|
215
|
+
param_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
|
|
216
|
+
sanitized_name = NameSanitizer.sanitize_method_name(param.name)
|
|
217
|
+
param_parts.append(f"{sanitized_name}: {param_type}")
|
|
218
|
+
|
|
219
|
+
# Add keyword-only separator
|
|
220
|
+
param_parts.append("*")
|
|
221
|
+
|
|
222
|
+
# Add all possible content-type parameters as optional
|
|
223
|
+
if op.request_body:
|
|
224
|
+
param_types_seen = set()
|
|
225
|
+
|
|
226
|
+
for content_type, schema in op.request_body.content.items():
|
|
227
|
+
param_info = self._get_content_type_param_info(content_type, schema, context)
|
|
228
|
+
|
|
229
|
+
# Avoid duplicate parameters (e.g., if multiple JSON variants)
|
|
230
|
+
param_key = param_info["name"]
|
|
231
|
+
if param_key not in param_types_seen:
|
|
232
|
+
param_parts.append(f"{param_info['name']}: {param_info['type']} | None = None")
|
|
233
|
+
param_types_seen.add(param_key)
|
|
234
|
+
|
|
235
|
+
# Add content_type parameter (no Literal, just str)
|
|
236
|
+
param_parts.append('content_type: str = "application/json"')
|
|
237
|
+
|
|
238
|
+
# Get return type
|
|
239
|
+
return_type = response_strategy.return_type
|
|
240
|
+
|
|
241
|
+
# Sanitize method name to snake_case
|
|
242
|
+
method_name = NameSanitizer.sanitize_method_name(op.operation_id)
|
|
243
|
+
|
|
244
|
+
# Write signature
|
|
245
|
+
params_str = ",\n ".join(param_parts)
|
|
246
|
+
writer.write_line(f"async def {method_name}(")
|
|
247
|
+
writer.indent()
|
|
248
|
+
writer.write_line(params_str)
|
|
249
|
+
writer.dedent()
|
|
250
|
+
writer.write_line(f") -> {return_type}:")
|
|
251
|
+
|
|
252
|
+
return writer.get_code()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper class for generating the HTTP request call for an endpoint method.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
|
11
|
+
|
|
12
|
+
# No specific utils needed yet, but NameSanitizer might be if param names are manipulated here
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pyopenapi_gen import IROperation
|
|
16
|
+
from pyopenapi_gen.context.render_context import RenderContext # For context.add_import if needed
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EndpointRequestGenerator:
|
|
22
|
+
"""Generates the self._transport.request(...) call for an endpoint method."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
|
25
|
+
self.schemas: dict[str, Any] = schemas or {}
|
|
26
|
+
|
|
27
|
+
def generate_request_call(
|
|
28
|
+
self,
|
|
29
|
+
writer: CodeWriter,
|
|
30
|
+
op: IROperation,
|
|
31
|
+
context: RenderContext, # Pass context for potential import needs
|
|
32
|
+
has_header_params: bool,
|
|
33
|
+
primary_content_type: str | None,
|
|
34
|
+
# resolved_body_type: str | None, # May not be directly needed here if logic relies on var names
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Writes the self._transport.request call to the CodeWriter."""
|
|
37
|
+
# Logic from EndpointMethodGenerator._write_request
|
|
38
|
+
args_list = []
|
|
39
|
+
|
|
40
|
+
# Determine if 'params' argument is needed for query parameters
|
|
41
|
+
# This relies on UrlArgsGenerator having created a 'params' dict if query params exist.
|
|
42
|
+
# A more robust way could be to check op.parameters directly, but this keeps coupling loose.
|
|
43
|
+
if any(p.param_in == "query" for p in op.parameters): # Check IROperation directly for query params
|
|
44
|
+
args_list.append("params=params")
|
|
45
|
+
else:
|
|
46
|
+
args_list.append("params=None")
|
|
47
|
+
|
|
48
|
+
# Determine 'json' and 'data' arguments based on request body
|
|
49
|
+
if op.request_body:
|
|
50
|
+
if primary_content_type == "application/json":
|
|
51
|
+
args_list.append("json=json_body") # Assumes json_body is defined
|
|
52
|
+
# args_list.append("data=None") # Not strictly needed if json is present, httpx handles it
|
|
53
|
+
elif primary_content_type and "multipart/form-data" in primary_content_type:
|
|
54
|
+
# For multipart, httpx uses 'files' or 'data' depending on content.
|
|
55
|
+
# UrlArgsGenerator created 'files_data'. Httpx typically uses 'files=' for file uploads.
|
|
56
|
+
# Let's assume 'files_data' is a dict suitable for 'files=' or 'data='
|
|
57
|
+
# If 'files_data' is specifically for file-like objects, 'files=files_data' is better.
|
|
58
|
+
# If it can also contain plain data, 'data=files_data' might be used by httpx.
|
|
59
|
+
# For simplicity and common use with files:
|
|
60
|
+
args_list.append("files=files_data") # Assumes files_data is defined
|
|
61
|
+
elif primary_content_type == "application/x-www-form-urlencoded":
|
|
62
|
+
args_list.append("data=form_data_body") # Assumes form_data_body is defined
|
|
63
|
+
elif primary_content_type: # Other types, like application/octet-stream
|
|
64
|
+
args_list.append("data=bytes_body") # Assumes bytes_body is defined
|
|
65
|
+
# else: # No specific content type handled, might mean no body or unhandled type
|
|
66
|
+
# args_list.append("json=None")
|
|
67
|
+
# args_list.append("data=None")
|
|
68
|
+
else: # No request body
|
|
69
|
+
args_list.append("json=None")
|
|
70
|
+
args_list.append("data=None")
|
|
71
|
+
|
|
72
|
+
# Determine 'headers' argument
|
|
73
|
+
if has_header_params: # This flag comes from UrlArgsGenerator
|
|
74
|
+
args_list.append("headers=headers") # Assumes headers dict is defined
|
|
75
|
+
else:
|
|
76
|
+
args_list.append("headers=None")
|
|
77
|
+
|
|
78
|
+
positional_args_str = f'"{op.method.upper()}", url' # url variable is assumed to be defined
|
|
79
|
+
keyword_args_str = ", ".join(args_list)
|
|
80
|
+
|
|
81
|
+
# Check length for formatting (120 is a common line length limit)
|
|
82
|
+
# Account for "response = await self._transport.request(" and ")" and surrounding spaces/indentation
|
|
83
|
+
# A rough estimate, effective line length for arguments should be less than ~120 - ~40 = 80
|
|
84
|
+
effective_args_len = len(positional_args_str) + len(", ") + len(keyword_args_str)
|
|
85
|
+
|
|
86
|
+
base_call_len = len("response = await self._transport.request()") + 2 # +2 for (,)
|
|
87
|
+
|
|
88
|
+
if base_call_len + effective_args_len <= 100: # Adjusted for typical black formatting preference
|
|
89
|
+
writer.write_line(f"response = await self._transport.request({positional_args_str}, {keyword_args_str})")
|
|
90
|
+
else:
|
|
91
|
+
writer.write_line(f"response = await self._transport.request(")
|
|
92
|
+
writer.indent()
|
|
93
|
+
writer.write_line(f"{positional_args_str},")
|
|
94
|
+
# Filter out "*=None" for cleaner multi-line calls if they are truly None and not just assigned None
|
|
95
|
+
# This might be overly complex here; httpx handles None correctly.
|
|
96
|
+
# Sticking to original logic for now.
|
|
97
|
+
num_args = len(args_list)
|
|
98
|
+
for i, arg in enumerate(args_list):
|
|
99
|
+
line_end = "," if i < num_args - 1 else ""
|
|
100
|
+
writer.write_line(f"{arg}{line_end}")
|
|
101
|
+
writer.dedent()
|
|
102
|
+
writer.write_line(")")
|
|
103
|
+
writer.write_line("") # Add a blank line for readability after the request call
|