hexcore 3.0.0__tar.gz → 4.0.0__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.
- {hexcore-3.0.0/hexcore.egg-info → hexcore-4.0.0}/PKG-INFO +1 -1
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/in_memory_buses.py +7 -3
- hexcore-4.0.0/hexcore/application/dtos/cursor.py +110 -0
- hexcore-4.0.0/hexcore/application/dtos/errors.py +30 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/config.py +5 -0
- hexcore-4.0.0/hexcore/infrastructure/api/__init__.py +76 -0
- hexcore-4.0.0/hexcore/infrastructure/api/app.py +131 -0
- hexcore-4.0.0/hexcore/infrastructure/api/cqrs.py +195 -0
- hexcore-4.0.0/hexcore/infrastructure/api/exception_handlers.py +131 -0
- hexcore-4.0.0/hexcore/infrastructure/api/health.py +255 -0
- hexcore-4.0.0/hexcore/infrastructure/api/lifespan.py +340 -0
- hexcore-4.0.0/hexcore/infrastructure/api/middlewares.py +142 -0
- hexcore-4.0.0/hexcore/infrastructure/api/rate_limit.py +165 -0
- hexcore-4.0.0/hexcore/infrastructure/api/routing.py +87 -0
- hexcore-4.0.0/hexcore/infrastructure/api/streaming.py +293 -0
- hexcore-4.0.0/hexcore/infrastructure/api/utils.py +346 -0
- hexcore-4.0.0/hexcore/infrastructure/cqrs/__init__.py +29 -0
- hexcore-4.0.0/hexcore/infrastructure/cqrs/cron_sql.py +340 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/implementations.py +24 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +3 -1
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +140 -1
- hexcore-4.0.0/hexcore/infrastructure/workers/__init__.py +19 -0
- hexcore-4.0.0/hexcore/infrastructure/workers/runner.py +349 -0
- hexcore-4.0.0/hexcore/testing/__init__.py +27 -0
- hexcore-4.0.0/hexcore/testing/fakes.py +175 -0
- hexcore-4.0.0/hexcore/testing/fixtures.py +105 -0
- hexcore-4.0.0/hexcore/testing/helpers.py +147 -0
- {hexcore-3.0.0 → hexcore-4.0.0/hexcore.egg-info}/PKG-INFO +1 -1
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore.egg-info/SOURCES.txt +29 -1
- {hexcore-3.0.0 → hexcore-4.0.0}/pyproject.toml +4 -1
- hexcore-4.0.0/tests/test_api_app_and_lifespan.py +428 -0
- hexcore-4.0.0/tests/test_api_cqrs_providers.py +197 -0
- hexcore-4.0.0/tests/test_api_exception_handlers.py +151 -0
- hexcore-4.0.0/tests/test_api_health.py +220 -0
- hexcore-4.0.0/tests/test_api_middlewares_and_routing.py +308 -0
- hexcore-4.0.0/tests/test_api_rate_limit.py +228 -0
- hexcore-4.0.0/tests/test_api_streaming.py +367 -0
- hexcore-4.0.0/tests/test_cron_sql.py +351 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_optional_dependencies.py +7 -0
- hexcore-4.0.0/tests/test_query_cursor_and_endpoint.py +504 -0
- hexcore-4.0.0/tests/test_testing_utilities.py +330 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_use_cases_query.py +3 -1
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_worker_bus_integration.py +38 -0
- hexcore-4.0.0/tests/test_worker_runner.py +363 -0
- hexcore-3.0.0/hexcore/infrastructure/api/__init__.py +0 -3
- hexcore-3.0.0/hexcore/infrastructure/api/utils.py +0 -195
- hexcore-3.0.0/hexcore/infrastructure/cqrs/__init__.py +0 -24
- hexcore-3.0.0/hexcore/infrastructure/workers/__init__.py +0 -1
- {hexcore-3.0.0 → hexcore-4.0.0}/LICENSE +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/MANIFEST.in +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/README.md +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/__main__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/adapters.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/config.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/factory.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/pipeline.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/registry.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/cqrs/scheduler.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/dtos/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/dtos/query.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/use_cases/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/application/use_cases/query.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/auth/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/auth/permissions.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/auth/value_objects.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/base.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/buses.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/commands.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/context.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/cron.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/decorators.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/exceptions.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/handlers.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/middleware.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/queries.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/resolution.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/serializer.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/cqrs/task_queues.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/events.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/services.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/domain/uow.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cli.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/middlewares.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/postgres_bus.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/postgres_lock.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/redis_bus.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/cqrs/redis_lock.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/events/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/events/events_backends/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/events/events_backends/memory.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/base.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/repositories/utils.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/task_queues/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/task_queues/celery_adapter.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/task_queues/procrastinate_adapter.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/uow/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/uow/scopes.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/workers/consumer.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/py.typed +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore/types.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore.egg-info/requires.txt +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/scripts/__init__.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/scripts/main.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/setup.cfg +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/conftest.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_basic.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_beanie_query_utils.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_config_loading.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_consumer_optional_event_bus.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_cqrs.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_cqrs_factory.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_cqrs_middlewares.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_domain_service_query.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_dotted_resolution.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_handler_registry.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_infrastructure_query_path.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_lock_error_policy.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_postgres_bus.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_postgres_lock.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_query_field_validation.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_rabbitmq_bus.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_redis_bus.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_redis_lock.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_repositories_utils.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_scheduler.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_scheduler_catchup.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_smart_routing.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_sql_session_layer.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_task_queues_adapters.py +0 -0
- {hexcore-3.0.0 → hexcore-4.0.0}/tests/test_uow_session_regression.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexcore
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Núcleo reutilizable para proyectos Python con arquitectura hexagonal y event handling. Provee abstracciones, utilidades y contratos para DDD, eventos y desacoplamiento de infraestructura.
|
|
5
5
|
Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -56,7 +56,10 @@ class InMemoryCommandBus(ICommandBus):
|
|
|
56
56
|
is_background = getattr(cmd_type, "__cqrs_background__", False)
|
|
57
57
|
|
|
58
58
|
if is_background and not is_worker_execution():
|
|
59
|
-
|
|
59
|
+
# `is None` y no truthiness: un enqueuer que implemente `__len__` o
|
|
60
|
+
# `__bool__` (un doble de test que cuenta lo encolado, p. ej.) es falsy
|
|
61
|
+
# cuando está vacío, y con `not self._enqueuer` se descartaría.
|
|
62
|
+
if self._enqueuer is None or self._serializer is None:
|
|
60
63
|
raise RuntimeError(
|
|
61
64
|
f"El comando '{cmd_type.__name__}' requiere ejecución en background, "
|
|
62
65
|
"pero el InMemoryCommandBus no tiene configurado un 'enqueuer' o 'serializer'."
|
|
@@ -150,8 +153,9 @@ class InMemoryEventBus(IEventBus):
|
|
|
150
153
|
)
|
|
151
154
|
|
|
152
155
|
if is_background:
|
|
153
|
-
# Enrutamiento hacia background
|
|
154
|
-
|
|
156
|
+
# Enrutamiento hacia background. `is None` por el mismo motivo que en
|
|
157
|
+
# `InMemoryCommandBus.dispatch`.
|
|
158
|
+
if self._enqueuer is None or self._serializer is None:
|
|
155
159
|
raise RuntimeError(
|
|
156
160
|
f"El handler de eventos '{event_handler.__name__}' requiere ejecución en background, "
|
|
157
161
|
"pero el InMemoryEventBus no tiene configurado un 'enqueuer' o 'serializer'."
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Paginación por cursor.
|
|
3
|
+
|
|
4
|
+
`limit`/`offset` degrada en tablas grandes: un `OFFSET 100000` obliga a la base a
|
|
5
|
+
escanear y descartar 100.000 filas antes de devolver la primera. Para los listados
|
|
6
|
+
grandes (tickets, líneas, movimientos) hace falta la variante por cursor, que salta
|
|
7
|
+
directo con un `WHERE (sort_key, id) > (...)`.
|
|
8
|
+
|
|
9
|
+
Se **añade**, no sustituye a `QueryResponseDTO`.
|
|
10
|
+
|
|
11
|
+
El cursor es opaco a propósito: codifica `(sort_key, id)` en base64url. Si fuera legible,
|
|
12
|
+
los clientes empezarían a construirlo a mano y quedaría congelado como API pública.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import binascii
|
|
18
|
+
import json
|
|
19
|
+
import typing as t
|
|
20
|
+
|
|
21
|
+
from pydantic import Field
|
|
22
|
+
|
|
23
|
+
from .base import DTO
|
|
24
|
+
from .query import FilterConditionDTO, SortDirection
|
|
25
|
+
|
|
26
|
+
T = t.TypeVar("T")
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CursorPageDTO",
|
|
30
|
+
"CursorRequestDTO",
|
|
31
|
+
"encode_cursor",
|
|
32
|
+
"decode_cursor",
|
|
33
|
+
"InvalidCursorError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidCursorError(ValueError):
|
|
38
|
+
"""El cursor recibido no se puede decodificar."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CursorPageDTO(DTO, t.Generic[T]):
|
|
42
|
+
"""
|
|
43
|
+
Una página por cursor.
|
|
44
|
+
|
|
45
|
+
`next_cursor` es None cuando no hay más resultados, que es la única señal de fin que
|
|
46
|
+
necesita el cliente: no hay `total`, porque contar es justamente lo que esta
|
|
47
|
+
paginación evita.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
items: list[T] = Field(default_factory=list)
|
|
51
|
+
next_cursor: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CursorRequestDTO(DTO):
|
|
55
|
+
"""Petición de una página por cursor."""
|
|
56
|
+
|
|
57
|
+
limit: int = Field(default=50, ge=1, le=500)
|
|
58
|
+
cursor: str | None = None
|
|
59
|
+
sort_field: str = "created_at"
|
|
60
|
+
direction: SortDirection = SortDirection.DESC
|
|
61
|
+
search: str | None = None
|
|
62
|
+
search_fields: list[str] = Field(default_factory=list)
|
|
63
|
+
filters: list[FilterConditionDTO] = Field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def encode_cursor(sort_value: t.Any, entity_id: t.Any) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Codifica la posición de la última fila de la página.
|
|
69
|
+
|
|
70
|
+
Se incluye el `id` además del campo de orden para desempatar: con sólo el `sort_key`,
|
|
71
|
+
varias filas con el mismo `created_at` se saltarían o se repetirían.
|
|
72
|
+
"""
|
|
73
|
+
payload = json.dumps(
|
|
74
|
+
{"k": _jsonable(sort_value), "i": _jsonable(entity_id)},
|
|
75
|
+
separators=(",", ":"),
|
|
76
|
+
)
|
|
77
|
+
return base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def decode_cursor(cursor: str) -> tuple[t.Any, t.Any]:
|
|
81
|
+
"""
|
|
82
|
+
Decodifica un cursor a `(sort_value, entity_id)`.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
InvalidCursorError: Si el cursor está corrupto o no tiene la forma esperada. Es
|
|
86
|
+
un `ValueError`, así que los handlers de F5 lo traducen a 422.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
padding = "=" * (-len(cursor) % 4)
|
|
90
|
+
payload = base64.urlsafe_b64decode(cursor + padding).decode()
|
|
91
|
+
data = json.loads(payload)
|
|
92
|
+
except (binascii.Error, UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
|
93
|
+
raise InvalidCursorError(f"Cursor inválido: {cursor!r}") from exc
|
|
94
|
+
|
|
95
|
+
if not isinstance(data, dict) or "k" not in data or "i" not in data:
|
|
96
|
+
raise InvalidCursorError(f"Cursor inválido: {cursor!r}")
|
|
97
|
+
|
|
98
|
+
return data["k"], data["i"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _jsonable(value: t.Any) -> t.Any:
|
|
102
|
+
"""Normaliza a algo serializable en JSON conservando el orden lexicográfico."""
|
|
103
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
104
|
+
return value
|
|
105
|
+
isoformat = getattr(value, "isoformat", None)
|
|
106
|
+
if callable(isoformat):
|
|
107
|
+
# ISO-8601 ordena lexicográficamente igual que cronológicamente, que es lo que
|
|
108
|
+
# necesita la comparación del WHERE.
|
|
109
|
+
return isoformat()
|
|
110
|
+
return str(value)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Errores de validación de campos de query, con datos estructurados.
|
|
3
|
+
|
|
4
|
+
`ValueError("Campo de orden no soportado: nope")` sólo se puede mostrar; el cliente no
|
|
5
|
+
puede señalar el input concreto ni ofrecer las alternativas. Esta subclase lleva `field` y
|
|
6
|
+
`allowed`, y sigue siendo un `ValueError`, así que todo el código que ya lo capturaba
|
|
7
|
+
—incluidos los handlers de F5— sigue funcionando.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typing as t
|
|
12
|
+
|
|
13
|
+
__all__ = ["UnsupportedQueryFieldError"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnsupportedQueryFieldError(ValueError):
|
|
17
|
+
"""Un campo de filtro, orden o búsqueda que el modelo no expone."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
field: str,
|
|
22
|
+
context: str,
|
|
23
|
+
allowed: t.Iterable[str] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.field = field
|
|
26
|
+
self.context = context
|
|
27
|
+
self.allowed: list[str] = sorted(allowed) if allowed is not None else []
|
|
28
|
+
# Se conserva el mensaje anterior palabra por palabra: hay tests y logs que lo
|
|
29
|
+
# buscan, y cambiarlo no aporta nada.
|
|
30
|
+
super().__init__(f"Campo de {context} no soportado: {field}")
|
|
@@ -21,6 +21,11 @@ class ServerConfig(BaseModel):
|
|
|
21
21
|
port: int = 8000
|
|
22
22
|
debug: bool = True
|
|
23
23
|
|
|
24
|
+
# Identidad de la app. La usa `create_app()` para el título y la versión de FastAPI,
|
|
25
|
+
# de modo que el camino feliz no necesite pasarlos.
|
|
26
|
+
app_title: str = "HexCore API"
|
|
27
|
+
app_version: str = "0.1.0"
|
|
28
|
+
|
|
24
29
|
# DB CONFIG
|
|
25
30
|
sql_database_url: str = "sqlite:///./db.sqlite3"
|
|
26
31
|
async_sql_database_url: str = "sqlite+aiosqlite:///./db.sqlite3"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from .cqrs import (
|
|
2
|
+
CQRSContainer,
|
|
3
|
+
configure_cqrs,
|
|
4
|
+
get_cqrs_container,
|
|
5
|
+
provide_command_bus,
|
|
6
|
+
provide_event_bus,
|
|
7
|
+
provide_query_bus,
|
|
8
|
+
provide_registry,
|
|
9
|
+
reset_cqrs,
|
|
10
|
+
)
|
|
11
|
+
from .exception_handlers import (
|
|
12
|
+
DEFAULT_EXCEPTION_STATUS_MAP,
|
|
13
|
+
register_exception_handlers,
|
|
14
|
+
)
|
|
15
|
+
from .health import (
|
|
16
|
+
DependencyReport,
|
|
17
|
+
HealthReport,
|
|
18
|
+
Probe,
|
|
19
|
+
check_health,
|
|
20
|
+
default_probes,
|
|
21
|
+
register_health_routes,
|
|
22
|
+
)
|
|
23
|
+
from .middlewares import (
|
|
24
|
+
REQUEST_ID,
|
|
25
|
+
RequestIDLogFilter,
|
|
26
|
+
RequestIDMiddleware,
|
|
27
|
+
TimingMiddleware,
|
|
28
|
+
get_request_id,
|
|
29
|
+
install_request_id_logging,
|
|
30
|
+
)
|
|
31
|
+
from .rate_limit import client_ip_key, rate_limit
|
|
32
|
+
from .routing import build_root_router, mount_routers
|
|
33
|
+
from .streaming import connection_slot, format_sse_event, sse_stream, ws_heartbeat
|
|
34
|
+
from .utils import build_query_endpoint, register_query_endpoint
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"build_query_endpoint",
|
|
38
|
+
"register_query_endpoint",
|
|
39
|
+
# Middlewares (F11)
|
|
40
|
+
"REQUEST_ID",
|
|
41
|
+
"RequestIDMiddleware",
|
|
42
|
+
"TimingMiddleware",
|
|
43
|
+
"RequestIDLogFilter",
|
|
44
|
+
"get_request_id",
|
|
45
|
+
"install_request_id_logging",
|
|
46
|
+
# Routing (F13)
|
|
47
|
+
"build_root_router",
|
|
48
|
+
"mount_routers",
|
|
49
|
+
# Handlers de excepción (F5)
|
|
50
|
+
"DEFAULT_EXCEPTION_STATUS_MAP",
|
|
51
|
+
"register_exception_handlers",
|
|
52
|
+
# Providers CQRS (F6)
|
|
53
|
+
"CQRSContainer",
|
|
54
|
+
"configure_cqrs",
|
|
55
|
+
"get_cqrs_container",
|
|
56
|
+
"reset_cqrs",
|
|
57
|
+
"provide_command_bus",
|
|
58
|
+
"provide_query_bus",
|
|
59
|
+
"provide_event_bus",
|
|
60
|
+
"provide_registry",
|
|
61
|
+
# Rate limiting (F12)
|
|
62
|
+
"rate_limit",
|
|
63
|
+
"client_ip_key",
|
|
64
|
+
# Streaming (F14)
|
|
65
|
+
"sse_stream",
|
|
66
|
+
"format_sse_event",
|
|
67
|
+
"ws_heartbeat",
|
|
68
|
+
"connection_slot",
|
|
69
|
+
# Health checks (F16)
|
|
70
|
+
"check_health",
|
|
71
|
+
"register_health_routes",
|
|
72
|
+
"default_probes",
|
|
73
|
+
"HealthReport",
|
|
74
|
+
"DependencyReport",
|
|
75
|
+
"Probe",
|
|
76
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory de la aplicación FastAPI.
|
|
3
|
+
|
|
4
|
+
Cada app repite CORS, middleware de request-id, handlers de excepción y health checks.
|
|
5
|
+
Aplicando las reglas de simplicidad del plan: el camino feliz es cero configuración
|
|
6
|
+
(`create_app()` a secas produce una app usable), y los interruptores van agrupados en un
|
|
7
|
+
solo objeto en vez de ocho keywords.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typing as t
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI
|
|
14
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from .exception_handlers import register_exception_handlers
|
|
18
|
+
from .health import Probe, register_health_routes
|
|
19
|
+
from .middlewares import RequestIDMiddleware, TimingMiddleware
|
|
20
|
+
from .routing import MountableRouter, mount_routers
|
|
21
|
+
|
|
22
|
+
__all__ = ["AppFeatures", "create_app"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AppFeatures(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Qué cablea `create_app()`. Todo activado por defecto.
|
|
28
|
+
|
|
29
|
+
Desactivar es explícito y localizado: ``create_app(features=AppFeatures(cors=False))``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
cors: bool = True
|
|
33
|
+
"""CORS con `config.allow_origins` / `allow_methods` / `allow_headers`."""
|
|
34
|
+
|
|
35
|
+
request_id: bool = True
|
|
36
|
+
"""Middleware `X-Request-ID` (F11)."""
|
|
37
|
+
|
|
38
|
+
timing: bool = True
|
|
39
|
+
"""Middleware `X-Response-Time-ms` (F11)."""
|
|
40
|
+
|
|
41
|
+
exception_handlers: bool = True
|
|
42
|
+
"""Mapeo de excepciones de dominio a HTTP (F5)."""
|
|
43
|
+
|
|
44
|
+
health: bool = True
|
|
45
|
+
"""Rutas `/health` y `/health/ready` (F16)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_app(
|
|
49
|
+
lifespan: t.Any | None = None,
|
|
50
|
+
*,
|
|
51
|
+
features: AppFeatures | None = None,
|
|
52
|
+
routers: t.Sequence[MountableRouter] | None = None,
|
|
53
|
+
health_probes: t.Sequence[Probe] | None = None,
|
|
54
|
+
exception_mapping: dict[type[Exception], int] | None = None,
|
|
55
|
+
**fastapi_kwargs: t.Any,
|
|
56
|
+
) -> FastAPI:
|
|
57
|
+
"""
|
|
58
|
+
Construye una `FastAPI` con lo que toda app necesita ya cableado.
|
|
59
|
+
|
|
60
|
+
`create_app()` sin argumentos ya da una app usable: `title` y `version` salen de
|
|
61
|
+
`ServerConfig`, CORS de `config.allow_origins`, y `/health` existe.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
lifespan: El lifespan. Típicamente de `build_lifespan(...)` (F4).
|
|
65
|
+
features: Qué cablear. Ver `AppFeatures`.
|
|
66
|
+
routers: Routers a montar. Acepta `APIRouter` o `(APIRouter, kwargs)` (F13).
|
|
67
|
+
health_probes: Sondas para `/health/ready`. Por defecto, las deducidas de la
|
|
68
|
+
configuración (F16).
|
|
69
|
+
exception_mapping: Excepciones extra a mapear, fusionadas con el default (F5).
|
|
70
|
+
**fastapi_kwargs: Se pasan tal cual a `FastAPI` (`title`, `version`,
|
|
71
|
+
`docs_url`, `openapi_tags`…). Lo que pases gana sobre los defaults derivados
|
|
72
|
+
de la configuración.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
La aplicación, lista para servir.
|
|
76
|
+
"""
|
|
77
|
+
resolved_features = features or AppFeatures()
|
|
78
|
+
config = _config()
|
|
79
|
+
|
|
80
|
+
app = FastAPI(lifespan=lifespan, **{**_fastapi_defaults(config), **fastapi_kwargs})
|
|
81
|
+
|
|
82
|
+
# Los middlewares de Starlette se ejecutan en orden inverso al de registro, así que
|
|
83
|
+
# el request-id se añade último para que envuelva a todo lo demás y su ContextVar
|
|
84
|
+
# esté disponible en el resto de la pila.
|
|
85
|
+
if resolved_features.cors:
|
|
86
|
+
app.add_middleware(
|
|
87
|
+
CORSMiddleware,
|
|
88
|
+
allow_origins=list(getattr(config, "allow_origins", ["*"])),
|
|
89
|
+
allow_credentials=bool(getattr(config, "allow_credentials", True)),
|
|
90
|
+
allow_methods=list(getattr(config, "allow_methods", ["*"])),
|
|
91
|
+
allow_headers=list(getattr(config, "allow_headers", ["*"])),
|
|
92
|
+
)
|
|
93
|
+
if resolved_features.timing:
|
|
94
|
+
app.add_middleware(TimingMiddleware)
|
|
95
|
+
if resolved_features.request_id:
|
|
96
|
+
app.add_middleware(RequestIDMiddleware)
|
|
97
|
+
|
|
98
|
+
if resolved_features.exception_handlers:
|
|
99
|
+
register_exception_handlers(app, mapping=exception_mapping)
|
|
100
|
+
|
|
101
|
+
if resolved_features.health:
|
|
102
|
+
register_health_routes(app, probes=health_probes)
|
|
103
|
+
|
|
104
|
+
if routers:
|
|
105
|
+
mount_routers(app, list(routers))
|
|
106
|
+
|
|
107
|
+
return app
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _fastapi_defaults(config: t.Any) -> dict[str, t.Any]:
|
|
111
|
+
"""
|
|
112
|
+
Defaults derivados de `ServerConfig`.
|
|
113
|
+
|
|
114
|
+
`debug` gobierna la documentación interactiva: en producción, `/docs` abierto es una
|
|
115
|
+
decisión que hay que tomar a propósito, no heredar.
|
|
116
|
+
"""
|
|
117
|
+
debug = bool(getattr(config, "debug", False))
|
|
118
|
+
return {
|
|
119
|
+
"title": getattr(config, "app_title", None) or "HexCore API",
|
|
120
|
+
"version": getattr(config, "app_version", None) or "0.1.0",
|
|
121
|
+
"debug": debug,
|
|
122
|
+
"docs_url": "/docs" if debug else None,
|
|
123
|
+
"redoc_url": "/redoc" if debug else None,
|
|
124
|
+
"openapi_url": "/openapi.json" if debug else None,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _config() -> t.Any:
|
|
129
|
+
from hexcore.config import LazyConfig
|
|
130
|
+
|
|
131
|
+
return LazyConfig.get_config()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Providers FastAPI para los buses CQRS.
|
|
3
|
+
|
|
4
|
+
Existen como funciones —y no como accesos directos al contenedor— por una única razón:
|
|
5
|
+
`app.dependency_overrides[provide_command_bus] = ...` en los tests. Si el endpoint
|
|
6
|
+
tocara el singleton, no habría nada que sobreescribir.
|
|
7
|
+
|
|
8
|
+
**Una sola fuente de verdad.** El contenedor que leen estos providers es el mismo que
|
|
9
|
+
usa el worker: se configura una vez con `configure_cqrs(...)` y tanto la app web como el
|
|
10
|
+
entrypoint del worker lo consumen. Duplicar la construcción es cómo se acaba con un
|
|
11
|
+
registry en el web que no tiene los handlers del worker.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
import typing as t
|
|
17
|
+
|
|
18
|
+
from hexcore.domain.cqrs.buses import (
|
|
19
|
+
AbstractCommandBus,
|
|
20
|
+
AbstractEventBus,
|
|
21
|
+
AbstractQueryBus,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if t.TYPE_CHECKING:
|
|
25
|
+
from hexcore.application.cqrs.factory import CQRSFactory
|
|
26
|
+
from hexcore.application.cqrs.registry import HandlerRegistry
|
|
27
|
+
from hexcore.domain.cqrs.serializer import ISerializer
|
|
28
|
+
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
29
|
+
from hexcore.infrastructure.workers.consumer import CQRSConsumer
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"CQRSContainer",
|
|
33
|
+
"configure_cqrs",
|
|
34
|
+
"get_cqrs_container",
|
|
35
|
+
"reset_cqrs",
|
|
36
|
+
"provide_command_bus",
|
|
37
|
+
"provide_query_bus",
|
|
38
|
+
"provide_event_bus",
|
|
39
|
+
"provide_registry",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CQRSContainer:
|
|
44
|
+
"""
|
|
45
|
+
Contenedor perezoso de los buses CQRS.
|
|
46
|
+
|
|
47
|
+
Construye los buses la primera vez que se piden y los cachea. Perezoso a propósito:
|
|
48
|
+
`configure_cqrs()` se puede llamar en import time sin tocar la BD ni el broker.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
factory: "CQRSFactory",
|
|
54
|
+
) -> None:
|
|
55
|
+
self._factory = factory
|
|
56
|
+
self._command_bus: AbstractCommandBus | None = None
|
|
57
|
+
self._query_bus: AbstractQueryBus | None = None
|
|
58
|
+
self._event_bus: AbstractEventBus | None = None
|
|
59
|
+
self._lock = threading.RLock()
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def registry(self) -> "HandlerRegistry":
|
|
63
|
+
return self._factory._registry # noqa: SLF001 - misma capa lógica
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def serializer(self) -> "ISerializer":
|
|
67
|
+
return self._factory.create_serializer()
|
|
68
|
+
|
|
69
|
+
def command_bus(self) -> AbstractCommandBus:
|
|
70
|
+
with self._lock:
|
|
71
|
+
if self._command_bus is None:
|
|
72
|
+
self._command_bus = t.cast(
|
|
73
|
+
AbstractCommandBus, self._factory.create_command_bus()
|
|
74
|
+
)
|
|
75
|
+
return self._command_bus
|
|
76
|
+
|
|
77
|
+
def query_bus(self) -> AbstractQueryBus:
|
|
78
|
+
with self._lock:
|
|
79
|
+
if self._query_bus is None:
|
|
80
|
+
self._query_bus = t.cast(
|
|
81
|
+
AbstractQueryBus, self._factory.create_query_bus()
|
|
82
|
+
)
|
|
83
|
+
return self._query_bus
|
|
84
|
+
|
|
85
|
+
def event_bus(self) -> AbstractEventBus:
|
|
86
|
+
with self._lock:
|
|
87
|
+
if self._event_bus is None:
|
|
88
|
+
self._event_bus = t.cast(
|
|
89
|
+
AbstractEventBus, self._factory.create_event_bus()
|
|
90
|
+
)
|
|
91
|
+
return self._event_bus
|
|
92
|
+
|
|
93
|
+
def build_consumer(self) -> "CQRSConsumer":
|
|
94
|
+
"""
|
|
95
|
+
Construye el `CQRSConsumer` del worker sobre **estos mismos** buses.
|
|
96
|
+
|
|
97
|
+
Es lo que garantiza que el worker y la app web comparten registry y serializer.
|
|
98
|
+
"""
|
|
99
|
+
from hexcore.infrastructure.workers.consumer import CQRSConsumer
|
|
100
|
+
|
|
101
|
+
return CQRSConsumer(
|
|
102
|
+
command_bus=self.command_bus(),
|
|
103
|
+
event_bus=self.event_bus(),
|
|
104
|
+
serializer=self.serializer,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_container: CQRSContainer | None = None
|
|
109
|
+
_container_lock = threading.RLock()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def configure_cqrs(
|
|
113
|
+
registry: "HandlerRegistry",
|
|
114
|
+
*,
|
|
115
|
+
config: t.Any = None,
|
|
116
|
+
enqueuer: "ITaskEnqueuer | None" = None,
|
|
117
|
+
) -> CQRSContainer:
|
|
118
|
+
"""
|
|
119
|
+
Configura el contenedor CQRS del proceso. Llamalo una vez, al arrancar.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
registry: El registry con los handlers ya registrados.
|
|
123
|
+
config: Un `CQRSConfig`. Por defecto, el de `ServerConfig.cqrs` si está, o
|
|
124
|
+
`CQRSConfig()`.
|
|
125
|
+
enqueuer: El `ITaskEnqueuer` para el Smart Routing. Sin él, un
|
|
126
|
+
`@background_command` registrado hace fallar la construcción del bus (P0-5).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
El contenedor, por si querés usarlo directamente (p. ej. `build_consumer()`).
|
|
130
|
+
"""
|
|
131
|
+
from hexcore.application.cqrs.config import CQRSConfig
|
|
132
|
+
from hexcore.application.cqrs.factory import CQRSFactory
|
|
133
|
+
|
|
134
|
+
global _container
|
|
135
|
+
|
|
136
|
+
if config is None:
|
|
137
|
+
from hexcore.config import LazyConfig
|
|
138
|
+
|
|
139
|
+
config = getattr(LazyConfig.get_config(), "cqrs", None) or CQRSConfig()
|
|
140
|
+
|
|
141
|
+
with _container_lock:
|
|
142
|
+
_container = CQRSContainer(
|
|
143
|
+
CQRSFactory(config, registry, enqueuer=enqueuer)
|
|
144
|
+
)
|
|
145
|
+
return _container
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_cqrs_container() -> CQRSContainer:
|
|
149
|
+
"""
|
|
150
|
+
El contenedor configurado.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
RuntimeError: Si nadie llamó a `configure_cqrs()`.
|
|
154
|
+
"""
|
|
155
|
+
with _container_lock:
|
|
156
|
+
if _container is None:
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
"El CQRS no está configurado. Llamá a "
|
|
159
|
+
"hexcore.infrastructure.api.cqrs.configure_cqrs(registry, enqueuer=...) "
|
|
160
|
+
"al arrancar la aplicación (o el worker), antes de resolver cualquier bus."
|
|
161
|
+
)
|
|
162
|
+
return _container
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def reset_cqrs() -> None:
|
|
166
|
+
"""Descarta el contenedor. Para tests y para reconfigurar en un worker."""
|
|
167
|
+
global _container
|
|
168
|
+
|
|
169
|
+
with _container_lock:
|
|
170
|
+
_container = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── Dependencias FastAPI ──────────────────────────────────────────────────────
|
|
174
|
+
# Deliberadamente triviales: su valor está en ser un objeto sobre el que hacer
|
|
175
|
+
# `app.dependency_overrides[...] = ...`.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def provide_command_bus() -> AbstractCommandBus:
|
|
179
|
+
"""Dependencia FastAPI: el CommandBus del proceso."""
|
|
180
|
+
return get_cqrs_container().command_bus()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def provide_query_bus() -> AbstractQueryBus:
|
|
184
|
+
"""Dependencia FastAPI: el QueryBus del proceso."""
|
|
185
|
+
return get_cqrs_container().query_bus()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def provide_event_bus() -> AbstractEventBus:
|
|
189
|
+
"""Dependencia FastAPI: el EventBus del proceso."""
|
|
190
|
+
return get_cqrs_container().event_bus()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def provide_registry() -> "HandlerRegistry":
|
|
194
|
+
"""Dependencia FastAPI: el `HandlerRegistry` del proceso."""
|
|
195
|
+
return get_cqrs_container().registry
|