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
@@ -0,0 +1,109 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import logging
6
+ import typing
7
+ from contextlib import contextmanager
8
+ from typing import Any, Generator, Literal
9
+
10
+ from jararaca.observability.decorators import (
11
+ AttributeMap,
12
+ AttributeValue,
13
+ TracingContextProvider,
14
+ TracingSpan,
15
+ TracingSpanContext,
16
+ get_tracing_ctx_provider,
17
+ )
18
+
19
+
20
+ @contextmanager
21
+ def start_span(
22
+ name: str,
23
+ attributes: AttributeMap | None = None,
24
+ ) -> Generator[None, Any, None]:
25
+ if trace_context_provider := get_tracing_ctx_provider():
26
+ with trace_context_provider.start_span_context(
27
+ trace_name=name, context_attributes=attributes
28
+ ):
29
+ yield
30
+ else:
31
+ yield
32
+
33
+
34
+ def spawn_trace(
35
+ name: str,
36
+ attributes: AttributeMap | None = None,
37
+ ) -> typing.ContextManager[None]:
38
+ logging.warning(
39
+ "spawn_trace is deprecated, use start_span as context manager instead."
40
+ )
41
+ return start_span(name=name, attributes=attributes)
42
+
43
+
44
+ def add_event(
45
+ name: str,
46
+ attributes: AttributeMap | None = None,
47
+ ) -> None:
48
+
49
+ if trace_context_provider := get_tracing_ctx_provider():
50
+ trace_context_provider.add_event(
51
+ event_name=name,
52
+ event_attributes=attributes,
53
+ )
54
+
55
+
56
+ def set_span_status(status_code: Literal["OK", "ERROR", "UNSET"]) -> None:
57
+
58
+ if trace_context_provider := get_tracing_ctx_provider():
59
+ trace_context_provider.set_span_status(status_code=status_code)
60
+
61
+
62
+ def record_exception(
63
+ exception: Exception,
64
+ attributes: AttributeMap | None = None,
65
+ escaped: bool = False,
66
+ ) -> None:
67
+
68
+ if trace_context_provider := get_tracing_ctx_provider():
69
+ trace_context_provider.record_exception(
70
+ exception=exception,
71
+ attributes=attributes,
72
+ escaped=escaped,
73
+ )
74
+
75
+
76
+ def set_span_attribute(
77
+ key: str,
78
+ value: AttributeValue,
79
+ ) -> None:
80
+
81
+ if trace_context_provider := get_tracing_ctx_provider():
82
+ trace_context_provider.set_span_attribute(
83
+ key=key,
84
+ value=value,
85
+ )
86
+
87
+
88
+ def get_tracing_provider() -> TracingContextProvider | None:
89
+ return get_tracing_ctx_provider()
90
+
91
+
92
+ def get_current_span_context() -> TracingSpanContext | None:
93
+
94
+ if trace_context_provider := get_tracing_ctx_provider():
95
+ return trace_context_provider.get_current_span_context()
96
+ return None
97
+
98
+
99
+ def get_current_span() -> TracingSpan | None:
100
+
101
+ if trace_context_provider := get_tracing_ctx_provider():
102
+ return trace_context_provider.get_current_span()
103
+ return None
104
+
105
+
106
+ def add_span_link(span_context: TracingSpanContext) -> None:
107
+
108
+ if trace_context_provider := get_tracing_ctx_provider():
109
+ trace_context_provider.add_link(span_context=span_context)
@@ -1,10 +1,14 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from contextlib import asynccontextmanager
2
6
  from typing import AsyncContextManager, AsyncGenerator, Protocol
3
7
 
4
8
  from jararaca.microservice import (
5
- AppContext,
6
9
  AppInterceptor,
7
10
  AppInterceptorWithLifecycle,
11
+ AppTransactionContext,
8
12
  Container,
9
13
  Microservice,
10
14
  )
@@ -32,7 +36,9 @@ class ObservabilityInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
32
36
  self.observability_provider = observability_provider
33
37
 
