schedq 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schedq/__init__.py +1 -0
- schedq/core.py +100 -0
- schedq-0.0.1.dist-info/METADATA +98 -0
- schedq-0.0.1.dist-info/RECORD +6 -0
- schedq-0.0.1.dist-info/WHEEL +5 -0
- schedq-0.0.1.dist-info/top_level.txt +1 -0
schedq/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import Schedq, Task
|
schedq/core.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
import datetime
|
|
4
|
+
import heapq
|
|
5
|
+
import uuid
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass(order=True)
|
|
9
|
+
class Task:
|
|
10
|
+
exectime: datetime.datetime
|
|
11
|
+
|
|
12
|
+
tid: str = dataclasses.field(compare=False)
|
|
13
|
+
name: str = dataclasses.field(compare=False)
|
|
14
|
+
interval: datetime.timedelta = dataclasses.field(compare=False)
|
|
15
|
+
ispaused: bool = dataclasses.field(compare=False, default=False)
|
|
16
|
+
taskfunc: typing.Callable[..., typing.Coroutine] = dataclasses.field(compare=False)
|
|
17
|
+
|
|
18
|
+
class Schedq:
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.mainloop = None
|
|
21
|
+
self.minheap = []
|
|
22
|
+
self.taskmap = {}
|
|
23
|
+
self.trigger = asyncio.Event()
|
|
24
|
+
|
|
25
|
+
def _add(self, tid: str, name: str, interval: datetime.timedelta, function: typing.Callable[..., typing.Coroutine]):
|
|
26
|
+
newtask = Task(datetime.datetime.now() + interval, tid, name, function, interval)
|
|
27
|
+
|
|
28
|
+
heapq.heappush(self.minheap, newtask)
|
|
29
|
+
self.taskmap[tid] = newtask
|
|
30
|
+
|
|
31
|
+
if self.mainloop:
|
|
32
|
+
self.trigger.set()
|
|
33
|
+
|
|
34
|
+
def flow(self):
|
|
35
|
+
def decorator(func: typing.Callable[..., typing.Coroutine]):
|
|
36
|
+
pass
|
|
37
|
+
return func
|
|
38
|
+
return decorator
|
|
39
|
+
|
|
40
|
+
def task(self, interval: datetime.timedelta, name: typing.Optional[str] = None):
|
|
41
|
+
def decorator(func: typing.Callable[..., typing.Coroutine]):
|
|
42
|
+
self._add(str(uuid.uuid4()), name or func.__name__, interval, func)
|
|
43
|
+
return func
|
|
44
|
+
return decorator
|
|
45
|
+
|
|
46
|
+
def pause(self, tid: str):
|
|
47
|
+
if tid in self.taskmap:
|
|
48
|
+
self.taskmap[tid].ispaused = True
|
|
49
|
+
|
|
50
|
+
def resume(self, tid: str):
|
|
51
|
+
if tid in self.taskmap:
|
|
52
|
+
self.taskmap[tid].ispaused = False
|
|
53
|
+
|
|
54
|
+
def invoke(self, tid: str):
|
|
55
|
+
if tid in self.taskmap:
|
|
56
|
+
self.taskmap[tid].exectime = datetime.datetime.now()
|
|
57
|
+
heapq.heapify(self.minheap)
|
|
58
|
+
|
|
59
|
+
if self.mainloop:
|
|
60
|
+
self.trigger.set()
|
|
61
|
+
|
|
62
|
+
async def start(self):
|
|
63
|
+
self.mainloop = asyncio.get_running_loop()
|
|
64
|
+
|
|
65
|
+
while True:
|
|
66
|
+
self.trigger.clear()
|
|
67
|
+
|
|
68
|
+
if not self.minheap:
|
|
69
|
+
await self.trigger.wait()
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
now = datetime.datetime.now()
|
|
73
|
+
headtask = self.minheap[0]
|
|
74
|
+
|
|
75
|
+
if now >= headtask.exectime:
|
|
76
|
+
task = heapq.heappop(self.minheap)
|
|
77
|
+
|
|
78
|
+
if not task.ispaused:
|
|
79
|
+
eid = str(uuid.uuid4())
|
|
80
|
+
|
|
81
|
+
async def taskrun(t=task, e=eid):
|
|
82
|
+
print(f"[RUNNING] Task: {t.name} - TID: {t.tid} - EID: {e}")
|
|
83
|
+
try:
|
|
84
|
+
await t.taskfunc(t.tid, e, t.name)
|
|
85
|
+
print(f"[SUCCESS] Task: {t.name} - EID: {e}")
|
|
86
|
+
except Exception as err:
|
|
87
|
+
print(f"[FAILED] Task: {t.name} - EID: {e} - Error: {err}")
|
|
88
|
+
|
|
89
|
+
asyncio.create_task(taskrun())
|
|
90
|
+
|
|
91
|
+
task.exectime = now + task.interval
|
|
92
|
+
heapq.heappush(self.minheap, task)
|
|
93
|
+
else:
|
|
94
|
+
leftover = (headtask.exectime - now).total_seconds()
|
|
95
|
+
|
|
96
|
+
if leftover > 0:
|
|
97
|
+
try:
|
|
98
|
+
await asyncio.wait_for(self.trigger.wait(), timeout=leftover)
|
|
99
|
+
except asyncio.TimeoutError:
|
|
100
|
+
pass
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schedq
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Lightweight, high-performance asynchronous task scheduling engine with no external dependencies.
|
|
5
|
+
Author-email: "Élcio M. Fernandes" <elciomfer@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# schedq
|
|
14
|
+
|
|
15
|
+
O **schedq** é um motor de agendamento de tarefas assíncronas em Python que foca em ser **extremamente leve, performático e independente**. Utilizando exclusivamente primitivas nativas da linguagem e estruturas de dados de alta performance, ele elimina a necessidade de infraestruturas pesadas para cenários concorrentes.
|
|
16
|
+
|
|
17
|
+
Inspirado na usabilidade moderna baseada em decoradores (como Prefect) e na eficiência matemática de baixo nível (uso de Filas de Prioridade), o schedq oferece controle total do tempo sem desperdício de CPU.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Recursos Atuais (O que ele já faz)
|
|
22
|
+
|
|
23
|
+
- **Agendamento por Min-Heap:** Organização interna baseada no módulo nativo `heapq` rodando em C. O motor avalia apenas o topo da árvore (O(1)) e dorme o tempo exato até a próxima tarefa, resultando em **0% de uso de CPU** ociosa.
|
|
24
|
+
- **Concorrência Assíncrona:** Construído sobre o `asyncio`. Execuções demoradas são disparadas como _background tasks_, impedindo que uma tarefa lenta atrase o relógio das demais.
|
|
25
|
+
- **Rastreamento por IDs (Observabilidade):** Separação nativa entre **TID** (Task ID, fixo para a definição da tarefa) e **EID** (Execution ID, único para cada ciclo de execução), ideal para estruturação de logs.
|
|
26
|
+
- **Interface Fluida (Decoradores):** Sintaxe amigável e limpa para registro de rotinas com suporte a nomes customizados opcionais.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Roadmap de Evolução (Próximos Passos)
|
|
31
|
+
|
|
32
|
+
Para transformar este motor leve em um orquestrador resiliente e pronto para ambientes críticos de produção, planejamos implementar os seguintes módulos de forma incremental:
|
|
33
|
+
|
|
34
|
+
### 1. Módulo de Persistência (Resiliência)
|
|
35
|
+
|
|
36
|
+
_Atualmente as tarefas vivem apenas na memória volátil do processo._
|
|
37
|
+
|
|
38
|
+
- **Objetivo:** Adicionar adaptadores opcionais para armazenamento de estados (ex: SQLite integrado ou Redis).
|
|
39
|
+
- **Recurso:** Mecanismo de **Misfire** para decidir o que fazer se o servidor reiniciar e perder a janela exata de execução de uma tarefa.
|
|
40
|
+
|
|
41
|
+
### 2. Módulo de Tolerância a Falhas (Retries & Circuit Breaker)
|
|
42
|
+
|
|
43
|
+
_Atualmente Exceptions dentro de uma task somem silenciosamente._
|
|
44
|
+
|
|
45
|
+
- **Objetivo:** Capturar erros em nível de execução sem derrubar o loop principal do motor.
|
|
46
|
+
- **Recurso:** Implementação de políticas de **Exponential Backoff** (tentativas automáticas com espaçamento de tempo crescente) e alertas para falhas definitivas.
|
|
47
|
+
|
|
48
|
+
### 3. Módulo de Controle de Concorrência (Limitação de Instâncias)
|
|
49
|
+
|
|
50
|
+
_Atualmente, se uma tarefa a cada 5s demorar 20s para rodar, o motor criará instâncias paralelas descontroladamente._
|
|
51
|
+
|
|
52
|
+
- **Objetivo:** Introduzir a propriedade `max_instances`.
|
|
53
|
+
- **Recurso:** Permitir que o motor pule (_skip_) ou enfileire o próximo disparo caso a instância anterior da mesma tarefa ainda esteja sendo executada.
|
|
54
|
+
|
|
55
|
+
### 4. Módulo de Controle Dinâmico (Gerenciamento em Runtime)
|
|
56
|
+
|
|
57
|
+
_Atualmente o motor roda em uma caixa preta após o `.start()`._
|
|
58
|
+
|
|
59
|
+
- **Objetivo:** Criar uma API programática para manipulação das tarefas em tempo real.
|
|
60
|
+
- **Recurso:** Métodos como `sched.pause(tid)`, `sched.resume(tid)` e `sched.trigger_now(tid)` para forçar a execução imediata ignorando o relógio.
|
|
61
|
+
|
|
62
|
+
### 5. Expressões Cron e Suporte a Fusos Horários (Timezones)
|
|
63
|
+
|
|
64
|
+
_Atualmente o motor suporta apenas intervalos relativos (`timedelta`)._
|
|
65
|
+
|
|
66
|
+
- **Objetivo:** Integração com parsers de Cron leves para agendamentos em horários humanos específicos (ex: "Toda segunda-feira às 08:00").
|
|
67
|
+
- **Recurso:** Tratamento nativo de Timezones para evitar desvios causados por fusos horários de servidores (UTC) ou horários de verão.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Como Usar (Exemplo de Implementação)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
import datetime
|
|
76
|
+
from scheduler import Scheduler
|
|
77
|
+
|
|
78
|
+
sched = Scheduler()
|
|
79
|
+
|
|
80
|
+
@sched.task(interval=datetime.timedelta(seconds=4), name="Task name")
|
|
81
|
+
async def example(tid: str, eid: str, name: str):
|
|
82
|
+
# Logic here
|
|
83
|
+
await asyncio.sleep(1)
|
|
84
|
+
|
|
85
|
+
async def main():
|
|
86
|
+
await sched.start()
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Diretrizes de Design
|
|
95
|
+
|
|
96
|
+
1. **Zero Bloqueio:** Nenhuma função síncrona ou método (`time.sleep`) deve interceptar o loop principal.
|
|
97
|
+
2. **Dependência Opcional:** Recursos mais pesados (como bancos de dados para persistência) devem ser plugáveis e opcionais para manter o core do motor sempre leve.
|
|
98
|
+
3. **Foco na Developer Experience (DX):** A complexidade matemática e de concorrência deve sempre ficar escondida sob os panos do motor.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
schedq/__init__.py,sha256=G6D3CrqUvylsJ2TYCoOEXU0IPbwZ6aY-sv3Jjnzt4_g,30
|
|
2
|
+
schedq/core.py,sha256=sfvKZWNMqVtZ5V0OGTXSZdYpX2jpss-8d-k_od9XV_8,3380
|
|
3
|
+
schedq-0.0.1.dist-info/METADATA,sha256=Ki0-h00Nsl-JucBWluLYDJ8PUxFDXb-bejZZ4GbnagA,4900
|
|
4
|
+
schedq-0.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
schedq-0.0.1.dist-info/top_level.txt,sha256=u4X1nOTJvpa8XJ9pwEzPwxHkcpyxJFnwKyJAEOggNow,7
|
|
6
|
+
schedq-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
schedq
|