jararaca 0.3.12a14__py3-none-any.whl → 0.3.12a16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jararaca might be problematic. Click here for more details.

jararaca/__init__.py CHANGED
@@ -165,7 +165,11 @@ if TYPE_CHECKING:
165
165
  from .presentation.websocket.websocket_interceptor import WebSocketInterceptor
166
166
  from .scheduler.decorators import ScheduledAction
167
167
  from .tools.app_config.interceptor import AppConfigurationInterceptor
168
- from .tools.typescript.decorators import MutationEndpoint, QueryEndpoint
168
+ from .tools.typescript.decorators import (
169
+ MutationEndpoint,
170
+ QueryEndpoint,
171
+ SplitInputOutput,
172
+ )
169
173
 
170
174
  __all__ = [
171
175
  "SetMetadata",
@@ -272,6 +276,7 @@ if TYPE_CHECKING:
272
276
  "AppConfigurationInterceptor",
273
277
  "QueryEndpoint",
274
278
  "MutationEndpoint",
279
+ "SplitInputOutput",
275
280
  "UseMiddleware",
276
281
  "UseDependency",
277
282
  "GlobalHttpErrorHandler",
@@ -501,6 +506,7 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
501
506
  ),
502
507
  "QueryEndpoint": (__SPEC_PARENT__, "tools.typescript.decorators", None),
503
508
  "MutationEndpoint": (__SPEC_PARENT__, "tools.typescript.decorators", None),
509
+ "SplitInputOutput": (__SPEC_PARENT__, "tools.typescript.decorators", None),
504
510
  "UseMiddleware": (__SPEC_PARENT__, "presentation.decorators", None),
505
511
  "UseDependency": (__SPEC_PARENT__, "presentation.decorators", None),
506
512
  "GlobalHttpErrorHandler": (__SPEC_PARENT__, "rpc.http.decorators", None),
@@ -1,7 +1,7 @@
1
1
  from contextlib import asynccontextmanager, contextmanager, suppress
2
2
  from contextvars import ContextVar
3
3
  from dataclasses import dataclass
4
- from typing import Any, AsyncGenerator, Generator
4
+ from typing import Any, AsyncGenerator, Generator, Protocol
5
5
 
6
6
  from sqlalchemy.ext.asyncio import (
7
7
  AsyncSession,
@@ -18,6 +18,42 @@ from jararaca.persistence.interceptors.decorators import (
18
18
  )
19
19
  from jararaca.reflect.metadata import get_metadata_value
20
20
 
21
+
22
+ class SessionManager(Protocol):
23
+ def spawn_session(self, connection_name: str | None = None) -> AsyncSession: ...
24
+
25
+
26
+ ctx_session_manager: ContextVar[SessionManager | None] = ContextVar(
27
+ "ctx_session_manager", default=None
28
+ )
29
+
30
+
31
+ @contextmanager
32
+ def providing_session_manager(
33
+ session_manager: SessionManager,
34
+ ) -> Generator[None, Any, None]:
35
+ """
36
+ Context manager to provide a session manager for the current context.
37
+ """
38
+ token = ctx_session_manager.set(session_manager)
39
+ try:
40
+ yield
41
+ finally:
42
+ with suppress(ValueError):
43
+ ctx_session_manager.reset(token)
44
+
45
+
46
+ def use_session_manager() -> SessionManager:
47
+ """
48
+ Retrieve the current session manager from the context variable.
49
+ Raises ValueError if no session manager is set.
50
+ """
51
+ session_manager = ctx_session_manager.get()
52
+ if session_manager is None:
53
+ raise ValueError("No session manager set in the context.")
54
+ return session_manager
55
+
56
+
21
57
  ctx_default_connection_name: ContextVar[str] = ContextVar(
22
58
  "ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
23
59
  )
@@ -71,7 +107,8 @@ async def providing_new_session(
71
107
  connection_name: str | None = None,
72
108
  ) -> AsyncGenerator[AsyncSession, None]:
73
109
 
74
- current_session = use_session(connection_name)
110
+ session_manager = use_session_manager()
111
+ current_session = session_manager.spawn_session(connection_name)
75
112
 
76
113
  async with AsyncSession(
77
114
  current_session.bind,
@@ -131,7 +168,7 @@ class AIOSQAConfig:
131
168
  self.inject_default = inject_default
132
169
 
133
170
 
134
- class AIOSqlAlchemySessionInterceptor(AppInterceptor):
171
+ class AIOSqlAlchemySessionInterceptor(AppInterceptor, SessionManager):
135
172
 
136
173
  def __init__(self, config: AIOSQAConfig):
137
174
  self.config = config
@@ -148,27 +185,33 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
148
185
  self, app_context: AppTransactionContext
149
186
  ) -> AsyncGenerator[None, None]:
150
187
 
151
- uses_connection_metadata = get_metadata_value(
152
- INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
153
- connection_name=self.config.connection_name
154
- ),
155
- self.config.inject_default,
156
- )
157
-
158
- if not uses_connection_metadata:
159
- yield
160
- return
161
-
162
- async with self.sessionmaker() as session, session.begin() as tx:
163
- token = ctx_default_connection_name.set(self.config.connection_name)
164
- with providing_session(session, tx, self.config.connection_name):
165
- try:
166
- yield
167
- if tx.is_active:
168
- await tx.commit()
169
- except Exception as e:
170
- await tx.rollback()
171
- raise e
172
- finally:
173
- with suppress(ValueError):
174
- ctx_default_connection_name.reset(token)
188
+ with providing_session_manager(self):
189
+ uses_connection_metadata = get_metadata_value(
190
+ INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
191
+ connection_name=self.config.connection_name
192
+ ),
193
+ self.config.inject_default,
194
+ )
195
+
196
+ if not uses_connection_metadata:
197
+ yield
198
+ return
199
+
200
+ async with self.sessionmaker() as session, session.begin() as tx:
201
+ token = ctx_default_connection_name.set(self.config.connection_name)
202
+ with providing_session(session, tx, self.config.connection_name):
203
+ try:
204
+ yield
205
+ if tx.is_active:
206
+ await tx.commit()
207
+ except Exception as e:
208
+ await tx.rollback()
209
+ raise e
210
+ finally:
211
+ with suppress(ValueError):
212
+ ctx_default_connection_name.reset(token)
213
+
214
+ def spawn_session(self, connection_name: str | None = None) -> AsyncSession:
215
+ connection_name = ensure_name(connection_name)
216
+ session = self.sessionmaker()
217
+ return session
@@ -1,5 +1,7 @@
1
1
  from typing import Any, Callable, TypeVar, cast