34
38
  @asynccontextmanager
35
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
39
+ async def intercept(
40
+ self, app_context: AppTransactionContext
41
+ ) -> AsyncGenerator[None, None]:
36
42
 
37
43
  async with self.observability_provider.tracing_provider.root_setup(app_context):
38
44
 
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -1,6 +1,10 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  import logging
2
6
  from contextlib import asynccontextmanager, contextmanager
3
- from typing import AsyncGenerator, Generator, Protocol
7
+ from typing import Any, AsyncGenerator, Generator, Literal, Protocol
4
8
 
5
9
  from opentelemetry import metrics, trace
6
10
  from opentelemetry._logs import set_logger_provider
@@ -23,51 +27,194 @@ from opentelemetry.sdk.trace import TracerProvider
23
27
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
24
28
  from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
25
29
 
26
- from jararaca.microservice import AppContext, Container, Microservice
30
+ from jararaca.messagebus.implicit_headers import (
31
+ ImplicitHeaders,
32
+ provide_implicit_headers,
33
+ use_implicit_headers,
34
+ )
35
+ from jararaca.microservice import (
36
+ AppTransactionContext,
37
+ Container,
38
+ Microservice,
39
+ use_app_transaction_context,
40
+ )
41
+ from jararaca.observability.constants import TRACEPARENT_KEY
27
42
  from jararaca.observability.decorators import (
43
+ AttributeMap,
44
+ AttributeValue,
28
45
  TracingContextProvider,
29
46
  TracingContextProviderFactory,
30
- get_tracing_ctx_provider,
47
+ TracingSpan,
48
+ TracingSpanContext,
31
49
  )
32
50
  from jararaca.observability.interceptor import ObservabilityProvider
33
51
 
34
52
  tracer: trace.Tracer = trace.get_tracer(__name__)
35
53
 
36
54
 
55
+ def extract_context_attributes(ctx: AppTransactionContext) -> dict[str, Any]:
56
+ tx_data = ctx.transaction_data
57
+ extra_attributes: dict[str, Any] = {}
58
+
59
+ if tx_data.context_type == "http":
60
+ extra_attributes = {
61
+ "http.method": tx_data.request.method,
62
+ "http.url": str(tx_data.request.url),
63
+ "http.path": tx_data.request.url.path,
64
+ "http.route.path": tx_data.request.scope["route"].path,
65
+ "http.route.endpoint.name": tx_data.request["route"].endpoint.__qualname__,
66
+ "http.query": tx_data.request.url.query,
67
+ **{
68
+ f"http.request.path_param.{k}": v
69
+ for k, v in tx_data.request.path_params.items()
70
+ },
71
+ **{
72
+ f"http.request.query_param.{k}": v
73
+ for k, v in tx_data.request.query_params.items()
74
+ },
75
+ **{
76
+ f"http.request.header.{k}": v
77
+ for k, v in tx_data.request.headers.items()
78
+ },
79
+ "http.request.client.host": (
80
+ tx_data.request.client.host if tx_data.request.client else ""
81
+ ),
82
+ }
83
+ elif tx_data.context_type == "message_bus":
84
+ extra_attributes = {
85
+ "bus.message.name": tx_data.message_type.__qualname__,
86
+ "bus.message.module": tx_data.message_type.__module__,
87
+ "bus.message.category": tx_data.message_type.MESSAGE_CATEGORY,
88
+ "bus.message.type": tx_data.message_type.MESSAGE_TYPE,
89
+ "bus.message.topic": tx_data.message_type.MESSAGE_TOPIC,
90
+ }
91
+ elif tx_data.context_type == "websocket":
92
+ extra_attributes = {
93
+ "ws.url": str(tx_data.websocket.url),
94
+ }
95
+ elif tx_data.context_type == "scheduler":
96
+ extra_attributes = {
97
+ "sched.task_name": tx_data.task_name,
98
+ "sched.scheduled_to": tx_data.scheduled_to.isoformat(),
99
+ "sched.cron_expression": tx_data.cron_expression,
100
+ "sched.triggered_at": tx_data.triggered_at.isoformat(),
101
+ }
102
+ return {
103
+ "app.context_type": tx_data.context_type,
104
+ "controller_member_reflect.rest_controller.class_name": ctx.controller_member_reflect.controller_reflect.controller_class.__qualname__,
105
+ "controller_member_reflect.rest_controller.module": ctx.controller_member_reflect.controller_reflect.controller_class.__module__,
106
+ "controller_member_reflect.member_function.name": ctx.controller_member_reflect.member_function.__qualname__,
107
+ "controller_member_reflect.member_function.module": ctx.controller_member_reflect.member_function.__module__,
108
+ **extra_attributes,
109
+ }
110
+
111
+
112
+ class OtelTracingSpan(TracingSpan):
113
+
114
+ def __init__(self, span: trace.Span) -> None:
115
+ self.span = span
116
+
117
+
118
+ class OtelTracingSpanContext(TracingSpanContext):
119
+
120
+ def __init__(self, span_context: trace.SpanContext) -> None:
121
+ self.span_context = span_context
122
+
123
+
37
124
  class OtelTracingContextProvider(TracingContextProvider):
