hexcore 2.5.0__tar.gz → 3.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-2.5.0/hexcore.egg-info → hexcore-3.0.0}/PKG-INFO +14 -4
- {hexcore-2.5.0 → hexcore-3.0.0}/README.md +13 -3
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/cqrs/config.py +6 -7
- hexcore-3.0.0/hexcore/application/cqrs/factory.py +215 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/cqrs/in_memory_buses.py +24 -7
- hexcore-3.0.0/hexcore/application/cqrs/registry.py +185 -0
- hexcore-3.0.0/hexcore/application/cqrs/scheduler.py +242 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/__init__.py +9 -0
- hexcore-3.0.0/hexcore/domain/cqrs/context.py +61 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/decorators.py +27 -17
- hexcore-3.0.0/hexcore/domain/cqrs/resolution.py +87 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/api/utils.py +21 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/middlewares.py +29 -20
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/postgres_bus.py +11 -3
- hexcore-3.0.0/hexcore/infrastructure/cqrs/postgres_lock.py +190 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +6 -6
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/redis_bus.py +12 -4
- hexcore-3.0.0/hexcore/infrastructure/cqrs/redis_lock.py +110 -0
- hexcore-3.0.0/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +208 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/task_queues/celery_adapter.py +51 -6
- hexcore-3.0.0/hexcore/infrastructure/task_queues/procrastinate_adapter.py +120 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/uow/__init__.py +20 -0
- hexcore-3.0.0/hexcore/infrastructure/uow/scopes.py +93 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/workers/consumer.py +67 -22
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +10 -6
- {hexcore-2.5.0 → hexcore-3.0.0/hexcore.egg-info}/PKG-INFO +14 -4
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore.egg-info/SOURCES.txt +13 -1
- {hexcore-2.5.0 → hexcore-3.0.0}/pyproject.toml +1 -1
- hexcore-3.0.0/tests/test_consumer_optional_event_bus.py +68 -0
- hexcore-3.0.0/tests/test_cqrs_factory.py +141 -0
- hexcore-3.0.0/tests/test_cqrs_middlewares.py +61 -0
- hexcore-3.0.0/tests/test_dotted_resolution.py +138 -0
- hexcore-3.0.0/tests/test_handler_registry.py +199 -0
- hexcore-3.0.0/tests/test_lock_error_policy.py +132 -0
- hexcore-3.0.0/tests/test_postgres_lock.py +152 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_scheduler.py +2 -0
- hexcore-3.0.0/tests/test_scheduler_catchup.py +361 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_smart_routing.py +33 -23
- hexcore-3.0.0/tests/test_sql_session_layer.py +338 -0
- hexcore-3.0.0/tests/test_task_queues_adapters.py +162 -0
- hexcore-3.0.0/tests/test_worker_bus_integration.py +271 -0
- hexcore-2.5.0/hexcore/application/cqrs/factory.py +0 -131
- hexcore-2.5.0/hexcore/application/cqrs/registry.py +0 -115
- hexcore-2.5.0/hexcore/application/cqrs/scheduler.py +0 -99
- hexcore-2.5.0/hexcore/infrastructure/cqrs/postgres_lock.py +0 -95
- hexcore-2.5.0/hexcore/infrastructure/cqrs/redis_lock.py +0 -62
- hexcore-2.5.0/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -66
- hexcore-2.5.0/hexcore/infrastructure/task_queues/procrastinate_adapter.py +0 -61
- hexcore-2.5.0/tests/test_postgres_lock.py +0 -77
- hexcore-2.5.0/tests/test_task_queues_adapters.py +0 -82
- {hexcore-2.5.0 → hexcore-3.0.0}/LICENSE +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/MANIFEST.in +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/__main__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/cqrs/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/cqrs/adapters.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/cqrs/pipeline.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/dtos/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/dtos/query.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/use_cases/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/application/use_cases/query.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/config.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/auth/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/auth/permissions.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/auth/value_objects.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/base.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/buses.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/commands.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/cron.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/exceptions.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/handlers.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/middleware.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/queries.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/serializer.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/cqrs/task_queues.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/events.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/services.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/domain/uow.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/api/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cli.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/events/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/events/events_backends/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/events/events_backends/memory.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/base.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/implementations.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/repositories/utils.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/task_queues/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/infrastructure/workers/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/py.typed +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore/types.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore.egg-info/requires.txt +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/scripts/__init__.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/scripts/main.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/setup.cfg +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/conftest.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_basic.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_beanie_query_utils.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_config_loading.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_cqrs.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_domain_service_query.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_infrastructure_query_path.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_optional_dependencies.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_postgres_bus.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_query_field_validation.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_rabbitmq_bus.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_redis_bus.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_redis_lock.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_repositories_utils.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.0}/tests/test_uow_session_regression.py +0 -0
- {hexcore-2.5.0 → hexcore-3.0.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:
|
|
3
|
+
Version: 3.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
|
|
@@ -297,7 +297,7 @@ HexCore v2 integra de forma nativa soporte para el patrón **CQRS (Command Query
|
|
|
297
297
|
|
|
298
298
|
El sistema se basa en 3 buses principales, configurables e independientes:
|
|
299
299
|
|
|
300
|
-
1. **`AbstractCommandBus`**: Despacha inteniones de mutación (`Command`) a un único `AbstractCommandHandler`. Los commands modifican el estado del sistema
|
|
300
|
+
1. **`AbstractCommandBus`**: Despacha inteniones de mutación (`Command`) a un único `AbstractCommandHandler`. Los commands modifican el estado del sistema. La transacción la gestiona el handler (el patrón que enseñan los ejemplos de use case); si preferís que la gestione el bus, añadí `TransactionMiddleware` explícitamente con su `uow_factory`.
|
|
301
301
|
2. **`AbstractQueryBus`**: Despacha intenciones de lectura (`Query`) a un único `AbstractQueryHandler`. Retornan un resultado sin mutar el estado.
|
|
302
302
|
3. **`EventBus`**: Distribuye eventos de dominio (`DomainEvent`) a múltiples suscriptores asíncronamente (vía `subscribe`/`publish`).
|
|
303
303
|
|
|
@@ -310,8 +310,9 @@ from hexcore.application.cqrs.config import CQRSConfig, BusConfig
|
|
|
310
310
|
config = ServerConfig(
|
|
311
311
|
cqrs=CQRSConfig(
|
|
312
312
|
command_bus=BusConfig(
|
|
313
|
-
#
|
|
314
|
-
|
|
313
|
+
# Sin middlewares por defecto. Los que no necesitan configuración se
|
|
314
|
+
# pueden declarar por dotted path:
|
|
315
|
+
middlewares=["hexcore.infrastructure.cqrs.middlewares.LoggingMiddleware"]
|
|
315
316
|
),
|
|
316
317
|
# Puedes sustituir el backend en memoria por uno distribuido (Ej: Celery, Procrastinate)
|
|
317
318
|
# backend="mi_app.infrastructure.ProcrastinateCommandBus"
|
|
@@ -319,6 +320,15 @@ config = ServerConfig(
|
|
|
319
320
|
)
|
|
320
321
|
```
|
|
321
322
|
|
|
323
|
+
> **`TransactionMiddleware` no es el default.** Comitea después del handler, así que
|
|
324
|
+
> con un handler que ya gestiona su propia transacción comitearías dos veces. Y
|
|
325
|
+
> necesita un `uow_factory` construido con *tu* engine, cosa que no se puede expresar
|
|
326
|
+
> como dotted path: instancialo a mano y pasá el pipeline al bus.
|
|
327
|
+
>
|
|
328
|
+
> ```python
|
|
329
|
+
> TransactionMiddleware(uow_factory=lambda: SqlAlchemyUnitOfWork(session=session_factory()))
|
|
330
|
+
> ```
|
|
331
|
+
|
|
322
332
|
---
|
|
323
333
|
|
|
324
334
|
### Guía de Migración: De Casos de Uso Clásicos a CQRS
|
|
@@ -252,7 +252,7 @@ HexCore v2 integra de forma nativa soporte para el patrón **CQRS (Command Query
|
|
|
252
252
|
|
|
253
253
|
El sistema se basa en 3 buses principales, configurables e independientes:
|
|
254
254
|
|
|
255
|
-
1. **`AbstractCommandBus`**: Despacha inteniones de mutación (`Command`) a un único `AbstractCommandHandler`. Los commands modifican el estado del sistema
|
|
255
|
+
1. **`AbstractCommandBus`**: Despacha inteniones de mutación (`Command`) a un único `AbstractCommandHandler`. Los commands modifican el estado del sistema. La transacción la gestiona el handler (el patrón que enseñan los ejemplos de use case); si preferís que la gestione el bus, añadí `TransactionMiddleware` explícitamente con su `uow_factory`.
|
|
256
256
|
2. **`AbstractQueryBus`**: Despacha intenciones de lectura (`Query`) a un único `AbstractQueryHandler`. Retornan un resultado sin mutar el estado.
|
|
257
257
|
3. **`EventBus`**: Distribuye eventos de dominio (`DomainEvent`) a múltiples suscriptores asíncronamente (vía `subscribe`/`publish`).
|
|
258
258
|
|
|
@@ -265,8 +265,9 @@ from hexcore.application.cqrs.config import CQRSConfig, BusConfig
|
|
|
265
265
|
config = ServerConfig(
|
|
266
266
|
cqrs=CQRSConfig(
|
|
267
267
|
command_bus=BusConfig(
|
|
268
|
-
#
|
|
269
|
-
|
|
268
|
+
# Sin middlewares por defecto. Los que no necesitan configuración se
|
|
269
|
+
# pueden declarar por dotted path:
|
|
270
|
+
middlewares=["hexcore.infrastructure.cqrs.middlewares.LoggingMiddleware"]
|
|
270
271
|
),
|
|
271
272
|
# Puedes sustituir el backend en memoria por uno distribuido (Ej: Celery, Procrastinate)
|
|
272
273
|
# backend="mi_app.infrastructure.ProcrastinateCommandBus"
|
|
@@ -274,6 +275,15 @@ config = ServerConfig(
|
|
|
274
275
|
)
|
|
275
276
|
```
|
|
276
277
|
|
|
278
|
+
> **`TransactionMiddleware` no es el default.** Comitea después del handler, así que
|
|
279
|
+
> con un handler que ya gestiona su propia transacción comitearías dos veces. Y
|
|
280
|
+
> necesita un `uow_factory` construido con *tu* engine, cosa que no se puede expresar
|
|
281
|
+
> como dotted path: instancialo a mano y pasá el pipeline al bus.
|
|
282
|
+
>
|
|
283
|
+
> ```python
|
|
284
|
+
> TransactionMiddleware(uow_factory=lambda: SqlAlchemyUnitOfWork(session=session_factory()))
|
|
285
|
+
> ```
|
|
286
|
+
|
|
277
287
|
---
|
|
278
288
|
|
|
279
289
|
### Guía de Migración: De Casos de Uso Clásicos a CQRS
|
|
@@ -55,13 +55,12 @@ class CQRSConfig(BaseModel):
|
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
enabled: bool = True
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
58
|
+
# Sin middlewares por defecto (P0-6). `TransactionMiddleware` *era* el default,
|
|
59
|
+
# pero adivinaba la sesión con el session factory interno de HexCore en vez del
|
|
60
|
+
# engine de la aplicación, y comiteaba por segunda vez sobre los handlers que ya
|
|
61
|
+
# gestionan su transacción. Si lo querés, declaralo explícitamente con su
|
|
62
|
+
# `uow_factory`.
|
|
63
|
+
command_bus: BusConfig = Field(default_factory=BusConfig)
|
|
65
64
|
query_bus: BusConfig = Field(default_factory=BusConfig)
|
|
66
65
|
event_bus: BusConfig = Field(default_factory=BusConfig)
|
|
67
66
|
serializer: t.Optional[str] = None # None = PydanticSerializer por defecto
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory para construir buses CQRS a partir de la configuración.
|
|
3
|
+
Resuelve backends, serializers y middlewares por dotted path.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import typing as t
|
|
9
|
+
|
|
10
|
+
from hexcore.domain.cqrs.buses import ICommandBus, IQueryBus, IEventBus
|
|
11
|
+
from hexcore.domain.cqrs.middleware import IMiddleware
|
|
12
|
+
from hexcore.domain.cqrs.serializer import ISerializer
|
|
13
|
+
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
14
|
+
|
|
15
|
+
from .config import CQRSConfig, BusConfig
|
|
16
|
+
from .registry import HandlerRegistry
|
|
17
|
+
from .pipeline import MiddlewarePipeline
|
|
18
|
+
from .in_memory_buses import InMemoryCommandBus, InMemoryQueryBus, InMemoryEventBus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _import_class(dotted_path: str) -> type:
|
|
22
|
+
"""Importa una clase a partir de su dotted path."""
|
|
23
|
+
module_path, class_name = dotted_path.rsplit(".", 1)
|
|
24
|
+
module = importlib.import_module(module_path)
|
|
25
|
+
return getattr(module, class_name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_middlewares(dotted_paths: list[str]) -> list[IMiddleware]:
|
|
29
|
+
"""
|
|
30
|
+
Instancia middlewares a partir de sus dotted paths.
|
|
31
|
+
|
|
32
|
+
Sólo sirve para middlewares construibles sin argumentos. Los que necesitan
|
|
33
|
+
configuración (p. ej. `TransactionMiddleware`, que requiere un `uow_factory`
|
|
34
|
+
atado al engine de la aplicación) hay que instanciarlos a mano y pasar el
|
|
35
|
+
`MiddlewarePipeline` al bus.
|
|
36
|
+
"""
|
|
37
|
+
middlewares: list[IMiddleware] = []
|
|
38
|
+
for path in dotted_paths:
|
|
39
|
+
cls = _import_class(path)
|
|
40
|
+
try:
|
|
41
|
+
middlewares.append(cls())
|
|
42
|
+
except TypeError as exc:
|
|
43
|
+
raise TypeError(
|
|
44
|
+
f"El middleware '{path}' no se puede construir sin argumentos: {exc}. "
|
|
45
|
+
"Declararlo por dotted path sólo funciona para middlewares sin "
|
|
46
|
+
"configuración; instancialo a mano y pasá el MiddlewarePipeline al bus."
|
|
47
|
+
) from exc
|
|
48
|
+
except ValueError as exc:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"El middleware '{path}' rechazó su construcción por defecto: {exc} "
|
|
51
|
+
"Declararlo por dotted path sólo funciona para middlewares sin "
|
|
52
|
+
"configuración; instancialo a mano y pasá el MiddlewarePipeline al bus."
|
|
53
|
+
) from exc
|
|
54
|
+
return middlewares
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_pipeline(bus_config: BusConfig) -> MiddlewarePipeline:
|
|
58
|
+
"""Construye un MiddlewarePipeline desde la configuración de un bus."""
|
|
59
|
+
middlewares = _build_middlewares(bus_config.middlewares)
|
|
60
|
+
return MiddlewarePipeline(middlewares)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CQRSFactory:
|
|
64
|
+
"""
|
|
65
|
+
Factory que construye las instancias de buses CQRS.
|
|
66
|
+
|
|
67
|
+
Usa CQRSConfig para determinar qué implementación de bus instanciar,
|
|
68
|
+
qué middlewares configurar y qué serializer utilizar.
|
|
69
|
+
|
|
70
|
+
Uso::
|
|
71
|
+
|
|
72
|
+
config = CQRSConfig(...)
|
|
73
|
+
registry = HandlerRegistry()
|
|
74
|
+
# ...registrar handlers...
|
|
75
|
+
|
|
76
|
+
factory = CQRSFactory(config, registry, enqueuer=ProcrastinateEnqueuer(app))
|
|
77
|
+
command_bus = factory.create_command_bus()
|
|
78
|
+
query_bus = factory.create_query_bus()
|
|
79
|
+
event_bus = factory.create_event_bus()
|
|
80
|
+
|
|
81
|
+
El `enqueuer` es lo que habilita el Smart Routing: sin él, los buses in-memory
|
|
82
|
+
no pueden enrutar un `@background_command` y la factory lo dice **al construir**,
|
|
83
|
+
no en el primer dispatch.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
config: CQRSConfig,
|
|
89
|
+
registry: HandlerRegistry,
|
|
90
|
+
enqueuer: ITaskEnqueuer | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self._config = config
|
|
93
|
+
self._registry = registry
|
|
94
|
+
self._enqueuer = enqueuer
|
|
95
|
+
self._serializer: ISerializer | None = None
|
|
96
|
+
|
|
97
|
+
def create_serializer(self) -> ISerializer:
|
|
98
|
+
"""
|
|
99
|
+
Crea el serializer configurado (PydanticSerializer por defecto).
|
|
100
|
+
|
|
101
|
+
La instancia se cachea: los buses y el consumer tienen que compartir el
|
|
102
|
+
mismo serializer para que el round-trip por la cola sea coherente.
|
|
103
|
+
"""
|
|
104
|
+
if self._serializer is not None:
|
|
105
|
+
return self._serializer
|
|
106
|
+
|
|
107
|
+
if self._config.serializer:
|
|
108
|
+
cls = _import_class(self._config.serializer)
|
|
109
|
+
self._serializer = t.cast(ISerializer, cls())
|
|
110
|
+
else:
|
|
111
|
+
from hexcore.infrastructure.cqrs.pydantic_serializer import PydanticSerializer
|
|
112
|
+
|
|
113
|
+
self._serializer = PydanticSerializer()
|
|
114
|
+
return self._serializer
|
|
115
|
+
|
|
116
|
+
def create_command_bus(self, **extra_kwargs: t.Any) -> ICommandBus:
|
|
117
|
+
"""
|
|
118
|
+
Crea el CommandBus configurado.
|
|
119
|
+
Si no hay backend explícito, retorna InMemoryCommandBus con el enqueuer y el
|
|
120
|
+
serializer necesarios para el Smart Routing.
|
|
121
|
+
"""
|
|
122
|
+
bus_config = self._config.command_bus
|
|
123
|
+
pipeline = _build_pipeline(bus_config)
|
|
124
|
+
|
|
125
|
+
if bus_config.backend is None:
|
|
126
|
+
self._assert_enqueuer_for_background_commands()
|
|
127
|
+
return InMemoryCommandBus(
|
|
128
|
+
registry=self._registry,
|
|
129
|
+
pipeline=pipeline,
|
|
130
|
+
enqueuer=self._enqueuer,
|
|
131
|
+
serializer=self.create_serializer(),
|
|
132
|
+
**extra_kwargs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Backend personalizado (ej. ProcrastinateCommandBus)
|
|
136
|
+
cls = _import_class(bus_config.backend)
|
|
137
|
+
return cls(
|
|
138
|
+
registry=self._registry,
|
|
139
|
+
serializer=self.create_serializer(),
|
|
140
|
+
pipeline=pipeline,
|
|
141
|
+
**bus_config.options,
|
|
142
|
+
**extra_kwargs,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def create_query_bus(self) -> IQueryBus:
|
|
146
|
+
"""
|
|
147
|
+
Crea el QueryBus configurado.
|
|
148
|
+
Nota: Las queries siempre son síncronas en CQRS puro.
|
|
149
|
+
"""
|
|
150
|
+
bus_config = self._config.query_bus
|
|
151
|
+
pipeline = _build_pipeline(bus_config)
|
|
152
|
+
|
|
153
|
+
if bus_config.backend is None:
|
|
154
|
+
return InMemoryQueryBus(
|
|
155
|
+
registry=self._registry,
|
|
156
|
+
pipeline=pipeline,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
cls = _import_class(bus_config.backend)
|
|
160
|
+
return cls(
|
|
161
|
+
registry=self._registry,
|
|
162
|
+
pipeline=pipeline,
|
|
163
|
+
**bus_config.options,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def create_event_bus(self, **extra_kwargs: t.Any) -> IEventBus:
|
|
167
|
+
"""
|
|
168
|
+
Crea el EventBus configurado.
|
|
169
|
+
|
|
170
|
+
Al bus in-memory se le pasan enqueuer y serializer para que los suscriptores
|
|
171
|
+
marcados con `@background_handler` se puedan enrutar.
|
|
172
|
+
"""
|
|
173
|
+
bus_config = self._config.event_bus
|
|
174
|
+
pipeline = _build_pipeline(bus_config)
|
|
175
|
+
|
|
176
|
+
if bus_config.backend is None:
|
|
177
|
+
return InMemoryEventBus(
|
|
178
|
+
pipeline=pipeline,
|
|
179
|
+
enqueuer=self._enqueuer,
|
|
180
|
+
serializer=self.create_serializer(),
|
|
181
|
+
**extra_kwargs,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
cls = _import_class(bus_config.backend)
|
|
185
|
+
return cls(
|
|
186
|
+
pipeline=pipeline,
|
|
187
|
+
**bus_config.options,
|
|
188
|
+
**extra_kwargs,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ── Validación ────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
def _assert_enqueuer_for_background_commands(self) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Falla al construir si hay commands de background registrados y no hay
|
|
196
|
+
enqueuer. El error en el primer dispatch llega demasiado tarde: para
|
|
197
|
+
entonces la petición del usuario ya está en vuelo.
|
|
198
|
+
"""
|
|
199
|
+
if self._enqueuer is not None:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
background = [
|
|
203
|
+
command_type.__name__
|
|
204
|
+
for command_type in self._registry.registered_commands
|
|
205
|
+
if getattr(command_type, "__cqrs_background__", False)
|
|
206
|
+
]
|
|
207
|
+
if not background:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
"CQRSFactory no tiene 'enqueuer', pero hay commands decorados con "
|
|
212
|
+
f"@background_command: {', '.join(sorted(background))}. "
|
|
213
|
+
"Pasá un ITaskEnqueuer al construir la factory, p. ej. "
|
|
214
|
+
"CQRSFactory(config, registry, enqueuer=ProcrastinateEnqueuer(app))."
|
|
215
|
+
)
|
|
@@ -8,6 +8,7 @@ import logging
|
|
|
8
8
|
|
|
9
9
|
from hexcore.domain.cqrs.buses import ICommandBus, IQueryBus, IEventBus
|
|
10
10
|
from hexcore.domain.cqrs.commands import Command
|
|
11
|
+
from hexcore.domain.cqrs.context import is_worker_execution, local_execution
|
|
11
12
|
from hexcore.domain.cqrs.queries import Query
|
|
12
13
|
from hexcore.domain.events import DomainEvent
|
|
13
14
|
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
@@ -28,6 +29,10 @@ class InMemoryCommandBus(ICommandBus):
|
|
|
28
29
|
Si el comando está decorado con `@background_command` y se provee un `enqueuer`,
|
|
29
30
|
el comando es automáticamente encolado para su ejecución en segundo plano
|
|
30
31
|
sin bloquear el proceso actual (retornando None).
|
|
32
|
+
|
|
33
|
+
Cuando el mensaje viene de un worker (``IN_WORKER`` activo, lo pone el
|
|
34
|
+
``CQRSConsumer``), el bus lo ejecuta **localmente** en vez de reencolarlo.
|
|
35
|
+
Esto permite usar el mismo bus en el proceso web y en el worker.
|
|
31
36
|
"""
|
|
32
37
|
|
|
33
38
|
def __init__(
|
|
@@ -44,17 +49,19 @@ class InMemoryCommandBus(ICommandBus):
|
|
|
44
49
|
|
|
45
50
|
async def dispatch(self, command: Command) -> t.Any:
|
|
46
51
|
cmd_type = type(command)
|
|
47
|
-
|
|
52
|
+
|
|
48
53
|
# 1. Smart Routing: ¿Debe irse a background?
|
|
54
|
+
# Si ya estamos dentro de un worker, el mensaje viene de la cola: hay que
|
|
55
|
+
# ejecutarlo, no volver a encolarlo.
|
|
49
56
|
is_background = getattr(cmd_type, "__cqrs_background__", False)
|
|
50
|
-
|
|
51
|
-
if is_background:
|
|
57
|
+
|
|
58
|
+
if is_background and not is_worker_execution():
|
|
52
59
|
if not self._enqueuer or not self._serializer:
|
|
53
60
|
raise RuntimeError(
|
|
54
61
|
f"El comando '{cmd_type.__name__}' requiere ejecución en background, "
|
|
55
62
|
"pero el InMemoryCommandBus no tiene configurado un 'enqueuer' o 'serializer'."
|
|
56
63
|
)
|
|
57
|
-
|
|
64
|
+
|
|
58
65
|
async def background_dispatcher(cmd: Command) -> None:
|
|
59
66
|
queue_name = getattr(cmd_type, "__cqrs_queue__", "default")
|
|
60
67
|
payload = self._serializer.serialize(cmd) # type: ignore
|
|
@@ -70,7 +77,10 @@ class InMemoryCommandBus(ICommandBus):
|
|
|
70
77
|
async def final_handler(cmd: t.Any) -> t.Any:
|
|
71
78
|
return await handler.handle(cmd)
|
|
72
79
|
|
|
73
|
-
|
|
80
|
+
# `local_execution` consume el flag de worker: si el handler despacha otro
|
|
81
|
+
# `@background_command`, ese sí debe encolarse.
|
|
82
|
+
with local_execution():
|
|
83
|
+
return await self._pipeline.execute(command, final_handler)
|
|
74
84
|
|
|
75
85
|
|
|
76
86
|
class InMemoryQueryBus(IQueryBus):
|
|
@@ -129,9 +139,15 @@ class InMemoryEventBus(IEventBus):
|
|
|
129
139
|
|
|
130
140
|
async def publish(self, event: DomainEvent) -> None:
|
|
131
141
|
handlers = self._handlers.get(type(event), [])
|
|
142
|
+
# Si el evento viene de un worker, sus handlers de background se ejecutan
|
|
143
|
+
# aquí; reencolarlos sería un bucle infinito.
|
|
144
|
+
in_worker = is_worker_execution()
|
|
132
145
|
|
|
133
146
|
for event_handler in handlers:
|
|
134
|
-
is_background =
|
|
147
|
+
is_background = (
|
|
148
|
+
getattr(event_handler, "__cqrs_background_handler__", False)
|
|
149
|
+
and not in_worker
|
|
150
|
+
)
|
|
135
151
|
|
|
136
152
|
if is_background:
|
|
137
153
|
# Enrutamiento hacia background
|
|
@@ -161,4 +177,5 @@ class InMemoryEventBus(IEventBus):
|
|
|
161
177
|
) -> None:
|
|
162
178
|
await _h(evt)
|
|
163
179
|
|
|
164
|
-
|
|
180
|
+
with local_execution():
|
|
181
|
+
await self._pipeline.execute(event, final_handler)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registro central de handlers CQRS.
|
|
3
|
+
Mapea tipos de Command/Query a sus handlers correspondientes.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import typing as t
|
|
9
|
+
|
|
10
|
+
from hexcore.domain.cqrs.commands import Command
|
|
11
|
+
from hexcore.domain.cqrs.queries import Query
|
|
12
|
+
from hexcore.domain.cqrs.handlers import ICommandHandler, IQueryHandler
|
|
13
|
+
from hexcore.domain.cqrs.exceptions import HandlerNotFoundError, DuplicateHandlerError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Tipos para factories de handlers (para DI/lazy instantiation)
|
|
17
|
+
CommandHandlerFactory = t.Callable[[], ICommandHandler[t.Any, t.Any]]
|
|
18
|
+
QueryHandlerFactory = t.Callable[[], IQueryHandler[t.Any, t.Any]]
|
|
19
|
+
|
|
20
|
+
TFactory = t.TypeVar("TFactory")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HandlerFactory(t.Generic[TFactory]):
|
|
24
|
+
"""
|
|
25
|
+
Marcador explícito de "esto es un factory, no un handler".
|
|
26
|
+
|
|
27
|
+
`callable(entry) and not isinstance(entry, ICommandHandler)` es ambiguo: un handler
|
|
28
|
+
que implemente `__call__` se confundiría con un factory, y un factory que herede de
|
|
29
|
+
la interfaz se confundiría con un handler. Envolver el callable elimina la
|
|
30
|
+
heurística.
|
|
31
|
+
|
|
32
|
+
No es obligatorio: registrar un `lambda` sigue funcionando (se detecta por
|
|
33
|
+
heurística, igual que antes) y `HandlerRegistry.factory()` construye el marcador
|
|
34
|
+
por ti.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
__slots__ = ("build",)
|
|
38
|
+
|
|
39
|
+
def __init__(self, build: t.Callable[[], TFactory]) -> None:
|
|
40
|
+
self.build = build
|
|
41
|
+
|
|
42
|
+
def __call__(self) -> TFactory:
|
|
43
|
+
return self.build()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HandlerRegistry:
|
|
47
|
+
"""
|
|
48
|
+
Registro de handlers para Commands y Queries.
|
|
49
|
+
|
|
50
|
+
Soporta dos modos de registro:
|
|
51
|
+
|
|
52
|
+
1. Registro directo de instancias (eager)
|
|
53
|
+
2. Registro de factories (lazy, para DI containers)
|
|
54
|
+
|
|
55
|
+
**Thread-safety.** El registro y la resolución están protegidos por un
|
|
56
|
+
`threading.RLock`. Hace falta porque `resolve_*` hace lazy-init con escritura en el
|
|
57
|
+
dict: sin lock, dos hilos (o dos hilos reales bajo el free-threading de Python
|
|
58
|
+
3.14) pueden instanciar el mismo handler dos veces y quedarse cada uno con la suya.
|
|
59
|
+
|
|
60
|
+
Uso::
|
|
61
|
+
|
|
62
|
+
registry = HandlerRegistry()
|
|
63
|
+
|
|
64
|
+
# Registro directo
|
|
65
|
+
registry.register_command_handler(CreateUserCommand, CreateUserHandler())
|
|
66
|
+
|
|
67
|
+
# Registro con factory (lazy) — marcador explícito
|
|
68
|
+
registry.register_command_handler(
|
|
69
|
+
CreateUserCommand, HandlerRegistry.factory(lambda: container.get(CreateUserHandler))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Resolución
|
|
73
|
+
handler = registry.resolve_command_handler(CreateUserCommand)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, *, allow_override: bool = False) -> None:
|
|
77
|
+
self._command_handlers: dict[
|
|
78
|
+
type[Command], ICommandHandler[t.Any, t.Any] | CommandHandlerFactory
|
|
79
|
+
] = {}
|
|
80
|
+
self._query_handlers: dict[
|
|
81
|
+
type[Query[t.Any]], IQueryHandler[t.Any, t.Any] | QueryHandlerFactory
|
|
82
|
+
] = {}
|
|
83
|
+
self._allow_override = allow_override
|
|
84
|
+
# Reentrante: un factory puede resolver otro handler del mismo registry.
|
|
85
|
+
self._lock = threading.RLock()
|
|
86
|
+
|
|
87
|
+
# ── Factories ─────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def factory(build: t.Callable[[], t.Any]) -> HandlerFactory[t.Any]:
|
|
91
|
+
"""
|
|
92
|
+
Envuelve un callable para registrarlo como factory sin ambigüedad.
|
|
93
|
+
|
|
94
|
+
Preferí esto a pasar el callable pelado cuando tu handler implementa
|
|
95
|
+
`__call__`, porque entonces la heurística no puede distinguirlos.
|
|
96
|
+
"""
|
|
97
|
+
return HandlerFactory(build)
|
|
98
|
+
|
|
99
|
+
# ── Command Handlers ──────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def register_command_handler(
|
|
102
|
+
self,
|
|
103
|
+
command_type: type[Command],
|
|
104
|
+
handler: ICommandHandler[t.Any, t.Any] | CommandHandlerFactory,
|
|
105
|
+
) -> "HandlerRegistry":
|
|
106
|
+
"""Registra un handler (o factory) para un tipo de command. Retorna self para fluent API."""
|
|
107
|
+
with self._lock:
|
|
108
|
+
if not self._allow_override and command_type in self._command_handlers:
|
|
109
|
+
raise DuplicateHandlerError(command_type)
|
|
110
|
+
self._command_handlers[command_type] = handler
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def resolve_command_handler(
|
|
114
|
+
self, command_type: type[Command]
|
|
115
|
+
) -> ICommandHandler[t.Any, t.Any]:
|
|
116
|
+
"""Resuelve el handler para el tipo de command dado."""
|
|
117
|
+
with self._lock:
|
|
118
|
+
entry = self._command_handlers.get(command_type)
|
|
119
|
+
if entry is None:
|
|
120
|
+
raise HandlerNotFoundError(command_type)
|
|
121
|
+
if _is_factory(entry, ICommandHandler):
|
|
122
|
+
# Es un factory, invocar y cachear la instancia
|
|
123
|
+
handler = t.cast(CommandHandlerFactory, entry)()
|
|
124
|
+
self._command_handlers[command_type] = handler
|
|
125
|
+
return handler
|
|
126
|
+
return t.cast(ICommandHandler[t.Any, t.Any], entry)
|
|
127
|
+
|
|
128
|
+
# ── Query Handlers ────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
def register_query_handler(
|
|
131
|
+
self,
|
|
132
|
+
query_type: type[Query[t.Any]],
|
|
133
|
+
handler: IQueryHandler[t.Any, t.Any] | QueryHandlerFactory,
|
|
134
|
+
) -> "HandlerRegistry":
|
|
135
|
+
"""Registra un handler (o factory) para un tipo de query. Retorna self para fluent API."""
|
|
136
|
+
with self._lock:
|
|
137
|
+
if not self._allow_override and query_type in self._query_handlers:
|
|
138
|
+
raise DuplicateHandlerError(query_type)
|
|
139
|
+
self._query_handlers[query_type] = handler
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def resolve_query_handler(
|
|
143
|
+
self, query_type: type[Query[t.Any]]
|
|
144
|
+
) -> IQueryHandler[t.Any, t.Any]:
|
|
145
|
+
"""Resuelve el handler para el tipo de query dado."""
|
|
146
|
+
with self._lock:
|
|
147
|
+
entry = self._query_handlers.get(query_type)
|
|
148
|
+
if entry is None:
|
|
149
|
+
raise HandlerNotFoundError(query_type)
|
|
150
|
+
if _is_factory(entry, IQueryHandler):
|
|
151
|
+
handler = t.cast(QueryHandlerFactory, entry)()
|
|
152
|
+
self._query_handlers[query_type] = handler
|
|
153
|
+
return handler
|
|
154
|
+
return t.cast(IQueryHandler[t.Any, t.Any], entry)
|
|
155
|
+
|
|
156
|
+
# ── Introspección ─────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def registered_commands(self) -> frozenset[type[Command]]:
|
|
160
|
+
"""Retorna los tipos de command registrados."""
|
|
161
|
+
with self._lock:
|
|
162
|
+
return frozenset(self._command_handlers.keys())
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def registered_queries(self) -> frozenset[type[Query[t.Any]]]:
|
|
166
|
+
"""Retorna los tipos de query registrados."""
|
|
167
|
+
with self._lock:
|
|
168
|
+
return frozenset(self._query_handlers.keys())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_factory(entry: t.Any, handler_interface: type) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Decide si `entry` es un factory de handlers o el handler mismo.
|
|
174
|
+
|
|
175
|
+
El marcador `HandlerFactory` es inequívoco. Para los callables pelados se mantiene
|
|
176
|
+
la heurística anterior por retrocompatibilidad, con una comprobación extra: un
|
|
177
|
+
objeto con método `handle` es un handler aunque además sea callable.
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(entry, HandlerFactory):
|
|
180
|
+
return True
|
|
181
|
+
if isinstance(entry, handler_interface):
|
|
182
|
+
return False
|
|
183
|
+
if hasattr(entry, "handle"):
|
|
184
|
+
return False
|
|
185
|
+
return callable(entry)
|