jararaca 0.2.37a12__py3-none-any.whl → 0.4.0a5__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.
- README.md +121 -0
- jararaca/__init__.py +267 -15
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +106 -0
- jararaca/broker_backend/mapper.py +25 -0
- jararaca/broker_backend/redis_broker_backend.py +168 -0
- jararaca/cli.py +840 -103
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +55 -16
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +5 -1
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +90 -85
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
- jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
- jararaca/messagebus/message.py +31 -0
- jararaca/messagebus/publisher.py +47 -4
- jararaca/messagebus/worker.py +1615 -135
- jararaca/microservice.py +248 -36
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +177 -16
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +8 -2
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +213 -18
- jararaca/persistence/base.py +40 -3
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +74 -32
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +170 -82
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +120 -41
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +34 -4
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +6 -2
- jararaca/presentation/websocket/websocket_interceptor.py +74 -23
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +81 -0
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +76 -0
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +758 -0
- jararaca/scheduler/decorators.py +89 -28
- jararaca/scheduler/types.py +11 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +10 -4
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1126 -189
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +372 -0
- jararaca/utils/retry.py +148 -0
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca/messagebus/types.py +0 -30
- jararaca/scheduler/scheduler.py +0 -154
- jararaca/tools/metadata.py +0 -47
- jararaca-0.2.37a12.dist-info/RECORD +0 -63
- /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import inspect
|
|
2
6
|
import re
|
|
3
7
|
import typing
|
|
@@ -8,7 +12,7 @@ from datetime import date, datetime, time
|
|
|
8
12
|
from decimal import Decimal
|
|
9
13
|
from enum import Enum
|
|
10
14
|
from io import StringIO
|
|
11
|
-
from types import NoneType, UnionType
|
|
15
|
+
from types import FunctionType, NoneType, UnionType
|
|
12
16
|
from typing import (
|
|
13
17
|
IO,
|
|
14
18
|
Annotated,
|
|
@@ -24,17 +28,27 @@ from typing import (
|
|
|
24
28
|
from uuid import UUID
|
|
25
29
|
|
|
26
30
|
from fastapi import Request, Response, UploadFile
|
|
27
|
-
from fastapi.params import Body, Cookie, Depends, Header, Path, Query
|
|
31
|
+
from fastapi.params import Body, Cookie, Depends, Form, Header, Path, Query
|
|
28
32
|
from fastapi.security.http import HTTPBase
|
|
29
|
-
from pydantic import BaseModel, PlainValidator
|
|
30
|
-
from pydantic_core import
|
|
33
|
+
from pydantic import BaseModel, PlainValidator, RootModel
|
|
34
|
+
from pydantic_core import PydanticUndefined
|
|
31
35
|
|
|
32
36
|
from jararaca.microservice import Microservice
|
|
33
|
-
from jararaca.presentation.decorators import HttpMapping, RestController
|
|
37
|
+
from jararaca.presentation.decorators import HttpMapping, RestController, UseMiddleware
|
|
34
38
|
from jararaca.presentation.websocket.decorators import RegisterWebSocketMessage
|
|
35
39
|
from jararaca.presentation.websocket.websocket_interceptor import (
|
|
36
40
|
WebSocketMessageWrapper,
|
|
37
41
|
)
|
|
42
|
+
from jararaca.reflect.decorators import (
|
|
43
|
+
resolve_bound_method_decorator,
|
|
44
|
+
resolve_method_decorators,
|
|
45
|
+
)
|
|
46
|
+
from jararaca.tools.typescript.decorators import (
|
|
47
|
+
ExposeType,
|
|
48
|
+
MutationEndpoint,
|
|
49
|
+
QueryEndpoint,
|
|
50
|
+
SplitInputOutput,
|
|
51
|
+
)
|
|
38
52
|
|
|
39
53
|
CONSTANT_PATTERN = re.compile(r"^[A-Z_]+$")
|
|
40
54
|
|
|
@@ -43,6 +57,206 @@ def is_constant(name: str) -> bool:
|
|
|
43
57
|
return CONSTANT_PATTERN.match(name) is not None
|
|
44
58
|
|
|
45
59
|
|
|
60
|
+
def unwrap_annotated_type(field_type: Any) -> tuple[Any, list[Any]]:
|
|
61
|
+
"""
|
|
62
|
+
Recursively unwrap Annotated types to find the real underlying type.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
field_type: The type to unwrap, which may be deeply nested Annotated types
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A tuple of (unwrapped_type, all_metadata) where:
|
|
69
|
+
- unwrapped_type is the final non-Annotated type
|
|
70
|
+
- all_metadata is a list of all metadata from all Annotated layers
|
|
71
|
+
"""
|
|
72
|
+
all_metadata = []
|
|
73
|
+
current_type = field_type
|
|
74
|
+
|
|
75
|
+
while get_origin(current_type) == Annotated:
|
|
76
|
+
# Collect metadata from current layer
|
|
77
|
+
if hasattr(current_type, "__metadata__"):
|
|
78
|
+
all_metadata.extend(current_type.__metadata__)
|
|
79
|
+
|
|
80
|
+
# Move to the next inner type
|
|
81
|
+
if hasattr(current_type, "__args__") and len(current_type.__args__) > 0:
|
|
82
|
+
current_type = current_type.__args__[0]
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
return current_type, all_metadata
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_upload_file_type(field_type: Any) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Check if a type is UploadFile or a list/array of UploadFile.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
field_type: The type to check
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if it's UploadFile or list[UploadFile], False otherwise
|
|
98
|
+
"""
|
|
99
|
+
if field_type == UploadFile:
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
# Check for list[UploadFile], List[UploadFile], etc.
|
|
103
|
+
origin = get_origin(field_type)
|
|
104
|
+
if origin in (list, frozenset, set):
|
|
105
|
+
args = getattr(field_type, "__args__", ())
|
|
106
|
+
if args and args[0] == UploadFile:
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def should_exclude_field(
|
|
113
|
+
field_name: str, field_type: Any, basemodel_type: Type[Any]
|
|
114
|
+
) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if a field should be excluded from TypeScript interface generation.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
field_name: The name of the field
|
|
120
|
+
field_type: The type annotation of the field
|
|
121
|
+
basemodel_type: The BaseModel class containing the field
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if the field should be excluded, False otherwise
|
|
125
|
+
"""
|
|
126
|
+
# Check if field is private (starts with underscore)
|
|
127
|
+
if field_name.startswith("_"):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
# Check if field has Pydantic Field annotation and is excluded via model_fields
|
|
131
|
+
if (
|
|
132
|
+
hasattr(basemodel_type, "model_fields")
|
|
133
|
+
and field_name in basemodel_type.model_fields
|
|
134
|
+
):
|
|
135
|
+
field_info = basemodel_type.model_fields[field_name]
|
|
136
|
+
|
|
137
|
+
# Check if field is excluded via Field(exclude=True)
|
|
138
|
+
if hasattr(field_info, "exclude") and field_info.exclude:
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
# Check if field is marked as private via Field(..., alias=None) pattern
|
|
142
|
+
if (
|
|
143
|
+
hasattr(field_info, "alias")
|
|
144
|
+
and field_info.alias is None
|
|
145
|
+
and field_name.startswith("_")
|
|
146
|
+
):
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
# Check for Annotated types with Field metadata
|
|
150
|
+
if get_origin(field_type) == Annotated:
|
|
151
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
152
|
+
for metadata in all_metadata:
|
|
153
|
+
# Check if this is a Pydantic Field by looking for expected attributes
|
|
154
|
+
if hasattr(metadata, "exclude") or hasattr(metadata, "alias"):
|
|
155
|
+
# Check if Field has exclude=True
|
|
156
|
+
if hasattr(metadata, "exclude") and metadata.exclude:
|
|
157
|
+
return True
|
|
158
|
+
# Check for private fields with alias=None
|
|
159
|
+
if (
|
|
160
|
+
hasattr(metadata, "alias")
|
|
161
|
+
and metadata.alias is None
|
|
162
|
+
and field_name.startswith("_")
|
|
163
|
+
):
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
# Check for Field instances assigned as default values
|
|
167
|
+
# This handles cases like: field_name: str = Field(exclude=True)
|
|
168
|
+
if (
|
|
169
|
+
hasattr(basemodel_type, "__annotations__")
|
|
170
|
+
and field_name in basemodel_type.__annotations__
|
|
171
|
+
):
|
|
172
|
+
# Check if there's a default value that's a Field instance
|
|
173
|
+
if hasattr(basemodel_type, field_name):
|
|
174
|
+
default_value = getattr(basemodel_type, field_name, None)
|
|
175
|
+
# Check if default value has Field-like attributes (duck typing approach)
|
|
176
|
+
if default_value is not None and hasattr(default_value, "exclude"):
|
|
177
|
+
if getattr(default_value, "exclude", False):
|
|
178
|
+
return True
|
|
179
|
+
# Check for private fields with alias=None in default Field
|
|
180
|
+
if (
|
|
181
|
+
default_value is not None
|
|
182
|
+
and hasattr(default_value, "alias")
|
|
183
|
+
and getattr(default_value, "alias", None) is None
|
|
184
|
+
and field_name.startswith("_")
|
|
185
|
+
):
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def has_default_value(
|
|
192
|
+
field_name: str, field_type: Any, basemodel_type: Type[Any]
|
|
193
|
+
) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Check if a field has a default value (making it optional in TypeScript).
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
field_name: The name of the field
|
|
199
|
+
field_type: The type annotation of the field
|
|
200
|
+
basemodel_type: The BaseModel class containing the field
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if the field has a default value, False otherwise
|
|
204
|
+
"""
|
|
205
|
+
# Skip literal types as they don't have defaults in the traditional sense
|
|
206
|
+
if get_origin(field_type) is Literal:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# Check if field has default in model_fields (standard Pydantic way)
|
|
210
|
+
if (
|
|
211
|
+
hasattr(basemodel_type, "model_fields")
|
|
212
|
+
and field_name in basemodel_type.model_fields
|
|
213
|
+
):
|
|
214
|
+
field_info = basemodel_type.model_fields[field_name]
|
|
215
|
+
if field_info.default is not PydanticUndefined:
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
# Check for Field instances assigned as default values
|
|
219
|
+
# This handles cases like: field_name: str = Field(default="value")
|
|
220
|
+
if (
|
|
221
|
+
hasattr(basemodel_type, "__annotations__")
|
|
222
|
+
and field_name in basemodel_type.__annotations__
|
|
223
|
+
):
|
|
224
|
+
if hasattr(basemodel_type, field_name):
|
|
225
|
+
default_value = getattr(basemodel_type, field_name, None)
|
|
226
|
+
# Check if it's a Field instance with a default
|
|
227
|
+
if default_value is not None and hasattr(default_value, "default"):
|
|
228
|
+
# Check if the Field has a default value set
|
|
229
|
+
field_default = getattr(default_value, "default", PydanticUndefined)
|
|
230
|
+
if field_default is not PydanticUndefined:
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
# Check for non-Field default values assigned directly to class attributes
|
|
234
|
+
# This handles cases like: field_name: str = "default_value"
|
|
235
|
+
if hasattr(basemodel_type, field_name):
|
|
236
|
+
default_value = getattr(basemodel_type, field_name, None)
|
|
237
|
+
# If it's not a Field instance but has a value, it's a default
|
|
238
|
+
if (
|
|
239
|
+
default_value is not None
|
|
240
|
+
and not hasattr(default_value, "exclude") # Not a Field instance
|
|
241
|
+
and not hasattr(default_value, "alias")
|
|
242
|
+
): # Not a Field instance
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
# Check for Annotated types with Field metadata that have defaults
|
|
246
|
+
if get_origin(field_type) == Annotated:
|
|
247
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
248
|
+
for metadata in all_metadata:
|
|
249
|
+
# Check if this is a Pydantic Field with a default
|
|
250
|
+
if hasattr(metadata, "default") and hasattr(
|
|
251
|
+
metadata, "exclude"
|
|
252
|
+
): # Ensure it's a Field
|
|
253
|
+
field_default = getattr(metadata, "default", PydanticUndefined)
|
|
254
|
+
if field_default is not PydanticUndefined:
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
|
|
46
260
|
class ParseContext:
|
|
47
261
|
def __init__(self) -> None:
|
|
48
262
|
self.mapped_types: set[Any] = set()
|
|
@@ -67,6 +281,13 @@ def snake_to_camel(snake_str: str) -> str:
|
|
|
67
281
|
return components[0] + "".join(x.title() for x in components[1:])
|
|
68
282
|
|
|
69
283
|
|
|
284
|
+
def pascal_to_camel(pascal_str: str) -> str:
|
|
285
|
+
"""Convert a PascalCase string to camelCase."""
|
|
286
|
+
if not pascal_str:
|
|
287
|
+
return pascal_str
|
|
288
|
+
return pascal_str[0].lower() + pascal_str[1:]
|
|
289
|
+
|
|
290
|
+
|
|
70
291
|
def parse_literal_value(value: Any) -> str:
|
|
71
292
|
if value is None:
|
|
72
293
|
return "null"
|
|
@@ -74,17 +295,80 @@ def parse_literal_value(value: Any) -> str:
|
|
|
74
295
|
use_parse_context().mapped_types.add(value.__class__)
|
|
75
296
|
return f"{value.__class__.__name__}.{value.name}"
|
|
76
297
|
if isinstance(value, str):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
298
|
+
# Properly escape quotes for TypeScript string literals
|
|
299
|
+
escaped_value = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
300
|
+
return f'"{escaped_value}"'
|
|
80
301
|
if isinstance(value, float):
|
|
81
302
|
return str(value)
|
|
82
303
|
if isinstance(value, bool):
|
|
83
|
-
|
|
304
|
+
# Ensure Python's True/False are properly converted to JavaScript's true/false
|
|
305
|
+
return "true" if value else "false"
|
|
306
|
+
# Special handling for Python symbols that might appear in literal types
|
|
307
|
+
if value is True:
|
|
308
|
+
return "true"
|
|
309
|
+
if value is False:
|
|
310
|
+
return "false"
|
|
311
|
+
if value is None:
|
|
312
|
+
return "null"
|
|
313
|
+
if isinstance(value, int):
|
|
314
|
+
return str(value)
|
|
84
315
|
return "unknown"
|
|
85
316
|
|
|
86
317
|
|
|
87
|
-
def
|
|
318
|
+
def get_generic_type_mapping(controller_class: type) -> dict[Any, Any]:
|
|
319
|
+
mapping: dict[Any, Any] = {}
|
|
320
|
+
if not hasattr(controller_class, "__orig_bases__"):
|
|
321
|
+
return mapping
|
|
322
|
+
|
|
323
|
+
for base in controller_class.__orig_bases__:
|
|
324
|
+
origin = get_origin(base)
|
|
325
|
+
if origin and hasattr(origin, "__parameters__"):
|
|
326
|
+
args = typing.get_args(base)
|
|
327
|
+
params = origin.__parameters__
|
|
328
|
+
if len(args) == len(params):
|
|
329
|
+
for param, arg in zip(params, args):
|
|
330
|
+
mapping[param] = arg
|
|
331
|
+
return mapping
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_field_type_for_ts(
|
|
335
|
+
field_type: Any,
|
|
336
|
+
context_suffix: str = "",
|
|
337
|
+
type_mapping: dict[Any, Any] | None = None,
|
|
338
|
+
) -> Any:
|
|
339
|
+
"""
|
|
340
|
+
Convert a Python type to its TypeScript equivalent.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
field_type: The Python type to convert
|
|
344
|
+
context_suffix: Suffix for split models (e.g., "Input", "Output")
|
|
345
|
+
type_mapping: Mapping of TypeVars to concrete types
|
|
346
|
+
"""
|
|
347
|
+
if type_mapping and field_type in type_mapping:
|
|
348
|
+
return get_field_type_for_ts(
|
|
349
|
+
type_mapping[field_type], context_suffix, type_mapping
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Handle RootModel types - use the wrapped type directly
|
|
353
|
+
if inspect.isclass(field_type) and issubclass(field_type, RootModel):
|
|
354
|
+
# For concrete RootModel subclasses, get the wrapped type from annotations
|
|
355
|
+
if (
|
|
356
|
+
hasattr(field_type, "__annotations__")
|
|
357
|
+
and "root" in field_type.__annotations__
|
|
358
|
+
):
|
|
359
|
+
wrapped_type = field_type.__annotations__["root"]
|
|
360
|
+
return get_field_type_for_ts(wrapped_type, context_suffix, type_mapping)
|
|
361
|
+
|
|
362
|
+
# For parameterized RootModel[T] types, get the type from pydantic metadata
|
|
363
|
+
if hasattr(field_type, "__pydantic_generic_metadata__"):
|
|
364
|
+
metadata = field_type.__pydantic_generic_metadata__
|
|
365
|
+
if metadata.get("origin") is RootModel and metadata.get("args"):
|
|
366
|
+
# Get the first (and only) type argument
|
|
367
|
+
wrapped_type = metadata["args"][0]
|
|
368
|
+
return get_field_type_for_ts(wrapped_type, context_suffix, type_mapping)
|
|
369
|
+
|
|
370
|
+
return "unknown"
|
|
371
|
+
|
|
88
372
|
if field_type is Response:
|
|
89
373
|
return "unknown"
|
|
90
374
|
if field_type is Any:
|
|
@@ -112,17 +396,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
112
396
|
if field_type == Decimal:
|
|
113
397
|
return "number"
|
|
114
398
|
if get_origin(field_type) == ClassVar:
|
|
115
|
-
return get_field_type_for_ts(
|
|
399
|
+
return get_field_type_for_ts(
|
|
400
|
+
field_type.__args__[0], context_suffix, type_mapping
|
|
401
|
+
)
|
|
116
402
|
if get_origin(field_type) == tuple:
|
|
117
|
-
return f"[{', '.join([get_field_type_for_ts(field) for field in field_type.__args__])}]"
|
|
403
|
+
return f"[{', '.join([get_field_type_for_ts(field, context_suffix, type_mapping) for field in field_type.__args__])}]"
|
|
118
404
|
if get_origin(field_type) == list or get_origin(field_type) == frozenset:
|
|
119
|
-
return f"Array<{get_field_type_for_ts(field_type.__args__[0])}>"
|
|
405
|
+
return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}>"
|
|
120
406
|
if get_origin(field_type) == set:
|
|
121
|
-
return f"Array<{get_field_type_for_ts(field_type.__args__[0])}> // Set"
|
|
407
|
+
return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}> // Set"
|
|
122
408
|
if get_origin(field_type) == dict:
|
|
123
|
-
return f"{{[key: {get_field_type_for_ts(field_type.__args__[0])}]: {get_field_type_for_ts(field_type.__args__[1])}}}"
|
|
409
|
+
return f"{{[key: {get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}]: {get_field_type_for_ts(field_type.__args__[1], context_suffix, type_mapping)}}}"
|
|
124
410
|
if inspect.isclass(field_type):
|
|
125
411
|
if not hasattr(field_type, "__pydantic_generic_metadata__"):
|
|
412
|
+
# Check if this is a split model and use appropriate suffix
|
|
413
|
+
if SplitInputOutput.is_split_model(field_type) and context_suffix:
|
|
414
|
+
return f"{field_type.__name__}{context_suffix}"
|
|
126
415
|
return field_type.__name__
|
|
127
416
|
pydantic_metadata = getattr(field_type, "__pydantic_generic_metadata__")
|
|
128
417
|
|
|
@@ -131,12 +420,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
131
420
|
if pydantic_metadata.get("origin") is not None
|
|
132
421
|
else field_type.__name__
|
|
133
422
|
)
|
|
423
|
+
|
|
424
|
+
# Check if this is a split model and use appropriate suffix
|
|
425
|
+
if SplitInputOutput.is_split_model(field_type) and context_suffix:
|
|
426
|
+
name = f"{field_type.__name__}{context_suffix}"
|
|
427
|
+
|
|
134
428
|
args = pydantic_metadata.get("args")
|
|
135
429
|
|
|
136
430
|
if len(args) > 0:
|
|
137
431
|
return "%s<%s>" % (
|
|
138
432
|
name,
|
|
139
|
-
", ".join(
|
|
433
|
+
", ".join(
|
|
434
|
+
[
|
|
435
|
+
get_field_type_for_ts(arg, context_suffix, type_mapping)
|
|
436
|
+
for arg in args
|
|
437
|
+
]
|
|
438
|
+
),
|
|
140
439
|
)
|
|
141
440
|
|
|
142
441
|
return name
|
|
@@ -146,16 +445,25 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
146
445
|
if get_origin(field_type) == Literal:
|
|
147
446
|
return " | ".join([parse_literal_value(x) for x in field_type.__args__])
|
|
148
447
|
if get_origin(field_type) == UnionType or get_origin(field_type) == typing.Union:
|
|
149
|
-
return " | ".join(
|
|
448
|
+
return " | ".join(
|
|
449
|
+
[
|
|
450
|
+
get_field_type_for_ts(x, context_suffix, type_mapping)
|
|
451
|
+
for x in field_type.__args__
|
|
452
|
+
]
|
|
453
|
+
)
|
|
150
454
|
if (get_origin(field_type) == Annotated) and (len(field_type.__args__) > 0):
|
|
455
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
456
|
+
|
|
151
457
|
if (
|
|
152
458
|
plain_validator := next(
|
|
153
|
-
(x for x in
|
|
459
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
154
460
|
None,
|
|
155
461
|
)
|
|
156
462
|
) is not None:
|
|
157
|
-
return get_field_type_for_ts(
|
|
158
|
-
|
|
463
|
+
return get_field_type_for_ts(
|
|
464
|
+
plain_validator.json_schema_input_type, context_suffix, type_mapping
|
|
465
|
+
)
|
|
466
|
+
return get_field_type_for_ts(unwrapped_type, context_suffix, type_mapping)
|
|
159
467
|
return "unknown"
|
|
160
468
|
|
|
161
469
|
|
|
@@ -170,6 +478,59 @@ def get_generic_args(field_type: Any) -> Any:
|
|
|
170
478
|
def parse_type_to_typescript_interface(
|
|
171
479
|
basemodel_type: Type[Any],
|
|
172
480
|
) -> tuple[set[type], str]:
|
|
481
|
+
"""
|
|
482
|
+
Parse a Pydantic model into TypeScript interface(s).
|
|
483
|
+
|
|
484
|
+
If the model is decorated with @SplitInputOutput, it generates both Input and Output interfaces.
|
|
485
|
+
Otherwise, it generates a single interface.
|
|
486
|
+
"""
|
|
487
|
+
# Check if this model should be split into Input/Output interfaces
|
|
488
|
+
if SplitInputOutput.is_split_model(basemodel_type):
|
|
489
|
+
return parse_split_input_output_interfaces(basemodel_type)
|
|
490
|
+
|
|
491
|
+
return parse_single_typescript_interface(basemodel_type)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def parse_split_input_output_interfaces(
|
|
495
|
+
basemodel_type: Type[Any],
|
|
496
|
+
) -> tuple[set[type], str]:
|
|
497
|
+
"""
|
|
498
|
+
Generate both Input and Output TypeScript interfaces for a split model.
|
|
499
|
+
"""
|
|
500
|
+
mapped_types: set[type] = set()
|
|
501
|
+
combined_output = StringIO()
|
|
502
|
+
|
|
503
|
+
# Generate Input interface (with optional fields)
|
|
504
|
+
input_mapped_types, input_interface = parse_single_typescript_interface(
|
|
505
|
+
basemodel_type, interface_suffix="Input", force_optional_defaults=True
|
|
506
|
+
)
|
|
507
|
+
mapped_types.update(input_mapped_types)
|
|
508
|
+
combined_output.write(input_interface)
|
|
509
|
+
|
|
510
|
+
# Generate Output interface (all fields required as they come from the backend)
|
|
511
|
+
output_mapped_types, output_interface = parse_single_typescript_interface(
|
|
512
|
+
basemodel_type, interface_suffix="Output", force_optional_defaults=False
|
|
513
|
+
)
|
|
514
|
+
mapped_types.update(output_mapped_types)
|
|
515
|
+
combined_output.write(output_interface)
|
|
516
|
+
|
|
517
|
+
return mapped_types, combined_output.getvalue()
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def parse_single_typescript_interface(
|
|
521
|
+
basemodel_type: Type[Any],
|
|
522
|
+
interface_suffix: str = "",
|
|
523
|
+
force_optional_defaults: bool | None = None,
|
|
524
|
+
) -> tuple[set[type], str]:
|
|
525
|
+
"""
|
|
526
|
+
Generate a single TypeScript interface for a Pydantic model.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
basemodel_type: The Pydantic model class
|
|
530
|
+
interface_suffix: Suffix to add to the interface name (e.g., "Input", "Output")
|
|
531
|
+
force_optional_defaults: If True, fields with defaults are optional. If False, all fields are required.
|
|
532
|
+
If None, uses the default behavior (fields with defaults are optional).
|
|
533
|
+
"""
|
|
173
534
|
string_builder = StringIO()
|
|
174
535
|
mapped_types: set[type] = set()
|
|
175
536
|
|
|
@@ -193,10 +554,11 @@ def parse_type_to_typescript_interface(
|
|
|
193
554
|
mapped_types.update(inherited_classes)
|
|
194
555
|
|
|
195
556
|
if Enum in inherited_classes:
|
|
557
|
+
enum_values = sorted([(x._name_, x.value) for x in basemodel_type])
|
|
196
558
|
return (
|
|
197
559
|
set(),
|
|
198
|
-
f"export enum {basemodel_type.__name__} {{\n"
|
|
199
|
-
+ "\n ".join([f'\t{
|
|
560
|
+
f"export enum {basemodel_type.__name__}{interface_suffix} {{\n"
|
|
561
|
+
+ "\n ".join([f'\t{name} = "{value}",' for name, value in enum_values])
|
|
200
562
|
+ "\n}\n",
|
|
201
563
|
)
|
|
202
564
|
|
|
@@ -219,66 +581,84 @@ def parse_type_to_typescript_interface(
|
|
|
219
581
|
for inherited_class in valid_inherited_classes
|
|
220
582
|
}
|
|
221
583
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
584
|
+
# Modify inheritance for split interfaces
|
|
585
|
+
extends_expression = ""
|
|
586
|
+
if len(valid_inherited_classes) > 0:
|
|
587
|
+
extends_base_names = []
|
|
588
|
+
for inherited_class in valid_inherited_classes:
|
|
589
|
+
base_name = get_field_type_for_ts(inherited_class, interface_suffix)
|
|
590
|
+
# If the inherited class is also a split model, use the appropriate suffix
|
|
591
|
+
if SplitInputOutput.is_split_model(inherited_class) and interface_suffix:
|
|
592
|
+
base_name = f"{inherited_class.__name__}{interface_suffix}"
|
|
593
|
+
|
|
594
|
+
if inherited_classes_consts_conflict[inherited_class]:
|
|
595
|
+
base_name = "Omit<%s, %s>" % (
|
|
596
|
+
base_name,
|
|
597
|
+
" | ".join(
|
|
598
|
+
sorted(
|
|
599
|
+
[
|
|
600
|
+
'"%s"' % field_name
|
|
601
|
+
for field_name in inherited_classes_consts_conflict[
|
|
602
|
+
inherited_class
|
|
603
|
+
]
|
|
236
604
|
]
|
|
237
|
-
)
|
|
238
|
-
)
|
|
605
|
+
)
|
|
606
|
+
),
|
|
239
607
|
)
|
|
240
|
-
|
|
241
|
-
|
|
608
|
+
extends_base_names.append(base_name)
|
|
609
|
+
|
|
610
|
+
extends_expression = " extends %s" % ", ".join(
|
|
611
|
+
sorted(extends_base_names, key=lambda x: str(x))
|
|
242
612
|
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
613
|
+
|
|
614
|
+
interface_name = f"{basemodel_type.__name__}{interface_suffix}"
|
|
246
615
|
|
|
247
616
|
if is_generic_type(basemodel_type):
|
|
617
|
+
generic_args = get_generic_args(basemodel_type)
|
|
248
618
|
string_builder.write(
|
|
249
|
-
f"export interface {
|
|
619
|
+
f"export interface {interface_name}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
|
|
250
620
|
)
|
|
251
621
|
else:
|
|
252
622
|
string_builder.write(
|
|
253
|
-
f"export interface {
|
|
623
|
+
f"export interface {interface_name}{extends_expression} {{\n"
|
|
254
624
|
)
|
|
255
625
|
|
|
256
626
|
if hasattr(basemodel_type, "__annotations__"):
|
|
257
|
-
#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
for field_name, field in
|
|
627
|
+
# Sort fields for consistent output
|
|
628
|
+
annotation_items = sorted(
|
|
629
|
+
basemodel_type.__annotations__.items(), key=lambda x: x[0]
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
for field_name, field in annotation_items:
|
|
263
633
|
if field_name in cls_consts:
|
|
264
634
|
continue
|
|
265
635
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
636
|
+
# Check if field should be excluded (private or excluded via Field)
|
|
637
|
+
if should_exclude_field(field_name, field, basemodel_type):
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
# Determine if field is optional based on the force_optional_defaults parameter
|
|
641
|
+
if force_optional_defaults is True:
|
|
642
|
+
# Input interface: fields with defaults are optional
|
|
643
|
+
is_optional = has_default_value(field_name, field, basemodel_type)
|
|
644
|
+
elif force_optional_defaults is False:
|
|
645
|
+
# Output interface: all fields are required (backend provides complete data)
|
|
646
|
+
is_optional = False
|
|
647
|
+
else:
|
|
648
|
+
# Default behavior: fields with defaults are optional
|
|
649
|
+
is_optional = has_default_value(field_name, field, basemodel_type)
|
|
650
|
+
|
|
274
651
|
string_builder.write(
|
|
275
|
-
f" {snake_to_camel(field_name) if not is_constant(field_name) else field_name}{'?' if
|
|
652
|
+
f" {snake_to_camel(field_name) if not is_constant(field_name) else field_name}{'?' if is_optional else ''}: {get_field_type_for_ts(field, interface_suffix)};\n"
|
|
276
653
|
)
|
|
277
654
|
mapped_types.update(extract_all_envolved_types(field))
|
|
278
655
|
mapped_types.add(field)
|
|
279
656
|
|
|
280
|
-
## Loop over computed fields
|
|
281
|
-
members =
|
|
657
|
+
## Loop over computed fields - sort them for consistent output
|
|
658
|
+
members = sorted(
|
|
659
|
+
inspect.getmembers_static(basemodel_type, lambda a: isinstance(a, property)),
|
|
660
|
+
key=lambda x: x[0],
|
|
661
|
+
)
|
|
282
662
|
for field_name, field in members:
|
|
283
663
|
if hasattr(field, "fget"):
|
|
284
664
|
module_func_name = field.fget.__module__ + "." + field.fget.__qualname__
|
|
@@ -296,7 +676,7 @@ def parse_type_to_typescript_interface(
|
|
|
296
676
|
return_type = NoneType
|
|
297
677
|
|
|
298
678
|
string_builder.write(
|
|
299
|
-
f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type)};\n"
|
|
679
|
+
f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type, interface_suffix)};\n"
|
|
300
680
|
)
|
|
301
681
|
mapped_types.update(extract_all_envolved_types(return_type))
|
|
302
682
|
mapped_types.add(return_type)
|
|
@@ -326,20 +706,28 @@ def write_microservice_to_typescript_interface(
|
|
|
326
706
|
websocket_registries: set[RegisterWebSocketMessage] = set()
|
|
327
707
|
mapped_types_set.add(WebSocketMessageWrapper)
|
|
328
708
|
|
|
709
|
+
# Add all explicitly exposed types
|
|
710
|
+
mapped_types_set.update(ExposeType.get_all_exposed_types())
|
|
711
|
+
|
|
329
712
|
for controller in microservice.controllers:
|
|
330
|
-
rest_controller = RestController.
|
|
713
|
+
rest_controller = RestController.get_last(controller)
|
|
331
714
|
|
|
332
715
|
if rest_controller is None:
|
|
333
716
|
continue
|
|
334
717
|
|
|
335
|
-
|
|
336
|
-
|
|
718
|
+
controller_class_strio, types, hooks_strio = (
|
|
719
|
+
write_rest_controller_to_typescript_interface(
|
|
720
|
+
rest_controller,
|
|
721
|
+
controller,
|
|
722
|
+
)
|
|
337
723
|
)
|
|
338
724
|
|
|
339
725
|
mapped_types_set.update(types)
|
|
340
|
-
rest_controller_buffer.write(
|
|
726
|
+
rest_controller_buffer.write(controller_class_strio.getvalue())
|
|
727
|
+
if hooks_strio is not None:
|
|
728
|
+
rest_controller_buffer.write(hooks_strio.getvalue())
|
|
341
729
|
|
|
342
|
-
registered = RegisterWebSocketMessage.
|
|
730
|
+
registered = RegisterWebSocketMessage.get_last(controller)
|
|
343
731
|
|
|
344
732
|
if registered is not None:
|
|
345
733
|
for message_type in registered.message_types:
|
|
@@ -354,49 +742,59 @@ def write_microservice_to_typescript_interface(
|
|
|
354
742
|
|
|
355
743
|
final_buffer.write(
|
|
356
744
|
"""
|
|
357
|
-
|
|
358
|
-
%s
|
|
359
|
-
}
|
|
360
|
-
"""
|
|
361
|
-
% "\n".join(
|
|
362
|
-
[
|
|
363
|
-
f'\t"{message.MESSAGE_ID}": {message.__name__};'
|
|
364
|
-
for registers in websocket_registries
|
|
365
|
-
for message in registers.message_types
|
|
366
|
-
]
|
|
367
|
-
)
|
|
368
|
-
)
|
|
745
|
+
/* eslint-disable */
|
|
369
746
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
headers: { [key: string]: string };
|
|
386
|
-
query: { [key: string]: unknown };
|
|
387
|
-
body: unknown;
|
|
388
|
-
responseType?: ResponseType;
|
|
747
|
+
// @ts-nocheck
|
|
748
|
+
|
|
749
|
+
// noinspection JSUnusedGlobalSymbols
|
|
750
|
+
|
|
751
|
+
import { HttpService, HttpBackend, HttpBackendRequest, ResponseType, createClassQueryHooks , createClassMutationHooks, createClassInfiniteQueryHooks, paginationModelByFirstArgPaginationFilter, recursiveCamelToSnakeCase } from "@jararaca/core";
|
|
752
|
+
|
|
753
|
+
function makeFormData(data: Record<string, any>): FormData {
|
|
754
|
+
const formData = new FormData();
|
|
755
|
+
for (const key in data) {
|
|
756
|
+
const value = data[key];
|
|
757
|
+
for (const v of genFormDataValue(value)) {
|
|
758
|
+
formData.append(key, v);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return formData;
|
|
389
762
|
}
|
|
390
763
|
|
|
391
|
-
|
|
392
|
-
|
|
764
|
+
function* genFormDataValue(value: any): any {
|
|
765
|
+
if (Array.isArray(value)) {
|
|
766
|
+
// Stringify arrays as JSON
|
|
767
|
+
for (const item of value) {
|
|
768
|
+
// formData.append(`${key}`, item);
|
|
769
|
+
yield* genFormDataValue(item);
|
|
770
|
+
}
|
|
771
|
+
} else if (typeof value === "object" && value.constructor === Object) {
|
|
772
|
+
// Stringify plain objects as JSON
|
|
773
|
+
// formData.append(key, JSON.stringify(value));
|
|
774
|
+
yield JSON.stringify(
|
|
775
|
+
recursiveCamelToSnakeCase(value)
|
|
776
|
+
);
|
|
777
|
+
} else {
|
|
778
|
+
// For primitives (string, number, boolean), append as-is
|
|
779
|
+
yield value;
|
|
780
|
+
}
|
|
393
781
|
}
|
|
394
782
|
|
|
395
|
-
export
|
|
396
|
-
|
|
783
|
+
export type WebSocketMessageMap = {
|
|
784
|
+
%s
|
|
397
785
|
}
|
|
398
786
|
"""
|
|
787
|
+
% "\n".join(
|
|
788
|
+
sorted(
|
|
789
|
+
[
|
|
790
|
+
f'\t"{message.MESSAGE_ID}": {message.__name__};'
|
|
791
|
+
for registers in websocket_registries
|
|
792
|
+
for message in registers.message_types
|
|
793
|
+
]
|
|
794
|
+
)
|
|
795
|
+
)
|
|
399
796
|
)
|
|
797
|
+
|
|
400
798
|
processed_types: set[Any] = set()
|
|
401
799
|
backlog: set[Any] = mapped_types_set.copy()
|
|
402
800
|
|
|
@@ -469,29 +867,108 @@ def is_primitive(field_type: Any) -> bool:
|
|
|
469
867
|
|
|
470
868
|
def write_rest_controller_to_typescript_interface(
|
|
471
869
|
rest_controller: RestController, controller: type
|
|
472
|
-
) -> tuple[
|
|
870
|
+
) -> tuple[StringIO, set[Any], StringIO | None]:
|
|
871
|
+
|
|
872
|
+
class_name = controller.__name__
|
|
873
|
+
|
|
874
|
+
decorated_queries: list[tuple[str, FunctionType, QueryEndpoint]] = []
|
|
875
|
+
decorated_mutations: list[tuple[str, FunctionType]] = []
|
|
473
876
|
|
|
474
877
|
class_buffer = StringIO()
|
|
475
878
|
|
|
476
|
-
class_buffer.write(f"export class {
|
|
879
|
+
class_buffer.write(f"export class {class_name} extends HttpService {{\n")
|
|
477
880
|
|
|
478
881
|
mapped_types: set[Any] = set()
|
|
479
882
|
|
|
480
|
-
|
|
481
|
-
|
|
883
|
+
# Compute type mapping for generics
|
|
884
|
+
type_mapping = get_generic_type_mapping(controller)
|
|
885
|
+
|
|
886
|
+
# Sort members for consistent output
|
|
887
|
+
member_items = sorted(
|
|
888
|
+
inspect.getmembers_static(controller, predicate=inspect.isfunction),
|
|
889
|
+
key=lambda x: x[0],
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
class_usemiddlewares = UseMiddleware.get_all_from_type(
|
|
893
|
+
controller, rest_controller.class_inherits_decorators
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
for name, member in member_items:
|
|
897
|
+
mapping = resolve_bound_method_decorator(
|
|
898
|
+
controller, name, HttpMapping, rest_controller.methods_inherit_decorators
|
|
899
|
+
)
|
|
900
|
+
effective_member = member
|
|
901
|
+
|
|
902
|
+
if mapping is not None:
|
|
482
903
|
return_type = member.__annotations__.get("return")
|
|
483
904
|
|
|
484
905
|
if return_type is None:
|
|
485
906
|
return_type = NoneType
|
|
486
907
|
|
|
908
|
+
if query_endpoint := resolve_bound_method_decorator(
|
|
909
|
+
controller,
|
|
910
|
+
name,
|
|
911
|
+
QueryEndpoint,
|
|
912
|
+
rest_controller.methods_inherit_decorators,
|
|
913
|
+
):
|
|
914
|
+
decorated_queries.append((name, member, query_endpoint))
|
|
915
|
+
if MutationEndpoint.is_mutation(effective_member):
|
|
916
|
+
decorated_mutations.append((name, member))
|
|
917
|
+
|
|
487
918
|
mapped_types.update(extract_all_envolved_types(return_type))
|
|
488
919
|
|
|
489
|
-
|
|
920
|
+
# For return types, use Output suffix if it's a split model
|
|
921
|
+
return_value_repr = get_field_type_for_ts(
|
|
922
|
+
return_type, "Output", type_mapping
|
|
923
|
+
)
|
|
490
924
|
|
|
491
925
|
arg_params_spec, parametes_mapped_types = extract_parameters(
|
|
492
|
-
member, rest_controller, mapping
|
|
926
|
+
member, rest_controller, mapping, type_mapping
|
|
493
927
|
)
|
|
494
928
|
|
|
929
|
+
# Collect middleware parameters separately
|
|
930
|
+
middleware_params_list: list[HttpParemeterSpec] = []
|
|
931
|
+
|
|
932
|
+
# Extract parameters from controller-level middlewares
|
|
933
|
+
for middleware_type in rest_controller.middlewares:
|
|
934
|
+
middleware_params, middleware_mapped_types = (
|
|
935
|
+
extract_middleware_parameters(
|
|
936
|
+
middleware_type, rest_controller, mapping
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
middleware_params_list.extend(middleware_params)
|
|
940
|
+
parametes_mapped_types.update(middleware_mapped_types)
|
|
941
|
+
|
|
942
|
+
# Extract parameters from class-level UseMiddleware decorators
|
|
943
|
+
for middleware_instance in class_usemiddlewares:
|
|
944
|
+
middleware_params, middleware_mapped_types = (
|
|
945
|
+
extract_middleware_parameters(
|
|
946
|
+
middleware_instance.middleware, rest_controller, mapping
|
|
947
|
+
)
|
|
948
|
+
)
|
|
949
|
+
middleware_params_list.extend(middleware_params)
|
|
950
|
+
parametes_mapped_types.update(middleware_mapped_types)
|
|
951
|
+
|
|
952
|
+
# Extract parameters from method-level middlewares (UseMiddleware)
|
|
953
|
+
# Get the method from the class to access its middleware decorators
|
|
954
|
+
method_middlewares = resolve_method_decorators(
|
|
955
|
+
controller,
|
|
956
|
+
name,
|
|
957
|
+
UseMiddleware,
|
|
958
|
+
rest_controller.methods_inherit_decorators,
|
|
959
|
+
)
|
|
960
|
+
for middleware_instance in method_middlewares:
|
|
961
|
+
middleware_params, middleware_mapped_types = (
|
|
962
|
+
extract_middleware_parameters(
|
|
963
|
+
middleware_instance.middleware, rest_controller, mapping
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
middleware_params_list.extend(middleware_params)
|
|
967
|
+
parametes_mapped_types.update(middleware_mapped_types)
|
|
968
|
+
|
|
969
|
+
# Combine parameters: middleware params first, then controller params
|
|
970
|
+
arg_params_spec = middleware_params_list + arg_params_spec
|
|
971
|
+
|
|
495
972
|
for param in parametes_mapped_types:
|
|
496
973
|
mapped_types.update(extract_all_envolved_types(param))
|
|
497
974
|
|
|
@@ -509,28 +986,73 @@ def write_rest_controller_to_typescript_interface(
|
|
|
509
986
|
class_buffer.write(f'\t\t\tmethod: "{mapping.method}",\n')
|
|
510
987
|
|
|
511
988
|
endpoint_path = parse_path_with_params(mapping.path, arg_params_spec)
|
|
512
|
-
|
|
513
|
-
|
|
989
|
+
|
|
990
|
+
# Properly handle path joining to avoid double slashes
|
|
991
|
+
controller_path = rest_controller.path or ""
|
|
992
|
+
# Also apply path transformation to the controller path
|
|
993
|
+
controller_path = parse_path_with_params(controller_path, arg_params_spec)
|
|
994
|
+
path_parts = []
|
|
995
|
+
|
|
996
|
+
if controller_path and controller_path.strip("/"):
|
|
997
|
+
path_parts.append(controller_path.strip("/"))
|
|
998
|
+
if endpoint_path and endpoint_path.strip("/"):
|
|
999
|
+
path_parts.append(endpoint_path.strip("/"))
|
|
1000
|
+
|
|
1001
|
+
final_path = "/".join(path_parts) if path_parts else ""
|
|
1002
|
+
# Ensure the path starts with a single slash
|
|
1003
|
+
formatted_path = f"/{final_path}" if final_path else "/"
|
|
1004
|
+
|
|
1005
|
+
class_buffer.write(f"\t\t\tpath: `{formatted_path}`,\n")
|
|
1006
|
+
|
|
1007
|
+
# Sort path params
|
|
1008
|
+
path_params = sorted(
|
|
1009
|
+
[param for param in arg_params_spec if param.type_ == "path"],
|
|
1010
|
+
key=lambda x: x.name,
|
|
514
1011
|
)
|
|
515
|
-
class_buffer.write(
|
|
1012
|
+
class_buffer.write("\t\t\tpathParams: {\n")
|
|
1013
|
+
for param in path_params:
|
|
1014
|
+
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
1015
|
+
class_buffer.write("\t\t\t},\n")
|
|
516
1016
|
|
|
1017
|
+
# Sort headers
|
|
1018
|
+
header_params = sorted(
|
|
1019
|
+
[param for param in arg_params_spec if param.type_ == "header"],
|
|
1020
|
+
key=lambda x: x.name,
|
|
1021
|
+
)
|
|
517
1022
|
class_buffer.write("\t\t\theaders: {\n")
|
|
518
|
-
for param in
|
|
519
|
-
|
|
520
|
-
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
521
|
-
|
|
1023
|
+
for param in header_params:
|
|
1024
|
+
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
522
1025
|
class_buffer.write("\t\t\t},\n")
|
|
1026
|
+
|
|
1027
|
+
# Sort query params
|
|
1028
|
+
query_params = sorted(
|
|
1029
|
+
[param for param in arg_params_spec if param.type_ == "query"],
|
|
1030
|
+
key=lambda x: x.name,
|
|
1031
|
+
)
|
|
523
1032
|
class_buffer.write("\t\t\tquery: {\n")
|
|
1033
|
+
for param in query_params:
|
|
1034
|
+
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
1035
|
+
class_buffer.write("\t\t\t},\n")
|
|
524
1036
|
|
|
525
|
-
for
|
|
526
|
-
|
|
1037
|
+
# Check if we need to use FormData (for file uploads or form parameters)
|
|
1038
|
+
form_params = [param for param in arg_params_spec if param.type_ == "form"]
|
|
1039
|
+
body_param = next((x for x in arg_params_spec if x.type_ == "body"), None)
|
|
1040
|
+
|
|
1041
|
+
if form_params:
|
|
1042
|
+
# Use FormData for file uploads and form parameters
|
|
1043
|
+
class_buffer.write("\t\t\tbody: makeFormData({\n")
|
|
1044
|
+
|
|
1045
|
+
# Add form parameters (including file uploads)
|
|
1046
|
+
for param in form_params:
|
|
527
1047
|
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
528
|
-
class_buffer.write("\t\t\t},\n")
|
|
529
1048
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1049
|
+
# Add body parameter if it exists alongside form params
|
|
1050
|
+
if body_param:
|
|
1051
|
+
class_buffer.write(f"\t\t\t\t...{body_param.name},\n")
|
|
1052
|
+
|
|
1053
|
+
class_buffer.write("\t\t\t})\n")
|
|
1054
|
+
elif body_param is not None:
|
|
1055
|
+
class_buffer.write(f"\t\t\tbody: {body_param.name}\n")
|
|
534
1056
|
else:
|
|
535
1057
|
class_buffer.write("\t\t\tbody: undefined\n")
|
|
536
1058
|
|
|
@@ -541,7 +1063,44 @@ def write_rest_controller_to_typescript_interface(
|
|
|
541
1063
|
|
|
542
1064
|
class_buffer.write("}\n")
|
|
543
1065
|
|
|
544
|
-
|
|
1066
|
+
controller_hooks_builder: StringIO | None = None
|
|
1067
|
+
|
|
1068
|
+
if decorated_queries or decorated_mutations:
|
|
1069
|
+
controller_hooks_builder = StringIO()
|
|
1070
|
+
controller_hooks_builder.write(
|
|
1071
|
+
f"export const {pascal_to_camel(class_name)} = {{\n"
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
if decorated_queries:
|
|
1075
|
+
controller_hooks_builder.write(
|
|
1076
|
+
f"\t...createClassQueryHooks({class_name},\n"
|
|
1077
|
+
)
|
|
1078
|
+
for name, member, _ in decorated_queries:
|
|
1079
|
+
controller_hooks_builder.write(f'\t\t"{snake_to_camel(name)}",\n')
|
|
1080
|
+
controller_hooks_builder.write("\t),\n")
|
|
1081
|
+
|
|
1082
|
+
if decorated_queries and any(
|
|
1083
|
+
query.has_infinite_query for _, _, query in decorated_queries
|
|
1084
|
+
):
|
|
1085
|
+
controller_hooks_builder.write(
|
|
1086
|
+
f"\t...createClassInfiniteQueryHooks({class_name}, {{\n"
|
|
1087
|
+
)
|
|
1088
|
+
for name, member, query in decorated_queries:
|
|
1089
|
+
if query.has_infinite_query:
|
|
1090
|
+
controller_hooks_builder.write(
|
|
1091
|
+
f'\t\t"{snake_to_camel(name)}": paginationModelByFirstArgPaginationFilter(),\n'
|
|
1092
|
+
)
|
|
1093
|
+
controller_hooks_builder.write("\t}),\n")
|
|
1094
|
+
if decorated_mutations:
|
|
1095
|
+
controller_hooks_builder.write(
|
|
1096
|
+
f"\t...createClassMutationHooks({class_name},\n"
|
|
1097
|
+
)
|
|
1098
|
+
for name, member in decorated_mutations:
|
|
1099
|
+
controller_hooks_builder.write(f'\t\t"{snake_to_camel(name)}",\n')
|
|
1100
|
+
controller_hooks_builder.write("\t),\n")
|
|
1101
|
+
controller_hooks_builder.write("};\n")
|
|
1102
|
+
|
|
1103
|
+
return class_buffer, mapped_types, controller_hooks_builder
|
|
545
1104
|
|
|
546
1105
|
|
|
547
1106
|
EXCLUDED_REQUESTS_TYPES = [Request, Response]
|
|
@@ -549,18 +1108,233 @@ EXCLUDED_REQUESTS_TYPES = [Request, Response]
|
|
|
549
1108
|
|
|
550
1109
|
@dataclass
|
|
551
1110
|
class HttpParemeterSpec:
|
|
552
|
-
type_: Literal["query", "path", "body", "header", "cookie"]
|
|
1111
|
+
type_: Literal["query", "path", "body", "header", "cookie", "form"]
|
|
553
1112
|
name: str
|
|
554
1113
|
required: bool
|
|
555
1114
|
argument_type_str: str
|
|
556
1115
|
|
|
557
1116
|
|
|
558
1117
|
def parse_path_with_params(path: str, parameters: list[HttpParemeterSpec]) -> str:
|
|
1118
|
+
# Use a regular expression to match both simple parameters {param} and
|
|
1119
|
+
# parameters with converters {param:converter}
|
|
1120
|
+
pattern = re.compile(r"{([^:}]+)(?::[^}]*)?}")
|
|
1121
|
+
|
|
1122
|
+
# For each parameter found in the path, replace it with :param format
|
|
559
1123
|
for parameter in parameters:
|
|
560
|
-
path =
|
|
1124
|
+
path = pattern.sub(
|
|
1125
|
+
lambda m: f":{m.group(1)}" if m.group(1) == parameter.name else m.group(0),
|
|
1126
|
+
path,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
561
1129
|
return path
|
|
562
1130
|
|
|
563
1131
|
|
|
1132
|
+
def extract_middleware_parameters(
|
|
1133
|
+
middleware_type: type,
|
|
1134
|
+
controller: RestController,
|
|
1135
|
+
mapping: HttpMapping,
|
|
1136
|
+
) -> tuple[list[HttpParemeterSpec], set[Any]]:
|
|
1137
|
+
"""
|
|
1138
|
+
Extract parameters from a middleware class's intercept method.
|
|
1139
|
+
"""
|
|
1140
|
+
parameters_list: list[HttpParemeterSpec] = []
|
|
1141
|
+
mapped_types: set[Any] = set()
|
|
1142
|
+
|
|
1143
|
+
# Get the intercept method from the middleware class
|
|
1144
|
+
if not hasattr(middleware_type, "intercept"):
|
|
1145
|
+
return parameters_list, mapped_types
|
|
1146
|
+
|
|
1147
|
+
intercept_method = getattr(middleware_type, "intercept")
|
|
1148
|
+
|
|
1149
|
+
# Use the same logic as extract_parameters but specifically for the intercept method
|
|
1150
|
+
try:
|
|
1151
|
+
signature = inspect.signature(intercept_method)
|
|
1152
|
+
for parameter_name, parameter in signature.parameters.items():
|
|
1153
|
+
# Skip 'self' parameter
|
|
1154
|
+
if parameter_name == "self":
|
|
1155
|
+
continue
|
|
1156
|
+
|
|
1157
|
+
parameter_type = parameter.annotation
|
|
1158
|
+
if parameter_type == inspect.Parameter.empty:
|
|
1159
|
+
continue
|
|
1160
|
+
|
|
1161
|
+
if parameter_type in EXCLUDED_REQUESTS_TYPES:
|
|
1162
|
+
continue
|
|
1163
|
+
|
|
1164
|
+
if get_origin(parameter_type) == Annotated:
|
|
1165
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(parameter_type)
|
|
1166
|
+
# Look for FastAPI parameter annotations in all metadata layers
|
|
1167
|
+
annotated_type_hook = None
|
|
1168
|
+
for metadata in all_metadata:
|
|
1169
|
+
if isinstance(
|
|
1170
|
+
metadata, (Header, Cookie, Form, Body, Query, Path, Depends)
|
|
1171
|
+
):
|
|
1172
|
+
annotated_type_hook = metadata
|
|
1173
|
+
break
|
|
1174
|
+
|
|
1175
|
+
if annotated_type_hook is None and all_metadata:
|
|
1176
|
+
# Fallback to first metadata if no FastAPI annotation found
|
|
1177
|
+
annotated_type_hook = all_metadata[0]
|
|
1178
|
+
|
|
1179
|
+
annotated_type = unwrapped_type
|
|
1180
|
+
if isinstance(annotated_type_hook, Header):
|
|
1181
|
+
mapped_types.add(str)
|
|
1182
|
+
parameters_list.append(
|
|
1183
|
+
HttpParemeterSpec(
|
|
1184
|
+
type_="header",
|
|
1185
|
+
name=parameter_name,
|
|
1186
|
+
required=True,
|
|
1187
|
+
argument_type_str=get_field_type_for_ts(str),
|
|
1188
|
+
)
|
|
1189
|
+
)
|
|
1190
|
+
elif isinstance(annotated_type_hook, Cookie):
|
|
1191
|
+
mapped_types.add(str)
|
|
1192
|
+
parameters_list.append(
|
|
1193
|
+
HttpParemeterSpec(
|
|
1194
|
+
type_="cookie",
|
|
1195
|
+
name=parameter_name,
|
|
1196
|
+
required=True,
|
|
1197
|
+
argument_type_str=get_field_type_for_ts(str),
|
|
1198
|
+
)
|
|
1199
|
+
)
|
|
1200
|
+
elif isinstance(annotated_type_hook, Form):
|
|
1201
|
+
mapped_types.add(annotated_type)
|
|
1202
|
+
parameters_list.append(
|
|
1203
|
+
HttpParemeterSpec(
|
|
1204
|
+
type_="form",
|
|
1205
|
+
name=parameter_name,
|
|
1206
|
+
required=True,
|
|
1207
|
+
argument_type_str=get_field_type_for_ts(annotated_type),
|
|
1208
|
+
)
|
|
1209
|
+
)
|
|
1210
|
+
elif isinstance(annotated_type_hook, Body):
|
|
1211
|
+
mapped_types.update(extract_all_envolved_types(parameter_type))
|
|
1212
|
+
# For body parameters, use Input suffix if it's a split model
|
|
1213
|
+
context_suffix = (
|
|
1214
|
+
"Input"
|
|
1215
|
+
if (
|
|
1216
|
+
inspect.isclass(parameter_type)
|
|
1217
|
+
and hasattr(parameter_type, "__dict__")
|
|
1218
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1219
|
+
)
|
|
1220
|
+
else ""
|
|
1221
|
+
)
|
|
1222
|
+
parameters_list.append(
|
|
1223
|
+
HttpParemeterSpec(
|
|
1224
|
+
type_="body",
|
|
1225
|
+
name=parameter_name,
|
|
1226
|
+
required=True,
|
|
1227
|
+
argument_type_str=get_field_type_for_ts(
|
|
1228
|
+
parameter_type, context_suffix
|
|
1229
|
+
),
|
|
1230
|
+
)
|
|
1231
|
+
)
|
|
1232
|
+
elif isinstance(annotated_type_hook, Query):
|
|
1233
|
+
mapped_types.add(parameter_type)
|
|
1234
|
+
# For query parameters, use Input suffix if it's a split model
|
|
1235
|
+
context_suffix = (
|
|
1236
|
+
"Input"
|
|
1237
|
+
if (
|
|
1238
|
+
inspect.isclass(parameter_type)
|
|
1239
|
+
and hasattr(parameter_type, "__dict__")
|
|
1240
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1241
|
+
)
|
|
1242
|
+
else ""
|
|
1243
|
+
)
|
|
1244
|
+
parameters_list.append(
|
|
1245
|
+
HttpParemeterSpec(
|
|
1246
|
+
type_="query",
|
|
1247
|
+
name=parameter_name,
|
|
1248
|
+
required=True,
|
|
1249
|
+
argument_type_str=get_field_type_for_ts(
|
|
1250
|
+
parameter_type, context_suffix
|
|
1251
|
+
),
|
|
1252
|
+
)
|
|
1253
|
+
)
|
|
1254
|
+
elif isinstance(annotated_type_hook, Path):
|
|
1255
|
+
mapped_types.add(parameter_type)
|
|
1256
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1257
|
+
context_suffix = (
|
|
1258
|
+
"Input"
|
|
1259
|
+
if (
|
|
1260
|
+
inspect.isclass(parameter_type)
|
|
1261
|
+
and hasattr(parameter_type, "__dict__")
|
|
1262
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1263
|
+
)
|
|
1264
|
+
else ""
|
|
1265
|
+
)
|
|
1266
|
+
parameters_list.append(
|
|
1267
|
+
HttpParemeterSpec(
|
|
1268
|
+
type_="path",
|
|
1269
|
+
name=parameter_name,
|
|
1270
|
+
required=True,
|
|
1271
|
+
argument_type_str=get_field_type_for_ts(
|
|
1272
|
+
parameter_type, context_suffix
|
|
1273
|
+
),
|
|
1274
|
+
)
|
|
1275
|
+
)
|
|
1276
|
+
elif isinstance(annotated_type_hook, Depends):
|
|
1277
|
+
# For Dependencies, recursively extract parameters
|
|
1278
|
+
depends_hook = (
|
|
1279
|
+
annotated_type_hook.dependency or parameter_type.__args__[0]
|
|
1280
|
+
)
|
|
1281
|
+
if isinstance(depends_hook, HTTPBase):
|
|
1282
|
+
# Skip HTTP authentication dependencies
|
|
1283
|
+
pass
|
|
1284
|
+
else:
|
|
1285
|
+
# TODO: We might need to recursively extract from dependencies
|
|
1286
|
+
# For now, skip to avoid infinite recursion
|
|
1287
|
+
pass
|
|
1288
|
+
else:
|
|
1289
|
+
# Handle non-annotated parameters - check if they are path parameters
|
|
1290
|
+
mapped_types.add(parameter_type)
|
|
1291
|
+
|
|
1292
|
+
# Check if parameter matches path parameters in controller or method paths
|
|
1293
|
+
if (
|
|
1294
|
+
# Match both simple parameters {param} and parameters with converters {param:converter}
|
|
1295
|
+
re.search(f"{{{parameter_name}(:.*?)?}}", controller.path)
|
|
1296
|
+
is not None
|
|
1297
|
+
or re.search(f"{{{parameter_name}(:.*?)?}}", mapping.path)
|
|
1298
|
+
is not None
|
|
1299
|
+
):
|
|
1300
|
+
# This is a path parameter
|
|
1301
|
+
context_suffix = (
|
|
1302
|
+
"Input"
|
|
1303
|
+
if (
|
|
1304
|
+
inspect.isclass(parameter_type)
|
|
1305
|
+
and hasattr(parameter_type, "__dict__")
|
|
1306
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1307
|
+
)
|
|
1308
|
+
else ""
|
|
1309
|
+
)
|
|
1310
|
+
parameters_list.append(
|
|
1311
|
+
HttpParemeterSpec(
|
|
1312
|
+
type_="path",
|
|
1313
|
+
name=parameter_name,
|
|
1314
|
+
required=True,
|
|
1315
|
+
argument_type_str=get_field_type_for_ts(
|
|
1316
|
+
parameter_type, context_suffix
|
|
1317
|
+
),
|
|
1318
|
+
)
|
|
1319
|
+
)
|
|
1320
|
+
elif is_primitive(parameter_type):
|
|
1321
|
+
# Default to query parameters for simple types that aren't in the path
|
|
1322
|
+
parameters_list.append(
|
|
1323
|
+
HttpParemeterSpec(
|
|
1324
|
+
type_="query",
|
|
1325
|
+
name=parameter_name,
|
|
1326
|
+
required=True,
|
|
1327
|
+
argument_type_str=get_field_type_for_ts(parameter_type),
|
|
1328
|
+
)
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
except (ValueError, TypeError):
|
|
1332
|
+
# If we can't inspect the signature, return empty
|
|
1333
|
+
pass
|
|
1334
|
+
|
|
1335
|
+
return parameters_list, mapped_types
|
|
1336
|
+
|
|
1337
|
+
|
|
564
1338
|
def mount_parametes_arguments(parameters: list[HttpParemeterSpec]) -> str:
|
|
565
1339
|
return ", ".join(
|
|
566
1340
|
[f"{parameter.name}: {parameter.argument_type_str}" for parameter in parameters]
|
|
@@ -568,7 +1342,10 @@ def mount_parametes_arguments(parameters: list[HttpParemeterSpec]) -> str:
|
|
|
568
1342
|
|
|
569
1343
|
|
|
570
1344
|
def extract_parameters(
|
|
571
|
-
member: Any,
|
|
1345
|
+
member: Any,
|
|
1346
|
+
controller: RestController,
|
|
1347
|
+
mapping: HttpMapping,
|
|
1348
|
+
type_mapping: dict[Any, Any] | None = None,
|
|
572
1349
|
) -> tuple[list[HttpParemeterSpec], set[Any]]:
|
|
573
1350
|
parameters_list: list[HttpParemeterSpec] = []
|
|
574
1351
|
mapped_types: set[Any] = set()
|
|
@@ -577,7 +1354,7 @@ def extract_parameters(
|
|
|
577
1354
|
if is_primitive(arg):
|
|
578
1355
|
continue
|
|
579
1356
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
580
|
-
arg, controller, mapping
|
|
1357
|
+
arg, controller, mapping, type_mapping
|
|
581
1358
|
)
|
|
582
1359
|
mapped_types.update(rec_mapped_types)
|
|
583
1360
|
parameters_list.extend(rec_parameters)
|
|
@@ -586,22 +1363,23 @@ def extract_parameters(
|
|
|
586
1363
|
if is_primitive(member):
|
|
587
1364
|
|
|
588
1365
|
if get_origin(member) is Annotated:
|
|
1366
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(member)
|
|
589
1367
|
if (
|
|
590
1368
|
plain_validator := next(
|
|
591
|
-
(x for x in
|
|
1369
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
592
1370
|
None,
|
|
593
1371
|
)
|
|
594
1372
|
) is not None:
|
|
595
1373
|
mapped_types.add(plain_validator.json_schema_input_type)
|
|
596
1374
|
return parameters_list, mapped_types
|
|
597
|
-
return extract_parameters(
|
|
1375
|
+
return extract_parameters(unwrapped_type, controller, mapping, type_mapping)
|
|
598
1376
|
return parameters_list, mapped_types
|
|
599
1377
|
|
|
600
1378
|
if hasattr(member, "__bases__"):
|
|
601
1379
|
for base in member.__bases__:
|
|
602
1380
|
# if base is not BaseModel:
|
|
603
1381
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
604
|
-
base, controller, mapping
|
|
1382
|
+
base, controller, mapping, type_mapping
|
|
605
1383
|
)
|
|
606
1384
|
mapped_types.update(rec_mapped_types)
|
|
607
1385
|
parameters_list.extend(rec_parameters)
|
|
@@ -613,9 +1391,26 @@ def extract_parameters(
|
|
|
613
1391
|
if parameter_type in EXCLUDED_REQUESTS_TYPES:
|
|
614
1392
|
continue
|
|
615
1393
|
|
|
1394
|
+
# Resolve generic type
|
|
1395
|
+
if type_mapping and parameter_type in type_mapping:
|
|
1396
|
+
parameter_type = type_mapping[parameter_type]
|
|
1397
|
+
|
|
616
1398
|
if get_origin(parameter_type) == Annotated:
|
|
617
|
-
|
|
618
|
-
|
|
1399
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(parameter_type)
|
|
1400
|
+
# Look for FastAPI parameter annotations in all metadata layers
|
|
1401
|
+
annotated_type_hook = None
|
|
1402
|
+
for metadata in all_metadata:
|
|
1403
|
+
if isinstance(
|
|
1404
|
+
metadata, (Header, Cookie, Form, Body, Query, Path, Depends)
|
|
1405
|
+
):
|
|
1406
|
+
annotated_type_hook = metadata
|
|
1407
|
+
break
|
|
1408
|
+
|
|
1409
|
+
if annotated_type_hook is None and all_metadata:
|
|
1410
|
+
# Fallback to first metadata if no FastAPI annotation found
|
|
1411
|
+
annotated_type_hook = all_metadata[0]
|
|
1412
|
+
|
|
1413
|
+
annotated_type = unwrapped_type
|
|
619
1414
|
if isinstance(annotated_type_hook, Header):
|
|
620
1415
|
mapped_types.add(str)
|
|
621
1416
|
parameters_list.append(
|
|
@@ -623,7 +1418,9 @@ def extract_parameters(
|
|
|
623
1418
|
type_="header",
|
|
624
1419
|
name=parameter_name,
|
|
625
1420
|
required=True,
|
|
626
|
-
argument_type_str=get_field_type_for_ts(
|
|
1421
|
+
argument_type_str=get_field_type_for_ts(
|
|
1422
|
+
str, "", type_mapping
|
|
1423
|
+
),
|
|
627
1424
|
)
|
|
628
1425
|
)
|
|
629
1426
|
elif isinstance(annotated_type_hook, Cookie):
|
|
@@ -633,37 +1430,87 @@ def extract_parameters(
|
|
|
633
1430
|
type_="cookie",
|
|
634
1431
|
name=parameter_name,
|
|
635
1432
|
required=True,
|
|
636
|
-
argument_type_str=get_field_type_for_ts(
|
|
1433
|
+
argument_type_str=get_field_type_for_ts(
|
|
1434
|
+
str, "", type_mapping
|
|
1435
|
+
),
|
|
1436
|
+
)
|
|
1437
|
+
)
|
|
1438
|
+
elif isinstance(annotated_type_hook, Form):
|
|
1439
|
+
mapped_types.add(annotated_type)
|
|
1440
|
+
parameters_list.append(
|
|
1441
|
+
HttpParemeterSpec(
|
|
1442
|
+
type_="form",
|
|
1443
|
+
name=parameter_name,
|
|
1444
|
+
required=True,
|
|
1445
|
+
argument_type_str=get_field_type_for_ts(
|
|
1446
|
+
annotated_type, "", type_mapping
|
|
1447
|
+
),
|
|
637
1448
|
)
|
|
638
1449
|
)
|
|
639
1450
|
elif isinstance(annotated_type_hook, Body):
|
|
640
1451
|
mapped_types.update(extract_all_envolved_types(parameter_type))
|
|
1452
|
+
# For body parameters, use Input suffix if it's a split model
|
|
1453
|
+
context_suffix = (
|
|
1454
|
+
"Input"
|
|
1455
|
+
if (
|
|
1456
|
+
inspect.isclass(parameter_type)
|
|
1457
|
+
and hasattr(parameter_type, "__dict__")
|
|
1458
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1459
|
+
)
|
|
1460
|
+
else ""
|
|
1461
|
+
)
|
|
641
1462
|
parameters_list.append(
|
|
642
1463
|
HttpParemeterSpec(
|
|
643
1464
|
type_="body",
|
|
644
1465
|
name=parameter_name,
|
|
645
1466
|
required=True,
|
|
646
|
-
argument_type_str=get_field_type_for_ts(
|
|
1467
|
+
argument_type_str=get_field_type_for_ts(
|
|
1468
|
+
parameter_type, context_suffix, type_mapping
|
|
1469
|
+
),
|
|
647
1470
|
)
|
|
648
1471
|
)
|
|
649
1472
|
elif isinstance(annotated_type_hook, Query):
|
|
650
1473
|
mapped_types.add(parameter_type)
|
|
1474
|
+
# For query parameters, use Input suffix if it's a split model
|
|
1475
|
+
context_suffix = (
|
|
1476
|
+
"Input"
|
|
1477
|
+
if (
|
|
1478
|
+
inspect.isclass(parameter_type)
|
|
1479
|
+
and hasattr(parameter_type, "__dict__")
|
|
1480
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1481
|
+
)
|
|
1482
|
+
else ""
|
|
1483
|
+
)
|
|
651
1484
|
parameters_list.append(
|
|
652
1485
|
HttpParemeterSpec(
|
|
653
1486
|
type_="query",
|
|
654
1487
|
name=parameter_name,
|
|
655
1488
|
required=True,
|
|
656
|
-
argument_type_str=get_field_type_for_ts(
|
|
1489
|
+
argument_type_str=get_field_type_for_ts(
|
|
1490
|
+
parameter_type, context_suffix, type_mapping
|
|
1491
|
+
),
|
|
657
1492
|
)
|
|
658
1493
|
)
|
|
659
1494
|
elif isinstance(annotated_type_hook, Path):
|
|
660
1495
|
mapped_types.add(parameter_type)
|
|
1496
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1497
|
+
context_suffix = (
|
|
1498
|
+
"Input"
|
|
1499
|
+
if (
|
|
1500
|
+
inspect.isclass(parameter_type)
|
|
1501
|
+
and hasattr(parameter_type, "__dict__")
|
|
1502
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1503
|
+
)
|
|
1504
|
+
else ""
|
|
1505
|
+
)
|
|
661
1506
|
parameters_list.append(
|
|
662
1507
|
HttpParemeterSpec(
|
|
663
1508
|
type_="path",
|
|
664
1509
|
name=parameter_name,
|
|
665
1510
|
required=True,
|
|
666
|
-
argument_type_str=get_field_type_for_ts(
|
|
1511
|
+
argument_type_str=get_field_type_for_ts(
|
|
1512
|
+
parameter_type, context_suffix, type_mapping
|
|
1513
|
+
),
|
|
667
1514
|
)
|
|
668
1515
|
)
|
|
669
1516
|
|
|
@@ -677,66 +1524,162 @@ def extract_parameters(
|
|
|
677
1524
|
|
|
678
1525
|
else:
|
|
679
1526
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
680
|
-
depends_hook, controller, mapping
|
|
1527
|
+
depends_hook, controller, mapping, type_mapping
|
|
681
1528
|
)
|
|
682
1529
|
mapped_types.update(rec_mapped_types)
|
|
683
1530
|
parameters_list.extend(rec_parameters)
|
|
684
|
-
elif
|
|
1531
|
+
elif (
|
|
1532
|
+
re.search(f":{parameter_name}(?:/|$)", controller.path) is not None
|
|
1533
|
+
):
|
|
685
1534
|
mapped_types.add(annotated_type)
|
|
1535
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1536
|
+
context_suffix = (
|
|
1537
|
+
"Input"
|
|
1538
|
+
if (
|
|
1539
|
+
inspect.isclass(annotated_type)
|
|
1540
|
+
and hasattr(annotated_type, "__dict__")
|
|
1541
|
+
and SplitInputOutput.is_split_model(annotated_type)
|
|
1542
|
+
)
|
|
1543
|
+
else ""
|
|
1544
|
+
)
|
|
686
1545
|
parameters_list.append(
|
|
687
1546
|
HttpParemeterSpec(
|
|
688
1547
|
type_="path",
|
|
689
1548
|
name=parameter_name,
|
|
690
1549
|
required=True,
|
|
691
|
-
argument_type_str=get_field_type_for_ts(
|
|
1550
|
+
argument_type_str=get_field_type_for_ts(
|
|
1551
|
+
annotated_type, context_suffix, type_mapping
|
|
1552
|
+
),
|
|
692
1553
|
)
|
|
693
1554
|
)
|
|
694
1555
|
else:
|
|
695
1556
|
mapped_types.add(annotated_type)
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1557
|
+
# Special handling for UploadFile and list[UploadFile] - should be treated as form data
|
|
1558
|
+
if is_upload_file_type(annotated_type):
|
|
1559
|
+
parameters_list.append(
|
|
1560
|
+
HttpParemeterSpec(
|
|
1561
|
+
type_="form",
|
|
1562
|
+
name=parameter_name,
|
|
1563
|
+
required=True,
|
|
1564
|
+
argument_type_str=get_field_type_for_ts(
|
|
1565
|
+
annotated_type, "", type_mapping
|
|
1566
|
+
),
|
|
1567
|
+
)
|
|
1568
|
+
)
|
|
1569
|
+
else:
|
|
1570
|
+
# For default parameters (treated as query), use Input suffix if it's a split model
|
|
1571
|
+
context_suffix = (
|
|
1572
|
+
"Input"
|
|
1573
|
+
if (
|
|
1574
|
+
inspect.isclass(annotated_type)
|
|
1575
|
+
and hasattr(annotated_type, "__dict__")
|
|
1576
|
+
and SplitInputOutput.is_split_model(annotated_type)
|
|
1577
|
+
)
|
|
1578
|
+
else ""
|
|
1579
|
+
)
|
|
1580
|
+
parameters_list.append(
|
|
1581
|
+
HttpParemeterSpec(
|
|
1582
|
+
type_="query",
|
|
1583
|
+
name=parameter_name,
|
|
1584
|
+
required=True,
|
|
1585
|
+
argument_type_str=get_field_type_for_ts(
|
|
1586
|
+
annotated_type, context_suffix, type_mapping
|
|
1587
|
+
),
|
|
1588
|
+
)
|
|
702
1589
|
)
|
|
703
|
-
)
|
|
704
1590
|
|
|
705
1591
|
elif inspect.isclass(parameter_type) and issubclass(
|
|
706
1592
|
parameter_type, BaseModel
|
|
707
1593
|
):
|
|
708
1594
|
mapped_types.update(extract_all_envolved_types(parameter_type))
|
|
1595
|
+
# For BaseModel parameters, use Input suffix if it's a split model
|
|
1596
|
+
context_suffix = (
|
|
1597
|
+
"Input" if SplitInputOutput.is_split_model(parameter_type) else ""
|
|
1598
|
+
)
|
|
709
1599
|
parameters_list.append(
|
|
710
1600
|
HttpParemeterSpec(
|
|
711
1601
|
type_="body",
|
|
712
1602
|
name=parameter_name,
|
|
713
1603
|
required=True,
|
|
714
|
-
argument_type_str=get_field_type_for_ts(
|
|
1604
|
+
argument_type_str=get_field_type_for_ts(
|
|
1605
|
+
parameter_type, context_suffix, type_mapping
|
|
1606
|
+
),
|
|
715
1607
|
)
|
|
716
1608
|
)
|
|
717
|
-
elif (
|
|
718
|
-
|
|
719
|
-
or mapping.path.find(f"{{{parameter_name}}}") != -1
|
|
720
|
-
):
|
|
1609
|
+
elif parameter_type == UploadFile or is_upload_file_type(parameter_type):
|
|
1610
|
+
# UploadFile and list[UploadFile] should be treated as form data
|
|
721
1611
|
mapped_types.add(parameter_type)
|
|
722
1612
|
parameters_list.append(
|
|
723
1613
|
HttpParemeterSpec(
|
|
724
|
-
type_="
|
|
1614
|
+
type_="form",
|
|
725
1615
|
name=parameter_name,
|
|
726
1616
|
required=True,
|
|
727
|
-
argument_type_str=get_field_type_for_ts(
|
|
1617
|
+
argument_type_str=get_field_type_for_ts(
|
|
1618
|
+
parameter_type, "", type_mapping
|
|
1619
|
+
),
|
|
728
1620
|
)
|
|
729
1621
|
)
|
|
730
|
-
|
|
1622
|
+
elif (
|
|
1623
|
+
# Match both simple parameters {param} and parameters with converters {param:converter}
|
|
1624
|
+
re.search(f"{{{parameter_name}(:.*?)?}}", controller.path) is not None
|
|
1625
|
+
or re.search(f"{{{parameter_name}(:.*?)?}}", mapping.path) is not None
|
|
1626
|
+
):
|
|
731
1627
|
mapped_types.add(parameter_type)
|
|
1628
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1629
|
+
context_suffix = (
|
|
1630
|
+
"Input"
|
|
1631
|
+
if (
|
|
1632
|
+
inspect.isclass(parameter_type)
|
|
1633
|
+
and hasattr(parameter_type, "__dict__")
|
|
1634
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1635
|
+
)
|
|
1636
|
+
else ""
|
|
1637
|
+
)
|
|
732
1638
|
parameters_list.append(
|
|
733
1639
|
HttpParemeterSpec(
|
|
734
|
-
type_="
|
|
1640
|
+
type_="path",
|
|
735
1641
|
name=parameter_name,
|
|
736
1642
|
required=True,
|
|
737
|
-
argument_type_str=get_field_type_for_ts(
|
|
1643
|
+
argument_type_str=get_field_type_for_ts(
|
|
1644
|
+
parameter_type, context_suffix, type_mapping
|
|
1645
|
+
),
|
|
738
1646
|
)
|
|
739
1647
|
)
|
|
1648
|
+
else:
|
|
1649
|
+
mapped_types.add(parameter_type)
|
|
1650
|
+
# Special handling for UploadFile and list[UploadFile] - should be treated as form data
|
|
1651
|
+
if is_upload_file_type(parameter_type):
|
|
1652
|
+
parameters_list.append(
|
|
1653
|
+
HttpParemeterSpec(
|
|
1654
|
+
type_="form",
|
|
1655
|
+
name=parameter_name,
|
|
1656
|
+
required=True,
|
|
1657
|
+
argument_type_str=get_field_type_for_ts(
|
|
1658
|
+
parameter_type, "", type_mapping
|
|
1659
|
+
),
|
|
1660
|
+
)
|
|
1661
|
+
)
|
|
1662
|
+
else:
|
|
1663
|
+
# For default parameters (treated as query), use Input suffix if it's a split model
|
|
1664
|
+
context_suffix = (
|
|
1665
|
+
"Input"
|
|
1666
|
+
if (
|
|
1667
|
+
inspect.isclass(parameter_type)
|
|
1668
|
+
and hasattr(parameter_type, "__dict__")
|
|
1669
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1670
|
+
)
|
|
1671
|
+
else ""
|
|
1672
|
+
)
|
|
1673
|
+
parameters_list.append(
|
|
1674
|
+
HttpParemeterSpec(
|
|
1675
|
+
type_="query",
|
|
1676
|
+
name=parameter_name,
|
|
1677
|
+
required=True,
|
|
1678
|
+
argument_type_str=get_field_type_for_ts(
|
|
1679
|
+
parameter_type, context_suffix, type_mapping
|
|
1680
|
+
),
|
|
1681
|
+
)
|
|
1682
|
+
)
|
|
740
1683
|
|
|
741
1684
|
if inspect.isclass(parameter_type) and not is_primitive(parameter_type):
|
|
742
1685
|
signature = inspect.signature(parameter_type)
|
|
@@ -746,28 +1689,29 @@ def extract_parameters(
|
|
|
746
1689
|
for _, parameter_type in parameter_members.items():
|
|
747
1690
|
if is_primitive(parameter_type.annotation):
|
|
748
1691
|
if get_origin(parameter_type.annotation) is not None:
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
|
|
1692
|
+
if get_origin(parameter_type.annotation) == Annotated:
|
|
1693
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(
|
|
1694
|
+
parameter_type.annotation
|
|
1695
|
+
)
|
|
1696
|
+
plain_validator = next(
|
|
1697
|
+
(
|
|
1698
|
+
x
|
|
1699
|
+
for x in all_metadata
|
|
1700
|
+
if isinstance(x, PlainValidator)
|
|
1701
|
+
),
|
|
1702
|
+
None,
|
|
760
1703
|
)
|
|
761
|
-
is not None
|
|
762
|
-
|
|
763
|
-
|
|
1704
|
+
if plain_validator is not None:
|
|
1705
|
+
mapped_types.add(
|
|
1706
|
+
plain_validator.json_schema_input_type
|
|
1707
|
+
)
|
|
764
1708
|
else:
|
|
765
1709
|
args = parameter_type.annotation.__args__
|
|
766
1710
|
mapped_types.update(args)
|
|
767
1711
|
else:
|
|
768
1712
|
continue
|
|
769
1713
|
_, types = extract_parameters(
|
|
770
|
-
parameter_type.annotation, controller, mapping
|
|
1714
|
+
parameter_type.annotation, controller, mapping, type_mapping
|
|
771
1715
|
)
|
|
772
1716
|
mapped_types.update(types)
|
|
773
1717
|
|
|
@@ -775,7 +1719,7 @@ def extract_parameters(
|
|
|
775
1719
|
for arg in member.__args__:
|
|
776
1720
|
|
|
777
1721
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
778
|
-
arg, controller, mapping
|
|
1722
|
+
arg, controller, mapping, type_mapping
|
|
779
1723
|
)
|
|
780
1724
|
mapped_types.update(rec_mapped_types)
|
|
781
1725
|
parameters_list.extend(rec_parameters)
|
|
@@ -791,22 +1735,15 @@ def extract_all_envolved_types(field_type: Any) -> set[Any]:
|
|
|
791
1735
|
|
|
792
1736
|
if is_primitive(field_type):
|
|
793
1737
|
if get_origin(field_type) is not None:
|
|
794
|
-
if (
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
x
|
|
800
|
-
for x in field_type.__metadata__
|
|
801
|
-
if isinstance(x, PlainValidator)
|
|
802
|
-
),
|
|
803
|
-
None,
|
|
804
|
-
)
|
|
1738
|
+
if get_origin(field_type) == Annotated:
|
|
1739
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
1740
|
+
plain_validator = next(
|
|
1741
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
1742
|
+
None,
|
|
805
1743
|
)
|
|
806
|
-
is not None
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return mapped_types
|
|
1744
|
+
if plain_validator is not None:
|
|
1745
|
+
mapped_types.add(plain_validator.json_schema_input_type)
|
|
1746
|
+
return mapped_types
|
|
810
1747
|
else:
|
|
811
1748
|
mapped_types.update(
|
|
812
1749
|
*[extract_all_envolved_types(arg) for arg in field_type.__args__]
|