jararaca 0.2.37a12__py3-none-any.whl → 0.4.0a5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +267 -15
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +106 -0
  5. jararaca/broker_backend/mapper.py +25 -0
  6. jararaca/broker_backend/redis_broker_backend.py +168 -0
  7. jararaca/cli.py +840 -103
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +4 -0
  11. jararaca/core/uow.py +55 -16
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/lifecycle.py +6 -2
  15. jararaca/messagebus/__init__.py +5 -1
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +90 -85
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
  23. jararaca/messagebus/message.py +31 -0
  24. jararaca/messagebus/publisher.py +47 -4
  25. jararaca/messagebus/worker.py +1615 -135
  26. jararaca/microservice.py +248 -36
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +177 -16
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +8 -2
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +213 -18
  34. jararaca/persistence/base.py +40 -3
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
  38. jararaca/persistence/interceptors/constants.py +5 -0
  39. jararaca/persistence/interceptors/decorators.py +50 -0
  40. jararaca/persistence/session.py +3 -0
  41. jararaca/persistence/sort_filter.py +4 -0
  42. jararaca/persistence/utilities.py +74 -32
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +170 -82
  45. jararaca/presentation/exceptions.py +23 -0
  46. jararaca/presentation/hooks.py +4 -0
  47. jararaca/presentation/http_microservice.py +4 -0
  48. jararaca/presentation/server.py +120 -41
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +34 -4
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +6 -2
  55. jararaca/presentation/websocket/websocket_interceptor.py +74 -23
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +81 -0
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +76 -0
  60. jararaca/rpc/__init__.py +3 -0
  61. jararaca/rpc/http/__init__.py +101 -0
  62. jararaca/rpc/http/backends/__init__.py +14 -0
  63. jararaca/rpc/http/backends/httpx.py +43 -9
  64. jararaca/rpc/http/backends/otel.py +4 -0
  65. jararaca/rpc/http/decorators.py +378 -113
  66. jararaca/rpc/http/httpx.py +3 -0
  67. jararaca/scheduler/__init__.py +3 -0
  68. jararaca/scheduler/beat_worker.py +758 -0
  69. jararaca/scheduler/decorators.py +89 -28
  70. jararaca/scheduler/types.py +11 -0
  71. jararaca/tools/app_config/__init__.py +3 -0
  72. jararaca/tools/app_config/decorators.py +7 -19
  73. jararaca/tools/app_config/interceptor.py +10 -4
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1126 -189
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +372 -0
  79. jararaca/utils/retry.py +148 -0
  80. jararaca-0.4.0a5.dist-info/LICENSE +674 -0
  81. jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  82. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca/messagebus/types.py +0 -30
  87. jararaca/scheduler/scheduler.py +0 -154
  88. jararaca/tools/metadata.py +0 -47
  89. jararaca-0.2.37a12.dist-info/RECORD +0 -63
  90. /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
  91. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
@@ -1,63 +1,124 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  import inspect
2
- from typing import Any, Callable, TypeVar, cast
6
+ from dataclasses import dataclass
7
+ from types import FunctionType
8
+ from typing import Any, Awaitable, Callable, TypeVar
9
+
10
+ from jararaca.reflect.controller_inspect import (
11
+ ControllerMemberReflect,
12
+ inspect_controller,
13
+ )
14
+ from jararaca.reflect.decorators import StackableDecorator
3
15
 
4
16
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
5
17
 
6
18
 
7
- class ScheduledAction:
8
- SCHEDULED_ACTION_ATTR = "__scheduled_action__"
19
+ class ScheduledAction(StackableDecorator):
9
20
 
10
21
  def __init__(
11
22
  self,
12
23
  cron: str,
13
24
  allow_overlap: bool = False,
14
- exclusive: bool = False,
25
+ exclusive: bool = True,
15
26
  timeout: int | None = None,
16
27
  exception_handler: Callable[[BaseException], None] | None = None,
28
+ name: str | None = None,
17
29
  ) -> None:
