jararaca 0.3.15__tar.gz → 0.3.17__tar.gz

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.

Files changed (91) hide show
  1. {jararaca-0.3.15 → jararaca-0.3.17}/PKG-INFO +2 -1
  2. {jararaca-0.3.15 → jararaca-0.3.17}/README.md +1 -0
  3. jararaca-0.3.17/docs/expose-type.md +221 -0
  4. {jararaca-0.3.15 → jararaca-0.3.17}/pyproject.toml +41 -1
  5. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/__init__.py +3 -0
  6. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/decorators.py +7 -4
  7. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/worker.py +111 -237
  8. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/tools/typescript/decorators.py +46 -0
  9. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/tools/typescript/interface_parser.py +4 -0
  10. {jararaca-0.3.15 → jararaca-0.3.17}/LICENSE +0 -0
  11. {jararaca-0.3.15 → jararaca-0.3.17}/docs/CNAME +0 -0
  12. {jararaca-0.3.15 → jararaca-0.3.17}/docs/architecture.md +0 -0
  13. {jararaca-0.3.15 → jararaca-0.3.17}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg +0 -0
  14. {jararaca-0.3.15 → jararaca-0.3.17}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.webp +0 -0
  15. {jararaca-0.3.15 → jararaca-0.3.17}/docs/assets/tracing_example.png +0 -0
  16. {jararaca-0.3.15 → jararaca-0.3.17}/docs/http-rpc.md +0 -0
  17. {jararaca-0.3.15 → jararaca-0.3.17}/docs/index.md +0 -0
  18. {jararaca-0.3.15 → jararaca-0.3.17}/docs/interceptors.md +0 -0
  19. {jararaca-0.3.15 → jararaca-0.3.17}/docs/messagebus.md +0 -0
  20. {jararaca-0.3.15 → jararaca-0.3.17}/docs/retry.md +0 -0
  21. {jararaca-0.3.15 → jararaca-0.3.17}/docs/scheduler.md +0 -0
  22. {jararaca-0.3.15 → jararaca-0.3.17}/docs/stylesheets/custom.css +0 -0
  23. {jararaca-0.3.15 → jararaca-0.3.17}/docs/websocket.md +0 -0
  24. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/__main__.py +0 -0
  25. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/broker_backend/__init__.py +0 -0
  26. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/broker_backend/mapper.py +0 -0
  27. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/broker_backend/redis_broker_backend.py +0 -0
  28. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/cli.py +0 -0
  29. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/common/__init__.py +0 -0
  30. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/core/__init__.py +0 -0
  31. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/core/providers.py +0 -0
  32. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/core/uow.py +0 -0
  33. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/di.py +0 -0
  34. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/files/entity.py.mako +0 -0
  35. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/lifecycle.py +0 -0
  36. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/__init__.py +0 -0
  37. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/bus_message_controller.py +0 -0
  38. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/consumers/__init__.py +0 -0
  39. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/interceptors/__init__.py +0 -0
  40. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +0 -0
  41. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/interceptors/publisher_interceptor.py +0 -0
  42. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/message.py +0 -0
  43. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/messagebus/publisher.py +0 -0
  44. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/microservice.py +0 -0
  45. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/observability/decorators.py +0 -0
  46. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/observability/interceptor.py +0 -0
  47. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/observability/providers/__init__.py +0 -0
  48. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/observability/providers/otel.py +0 -0
  49. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/base.py +0 -0
  50. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/exports.py +0 -0
  51. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/interceptors/__init__.py +0 -0
  52. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/interceptors/aiosqa_interceptor.py +0 -0
  53. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/interceptors/constants.py +0 -0
  54. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/interceptors/decorators.py +0 -0
  55. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/session.py +0 -0
  56. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/sort_filter.py +0 -0
  57. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/persistence/utilities.py +0 -0
  58. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/__init__.py +0 -0
  59. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/decorators.py +0 -0
  60. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/hooks.py +0 -0
  61. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/http_microservice.py +0 -0
  62. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/server.py +0 -0
  63. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/__init__.py +0 -0
  64. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/base_types.py +0 -0
  65. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/context.py +0 -0
  66. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/decorators.py +0 -0
  67. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/redis.py +0 -0
  68. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/types.py +0 -0
  69. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/presentation/websocket/websocket_interceptor.py +0 -0
  70. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/py.typed +0 -0
  71. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/reflect/__init__.py +0 -0
  72. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/reflect/controller_inspect.py +0 -0
  73. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/reflect/metadata.py +0 -0
  74. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/__init__.py +0 -0
  75. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/http/__init__.py +0 -0
  76. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/http/backends/__init__.py +0 -0
  77. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/http/backends/httpx.py +0 -0
  78. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/http/backends/otel.py +0 -0
  79. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/http/decorators.py +0 -0
  80. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/rpc/http/httpx.py +0 -0
  81. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/scheduler/__init__.py +0 -0
  82. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/scheduler/beat_worker.py +0 -0
  83. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/scheduler/decorators.py +0 -0
  84. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/scheduler/types.py +0 -0
  85. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/tools/app_config/__init__.py +0 -0
  86. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/tools/app_config/decorators.py +0 -0
  87. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/tools/app_config/interceptor.py +0 -0
  88. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/tools/typescript/__init__.py +0 -0
  89. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/utils/__init__.py +0 -0
  90. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/utils/rabbitmq_utils.py +0 -0
  91. {jararaca-0.3.15 → jararaca-0.3.17}/src/jararaca/utils/retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.15
