jararaca 0.3.11a16__py3-none-any.whl → 0.3.12__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.
Potentially problematic release.
This version of jararaca might be problematic. Click here for more details.
- README.md +120 -0
- jararaca/__init__.py +106 -8
- jararaca/cli.py +216 -31
- jararaca/messagebus/worker.py +749 -272
- jararaca/microservice.py +42 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +82 -73
- jararaca/persistence/interceptors/constants.py +1 -0
- jararaca/persistence/interceptors/decorators.py +45 -0
- jararaca/presentation/server.py +57 -11
- jararaca/presentation/websocket/redis.py +113 -7
- jararaca/reflect/metadata.py +1 -1
- jararaca/rpc/http/__init__.py +97 -0
- jararaca/rpc/http/backends/__init__.py +10 -0
- jararaca/rpc/http/backends/httpx.py +39 -9
- jararaca/rpc/http/decorators.py +302 -6
- jararaca/scheduler/beat_worker.py +550 -91
- jararaca/tools/typescript/__init__.py +0 -0
- jararaca/tools/typescript/decorators.py +95 -0
- jararaca/tools/typescript/interface_parser.py +699 -156
- jararaca-0.3.12.dist-info/LICENSE +674 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/METADATA +4 -3
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/RECORD +26 -19
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/WHEEL +1 -1
- pyproject.toml +86 -0
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/entry_points.txt +0 -0
|
@@ -8,7 +8,7 @@ from datetime import date, datetime, time
|
|
|
8
8
|
from decimal import Decimal
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from io import StringIO
|
|
11
|
-
from types import NoneType, UnionType
|
|
11
|
+
from types import FunctionType, NoneType, UnionType
|
|
12
12
|
from typing import (
|
|
13
13
|
IO,
|
|
14
14
|
Annotated,
|
|
@@ -24,10 +24,10 @@ from typing import (
|
|
|
24
24
|
from uuid import UUID
|
|
25
25
|
|
|
26
26
|
from fastapi import Request, Response, UploadFile
|
|
27
|
-
from fastapi.params import Body, Cookie, Depends, Header, Path, Query
|
|
27
|
+
from fastapi.params import Body, Cookie, Depends, Form, Header, Path, Query
|
|
28
28
|
from fastapi.security.http import HTTPBase
|
|
29
29
|
from pydantic import BaseModel, PlainValidator
|
|
30
|
-
from pydantic_core import
|
|
30
|
+
from pydantic_core import PydanticUndefined
|
|
31
31
|
|
|
32
32
|
from jararaca.microservice import Microservice
|
|
33
33
|
from jararaca.presentation.decorators import HttpMapping, RestController
|
|
@@ -35,6 +35,11 @@ from jararaca.presentation.websocket.decorators import RegisterWebSocketMessage
|
|
|
35
35
|
from jararaca.presentation.websocket.websocket_interceptor import (
|
|
36
36
|
WebSocketMessageWrapper,
|
|
37
37
|
)
|
|
38
|
+
from jararaca.tools.typescript.decorators import (
|
|
39
|
+
MutationEndpoint,
|
|
40
|
+
QueryEndpoint,
|
|
41
|
+
SplitInputOutput,
|
|
42
|
+
)
|
|
38
43
|
|
|
39
44
|
CONSTANT_PATTERN = re.compile(r"^[A-Z_]+$")
|
|
40
45
|
|
|
@@ -43,6 +48,206 @@ def is_constant(name: str) -> bool:
|
|
|
43
48
|
return CONSTANT_PATTERN.match(name) is not None
|
|
44
49
|
|
|
45
50
|
|
|
51
|
+
def unwrap_annotated_type(field_type: Any) -> tuple[Any, list[Any]]:
|
|
52
|
+
"""
|
|
53
|
+
Recursively unwrap Annotated types to find the real underlying type.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
field_type: The type to unwrap, which may be deeply nested Annotated types
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A tuple of (unwrapped_type, all_metadata) where:
|
|
60
|
+
- unwrapped_type is the final non-Annotated type
|
|
61
|
+
- all_metadata is a list of all metadata from all Annotated layers
|
|
62
|
+
"""
|
|
63
|
+
all_metadata = []
|
|
64
|
+
current_type = field_type
|
|
65
|
+
|
|
66
|
+
while get_origin(current_type) == Annotated:
|
|
67
|
+
# Collect metadata from current layer
|
|
68
|
+
if hasattr(current_type, "__metadata__"):
|
|
69
|
+
all_metadata.extend(current_type.__metadata__)
|
|
70
|
+
|
|
71
|
+
# Move to the next inner type
|
|
72
|
+
if hasattr(current_type, "__args__") and len(current_type.__args__) > 0:
|
|
73
|
+
current_type = current_type.__args__[0]
|
|
74
|
+
else:
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
return current_type, all_metadata
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_upload_file_type(field_type: Any) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Check if a type is UploadFile or a list/array of UploadFile.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
field_type: The type to check
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if it's UploadFile or list[UploadFile], False otherwise
|
|
89
|
+
"""
|
|
90
|
+
if field_type == UploadFile:
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
# Check for list[UploadFile], List[UploadFile], etc.
|
|
94
|
+
origin = get_origin(field_type)
|
|
95
|
+
if origin in (list, frozenset, set):
|
|
96
|
+
args = getattr(field_type, "__args__", ())
|
|
97
|
+
if args and args[0] == UploadFile:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def should_exclude_field(
|
|
104
|
+
field_name: str, field_type: Any, basemodel_type: Type[Any]
|
|
105
|
+
) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
Check if a field should be excluded from TypeScript interface generation.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
field_name: The name of the field
|
|
111
|
+
field_type: The type annotation of the field
|
|
112
|
+
basemodel_type: The BaseModel class containing the field
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if the field should be excluded, False otherwise
|
|
116
|
+
"""
|
|
117
|
+
# Check if field is private (starts with underscore)
|
|
118
|
+
if field_name.startswith("_"):
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
# Check if field has Pydantic Field annotation and is excluded via model_fields
|
|
122
|
+
if (
|
|
123
|
+
hasattr(basemodel_type, "model_fields")
|
|
124
|
+
and field_name in basemodel_type.model_fields
|
|
125
|
+
):
|
|
126
|
+
field_info = basemodel_type.model_fields[field_name]
|
|
127
|
+
|
|
128
|
+
# Check if field is excluded via Field(exclude=True)
|
|
129
|
+
if hasattr(field_info, "exclude") and field_info.exclude:
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
# Check if field is marked as private via Field(..., alias=None) pattern
|
|
133
|
+
if (
|
|
134
|
+
hasattr(field_info, "alias")
|
|
135
|
+
and field_info.alias is None
|
|
136
|
+
and field_name.startswith("_")
|
|
137
|
+
):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
# Check for Annotated types with Field metadata
|
|
141
|
+
if get_origin(field_type) == Annotated:
|
|
142
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
143
|
+
for metadata in all_metadata:
|
|
144
|
+
# Check if this is a Pydantic Field by looking for expected attributes
|
|
145
|
+
if hasattr(metadata, "exclude") or hasattr(metadata, "alias"):
|
|
146
|
+
# Check if Field has exclude=True
|
|
147
|
+
if hasattr(metadata, "exclude") and metadata.exclude:
|
|
148
|
+
return True
|
|
149
|
+
# Check for private fields with alias=None
|
|
150
|
+
if (
|
|
151
|
+
hasattr(metadata, "alias")
|
|
152
|
+
and metadata.alias is None
|
|
153
|
+
and field_name.startswith("_")
|
|
154
|
+
):
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
# Check for Field instances assigned as default values
|
|
158
|
+
# This handles cases like: field_name: str = Field(exclude=True)
|
|
159
|
+
if (
|
|
160
|
+
hasattr(basemodel_type, "__annotations__")
|
|
161
|
+
and field_name in basemodel_type.__annotations__
|
|
162
|
+
):
|
|
163
|
+
# Check if there's a default value that's a Field instance
|
|
164
|
+
if hasattr(basemodel_type, field_name):
|
|
165
|
+
default_value = getattr(basemodel_type, field_name, None)
|
|
166
|
+
# Check if default value has Field-like attributes (duck typing approach)
|
|
167
|
+
if default_value is not None and hasattr(default_value, "exclude"):
|
|
168
|
+
if getattr(default_value, "exclude", False):
|
|
169
|
+
return True
|
|
170
|
+
# Check for private fields with alias=None in default Field
|
|
171
|
+
if (
|
|
172
|
+
default_value is not None
|
|
173
|
+
and hasattr(default_value, "alias")
|
|
174
|
+
and getattr(default_value, "alias", None) is None
|
|
175
|
+
and field_name.startswith("_")
|
|
176
|
+
):
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def has_default_value(
|
|
183
|
+
field_name: str, field_type: Any, basemodel_type: Type[Any]
|
|
184
|
+
) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Check if a field has a default value (making it optional in TypeScript).
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
field_name: The name of the field
|
|
190
|
+
field_type: The type annotation of the field
|
|
191
|
+
basemodel_type: The BaseModel class containing the field
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if the field has a default value, False otherwise
|
|
195
|
+
"""
|
|
196
|
+
# Skip literal types as they don't have defaults in the traditional sense
|
|
197
|
+
if get_origin(field_type) is Literal:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Check if field has default in model_fields (standard Pydantic way)
|
|
201
|
+
if (
|
|
202
|
+
hasattr(basemodel_type, "model_fields")
|
|
203
|
+
and field_name in basemodel_type.model_fields
|
|
204
|
+
):
|
|
205
|
+
field_info = basemodel_type.model_fields[field_name]
|
|
206
|
+
if field_info.default is not PydanticUndefined:
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
# Check for Field instances assigned as default values
|
|
210
|
+
# This handles cases like: field_name: str = Field(default="value")
|
|
211
|
+
if (
|
|
212
|
+
hasattr(basemodel_type, "__annotations__")
|
|
213
|
+
and field_name in basemodel_type.__annotations__
|
|
214
|
+
):
|
|
215
|
+
if hasattr(basemodel_type, field_name):
|
|
216
|
+
default_value = getattr(basemodel_type, field_name, None)
|
|
217
|
+
# Check if it's a Field instance with a default
|
|
218
|
+
if default_value is not None and hasattr(default_value, "default"):
|
|
219
|
+
# Check if the Field has a default value set
|
|
220
|
+
field_default = getattr(default_value, "default", PydanticUndefined)
|
|
221
|
+
if field_default is not PydanticUndefined:
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
# Check for non-Field default values assigned directly to class attributes
|
|
225
|
+
# This handles cases like: field_name: str = "default_value"
|
|
226
|
+
if hasattr(basemodel_type, field_name):
|
|
227
|
+
default_value = getattr(basemodel_type, field_name, None)
|
|
228
|
+
# If it's not a Field instance but has a value, it's a default
|
|
229
|
+
if (
|
|
230
|
+
default_value is not None
|
|
231
|
+
and not hasattr(default_value, "exclude") # Not a Field instance
|
|
232
|
+
and not hasattr(default_value, "alias")
|
|
233
|
+
): # Not a Field instance
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
# Check for Annotated types with Field metadata that have defaults
|
|
237
|
+
if get_origin(field_type) == Annotated:
|
|
238
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
239
|
+
for metadata in all_metadata:
|
|
240
|
+
# Check if this is a Pydantic Field with a default
|
|
241
|
+
if hasattr(metadata, "default") and hasattr(
|
|
242
|
+
metadata, "exclude"
|
|
243
|
+
): # Ensure it's a Field
|
|
244
|
+
field_default = getattr(metadata, "default", PydanticUndefined)
|
|
245
|
+
if field_default is not PydanticUndefined:
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
46
251
|
class ParseContext:
|
|
47
252
|
def __init__(self) -> None:
|
|
48
253
|
self.mapped_types: set[Any] = set()
|
|
@@ -67,6 +272,13 @@ def snake_to_camel(snake_str: str) -> str:
|
|
|
67
272
|
return components[0] + "".join(x.title() for x in components[1:])
|
|
68
273
|
|
|
69
274
|
|
|
275
|
+
def pascal_to_camel(pascal_str: str) -> str:
|
|
276
|
+
"""Convert a PascalCase string to camelCase."""
|
|
277
|
+
if not pascal_str:
|
|
278
|
+
return pascal_str
|
|
279
|
+
return pascal_str[0].lower() + pascal_str[1:]
|
|
280
|
+
|
|
281
|
+
|
|
70
282
|
def parse_literal_value(value: Any) -> str:
|
|
71
283
|
if value is None:
|
|
72
284
|
return "null"
|
|
@@ -94,7 +306,14 @@ def parse_literal_value(value: Any) -> str:
|
|
|
94
306
|
return "unknown"
|
|
95
307
|
|
|
96
308
|
|
|
97
|
-
def get_field_type_for_ts(field_type: Any) -> Any:
|
|
309
|
+
def get_field_type_for_ts(field_type: Any, context_suffix: str = "") -> Any:
|
|
310
|
+
"""
|
|
311
|
+
Convert a Python type to its TypeScript equivalent.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
field_type: The Python type to convert
|
|
315
|
+
context_suffix: Suffix for split models (e.g., "Input", "Output")
|
|
316
|
+
"""
|
|
98
317
|
if field_type is Response:
|
|
99
318
|
return "unknown"
|
|
100
319
|
if field_type is Any:
|
|
@@ -122,17 +341,20 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
122
341
|
if field_type == Decimal:
|
|
123
342
|
return "number"
|
|
124
343
|
if get_origin(field_type) == ClassVar:
|
|
125
|
-
return get_field_type_for_ts(field_type.__args__[0])
|
|
344
|
+
return get_field_type_for_ts(field_type.__args__[0], context_suffix)
|
|
126
345
|
if get_origin(field_type) == tuple:
|
|
127
|
-
return f"[{', '.join([get_field_type_for_ts(field) for field in field_type.__args__])}]"
|
|
346
|
+
return f"[{', '.join([get_field_type_for_ts(field, context_suffix) for field in field_type.__args__])}]"
|
|
128
347
|
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])}>"
|
|
348
|
+
return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix)}>"
|
|
130
349
|
if get_origin(field_type) == set:
|
|
131
|
-
return f"Array<{get_field_type_for_ts(field_type.__args__[0])}> // Set"
|
|
350
|
+
return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix)}> // Set"
|
|
132
351
|
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])}}}"
|
|
352
|
+
return f"{{[key: {get_field_type_for_ts(field_type.__args__[0], context_suffix)}]: {get_field_type_for_ts(field_type.__args__[1], context_suffix)}}}"
|
|
134
353
|
if inspect.isclass(field_type):
|
|
135
354
|
if not hasattr(field_type, "__pydantic_generic_metadata__"):
|
|
355
|
+
# Check if this is a split model and use appropriate suffix
|
|
356
|
+
if SplitInputOutput.is_split_model(field_type) and context_suffix:
|
|
357
|
+
return f"{field_type.__name__}{context_suffix}"
|
|
136
358
|
return field_type.__name__
|
|
137
359
|
pydantic_metadata = getattr(field_type, "__pydantic_generic_metadata__")
|
|
138
360
|
|
|
@@ -141,12 +363,17 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
141
363
|
if pydantic_metadata.get("origin") is not None
|
|
142
364
|
else field_type.__name__
|
|
143
365
|
)
|
|
366
|
+
|
|
367
|
+
# Check if this is a split model and use appropriate suffix
|
|
368
|
+
if SplitInputOutput.is_split_model(field_type) and context_suffix:
|
|
369
|
+
name = f"{field_type.__name__}{context_suffix}"
|
|
370
|
+
|
|
144
371
|
args = pydantic_metadata.get("args")
|
|
145
372
|
|
|
146
373
|
if len(args) > 0:
|
|
147
374
|
return "%s<%s>" % (
|
|
148
375
|
name,
|
|
149
|
-
", ".join([get_field_type_for_ts(arg) for arg in args]),
|
|
376
|
+
", ".join([get_field_type_for_ts(arg, context_suffix) for arg in args]),
|
|
150
377
|
)
|
|
151
378
|
|
|
152
379
|
return name
|
|
@@ -156,16 +383,22 @@ def get_field_type_for_ts(field_type: Any) -> Any:
|
|
|
156
383
|
if get_origin(field_type) == Literal:
|
|
157
384
|
return " | ".join([parse_literal_value(x) for x in field_type.__args__])
|
|
158
385
|
if get_origin(field_type) == UnionType or get_origin(field_type) == typing.Union:
|
|
159
|
-
return " | ".join(
|
|
386
|
+
return " | ".join(
|
|
387
|
+
[get_field_type_for_ts(x, context_suffix) for x in field_type.__args__]
|
|
388
|
+
)
|
|
160
389
|
if (get_origin(field_type) == Annotated) and (len(field_type.__args__) > 0):
|
|
390
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
391
|
+
|
|
161
392
|
if (
|
|
162
393
|
plain_validator := next(
|
|
163
|
-
(x for x in
|
|
394
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
164
395
|
None,
|
|
165
396
|
)
|
|
166
397
|
) is not None:
|
|
167
|
-
return get_field_type_for_ts(
|
|
168
|
-
|
|
398
|
+
return get_field_type_for_ts(
|
|
399
|
+
plain_validator.json_schema_input_type, context_suffix
|
|
400
|
+
)
|
|
401
|
+
return get_field_type_for_ts(unwrapped_type, context_suffix)
|
|
169
402
|
return "unknown"
|
|
170
403
|
|
|
171
404
|
|
|
@@ -180,6 +413,59 @@ def get_generic_args(field_type: Any) -> Any:
|
|
|
180
413
|
def parse_type_to_typescript_interface(
|
|
181
414
|
basemodel_type: Type[Any],
|
|
182
415
|
) -> tuple[set[type], str]:
|
|
416
|
+
"""
|
|
417
|
+
Parse a Pydantic model into TypeScript interface(s).
|
|
418
|
+
|
|
419
|
+
If the model is decorated with @SplitInputOutput, it generates both Input and Output interfaces.
|
|
420
|
+
Otherwise, it generates a single interface.
|
|
421
|
+
"""
|
|
422
|
+
# Check if this model should be split into Input/Output interfaces
|
|
423
|
+
if SplitInputOutput.is_split_model(basemodel_type):
|
|
424
|
+
return parse_split_input_output_interfaces(basemodel_type)
|
|
425
|
+
|
|
426
|
+
return parse_single_typescript_interface(basemodel_type)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def parse_split_input_output_interfaces(
|
|
430
|
+
basemodel_type: Type[Any],
|
|
431
|
+
) -> tuple[set[type], str]:
|
|
432
|
+
"""
|
|
433
|
+
Generate both Input and Output TypeScript interfaces for a split model.
|
|
434
|
+
"""
|
|
435
|
+
mapped_types: set[type] = set()
|
|
436
|
+
combined_output = StringIO()
|
|
437
|
+
|
|
438
|
+
# Generate Input interface (with optional fields)
|
|
439
|
+
input_mapped_types, input_interface = parse_single_typescript_interface(
|
|
440
|
+
basemodel_type, interface_suffix="Input", force_optional_defaults=True
|
|
441
|
+
)
|
|
442
|
+
mapped_types.update(input_mapped_types)
|
|
443
|
+
combined_output.write(input_interface)
|
|
444
|
+
|
|
445
|
+
# Generate Output interface (all fields required as they come from the backend)
|
|
446
|
+
output_mapped_types, output_interface = parse_single_typescript_interface(
|
|
447
|
+
basemodel_type, interface_suffix="Output", force_optional_defaults=False
|
|
448
|
+
)
|
|
449
|
+
mapped_types.update(output_mapped_types)
|
|
450
|
+
combined_output.write(output_interface)
|
|
451
|
+
|
|
452
|
+
return mapped_types, combined_output.getvalue()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def parse_single_typescript_interface(
|
|
456
|
+
basemodel_type: Type[Any],
|
|
457
|
+
interface_suffix: str = "",
|
|
458
|
+
force_optional_defaults: bool | None = None,
|
|
459
|
+
) -> tuple[set[type], str]:
|
|
460
|
+
"""
|
|
461
|
+
Generate a single TypeScript interface for a Pydantic model.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
basemodel_type: The Pydantic model class
|
|
465
|
+
interface_suffix: Suffix to add to the interface name (e.g., "Input", "Output")
|
|
466
|
+
force_optional_defaults: If True, fields with defaults are optional. If False, all fields are required.
|
|
467
|
+
If None, uses the default behavior (fields with defaults are optional).
|
|
468
|
+
"""
|
|
183
469
|
string_builder = StringIO()
|
|
184
470
|
mapped_types: set[type] = set()
|
|
185
471
|
|
|
@@ -206,7 +492,7 @@ def parse_type_to_typescript_interface(
|
|
|
206
492
|
enum_values = sorted([(x._name_, x.value) for x in basemodel_type])
|
|
207
493
|
return (
|
|
208
494
|
set(),
|
|
209
|
-
f"export enum {basemodel_type.__name__} {{\n"
|
|
495
|
+
f"export enum {basemodel_type.__name__}{interface_suffix} {{\n"
|
|
210
496
|
+ "\n ".join([f'\t{name} = "{value}",' for name, value in enum_values])
|
|
211
497
|
+ "\n}\n",
|
|
212
498
|
)
|
|
@@ -230,46 +516,46 @@ def parse_type_to_typescript_interface(
|
|
|
230
516
|
for inherited_class in valid_inherited_classes
|
|
231
517
|
}
|
|
232
518
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
519
|
+
# Modify inheritance for split interfaces
|
|
520
|
+
extends_expression = ""
|
|
521
|
+
if len(valid_inherited_classes) > 0:
|
|
522
|
+
extends_base_names = []
|
|
523
|
+
for inherited_class in valid_inherited_classes:
|
|
524
|
+
base_name = get_field_type_for_ts(inherited_class, interface_suffix)
|
|
525
|
+
# If the inherited class is also a split model, use the appropriate suffix
|
|
526
|
+
if SplitInputOutput.is_split_model(inherited_class) and interface_suffix:
|
|
527
|
+
base_name = f"{inherited_class.__name__}{interface_suffix}"
|
|
528
|
+
|
|
529
|
+
if inherited_classes_consts_conflict[inherited_class]:
|
|
530
|
+
base_name = "Omit<%s, %s>" % (
|
|
531
|
+
base_name,
|
|
532
|
+
" | ".join(
|
|
533
|
+
sorted(
|
|
534
|
+
[
|
|
535
|
+
'"%s"' % field_name
|
|
536
|
+
for field_name in inherited_classes_consts_conflict[
|
|
537
|
+
inherited_class
|
|
538
|
+
]
|
|
539
|
+
]
|
|
254
540
|
)
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
extends_base_names.append(base_name)
|
|
544
|
+
|
|
545
|
+
extends_expression = " extends %s" % ", ".join(
|
|
546
|
+
sorted(extends_base_names, key=lambda x: str(x))
|
|
260
547
|
)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
548
|
+
|
|
549
|
+
interface_name = f"{basemodel_type.__name__}{interface_suffix}"
|
|
264
550
|
|
|
265
551
|
if is_generic_type(basemodel_type):
|
|
266
552
|
generic_args = get_generic_args(basemodel_type)
|
|
267
553
|
string_builder.write(
|
|
268
|
-
f"export interface {
|
|
554
|
+
f"export interface {interface_name}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
|
|
269
555
|
)
|
|
270
556
|
else:
|
|
271
557
|
string_builder.write(
|
|
272
|
-
f"export interface {
|
|
558
|
+
f"export interface {interface_name}{extends_expression} {{\n"
|
|
273
559
|
)
|
|
274
560
|
|
|
275
561
|
if hasattr(basemodel_type, "__annotations__"):
|
|
@@ -282,16 +568,23 @@ def parse_type_to_typescript_interface(
|
|
|
282
568
|
if field_name in cls_consts:
|
|
283
569
|
continue
|
|
284
570
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
571
|
+
# Check if field should be excluded (private or excluded via Field)
|
|
572
|
+
if should_exclude_field(field_name, field, basemodel_type):
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
# Determine if field is optional based on the force_optional_defaults parameter
|
|
576
|
+
if force_optional_defaults is True:
|
|
577
|
+
# Input interface: fields with defaults are optional
|
|
578
|
+
is_optional = has_default_value(field_name, field, basemodel_type)
|
|
579
|
+
elif force_optional_defaults is False:
|
|
580
|
+
# Output interface: all fields are required (backend provides complete data)
|
|
581
|
+
is_optional = False
|
|
582
|
+
else:
|
|
583
|
+
# Default behavior: fields with defaults are optional
|
|
584
|
+
is_optional = has_default_value(field_name, field, basemodel_type)
|
|
585
|
+
|
|
293
586
|
string_builder.write(
|
|
294
|
-
f" {snake_to_camel(field_name) if not is_constant(field_name) else field_name}{'?' if
|
|
587
|
+
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
588
|
)
|
|
296
589
|
mapped_types.update(extract_all_envolved_types(field))
|
|
297
590
|
mapped_types.add(field)
|
|
@@ -318,7 +611,7 @@ def parse_type_to_typescript_interface(
|
|
|
318
611
|
return_type = NoneType
|
|
319
612
|
|
|
320
613
|
string_builder.write(
|
|
321
|
-
f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type)};\n"
|
|
614
|
+
f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type, interface_suffix)};\n"
|
|
322
615
|
)
|
|
323
616
|
mapped_types.update(extract_all_envolved_types(return_type))
|
|
324
617
|
mapped_types.add(return_type)
|
|
@@ -354,12 +647,17 @@ def write_microservice_to_typescript_interface(
|
|
|
354
647
|
if rest_controller is None:
|
|
355
648
|
continue
|
|
356
649
|
|
|
357
|
-
|
|
358
|
-
|
|
650
|
+
controller_class_strio, types, hooks_strio = (
|
|
651
|
+
write_rest_controller_to_typescript_interface(
|
|
652
|
+
rest_controller,
|
|
653
|
+
controller,
|
|
654
|
+
)
|
|
359
655
|
)
|
|
360
656
|
|
|
361
657
|
mapped_types_set.update(types)
|
|
362
|
-
rest_controller_buffer.write(
|
|
658
|
+
rest_controller_buffer.write(controller_class_strio.getvalue())
|
|
659
|
+
if hooks_strio is not None:
|
|
660
|
+
rest_controller_buffer.write(hooks_strio.getvalue())
|
|
363
661
|
|
|
364
662
|
registered = RegisterWebSocketMessage.get(controller)
|
|
365
663
|
|
|
@@ -376,6 +674,42 @@ def write_microservice_to_typescript_interface(
|
|
|
376
674
|
|
|
377
675
|
final_buffer.write(
|
|
378
676
|
"""
|
|
677
|
+
/* eslint-disable */
|
|
678
|
+
|
|
679
|
+
// @ts-nocheck
|
|
680
|
+
|
|
681
|
+
// noinspection JSUnusedGlobalSymbols
|
|
682
|
+
|
|
683
|
+
import { HttpService, HttpBackend, HttpBackendRequest, ResponseType, createClassQueryHooks , createClassMutationHooks, createClassInfiniteQueryHooks, paginationModelByFirstArgPaginationFilter } from "@jararaca/core";
|
|
684
|
+
|
|
685
|
+
function makeFormData(data: Record<string, any>): FormData {
|
|
686
|
+
const formData = new FormData();
|
|
687
|
+
for (const key in data) {
|
|
688
|
+
const value = data[key];
|
|
689
|
+
for (const v of genFormDataValue(value)) {
|
|
690
|
+
formData.append(key, v);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return formData;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function* genFormDataValue(value: any): any {
|
|
697
|
+
if (Array.isArray(value)) {
|
|
698
|
+
// Stringify arrays as JSON
|
|
699
|
+
for (const item of value) {
|
|
700
|
+
// formData.append(`${key}`, item);
|
|
701
|
+
yield* genFormDataValue(item);
|
|
702
|
+
}
|
|
703
|
+
} else if (typeof value === "object" && value.constructor === Object) {
|
|
704
|
+
// Stringify plain objects as JSON
|
|
705
|
+
// formData.append(key, JSON.stringify(value));
|
|
706
|
+
yield JSON.stringify(value);
|
|
707
|
+
} else {
|
|
708
|
+
// For primitives (string, number, boolean), append as-is
|
|
709
|
+
yield value;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
379
713
|
export type WebSocketMessageMap = {
|
|
380
714
|
%s
|
|
381
715
|
}
|
|
@@ -391,36 +725,6 @@ export type WebSocketMessageMap = {
|
|
|
391
725
|
)
|
|
392
726
|
)
|
|
393
727
|
|
|
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
728
|
processed_types: set[Any] = set()
|
|
425
729
|
backlog: set[Any] = mapped_types_set.copy()
|
|
426
730
|
|
|
@@ -493,11 +797,16 @@ def is_primitive(field_type: Any) -> bool:
|
|
|
493
797
|
|
|
494
798
|
def write_rest_controller_to_typescript_interface(
|
|
495
799
|
rest_controller: RestController, controller: type
|
|
496
|
-
) -> tuple[
|
|
800
|
+
) -> tuple[StringIO, set[Any], StringIO | None]:
|
|
801
|
+
|
|
802
|
+
class_name = controller.__name__
|
|
803
|
+
|
|
804
|
+
decorated_queries: list[tuple[str, FunctionType, QueryEndpoint]] = []
|
|
805
|
+
decorated_mutations: list[tuple[str, FunctionType]] = []
|
|
497
806
|
|
|
498
807
|
class_buffer = StringIO()
|
|
499
808
|
|
|
500
|
-
class_buffer.write(f"export class {
|
|
809
|
+
class_buffer.write(f"export class {class_name} extends HttpService {{\n")
|
|
501
810
|
|
|
502
811
|
mapped_types: set[Any] = set()
|
|
503
812
|
|
|
@@ -513,9 +822,15 @@ def write_rest_controller_to_typescript_interface(
|
|
|
513
822
|
if return_type is None:
|
|
514
823
|
return_type = NoneType
|
|
515
824
|
|
|
825
|
+
if query_endpoint := QueryEndpoint.extract_query_endpoint(member):
|
|
826
|
+
decorated_queries.append((name, member, query_endpoint))
|
|
827
|
+
if MutationEndpoint.is_mutation(member):
|
|
828
|
+
decorated_mutations.append((name, member))
|
|
829
|
+
|
|
516
830
|
mapped_types.update(extract_all_envolved_types(return_type))
|
|
517
831
|
|
|
518
|
-
|
|
832
|
+
# For return types, use Output suffix if it's a split model
|
|
833
|
+
return_value_repr = get_field_type_for_ts(return_type, "Output")
|
|
519
834
|
|
|
520
835
|
arg_params_spec, parametes_mapped_types = extract_parameters(
|
|
521
836
|
member, rest_controller, mapping
|
|
@@ -538,10 +853,31 @@ def write_rest_controller_to_typescript_interface(
|
|
|
538
853
|
class_buffer.write(f'\t\t\tmethod: "{mapping.method}",\n')
|
|
539
854
|
|
|
540
855
|
endpoint_path = parse_path_with_params(mapping.path, arg_params_spec)
|
|
541
|
-
|
|
542
|
-
|
|
856
|
+
|
|
857
|
+
# Properly handle path joining to avoid double slashes
|
|
858
|
+
controller_path = rest_controller.path or ""
|
|
859
|
+
path_parts = []
|
|
860
|
+
|
|
861
|
+
if controller_path and controller_path.strip("/"):
|
|
862
|
+
path_parts.append(controller_path.strip("/"))
|
|
863
|
+
if endpoint_path and endpoint_path.strip("/"):
|
|
864
|
+
path_parts.append(endpoint_path.strip("/"))
|
|
865
|
+
|
|
866
|
+
final_path = "/".join(path_parts) if path_parts else ""
|
|
867
|
+
# Ensure the path starts with a single slash
|
|
868
|
+
formatted_path = f"/{final_path}" if final_path else "/"
|
|
869
|
+
|
|
870
|
+
class_buffer.write(f"\t\t\tpath: `{formatted_path}`,\n")
|
|
871
|
+
|
|
872
|
+
# Sort path params
|
|
873
|
+
path_params = sorted(
|
|
874
|
+
[param for param in arg_params_spec if param.type_ == "path"],
|
|
875
|
+
key=lambda x: x.name,
|
|
543
876
|
)
|
|
544
|
-
class_buffer.write(
|
|
877
|
+
class_buffer.write("\t\t\tpathParams: {\n")
|
|
878
|
+
for param in path_params:
|
|
879
|
+
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
880
|
+
class_buffer.write("\t\t\t},\n")
|
|
545
881
|
|
|
546
882
|
# Sort headers
|
|
547
883
|
header_params = sorted(
|
|
@@ -563,10 +899,25 @@ def write_rest_controller_to_typescript_interface(
|
|
|
563
899
|
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
564
900
|
class_buffer.write("\t\t\t},\n")
|
|
565
901
|
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
902
|
+
# Check if we need to use FormData (for file uploads or form parameters)
|
|
903
|
+
form_params = [param for param in arg_params_spec if param.type_ == "form"]
|
|
904
|
+
body_param = next((x for x in arg_params_spec if x.type_ == "body"), None)
|
|
905
|
+
|
|
906
|
+
if form_params:
|
|
907
|
+
# Use FormData for file uploads and form parameters
|
|
908
|
+
class_buffer.write("\t\t\tbody: makeFormData({\n")
|
|
909
|
+
|
|
910
|
+
# Add form parameters (including file uploads)
|
|
911
|
+
for param in form_params:
|
|
912
|
+
class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
|
|
913
|
+
|
|
914
|
+
# Add body parameter if it exists alongside form params
|
|
915
|
+
if body_param:
|
|
916
|
+
class_buffer.write(f"\t\t\t\t...{body_param.name},\n")
|
|
917
|
+
|
|
918
|
+
class_buffer.write("\t\t\t})\n")
|
|
919
|
+
elif body_param is not None:
|
|
920
|
+
class_buffer.write(f"\t\t\tbody: {body_param.name}\n")
|
|
570
921
|
else:
|
|
571
922
|
class_buffer.write("\t\t\tbody: undefined\n")
|
|
572
923
|
|
|
@@ -577,7 +928,44 @@ def write_rest_controller_to_typescript_interface(
|
|
|
577
928
|
|
|
578
929
|
class_buffer.write("}\n")
|
|
579
930
|
|
|
580
|
-
|
|
931
|
+
controller_hooks_builder: StringIO | None = None
|
|
932
|
+
|
|
933
|
+
if decorated_queries or decorated_mutations:
|
|
934
|
+
controller_hooks_builder = StringIO()
|
|
935
|
+
controller_hooks_builder.write(
|
|
936
|
+
f"export const {pascal_to_camel(class_name)} = {{\n"
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
if decorated_queries:
|
|
940
|
+
controller_hooks_builder.write(
|
|
941
|
+
f"\t...createClassQueryHooks({class_name},\n"
|
|
942
|
+
)
|
|
943
|
+
for name, member, _ in decorated_queries:
|
|
944
|
+
controller_hooks_builder.write(f'\t\t"{snake_to_camel(name)}",\n')
|
|
945
|
+
controller_hooks_builder.write("\t),\n")
|
|
946
|
+
|
|
947
|
+
if decorated_queries and any(
|
|
948
|
+
query.has_infinite_query for _, _, query in decorated_queries
|
|
949
|
+
):
|
|
950
|
+
controller_hooks_builder.write(
|
|
951
|
+
f"\t...createClassInfiniteQueryHooks({class_name}, {{\n"
|
|
952
|
+
)
|
|
953
|
+
for name, member, query in decorated_queries:
|
|
954
|
+
if query.has_infinite_query:
|
|
955
|
+
controller_hooks_builder.write(
|
|
956
|
+
f'\t\t"{snake_to_camel(name)}": paginationModelByFirstArgPaginationFilter(),\n'
|
|
957
|
+
)
|
|
958
|
+
controller_hooks_builder.write("\t}),\n")
|
|
959
|
+
if decorated_mutations:
|
|
960
|
+
controller_hooks_builder.write(
|
|
961
|
+
f"\t...createClassMutationHooks({class_name},\n"
|
|
962
|
+
)
|
|
963
|
+
for name, member in decorated_mutations:
|
|
964
|
+
controller_hooks_builder.write(f'\t\t"{snake_to_camel(name)}",\n')
|
|
965
|
+
controller_hooks_builder.write("\t),\n")
|
|
966
|
+
controller_hooks_builder.write("};\n")
|
|
967
|
+
|
|
968
|
+
return class_buffer, mapped_types, controller_hooks_builder
|
|
581
969
|
|
|
582
970
|
|
|
583
971
|
EXCLUDED_REQUESTS_TYPES = [Request, Response]
|
|
@@ -585,15 +973,26 @@ EXCLUDED_REQUESTS_TYPES = [Request, Response]
|
|
|
585
973
|
|
|
586
974
|
@dataclass
|
|
587
975
|
class HttpParemeterSpec:
|
|
588
|
-
type_: Literal["query", "path", "body", "header", "cookie"]
|
|
976
|
+
type_: Literal["query", "path", "body", "header", "cookie", "form"]
|
|
589
977
|
name: str
|
|
590
978
|
required: bool
|
|
591
979
|
argument_type_str: str
|
|
592
980
|
|
|
593
981
|
|
|
594
982
|
def parse_path_with_params(path: str, parameters: list[HttpParemeterSpec]) -> str:
|
|
983
|
+
# Use a regular expression to match both simple parameters {param} and
|
|
984
|
+
# parameters with converters {param:converter}
|
|
985
|
+
import re
|
|
986
|
+
|
|
987
|
+
pattern = re.compile(r"{([^:}]+)(?::[^}]*)?}")
|
|
988
|
+
|
|
989
|
+
# For each parameter found in the path, replace it with :param format
|
|
595
990
|
for parameter in parameters:
|
|
596
|
-
path =
|
|
991
|
+
path = pattern.sub(
|
|
992
|
+
lambda m: f":{m.group(1)}" if m.group(1) == parameter.name else m.group(0),
|
|
993
|
+
path,
|
|
994
|
+
)
|
|
995
|
+
|
|
597
996
|
return path
|
|
598
997
|
|
|
599
998
|
|
|
@@ -622,15 +1021,16 @@ def extract_parameters(
|
|
|
622
1021
|
if is_primitive(member):
|
|
623
1022
|
|
|
624
1023
|
if get_origin(member) is Annotated:
|
|
1024
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(member)
|
|
625
1025
|
if (
|
|
626
1026
|
plain_validator := next(
|
|
627
|
-
(x for x in
|
|
1027
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
628
1028
|
None,
|
|
629
1029
|
)
|
|
630
1030
|
) is not None:
|
|
631
1031
|
mapped_types.add(plain_validator.json_schema_input_type)
|
|
632
1032
|
return parameters_list, mapped_types
|
|
633
|
-
return extract_parameters(
|
|
1033
|
+
return extract_parameters(unwrapped_type, controller, mapping)
|
|
634
1034
|
return parameters_list, mapped_types
|
|
635
1035
|
|
|
636
1036
|
if hasattr(member, "__bases__"):
|
|
@@ -650,8 +1050,21 @@ def extract_parameters(
|
|
|
650
1050
|
continue
|
|
651
1051
|
|
|
652
1052
|
if get_origin(parameter_type) == Annotated:
|
|
653
|
-
|
|
654
|
-
|
|
1053
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(parameter_type)
|
|
1054
|
+
# Look for FastAPI parameter annotations in all metadata layers
|
|
1055
|
+
annotated_type_hook = None
|
|
1056
|
+
for metadata in all_metadata:
|
|
1057
|
+
if isinstance(
|
|
1058
|
+
metadata, (Header, Cookie, Form, Body, Query, Path, Depends)
|
|
1059
|
+
):
|
|
1060
|
+
annotated_type_hook = metadata
|
|
1061
|
+
break
|
|
1062
|
+
|
|
1063
|
+
if annotated_type_hook is None and all_metadata:
|
|
1064
|
+
# Fallback to first metadata if no FastAPI annotation found
|
|
1065
|
+
annotated_type_hook = all_metadata[0]
|
|
1066
|
+
|
|
1067
|
+
annotated_type = unwrapped_type
|
|
655
1068
|
if isinstance(annotated_type_hook, Header):
|
|
656
1069
|
mapped_types.add(str)
|
|
657
1070
|
parameters_list.append(
|
|
@@ -672,34 +1085,80 @@ def extract_parameters(
|
|
|
672
1085
|
argument_type_str=get_field_type_for_ts(str),
|
|
673
1086
|
)
|
|
674
1087
|
)
|
|
1088
|
+
elif isinstance(annotated_type_hook, Form):
|
|
1089
|
+
mapped_types.add(annotated_type)
|
|
1090
|
+
parameters_list.append(
|
|
1091
|
+
HttpParemeterSpec(
|
|
1092
|
+
type_="form",
|
|
1093
|
+
name=parameter_name,
|
|
1094
|
+
required=True,
|
|
1095
|
+
argument_type_str=get_field_type_for_ts(annotated_type),
|
|
1096
|
+
)
|
|
1097
|
+
)
|
|
675
1098
|
elif isinstance(annotated_type_hook, Body):
|
|
676
1099
|
mapped_types.update(extract_all_envolved_types(parameter_type))
|
|
1100
|
+
# For body parameters, use Input suffix if it's a split model
|
|
1101
|
+
context_suffix = (
|
|
1102
|
+
"Input"
|
|
1103
|
+
if (
|
|
1104
|
+
inspect.isclass(parameter_type)
|
|
1105
|
+
and hasattr(parameter_type, "__dict__")
|
|
1106
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1107
|
+
)
|
|
1108
|
+
else ""
|
|
1109
|
+
)
|
|
677
1110
|
parameters_list.append(
|
|
678
1111
|
HttpParemeterSpec(
|
|
679
1112
|
type_="body",
|
|
680
1113
|
name=parameter_name,
|
|
681
1114
|
required=True,
|
|
682
|
-
argument_type_str=get_field_type_for_ts(
|
|
1115
|
+
argument_type_str=get_field_type_for_ts(
|
|
1116
|
+
parameter_type, context_suffix
|
|
1117
|
+
),
|
|
683
1118
|
)
|
|
684
1119
|
)
|
|
685
1120
|
elif isinstance(annotated_type_hook, Query):
|
|
686
1121
|
mapped_types.add(parameter_type)
|
|
1122
|
+
# For query parameters, use Input suffix if it's a split model
|
|
1123
|
+
context_suffix = (
|
|
1124
|
+
"Input"
|
|
1125
|
+
if (
|
|
1126
|
+
inspect.isclass(parameter_type)
|
|
1127
|
+
and hasattr(parameter_type, "__dict__")
|
|
1128
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1129
|
+
)
|
|
1130
|
+
else ""
|
|
1131
|
+
)
|
|
687
1132
|
parameters_list.append(
|
|
688
1133
|
HttpParemeterSpec(
|
|
689
1134
|
type_="query",
|
|
690
1135
|
name=parameter_name,
|
|
691
1136
|
required=True,
|
|
692
|
-
argument_type_str=get_field_type_for_ts(
|
|
1137
|
+
argument_type_str=get_field_type_for_ts(
|
|
1138
|
+
parameter_type, context_suffix
|
|
1139
|
+
),
|
|
693
1140
|
)
|
|
694
1141
|
)
|
|
695
1142
|
elif isinstance(annotated_type_hook, Path):
|
|
696
1143
|
mapped_types.add(parameter_type)
|
|
1144
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1145
|
+
context_suffix = (
|
|
1146
|
+
"Input"
|
|
1147
|
+
if (
|
|
1148
|
+
inspect.isclass(parameter_type)
|
|
1149
|
+
and hasattr(parameter_type, "__dict__")
|
|
1150
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1151
|
+
)
|
|
1152
|
+
else ""
|
|
1153
|
+
)
|
|
697
1154
|
parameters_list.append(
|
|
698
1155
|
HttpParemeterSpec(
|
|
699
1156
|
type_="path",
|
|
700
1157
|
name=parameter_name,
|
|
701
1158
|
required=True,
|
|
702
|
-
argument_type_str=get_field_type_for_ts(
|
|
1159
|
+
argument_type_str=get_field_type_for_ts(
|
|
1160
|
+
parameter_type, context_suffix
|
|
1161
|
+
),
|
|
703
1162
|
)
|
|
704
1163
|
)
|
|
705
1164
|
|
|
@@ -717,62 +1176,152 @@ def extract_parameters(
|
|
|
717
1176
|
)
|
|
718
1177
|
mapped_types.update(rec_mapped_types)
|
|
719
1178
|
parameters_list.extend(rec_parameters)
|
|
720
|
-
elif
|
|
1179
|
+
elif (
|
|
1180
|
+
re.search(f":{parameter_name}(?:/|$)", controller.path) is not None
|
|
1181
|
+
):
|
|
721
1182
|
mapped_types.add(annotated_type)
|
|
1183
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1184
|
+
context_suffix = (
|
|
1185
|
+
"Input"
|
|
1186
|
+
if (
|
|
1187
|
+
inspect.isclass(annotated_type)
|
|
1188
|
+
and hasattr(annotated_type, "__dict__")
|
|
1189
|
+
and SplitInputOutput.is_split_model(annotated_type)
|
|
1190
|
+
)
|
|
1191
|
+
else ""
|
|
1192
|
+
)
|
|
722
1193
|
parameters_list.append(
|
|
723
1194
|
HttpParemeterSpec(
|
|
724
1195
|
type_="path",
|
|
725
1196
|
name=parameter_name,
|
|
726
1197
|
required=True,
|
|
727
|
-
argument_type_str=get_field_type_for_ts(
|
|
1198
|
+
argument_type_str=get_field_type_for_ts(
|
|
1199
|
+
annotated_type, context_suffix
|
|
1200
|
+
),
|
|
728
1201
|
)
|
|
729
1202
|
)
|
|
730
1203
|
else:
|
|
731
1204
|
mapped_types.add(annotated_type)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1205
|
+
# Special handling for UploadFile and list[UploadFile] - should be treated as form data
|
|
1206
|
+
if is_upload_file_type(annotated_type):
|
|
1207
|
+
parameters_list.append(
|
|
1208
|
+
HttpParemeterSpec(
|
|
1209
|
+
type_="form",
|
|
1210
|
+
name=parameter_name,
|
|
1211
|
+
required=True,
|
|
1212
|
+
argument_type_str=get_field_type_for_ts(annotated_type),
|
|
1213
|
+
)
|
|
1214
|
+
)
|
|
1215
|
+
else:
|
|
1216
|
+
# For default parameters (treated as query), use Input suffix if it's a split model
|
|
1217
|
+
context_suffix = (
|
|
1218
|
+
"Input"
|
|
1219
|
+
if (
|
|
1220
|
+
inspect.isclass(annotated_type)
|
|
1221
|
+
and hasattr(annotated_type, "__dict__")
|
|
1222
|
+
and SplitInputOutput.is_split_model(annotated_type)
|
|
1223
|
+
)
|
|
1224
|
+
else ""
|
|
1225
|
+
)
|
|
1226
|
+
parameters_list.append(
|
|
1227
|
+
HttpParemeterSpec(
|
|
1228
|
+
type_="query",
|
|
1229
|
+
name=parameter_name,
|
|
1230
|
+
required=True,
|
|
1231
|
+
argument_type_str=get_field_type_for_ts(
|
|
1232
|
+
annotated_type, context_suffix
|
|
1233
|
+
),
|
|
1234
|
+
)
|
|
738
1235
|
)
|
|
739
|
-
)
|
|
740
1236
|
|
|
741
1237
|
elif inspect.isclass(parameter_type) and issubclass(
|
|
742
1238
|
parameter_type, BaseModel
|
|
743
1239
|
):
|
|
744
1240
|
mapped_types.update(extract_all_envolved_types(parameter_type))
|
|
1241
|
+
# For BaseModel parameters, use Input suffix if it's a split model
|
|
1242
|
+
context_suffix = (
|
|
1243
|
+
"Input" if SplitInputOutput.is_split_model(parameter_type) else ""
|
|
1244
|
+
)
|
|
745
1245
|
parameters_list.append(
|
|
746
1246
|
HttpParemeterSpec(
|
|
747
1247
|
type_="body",
|
|
748
1248
|
name=parameter_name,
|
|
749
1249
|
required=True,
|
|
750
|
-
argument_type_str=get_field_type_for_ts(
|
|
1250
|
+
argument_type_str=get_field_type_for_ts(
|
|
1251
|
+
parameter_type, context_suffix
|
|
1252
|
+
),
|
|
751
1253
|
)
|
|
752
1254
|
)
|
|
753
|
-
elif (
|
|
754
|
-
|
|
755
|
-
or mapping.path.find(f"{{{parameter_name}}}") != -1
|
|
756
|
-
):
|
|
1255
|
+
elif parameter_type == UploadFile or is_upload_file_type(parameter_type):
|
|
1256
|
+
# UploadFile and list[UploadFile] should be treated as form data
|
|
757
1257
|
mapped_types.add(parameter_type)
|
|
758
1258
|
parameters_list.append(
|
|
759
1259
|
HttpParemeterSpec(
|
|
760
|
-
type_="
|
|
1260
|
+
type_="form",
|
|
761
1261
|
name=parameter_name,
|
|
762
1262
|
required=True,
|
|
763
1263
|
argument_type_str=get_field_type_for_ts(parameter_type),
|
|
764
1264
|
)
|
|
765
1265
|
)
|
|
766
|
-
|
|
1266
|
+
elif (
|
|
1267
|
+
# Match both simple parameters {param} and parameters with converters {param:converter}
|
|
1268
|
+
re.search(f"{{{parameter_name}(:.*?)?}}", controller.path) is not None
|
|
1269
|
+
or re.search(f"{{{parameter_name}(:.*?)?}}", mapping.path) is not None
|
|
1270
|
+
):
|
|
767
1271
|
mapped_types.add(parameter_type)
|
|
1272
|
+
# For path parameters, use Input suffix if it's a split model
|
|
1273
|
+
context_suffix = (
|
|
1274
|
+
"Input"
|
|
1275
|
+
if (
|
|
1276
|
+
inspect.isclass(parameter_type)
|
|
1277
|
+
and hasattr(parameter_type, "__dict__")
|
|
1278
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1279
|
+
)
|
|
1280
|
+
else ""
|
|
1281
|
+
)
|
|
768
1282
|
parameters_list.append(
|
|
769
1283
|
HttpParemeterSpec(
|
|
770
|
-
type_="
|
|
1284
|
+
type_="path",
|
|
771
1285
|
name=parameter_name,
|
|
772
1286
|
required=True,
|
|
773
|
-
argument_type_str=get_field_type_for_ts(
|
|
1287
|
+
argument_type_str=get_field_type_for_ts(
|
|
1288
|
+
parameter_type, context_suffix
|
|
1289
|
+
),
|
|
774
1290
|
)
|
|
775
1291
|
)
|
|
1292
|
+
else:
|
|
1293
|
+
mapped_types.add(parameter_type)
|
|
1294
|
+
# Special handling for UploadFile and list[UploadFile] - should be treated as form data
|
|
1295
|
+
if is_upload_file_type(parameter_type):
|
|
1296
|
+
parameters_list.append(
|
|
1297
|
+
HttpParemeterSpec(
|
|
1298
|
+
type_="form",
|
|
1299
|
+
name=parameter_name,
|
|
1300
|
+
required=True,
|
|
1301
|
+
argument_type_str=get_field_type_for_ts(parameter_type),
|
|
1302
|
+
)
|
|
1303
|
+
)
|
|
1304
|
+
else:
|
|
1305
|
+
# For default parameters (treated as query), use Input suffix if it's a split model
|
|
1306
|
+
context_suffix = (
|
|
1307
|
+
"Input"
|
|
1308
|
+
if (
|
|
1309
|
+
inspect.isclass(parameter_type)
|
|
1310
|
+
and hasattr(parameter_type, "__dict__")
|
|
1311
|
+
and SplitInputOutput.is_split_model(parameter_type)
|
|
1312
|
+
)
|
|
1313
|
+
else ""
|
|
1314
|
+
)
|
|
1315
|
+
parameters_list.append(
|
|
1316
|
+
HttpParemeterSpec(
|
|
1317
|
+
type_="query",
|
|
1318
|
+
name=parameter_name,
|
|
1319
|
+
required=True,
|
|
1320
|
+
argument_type_str=get_field_type_for_ts(
|
|
1321
|
+
parameter_type, context_suffix
|
|
1322
|
+
),
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
776
1325
|
|
|
777
1326
|
if inspect.isclass(parameter_type) and not is_primitive(parameter_type):
|
|
778
1327
|
signature = inspect.signature(parameter_type)
|
|
@@ -782,21 +1331,22 @@ def extract_parameters(
|
|
|
782
1331
|
for _, parameter_type in parameter_members.items():
|
|
783
1332
|
if is_primitive(parameter_type.annotation):
|
|
784
1333
|
if get_origin(parameter_type.annotation) is not None:
|
|
785
|
-
if (
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
|
|
1334
|
+
if get_origin(parameter_type.annotation) == Annotated:
|
|
1335
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(
|
|
1336
|
+
parameter_type.annotation
|
|
1337
|
+
)
|
|
1338
|
+
plain_validator = next(
|
|
1339
|
+
(
|
|
1340
|
+
x
|
|
1341
|
+
for x in all_metadata
|
|
1342
|
+
if isinstance(x, PlainValidator)
|
|
1343
|
+
),
|
|
1344
|
+
None,
|
|
796
1345
|
)
|
|
797
|
-
is not None
|
|
798
|
-
|
|
799
|
-
|
|
1346
|
+
if plain_validator is not None:
|
|
1347
|
+
mapped_types.add(
|
|
1348
|
+
plain_validator.json_schema_input_type
|
|
1349
|
+
)
|
|
800
1350
|
else:
|
|
801
1351
|
args = parameter_type.annotation.__args__
|
|
802
1352
|
mapped_types.update(args)
|
|
@@ -827,22 +1377,15 @@ def extract_all_envolved_types(field_type: Any) -> set[Any]:
|
|
|
827
1377
|
|
|
828
1378
|
if is_primitive(field_type):
|
|
829
1379
|
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
|
-
)
|
|
1380
|
+
if get_origin(field_type) == Annotated:
|
|
1381
|
+
unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
|
|
1382
|
+
plain_validator = next(
|
|
1383
|
+
(x for x in all_metadata if isinstance(x, PlainValidator)),
|
|
1384
|
+
None,
|
|
841
1385
|
)
|
|
842
|
-
is not None
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
return mapped_types
|
|
1386
|
+
if plain_validator is not None:
|
|
1387
|
+
mapped_types.add(plain_validator.json_schema_input_type)
|
|
1388
|
+
return mapped_types
|
|
846
1389
|
else:
|
|
847
1390
|
mapped_types.update(
|
|
848
1391
|
*[extract_all_envolved_types(arg) for arg in field_type.__args__]
|