2
2
 
3
+ from pydantic import BaseModel
4
+
3
5
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
4
6
 
5
7
 
@@ -60,3 +62,34 @@ class MutationEndpoint:
60
62
  Check if the function is marked as a mutation endpoint.
61
63
  """
62
64
  return getattr(func, MutationEndpoint.METADATA_KEY, False)
65
+
66
+
67
+ BASEMODEL_T = TypeVar("BASEMODEL_T", bound=BaseModel)
68
+
69
+
70
+ class SplitInputOutput:
71
+ """
72
+ Decorator to mark a Pydantic model to generate separate Input and Output TypeScript interfaces.
73
+
74
+ Input interface: Used for API inputs (mutations/queries), handles optional fields with defaults
75
+ Output interface: Used for API outputs, represents the complete object structure
76
+ """
77
+
78
+ METADATA_KEY = "__jararaca_split_input_output__"
79
+
80
+ def __init__(self) -> None:
81
+ pass
82
+
83
+ def __call__(self, cls: type[BASEMODEL_T]) -> type[BASEMODEL_T]:
84
+ """
85
+ Decorate the Pydantic model class to mark it for split interface generation.
86
+ """
87
+ setattr(cls, self.METADATA_KEY, True)
88
+ return cls
89
+
90
+ @staticmethod
91
+ def is_split_model(cls: type) -> bool:
92
+ """
93
+ Check if the Pydantic model is marked for split interface generation.
94
+ """
95
+ return getattr(cls, SplitInputOutput.METADATA_KEY, False)
@@ -27,7 +27,7 @@ from fastapi import Request, Response, UploadFile
27
27
  from fastapi.params import Body, Cookie, Depends, Header, Path, Query
28
28
  from fastapi.security.http import HTTPBase
29
29
  from pydantic import BaseModel, PlainValidator
30
- from pydantic_core import PydanticUndefinedType
30
+ from pydantic_core import PydanticUndefined
31
31
 
32
32
  from jararaca.microservice import Microservice
33
33
  from jararaca.presentation.decorators import HttpMapping, RestController
@@ -35,7 +35,11 @@ from jararaca.presentation.websocket.decorators import RegisterWebSocketMessage
35
35
  from jararaca.presentation.websocket.websocket_interceptor import (
36
36
  WebSocketMessageWrapper,
37
37
  )
38
- from jararaca.tools.typescript.decorators import MutationEndpoint, QueryEndpoint
38
+ from jararaca.tools.typescript.decorators import (
39
+ MutationEndpoint,
40
+ QueryEndpoint,
41
+ SplitInputOutput,
42
+ )
39
43
 
40
44
  CONSTANT_PATTERN = re.compile(r"^[A-Z_]+$")
41
45
 
@@ -44,6 +48,152 @@ def is_constant(name: str) -> bool:
44
48
  return CONSTANT_PATTERN.match(name) is not None
45
49
 
46
50
 
51
+ def should_exclude_field(
52
+ field_name: str, field_type: Any, basemodel_type: Type[Any]
53
+ ) -> bool:
54
+ """
55
+ Check if a field should be excluded from TypeScript interface generation.
56
+
57
+ Args:
58
+ field_name: The name of the field
59
+ field_type: The type annotation of the field
60
+ basemodel_type: The BaseModel class containing the field
61
+
62
+ Returns:
63
+ True if the field should be excluded, False otherwise
64
+ """
65
+ # Check if field is private (starts with underscore)
66
+ if field_name.startswith("_"):
67
+ return True
68
+
69
+ # Check if field has Pydantic Field annotation and is excluded via model_fields
70
+ if (
71
+ hasattr(basemodel_type, "model_fields")
72
+ and field_name in basemodel_type.model_fields
73
+ ):
74
+ field_info = basemodel_type.model_fields[field_name]
75
+
76
+ # Check if field is excluded via Field(exclude=True)
77
+ if hasattr(field_info, "exclude") and field_info.exclude:
78
+ return True
79
+
80
+ # Check if field is marked as private via Field(..., alias=None) pattern
81
+ if (
82
+ hasattr(field_info, "alias")
83
+ and field_info.alias is None
84
+ and field_name.startswith("_")
85
+ ):
86
+ return True
87
+
88
+ # Check for Annotated types with Field metadata
89
+ if get_origin(field_type) == Annotated:
90
+ for metadata in field_type.__metadata__:
91
+ # Check if this is a Pydantic Field by looking for expected attributes
92
+ if hasattr(metadata, "exclude") or hasattr(metadata, "alias"):
93
+ # Check if Field has exclude=True
94
+ if hasattr(metadata, "exclude") and metadata.exclude:
95
+ return True
96
+ # Check for private fields with alias=None
97
+ if (
98
+ hasattr(metadata, "alias")
99
+ and metadata.alias is None
100
+ and field_name.startswith("_")
101
+ ):
102
+ return True
103
+
104
+ # Check for Field instances assigned as default values
105
+ # This handles cases like: field_name: str = Field(exclude=True)
106
+ if (
107
+ hasattr(basemodel_type, "__annotations__")
108
+ and field_name in basemodel_type.__annotations__
109
+ ):
110
+ # Check if there's a default value that's a Field instance
111
+ if hasattr(basemodel_type, field_name):
112
+ default_value = getattr(basemodel_type, field_name, None)
113
+ # Check if default value has Field-like attributes (duck typing approach)
114
+ if default_value is not None and hasattr(default_value, "exclude"):
115
+ if getattr(default_value, "exclude", False):
116
+ return True
117
+ # Check for private fields with alias=None in default Field
118
+ if (
119
+ default_value is not None
120
+ and hasattr(default_value, "alias")
121
+ and getattr(default_value, "alias", None) is None
122
+ and field_name.startswith("_")
123
+ ):
124
+ return True
125
+
126
+ return False
127
+
128
+
129
+ def has_default_value(
130
+ field_name: str, field_type: Any, basemodel_type: Type[Any]
131
+ ) -> bool:
132
+ """
133
+ Check if a field has a default value (making it optional in TypeScript).
134
+
135
+ Args:
136
+ field_name: The name of the field
137
+ field_type: The type annotation of the field
138
+ basemodel_type: The BaseModel class containing the field
139
+
140
+ Returns:
141
+ True if the field has a default value, False otherwise
142
+ """
143
+ # Skip literal types as they don't have defaults in the traditional sense
144
+ if get_origin(field_type) is Literal:
145
+ return False
146
+
147
+ # Check if field has default in model_fields (standard Pydantic way)
148
+ if (
149
+ hasattr(basemodel_type, "model_fields")
150
+ and field_name in basemodel_type.model_fields
151
+ ):
152
+ field_info = basemodel_type.model_fields[field_name]
153
+ if field_info.default is not PydanticUndefined:
154
+ return True
155
+
156
+ # Check for Field instances assigned as default values
157
+ # This handles cases like: field_name: str = Field(default="value")
158
+ if (
159
+ hasattr(basemodel_type, "__annotations__")
160
+ and field_name in basemodel_type.__annotations__
161
+ ):
162
+ if hasattr(basemodel_type, field_name):
163
+ default_value = getattr(basemodel_type, field_name, None)
164
+ # Check if it's a Field instance with a default
165
+ if default_value is not None and hasattr(default_value, "default"):
166
+ # Check if the Field has a default value set
167
+ field_default = getattr(default_value, "default", PydanticUndefined)
168
+ if field_default is not PydanticUndefined:
169
+ return True
170
+
171
+ # Check for non-Field default values assigned directly to class attributes
172
+ # This handles cases like: field_name: str = "default_value"
173
+ if hasattr(basemodel_type, field_name):
174
+ default_value = getattr(basemodel_type, field_name, None)
175
+ # If it's not a Field instance but has a value, it's a default
176
+ if (
177
+ default_value is not None
178
+ and not hasattr(default_value, "exclude") # Not a Field instance
179
+ and not hasattr(default_value, "alias")
180
+ ): # Not a Field instance
181
+ return True
182
+
183
+ # Check for Annotated types with Field metadata that have defaults
184
+ if get_origin(field_type) == Annotated:
185
+ for metadata in field_type.__metadata__:
186
+ # Check if this is a Pydantic Field with a default
187
+ if hasattr(metadata, "default") and hasattr(
188
+ metadata, "exclude"
189
+ ): # Ensure it's a Field
190
+ field_default = getattr(metadata, "default", PydanticUndefined)
191
+ if field_default is not PydanticUndefined:
192
+ return True
193
+
194
+ return False
195
+
196
+
47
197
  class ParseContext:
48
198
  def __init__(self) -> None:
49
199
  self.mapped_types: set[Any] = set()
@@ -102,7 +252,14 @@ def parse_literal_value(value: Any) -> str:
102
252
  return "unknown"
103
253
 
104
254
 
105
- def get_field_type_for_ts(field_type: Any) -> Any:
255
+ def get_field_type_for_ts(field_type: Any, context_suffix: str = "") -> Any:
256
+ """
257
+ Convert a Python type to its TypeScript equivalent.
258
+
259
+ Args:
260
+ field_type: The Python type to convert
261
+ context_suffix: Suffix for split models (e.g., "Input", "Output")
262
+ """
106
263
  if field_type is Response:
107
264
  return "unknown"
108
265
  if field_type is Any:
@@ -130,17 +287,20 @@ def get_field_type_for_ts(field_type: Any) -> Any:
130
287
  if field_type == Decimal:
131
288
  return "number"
132
289
  if get_origin(field_type) == ClassVar:
133
- return get_field_type_for_ts(field_type.__args__[0])
290
+ return get_field_type_for_ts(field_type.__args__[0], context_suffix)
134
291
  if get_origin(field_type) == tuple:
135
- return f"[{', '.join([get_field_type_for_ts(field) for field in field_type.__args__])}]"
292
+ return f"[{', '.join([get_field_type_for_ts(field, context_suffix) for field in field_type.__args__])}]"
136
293
  if get_origin(field_type) == list or get_origin(field_type) == frozenset:
137
- return f"Array<{get_field_type_for_ts(field_type.__args__[0])}>"
294
+ return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix)}>"
138
295
  if get_origin(field_type) == set:
139
- return f"Array<{get_field_type_for_ts(field_type.__args__[0])}> // Set"
296
+ return f"Array<{get_field_type_for_ts(field_type.__args__[0], context_suffix)}> // Set"
140
297
  if get_origin(field_type) == dict:
141
- return f"{{[key: {get_field_type_for_ts(field_type.__args__[0])}]: {get_field_type_for_ts(field_type.__args__[1])}}}"
298
+ return f"{{[key: {get_field_type_for_ts(field_type.__args__[0], context_suffix)}]: {get_field_type_for_ts(field_type.__args__[1], context_suffix)}}}"
142
299
  if inspect.isclass(field_type):
143
300
  if not hasattr(field_type, "__pydantic_generic_metadata__"):
301
+ # Check if this is a split model and use appropriate suffix
302
+ if SplitInputOutput.is_split_model(field_type) and context_suffix:
303
+ return f"{field_type.__name__}{context_suffix}"
144
304
  return field_type.__name__
145
305
  pydantic_metadata = getattr(field_type, "__pydantic_generic_metadata__")
146
306
 
@@ -149,12 +309,17 @@ def get_field_type_for_ts(field_type: Any) -> Any:
149
309
  if pydantic_metadata.get("origin") is not None
150
310
  else field_type.__name__
151
311
  )
312
+
313
+ # Check if this is a split model and use appropriate suffix
314
+ if SplitInputOutput.is_split_model(field_type) and context_suffix:
315
+ name = f"{field_type.__name__}{context_suffix}"
316
+
152
317
  args = pydantic_metadata.get("args")
153
318
 
154
319
  if len(args) > 0:
155
320
  return "%s<%s>" % (
156
321
  name,
157
- ", ".join([get_field_type_for_ts(arg) for arg in args]),
322
+ ", ".join([get_field_type_for_ts(arg, context_suffix) for arg in args]),
158
323
  )
159
324
 
160
325
  return name
@@ -164,7 +329,9 @@ def get_field_type_for_ts(field_type: Any) -> Any:
164
329
  if get_origin(field_type) == Literal:
165
330
  return " | ".join([parse_literal_value(x) for x in field_type.__args__])
166
331
  if get_origin(field_type) == UnionType or get_origin(field_type) == typing.Union:
167
- return " | ".join([get_field_type_for_ts(x) for x in field_type.__args__])
332
+ return " | ".join(
333
+ [get_field_type_for_ts(x, context_suffix) for x in field_type.__args__]
334
+ )
168
335
  if (get_origin(field_type) == Annotated) and (len(field_type.__args__) > 0):
169
336
  if (
170
337
  plain_validator := next(
@@ -172,8 +339,10 @@ def get_field_type_for_ts(field_type: Any) -> Any:
172
339
  None,
173
340
  )
174
341
  ) is not None:
175
- return get_field_type_for_ts(plain_validator.json_schema_input_type)
176
- return get_field_type_for_ts(field_type.__args__[0])
342
+ return get_field_type_for_ts(
343
+ plain_validator.json_schema_input_type, context_suffix
344
+ )
345
+ return get_field_type_for_ts(field_type.__args__[0], context_suffix)
177
346
  return "unknown"
178
347
 
179
348
 
@@ -188,6 +357,59 @@ def get_generic_args(field_type: Any) -> Any:
188
357
  def parse_type_to_typescript_interface(
189
358
  basemodel_type: Type[Any],
190
359
  ) -> tuple[set[type], str]:
360
+ """
361
+ Parse a Pydantic model into TypeScript interface(s).
362
+
363
+ If the model is decorated with @SplitInputOutput, it generates both Input and Output interfaces.
364
+ Otherwise, it generates a single interface.
365
+ """
366
+ # Check if this model should be split into Input/Output interfaces
367
+ if SplitInputOutput.is_split_model(basemodel_type):
368
+ return parse_split_input_output_interfaces(basemodel_type)
369
+
370
+ return parse_single_typescript_interface(basemodel_type)
371
+
372
+
373
+ def parse_split_input_output_interfaces(
374
+ basemodel_type: Type[Any],
375
+ ) -> tuple[set[type], str]:
376
+ """
377
+ Generate both Input and Output TypeScript interfaces for a split model.
378
+ """
379
+ mapped_types: set[type] = set()
380
+ combined_output = StringIO()
381
+
382
+ # Generate Input interface (with optional fields)
383
+ input_mapped_types, input_interface = parse_single_typescript_interface(
384
+ basemodel_type, interface_suffix="Input", force_optional_defaults=True
385
+ )
386
+ mapped_types.update(input_mapped_types)
387
+ combined_output.write(input_interface)
388
+
389
+ # Generate Output interface (all fields required as they come from the backend)
390
+ output_mapped_types, output_interface = parse_single_typescript_interface(
391
+ basemodel_type, interface_suffix="Output", force_optional_defaults=False
392
+ )
393
+ mapped_types.update(output_mapped_types)
394
+ combined_output.write(output_interface)
395
+
396
+ return mapped_types, combined_output.getvalue()
397
+
398
+
399
+ def parse_single_typescript_interface(
400
+ basemodel_type: Type[Any],
401
+ interface_suffix: str = "",
402
+ force_optional_defaults: bool | None = None,
403
+ ) -> tuple[set[type], str]:
404
+ """
405
+ Generate a single TypeScript interface for a Pydantic model.
406
+
407
+ Args:
408
+ basemodel_type: The Pydantic model class
409
+ interface_suffix: Suffix to add to the interface name (e.g., "Input", "Output")
410
+ force_optional_defaults: If True, fields with defaults are optional. If False, all fields are required.
411
+ If None, uses the default behavior (fields with defaults are optional).
412
+ """
191
413
  string_builder = StringIO()
192
414
  mapped_types: set[type] = set()
193
415
 
@@ -214,7 +436,7 @@ def parse_type_to_typescript_interface(
214
436
  enum_values = sorted([(x._name_, x.value) for x in basemodel_type])
215
437
  return (
216
438
  set(),
217
- f"export enum {basemodel_type.__name__} {{\n"
439
+ f"export enum {basemodel_type.__name__}{interface_suffix} {{\n"
218
440
  + "\n ".join([f'\t{name} = "{value}",' for name, value in enum_values])
219
441
  + "\n}\n",
220
442
  )
@@ -238,46 +460,46 @@ def parse_type_to_typescript_interface(
238
460
  for inherited_class in valid_inherited_classes
239
461
  }
240
462
 
241
- extends_expression = (
242
- " extends %s"
243
- % ", ".join(
244
- sorted(
245
- [
246
- (
247
- "%s" % get_field_type_for_ts(inherited_class)
248
- if not inherited_classes_consts_conflict[inherited_class]
249
- else "Omit<%s, %s>"
250
- % (
251
- get_field_type_for_ts(inherited_class),
252
- " | ".join(
253
- sorted(
254
- [
255
- '"%s"' % field_name
256
- for field_name in inherited_classes_consts_conflict[
257
- inherited_class
258
- ]
259
- ]
260
- )
261
- ),
463
+ # Modify inheritance for split interfaces
464
+ extends_expression = ""
465
+ if len(valid_inherited_classes) > 0:
466
+ extends_base_names = []
467
+ for inherited_class in valid_inherited_classes:
468
+ base_name = get_field_type_for_ts(inherited_class, interface_suffix)
469
+ # If the inherited class is also a split model, use the appropriate suffix
470
+ if SplitInputOutput.is_split_model(inherited_class) and interface_suffix:
471
+ base_name = f"{inherited_class.__name__}{interface_suffix}"
472
+
473
+ if inherited_classes_consts_conflict[inherited_class]:
474
+ base_name = "Omit<%s, %s>" % (
475
+ base_name,
476
+ " | ".join(
477
+ sorted(
478
+ [
479
+ '"%s"' % field_name
480
+ for field_name in inherited_classes_consts_conflict[
481
+ inherited_class
482
+ ]
483
+ ]
262
484
  )
263
- )
264
- for inherited_class in valid_inherited_classes
265
- ],
266
- key=lambda x: str(x),
267
- )
485
+ ),
486
+ )
487
+ extends_base_names.append(base_name)
488
+
489
+ extends_expression = " extends %s" % ", ".join(
490
+ sorted(extends_base_names, key=lambda x: str(x))
268
491
  )
269
- if len(valid_inherited_classes) > 0
270
- else ""
271
- )
492
+
493
+ interface_name = f"{basemodel_type.__name__}{interface_suffix}"
272
494
 
273
495
  if is_generic_type(basemodel_type):
274
496
  generic_args = get_generic_args(basemodel_type)
275
497
  string_builder.write(
276
- f"export interface {basemodel_type.__name__}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
498
+ f"export interface {interface_name}<{', '.join(sorted([arg.__name__ for arg in generic_args]))}>{extends_expression} {{\n"
277
499
  )
278
500
  else:
279
501
  string_builder.write(
280
- f"export interface {basemodel_type.__name__}{extends_expression} {{\n"
502
+ f"export interface {interface_name}{extends_expression} {{\n"
281
503
  )
282
504
 
283
505
  if hasattr(basemodel_type, "__annotations__"):
@@ -290,16 +512,23 @@ def parse_type_to_typescript_interface(
290
512
  if field_name in cls_consts:
291
513
  continue
292
514
 
293
- has_default_value = (
294
- get_origin(field) is not Literal
295
- and field_name in basemodel_type.model_fields
296
- and not isinstance(
297
- basemodel_type.model_fields[field_name].default,
298
- PydanticUndefinedType,
299
- )
300
- )
515
+ # Check if field should be excluded (private or excluded via Field)
516
+ if should_exclude_field(field_name, field, basemodel_type):
517
+ continue
518
+
519
+ # Determine if field is optional based on the force_optional_defaults parameter
520
+ if force_optional_defaults is True:
521
+ # Input interface: fields with defaults are optional
522
+ is_optional = has_default_value(field_name, field, basemodel_type)
523
+ elif force_optional_defaults is False:
524
+ # Output interface: all fields are required (backend provides complete data)
525
+ is_optional = False
526
+ else:
527
+ # Default behavior: fields with defaults are optional
528
+ is_optional = has_default_value(field_name, field, basemodel_type)
529
+
301
530
  string_builder.write(
302
- 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"
531
+ 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"
303
532
  )
304
533
  mapped_types.update(extract_all_envolved_types(field))
305
534
  mapped_types.add(field)
@@ -326,7 +555,7 @@ def parse_type_to_typescript_interface(
326
555
  return_type = NoneType
327
556
 
328
557
  string_builder.write(
329
- f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type)};\n"
558
+ f" {snake_to_camel(field_name)}: {get_field_type_for_ts(return_type, interface_suffix)};\n"
330
559
  )
331
560
  mapped_types.update(extract_all_envolved_types(return_type))
332
561
  mapped_types.add(return_type)
@@ -515,7 +744,8 @@ def write_rest_controller_to_typescript_interface(
515
744
 
516
745
  mapped_types.update(extract_all_envolved_types(return_type))
517
746
 
518
- return_value_repr = get_field_type_for_ts(return_type)
747
+ # For return types, use Output suffix if it's a split model
748
+ return_value_repr = get_field_type_for_ts(return_type, "Output")
519
749
 
520
750
  arg_params_spec, parametes_mapped_types = extract_parameters(
521
751
  member, rest_controller, mapping
@@ -732,32 +962,68 @@ def extract_parameters(
732
962
  )
733
963
  elif isinstance(annotated_type_hook, Body):
734
964
  mapped_types.update(extract_all_envolved_types(parameter_type))
965
+ # For body parameters, use Input suffix if it's a split model
966
+ context_suffix = (
967
+ "Input"
968
+ if (
969
+ inspect.isclass(parameter_type)
970
+ and hasattr(parameter_type, "__dict__")
971
+ and SplitInputOutput.is_split_model(parameter_type)
972
+ )
973
+ else ""
974
+ )
735
975
  parameters_list.append(
736
976
  HttpParemeterSpec(
737
977
  type_="body",
738
978
  name=parameter_name,
739
979
  required=True,
740
- argument_type_str=get_field_type_for_ts(parameter_type),
980
+ argument_type_str=get_field_type_for_ts(
981
+ parameter_type, context_suffix
982
+ ),
741
983
  )
742
984
  )
743
985
  elif isinstance(annotated_type_hook, Query):
744
986
  mapped_types.add(parameter_type)
987
+ # For query parameters, use Input suffix if it's a split model
988
+ context_suffix = (
989
+ "Input"
990
+ if (
991
+ inspect.isclass(parameter_type)
992
+ and hasattr(parameter_type, "__dict__")
993
+ and SplitInputOutput.is_split_model(parameter_type)
994
+ )
995
+ else ""
996
+ )
745
997
  parameters_list.append(
746
998
  HttpParemeterSpec(
747
999
  type_="query",
748
1000
  name=parameter_name,
749
1001
  required=True,
750
- argument_type_str=get_field_type_for_ts(parameter_type),
1002
+ argument_type_str=get_field_type_for_ts(
1003
+ parameter_type, context_suffix
1004
+ ),
751
1005
  )
752
1006
  )
753
1007
  elif isinstance(annotated_type_hook, Path):
754
1008
  mapped_types.add(parameter_type)
1009
+ # For path parameters, use Input suffix if it's a split model
1010
+ context_suffix = (
1011
+ "Input"
1012
+ if (
1013
+ inspect.isclass(parameter_type)
1014
+ and hasattr(parameter_type, "__dict__")
1015
+ and SplitInputOutput.is_split_model(parameter_type)
1016
+ )
1017
+ else ""
1018
+ )
755
1019
  parameters_list.append(
756
1020
  HttpParemeterSpec(
757
1021
  type_="path",
758
1022
  name=parameter_name,
759
1023
  required=True,
760
- argument_type_str=get_field_type_for_ts(parameter_type),
1024
+ argument_type_str=get_field_type_for_ts(
1025
+ parameter_type, context_suffix
1026
+ ),
761
1027
  )
762
1028
  )
763
1029
 
@@ -779,22 +1045,46 @@ def extract_parameters(
779
1045
  re.search(f":{parameter_name}(?:/|$)", controller.path) is not None
780
1046
  ):
781
1047
  mapped_types.add(annotated_type)
1048
+ # For path parameters, use Input suffix if it's a split model
1049
+ context_suffix = (
1050
+ "Input"
1051
+ if (
1052
+ inspect.isclass(annotated_type)
1053
+ and hasattr(annotated_type, "__dict__")
1054
+ and SplitInputOutput.is_split_model(annotated_type)
1055
+ )
1056
+ else ""
1057
+ )
782
1058
  parameters_list.append(
783
1059
  HttpParemeterSpec(
784
1060
  type_="path",
785
1061
  name=parameter_name,
786
1062
  required=True,
787
- argument_type_str=get_field_type_for_ts(annotated_type),
1063
+ argument_type_str=get_field_type_for_ts(
1064
+ annotated_type, context_suffix
1065
+ ),
788
1066
  )
789
1067
  )
790
1068
  else:
791
1069
  mapped_types.add(annotated_type)
1070
+ # For default parameters (treated as query), use Input suffix if it's a split model
1071
+ context_suffix = (
1072
+ "Input"
1073
+ if (
1074
+ inspect.isclass(annotated_type)
1075
+ and hasattr(annotated_type, "__dict__")
1076
+ and SplitInputOutput.is_split_model(annotated_type)
1077
+ )
1078
+ else ""
1079
+ )
792
1080
  parameters_list.append(
793
1081
  HttpParemeterSpec(
794
1082
  type_="query",
795
1083
  name=parameter_name,
796
1084
  required=True,
797
- argument_type_str=get_field_type_for_ts(annotated_type),
1085
+ argument_type_str=get_field_type_for_ts(
1086
+ annotated_type, context_suffix
1087
+ ),
798
1088
  )
799
1089
  )
800
1090
 
@@ -802,12 +1092,18 @@ def extract_parameters(
802
1092
  parameter_type, BaseModel
803
1093
  ):
804
1094
  mapped_types.update(extract_all_envolved_types(parameter_type))
1095
+ # For BaseModel parameters, use Input suffix if it's a split model
1096
+ context_suffix = (
1097
+ "Input" if SplitInputOutput.is_split_model(parameter_type) else ""
1098
+ )
805
1099
  parameters_list.append(
806
1100
  HttpParemeterSpec(
807
1101
  type_="body",
808
1102
  name=parameter_name,
809
1103
  required=True,
810
- argument_type_str=get_field_type_for_ts(parameter_type),
1104
+ argument_type_str=get_field_type_for_ts(
1105
+ parameter_type, context_suffix
1106
+ ),
811
1107
  )
812
1108
  )
813
1109
  elif (
@@ -816,22 +1112,46 @@ def extract_parameters(
816
1112
  or re.search(f"{{{parameter_name}(:.*?)?}}", mapping.path) is not None
817
1113
  ):
818
1114
  mapped_types.add(parameter_type)
1115
+ # For path parameters, use Input suffix if it's a split model
1116
+ context_suffix = (
1117
+ "Input"
1118
+ if (
1119
+ inspect.isclass(parameter_type)
1120
+ and hasattr(parameter_type, "__dict__")
1121
+ and SplitInputOutput.is_split_model(parameter_type)
1122
+ )
1123
+ else ""
1124
+ )
819
1125
  parameters_list.append(
820
1126
  HttpParemeterSpec(
821
1127
  type_="path",
822
1128
  name=parameter_name,
823
1129
  required=True,
824
- argument_type_str=get_field_type_for_ts(parameter_type),
1130
+ argument_type_str=get_field_type_for_ts(
1131
+ parameter_type, context_suffix
1132
+ ),
825
1133
  )
826
1134
  )
827
1135
  else:
828
1136
  mapped_types.add(parameter_type)
1137
+ # For default parameters (treated as query), use Input suffix if it's a split model
1138
+ context_suffix = (
1139
+ "Input"
1140
+ if (
1141
+ inspect.isclass(parameter_type)
1142
+ and hasattr(parameter_type, "__dict__")
1143
+ and SplitInputOutput.is_split_model(parameter_type)
1144
+ )
1145
+ else ""
1146
+ )
829
1147
  parameters_list.append(
830
1148
  HttpParemeterSpec(
831
1149
  type_="query",
832
1150
  name=parameter_name,
833
1151
  required=True,
834
- argument_type_str=get_field_type_for_ts(parameter_type),
1152
+ argument_type_str=get_field_type_for_ts(
1153
+ parameter_type, context_suffix
1154
+ ),
835
1155
  )
836
1156
  )
837
1157
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.12a14
3
+ Version: 0.3.12a16
4
4
  Summary: A simple and fast API framework for Python
5
5
  Home-page: https://github.com/LuscasLeo/jararaca
6
6
  Author: Lucas S
@@ -1,7 +1,7 @@
1
1
  LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  README.md,sha256=2qMM__t_MoLKZr4IY9tXjo-Jn6LKjuHMb1qbyXpgL08,3401
3
- pyproject.toml,sha256=3QVWSJEkvEp1kWJ7e8UU9gvW6RtQRqMPvGq7-u2cjiU,2041
4
- jararaca/__init__.py,sha256=h3MkFZ9xNllb-YTB-M1WH2HnSJbkev6t1c984Nn6G1w,21887
3
+ pyproject.toml,sha256=CJhUE0Llc5_AlQo4FUo5kjI7cblJBlzu4-YmrIZ0QCQ,2041
4
+ jararaca/__init__.py,sha256=vK3zyIVLckwZgj1FPX6jzSbzaSWmSy3wQ2KMwmpJnmg,22046
5
5
  jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
6
6
  jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
7
7
  jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
@@ -32,7 +32,7 @@ jararaca/observability/providers/otel.py,sha256=8N1F32W43t7c8cwmtTh6Yz9b7HyfGFSR
32
32
  jararaca/persistence/base.py,sha256=xnGUbsLNz3gO-9iJt-Sn5NY13Yc9-misP8wLwQuGGoM,1024
33
33
  jararaca/persistence/exports.py,sha256=Ghx4yoFaB4QVTb9WxrFYgmcSATXMNvrOvT8ybPNKXCA,62
34
34
  jararaca/persistence/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- jararaca/persistence/interceptors/aiosqa_interceptor.py,sha256=_qhTONCtusGIvQg3FOHVnsxWXGi77gsNi1fH0Cv6kxg,5301
35
+ jararaca/persistence/interceptors/aiosqa_interceptor.py,sha256=Cvr8wtCpRHE1Ddy9NvlZvXE2EVV0jBIibHCqj4pRkvc,6689
36
36
  jararaca/persistence/interceptors/constants.py,sha256=o8g5RxDX9dSSnM9eXYlTJalGPfWxYm6-CAA8-FooUzE,36
37
37
  jararaca/persistence/interceptors/decorators.py,sha256=p37r9ux5w_bvn8xPNPhScl3fjCc2enqTR0cJYLxMsrI,1627
38
38
  jararaca/persistence/session.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -69,13 +69,13 @@ jararaca/tools/app_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
69
69
  jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaNXMp8fd1Ndk,941
70
70
  jararaca/tools/app_config/interceptor.py,sha256=HV8h4AxqUc_ACs5do4BSVlyxlRXzx7HqJtoVO9tfRnQ,2611
71
71
  jararaca/tools/typescript/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- jararaca/tools/typescript/decorators.py,sha256=DbwZADDzzo3cbMfNcY6ldILy_80N4x25ChyQoX1q56s,1956
73
- jararaca/tools/typescript/interface_parser.py,sha256=SsTgYUWhX79e2rvcu5A5kvs7bJ31J05VQUushj9tudk,33689
72
+ jararaca/tools/typescript/decorators.py,sha256=y1zBq8mBZ8CBXlZ0nKy2RyIgCvP9kp4elACbaC6dptQ,2946
73
+ jararaca/tools/typescript/interface_parser.py,sha256=OMvz4vcsKD0eYPwH1hd3PJpLP2Bc_Ex0CefIhsJArf4,47306
74
74
  jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  jararaca/utils/rabbitmq_utils.py,sha256=ytdAFUyv-OBkaVnxezuJaJoLrmN7giZgtKeet_IsMBs,10918
76
76
  jararaca/utils/retry.py,sha256=DzPX_fXUvTqej6BQ8Mt2dvLo9nNlTBm7Kx2pFZ26P2Q,4668
77
- jararaca-0.3.12a14.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
78
- jararaca-0.3.12a14.dist-info/METADATA,sha256=tlstrP3vETc9kcSVacSbX6rYO6j1VBTX2AoMSwdH1Ok,4996
79
- jararaca-0.3.12a14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
80
- jararaca-0.3.12a14.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
81
- jararaca-0.3.12a14.dist-info/RECORD,,
77
+ jararaca-0.3.12a16.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
78
+ jararaca-0.3.12a16.dist-info/METADATA,sha256=d5M-tdj2etlzLG_d1OofoaoDIFYwLqgyhHda36VMTJU,4996
79
+ jararaca-0.3.12a16.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
80
+ jararaca-0.3.12a16.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
81
+ jararaca-0.3.12a16.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.12a14"
3
+ version = "0.3.12a16"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"