3
+ Version: 0.3.17
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
@@ -74,6 +74,7 @@ Jararaca is an async-first microservice framework designed to simplify the devel
74
74
  - Command-line tool for generating TypeScript types
75
75
  - Support for REST endpoints, WebSocket events, and message bus payloads
76
76
  - Type-safe frontend-backend communication
77
+ - **`@ExposeType` decorator** - Explicitly expose types for TypeScript generation without needing them in endpoints
77
78
 
78
79
  ### Hexagonal Architecture
79
80
  - Clear separation of concerns
@@ -37,6 +37,7 @@ Jararaca is an async-first microservice framework designed to simplify the devel
37
37
  - Command-line tool for generating TypeScript types
38
38
  - Support for REST endpoints, WebSocket events, and message bus payloads
39
39
  - Type-safe frontend-backend communication
40
+ - **`@ExposeType` decorator** - Explicitly expose types for TypeScript generation without needing them in endpoints
40
41
 
41
42
  ### Hexagonal Architecture
42
43
  - Clear separation of concerns
@@ -0,0 +1,221 @@
1
+ # Exposing Types for TypeScript Generation
2
+
3
+ The `@ExposeType` decorator allows you to explicitly expose types for TypeScript interface generation without requiring them to be directly referenced in REST endpoints, WebSocket messages, or as indirect dependencies.
4
+
5
+ ## Use Cases
6
+
7
+ The `@ExposeType` decorator is useful when you have types that:
8
+
9
+ 1. **Are used only on the frontend** - Types that exist for frontend state management, validation, or business logic
10
+ 2. **Are part of a shared schema** - Common types that multiple parts of your application use but aren't directly in API contracts
11
+ 3. **Need to be pre-generated** - Types that you want available immediately even if they're not yet used in any endpoints
12
+ 4. **Are utility types** - Helper types, enums, or constants that the frontend needs to know about
13
+
14
+ ## Basic Usage
15
+
16
+ Simply decorate any Pydantic model with `@ExposeType()`:
17
+
18
+ ```python
19
+ from pydantic import BaseModel
20
+
21
+ from jararaca import ExposeType
22
+
23
+
24
+ @ExposeType()
25
+ class UserPermission(BaseModel):
26
+ id: str
27
+ name: str
28
+ description: str
29
+ resource: str
30
+ action: str
31
+ ```
32
+
33
+ This type will now be included in the generated TypeScript output when you run:
34
+
35
+ ```bash
36
+ jararaca gen-tsi app:app output.ts
37
+ ```
38
+
39
+ ## Example: Frontend-Only Types
40
+
41
+ ```python
42
+ from pydantic import BaseModel
43
+
44
+ from jararaca import ExposeType
45
+
46
+
47
+ @ExposeType()
48
+ class FilterState(BaseModel):
49
+ """Frontend state for table filtering."""
50
+ search_query: str
51
+ sort_column: str
52
+ sort_direction: str
53
+ page: int
54
+ page_size: int
55
+
56
+ @ExposeType()
57
+ class UITheme(BaseModel):
58
+ """Frontend theme configuration."""
59
+ primary_color: str
60
+ secondary_color: str
61
+ dark_mode: bool
62
+ ```
63
+
64
+ ## Example: Error Codes and Constants
65
+
66
+ ```python
67
+ from enum import Enum
68
+
69
+ from pydantic import BaseModel
70
+
71
+ from jararaca import ExposeType
72
+
73
+
74
+ @ExposeType()
75
+ class ErrorCode(str, Enum):
76
+ """Standard error codes."""
77
+ UNAUTHORIZED = "UNAUTHORIZED"
78
+ NOT_FOUND = "NOT_FOUND"
79
+ VALIDATION_ERROR = "VALIDATION_ERROR"
80
+ INTERNAL_ERROR = "INTERNAL_ERROR"
81
+
82
+ @ExposeType()
83
+ class ApiErrorDetail(BaseModel):
84
+ """Detailed error information."""
85
+ code: ErrorCode
86
+ message: str
87
+ field: str | None = None
88
+ details: dict[str, str] | None = None
89
+ ```
90
+
91
+ ## Example: Complex Nested Types
92
+
93
+ ```python
94
+ from pydantic import BaseModel
95
+
96
+ from jararaca import ExposeType
97
+
98
+
99
+ @ExposeType()
100
+ class Address(BaseModel):
101
+ street: str
102
+ city: str
103
+ country: str
104
+ postal_code: str
105
+
106
+ @ExposeType()
107
+ class ContactInfo(BaseModel):
108
+ email: str
109
+ phone: str | None = None
110
+ address: Address
111
+
112
+ @ExposeType()
113
+ class Organization(BaseModel):
114
+ """Complete organization structure."""
115
+ id: str
116
+ name: str
117
+ contacts: list[ContactInfo]
118
+ settings: dict[str, str]
119
+ ```
120
+
121
+ When you expose a type with nested structures, the decorator ensures that all related types are also included in the TypeScript generation.
122
+
123
+ ## Comparison: With vs Without @ExposeType
124
+
125
+ ### Without @ExposeType
126
+
127
+ ```python
128
+ class UserRole(BaseModel):
129
+ """Only generated if used in an endpoint or as a dependency."""
130
+ id: str
131
+ name: str
132
+
133
+ @RestController("/api/users")
134
+ class UserController:
135
+ @Get("/{user_id}")
136
+ async def get_user(self, user_id: str) -> UserResponse:
137
+ # UserRole is only generated if UserResponse references it
138
+ return UserResponse(...)
139
+ ```
140
+
141
+ ### With @ExposeType
142
+
143
+ ```python
144
+ @ExposeType()
145
+ class UserRole(BaseModel):
146
+ """Always generated, available immediately."""
147
+ id: str
148
+ name: str
149
+
150
+ @RestController("/api/users")
151
+ class UserController:
152
+ @Get("/{user_id}")
153
+ async def get_user(self, user_id: str) -> UserResponse:
154
+ # UserRole is available in TypeScript even if not used yet
155
+ return UserResponse(...)
156
+ ```
157
+
158
+ ## Integration with Other Decorators
159
+
160
+ The `@ExposeType` decorator works seamlessly with other TypeScript generation decorators:
161
+
162
+ ```python
163
+ from jararaca import ExposeType, SplitInputOutput
164
+
165
+
166
+ @ExposeType()
167
+ @SplitInputOutput()
168
+ class UserProfile(BaseModel):
169
+ """Generates UserProfileInput and UserProfileOutput interfaces."""
170
+ id: str
171
+ username: str
172
+ email: str
173
+ created_at: str
174
+ updated_at: str
175
+ ```
176
+
177
+ This creates both `UserProfileInput` and `UserProfileOutput` TypeScript interfaces.
178
+
179
+ ## Best Practices
180
+
181
+ 1. **Use for shared types**: Apply `@ExposeType` to types that are used across multiple parts of your application
182
+ 2. **Document the purpose**: Add clear docstrings explaining why a type is exposed
183
+ 3. **Avoid overuse**: Only expose types that the frontend actually needs - don't expose internal implementation details
184
+ 4. **Combine with other decorators**: Use alongside `@SplitInputOutput` when appropriate
185
+ 5. **Group related types**: Keep exposed types in dedicated modules (e.g., `shared_types.py`)
186
+
187
+ ## Viewing Exposed Types
188
+
189
+ All types decorated with `@ExposeType` are tracked globally. You can check which types are exposed:
190
+
191
+ ```python
192
+ from jararaca.tools.typescript.decorators import ExposeType
193
+
194
+ # Get all exposed types
195
+ exposed = ExposeType.get_all_exposed_types()
196
+ print(f"Exposed {len(exposed)} types: {[t.__name__ for t in exposed]}")
197
+ ```
198
+
199
+ ## Generated TypeScript
200
+
201
+ Given this Python code:
202
+
203
+ ```python
204
+ @ExposeType()
205
+ class NotificationPreference(BaseModel):
206
+ email_enabled: bool
207
+ push_enabled: bool
208
+ frequency: str
209
+ ```
210
+
211
+ The generated TypeScript will be:
212
+
213
+ ```typescript
214
+ export interface NotificationPreference {
215
+ emailEnabled: boolean;
216
+ pushEnabled: boolean;
217
+ frequency: string;
218
+ }
219
+ ```
220
+
221
+ The type is available in your TypeScript code even if no REST endpoint uses it yet.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.15"
3
+ version = "0.3.17"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"
@@ -62,6 +62,10 @@ mkdocs-mermaid2-plugin = "^1.2.1"
62
62
  [tool.poetry.group.dev.dependencies]
