hexcore 2.3.0__tar.gz → 2.5.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.3.0 → hexcore-2.5.0}/PKG-INFO +146 -32
- hexcore-2.3.0/hexcore.egg-info/PKG-INFO → hexcore-2.5.0/README.md +116 -47
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/__init__.py +2 -0
- hexcore-2.5.0/hexcore/application/cqrs/scheduler.py +99 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/__init__.py +5 -0
- hexcore-2.5.0/hexcore/domain/cqrs/cron.py +69 -0
- hexcore-2.5.0/hexcore/infrastructure/cqrs/postgres_bus.py +126 -0
- hexcore-2.5.0/hexcore/infrastructure/cqrs/postgres_lock.py +95 -0
- hexcore-2.5.0/hexcore/infrastructure/cqrs/redis_bus.py +143 -0
- hexcore-2.5.0/hexcore/infrastructure/cqrs/redis_lock.py +62 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/base.py +6 -3
- hexcore-2.5.0/hexcore/infrastructure/repositories/implementations.py +198 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/utils.py +15 -6
- hexcore-2.5.0/hexcore/infrastructure/task_queues/__init__.py +3 -0
- hexcore-2.5.0/hexcore/infrastructure/task_queues/celery_adapter.py +83 -0
- hexcore-2.5.0/hexcore/infrastructure/task_queues/procrastinate_adapter.py +61 -0
- hexcore-2.5.0/hexcore/infrastructure/uow/__init__.py +189 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/types.py +4 -1
- hexcore-2.3.0/README.md → hexcore-2.5.0/hexcore.egg-info/PKG-INFO +161 -21
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore.egg-info/SOURCES.txt +16 -0
- hexcore-2.5.0/hexcore.egg-info/requires.txt +42 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/pyproject.toml +27 -10
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_beanie_query_utils.py +2 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_infrastructure_query_path.py +3 -0
- hexcore-2.5.0/tests/test_optional_dependencies.py +50 -0
- hexcore-2.5.0/tests/test_postgres_bus.py +73 -0
- hexcore-2.5.0/tests/test_postgres_lock.py +77 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_query_field_validation.py +2 -0
- hexcore-2.5.0/tests/test_redis_bus.py +75 -0
- hexcore-2.5.0/tests/test_redis_lock.py +69 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_repositories_utils.py +2 -0
- hexcore-2.5.0/tests/test_scheduler.py +115 -0
- hexcore-2.5.0/tests/test_task_queues_adapters.py +82 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_uow_session_regression.py +2 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_use_cases_query.py +2 -0
- hexcore-2.3.0/hexcore/infrastructure/repositories/implementations.py +0 -212
- hexcore-2.3.0/hexcore/infrastructure/uow/__init__.py +0 -181
- hexcore-2.3.0/hexcore.egg-info/requires.txt +0 -17
- {hexcore-2.3.0 → hexcore-2.5.0}/LICENSE +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/MANIFEST.in +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/__main__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/adapters.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/config.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/factory.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/in_memory_buses.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/pipeline.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/cqrs/registry.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/dtos/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/dtos/query.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/use_cases/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/application/use_cases/query.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/config.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/auth/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/auth/permissions.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/auth/value_objects.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/base.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/buses.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/commands.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/decorators.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/exceptions.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/handlers.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/middleware.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/queries.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/serializer.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/cqrs/task_queues.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/events.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/services.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/domain/uow.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/api/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/api/utils.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cli.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cqrs/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cqrs/middlewares.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/events/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/events/events_backends/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/events/events_backends/memory.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/workers/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/workers/consumer.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore/py.typed +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/scripts/__init__.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/scripts/main.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/setup.cfg +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/conftest.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_basic.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_config_loading.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_cqrs.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_domain_service_query.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_rabbitmq_bus.py +0 -0
- {hexcore-2.3.0 → hexcore-2.5.0}/tests/test_smart_routing.py +0 -0
|
@@ -1,27 +1,46 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexcore
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist:
|
|
11
|
-
Requires-Dist: alembic>=1.16.5
|
|
12
|
-
Requires-Dist: asyncpg>=0.30.0
|
|
13
|
-
Requires-Dist: beanie>=2.0.0
|
|
14
|
-
Requires-Dist: fastapi>=0.116.1
|
|
15
|
-
Requires-Dist: pika>=1.3.2
|
|
10
|
+
Requires-Dist: croniter>=2.0.0
|
|
16
11
|
Requires-Dist: pydantic>=2.11.7
|
|
17
|
-
Requires-Dist: redis>=6.4.0
|
|
18
|
-
Requires-Dist: sqlalchemy>=2.0.43
|
|
19
12
|
Requires-Dist: typer>=0.17.3
|
|
20
13
|
Requires-Dist: ruff>=0.12.11
|
|
21
|
-
Provides-Extra:
|
|
22
|
-
Requires-Dist:
|
|
14
|
+
Provides-Extra: api
|
|
15
|
+
Requires-Dist: fastapi>=0.116.1; extra == "api"
|
|
16
|
+
Provides-Extra: redis
|
|
17
|
+
Requires-Dist: redis>=6.4.0; extra == "redis"
|
|
18
|
+
Provides-Extra: mongo
|
|
19
|
+
Requires-Dist: beanie>=2.0.0; extra == "mongo"
|
|
20
|
+
Provides-Extra: sql
|
|
21
|
+
Requires-Dist: sqlalchemy>=2.0.43; extra == "sql"
|
|
22
|
+
Requires-Dist: alembic>=1.16.5; extra == "sql"
|
|
23
|
+
Requires-Dist: asyncpg>=0.30.0; extra == "sql"
|
|
24
|
+
Requires-Dist: aiosqlite>=0.21.0; extra == "sql"
|
|
23
25
|
Provides-Extra: rabbitmq
|
|
24
26
|
Requires-Dist: aio-pika>=9.4.0; extra == "rabbitmq"
|
|
27
|
+
Requires-Dist: pika>=1.3.2; extra == "rabbitmq"
|
|
28
|
+
Provides-Extra: procrastinate
|
|
29
|
+
Requires-Dist: procrastinate>=3.0.0; extra == "procrastinate"
|
|
30
|
+
Provides-Extra: celery
|
|
31
|
+
Requires-Dist: celery>=5.4.0; extra == "celery"
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: fastapi>=0.116.1; extra == "all"
|
|
34
|
+
Requires-Dist: redis>=6.4.0; extra == "all"
|
|
35
|
+
Requires-Dist: beanie>=2.0.0; extra == "all"
|
|
36
|
+
Requires-Dist: sqlalchemy>=2.0.43; extra == "all"
|
|
37
|
+
Requires-Dist: alembic>=1.16.5; extra == "all"
|
|
38
|
+
Requires-Dist: asyncpg>=0.30.0; extra == "all"
|
|
39
|
+
Requires-Dist: aiosqlite>=0.21.0; extra == "all"
|
|
40
|
+
Requires-Dist: aio-pika>=9.4.0; extra == "all"
|
|
41
|
+
Requires-Dist: pika>=1.3.2; extra == "all"
|
|
42
|
+
Requires-Dist: procrastinate>=3.0.0; extra == "all"
|
|
43
|
+
Requires-Dist: celery>=5.4.0; extra == "all"
|
|
25
44
|
Dynamic: license-file
|
|
26
45
|
|
|
27
46
|
# HexCore [](https://pepy.tech/projects/hexcore)
|
|
@@ -516,23 +535,39 @@ class ProcrastinateEnqueuer(ITaskEnqueuer):
|
|
|
516
535
|
await process_generic_task.defer_async(task_name=task_name, payload=payload)
|
|
517
536
|
```
|
|
518
537
|
|
|
519
|
-
####
|
|
538
|
+
#### 2. Configurar tus Buses con un Adaptador Oficial
|
|
520
539
|
|
|
521
|
-
|
|
540
|
+
HexCore provee adaptadores *plug & play* para **Celery** y **Procrastinate**. Simplemente importa el enqueuer, pásale tu app y configúralo en los buses de memoria.
|
|
522
541
|
|
|
523
|
-
|
|
524
|
-
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus, InMemoryEventBus
|
|
542
|
+
Si además deseas persistencia o distribución de Eventos entre múltiples workers/servidores (Pub/Sub), puedes cambiar el `InMemoryEventBus` por `RedisEventBus`, `PostgresEventBus` o `RabbitMQEventBus`:
|
|
525
543
|
|
|
526
|
-
|
|
544
|
+
```python
|
|
545
|
+
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus
|
|
546
|
+
from hexcore.infrastructure.task_queues.celery_adapter import CeleryEnqueuer
|
|
547
|
+
from hexcore.infrastructure.cqrs.redis_bus import RedisEventBus
|
|
548
|
+
from celery import Celery
|
|
549
|
+
import redis.asyncio as redis
|
|
550
|
+
|
|
551
|
+
# 1. Adaptador de Task Queue (Para Comandos asíncronos y Event Handlers asíncronos)
|
|
552
|
+
app = Celery("my_app", broker="redis://localhost:6379/0")
|
|
553
|
+
enqueuer = CeleryEnqueuer(app)
|
|
527
554
|
serializer = PydanticSerializer()
|
|
528
555
|
|
|
529
|
-
# El bus evalúa: ¿Tiene el comando @background_command? Si es así, usa el enqueuer.
|
|
530
556
|
command_bus = InMemoryCommandBus(registry=registry, enqueuer=enqueuer, serializer=serializer)
|
|
531
557
|
|
|
532
|
-
#
|
|
533
|
-
|
|
558
|
+
# 2. Event Bus (Para enviar los Eventos por la red)
|
|
559
|
+
redis_client = redis.from_url("redis://localhost:6379/0")
|
|
560
|
+
event_bus = RedisEventBus(
|
|
561
|
+
redis_client=redis_client,
|
|
562
|
+
serializer=serializer,
|
|
563
|
+
stream_name="hexcore:events",
|
|
564
|
+
group_name="api_workers",
|
|
565
|
+
enqueuer=enqueuer # <-- Importante para inyectarle la habilidad de Smart Routing
|
|
566
|
+
)
|
|
534
567
|
```
|
|
535
568
|
|
|
569
|
+
> **Tip:** También dispones de `PostgresEventBus(pool, serializer, channel_name)` que usa `LISTEN/NOTIFY` nativo si quieres 0 dependencias externas aparte de tu BD de siempre.
|
|
570
|
+
|
|
536
571
|
#### 4. Ejecutar tareas genéricas
|
|
537
572
|
|
|
538
573
|
Para encolar la tarea genérica (`@background_task`), la llamas indirectamente pasándola por el enqueuer:
|
|
@@ -548,10 +583,11 @@ await enqueuer.enqueue_task(
|
|
|
548
583
|
|
|
549
584
|
#### 5. Levantar el Worker (Consumidor Universal)
|
|
550
585
|
|
|
551
|
-
En el entrypoint de tu worker
|
|
586
|
+
En el entrypoint de tu worker, usa la función utilitaria `register_hexcore_celery_tasks` para autoconfigurar las rutas en una sola línea:
|
|
552
587
|
|
|
553
588
|
```python
|
|
554
589
|
from hexcore.infrastructure.workers.consumer import CQRSConsumer
|
|
590
|
+
from hexcore.infrastructure.task_queues.celery_adapter import register_hexcore_celery_tasks
|
|
555
591
|
|
|
556
592
|
consumer = CQRSConsumer(
|
|
557
593
|
command_bus=command_bus, # Tu CommandBus configurado
|
|
@@ -559,20 +595,98 @@ consumer = CQRSConsumer(
|
|
|
559
595
|
serializer=serializer
|
|
560
596
|
)
|
|
561
597
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
598
|
+
# ¡Magia! Registra las tareas 'hexcore.process_command', 'hexcore.process_handler', etc.
|
|
599
|
+
register_hexcore_celery_tasks(app, consumer)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Tareas Periódicas Dinámicas (Cronjobs en Caliente)
|
|
605
|
+
|
|
606
|
+
HexCore incluye un **`DynamicScheduler`** que te permite programar tareas (`@background_task`) para que se ejecuten periódicamente. La ventaja clave es que lee la configuración desde un repositorio (como tu Base de Datos), permitiendo activar, desactivar o cambiar los horarios **sin necesidad de reiniciar tus servidores**.
|
|
607
|
+
|
|
608
|
+
### 1. Implementa tu Repositorio
|
|
609
|
+
Implementa `ICronJobRepository` para decirle al Scheduler de dónde leer la configuración (ej. usando SQLAlchemy, MongoDB o Redis):
|
|
610
|
+
|
|
611
|
+
```python
|
|
612
|
+
from hexcore.domain.cqrs.cron import ICronJobRepository, CronJobDefinition
|
|
613
|
+
from datetime import datetime
|
|
614
|
+
|
|
615
|
+
class MiCronRepository(ICronJobRepository):
|
|
616
|
+
async def get_active_jobs(self) -> list[CronJobDefinition]:
|
|
617
|
+
# SELECT * FROM cronjobs WHERE is_active = true
|
|
618
|
+
return [
|
|
619
|
+
CronJobDefinition(
|
|
620
|
+
job_id="1",
|
|
621
|
+
task_name="hexcore.process_task", # o cualquier @background_task
|
|
622
|
+
cron_expression="*/5 * * * *", # cada 5 minutos
|
|
623
|
+
payload={"task_name": "clean_db", "payload": {}}
|
|
624
|
+
)
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
async def update_last_run(self, job_id: str, run_time: datetime) -> None:
|
|
628
|
+
# UPDATE cronjobs SET last_run = run_time WHERE id = job_id
|
|
629
|
+
pass
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### 2. Levanta el Scheduler
|
|
633
|
+
En un proceso en background de tu API o en un microservicio separado, arranca el Scheduler inyectándole tu Enqueuer favorito (Celery, Procrastinate):
|
|
634
|
+
|
|
635
|
+
```python
|
|
636
|
+
from hexcore.application.cqrs.scheduler import DynamicScheduler
|
|
637
|
+
import asyncio
|
|
638
|
+
|
|
639
|
+
async def run_scheduler():
|
|
640
|
+
repo = MiCronRepository()
|
|
641
|
+
scheduler = DynamicScheduler(
|
|
642
|
+
repository=repo,
|
|
643
|
+
enqueuer=enqueuer,
|
|
644
|
+
tick_interval_seconds=60
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
await scheduler.start() # Bucle infinito
|
|
648
|
+
|
|
649
|
+
# En FastAPI puedes usar lifespan para lanzarlo:
|
|
650
|
+
# asyncio.create_task(run_scheduler())
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
El Scheduler evaluará las expresiones (gracias a `croniter`) y delegará la carga pesada al enqueuer. ¡Tu Worker no necesita saber de horarios, solo ejecuta las tareas cuando le llegan!
|
|
654
|
+
|
|
655
|
+
### 3. Distributed Locks (Evitar ejecuciones dobles)
|
|
656
|
+
|
|
657
|
+
Si corres tu aplicación en múltiples contenedores o réplicas (ej. Kubernetes), podrías tener múltiples instancias del `DynamicScheduler` ejecutándose al mismo tiempo. Para evitar que el mismo cronjob se encole dos veces en el mismo minuto, HexCore soporta **Locks Distribuidos**.
|
|
658
|
+
|
|
659
|
+
Puedes inyectar un proveedor de locks (`ILockProvider`) usando Redis o PostgreSQL (si lo usas como tu DB). Al inyectarlo, el Scheduler bloqueará atómicamente la tarea a través de toda tu red.
|
|
660
|
+
|
|
661
|
+
#### Usando Redis
|
|
662
|
+
```python
|
|
663
|
+
from hexcore.infrastructure.cqrs.redis_lock import RedisLockProvider
|
|
664
|
+
import redis.asyncio as redis
|
|
665
|
+
|
|
666
|
+
redis_client = redis.from_url("redis://localhost:6379/0")
|
|
667
|
+
lock_provider = RedisLockProvider(redis_client)
|
|
668
|
+
|
|
669
|
+
scheduler = DynamicScheduler(
|
|
670
|
+
repository=repo,
|
|
671
|
+
enqueuer=enqueuer,
|
|
672
|
+
lock_provider=lock_provider
|
|
673
|
+
)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
#### Usando PostgreSQL (asyncpg)
|
|
677
|
+
Si usas Procrastinate o bases de datos SQL y no quieres levantar Redis:
|
|
566
678
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
# HexCore resuelve y ejecuta exclusivamente el Event Handler asíncrono
|
|
570
|
-
await consumer.process_handler(handler_name, payload)
|
|
679
|
+
```python
|
|
680
|
+
from hexcore.infrastructure.cqrs.postgres_lock import PostgresLockProvider
|
|
571
681
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
682
|
+
lock_provider = PostgresLockProvider(my_asyncpg_pool)
|
|
683
|
+
await lock_provider.setup() # Crea la tabla de locks si no existe
|
|
684
|
+
|
|
685
|
+
scheduler = DynamicScheduler(
|
|
686
|
+
repository=repo,
|
|
687
|
+
enqueuer=enqueuer,
|
|
688
|
+
lock_provider=lock_provider
|
|
689
|
+
)
|
|
576
690
|
```
|
|
577
691
|
|
|
578
692
|
---
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: hexcore
|
|
3
|
-
Version: 2.3.0
|
|
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
|
-
Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.12
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: aiosqlite>=0.21.0
|
|
11
|
-
Requires-Dist: alembic>=1.16.5
|
|
12
|
-
Requires-Dist: asyncpg>=0.30.0
|
|
13
|
-
Requires-Dist: beanie>=2.0.0
|
|
14
|
-
Requires-Dist: fastapi>=0.116.1
|
|
15
|
-
Requires-Dist: pika>=1.3.2
|
|
16
|
-
Requires-Dist: pydantic>=2.11.7
|
|
17
|
-
Requires-Dist: redis>=6.4.0
|
|
18
|
-
Requires-Dist: sqlalchemy>=2.0.43
|
|
19
|
-
Requires-Dist: typer>=0.17.3
|
|
20
|
-
Requires-Dist: ruff>=0.12.11
|
|
21
|
-
Provides-Extra: procrastinate
|
|
22
|
-
Requires-Dist: procrastinate>=3.0.0; extra == "procrastinate"
|
|
23
|
-
Provides-Extra: rabbitmq
|
|
24
|
-
Requires-Dist: aio-pika>=9.4.0; extra == "rabbitmq"
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
|
|
27
1
|
# HexCore [](https://pepy.tech/projects/hexcore)
|
|
28
2
|
HexCore es un módulo base reutilizable para proyectos Python que implementan arquitectura hexagonal y event handling.
|
|
29
3
|
|
|
@@ -516,23 +490,39 @@ class ProcrastinateEnqueuer(ITaskEnqueuer):
|
|
|
516
490
|
await process_generic_task.defer_async(task_name=task_name, payload=payload)
|
|
517
491
|
```
|
|
518
492
|
|
|
519
|
-
####
|
|
493
|
+
#### 2. Configurar tus Buses con un Adaptador Oficial
|
|
520
494
|
|
|
521
|
-
|
|
495
|
+
HexCore provee adaptadores *plug & play* para **Celery** y **Procrastinate**. Simplemente importa el enqueuer, pásale tu app y configúralo en los buses de memoria.
|
|
522
496
|
|
|
523
|
-
|
|
524
|
-
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus, InMemoryEventBus
|
|
497
|
+
Si además deseas persistencia o distribución de Eventos entre múltiples workers/servidores (Pub/Sub), puedes cambiar el `InMemoryEventBus` por `RedisEventBus`, `PostgresEventBus` o `RabbitMQEventBus`:
|
|
525
498
|
|
|
526
|
-
|
|
499
|
+
```python
|
|
500
|
+
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus
|
|
501
|
+
from hexcore.infrastructure.task_queues.celery_adapter import CeleryEnqueuer
|
|
502
|
+
from hexcore.infrastructure.cqrs.redis_bus import RedisEventBus
|
|
503
|
+
from celery import Celery
|
|
504
|
+
import redis.asyncio as redis
|
|
505
|
+
|
|
506
|
+
# 1. Adaptador de Task Queue (Para Comandos asíncronos y Event Handlers asíncronos)
|
|
507
|
+
app = Celery("my_app", broker="redis://localhost:6379/0")
|
|
508
|
+
enqueuer = CeleryEnqueuer(app)
|
|
527
509
|
serializer = PydanticSerializer()
|
|
528
510
|
|
|
529
|
-
# El bus evalúa: ¿Tiene el comando @background_command? Si es así, usa el enqueuer.
|
|
530
511
|
command_bus = InMemoryCommandBus(registry=registry, enqueuer=enqueuer, serializer=serializer)
|
|
531
512
|
|
|
532
|
-
#
|
|
533
|
-
|
|
513
|
+
# 2. Event Bus (Para enviar los Eventos por la red)
|
|
514
|
+
redis_client = redis.from_url("redis://localhost:6379/0")
|
|
515
|
+
event_bus = RedisEventBus(
|
|
516
|
+
redis_client=redis_client,
|
|
517
|
+
serializer=serializer,
|
|
518
|
+
stream_name="hexcore:events",
|
|
519
|
+
group_name="api_workers",
|
|
520
|
+
enqueuer=enqueuer # <-- Importante para inyectarle la habilidad de Smart Routing
|
|
521
|
+
)
|
|
534
522
|
```
|
|
535
523
|
|
|
524
|
+
> **Tip:** También dispones de `PostgresEventBus(pool, serializer, channel_name)` que usa `LISTEN/NOTIFY` nativo si quieres 0 dependencias externas aparte de tu BD de siempre.
|
|
525
|
+
|
|
536
526
|
#### 4. Ejecutar tareas genéricas
|
|
537
527
|
|
|
538
528
|
Para encolar la tarea genérica (`@background_task`), la llamas indirectamente pasándola por el enqueuer:
|
|
@@ -548,10 +538,11 @@ await enqueuer.enqueue_task(
|
|
|
548
538
|
|
|
549
539
|
#### 5. Levantar el Worker (Consumidor Universal)
|
|
550
540
|
|
|
551
|
-
En el entrypoint de tu worker
|
|
541
|
+
En el entrypoint de tu worker, usa la función utilitaria `register_hexcore_celery_tasks` para autoconfigurar las rutas en una sola línea:
|
|
552
542
|
|
|
553
543
|
```python
|
|
554
544
|
from hexcore.infrastructure.workers.consumer import CQRSConsumer
|
|
545
|
+
from hexcore.infrastructure.task_queues.celery_adapter import register_hexcore_celery_tasks
|
|
555
546
|
|
|
556
547
|
consumer = CQRSConsumer(
|
|
557
548
|
command_bus=command_bus, # Tu CommandBus configurado
|
|
@@ -559,20 +550,98 @@ consumer = CQRSConsumer(
|
|
|
559
550
|
serializer=serializer
|
|
560
551
|
)
|
|
561
552
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
553
|
+
# ¡Magia! Registra las tareas 'hexcore.process_command', 'hexcore.process_handler', etc.
|
|
554
|
+
register_hexcore_celery_tasks(app, consumer)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## Tareas Periódicas Dinámicas (Cronjobs en Caliente)
|
|
560
|
+
|
|
561
|
+
HexCore incluye un **`DynamicScheduler`** que te permite programar tareas (`@background_task`) para que se ejecuten periódicamente. La ventaja clave es que lee la configuración desde un repositorio (como tu Base de Datos), permitiendo activar, desactivar o cambiar los horarios **sin necesidad de reiniciar tus servidores**.
|
|
566
562
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
# HexCore resuelve y ejecuta exclusivamente el Event Handler asíncrono
|
|
570
|
-
await consumer.process_handler(handler_name, payload)
|
|
563
|
+
### 1. Implementa tu Repositorio
|
|
564
|
+
Implementa `ICronJobRepository` para decirle al Scheduler de dónde leer la configuración (ej. usando SQLAlchemy, MongoDB o Redis):
|
|
571
565
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
566
|
+
```python
|
|
567
|
+
from hexcore.domain.cqrs.cron import ICronJobRepository, CronJobDefinition
|
|
568
|
+
from datetime import datetime
|
|
569
|
+
|
|
570
|
+
class MiCronRepository(ICronJobRepository):
|
|
571
|
+
async def get_active_jobs(self) -> list[CronJobDefinition]:
|
|
572
|
+
# SELECT * FROM cronjobs WHERE is_active = true
|
|
573
|
+
return [
|
|
574
|
+
CronJobDefinition(
|
|
575
|
+
job_id="1",
|
|
576
|
+
task_name="hexcore.process_task", # o cualquier @background_task
|
|
577
|
+
cron_expression="*/5 * * * *", # cada 5 minutos
|
|
578
|
+
payload={"task_name": "clean_db", "payload": {}}
|
|
579
|
+
)
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
async def update_last_run(self, job_id: str, run_time: datetime) -> None:
|
|
583
|
+
# UPDATE cronjobs SET last_run = run_time WHERE id = job_id
|
|
584
|
+
pass
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### 2. Levanta el Scheduler
|
|
588
|
+
En un proceso en background de tu API o en un microservicio separado, arranca el Scheduler inyectándole tu Enqueuer favorito (Celery, Procrastinate):
|
|
589
|
+
|
|
590
|
+
```python
|
|
591
|
+
from hexcore.application.cqrs.scheduler import DynamicScheduler
|
|
592
|
+
import asyncio
|
|
593
|
+
|
|
594
|
+
async def run_scheduler():
|
|
595
|
+
repo = MiCronRepository()
|
|
596
|
+
scheduler = DynamicScheduler(
|
|
597
|
+
repository=repo,
|
|
598
|
+
enqueuer=enqueuer,
|
|
599
|
+
tick_interval_seconds=60
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
await scheduler.start() # Bucle infinito
|
|
603
|
+
|
|
604
|
+
# En FastAPI puedes usar lifespan para lanzarlo:
|
|
605
|
+
# asyncio.create_task(run_scheduler())
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
El Scheduler evaluará las expresiones (gracias a `croniter`) y delegará la carga pesada al enqueuer. ¡Tu Worker no necesita saber de horarios, solo ejecuta las tareas cuando le llegan!
|
|
609
|
+
|
|
610
|
+
### 3. Distributed Locks (Evitar ejecuciones dobles)
|
|
611
|
+
|
|
612
|
+
Si corres tu aplicación en múltiples contenedores o réplicas (ej. Kubernetes), podrías tener múltiples instancias del `DynamicScheduler` ejecutándose al mismo tiempo. Para evitar que el mismo cronjob se encole dos veces en el mismo minuto, HexCore soporta **Locks Distribuidos**.
|
|
613
|
+
|
|
614
|
+
Puedes inyectar un proveedor de locks (`ILockProvider`) usando Redis o PostgreSQL (si lo usas como tu DB). Al inyectarlo, el Scheduler bloqueará atómicamente la tarea a través de toda tu red.
|
|
615
|
+
|
|
616
|
+
#### Usando Redis
|
|
617
|
+
```python
|
|
618
|
+
from hexcore.infrastructure.cqrs.redis_lock import RedisLockProvider
|
|
619
|
+
import redis.asyncio as redis
|
|
620
|
+
|
|
621
|
+
redis_client = redis.from_url("redis://localhost:6379/0")
|
|
622
|
+
lock_provider = RedisLockProvider(redis_client)
|
|
623
|
+
|
|
624
|
+
scheduler = DynamicScheduler(
|
|
625
|
+
repository=repo,
|
|
626
|
+
enqueuer=enqueuer,
|
|
627
|
+
lock_provider=lock_provider
|
|
628
|
+
)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
#### Usando PostgreSQL (asyncpg)
|
|
632
|
+
Si usas Procrastinate o bases de datos SQL y no quieres levantar Redis:
|
|
633
|
+
|
|
634
|
+
```python
|
|
635
|
+
from hexcore.infrastructure.cqrs.postgres_lock import PostgresLockProvider
|
|
636
|
+
|
|
637
|
+
lock_provider = PostgresLockProvider(my_asyncpg_pool)
|
|
638
|
+
await lock_provider.setup() # Crea la tabla de locks si no existe
|
|
639
|
+
|
|
640
|
+
scheduler = DynamicScheduler(
|
|
641
|
+
repository=repo,
|
|
642
|
+
enqueuer=enqueuer,
|
|
643
|
+
lock_provider=lock_provider
|
|
644
|
+
)
|
|
576
645
|
```
|
|
577
646
|
|
|
578
647
|
---
|
|
@@ -9,6 +9,7 @@ from .pipeline import MiddlewarePipeline
|
|
|
9
9
|
from .in_memory_buses import InMemoryCommandBus, InMemoryQueryBus, InMemoryEventBus
|
|
10
10
|
from .adapters import UseCaseCommandHandler
|
|
11
11
|
from .factory import CQRSFactory
|
|
12
|
+
from .scheduler import DynamicScheduler
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"HandlerRegistry",
|
|
@@ -21,4 +22,5 @@ __all__ = [
|
|
|
21
22
|
"InMemoryEventBus",
|
|
22
23
|
"UseCaseCommandHandler",
|
|
23
24
|
"CQRSFactory",
|
|
25
|
+
"DynamicScheduler",
|
|
24
26
|
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Servicio encargado de evaluar y encolar tareas periódicas.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from hexcore.domain.cqrs.cron import ICronJobRepository, ILockProvider
|
|
11
|
+
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DynamicScheduler:
|
|
17
|
+
"""
|
|
18
|
+
Evalúa constantemente un repositorio de tareas periódicas y delega la ejecución
|
|
19
|
+
al TaskEnqueuer (Celery/Procrastinate). Permite cambiar configuraciones cron en "caliente".
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
repository: ICronJobRepository,
|
|
25
|
+
enqueuer: ITaskEnqueuer,
|
|
26
|
+
lock_provider: ILockProvider | None = None,
|
|
27
|
+
tick_interval_seconds: int = 30
|
|
28
|
+
) -> None:
|
|
29
|
+
self.repository = repository
|
|
30
|
+
self.enqueuer = enqueuer
|
|
31
|
+
self.lock_provider = lock_provider
|
|
32
|
+
self.tick_interval_seconds = tick_interval_seconds
|
|
33
|
+
|
|
34
|
+
self._stop_event = asyncio.Event()
|
|
35
|
+
|
|
36
|
+
async def start(self) -> None:
|
|
37
|
+
"""Inicia el bucle infinito del scheduler."""
|
|
38
|
+
import croniter
|
|
39
|
+
|
|
40
|
+
logger.info(f"[*] DynamicScheduler started (Tick interval: {self.tick_interval_seconds}s)")
|
|
41
|
+
self._stop_event.clear()
|
|
42
|
+
|
|
43
|
+
# El estado base es el momento en que se levantó el scheduler.
|
|
44
|
+
last_check_time = datetime.now(timezone.utc)
|
|
45
|
+
|
|
46
|
+
while not self._stop_event.is_set():
|
|
47
|
+
try:
|
|
48
|
+
# 1. Esperamos el tiempo del tick
|
|
49
|
+
await asyncio.sleep(self.tick_interval_seconds)
|
|
50
|
+
|
|
51
|
+
current_time = datetime.now(timezone.utc)
|
|
52
|
+
|
|
53
|
+
# 2. Obtenemos la configuración fresca de BD
|
|
54
|
+
active_jobs = await self.repository.get_active_jobs()
|
|
55
|
+
|
|
56
|
+
for job in active_jobs:
|
|
57
|
+
# Determinamos desde cuándo chequear. Si el job tiene `last_run_at`, lo usamos.
|
|
58
|
+
# Sino, usamos el `last_check_time` global del bucle.
|
|
59
|
+
base_time = job.last_run_at or last_check_time
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# 3. Comprobamos si la expresión cron cayó entre base_time y current_time
|
|
63
|
+
if croniter.croniter.match(job.cron_expression, current_time):
|
|
64
|
+
# Evitar múltiples encolados en el mismo tick (minuto)
|
|
65
|
+
# Intentar adquirir lock si hay proveedor (Distributed Locks)
|
|
66
|
+
if self.lock_provider:
|
|
67
|
+
# Usamos el minuto actual (truncado) para evitar colisiones distribuidas
|
|
68
|
+
minute_key = current_time.replace(second=0, microsecond=0).isoformat()
|
|
69
|
+
lock_key = f"hexcore:cron_lock:{job.job_id}:{minute_key}"
|
|
70
|
+
|
|
71
|
+
lock_acquired = await self.lock_provider.acquire_lock(lock_key, ttl_seconds=60)
|
|
72
|
+
if not lock_acquired:
|
|
73
|
+
logger.debug(f"CronJob '{job.job_id}' ignorado (lock adquirido por otra réplica).")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
logger.info(f"Encolando CronJob '{job.job_id}' -> Task: {job.task_name}")
|
|
77
|
+
|
|
78
|
+
await self.enqueuer.enqueue_task(
|
|
79
|
+
task_name=job.task_name,
|
|
80
|
+
payload=job.payload,
|
|
81
|
+
queue=job.queue
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Actualizamos el timestamp en el repositorio
|
|
85
|
+
await self.repository.update_last_run(job.job_id, current_time)
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Error procesando el cron job {job.job_id}: {e}")
|
|
89
|
+
|
|
90
|
+
last_check_time = current_time
|
|
91
|
+
|
|
92
|
+
except asyncio.CancelledError:
|
|
93
|
+
break
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Error crítico en el bucle del scheduler: {e}")
|
|
96
|
+
|
|
97
|
+
def stop(self) -> None:
|
|
98
|
+
"""Detiene el bucle del scheduler de forma segura."""
|
|
99
|
+
self._stop_event.set()
|
|
@@ -21,6 +21,7 @@ from .handlers import ICommandHandler, IQueryHandler
|
|
|
21
21
|
from .buses import ICommandBus, IQueryBus, IEventBus
|
|
22
22
|
from .middleware import IMiddleware
|
|
23
23
|
from .serializer import ISerializer
|
|
24
|
+
from .cron import CronJobDefinition, ICronJobRepository, ILockProvider
|
|
24
25
|
|
|
25
26
|
__all__ = [
|
|
26
27
|
# Nombres canónicos (Abstract*)
|
|
@@ -34,6 +35,10 @@ __all__ = [
|
|
|
34
35
|
"AbstractMiddleware",
|
|
35
36
|
"NextHandler",
|
|
36
37
|
"AbstractSerializer",
|
|
38
|
+
# Cron
|
|
39
|
+
"CronJobDefinition",
|
|
40
|
+
"ICronJobRepository",
|
|
41
|
+
"ILockProvider",
|
|
37
42
|
# Aliases (I*)
|
|
38
43
|
"ICommandHandler",
|
|
39
44
|
"IQueryHandler",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Definiciones de Dominio para el agendamiento dinámico de Tareas Periódicas (Cronjobs).
|
|
3
|
+
"""
|
|
4
|
+
import abc
|
|
5
|
+
import typing as t
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CronJobDefinition:
|
|
12
|
+
"""
|
|
13
|
+
Representa la definición de una tarea periódica que debe ejecutarse.
|
|
14
|
+
Esta configuración normalmente se guarda en una BD para permitir
|
|
15
|
+
cambios "en caliente" (hot reload).
|
|
16
|
+
"""
|
|
17
|
+
job_id: str
|
|
18
|
+
task_name: str
|
|
19
|
+
cron_expression: str
|
|
20
|
+
payload: dict[str, t.Any] = field(default_factory=dict)
|
|
21
|
+
queue: str = "default"
|
|
22
|
+
is_active: bool = True
|
|
23
|
+
|
|
24
|
+
# Para control de estado si se usa sin locks distribuidos
|
|
25
|
+
last_run_at: datetime | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ICronJobRepository(abc.ABC):
|
|
29
|
+
"""
|
|
30
|
+
Repositorio que el usuario debe implementar (ej. usando SQLAlchemy, Beanie, etc.)
|
|
31
|
+
para proveer al DynamicScheduler la lista actualizada de tareas a ejecutar.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
async def get_active_jobs(self) -> list[CronJobDefinition]:
|
|
36
|
+
"""
|
|
37
|
+
Retorna la lista de trabajos activos.
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
async def update_last_run(self, job_id: str, run_time: datetime) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Actualiza la fecha de última ejecución de un job para evitar
|
|
45
|
+
que se ejecute repetidamente en el mismo tick.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ILockProvider(abc.ABC):
|
|
51
|
+
"""
|
|
52
|
+
Proveedor de Locks Distribuidos (ej. Redis, PostgreSQL Advisory Locks).
|
|
53
|
+
Utilizado por el DynamicScheduler para garantizar que múltiples réplicas
|
|
54
|
+
del scheduler no ejecuten el mismo cronjob de manera duplicada.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
async def acquire_lock(self, lock_key: str, ttl_seconds: int) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Intenta adquirir un lock. Retorna True si lo consiguió, False si ya estaba tomado.
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abc.abstractmethod
|
|
65
|
+
async def release_lock(self, lock_key: str) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Libera el lock explícitamente (opcional, ya que el TTL lo hace automáticamente).
|
|
68
|
+
"""
|
|
69
|
+
pass
|