18
30
  """
19
31
  :param cron: A string representing the cron expression for the scheduled action.
20
32
  :param allow_overlap: A boolean indicating if the scheduled action should new executions even if the previous one is still running.
21
33
  :param exclusive: A boolean indicating if the scheduled action should be executed in one instance of the application. (Requires a distributed lock provided by a backend)
34
+ :param exception_handler: A callable that will be called when an exception is raised during the execution of the scheduled action.
35
+ :param timeout: An integer representing the timeout for the scheduled action in seconds. If the scheduled action takes longer than this time, it will be terminated.
36
+ :param name: An optional name for the scheduled action, used for filtering which actions to run.
22
37
  """
23
38
  self.cron = cron
39
+ """
40
+ A string representing the cron expression for the scheduled action.
41
+ """
42
+
24
43
  self.allow_overlap = allow_overlap
44
+ """
45
+ A boolean indicating if the scheduled action should new executions even if the previous one is still running.
46
+ """
47
+
25
48
  self.exclusive = exclusive
49
+ """
50
+ A boolean indicating if the scheduled action should be executed
51
+ in one instance of the application. (Requires a distributed lock provided by a backend)
52
+ """
53
+
26
54
  self.exception_handler = exception_handler
55
+ """
56
+ A callable that will be called when an exception is raised during the execution of the scheduled action.
57
+ """
58
+
27
59
  self.timeout = timeout
60
+ """
61
+ An integer representing the timeout for the scheduled action in seconds.
62
+ If the scheduled action takes longer than this time, it will be terminated.
63
+ """
28
64
 
29
- def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
30
- ScheduledAction.register(func, self)
31
- return func
65
+ self.name = name
66
+ """
67
+ An optional name for the scheduled action, used for filtering which actions to run.
68
+ """
32
69
 
33
70
  @staticmethod
34
- def register(func: DECORATED_FUNC, scheduled_action: "ScheduledAction") -> None:
35
- setattr(func, ScheduledAction.SCHEDULED_ACTION_ATTR, scheduled_action)
71
+ def get_function_id(
72
+ func: Callable[..., Any],
73
+ ) -> str:
74
+ """
75
+ Get the function ID of the scheduled action.
76
+ This is used to identify the scheduled action in the message broker.
77
+ """
78
+ return f"{func.__module__}.{func.__qualname__}"
36
79
 
37
- @staticmethod
38
- def get_scheduled_action(func: DECORATED_FUNC) -> "ScheduledAction | None":
39
- if not hasattr(func, ScheduledAction.SCHEDULED_ACTION_ATTR):
40
- return None
41
80
 
42
- return cast(
43
- ScheduledAction, getattr(func, ScheduledAction.SCHEDULED_ACTION_ATTR)
44
- )
81
+ @dataclass(frozen=True)
82
+ class ScheduledActionData:
83
+ spec: ScheduledAction
84
+ controller_member: ControllerMemberReflect
85
+ callable: Callable[..., Awaitable[None]]
45
86
 
46
- @staticmethod
47
- def get_type_scheduled_actions(
48
- instance: Any,
49
- ) -> list[tuple[Callable[..., Any], "ScheduledAction"]]:
50
87
 
51
- members = inspect.getmembers(instance, predicate=inspect.ismethod)
88
+ def get_type_scheduled_actions(
89
+ instance: Any,
90
+ ) -> list[ScheduledActionData]:
91
+
92
+ _, member_metadata_map = inspect_controller(instance.__class__)
93
+
94
+ members: list[tuple[str, FunctionType]] = []
95
+ for name, value in inspect.getmembers_static(
96
+ instance, predicate=inspect.isfunction
97
+ ):
98
+
99
+ members.append((name, value))
52
100
 
53
- scheduled_actions: list[tuple[Callable[..., Any], "ScheduledAction"]] = []
101
+ scheduled_actions: list[ScheduledActionData] = []
54
102
 
55
- for _, member in members:
56
- scheduled_action = ScheduledAction.get_scheduled_action(member)
103
+ for name, member in members:
104
+ scheduled_action = ScheduledAction.get_last(member)
57
105
 