63
63
  httptools = "^0.6.1"
64
64
  httpx = "^0.27.2"
65
+ pytest = "^8.0.0"
66
+ pytest-asyncio = "^0.23.0"
67
+ pytest-cov = "^4.1.0"
68
+ pytest-mock = "^3.12.0"
65
69
 
66
70
  [build-system]
67
71
  requires = ["poetry-core"]
@@ -84,3 +88,39 @@ jararaca = "jararaca.cli:cli"
84
88
  [[tool.mypy.overrides]]
85
89
  module = "mako.*"
86
90
  ignore_missing_imports = true
91
+
92
+ [tool.pytest.ini_options]
93
+ testpaths = ["tests"]
94
+ python_files = ["test_*.py"]
95
+ python_classes = ["Test*"]
96
+ python_functions = ["test_*"]
97
+ asyncio_mode = "auto"
98
+ addopts = [
99
+ "--strict-markers",
100
+ "--strict-config",
101
+ "--showlocals",
102
+ ]
103
+ markers = [
104
+ "unit: Unit tests",
105
+ "integration: Integration tests",
106
+ "slow: Slow tests",
107
+ ]
108
+
109
+ [tool.coverage.run]
110
+ source = ["src/jararaca"]
111
+ omit = [
112
+ "*/tests/*",
113
+ "*/__pycache__/*",
114
+ "*/.venv/*",
115
+ ]
116
+
117
+ [tool.coverage.report]
118
+ exclude_lines = [
119
+ "pragma: no cover",
120
+ "def __repr__",
121
+ "raise AssertionError",
122
+ "raise NotImplementedError",
123
+ "if __name__ == .__main__.:",
124
+ "if TYPE_CHECKING:",
125
+ "@abstractmethod",
126
+ ]
@@ -171,6 +171,7 @@ if TYPE_CHECKING:
171
171
  from .scheduler.decorators import ScheduledAction
