hexcore 2.2.0__tar.gz → 2.3.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-2.2.0/hexcore.egg-info → hexcore-2.3.0}/PKG-INFO +122 -1
- {hexcore-2.2.0 → hexcore-2.3.0}/README.md +121 -0
- hexcore-2.3.0/hexcore/application/cqrs/in_memory_buses.py +164 -0
- hexcore-2.3.0/hexcore/domain/cqrs/decorators.py +78 -0
- hexcore-2.3.0/hexcore/domain/cqrs/task_queues.py +64 -0
- hexcore-2.3.0/hexcore/infrastructure/workers/consumer.py +123 -0
- {hexcore-2.2.0 → hexcore-2.3.0/hexcore.egg-info}/PKG-INFO +122 -1
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore.egg-info/SOURCES.txt +4 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/pyproject.toml +1 -1
- hexcore-2.3.0/tests/test_smart_routing.py +218 -0
- hexcore-2.2.0/hexcore/application/cqrs/in_memory_buses.py +0 -99
- {hexcore-2.2.0 → hexcore-2.3.0}/LICENSE +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/MANIFEST.in +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/__main__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/cqrs/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/cqrs/adapters.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/cqrs/config.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/cqrs/factory.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/cqrs/pipeline.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/cqrs/registry.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/dtos/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/dtos/query.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/use_cases/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/application/use_cases/query.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/config.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/auth/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/auth/permissions.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/auth/value_objects.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/base.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/buses.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/commands.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/exceptions.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/handlers.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/middleware.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/queries.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/cqrs/serializer.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/events.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/services.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/domain/uow.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/api/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/api/utils.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cli.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cqrs/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cqrs/middlewares.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/events/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/events/events_backends/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/events/events_backends/memory.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/base.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/implementations.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/repositories/utils.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/uow/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/workers/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/py.typed +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore/types.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore.egg-info/requires.txt +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/scripts/__init__.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/scripts/main.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/setup.cfg +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/conftest.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_basic.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_beanie_query_utils.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_config_loading.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_cqrs.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_domain_service_query.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_infrastructure_query_path.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_query_field_validation.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_rabbitmq_bus.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_repositories_utils.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_uow_session_regression.py +0 -0
- {hexcore-2.2.0 → hexcore-2.3.0}/tests/test_use_cases_query.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexcore
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -456,6 +456,127 @@ En ambos casos, es tu **EventBus** (o un consumidor como Procrastinate) el encar
|
|
|
456
456
|
|
|
457
457
|
---
|
|
458
458
|
|
|
459
|
+
### Integración con Task Queues (Smart Routing)
|
|
460
|
+
|
|
461
|
+
HexCore v2 hace que la delegación de tareas a **Celery**, **Procrastinate** o **ARQ** sea increíblemente sencilla y mágica a través del patrón de **Smart Routing**.
|
|
462
|
+
|
|
463
|
+
Ya no necesitas instanciar buses separados para código síncrono y asíncrono. HexCore enruta automáticamente tus comandos y eventos hacia las colas de background usando simples decoradores.
|
|
464
|
+
|
|
465
|
+
#### 1. Decoradores de Background
|
|
466
|
+
|
|
467
|
+
HexCore ofrece 3 decoradores esenciales en `hexcore.domain.cqrs.decorators` para cubrir todos los casos de uso:
|
|
468
|
+
|
|
469
|
+
1. **`@background_command(queue="...")`**: Aplícalo sobre una clase `Command`. Todo el comando y su handler se ejecutarán asíncronamente en el Worker. Ideal para operaciones pesadas iniciadas por el usuario (ej. Generar un reporte PDF masivo).
|
|
470
|
+
2. **`@background_handler(queue="...")`**: Aplícalo sobre una función que maneje un evento (`DomainEvent`). Permite que un solo evento dispare algunas acciones síncronas rápidas y otras asíncronas lentas (ej. Enviar emails).
|
|
471
|
+
3. **`@background_task(queue="...")`**: Aplícalo sobre funciones o utilidades genéricas que no pertenecen al modelo estricto de CQRS (ej. Limpiar base de datos, tareas tipo CRON).
|
|
472
|
+
|
|
473
|
+
**Ejemplos de uso:**
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from hexcore.domain.cqrs.decorators import background_command, background_handler, background_task
|
|
477
|
+
from hexcore.domain.cqrs.commands import Command
|
|
478
|
+
|
|
479
|
+
# 1. Comando de ejecución asíncrona obligatoria
|
|
480
|
+
@background_command(queue="high_priority")
|
|
481
|
+
class SendEmailCommand(Command):
|
|
482
|
+
user_id: str
|
|
483
|
+
template: str
|
|
484
|
+
|
|
485
|
+
# 2. Handler de evento asíncrono
|
|
486
|
+
@background_handler(queue="analytics")
|
|
487
|
+
async def send_analytics_on_user_created(event: UserCreatedEvent):
|
|
488
|
+
# Lógica costosa...
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
# 3. Tarea genérica (Non-CQRS)
|
|
492
|
+
@background_task(queue="maintenance")
|
|
493
|
+
async def clean_old_records_task(days_retention: int):
|
|
494
|
+
# Limpieza de base de datos...
|
|
495
|
+
pass
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### 2. Crear el Enqueuer (Adaptador)
|
|
499
|
+
|
|
500
|
+
Implementa la interfaz genérica `ITaskEnqueuer` usando la SDK de tu Task Queue favorito:
|
|
501
|
+
|
|
502
|
+
```python
|
|
503
|
+
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
504
|
+
|
|
505
|
+
class ProcrastinateEnqueuer(ITaskEnqueuer):
|
|
506
|
+
async def enqueue_command(self, command_name: str, payload: dict, queue: str) -> None:
|
|
507
|
+
await process_cqrs_command.defer_async(payload=payload)
|
|
508
|
+
|
|
509
|
+
async def enqueue_handler(self, handler_name: str, payload: dict, queue: str) -> None:
|
|
510
|
+
await process_cqrs_handler.defer_async(handler_name=handler_name, payload=payload)
|
|
511
|
+
|
|
512
|
+
async def enqueue_event(self, event_name: str, payload: dict, queue: str) -> None:
|
|
513
|
+
pass # Usualmente no se usa directamente si utilizas @background_handler
|
|
514
|
+
|
|
515
|
+
async def enqueue_task(self, task_name: str, payload: dict, queue: str) -> None:
|
|
516
|
+
await process_generic_task.defer_async(task_name=task_name, payload=payload)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### 3. Configurar tus Buses (Enrutamiento Automático)
|
|
520
|
+
|
|
521
|
+
Al inyectar tu enqueuer en los buses estándar de memoria en tu API, estos adquieren la habilidad de enrutamiento inteligente. (Si usas un comando decorado pero no inyectas un enqueuer, HexCore levantará una excepción tempranamente).
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus, InMemoryEventBus
|
|
525
|
+
|
|
526
|
+
enqueuer = ProcrastinateEnqueuer()
|
|
527
|
+
serializer = PydanticSerializer()
|
|
528
|
+
|
|
529
|
+
# El bus evalúa: ¿Tiene el comando @background_command? Si es así, usa el enqueuer.
|
|
530
|
+
command_bus = InMemoryCommandBus(registry=registry, enqueuer=enqueuer, serializer=serializer)
|
|
531
|
+
|
|
532
|
+
# El bus evalúa suscriptores: ¿Tienen @background_handler? Si es así, usa el enqueuer.
|
|
533
|
+
event_bus = InMemoryEventBus(enqueuer=enqueuer, serializer=serializer)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### 4. Ejecutar tareas genéricas
|
|
537
|
+
|
|
538
|
+
Para encolar la tarea genérica (`@background_task`), la llamas indirectamente pasándola por el enqueuer:
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
# Así se encola una tarea genérica sin CQRS:
|
|
542
|
+
await enqueuer.enqueue_task(
|
|
543
|
+
task_name=clean_old_records_task.__cqrs_task_name__,
|
|
544
|
+
payload={"days_retention": 30},
|
|
545
|
+
queue=clean_old_records_task.__cqrs_queue__
|
|
546
|
+
)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### 5. Levantar el Worker (Consumidor Universal)
|
|
550
|
+
|
|
551
|
+
En el entrypoint de tu worker (ej. Celery o Procrastinate), usa el `CQRSConsumer` de HexCore para deserializar y ejecutar los payloads interceptados. El consumidor usa resolución dinámica para invocar la función correcta automáticamente.
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
from hexcore.infrastructure.workers.consumer import CQRSConsumer
|
|
555
|
+
|
|
556
|
+
consumer = CQRSConsumer(
|
|
557
|
+
command_bus=command_bus, # Tu CommandBus configurado
|
|
558
|
+
event_bus=event_bus,
|
|
559
|
+
serializer=serializer
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
@app.task(name="process_cqrs_command")
|
|
563
|
+
async def process_cqrs_command(payload: dict):
|
|
564
|
+
# HexCore deserializa y ejecuta el Command usando el CommandBus local
|
|
565
|
+
await consumer.process_command(payload)
|
|
566
|
+
|
|
567
|
+
@app.task(name="process_cqrs_handler")
|
|
568
|
+
async def process_cqrs_handler(handler_name: str, payload: dict):
|
|
569
|
+
# HexCore resuelve y ejecuta exclusivamente el Event Handler asíncrono
|
|
570
|
+
await consumer.process_handler(handler_name, payload)
|
|
571
|
+
|
|
572
|
+
@app.task(name="process_generic_task")
|
|
573
|
+
async def process_generic_task(task_name: str, payload: dict):
|
|
574
|
+
# HexCore resuelve e inyecta los kwargs a la función pura
|
|
575
|
+
await consumer.process_task(task_name, payload)
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
459
580
|
## Referencias
|
|
460
581
|
|
|
461
582
|
- [CONTRIBUTING.md](./CONTRIBUTING.md): Pautas de colaboración.
|
|
@@ -430,6 +430,127 @@ En ambos casos, es tu **EventBus** (o un consumidor como Procrastinate) el encar
|
|
|
430
430
|
|
|
431
431
|
---
|
|
432
432
|
|
|
433
|
+
### Integración con Task Queues (Smart Routing)
|
|
434
|
+
|
|
435
|
+
HexCore v2 hace que la delegación de tareas a **Celery**, **Procrastinate** o **ARQ** sea increíblemente sencilla y mágica a través del patrón de **Smart Routing**.
|
|
436
|
+
|
|
437
|
+
Ya no necesitas instanciar buses separados para código síncrono y asíncrono. HexCore enruta automáticamente tus comandos y eventos hacia las colas de background usando simples decoradores.
|
|
438
|
+
|
|
439
|
+
#### 1. Decoradores de Background
|
|
440
|
+
|
|
441
|
+
HexCore ofrece 3 decoradores esenciales en `hexcore.domain.cqrs.decorators` para cubrir todos los casos de uso:
|
|
442
|
+
|
|
443
|
+
1. **`@background_command(queue="...")`**: Aplícalo sobre una clase `Command`. Todo el comando y su handler se ejecutarán asíncronamente en el Worker. Ideal para operaciones pesadas iniciadas por el usuario (ej. Generar un reporte PDF masivo).
|
|
444
|
+
2. **`@background_handler(queue="...")`**: Aplícalo sobre una función que maneje un evento (`DomainEvent`). Permite que un solo evento dispare algunas acciones síncronas rápidas y otras asíncronas lentas (ej. Enviar emails).
|
|
445
|
+
3. **`@background_task(queue="...")`**: Aplícalo sobre funciones o utilidades genéricas que no pertenecen al modelo estricto de CQRS (ej. Limpiar base de datos, tareas tipo CRON).
|
|
446
|
+
|
|
447
|
+
**Ejemplos de uso:**
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
from hexcore.domain.cqrs.decorators import background_command, background_handler, background_task
|
|
451
|
+
from hexcore.domain.cqrs.commands import Command
|
|
452
|
+
|
|
453
|
+
# 1. Comando de ejecución asíncrona obligatoria
|
|
454
|
+
@background_command(queue="high_priority")
|
|
455
|
+
class SendEmailCommand(Command):
|
|
456
|
+
user_id: str
|
|
457
|
+
template: str
|
|
458
|
+
|
|
459
|
+
# 2. Handler de evento asíncrono
|
|
460
|
+
@background_handler(queue="analytics")
|
|
461
|
+
async def send_analytics_on_user_created(event: UserCreatedEvent):
|
|
462
|
+
# Lógica costosa...
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
# 3. Tarea genérica (Non-CQRS)
|
|
466
|
+
@background_task(queue="maintenance")
|
|
467
|
+
async def clean_old_records_task(days_retention: int):
|
|
468
|
+
# Limpieza de base de datos...
|
|
469
|
+
pass
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### 2. Crear el Enqueuer (Adaptador)
|
|
473
|
+
|
|
474
|
+
Implementa la interfaz genérica `ITaskEnqueuer` usando la SDK de tu Task Queue favorito:
|
|
475
|
+
|
|
476
|
+
```python
|
|
477
|
+
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
478
|
+
|
|
479
|
+
class ProcrastinateEnqueuer(ITaskEnqueuer):
|
|
480
|
+
async def enqueue_command(self, command_name: str, payload: dict, queue: str) -> None:
|
|
481
|
+
await process_cqrs_command.defer_async(payload=payload)
|
|
482
|
+
|
|
483
|
+
async def enqueue_handler(self, handler_name: str, payload: dict, queue: str) -> None:
|
|
484
|
+
await process_cqrs_handler.defer_async(handler_name=handler_name, payload=payload)
|
|
485
|
+
|
|
486
|
+
async def enqueue_event(self, event_name: str, payload: dict, queue: str) -> None:
|
|
487
|
+
pass # Usualmente no se usa directamente si utilizas @background_handler
|
|
488
|
+
|
|
489
|
+
async def enqueue_task(self, task_name: str, payload: dict, queue: str) -> None:
|
|
490
|
+
await process_generic_task.defer_async(task_name=task_name, payload=payload)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### 3. Configurar tus Buses (Enrutamiento Automático)
|
|
494
|
+
|
|
495
|
+
Al inyectar tu enqueuer en los buses estándar de memoria en tu API, estos adquieren la habilidad de enrutamiento inteligente. (Si usas un comando decorado pero no inyectas un enqueuer, HexCore levantará una excepción tempranamente).
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus, InMemoryEventBus
|
|
499
|
+
|
|
500
|
+
enqueuer = ProcrastinateEnqueuer()
|
|
501
|
+
serializer = PydanticSerializer()
|
|
502
|
+
|
|
503
|
+
# El bus evalúa: ¿Tiene el comando @background_command? Si es así, usa el enqueuer.
|
|
504
|
+
command_bus = InMemoryCommandBus(registry=registry, enqueuer=enqueuer, serializer=serializer)
|
|
505
|
+
|
|
506
|
+
# El bus evalúa suscriptores: ¿Tienen @background_handler? Si es así, usa el enqueuer.
|
|
507
|
+
event_bus = InMemoryEventBus(enqueuer=enqueuer, serializer=serializer)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### 4. Ejecutar tareas genéricas
|
|
511
|
+
|
|
512
|
+
Para encolar la tarea genérica (`@background_task`), la llamas indirectamente pasándola por el enqueuer:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
# Así se encola una tarea genérica sin CQRS:
|
|
516
|
+
await enqueuer.enqueue_task(
|
|
517
|
+
task_name=clean_old_records_task.__cqrs_task_name__,
|
|
518
|
+
payload={"days_retention": 30},
|
|
519
|
+
queue=clean_old_records_task.__cqrs_queue__
|
|
520
|
+
)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### 5. Levantar el Worker (Consumidor Universal)
|
|
524
|
+
|
|
525
|
+
En el entrypoint de tu worker (ej. Celery o Procrastinate), usa el `CQRSConsumer` de HexCore para deserializar y ejecutar los payloads interceptados. El consumidor usa resolución dinámica para invocar la función correcta automáticamente.
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
from hexcore.infrastructure.workers.consumer import CQRSConsumer
|
|
529
|
+
|
|
530
|
+
consumer = CQRSConsumer(
|
|
531
|
+
command_bus=command_bus, # Tu CommandBus configurado
|
|
532
|
+
event_bus=event_bus,
|
|
533
|
+
serializer=serializer
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
@app.task(name="process_cqrs_command")
|
|
537
|
+
async def process_cqrs_command(payload: dict):
|
|
538
|
+
# HexCore deserializa y ejecuta el Command usando el CommandBus local
|
|
539
|
+
await consumer.process_command(payload)
|
|
540
|
+
|
|
541
|
+
@app.task(name="process_cqrs_handler")
|
|
542
|
+
async def process_cqrs_handler(handler_name: str, payload: dict):
|
|
543
|
+
# HexCore resuelve y ejecuta exclusivamente el Event Handler asíncrono
|
|
544
|
+
await consumer.process_handler(handler_name, payload)
|
|
545
|
+
|
|
546
|
+
@app.task(name="process_generic_task")
|
|
547
|
+
async def process_generic_task(task_name: str, payload: dict):
|
|
548
|
+
# HexCore resuelve e inyecta los kwargs a la función pura
|
|
549
|
+
await consumer.process_task(task_name, payload)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
433
554
|
## Referencias
|
|
434
555
|
|
|
435
556
|
- [CONTRIBUTING.md](./CONTRIBUTING.md): Pautas de colaboración.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementaciones In-Memory de los buses CQRS con soporte para Enrutamiento Inteligente (Smart Routing).
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import typing as t
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from hexcore.domain.cqrs.buses import ICommandBus, IQueryBus, IEventBus
|
|
10
|
+
from hexcore.domain.cqrs.commands import Command
|
|
11
|
+
from hexcore.domain.cqrs.queries import Query
|
|
12
|
+
from hexcore.domain.events import DomainEvent
|
|
13
|
+
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
14
|
+
from hexcore.domain.cqrs.serializer import ISerializer
|
|
15
|
+
|
|
16
|
+
from .registry import HandlerRegistry
|
|
17
|
+
from .pipeline import MiddlewarePipeline
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("hexcore.cqrs.buses")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InMemoryCommandBus(ICommandBus):
|
|
23
|
+
"""
|
|
24
|
+
Bus de commands síncrono en memoria con Smart Routing.
|
|
25
|
+
Resuelve el handler desde el registry, ejecuta el pipeline de middlewares
|
|
26
|
+
y retorna el resultado.
|
|
27
|
+
|
|
28
|
+
Si el comando está decorado con `@background_command` y se provee un `enqueuer`,
|
|
29
|
+
el comando es automáticamente encolado para su ejecución en segundo plano
|
|
30
|
+
sin bloquear el proceso actual (retornando None).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
registry: HandlerRegistry,
|
|
36
|
+
pipeline: MiddlewarePipeline | None = None,
|
|
37
|
+
enqueuer: ITaskEnqueuer | None = None,
|
|
38
|
+
serializer: ISerializer | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._registry = registry
|
|
41
|
+
self._pipeline = pipeline or MiddlewarePipeline()
|
|
42
|
+
self._enqueuer = enqueuer
|
|
43
|
+
self._serializer = serializer
|
|
44
|
+
|
|
45
|
+
async def dispatch(self, command: Command) -> t.Any:
|
|
46
|
+
cmd_type = type(command)
|
|
47
|
+
|
|
48
|
+
# 1. Smart Routing: ¿Debe irse a background?
|
|
49
|
+
is_background = getattr(cmd_type, "__cqrs_background__", False)
|
|
50
|
+
|
|
51
|
+
if is_background:
|
|
52
|
+
if not self._enqueuer or not self._serializer:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
f"El comando '{cmd_type.__name__}' requiere ejecución en background, "
|
|
55
|
+
"pero el InMemoryCommandBus no tiene configurado un 'enqueuer' o 'serializer'."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def background_dispatcher(cmd: Command) -> None:
|
|
59
|
+
queue_name = getattr(cmd_type, "__cqrs_queue__", "default")
|
|
60
|
+
payload = self._serializer.serialize(cmd) # type: ignore
|
|
61
|
+
await self._enqueuer.enqueue_command(cmd_type.__name__, payload, queue=queue_name) # type: ignore
|
|
62
|
+
logger.debug("[SmartRouting] Comando %s enrutado a background (queue=%s)", cmd_type.__name__, queue_name)
|
|
63
|
+
|
|
64
|
+
await self._pipeline.execute(command, background_dispatcher)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# 2. Ejecución Local Estándar
|
|
68
|
+
handler = self._registry.resolve_command_handler(cmd_type)
|
|
69
|
+
|
|
70
|
+
async def final_handler(cmd: t.Any) -> t.Any:
|
|
71
|
+
return await handler.handle(cmd)
|
|
72
|
+
|
|
73
|
+
return await self._pipeline.execute(command, final_handler)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InMemoryQueryBus(IQueryBus):
|
|
77
|
+
"""
|
|
78
|
+
Bus de queries síncrono en memoria.
|
|
79
|
+
Las queries NUNCA pasan por buses asíncronos
|
|
80
|
+
(principio CQRS: las lecturas son siempre síncronas).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
registry: HandlerRegistry,
|
|
86
|
+
pipeline: MiddlewarePipeline | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self._registry = registry
|
|
89
|
+
self._pipeline = pipeline or MiddlewarePipeline()
|
|
90
|
+
|
|
91
|
+
async def ask(self, query: Query[t.Any]) -> t.Any:
|
|
92
|
+
handler = self._registry.resolve_query_handler(type(query))
|
|
93
|
+
|
|
94
|
+
async def final_handler(q: t.Any) -> t.Any:
|
|
95
|
+
return await handler.handle(q)
|
|
96
|
+
|
|
97
|
+
return await self._pipeline.execute(query, final_handler)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class InMemoryEventBus(IEventBus):
|
|
101
|
+
"""
|
|
102
|
+
Bus de eventos en memoria con Smart Routing para Handlers Asíncronos.
|
|
103
|
+
|
|
104
|
+
Permite publicar eventos a todos los suscriptores. Si un suscriptor
|
|
105
|
+
fue decorado con `@background_handler`, se enviará un mensaje al Task Queue
|
|
106
|
+
para que *solo* ese handler se ejecute en background para este evento.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
pipeline: MiddlewarePipeline | None = None,
|
|
112
|
+
enqueuer: ITaskEnqueuer | None = None,
|
|
113
|
+
serializer: ISerializer | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
self._handlers: dict[
|
|
116
|
+
type[DomainEvent],
|
|
117
|
+
list[t.Callable[[DomainEvent], t.Awaitable[None]]],
|
|
118
|
+
] = {}
|
|
119
|
+
self._pipeline = pipeline or MiddlewarePipeline()
|
|
120
|
+
self._enqueuer = enqueuer
|
|
121
|
+
self._serializer = serializer
|
|
122
|
+
|
|
123
|
+
def subscribe(
|
|
124
|
+
self,
|
|
125
|
+
event_type: type[DomainEvent],
|
|
126
|
+
handler: t.Callable[[DomainEvent], t.Awaitable[None]],
|
|
127
|
+
) -> None:
|
|
128
|
+
self._handlers.setdefault(event_type, []).append(handler)
|
|
129
|
+
|
|
130
|
+
async def publish(self, event: DomainEvent) -> None:
|
|
131
|
+
handlers = self._handlers.get(type(event), [])
|
|
132
|
+
|
|
133
|
+
for event_handler in handlers:
|
|
134
|
+
is_background = getattr(event_handler, "__cqrs_background_handler__", False)
|
|
135
|
+
|
|
136
|
+
if is_background:
|
|
137
|
+
# Enrutamiento hacia background
|
|
138
|
+
if not self._enqueuer or not self._serializer:
|
|
139
|
+
raise RuntimeError(
|
|
140
|
+
f"El handler de eventos '{event_handler.__name__}' requiere ejecución en background, "
|
|
141
|
+
"pero el InMemoryEventBus no tiene configurado un 'enqueuer' o 'serializer'."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def background_dispatcher(
|
|
145
|
+
evt: t.Any,
|
|
146
|
+
_h: t.Callable[..., t.Awaitable[None]] = event_handler,
|
|
147
|
+
) -> None:
|
|
148
|
+
handler_name = getattr(_h, "__cqrs_handler_name__")
|
|
149
|
+
queue_name = getattr(_h, "__cqrs_queue__", "default")
|
|
150
|
+
payload = self._serializer.serialize(evt) # type: ignore
|
|
151
|
+
await self._enqueuer.enqueue_handler(handler_name, payload, queue=queue_name) # type: ignore
|
|
152
|
+
logger.debug("[SmartRouting] EventHandler %s enrutado a background (queue=%s)", handler_name, queue_name)
|
|
153
|
+
|
|
154
|
+
await self._pipeline.execute(event, background_dispatcher)
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
# Ejecución local síncrona
|
|
158
|
+
async def final_handler(
|
|
159
|
+
evt: t.Any,
|
|
160
|
+
_h: t.Callable[..., t.Awaitable[None]] = event_handler,
|
|
161
|
+
) -> None:
|
|
162
|
+
await _h(evt)
|
|
163
|
+
|
|
164
|
+
await self._pipeline.execute(event, final_handler)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decoradores para habilitar el enrutamiento inteligente (Smart Routing) de tareas, comandos
|
|
3
|
+
y manejadores de eventos hacia Task Queues (Celery, Procrastinate, etc.) de forma transparente.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import typing as t
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def background_command(queue: str = "default") -> t.Callable[[t.Type[t.Any]], t.Type[t.Any]]:
|
|
11
|
+
"""
|
|
12
|
+
Decora una clase `Command` para indicar que su ejecución debe enviarse siempre
|
|
13
|
+
a un procesador en segundo plano (Task Queue).
|
|
14
|
+
|
|
15
|
+
Cuando el CommandBus detecta este decorador, no ejecutará el comando localmente,
|
|
16
|
+
sino que llamará a `ITaskEnqueuer.enqueue_command()`.
|
|
17
|
+
|
|
18
|
+
Ejemplo:
|
|
19
|
+
@background_command(queue="emails")
|
|
20
|
+
class SendEmailCommand(Command):
|
|
21
|
+
...
|
|
22
|
+
"""
|
|
23
|
+
def wrapper(cls: t.Type[t.Any]) -> t.Type[t.Any]:
|
|
24
|
+
cls.__cqrs_background__ = True
|
|
25
|
+
cls.__cqrs_queue__ = queue
|
|
26
|
+
return cls
|
|
27
|
+
|
|
28
|
+
return wrapper
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def background_handler(queue: str = "default") -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
32
|
+
"""
|
|
33
|
+
Decora un manejador de eventos (Event Handler) para indicar que su ejecución
|
|
34
|
+
debe ocurrir de forma asíncrona en un Worker de background.
|
|
35
|
+
|
|
36
|
+
Cuando el EventBus detecta este decorador en un handler suscrito, en lugar de
|
|
37
|
+
ejecutarlo directamente, delegará la tarea a `ITaskEnqueuer.enqueue_handler()`.
|
|
38
|
+
|
|
39
|
+
Ejemplo:
|
|
40
|
+
@background_handler(queue="analytics")
|
|
41
|
+
async def on_user_created(event: UserCreatedEvent):
|
|
42
|
+
...
|
|
43
|
+
"""
|
|
44
|
+
def wrapper(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
45
|
+
func.__cqrs_background_handler__ = True
|
|
46
|
+
func.__cqrs_queue__ = queue
|
|
47
|
+
|
|
48
|
+
# Guardamos el nombre calificado del handler para poder resolverlo luego en el Worker
|
|
49
|
+
if not hasattr(func, "__qualname__"):
|
|
50
|
+
func.__cqrs_handler_name__ = func.__name__
|
|
51
|
+
else:
|
|
52
|
+
func.__cqrs_handler_name__ = f"{func.__module__}.{func.__qualname__}"
|
|
53
|
+
|
|
54
|
+
return func
|
|
55
|
+
|
|
56
|
+
return wrapper
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def background_task(queue: str = "default") -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
60
|
+
"""
|
|
61
|
+
Decora una función genérica para convertirla en una Tarea de Background
|
|
62
|
+
(independiente de CQRS).
|
|
63
|
+
|
|
64
|
+
La función original queda marcada, y puede ser registrada en el
|
|
65
|
+
`TaskManager` de HexCore o ejecutada directamente usando el `ITaskEnqueuer`.
|
|
66
|
+
"""
|
|
67
|
+
def wrapper(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
68
|
+
func.__cqrs_background_task__ = True
|
|
69
|
+
func.__cqrs_queue__ = queue
|
|
70
|
+
|
|
71
|
+
if not hasattr(func, "__qualname__"):
|
|
72
|
+
func.__cqrs_task_name__ = func.__name__
|
|
73
|
+
else:
|
|
74
|
+
func.__cqrs_task_name__ = f"{func.__module__}.{func.__qualname__}"
|
|
75
|
+
|
|
76
|
+
return func
|
|
77
|
+
|
|
78
|
+
return wrapper
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interfaces fundamentales para delegar mensajes a Task Queues (Celery, Procrastinate, RQ, ARQ).
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import abc
|
|
7
|
+
import typing as t
|
|
8
|
+
|
|
9
|
+
if t.TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ITaskEnqueuer(abc.ABC):
|
|
14
|
+
"""
|
|
15
|
+
Puerto (Puerto de Salida) genérico para encolar tareas asíncronas
|
|
16
|
+
o en paralelo hacia un backend de colas de mensajes (Task Queue).
|
|
17
|
+
|
|
18
|
+
Un adaptador concreto (ej. `CeleryTaskEnqueuer`) usará la SDK
|
|
19
|
+
nativa del worker para empujar el `payload`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
async def enqueue_command(self, command_name: str, payload: dict[str, t.Any], queue: str) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Encola un comando de dominio serializado para ser ejecutado en background.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
command_name: Nombre calificado del comando (e.g. 'CreateUserCommand').
|
|
29
|
+
payload: Payload pre-serializado.
|
|
30
|
+
queue: Nombre de la cola de destino.
|
|
31
|
+
"""
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
async def enqueue_event(self, event_name: str, payload: dict[str, t.Any], queue: str) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Encola un evento de dominio serializado para ser distribuido genéricamente.
|
|
38
|
+
"""
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
async def enqueue_handler(self, handler_name: str, payload: dict[str, t.Any], queue: str) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Encola la ejecución de un *Event Handler Específico* para un evento.
|
|
45
|
+
Esto permite que un Evento dispare handlers asíncronos individuales.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
handler_name: Nombre calificado del handler (func.__cqrs_handler_name__).
|
|
49
|
+
payload: Payload pre-serializado del evento.
|
|
50
|
+
queue: Nombre de la cola.
|
|
51
|
+
"""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
async def enqueue_task(self, task_name: str, payload: dict[str, t.Any], queue: str) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Encola una tarea genérica de background (independiente de CQRS).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
task_name: Nombre calificado de la tarea (func.__cqrs_task_name__).
|
|
61
|
+
payload: Argumentos serializados para la tarea.
|
|
62
|
+
queue: Nombre de la cola.
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Consumidor Universal para colas de tareas (Celery, Procrastinate).
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
import logging
|
|
8
|
+
import typing as t
|
|
9
|
+
|
|
10
|
+
from hexcore.domain.cqrs.buses import AbstractCommandBus, AbstractEventBus
|
|
11
|
+
from hexcore.domain.cqrs.serializer import ISerializer
|
|
12
|
+
from hexcore.domain.events import DomainEvent
|
|
13
|
+
|
|
14
|
+
if t.TYPE_CHECKING:
|
|
15
|
+
from hexcore.domain.cqrs.commands import Command
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("hexcore.workers.consumer")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_callable(qualname: str) -> t.Callable[..., t.Any]:
|
|
21
|
+
"""Resuelve un import por su nombre calificado (ej. 'myapp.events.on_user_created')."""
|
|
22
|
+
try:
|
|
23
|
+
module_path, func_name = qualname.rsplit(".", 1)
|
|
24
|
+
module = importlib.import_module(module_path)
|
|
25
|
+
return getattr(module, func_name) # type: ignore
|
|
26
|
+
except (ValueError, ModuleNotFoundError, AttributeError) as exc:
|
|
27
|
+
raise RuntimeError(f"No se pudo resolver el handler/tarea '{qualname}': {exc}") from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CQRSConsumer:
|
|
31
|
+
"""
|
|
32
|
+
Ayudante universal para recibir mensajes serializados desde un Worker
|
|
33
|
+
y despacharlos a los handlers locales usando buses en memoria.
|
|
34
|
+
|
|
35
|
+
Uso (ej. Procrastinate):
|
|
36
|
+
|
|
37
|
+
consumer = CQRSConsumer(local_cmd_bus, local_evt_bus, serializer)
|
|
38
|
+
|
|
39
|
+
@app.task(name="process_cqrs_command")
|
|
40
|
+
async def process_cqrs_command(payload: dict):
|
|
41
|
+
await consumer.process_command(payload)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
command_bus: AbstractCommandBus,
|
|
47
|
+
event_bus: AbstractEventBus,
|
|
48
|
+
serializer: ISerializer,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Args:
|
|
52
|
+
command_bus: El bus de comandos *local* (usualmente InMemoryCommandBus)
|
|
53
|
+
que tiene registrados los handlers.
|
|
54
|
+
event_bus: El bus de eventos *local* (usualmente InMemoryEventBus).
|
|
55
|
+
serializer: El serializador usado (ej. PydanticSerializer).
|
|
56
|
+
"""
|
|
57
|
+
self._command_bus = command_bus
|
|
58
|
+
self._event_bus = event_bus
|
|
59
|
+
self._serializer = serializer
|
|
60
|
+
|
|
61
|
+
async def process_command(self, payload: dict[str, t.Any]) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Deserializa un payload de comando y lo despacha al bus local.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
command = self._serializer.deserialize(payload)
|
|
67
|
+
cmd = t.cast("Command", command)
|
|
68
|
+
|
|
69
|
+
logger.info("[CQRSConsumer] Procesando comando en background: %s", type(cmd).__qualname__)
|
|
70
|
+
await self._command_bus.dispatch(cmd)
|
|
71
|
+
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logger.exception("[CQRSConsumer] Error procesando comando: %s", exc)
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
async def process_event(self, payload: dict[str, t.Any]) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Deserializa un payload de evento y lo publica en el bus local
|
|
79
|
+
para que todos los handlers (suscriptores) locales lo consuman.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
event = self._serializer.deserialize(payload)
|
|
83
|
+
evt = t.cast(DomainEvent, event)
|
|
84
|
+
|
|
85
|
+
logger.info("[CQRSConsumer] Procesando evento en background: %s", evt.event_name)
|
|
86
|
+
await self._event_bus.publish(evt)
|
|
87
|
+
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
logger.exception("[CQRSConsumer] Error procesando evento: %s", exc)
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
async def process_handler(self, handler_name: str, payload: dict[str, t.Any]) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Deserializa el evento y ejecuta *específicamente* un handler decorado con `@background_handler`.
|
|
95
|
+
Esto es inyectado por el Smart Routing del EventBus.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
event = self._serializer.deserialize(payload)
|
|
99
|
+
evt = t.cast(DomainEvent, event)
|
|
100
|
+
|
|
101
|
+
logger.info("[CQRSConsumer] Ejecutando EventHandler individual en background: %s", handler_name)
|
|
102
|
+
|
|
103
|
+
handler_func = _resolve_callable(handler_name)
|
|
104
|
+
await handler_func(evt)
|
|
105
|
+
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.exception("[CQRSConsumer] Error procesando EventHandler %s: %s", handler_name, exc)
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
async def process_task(self, task_name: str, payload: dict[str, t.Any]) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Ejecuta una tarea genérica de background decorada con `@background_task`.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
logger.info("[CQRSConsumer] Ejecutando Task genérica en background: %s", task_name)
|
|
116
|
+
|
|
117
|
+
task_func = _resolve_callable(task_name)
|
|
118
|
+
# El payload en tareas genéricas son los kwargs
|
|
119
|
+
await task_func(**payload)
|
|
120
|
+
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.exception("[CQRSConsumer] Error ejecutando Task %s: %s", task_name, exc)
|
|
123
|
+
raise
|