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.
Files changed (88) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +184 -12
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +4 -0
  5. jararaca/broker_backend/mapper.py +4 -0
  6. jararaca/broker_backend/redis_broker_backend.py +9 -3
  7. jararaca/cli.py +272 -47
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +4 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/lifecycle.py +6 -2
  15. jararaca/messagebus/__init__.py +4 -0
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +33 -67
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
  23. jararaca/messagebus/message.py +4 -0
  24. jararaca/messagebus/publisher.py +6 -0
  25. jararaca/messagebus/worker.py +850 -383
  26. jararaca/microservice.py +110 -1
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +170 -13
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +4 -0
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +202 -11
  34. jararaca/persistence/base.py +38 -2
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  38. jararaca/persistence/interceptors/constants.py +5 -0
  39. jararaca/persistence/interceptors/decorators.py +50 -0
  40. jararaca/persistence/session.py +3 -0
  41. jararaca/persistence/sort_filter.py +4 -0
  42. jararaca/persistence/utilities.py +50 -20
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +88 -86
  45. jararaca/presentation/exceptions.py +23 -0
  46. jararaca/presentation/hooks.py +4 -0
  47. jararaca/presentation/http_microservice.py +4 -0
  48. jararaca/presentation/server.py +97 -45
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +4 -0
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +4 -0
  55. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +16 -10
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +34 -25
  60. jararaca/rpc/__init__.py +3 -0
  61. jararaca/rpc/http/__init__.py +101 -0
  62. jararaca/rpc/http/backends/__init__.py +14 -0
  63. jararaca/rpc/http/backends/httpx.py +43 -9
  64. jararaca/rpc/http/backends/otel.py +4 -0
  65. jararaca/rpc/http/decorators.py +378 -113
  66. jararaca/rpc/http/httpx.py +3 -0
  67. jararaca/scheduler/__init__.py +3 -0
  68. jararaca/scheduler/beat_worker.py +521 -105
  69. jararaca/scheduler/decorators.py +15 -22
  70. jararaca/scheduler/types.py +4 -0
  71. jararaca/tools/app_config/__init__.py +3 -0
  72. jararaca/tools/app_config/decorators.py +7 -19
  73. jararaca/tools/app_config/interceptor.py +6 -2
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1074 -173
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +65 -39
  79. jararaca/utils/retry.py +10 -3
  80. jararaca-0.4.0a5.dist-info/LICENSE +674 -0
  81. jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  82. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  87. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  88. {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 PydanticUndefinedType
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 get_field_type_for_ts(field_type: Any) -> Any:
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(field_type.__args__[0])
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([get_field_type_for_ts(arg) for arg in args]),
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([get_field_type_for_ts(x) for x in field_type.__args__])
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 field_type.__metadata__ if isinstance(x, PlainValidator)),
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(plain_validator.json_schema_input_type)
168
- return get_field_type_for_ts(field_type.__args__[0])
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
- extends_expression = (
234
- " extends %s"
235
- % ", ".join(
236
- sorted(
237
- [
238
- (
239
- "%s" % get_field_type_for_ts(inherited_class)
240
- if not inherited_classes_consts_conflict[inherited_class]
241
- else "Omit<%s, %s>"
242
- % (
243
- get_field_type_for_ts(inherited_class),
244
- " | ".join(
245
- sorted(
246
- [
247
- '"%s"' % field_name
248
- for field_name in inherited_classes_consts_conflict[
249
- inherited_class
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
- for inherited_class in valid_inherited_classes
257
- ],
258
- key=lambda x: str(x),
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
- if len(valid_inherited_classes) > 0
262
- else ""
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 {basemodel_type.__name__}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
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 {basemodel_type.__name__}{extends_expression} {{\n"
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
- has_default_value = (
286
- get_origin(field) is not Literal
287
- and field_name in basemodel_type.model_fields
288
- and not isinstance(
289
- basemodel_type.model_fields[field_name].default,
290
- PydanticUndefinedType,
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 has_default_value else ''}: {get_field_type_for_ts(field)};\n"
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.getmembers(basemodel_type, lambda a: isinstance(a, property)),
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.get_controller(controller)
713
+ rest_controller = RestController.get_last(controller)
353
714
 
354
715
  if rest_controller is None:
355
716
  continue
356
717
 
357
- controller_class_str, types = write_rest_controller_to_typescript_interface(
358
- rest_controller, controller
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(controller_class_str)
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.get(controller)
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[str, set[Any]]:
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 {controller.__name__} extends HttpService {{\n")
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.getmembers(controller, predicate=inspect.isfunction), key=lambda x: x[0]
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
- if (mapping := HttpMapping.get_http_mapping(member)) is not None:
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
- return_value_repr = get_field_type_for_ts(return_type)
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
- final_path = "/".join(
542
- s.strip("/") for s in [rest_controller.path, endpoint_path]
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(f"\t\t\tpath: `/{final_path}`,\n")
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
- body := next((x for x in arg_params_spec if x.type_ == "body"), None)
568
- ) is not None:
569
- class_buffer.write(f"\t\t\tbody: {body.name}\n")
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
- return class_buffer.getvalue(), mapped_types
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 = path.replace(f"{{{parameter.name}}}", f"${{{parameter.name}}}")
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, controller: RestController, mapping: HttpMapping
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 member.__metadata__ if isinstance(x, PlainValidator)),
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(member.__args__[0], controller, mapping)
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
- annotated_type_hook = parameter_type.__metadata__[0]
654
- annotated_type = parameter_type.__args__[0]
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(str),
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(str),
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(parameter_type),
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(parameter_type),
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(parameter_type),
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 controller.path.find(f":{parameter_name}") != -1:
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(annotated_type),
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
- parameters_list.append(
733
- HttpParemeterSpec(
734
- type_="query",
735
- name=parameter_name,
736
- required=True,
737
- argument_type_str=get_field_type_for_ts(annotated_type),
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(parameter_type),
1604
+ argument_type_str=get_field_type_for_ts(
1605
+ parameter_type, context_suffix, type_mapping
1606
+ ),
751
1607
  )
752
1608
  )
753
- elif (
754
- controller.path.find(f"{{{parameter_name}}}") != -1
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_="path",
1614
+ type_="form",
761
1615
  name=parameter_name,
762
1616
  required=True,
763
- argument_type_str=get_field_type_for_ts(parameter_type),
1617
+ argument_type_str=get_field_type_for_ts(
1618
+ parameter_type, "", type_mapping
1619
+ ),
764
1620
  )
765
1621
  )
766
- else:
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_="query",
1640
+ type_="path",
771
1641
  name=parameter_name,
772
1642
  required=True,
773
- argument_type_str=get_field_type_for_ts(parameter_type),
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
- get_origin(parameter_type.annotation) == Annotated
787
- and (
788
- plain_validator := next(
789
- (
790
- x
791
- for x in parameter_type.annotation.__metadata__
792
- if isinstance(x, PlainValidator)
793
- ),
794
- None,
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
- mapped_types.add(plain_validator.json_schema_input_type)
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
- get_origin(field_type) == Annotated
832
- and (
833
- plain_validator := next(
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
- mapped_types.add(plain_validator.json_schema_input_type)
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__]