172
172
  from .tools.app_config.interceptor import AppConfigurationInterceptor
173
173
  from .tools.typescript.decorators import (
174
+ ExposeType,
174
175
  MutationEndpoint,
175
176
  QueryEndpoint,
176
177
  SplitInputOutput,
@@ -279,6 +280,7 @@ if TYPE_CHECKING:
279
280
  "MessageBusPublisherInterceptor",
280
281
  "RedisWebSocketConnectionBackend",
281
282
  "AppConfigurationInterceptor",
283
+ "ExposeType",
282
284
  "QueryEndpoint",
283
285
  "MutationEndpoint",
284
286
  "SplitInputOutput",
@@ -510,6 +512,7 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
510
512
  "tools.app_config.interceptor",
511
513
  None,
512
514
  ),
515
+ "ExposeType": (__SPEC_PARENT__, "tools.typescript.decorators", None),
513
516
  "QueryEndpoint": (__SPEC_PARENT__, "tools.typescript.decorators", None),
514
517
  "MutationEndpoint": (__SPEC_PARENT__, "tools.typescript.decorators", None),
515
518
  "SplitInputOutput": (__SPEC_PARENT__, "tools.typescript.decorators", None),
@@ -11,6 +11,8 @@ from jararaca.scheduler.decorators import ScheduledAction, ScheduledActionData
11
11
 
12
12
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
13
13
  DECORATED_T = TypeVar("DECORATED_T", bound=Any)
14
+ INSTANCE_T = TypeVar("INSTANCE_T", bound=Any)
15
+ RETURN_T = TypeVar("RETURN_T", bound=Any)
14
16
 
15
17
 
16
18
  class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
@@ -35,8 +37,9 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
35
37
  self.name = name
36
38
 