58
- if scheduled_action is None:
59
- continue
106
+ if scheduled_action is None:
107
+ continue
60
108
 
61
- scheduled_actions.append((member, scheduled_action))
109
+ if name not in member_metadata_map:
110
+ raise Exception(
111
+ f"Member '{name}' is not a valid controller member in '{instance.__class__.__name__}'"
112
+ )
113
+
114
+ member_metadata = member_metadata_map[name]
115
+
116
+ scheduled_actions.append(
117
+ ScheduledActionData(
118
+ callable=member,
119
+ spec=scheduled_action,
120
+ controller_member=member_metadata,
121
+ )
122
+ )
62
123
 
63
- return scheduled_actions
124
+ return scheduled_actions
@@ -0,0 +1,11 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class DelayedMessageData(BaseModel):
9
+ message_topic: str
10
+ dispatch_time: int
11
+ payload: bytes
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -1,31 +1,19 @@
1
- from typing import Any, Type, TypeVar, cast
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from typing import Any, Type, TypeVar
2
6
 
3
7
  from pydantic import BaseModel
4
8
 
5
9
  from jararaca.core.providers import Token
10
+ from jararaca.reflect.decorators import StackableDecorator
6
11
 
7
12
  DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
8
13
 
9
14
 
10
- class RequiresConfig:
11
-
12
- REQUIRE_CONFIG_ATTR = "__requires_config__"
15
+ class RequiresConfig(StackableDecorator):
13
16
 
14
17
  def __init__(self, token: Token[Any], config: Type[BaseModel]):
15
18
  self.config = config
16
19
  self.token = token
17
-
18
- @staticmethod
19
- def register(cls: Type[DECORATED_CLASS], config: "RequiresConfig") -> None:
20
- setattr(cls, RequiresConfig.REQUIRE_CONFIG_ATTR, config)
21
-
22
- @staticmethod
23
- def get(cls: Type[DECORATED_CLASS]) -> "RequiresConfig | None":
24
- if not hasattr(cls, RequiresConfig.REQUIRE_CONFIG_ATTR):
25
- return None
26
-
27
- return cast(RequiresConfig, getattr(cls, RequiresConfig.REQUIRE_CONFIG_ATTR))
28
-
29
- def __call__(self, cls: Type[DECORATED_CLASS]) -> Type[DECORATED_CLASS]:
30
- RequiresConfig.register(cls, self)
31
- return cls
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  import logging
2
6
  import os
3
7
  from contextlib import asynccontextmanager
@@ -7,9 +11,9 @@ from pydantic import BaseModel
7
11
 
8
12
  from jararaca.core.providers import Token
9
13
  from jararaca.microservice import (
10
- AppContext,
11
14
  AppInterceptor,
12
15
  AppInterceptorWithLifecycle,
16
+ AppTransactionContext,
13
17
  Container,
14
18
  Microservice,
15
19
  )
@@ -40,7 +44,9 @@ class AppConfigurationInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
40
44
  self.config_parser = config_parser
41
45
 
42
46
  @asynccontextmanager
43
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
47
+ async def intercept(
48
+ self, app_context: AppTransactionContext
49
+ ) -> AsyncGenerator[None, None]:
44
50
  yield
45
51
 
46
52
  def instance_basemodels(self, basemodel_type: Type[BaseModel]) -> BaseModel:
@@ -54,7 +60,7 @@ class AppConfigurationInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
54
60
  *[
55
61
  (config.token, config.config)
56
62
  for controller in app.controllers
57
- if (config := RequiresConfig.get(controller))
63
+ if (config := RequiresConfig.get_last(controller))
58
64
  ],
59
65
  ]
60
66
 
@@ -88,7 +94,7 @@ class AppConfigurationInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
88
94
 
89
95
  yield
90
96
 
91
- logger.info("finalizando")
97
+ logger.debug("finalizando")
92
98
 
93
99
 
