jararaca 0.3.11a16__py3-none-any.whl → 0.4.0a19__py3-none-any.whl

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