jararaca 0.3.11a16__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 +184 -12
- 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 +272 -47
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -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 +33 -67
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
- jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +850 -383
- jararaca/microservice.py +110 -1
- 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 +202 -11
- jararaca/persistence/base.py +38 -2
- 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 +50 -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 +238 -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 +378 -113
- 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 +1074 -173
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +65 -39
- jararaca/utils/retry.py +10 -3
- 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.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -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.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"
|
|
@@ -94,7 +315,60 @@ def parse_literal_value(value: Any) -> str:
|
|
|
94
315
|
return "unknown"
|
|
95
316
|
|
|
96
317
|
|
|
97
|
-
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
|
+
|
|
98
372
|
if field_type is Response:
|
|
99
373
|
return "unknown"
|
|
100
374
|
if field_type is Any:
|
|
@@ -122,17 +396,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
122
396
|
if field_type == Decimal:
|
|
123
397
|
return "number"
|
|
124
398
|
if get_origin(field_type) == ClassVar:
|
|
125
|
-
return get_field_type_for_ts(
|
|
399
|
+
return get_field_type_for_ts(
|
|
400
|
+
field_type.__args__[0], context_suffix, type_mapping
|
|
401
|
+
)
|
|
126
402
|
if get_origin(field_type) == tuple:
|
|
127
|
-
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__])}]"
|
|
128
404
|
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])}>"
|
|
405
|
+
return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix, type_mapping)}>"
|
|
130
406
|
if get_origin(field_type) == set:
|
|
131
|
-
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"
|
|
132
408
|
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])}}}"
|
|
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)}}}"
|
|
134
410
|
if inspect.isclass(field_type):
|
|
135
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}"
|
|
136
415
|
return field_type.__name__
|
|
137
416
|
pydantic_metadata = getattr(field_type, "__pydantic_generic_metadata__")
|
|
138
417
|
|
|
@@ -141,12 +420,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
141
420
|
if pydantic_metadata.get("origin") is not None
|
|
142
421
|
else field_type.__name__
|
|
143
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
|
+
|
|
144
428
|
args = pydantic_metadata.get("args")
|
|
145
429
|
|
|
146
430
|
if len(args) > 0:
|
|
147
431
|
return "%s<%s>" % (
|
|
148
432
|
name,
|
|
149
|
-
", ".join(
|
|
433
|
+
", ".join(
|
|
434
|
+
[
|
|
435
|
+
get_field_type_for_ts(arg, context_suffix, type_mapping)
|
|
436
|
+
for arg in args
|
|
437
|
+
]
|
|
438
|
+
),
|
|
150
439
|
)
|
|
151
440
|
|
|
152
441
|
return name
|
|
@@ -156,16 +445,25 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
156
445
|
if get_origin(field_type) == Literal:
|
|
157
446
|
return " | ".join([parse_literal_value(x) for x in field_type.__args__])
|
|
158
447
|
if get_origin(field_type) == UnionType or get_origin(field_type) == typing.Union:
|
|
159
|
-
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
|
+
)
|
|
160
454
|
if (get_origin(field_type) == Annotated) and (len(field_type.__args__) > 0):
|
|
455
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
456
|
+
|
|
161
457
|
if (
|
|
162
458
|
plain_validator := next(
|
|
163
|
-
(x for x in
|
|
459
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
164
460
|
None,
|
|
165
461
|
)
|
|
166
462
|
) is not None:
|
|
167
|
-
return get_field_type_for_ts(
|
|
168
|
-
|
|
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)
|
|
169
467
|
return "unknown"
|
|
170
468
|
|
|
171
469
|
|
|
@@ -180,6 +478,59 @@ def get_generic_args(field_type: Any) -> Any:
|
|
|
180
478
|
def parse_type_to_typescript_interface(
|
|
181
479
|
basemodel_type: Type[Any],
|
|
182
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
|
+
"""
|
|
183
534
|
string_builder = StringIO()
|
|
184
535
|
mapped_types: set[type] = set()
|
|
185
536
|
|
|
@@ -206,7 +557,7 @@ def parse_type_to_typescript_interface(
|
|
|
206
557
|
enum_values = sorted([(x._name_, x.value) for x in basemodel_type])
|
|
207
558
|
return (
|
|
208
559
|
set(),
|
|
209
|
-
f"export enum {basemodel_type.__name__} {{\n"
|
|
560
|
+
f"export enum {basemodel_type.__name__}{interface_suffix} {{\n"
|
|
210
561
|
+ "\n ".join([f'\t{name} = "{value}",' for name, value in enum_values])
|
|
211
562
|
+ "\n}\n",
|
|
212
563
|
)
|
|
@@ -230,46 +581,46 @@ def parse_type_to_typescript_interface(
|
|
|
230
581
|
for inherited_class in valid_inherited_classes
|
|
231
582
|
}
|
|
232
583
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
]
|
|
604
|
+
]
|
|
254
605
|
)
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
extends_base_names.append(base_name)
|
|
609
|
+
|
|
610
|
+
extends_expression = " extends %s" % ", ".join(
|
|
611
|
+
sorted(extends_base_names, key=lambda x: str(x))
|
|
260
612
|
)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
613
|
+
|
|
614
|
+
interface_name = f"{basemodel_type.__name__}{interface_suffix}"
|
|
264
615
|
|
|
265
616
|
if is_generic_type(basemodel_type):
|
|
266
617
|
generic_args = get_generic_args(basemodel_type)
|
|
267
618
|
string_builder.write(
|
|
268
|
-
f"export interface {
|
|
619
|
+
f"export interface {interface_name}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
|
|
269
620
|
)
|
|
270
621
|
else:
|
|
271
622
|
string_builder.write(
|
|
272
|
-
f"export interface {
|
|
623
|
+
f"export interface {interface_name}{extends_expression} {{\n"
|
|
273
624
|
)
|
|
274
625
|
|
|
275
626
|
if hasattr(basemodel_type, "__annotations__"):
|
|
@@ -282,23 +633,30 @@ def parse_type_to_typescript_interface(
|
|
|
282
633
|
if field_name in cls_consts:
|
|
283
634
|
continue
|
|
284
635
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
|
|
293
651
|
string_builder.write(
|
|
294
|
-
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"
|
|
295
653
|
)
|
|
296
654
|
mapped_types.update(extract_all_envolved_types(field))
|
|
297
655
|
mapped_types.add(field)
|
|
298
656
|
|
|
299
657
|
## Loop over computed fields - sort them for consistent output
|
|
300
658
|
members = sorted(
|
|
301
|
-
inspect.
|
|
659
|
+
inspect.getmembers_static(basemodel_type, lambda a: isinstance(a, property)),
|
|
302
660
|
key=lambda x: x[0],
|
|
303
661
|
)
|
|
304
662
|
for field_name, field in members:
|
|
@@ -318,7 +676,7 @@ def parse_type_to_typescript_interface(
|
|
|
318
676
|
return_type = NoneType
|
|
319
677
|
|
|
320
678
|
string_builder.write(
|
|
321
|
-
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"
|
|
322
680
|
)
|
|
323
681
|
mapped_types.update(extract_all_envolved_types(return_type))
|
|
324
682
|
mapped_types.add(return_type)
|
|
@@ -348,20 +706,28 @@ def write_microservice_to_typescript_interface(
|
|
|
348
706
|
websocket_registries: set[RegisterWebSocketMessage] = set()
|
|
349
707
|
mapped_types_set.add(WebSocketMessageWrapper)
|
|
350
708
|
|
|
709
|
+
# Add all explicitly exposed types
|
|
710
|
+
mapped_types_set.update(ExposeType.get_all_exposed_types())
|
|
711
|
+
|
|
351
712
|
for controller in microservice.controllers:
|
|
352
|
-
rest_controller = RestController.
|
|
713
|
+
rest_controller = RestController.get_last(controller)
|
|
353
714
|
|
|
354
715
|
if rest_controller is None:
|
|
355
716
|
continue
|
|
356
717
|
|
|
357
|
-
|
|
358
|
-
|
|
718
|
+
controller_class_strio, types, hooks_strio = (
|
|
719
|
+
write_rest_controller_to_typescript_interface(
|
|
720
|
+
rest_controller,
|
|
721
|
+
controller,
|
|
722
|
+
)
|
|
359
723
|
)
|
|
360
724
|
|
|
361
725
|
mapped_types_set.update(types)
|
|
362
|
-
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())
|
|
363
729
|
|
|
364
|
-
registered = RegisterWebSocketMessage.
|
|
730
|
+
registered = RegisterWebSocketMessage.get_last(controller)
|
|
365
731
|
|
|
366
732
|
if registered is not None:
|
|
367
733
|
for message_type in registered.message_types:
|
|
@@ -376,6 +742,44 @@ def write_microservice_to_typescript_interface(
|
|
|
376
742
|
|
|
377
743
|
final_buffer.write(
|
|
378
744
|
"""
|
|
745
|
+
/* eslint-disable */
|
|
746
|
+
|
|
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;
|
|
762
|
+
}
|
|
763
|
+
|
|
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
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
379
783
|
export type WebSocketMessageMap = {
|
|
380
784
|
%s
|
|
381
785
|
}
|
|
@@ -391,36 +795,6 @@ export type WebSocketMessageMap = {
|
|
|
391
795
|
)
|
|
392
796
|
)
|
|
393
797
|
|
|
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
798
|
processed_types: set[Any] = set()
|
|
425
799
|
backlog: set[Any] = mapped_types_set.copy()
|
|
426
800
|
|
|
@@ -493,33 +867,107 @@ def is_primitive(field_type: Any) -> bool:
|
|
|
493
867
|
|
|
494
868
|
def write_rest_controller_to_typescript_interface(
|
|
495
869
|
rest_controller: RestController, controller: type
|
|
496
|
-
) -> 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]] = []
|
|
497
876
|
|
|
498
877
|
class_buffer = StringIO()
|
|
499
878
|
|
|
500
|
-
class_buffer.write(f"export class {
|
|
879
|
+
class_buffer.write(f"export class {class_name} extends HttpService {{\n")
|
|
501
880
|
|
|
502
881
|
mapped_types: set[Any] = set()
|
|
503
882
|
|
|
883
|
+
# Compute type mapping for generics
|
|
884
|
+
type_mapping = get_generic_type_mapping(controller)
|
|
885
|
+
|
|
504
886
|
# Sort members for consistent output
|
|
505
887
|
member_items = sorted(
|
|
506
|
-
inspect.
|
|
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
|
|
507
894
|
)
|
|
508
895
|
|
|
509
896
|
for name, member in member_items:
|
|
510
|
-
|
|
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:
|
|
511
903
|
return_type = member.__annotations__.get("return")
|
|
512
904
|
|
|
513
905
|
if return_type is None:
|
|
514
906
|
return_type = NoneType
|
|
515
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
|
+
|
|
516
918
|
mapped_types.update(extract_all_envolved_types(return_type))
|
|
517
919
|
|
|
518
|
-
|
|
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
|
+
)
|
|
519
924
|
|
|
520
925
|
arg_params_spec, parametes_mapped_types = extract_parameters(
|
|
521
|
-
member, rest_controller, mapping
|
|
926
|
+
member, rest_controller, mapping, type_mapping
|
|
927
|
+
)
|
|
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,
|
|
522
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
|
|
523
971
|
|
|
524
972
|
for param in parametes_mapped_types:
|
|
525
973
|
mapped_types.update(extract_all_envolved_types(param))
|
|
@@ -538,10 +986,33 @@ def write_rest_controller_to_typescript_interface(
|
|
|
538
986
|
class_buffer.write(f'\t\t\tmethod: "{mapping.method}",\n')
|
|
539
987
|
|
|
540
988
|
endpoint_path = parse_path_with_params(mapping.path, arg_params_spec)
|
|
541
|
-
|
|
542
|
-
|
|
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,
|
|
543
1011
|
)
|
|
544
|
-
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")
|
|
545
1016
|
|
|
546
1017
|
# Sort headers
|
|
547
1018
|
header_params = sorted(
|
|
@@ -563,10 +1034,25 @@ def write_rest_controller_to_typescript_interface(
|
|
|
563
1034
|
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
564
1035
|
class_buffer.write("\t\t\t},\n")
|
|
565
1036
|
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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:
|
|
1047
|
+
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
1048
|
+
|
|
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")
|
|
570
1056
|
else:
|
|
571
1057
|
class_buffer.write("\t\t\tbody: undefined\n")
|
|
572
1058
|
|
|
@@ -577,7 +1063,44 @@ def write_rest_controller_to_typescript_interface(
|
|
|
577
1063
|
|
|
578
1064
|
class_buffer.write("}\n")
|
|
579
1065
|
|
|
580
|
-
|
|
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
|
|
581
1104
|
|
|
582
1105
|
|
|
583
1106
|
EXCLUDED_REQUESTS_TYPES = [Request, Response]
|
|
@@ -585,18 +1108,233 @@ EXCLUDED_REQUESTS_TYPES = [Request, Response]
|
|
|
585
1108
|
|
|
586
1109
|
@dataclass
|
|
587
1110
|
class HttpParemeterSpec:
|
|
588
|
-
type_: Literal["query", "path", "body", "header", "cookie"]
|
|
1111
|
+
type_: Literal["query", "path", "body", "header", "cookie", "form"]
|
|
589
1112
|
name: str
|
|
590
1113
|
required: bool
|
|
591
1114
|
argument_type_str: str
|
|
592
1115
|
|
|
593
1116
|
|
|
594
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
|
|
595
1123
|
for parameter in parameters:
|
|
596
|
-
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
|
+
|
|
597
1129
|
return path
|
|
598
1130
|
|
|
599
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
|
+
|
|
600
1338
|
def mount_parametes_arguments(parameters: list[HttpParemeterSpec]) -> str:
|
|
601
1339
|
return ", ".join(
|
|
602
1340
|
[f"{parameter.name}: {parameter.argument_type_str}" for parameter in parameters]
|
|
@@ -604,7 +1342,10 @@ def mount_parametes_arguments(parameters: list[HttpParemeterSpec]) -> str:
|
|
|
604
1342
|
|
|
605
1343
|
|
|
606
1344
|
def extract_parameters(
|
|
607
|
-
member: Any,
|
|
1345
|
+
member: Any,
|
|
1346
|
+
controller: RestController,
|
|
1347
|
+
mapping: HttpMapping,
|
|
1348
|
+
type_mapping: dict[Any, Any] | None = None,
|
|
608
1349
|
) -> tuple[list[HttpParemeterSpec], set[Any]]:
|
|
609
1350
|
parameters_list: list[HttpParemeterSpec] = []
|
|
610
1351
|
mapped_types: set[Any] = set()
|
|
@@ -613,7 +1354,7 @@ def extract_parameters(
|
|
|
613
1354
|
if is_primitive(arg):
|
|
614
1355
|
continue
|
|
615
1356
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
616
|
-
arg, controller, mapping
|
|
1357
|
+
arg, controller, mapping, type_mapping
|
|
617
1358
|
)
|
|
618
1359
|
mapped_types.update(rec_mapped_types)
|
|
619
1360
|
parameters_list.extend(rec_parameters)
|
|
@@ -622,22 +1363,23 @@ def extract_parameters(
|
|
|
622
1363
|
if is_primitive(member):
|
|
623
1364
|
|
|
624
1365
|
if get_origin(member) is Annotated:
|
|
1366
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(member)
|
|
625
1367
|
if (
|
|
626
1368
|
plain_validator := next(
|
|
627
|
-
(x for x in
|
|
1369
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
628
1370
|
None,
|
|
629
1371
|
)
|
|
630
1372
|
) is not None:
|
|
631
1373
|
mapped_types.add(plain_validator.json_schema_input_type)
|
|
632
1374
|
return parameters_list, mapped_types
|
|
633
|
-
return extract_parameters(
|
|
1375
|
+
return extract_parameters(unwrapped_type, controller, mapping, type_mapping)
|
|
634
1376
|
return parameters_list, mapped_types
|
|
635
1377
|
|
|
636
1378
|
if hasattr(member, "__bases__"):
|
|
637
1379
|
for base in member.__bases__:
|
|
638
1380
|
# if base is not BaseModel:
|
|
639
1381
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
640
|
-
base, controller, mapping
|
|
1382
|
+
base, controller, mapping, type_mapping
|
|
641
1383
|
)
|
|
642
1384
|
mapped_types.update(rec_mapped_types)
|
|
643
1385
|
parameters_list.extend(rec_parameters)
|
|
@@ -649,9 +1391,26 @@ def extract_parameters(
|
|
|
649
1391
|
if parameter_type in EXCLUDED_REQUESTS_TYPES:
|
|
650
1392
|
continue
|
|
651
1393
|
|
|
1394
|
+
# Resolve generic type
|
|
1395
|
+
if type_mapping and parameter_type in type_mapping:
|
|
1396
|
+
parameter_type = type_mapping[parameter_type]
|
|
1397
|
+
|
|
652
1398
|
if get_origin(parameter_type) == Annotated:
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
655
1414
|
if isinstance(annotated_type_hook, Header):
|
|
656
1415
|
mapped_types.add(str)
|
|
657
1416
|
parameters_list.append(
|
|
@@ -659,7 +1418,9 @@ def extract_parameters(
|
|
|
659
1418
|
type_="header",
|
|
660
1419
|
name=parameter_name,
|
|
661
1420
|
required=True,
|
|
662
|
-
argument_type_str=get_field_type_for_ts(
|
|
1421
|
+
argument_type_str=get_field_type_for_ts(
|
|
1422
|
+
str, "", type_mapping
|
|
1423
|
+
),
|
|
663
1424
|
)
|
|
664
1425
|
)
|
|
665
1426
|
elif isinstance(annotated_type_hook, Cookie):
|
|
@@ -669,37 +1430,87 @@ def extract_parameters(
|
|
|
669
1430
|
type_="cookie",
|
|
670
1431
|
name=parameter_name,
|
|
671
1432
|
required=True,
|
|
672
|
-
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
|
+
),
|
|
673
1448
|
)
|
|
674
1449
|
)
|
|
675
1450
|
elif isinstance(annotated_type_hook, Body):
|
|
676
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
|
+
)
|
|
677
1462
|
parameters_list.append(
|
|
678
1463
|
HttpParemeterSpec(
|
|
679
1464
|
type_="body",
|
|
680
1465
|
name=parameter_name,
|
|
681
1466
|
required=True,
|
|
682
|
-
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
|
+
),
|
|
683
1470
|
)
|
|
684
1471
|
)
|
|
685
1472
|
elif isinstance(annotated_type_hook, Query):
|
|
686
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
|
+
)
|
|
687
1484
|
parameters_list.append(
|
|
688
1485
|
HttpParemeterSpec(
|
|
689
1486
|
type_="query",
|
|
690
1487
|
name=parameter_name,
|
|
691
1488
|
required=True,
|
|
692
|
-
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
|
+
),
|
|
693
1492
|
)
|
|
694
1493
|
)
|
|
695
1494
|
elif isinstance(annotated_type_hook, Path):
|
|
696
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
|
+
)
|
|
697
1506
|
parameters_list.append(
|
|
698
1507
|
HttpParemeterSpec(
|
|
699
1508
|
type_="path",
|
|
700
1509
|
name=parameter_name,
|
|
701
1510
|
required=True,
|
|
702
|
-
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
|
+
),
|
|
703
1514
|
)
|
|
704
1515
|
)
|
|
705
1516
|
|
|
@@ -713,66 +1524,162 @@ def extract_parameters(
|
|
|
713
1524
|
|
|
714
1525
|
else:
|
|
715
1526
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
716
|
-
depends_hook, controller, mapping
|
|
1527
|
+
depends_hook, controller, mapping, type_mapping
|
|
717
1528
|
)
|
|
718
1529
|
mapped_types.update(rec_mapped_types)
|
|
719
1530
|
parameters_list.extend(rec_parameters)
|
|
720
|
-
elif
|
|
1531
|
+
elif (
|
|
1532
|
+
re.search(f":{parameter_name}(?:/|$)", controller.path) is not None
|
|
1533
|
+
):
|
|
721
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
|
+
)
|
|
722
1545
|
parameters_list.append(
|
|
723
1546
|
HttpParemeterSpec(
|
|
724
1547
|
type_="path",
|
|
725
1548
|
name=parameter_name,
|
|
726
1549
|
required=True,
|
|
727
|
-
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
|
+
),
|
|
728
1553
|
)
|
|
729
1554
|
)
|
|
730
1555
|
else:
|
|
731
1556
|
mapped_types.add(annotated_type)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
)
|
|
738
1589
|
)
|
|
739
|
-
)
|
|
740
1590
|
|
|
741
1591
|
elif inspect.isclass(parameter_type) and issubclass(
|
|
742
1592
|
parameter_type, BaseModel
|
|
743
1593
|
):
|
|
744
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
|
+
)
|
|
745
1599
|
parameters_list.append(
|
|
746
1600
|
HttpParemeterSpec(
|
|
747
1601
|
type_="body",
|
|
748
1602
|
name=parameter_name,
|
|
749
1603
|
required=True,
|
|
750
|
-
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
|
+
),
|
|
751
1607
|
)
|
|
752
1608
|
)
|
|
753
|
-
elif (
|
|
754
|
-
|
|
755
|
-
or mapping.path.find(f"{{{parameter_name}}}") != -1
|
|
756
|
-
):
|
|
1609
|
+
elif parameter_type == UploadFile or is_upload_file_type(parameter_type):
|
|
1610
|
+
# UploadFile and list[UploadFile] should be treated as form data
|
|
757
1611
|
mapped_types.add(parameter_type)
|
|
758
1612
|
parameters_list.append(
|
|
759
1613
|
HttpParemeterSpec(
|
|
760
|
-
type_="
|
|
1614
|
+
type_="form",
|
|
761
1615
|
name=parameter_name,
|
|
762
1616
|
required=True,
|
|
763
|
-
argument_type_str=get_field_type_for_ts(
|
|
1617
|
+
argument_type_str=get_field_type_for_ts(
|
|
1618
|
+
parameter_type, "", type_mapping
|
|
1619
|
+
),
|
|
764
1620
|
)
|
|
765
1621
|
)
|
|
766
|
-
|
|
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
|
+
):
|
|
767
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
|
+
)
|
|
768
1638
|
parameters_list.append(
|
|
769
1639
|
HttpParemeterSpec(
|
|
770
|
-
type_="
|
|
1640
|
+
type_="path",
|
|
771
1641
|
name=parameter_name,
|
|
772
1642
|
required=True,
|
|
773
|
-
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
|
+
),
|
|
774
1646
|
)
|
|
775
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
|
+
)
|
|
776
1683
|
|
|
777
1684
|
if inspect.isclass(parameter_type) and not is_primitive(parameter_type):
|
|
778
1685
|
signature = inspect.signature(parameter_type)
|
|
@@ -782,28 +1689,29 @@ def extract_parameters(
|
|
|
782
1689
|
for _, parameter_type in parameter_members.items():
|
|
783
1690
|
if is_primitive(parameter_type.annotation):
|
|
784
1691
|
if get_origin(parameter_type.annotation) is not None:
|
|
785
|
-
if (
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
|
|
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,
|
|
796
1703
|
)
|
|
797
|
-
is not None
|
|
798
|
-
|
|
799
|
-
|
|
1704
|
+
if plain_validator is not None:
|
|
1705
|
+
mapped_types.add(
|
|
1706
|
+
plain_validator.json_schema_input_type
|
|
1707
|
+
)
|
|
800
1708
|
else:
|
|
801
1709
|
args = parameter_type.annotation.__args__
|
|
802
1710
|
mapped_types.update(args)
|
|
803
1711
|
else:
|
|
804
1712
|
continue
|
|
805
1713
|
_, types = extract_parameters(
|
|
806
|
-
parameter_type.annotation, controller, mapping
|
|
1714
|
+
parameter_type.annotation, controller, mapping, type_mapping
|
|
807
1715
|
)
|
|
808
1716
|
mapped_types.update(types)
|
|
809
1717
|
|
|
@@ -811,7 +1719,7 @@ def extract_parameters(
|
|
|
811
1719
|
for arg in member.__args__:
|
|
812
1720
|
|
|
813
1721
|
rec_parameters, rec_mapped_types = extract_parameters(
|
|
814
|
-
arg, controller, mapping
|
|
1722
|
+
arg, controller, mapping, type_mapping
|
|
815
1723
|
)
|
|
816
1724
|
mapped_types.update(rec_mapped_types)
|
|
817
1725
|
parameters_list.extend(rec_parameters)
|
|
@@ -827,22 +1735,15 @@ def extract_all_envolved_types(field_type: Any) -> set[Any]:
|
|
|
827
1735
|
|
|
828
1736
|
if is_primitive(field_type):
|
|
829
1737
|
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
|
-
)
|
|
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,
|
|
841
1743
|
)
|
|
842
|
-
is not None
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
return mapped_types
|
|
1744
|
+
if plain_validator is not None:
|
|
1745
|
+
mapped_types.add(plain_validator.json_schema_input_type)
|
|
1746
|
+
return mapped_types
|
|
846
1747
|
else:
|
|
847
1748
|
mapped_types.update(
|
|
848
1749
|
*[extract_all_envolved_types(arg) for arg in field_type.__args__]
|