94
100
  class AppConfigValidationError(Exception):
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -0,0 +1,120 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from typing import Any, Callable, TypeVar
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from jararaca.reflect.decorators import StackableDecorator
10
+
11
+ DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
12
+
13
+
14
+ class QueryEndpoint(StackableDecorator):
15
+ """
16
+ Decorator to mark a endpoint function as a query endpoint for Typescript generation.
17
+ """
18
+
19
+ def __init__(self, has_infinite_query: bool = False) -> None:
20
+ """
21
+ Initialize the QueryEndpoint decorator.
22
+
23
+ Args:
24
+ has_infinite_query: Whether the query endpoint supports infinite queries.
25
+ Important:
26
+ - Make sure a PaginatedQuery child instance is on the first argument
27
+ - Make sure the endpoint is a Patch (recommended) or Put method
28
+ - Make sure the endpoint returns a Paginated[T]
29
+ """
30
+ self.has_infinite_query = has_infinite_query
31
+
32
+ @staticmethod
33
+ def extract_query_endpoint(func: Any) -> "QueryEndpoint | None":
34
+ """
35
+ Check if the function is marked as a query endpoint.
36
+ """
37
+ return QueryEndpoint.get_last(func)
38
+
39
+
40
+ class MutationEndpoint(StackableDecorator):
41
+ """
42
+ Decorator to mark a endpoint function as a mutation endpoint for Typescript generation.
43
+ """
44
+
45
+ def __init__(self) -> None: ...
46
+
47
+ @staticmethod
48
+ def is_mutation(func: Any) -> bool:
49
+ """
50
+ Check if the function is marked as a mutation endpoint.
51
+ """
52
+ return MutationEndpoint.get_last(func) is not None
53
+
54
+
55
+ BASEMODEL_T = TypeVar("BASEMODEL_T", bound=BaseModel)
56
+
57
+
58
+ class SplitInputOutput(StackableDecorator):
59
+ """
60
+ Decorator to mark a Pydantic model to generate separate Input and Output TypeScript interfaces.
61
+
62
+ Input interface: Used for API inputs (mutations/queries), handles optional fields with defaults
63
+ Output interface: Used for API outputs, represents the complete object structure
64
+ """
65
+
66
+ def __init__(self) -> None:
67
+ pass
68
+
69
+ @staticmethod
70
+ def is_split_model(cls: type) -> bool:
71
+ """
72
+ Check if the Pydantic model is marked for split interface generation.
73
+ """
74
+ return SplitInputOutput.get_last(cls) is not None
75
+
76
+
77
+ class ExposeType:
78
+ """
79
+ Decorator to explicitly expose types for TypeScript interface generation.
80
+
81
+ Use this decorator to include types in the generated TypeScript output without
82
+ needing them as request/response bodies or indirect dependencies.
83
+
84
+ Example:
85
+ @ExposeType()
86
+ class UserRole(BaseModel):
87
+ id: str
88
+ name: str
89
+
90
+ # This ensures UserRole interface is generated even if it's not
91
+ # directly referenced in any REST endpoint
92
+ """
93
+
94
+ METADATA_KEY = "__jararaca_expose_type__"
95
+ _exposed_types: set[type] = set()
96
+
97
+ def __init__(self) -> None:
98
+ pass
99
+
100
+ def __call__(self, cls: type[BASEMODEL_T]) -> type[BASEMODEL_T]:
101
+ """
102
+ Decorate the type to mark it for explicit TypeScript generation.
103
+ """
104
+ setattr(cls, self.METADATA_KEY, True)
105
+ ExposeType._exposed_types.add(cls)
106
+ return cls
107
+
108
+ @staticmethod
109
+ def is_exposed_type(cls: type) -> bool:
110
+ """
111
+ Check if the type is marked for explicit exposure.
112
+ """
113
+ return getattr(cls, ExposeType.METADATA_KEY, False)
114
+
115
+ @staticmethod
116
+ def get_all_exposed_types() -> set[type]:
117
+ """
118
+ Get all types that have been marked for explicit exposure.
119
+ """
120
+ return ExposeType._exposed_types.copy()