37
39
  def __call__(
38
- self, func: Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]
39
- ) -> Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]:
40
+ self,
41
+ func: Callable[[INSTANCE_T, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]],
42
+ ) -> Callable[[INSTANCE_T, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]:
40
43
 
41
44
  MessageHandler[Any].register(func, self)
42
45
 
@@ -52,7 +55,7 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
52
55
 
53
56
  @staticmethod
54
57
  def get_message_incoming(
55
- func: Callable[[MessageOf[Any]], Awaitable[Any]],
58
+ func: Callable[[Any, MessageOf[Any]], Awaitable[None]],
56
59
  ) -> "MessageHandler[Message] | None":
57
60
  if not hasattr(func, MessageHandler.MESSAGE_INCOMING_ATTR):
58
61
  return None
@@ -66,7 +69,7 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
66
69
  class MessageHandlerData:
67
70
  message_type: type[Any]
68
71
  spec: MessageHandler[Message]
69
- instance_callable: Callable[[MessageOf[Any]], Awaitable[None]]
72
+ instance_callable: Callable[..., Awaitable[None]]
70
73
  controller_member: ControllerMemberReflect
71
74
 
72
75
 
@@ -188,11 +188,8 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
188
188
  # Connection resilience attributes
189
189
  self.connection_healthy = False
190
190
  self.connection_lock = asyncio.Lock()
191
- self.reconnection_event = asyncio.Event()
192
- self.reconnection_in_progress = False
193
191
  self.consumer_tags: dict[str, str] = {} # Track consumer tags for cleanup
194
192
  self.health_check_task: asyncio.Task[Any] | None = None
195
- self.reconnection_task: asyncio.Task[Any] | None = None
196
193
 
197
194
  async def _verify_infrastructure(self) -> bool:
198
195
  """
@@ -229,10 +226,6 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
229
226
  routing_key = f"{handler.message_type.MESSAGE_TOPIC}.#"
230
227
 
231
228
  async def setup_consumer() -> None:
232
- # Wait for connection to be healthy if reconnection is in progress
233
- if self.reconnection_in_progress:
234
- await self.reconnection_event.wait()
235
-
236
229
  # Create a channel using the context manager
237
230
  async with self.create_channel(queue_name) as channel:
238
231
  queue = await RabbitmqUtils.get_queue(
@@ -289,10 +282,6 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
289
282
  routing_key = queue_name
290
283
 
291
284
  async def setup_consumer() -> None:
292
- # Wait for connection to be healthy if reconnection is in progress
293
- if self.reconnection_in_progress:
294
- await self.reconnection_event.wait()
295
-
296
285
  # Create a channel using the context manager
297
286
  async with self.create_channel(queue_name) as channel:
298
287
  queue = await RabbitmqUtils.get_queue(
@@ -341,106 +330,107 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
341
330
  Main consume method that sets up all message handlers and scheduled actions with retry mechanisms.
342
331
  """
343
332
  # Establish initial connection
344
- async with self.connect() as connection:
345
- self.connection_healthy = True
346
-
347
- # Start connection health monitoring
348
- self.health_check_task = asyncio.create_task(
349
- self._monitor_connection_health()
350
- )
333
+ try:
334
+ async with self.connect() as connection:
335
+ self.connection_healthy = True
351
336
 
352
- # Verify infrastructure with retry
353
- infra_check_success = await retry_with_backoff(
354
- self._verify_infrastructure,
355
- retry_config=self.config.connection_retry_config,
356
- retry_exceptions=(Exception,),
357
- )
337
+ # Start connection health monitoring
338
+ self.health_check_task = asyncio.create_task(
339
+ self._monitor_connection_health()
340
+ )
358
341
 
359
- if not infra_check_success:
360
- logger.critical(
361
- "Failed to verify RabbitMQ infrastructure. Shutting down."
342
+ # Verify infrastructure with retry
343
+ infra_check_success = await retry_with_backoff(
344
+ self._verify_infrastructure,
345
+ retry_config=self.config.connection_retry_config,
346
+ retry_exceptions=(Exception,),
362
347
  )
363
- self.shutdown_event.set()
364
- return
365
348
 
366
- async def wait_for(
367
- type: str, name: str, coroutine: Awaitable[bool]
368
- ) -> tuple[str, str, bool]:
369
- return type, name, await coroutine
349
+ if not infra_check_success:
350
+ logger.critical(
351
+ "Failed to verify RabbitMQ infrastructure. Shutting down."
352
+ )
353
+ self.shutdown_event.set()
354
+ return
370
355
 
371
- tasks: set[asyncio.Task[tuple[str, str, bool]]] = set()
356
+ async def wait_for(
357
+ type: str, name: str, coroutine: Awaitable[bool]
358
+ ) -> tuple[str, str, bool]:
359
+ return type, name, await coroutine
372
360
 
373
- # Setup message handlers
374
- for handler in self.message_handler_set:
375
- queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.instance_callable.__module__}.{handler.instance_callable.__qualname__}"
376
- self.incoming_map[queue_name] = handler
361
+ tasks: set[asyncio.Task[tuple[str, str, bool]]] = set()
377
362
 
378
- tasks.add(
379
- task := asyncio.create_task(
380
- wait_for(
381
- "message_handler",
382
- queue_name,
383
- self._setup_message_handler_consumer(handler),
384
- )
385
- )
386
- )
363
+ # Setup message handlers
364
+ for handler in self.message_handler_set:
365
+ queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.instance_callable.__module__}.{handler.instance_callable.__qualname__}"
366
+ self.incoming_map[queue_name] = handler
387
367
 