38
125
 
39
- def __init__(self, app_context: AppContext) -> None:
126
+ def __init__(self, app_context: AppTransactionContext) -> None:
40
127
  self.app_context = app_context
41
128
 
42
129
  @contextmanager
43
- def __call__(
130
+ def start_span_context(
44
131
  self,
45
132
  trace_name: str,
46
- context_attributes: dict[str, str],
133
+ context_attributes: AttributeMap | None,
47
134
  ) -> Generator[None, None, None]:
48
135
 
49
136
  with tracer.start_as_current_span(trace_name, attributes=context_attributes):
50
137
  yield
51
138
 
139
+ def add_event(
140
+ self, event_name: str, event_attributes: AttributeMap | None = None
141
+ ) -> None:
142
+ trace.get_current_span().add_event(name=event_name, attributes=event_attributes)
143
+
144
+ def set_span_status(self, status_code: Literal["OK", "ERROR", "UNSET"]) -> None:
145
+ span = trace.get_current_span()
146
+ if status_code == "OK":
147
+ span.set_status(trace.Status(trace.StatusCode.OK))
148
+ elif status_code == "ERROR":
149
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
150
+ else:
151
+ span.set_status(trace.Status(trace.StatusCode.UNSET))
152
+
153
+ def record_exception(
154
+ self,
155
+ exception: Exception,
156
+ attributes: AttributeMap | None = None,
157
+ escaped: bool = False,
158
+ ) -> None:
159
+ span = trace.get_current_span()
160
+ span.record_exception(exception, attributes=attributes, escaped=escaped)
161
+
162
+ def set_span_attribute(self, key: str, value: AttributeValue) -> None:
163
+ span = trace.get_current_span()
164
+ span.set_attribute(key, value)
165
+
166
+ def update_span_name(self, new_name: str) -> None:
167
+ span = trace.get_current_span()
168
+
169
+ span.update_name(new_name)
170
+
171
+ def add_link(self, span_context: TracingSpanContext) -> None:
172
+ if not isinstance(span_context, OtelTracingSpanContext):
173
+ return
174
+ span = trace.get_current_span()
175
+ span.add_link(span_context.span_context)
176
+
177
+ def get_current_span(self) -> TracingSpan | None:
178
+ return OtelTracingSpan(trace.get_current_span())
179
+
180
+ def get_current_span_context(self) -> TracingSpanContext | None:
181
+ return OtelTracingSpanContext(trace.get_current_span().get_span_context())
182
+
52
183
 
53
184
  class OtelTracingContextProviderFactory(TracingContextProviderFactory):
54
185
 
55
- def provide_provider(self, app_context: AppContext) -> TracingContextProvider:
186
+ def provide_provider(
187
+ self, app_context: AppTransactionContext
188
+ ) -> TracingContextProvider:
56
189
  return OtelTracingContextProvider(app_context)
57
190
 
58
191
  @asynccontextmanager
59
- async def root_setup(self, app_context: AppContext) -> AsyncGenerator[None, None]:
192
+ async def root_setup(
193
+ self, app_tx_ctx: AppTransactionContext
194
+ ) -> AsyncGenerator[None, None]:
60
195
 
61
196
  title: str = "Unmapped App Context Execution"
62
- headers = {}
197
+ headers: dict[str, Any] = {}
198
+ tx_data = app_tx_ctx.transaction_data
199
+ extra_attributes = extract_context_attributes(app_tx_ctx)
63
200
 
64
- if app_context.context_type == "http":
201
+ if tx_data.context_type == "http":
202
+ headers = dict(tx_data.request.headers)
203
+ title = f"HTTP {tx_data.request.method} {tx_data.request.url}"
204
+ extra_attributes["http.request.body"] = (await tx_data.request.body())[
205
+ :5000
206
+ ].decode(errors="ignore")
65
207
 
66
- headers = dict(app_context.request.headers)
67
- title = f"HTTP {app_context.request.method} {app_context.request.url}"
208
+ elif tx_data.context_type == "message_bus":
209
+ title = f"Message Bus {tx_data.topic}"
210
+ headers = use_implicit_headers() or {}
68
211
 
69
- elif app_context.context_type == "message_bus":
70
- title = f"Message Bus {app_context.topic}"
212
+ elif tx_data.context_type == "websocket":
213
+ headers = dict(tx_data.websocket.headers)
214
+ title = f"WebSocket {tx_data.websocket.url}"
215
+
216
+ elif tx_data.context_type == "scheduler":
217
+ title = f"Scheduler Task {tx_data.task_name}"
71
218
 
72
219
  carrier = {
73
220
  key: value
@@ -86,8 +233,28 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
86
233
 
87
234
  ctx2 = W3CBaggagePropagator().extract(b2, context=ctx)
88
235
 
89
- with tracer.start_as_current_span(name=title, context=ctx2):
90
- yield
236
+ with tracer.start_as_current_span(
237
+ name=title,
238
+ context=ctx2,
239
+ attributes={
240
+ **extra_attributes,
241
+ },
242
+ ) as root_span:
243
+ cx = root_span.get_span_context()
244
+ span_traceparent_id = hex(cx.trace_id)[2:].rjust(32, "0")
245
+ if app_tx_ctx.transaction_data.context_type == "http":
246
+ app_tx_ctx.transaction_data.request.scope[TRACEPARENT_KEY] = (
247
+ span_traceparent_id
248
+ )
249
+ elif app_tx_ctx.transaction_data.context_type == "websocket":
250
+ app_tx_ctx.transaction_data.websocket.scope[TRACEPARENT_KEY] = (
251
+ span_traceparent_id
252
+ )
253
+ tracing_headers: ImplicitHeaders = {}
254
+ TraceContextTextMapPropagator().inject(tracing_headers)
255
+ W3CBaggagePropagator().inject(tracing_headers)
256
+ with provide_implicit_headers(tracing_headers):
257
+ yield
91
258
 
92
259
 
93
260
  class LoggerHandlerCallback(Protocol):
@@ -95,6 +262,34 @@ class LoggerHandlerCallback(Protocol):
95
262
  def __call__(self, logger_handler: logging.Handler) -> None: ...
96
263
 
97
264
 
265
+ class CustomLoggingHandler(LoggingHandler):
266
+
267
+ def _translate(self, record: logging.LogRecord) -> dict[str, Any]:
268
+ try:
269
+ ctx = use_app_transaction_context()
270
+ data = super()._translate(record)
271
+ extra_attributes = extract_context_attributes(ctx)
272
+
273
+ current_span = trace.get_current_span()
274
+
275
+ data["attributes"] = {
276
+ **data.get("attributes", {}),
277
+ **extra_attributes,
278
+ **(
279
+ {
280
+ "span_name": current_span.name,
281
+ }
282
+ if hasattr(current_span, "name")
283
+ and current_span.is_recording() is False
284
+ else {}
285
+ ),
286
+ }
287
+
288
+ return data
289
+ except LookupError:
290
+ return super()._translate(record)
291
+
292
+
98
293
  class OtelObservabilityProvider(ObservabilityProvider):
99
294
 
100
295
  def __init__(
@@ -139,11 +334,11 @@ class OtelObservabilityProvider(ObservabilityProvider):
139
334
  BatchLogRecordProcessor(self.logs_exporter)
140
335
  )
141
336
 
142
- logging_handler = LoggingHandler(
337
+ logging_handler = CustomLoggingHandler(
143
338
  level=logging.DEBUG, logger_provider=logger_provider
144
339
  )
145
340
 
146
- logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
341
+ # logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
147
342
 
148
343
  self.logging_handler_callback(logging_handler)
149
344
 
@@ -1,6 +1,11 @@
1
- from typing import Any, Self, Type, TypeVar
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from typing import Any, Callable, Protocol, Self, Type, TypeVar
2
6
 
3
7
  from pydantic import BaseModel
8
+ from sqlalchemy.ext.asyncio import AsyncAttrs
4
9
  from sqlalchemy.orm import DeclarativeBase
5
10
 
6
11
  IDENTIFIABLE_SCHEMA_T = TypeVar("IDENTIFIABLE_SCHEMA_T")
@@ -20,14 +25,46 @@ def recursive_get_dict(obj: Any) -> Any:
20
25
  return obj
21
26
 
22
27
 
23
- class BaseEntity(DeclarativeBase):
28
+ RESULT_T = TypeVar("RESULT_T", covariant=True)
29
+ ENTITY_T_CONTRA = TypeVar("ENTITY_T_CONTRA", bound="BaseEntity", contravariant=True)
30
+
31
+
32
+ class EntityParserType(Protocol[ENTITY_T_CONTRA, RESULT_T]):
33
+
34
+ @classmethod
35
+ def parse_entity(cls, model: ENTITY_T_CONTRA) -> "RESULT_T": ...
36
+
37
+
38
+ EntityParserFunc = Callable[[ENTITY_T_CONTRA], RESULT_T]
39
+
40
+ BASED_BASE_ENTITY_T = TypeVar("BASED_BASE_ENTITY_T", bound="BaseEntity")
41
+
42
+
43
+ class BaseEntity(AsyncAttrs, DeclarativeBase):
24
44
 
25
45
  @classmethod
26
46
  def from_basemodel(cls, mutation: T_BASEMODEL) -> "Self":
27
47
  intersection = set(cls.__annotations__.keys()) & set(
28
- mutation.model_fields.keys()
48
+ mutation.__class__.model_fields.keys()
29
49
  )
30
50
  return cls(**{k: getattr(mutation, k) for k in intersection})
31
51
 
32
52
  def to_basemodel(self, model: Type[T_BASEMODEL]) -> T_BASEMODEL:
33
53
  return model.model_validate(recursive_get_dict(self))
54
+
55
+ def parse_entity_with_func(
56
+ self, model_cls: EntityParserFunc["Self", RESULT_T]
57
+ ) -> RESULT_T:
58
+ return model_cls(self)
59
+
60
+ def parse_entity_with_type(
61
+ self: BASED_BASE_ENTITY_T,
62
+ model_cls: Type[EntityParserType[BASED_BASE_ENTITY_T, RESULT_T]],
63
+ ) -> RESULT_T:
64
+ return model_cls.parse_entity(self)
65
+
66
+ def __rshift__(self, model: EntityParserFunc["Self", RESULT_T]) -> RESULT_T:
67
+ return self.parse_entity_with_func(model)
68
+
69
+ def __and__(self, model: Type[EntityParserType["Self", RESULT_T]]) -> RESULT_T:
70
+ return self.parse_entity_with_type(model)
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from .base import BaseEntity
2
6
 
3
7
  __all__ = [
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later