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