388
- # Setup scheduled actions
389
- for scheduled_action in self.scheduled_actions:
390
- queue_name = f"{scheduled_action.callable.__module__}.{scheduled_action.callable.__qualname__}"
391
- tasks.add(
392
- task := asyncio.create_task(
393
- wait_for(
394
- "scheduled_action",
395
- queue_name,
396
- self._setup_scheduled_action_consumer(scheduled_action),
368
+ tasks.add(
369
+ task := asyncio.create_task(
370
+ wait_for(
371
+ "message_handler",
372
+ queue_name,
373
+ self._setup_message_handler_consumer(handler),
374
+ )
397
375
  )
398
376
  )
399
- )
400
377
 
401
- async def handle_task_results() -> None:
402
- for task in asyncio.as_completed(tasks):
403
- type, name, success = await task
404
- if success:
405
- logger.info(f"Successfully set up {type} consumer for {name}")
406
- else:
407
- logger.warning(
408
- f"Failed to set up {type} consumer for {name}, will not process messages from this queue"
378
+ # Setup scheduled actions
379
+ for scheduled_action in self.scheduled_actions:
380
+ queue_name = f"{scheduled_action.callable.__module__}.{scheduled_action.callable.__qualname__}"
381
+ tasks.add(
382
+ task := asyncio.create_task(
383
+ wait_for(
384
+ "scheduled_action",
385
+ queue_name,
386
+ self._setup_scheduled_action_consumer(scheduled_action),
387
+ )
409
388
  )
389
+ )
410
390
 
411
- handle_task_results_task = asyncio.create_task(handle_task_results())
412
-
413
- # Wait for shutdown signal
414
- await self.shutdown_event.wait()
415
- logger.info("Shutdown event received, stopping consumers")
391
+ async def handle_task_results() -> None:
392
+ for task in asyncio.as_completed(tasks):
393
+ type, name, success = await task
394
+ if success:
395
+ logger.info(
396
+ f"Successfully set up {type} consumer for {name}"
397
+ )
398
+ else:
399
+ logger.warning(
400
+ f"Failed to set up {type} consumer for {name}, will not process messages from this queue"
401
+ )
416
402
 
417
- # Cancel health monitoring
418
- if self.health_check_task:
419
- self.health_check_task.cancel()
420
- with suppress(asyncio.CancelledError):
421
- await self.health_check_task
403
+ handle_task_results_task = asyncio.create_task(handle_task_results())
422
404
 
423
- # Cancel reconnection task if running
424
- if self.reconnection_task:
425
- self.reconnection_task.cancel()
426
- with suppress(asyncio.CancelledError):
427
- await self.reconnection_task
405
+ # Wait for shutdown signal
406
+ await self.shutdown_event.wait()
407
+ logger.info("Shutdown event received, stopping consumers")
428
408
 
429
- handle_task_results_task.cancel()
430
- with suppress(asyncio.CancelledError):
431
- await handle_task_results_task
432
- for task in tasks:
433
- if not task.done():
434
- task.cancel()
409
+ # Cancel health monitoring
410
+ if self.health_check_task:
411
+ self.health_check_task.cancel()
435
412
  with suppress(asyncio.CancelledError):
436
- await task
437
- logger.info("Worker shutting down")
413
+ await self.health_check_task
438
414
 
439
- # Wait for all tasks to complete
440
- await self.wait_all_tasks_done()
441
-
442
- # Close all channels and the connection
443
- await self.close_channels_and_connection()
415
+ handle_task_results_task.cancel()
416
+ with suppress(asyncio.CancelledError):
417
+ await handle_task_results_task
418
+ for task in tasks:
419
+ if not task.done():
420
+ task.cancel()
421
+ with suppress(asyncio.CancelledError):
422
+ await task
423
+ logger.info("Worker shutting down")
424
+
425
+ # Wait for all tasks to complete
426
+ await self.wait_all_tasks_done()
427
+
428
+ # Close all channels and the connection
429
+ await self.close_channels_and_connection()
430
+ except Exception as e:
431
+ logger.critical(f"Failed to establish initial connection to RabbitMQ: {e}")
432
+ # Re-raise the exception so it can be caught by the caller
433
+ raise
444
434
 
445
435
  async def wait_all_tasks_done(self) -> None:
446
436
  if not self.tasks:
@@ -478,12 +468,6 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
478
468
  with suppress(asyncio.CancelledError):
479
469
  await self.health_check_task
480
470
 
481
- # Cancel reconnection task if running
482
- if self.reconnection_task:
483
- self.reconnection_task.cancel()
484
- with suppress(asyncio.CancelledError):
485
- await self.reconnection_task
486
-
487
471
  await self.wait_all_tasks_done()
488
472
  await self.close_channels_and_connection()
489
473
 
@@ -492,16 +476,6 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
492
476
  Get the channel for a specific queue, or None if not found.
493
477
  This helps with error handling when a channel might have been closed.
494
478
  """
495
- # If reconnection is in progress, wait for it to complete
496
- if self.reconnection_in_progress:
497
- try:
498
- await asyncio.wait_for(self.reconnection_event.wait(), timeout=30.0)
499
- except asyncio.TimeoutError:
500
- logger.warning(
501
- f"Timeout waiting for reconnection when getting channel for {queue_name}"
502
- )
503
- return None
504
-
505
479
  if queue_name not in self.channels:
506
480
  logger.warning(f"No channel found for queue {queue_name}")
507
481
  return None
@@ -530,17 +504,17 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
530
504
  logger.error(
531
505
  f"Failed to recreate channel for {queue_name}: {e}"
532
506
  )
533
- # Trigger reconnection if channel creation fails
507
+ # Trigger shutdown if channel creation fails
534
508
  self._trigger_reconnection()
535
509
  return None
536
510
  else:
537
- # Connection is not healthy, trigger reconnection
511
+ # Connection is not healthy, trigger shutdown
538
512
  self._trigger_reconnection()
539
513
  return None
540
514
  return channel
541
515
  except Exception as e:
542
516
  logger.error(f"Error accessing channel for queue {queue_name}: {e}")
543
- # Trigger reconnection on any channel access error
517
+ # Trigger shutdown on any channel access error
544
518
  self._trigger_reconnection()
545
519
  return None
546
520
 
@@ -691,33 +665,14 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
691
665
  yield new_channel
692
666
  return
693
667
  else:
694
- # Connection is not healthy, wait for reconnection
695
- if self.reconnection_in_progress:
696
- try:
697
- await asyncio.wait_for(
698
- self.reconnection_event.wait(), timeout=30.0
699
- )
700
- # Retry after reconnection
701
- continue
702
- except asyncio.TimeoutError:
703
- logger.warning(
704
- f"Timeout waiting for reconnection for queue {queue_name}"
705
- )
706
-
707
- # Still no connection, trigger reconnection
708
- if not self.reconnection_in_progress:
709
- self._trigger_reconnection()
710
-
711
- if attempt < max_retries - 1:
712
- logger.info(
713
- f"Retrying channel access for {queue_name} in {retry_delay}s"
714
- )
715
- await asyncio.sleep(retry_delay)
716
- retry_delay *= 2
717
- else:
718
- raise RuntimeError(
719
- f"Cannot get channel for queue {queue_name}: no connection available after {max_retries} attempts"
720
- )
668
+ # Connection is not healthy, trigger shutdown
669
+ logger.error(
670
+ f"Connection not healthy while getting channel for {queue_name}, triggering shutdown"
671
+ )
672
+ self._trigger_reconnection()
673
+ raise RuntimeError(
674
+ f"Cannot get channel for queue {queue_name}: connection is not healthy"
675
+ )
721
676
 
722
677
  except Exception as e:
723
678
  if attempt < max_retries - 1:
@@ -734,7 +689,7 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
734
689
 
735
690
  async def _monitor_connection_health(self) -> None:
736
691
  """
737
- Monitor connection health and trigger reconnection if needed.
692
+ Monitor connection health and trigger shutdown if connection is lost.
738
693
  This runs as a background task.
739
694
  """
740
695
  while not self.shutdown_event.is_set():
@@ -746,11 +701,11 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
746
701
 
747
702
  # Check connection health
748
703
  if not await self._is_connection_healthy():
749
- logger.warning(
750
- "Connection health check failed, triggering reconnection"
704
+ logger.error(
705
+ "Connection health check failed, initiating worker shutdown"
751
706
  )
752
- if not self.reconnection_in_progress:
753
- self._trigger_reconnection()
707
+ self.shutdown()
708
+ break
754
709
 
755
710
  except asyncio.CancelledError:
756
711
  logger.info("Connection health monitoring cancelled")
@@ -778,74 +733,12 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
778
733
 
779
734
  def _trigger_reconnection(self) -> None:
780
735
  """
781
- Trigger reconnection process.
736
+ Trigger worker shutdown due to connection loss.
782
737
  """
783
- if not self.reconnection_in_progress and not self.shutdown_event.is_set():
784
- self.reconnection_in_progress = True
738
+ if not self.shutdown_event.is_set():
739
+ logger.error("Connection lost, initiating worker shutdown")
785
740
  self.connection_healthy = False
786
- self.reconnection_event.clear()
787
-
788
- # Start reconnection task
789
- self.reconnection_task = asyncio.create_task(self._handle_reconnection())
790
- self.reconnection_task.add_done_callback(self._on_reconnection_done)
791
-
792
- def _on_reconnection_done(self, task: asyncio.Task[Any]) -> None:
793
- """
794
- Handle completion of reconnection task.
795
- """
796
- self.reconnection_in_progress = False
797
- if task.exception():
798
- logger.error(f"Reconnection task failed: {task.exception()}")
799
- else:
800
- logger.info("Reconnection completed successfully")
801
-
802
- async def _handle_reconnection(self) -> None:
803
- """
804
- Handle the reconnection process with exponential backoff.
805
- """
806
- logger.info("Starting reconnection process")
807
-
808
- # Close existing connection and channels
809
- await self._cleanup_connection()
810
-
811
- reconnection_config = self.config.reconnection_backoff_config
812
- attempt = 0
813
-
814
- while not self.shutdown_event.is_set():
815
- try:
816
- attempt += 1
817
- logger.info(f"Reconnection attempt {attempt}")
818
-
819
- # Establish new connection
820
- self.connection = await self._establish_connection()
821
- self.connection_healthy = True
822
-
823
- # Re-establish all consumers
824
- await self._reestablish_consumers()
825
-
826
- logger.info("Reconnection successful")
827
- self.reconnection_event.set()
828
- return
829
-
830
- except Exception as e:
831
- logger.error(f"Reconnection attempt {attempt} failed: {e}")
832
-
833
- if self.shutdown_event.is_set():
834
- break
835
-
836
- # Calculate backoff delay
837
- delay = reconnection_config.initial_delay * (
838
- reconnection_config.backoff_factor ** (attempt - 1)
839
- )
840
- if reconnection_config.jitter:
841
- jitter_amount = delay * 0.25
842
- delay = delay + random.uniform(-jitter_amount, jitter_amount)
843
- delay = max(delay, 0.1)
844
-
845
- delay = min(delay, reconnection_config.max_delay)
846
-
847
- logger.info(f"Retrying reconnection in {delay:.2f} seconds")
848
- await asyncio.sleep(delay)
741
+ self.shutdown()
849
742
 
850
743
  async def _cleanup_connection(self) -> None:
851
744
  """
@@ -889,32 +782,6 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
889
782
  self.connection = None
890
783
  self.connection_healthy = False
891
784
 
892
- async def _reestablish_consumers(self) -> None:
893
- """
894
- Re-establish all consumers after reconnection.
895
- """
896
- logger.info("Re-establishing consumers after reconnection")
897
-
898
- # Re-establish message handlers
899
- for handler in self.message_handler_set:
900
- queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.instance_callable.__module__}.{handler.instance_callable.__qualname__}"
901
- try:
902
- await self._setup_message_handler_consumer(handler)
903
- logger.info(f"Re-established consumer for {queue_name}")
904
- except Exception as e:
905
- logger.error(f"Failed to re-establish consumer for {queue_name}: {e}")
906
-
907
- # Re-establish scheduled actions
908
- for scheduled_action in self.scheduled_actions:
909
- queue_name = f"{scheduled_action.callable.__module__}.{scheduled_action.callable.__qualname__}"
910
- try:
911
- await self._setup_scheduled_action_consumer(scheduled_action)
912
- logger.info(f"Re-established scheduler consumer for {queue_name}")
913
- except Exception as e:
914
- logger.error(
915
- f"Failed to re-establish scheduler consumer for {queue_name}: {e}"
916
- )
917
-
918
785
 
919
786
  def create_message_bus(
920
787
  broker_url: str,
@@ -1798,7 +1665,14 @@ class MessageBusWorker:
1798
1665
  loop.add_signal_handler(signal.SIGINT, on_shutdown, loop)
1799
1666
  # Add graceful shutdown handler for SIGTERM as well
1800
1667
  loop.add_signal_handler(signal.SIGTERM, on_shutdown, loop)
1801
- runner.run(self.start_async())
1668
+ try:
1669
+ runner.run(self.start_async())
1670
+ except Exception as e:
1671
+ logger.critical(f"Worker failed to start due to connection error: {e}")
1672
+ # Exit with error code 1 to indicate startup failure
1673
+ import sys
1674
+
1675
+ sys.exit(1)
1802
1676
 
1803
1677
  async def _graceful_shutdown(self) -> None:
1804
1678
  """Handles graceful shutdown process"""
@@ -93,3 +93,49 @@ class SplitInputOutput:
93
93
  Check if the Pydantic model is marked for split interface generation.
94
94
  """
95
95
  return getattr(cls, SplitInputOutput.METADATA_KEY, False)
96
+
97
+
98
+ class ExposeType:
99
+ """
100
+ Decorator to explicitly expose types for TypeScript interface generation.
101
+
102
+ Use this decorator to include types in the generated TypeScript output without
103
+ needing them as request/response bodies or indirect dependencies.
104
+
105
+ Example:
106
+ @ExposeType()
107
+ class UserRole(BaseModel):
108
+ id: str
109
+ name: str
110
+
111
+ # This ensures UserRole interface is generated even if it's not
112
+ # directly referenced in any REST endpoint
113
+ """
114
+
115
+ METADATA_KEY = "__jararaca_expose_type__"
116
+ _exposed_types: set[type] = set()
117
+
118
+ def __init__(self) -> None:
119
+ pass
120
+
121
+ def __call__(self, cls: type[BASEMODEL_T]) -> type[BASEMODEL_T]:
122
+ """
123
+ Decorate the type to mark it for explicit TypeScript generation.
124
+ """
125
+ setattr(cls, self.METADATA_KEY, True)
126
+ ExposeType._exposed_types.add(cls)
127
+ return cls
128
+
129
+ @staticmethod
130
+ def is_exposed_type(cls: type) -> bool:
131
+ """
132
+ Check if the type is marked for explicit exposure.
133
+ """
134
+ return getattr(cls, ExposeType.METADATA_KEY, False)
135
+
136
+ @staticmethod
137
+ def get_all_exposed_types() -> set[type]:
138
+ """
139
+ Get all types that have been marked for explicit exposure.
140
+ """
141
+ return ExposeType._exposed_types.copy()
@@ -36,6 +36,7 @@ from jararaca.presentation.websocket.websocket_interceptor import (
36
36
  WebSocketMessageWrapper,
37
37
  )
38
38
  from jararaca.tools.typescript.decorators import (
39
+ ExposeType,
39
40
  MutationEndpoint,
40
41
  QueryEndpoint,
41
42
  SplitInputOutput,
@@ -661,6 +662,9 @@ def write_microservice_to_typescript_interface(
661
662
  websocket_registries: set[RegisterWebSocketMessage] = set()
662
663
  mapped_types_set.add(WebSocketMessageWrapper)
663
664
 
665
+ # Add all explicitly exposed types
666
+ mapped_types_set.update(ExposeType.get_all_exposed_types())
667
+
664
668
  for controller in microservice.controllers:
665
669
  rest_controller = RestController.get_controller(controller)
666
670
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes