py2autotask 2.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.
- py2autotask/__init__.py +53 -0
- py2autotask/cli.py +388 -0
- py2autotask/task.py +634 -0
- py2autotask-2.0.1.dist-info/METADATA +7 -0
- py2autotask-2.0.1.dist-info/RECORD +7 -0
- py2autotask-2.0.1.dist-info/WHEEL +4 -0
- py2autotask-2.0.1.dist-info/entry_points.txt +2 -0
py2autotask/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# tasks/__init__.py
|
|
2
|
+
|
|
3
|
+
from .task import (
|
|
4
|
+
# Sınıflar
|
|
5
|
+
TaskScheduler,
|
|
6
|
+
TaskDef,
|
|
7
|
+
TaskResult,
|
|
8
|
+
|
|
9
|
+
# Enum'lar
|
|
10
|
+
TaskState,
|
|
11
|
+
TaskPriority,
|
|
12
|
+
|
|
13
|
+
# Global fonksiyonlar (varsayılan scheduler üzerinden)
|
|
14
|
+
task,
|
|
15
|
+
config_task,
|
|
16
|
+
stop_task,
|
|
17
|
+
cont_task,
|
|
18
|
+
restart_task,
|
|
19
|
+
remove_task,
|
|
20
|
+
trigger_task,
|
|
21
|
+
get_scheduler,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "2.0.0"
|
|
25
|
+
__author__ = "Sen"
|
|
26
|
+
__license__ = "MIT"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Sınıflar
|
|
30
|
+
"TaskScheduler",
|
|
31
|
+
"TaskDef",
|
|
32
|
+
"TaskResult",
|
|
33
|
+
|
|
34
|
+
# Enum'lar
|
|
35
|
+
"TaskState",
|
|
36
|
+
"TaskPriority",
|
|
37
|
+
|
|
38
|
+
# API
|
|
39
|
+
"task",
|
|
40
|
+
"config_task",
|
|
41
|
+
"stop_task",
|
|
42
|
+
"cont_task",
|
|
43
|
+
"restart_task",
|
|
44
|
+
"remove_task",
|
|
45
|
+
"trigger_task",
|
|
46
|
+
"get_scheduler",
|
|
47
|
+
|
|
48
|
+
# Meta
|
|
49
|
+
"__version__",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
_sched = get_scheduler()
|
|
53
|
+
_sched.max_concurrent = 20
|
py2autotask/cli.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# tasks/cli.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import signal
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .task import (
|
|
15
|
+
TaskScheduler,
|
|
16
|
+
TaskPriority,
|
|
17
|
+
TaskState,
|
|
18
|
+
get_scheduler,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ─────────────────────────────────────────────────────────────
|
|
23
|
+
# YARDIMCILAR
|
|
24
|
+
# ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
PRIORITY_MAP = {
|
|
27
|
+
"low": TaskPriority.LOW,
|
|
28
|
+
"normal": TaskPriority.NORMAL,
|
|
29
|
+
"high": TaskPriority.HIGH,
|
|
30
|
+
"critical": TaskPriority.CRITICAL,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_code(args) -> str:
|
|
35
|
+
"""--file veya pozisyonel koddan pycode üretir."""
|
|
36
|
+
if args.file:
|
|
37
|
+
path = Path(args.file)
|
|
38
|
+
if not path.exists():
|
|
39
|
+
raise FileNotFoundError(f"File not found: {args.file}")
|
|
40
|
+
return path.read_text(encoding="utf-8")
|
|
41
|
+
if args.code:
|
|
42
|
+
return args.code
|
|
43
|
+
raise ValueError("You must provide --file or inline code.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _setup_logging(level: str):
|
|
47
|
+
logging.basicConfig(
|
|
48
|
+
level=getattr(logging, level.upper(), logging.INFO),
|
|
49
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
50
|
+
datefmt="%H:%M:%S",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_priority(p: str) -> TaskPriority:
|
|
55
|
+
return PRIORITY_MAP.get(p.lower(), TaskPriority.NORMAL)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _print_result(res):
|
|
59
|
+
icon = {
|
|
60
|
+
TaskState.SUCCESS: "✓",
|
|
61
|
+
TaskState.FAILED: "✗",
|
|
62
|
+
TaskState.TIMEOUT: "⏱",
|
|
63
|
+
TaskState.CANCELLED: "⊘",
|
|
64
|
+
TaskState.STOPPED: "■",
|
|
65
|
+
}.get(res.state, "•")
|
|
66
|
+
|
|
67
|
+
print(
|
|
68
|
+
f"{icon} [{res.name}] {res.state.name} "
|
|
69
|
+
f"({res.duration:.3f}s, try={res.attempts}, run#{res.run_index})"
|
|
70
|
+
)
|
|
71
|
+
if res.result is not None:
|
|
72
|
+
print(f" → result: {res.result!r}")
|
|
73
|
+
if res.error is not None:
|
|
74
|
+
print(f" → error: {type(res.error).__name__}: {res.error}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ─────────────────────────────────────────────────────────────
|
|
78
|
+
# KOMUT: run
|
|
79
|
+
# ─────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async def cmd_run(args) -> int:
|
|
82
|
+
scheduler = get_scheduler()
|
|
83
|
+
scheduler.auto_start = True
|
|
84
|
+
|
|
85
|
+
code = _load_code(args)
|
|
86
|
+
name = args.name or "main"
|
|
87
|
+
|
|
88
|
+
scheduler.task(
|
|
89
|
+
name,
|
|
90
|
+
code,
|
|
91
|
+
priority=_parse_priority(args.priority),
|
|
92
|
+
delay=args.delay,
|
|
93
|
+
timeout=args.timeout,
|
|
94
|
+
interval=args.interval,
|
|
95
|
+
max_runs=args.max_runs,
|
|
96
|
+
retries=args.retries,
|
|
97
|
+
retry_delay=args.retry_delay,
|
|
98
|
+
retry_backoff=args.retry_backoff,
|
|
99
|
+
auto_start=True,
|
|
100
|
+
on_success=lambda r: _print_result(r) if args.verbose else None,
|
|
101
|
+
on_failure=lambda r: _print_result(r),
|
|
102
|
+
on_retry=lambda r, a: print(f"↻ [{r.name}] retrying (#{a})"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
td = scheduler.get_task(name)
|
|
106
|
+
print(f"▶ Task started: {name} (id={td.id})")
|
|
107
|
+
|
|
108
|
+
if td._asyncio_task:
|
|
109
|
+
try:
|
|
110
|
+
await td._asyncio_task
|
|
111
|
+
except asyncio.CancelledError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
if td.last_result:
|
|
115
|
+
_print_result(td.last_result)
|
|
116
|
+
|
|
117
|
+
await scheduler.shutdown(timeout=2.0)
|
|
118
|
+
return 0 if (td.last_result and td.last_result.success) else 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_task_config(cfg: Dict[str, Any]) -> Dict[str, Any]:
|
|
123
|
+
return {
|
|
124
|
+
"task_name": cfg["name"],
|
|
125
|
+
"pycode": cfg.get("code") or Path(cfg["file"]).read_text(encoding="utf-8"),
|
|
126
|
+
"priority": _parse_priority(cfg.get("priority", "normal")),
|
|
127
|
+
"delay": cfg.get("delay", 0.0),
|
|
128
|
+
"timeout": cfg.get("timeout"),
|
|
129
|
+
"interval": cfg.get("interval"),
|
|
130
|
+
"max_runs": cfg.get("max_runs"),
|
|
131
|
+
"retries": cfg.get("retries", 0),
|
|
132
|
+
"retry_delay": cfg.get("retry_delay", 1.0),
|
|
133
|
+
"retry_backoff": cfg.get("retry_backoff", 1.0),
|
|
134
|
+
"auto_start": cfg.get("auto_start", True),
|
|
135
|
+
"depends_on": cfg.get("depends_on", []),
|
|
136
|
+
"metadata": cfg.get("metadata", {}),
|
|
137
|
+
"tags": cfg.get("tags", []),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _load_config(path: str) -> List[Dict[str, Any]]:
|
|
142
|
+
p = Path(path)
|
|
143
|
+
if not p.exists():
|
|
144
|
+
raise FileNotFoundError(f"Config not found: {path}")
|
|
145
|
+
|
|
146
|
+
text = p.read_text(encoding="utf-8")
|
|
147
|
+
if p.suffix in (".yaml", ".yml"):
|
|
148
|
+
try:
|
|
149
|
+
import yaml
|
|
150
|
+
except ImportError:
|
|
151
|
+
print("For YAML, install pyyaml: `pip install pyyaml`", file=sys.stderr)
|
|
152
|
+
sys.exit(2)
|
|
153
|
+
data = yaml.safe_load(text)
|
|
154
|
+
else:
|
|
155
|
+
data = json.loads(text)
|
|
156
|
+
|
|
157
|
+
if isinstance(data, dict) and "tasks" in data:
|
|
158
|
+
return data["tasks"]
|
|
159
|
+
if isinstance(data, list):
|
|
160
|
+
return data
|
|
161
|
+
raise ValueError("Config 'tasks:' list or array expected.")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def cmd_batch(args) -> int:
|
|
165
|
+
scheduler = get_scheduler()
|
|
166
|
+
scheduler.auto_start = True
|
|
167
|
+
|
|
168
|
+
tasks = _load_config(args.config)
|
|
169
|
+
print(f"→ {len(tasks)} loading")
|
|
170
|
+
|
|
171
|
+
for cfg in tasks:
|
|
172
|
+
params = _parse_task_config(cfg)
|
|
173
|
+
try:
|
|
174
|
+
scheduler.task(**params)
|
|
175
|
+
print(f" [+] {params['task_name']}")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f" [!] {params['task_name']}: {e}", file=sys.stderr)
|
|
178
|
+
|
|
179
|
+
await scheduler.wait_all(timeout=args.timeout)
|
|
180
|
+
await scheduler.shutdown(timeout=2.0)
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_list(args) -> int:
|
|
185
|
+
tasks = _load_config(args.config)
|
|
186
|
+
print(f"{'NAME':<20} {'INTERVAL':>10} {'TIMEOUT':>10} {'RETRIES':>8} {'PRIORITY':>10}")
|
|
187
|
+
print("-" * 62)
|
|
188
|
+
for cfg in tasks:
|
|
189
|
+
print(
|
|
190
|
+
f"{cfg['name']:<20} "
|
|
191
|
+
f"{str(cfg.get('interval', '-')):>10} "
|
|
192
|
+
f"{str(cfg.get('timeout', '-')):>10} "
|
|
193
|
+
f"{str(cfg.get('retries', 0)):>8} "
|
|
194
|
+
f"{str(cfg.get('priority', 'normal')):>10}"
|
|
195
|
+
)
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ─────────────────────────────────────────────────────────────
|
|
200
|
+
# KOMUT: interactive (REPL benzeri)
|
|
201
|
+
# ─────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
async def cmd_shell(args) -> int:
|
|
204
|
+
scheduler = get_scheduler()
|
|
205
|
+
print("Task Shell — 'help' or 'exit'.")
|
|
206
|
+
|
|
207
|
+
loop = asyncio.get_event_loop()
|
|
208
|
+
stop_flag = {"run": True}
|
|
209
|
+
|
|
210
|
+
def _sig(*_):
|
|
211
|
+
stop_flag["run"] = False
|
|
212
|
+
|
|
213
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
214
|
+
try:
|
|
215
|
+
loop.add_signal_handler(sig, _sig)
|
|
216
|
+
except NotImplementedError:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
while stop_flag["run"]:
|
|
220
|
+
try:
|
|
221
|
+
line = await loop.run_in_executor(None, input, "tasks> ")
|
|
222
|
+
except (EOFError, KeyboardInterrupt):
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
line = line.strip()
|
|
226
|
+
if not line:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
parts = line.split(maxsplit=2)
|
|
230
|
+
cmd = parts[0].lower()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if cmd in ("exit", "quit"):
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
elif cmd == "help":
|
|
237
|
+
print(
|
|
238
|
+
"Komutlar:\n"
|
|
239
|
+
" run <name> <pycode...> create task\n"
|
|
240
|
+
" stop <name> stop\n"
|
|
241
|
+
" cont <name> continue\n"
|
|
242
|
+
" restart <name> shutdown\n"
|
|
243
|
+
" trigger <name> run now\n"
|
|
244
|
+
" remove <name> delete\n"
|
|
245
|
+
" config <name> <k=v> [<k=v>...] re-config\n"
|
|
246
|
+
" list list the tasks\n"
|
|
247
|
+
" results <name> result history\n"
|
|
248
|
+
" summary summary\n"
|
|
249
|
+
" exit exit"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
elif cmd == "run":
|
|
253
|
+
if len(parts) < 3:
|
|
254
|
+
print("Use: run <name> <pycode>")
|
|
255
|
+
continue
|
|
256
|
+
scheduler.task(parts[1], parts[2])
|
|
257
|
+
print(f" [+] {parts[1]}")
|
|
258
|
+
|
|
259
|
+
elif cmd == "stop":
|
|
260
|
+
scheduler.stop_task(parts[1])
|
|
261
|
+
|
|
262
|
+
elif cmd == "cont":
|
|
263
|
+
scheduler.cont_task(parts[1])
|
|
264
|
+
|
|
265
|
+
elif cmd == "restart":
|
|
266
|
+
scheduler.restart_task(parts[1])
|
|
267
|
+
|
|
268
|
+
elif cmd == "trigger":
|
|
269
|
+
scheduler.trigger_task(parts[1])
|
|
270
|
+
|
|
271
|
+
elif cmd == "remove":
|
|
272
|
+
scheduler.remove_task(parts[1])
|
|
273
|
+
|
|
274
|
+
elif cmd == "config":
|
|
275
|
+
if len(parts) < 3:
|
|
276
|
+
print("Use: config <name> k=v ...")
|
|
277
|
+
continue
|
|
278
|
+
kwargs = {}
|
|
279
|
+
for kv in parts[2].split():
|
|
280
|
+
if "=" not in kv:
|
|
281
|
+
continue
|
|
282
|
+
k, v = kv.split("=", 1)
|
|
283
|
+
# tip dönüşümü
|
|
284
|
+
try:
|
|
285
|
+
v = int(v)
|
|
286
|
+
except ValueError:
|
|
287
|
+
try:
|
|
288
|
+
v = float(v)
|
|
289
|
+
except ValueError:
|
|
290
|
+
pass
|
|
291
|
+
kwargs[k] = v
|
|
292
|
+
scheduler.config_task(parts[1], **kwargs)
|
|
293
|
+
|
|
294
|
+
elif cmd == "list":
|
|
295
|
+
for n, s in scheduler.list_tasks().items():
|
|
296
|
+
print(f" {n:<20} {s.name}")
|
|
297
|
+
|
|
298
|
+
elif cmd == "results":
|
|
299
|
+
for r in scheduler.get_results(parts[1]):
|
|
300
|
+
_print_result(r)
|
|
301
|
+
|
|
302
|
+
elif cmd == "summary":
|
|
303
|
+
for k, v in scheduler.summary().items():
|
|
304
|
+
print(f" {k:<10} {v}")
|
|
305
|
+
|
|
306
|
+
else:
|
|
307
|
+
print(f"Unknown command: {cmd}")
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
print(f" [!] {type(e).__name__}: {e}", file=sys.stderr)
|
|
311
|
+
|
|
312
|
+
print("\nClosing...")
|
|
313
|
+
await scheduler.shutdown(timeout=3.0)
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ─────────────────────────────────────────────────────────────
|
|
318
|
+
# PARSER
|
|
319
|
+
# ─────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
322
|
+
p = argparse.ArgumentParser(
|
|
323
|
+
prog="tasks",
|
|
324
|
+
description="Asynchronous task scheduler CLI",
|
|
325
|
+
)
|
|
326
|
+
p.add_argument("--log", default="INFO", help="Log level (DEBUG/INFO/WARNING)")
|
|
327
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
328
|
+
|
|
329
|
+
# ── run ──
|
|
330
|
+
r = sub.add_parser("run", help="Run a single task")
|
|
331
|
+
r.add_argument("name", nargs="?", default="main", help="Task name")
|
|
332
|
+
r.add_argument("code", nargs="?", help="Inline Python code")
|
|
333
|
+
r.add_argument("--file", "-f", help="Python file for execution")
|
|
334
|
+
r.add_argument("--priority", "-p", default="normal",
|
|
335
|
+
choices=list(PRIORITY_MAP.keys()))
|
|
336
|
+
r.add_argument("--delay", type=float, default=0.0)
|
|
337
|
+
r.add_argument("--timeout", type=float, default=None)
|
|
338
|
+
r.add_argument("--interval", type=float, default=None)
|
|
339
|
+
r.add_argument("--max-runs", type=int, default=None)
|
|
340
|
+
r.add_argument("--retries", type=int, default=0)
|
|
341
|
+
r.add_argument("--retry-delay", type=float, default=1.0)
|
|
342
|
+
r.add_argument("--retry-backoff", type=float, default=1.0)
|
|
343
|
+
r.add_argument("--verbose", "-v", action="store_true")
|
|
344
|
+
r.set_defaults(func=cmd_run)
|
|
345
|
+
|
|
346
|
+
# ── batch ──
|
|
347
|
+
b = sub.add_parser("batch", help="Run in batch from config file")
|
|
348
|
+
b.add_argument("config", help="JSON or YAML config path")
|
|
349
|
+
b.add_argument("--timeout", type=float, default=None,
|
|
350
|
+
help="Timeout for all tasks")
|
|
351
|
+
b.set_defaults(func=cmd_batch)
|
|
352
|
+
|
|
353
|
+
# ── list ──
|
|
354
|
+
l = sub.add_parser("list", help="List the tasks")
|
|
355
|
+
l.add_argument("config")
|
|
356
|
+
l.set_defaults(func=cmd_list)
|
|
357
|
+
|
|
358
|
+
# ── shell ──
|
|
359
|
+
sh = sub.add_parser("shell", help="Task Shell")
|
|
360
|
+
sh.set_defaults(func=cmd_shell)
|
|
361
|
+
|
|
362
|
+
return p
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ─────────────────────────────────────────────────────────────
|
|
366
|
+
# MAIN
|
|
367
|
+
# ─────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
370
|
+
parser = build_parser()
|
|
371
|
+
args = parser.parse_args(argv)
|
|
372
|
+
|
|
373
|
+
_setup_logging(args.log)
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
if asyncio.iscoroutinefunction(args.func):
|
|
377
|
+
return asyncio.run(args.func(args))
|
|
378
|
+
return args.func(args)
|
|
379
|
+
except KeyboardInterrupt:
|
|
380
|
+
print("\nCanceled.", file=sys.stderr)
|
|
381
|
+
return 130
|
|
382
|
+
except Exception as e:
|
|
383
|
+
print(f"[!] {type(e).__name__}: {e}", file=sys.stderr)
|
|
384
|
+
return 1
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
sys.exit(main())
|
py2autotask/task.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
# task.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ─────────────────────────────────────────────────────────────
|
|
18
|
+
# ENUM & VERİ SINIFLARI
|
|
19
|
+
# ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
class TaskState(Enum):
|
|
22
|
+
PENDING = auto()
|
|
23
|
+
RUNNING = auto()
|
|
24
|
+
SUCCESS = auto()
|
|
25
|
+
FAILED = auto()
|
|
26
|
+
CANCELLED = auto()
|
|
27
|
+
RETRYING = auto()
|
|
28
|
+
TIMEOUT = auto()
|
|
29
|
+
STOPPED = auto() # stop_task ile durduruldu (devam ettirilebilir)
|
|
30
|
+
PAUSED = auto() # cont_task ile devam ettirilecek
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TaskPriority(Enum):
|
|
34
|
+
LOW = 3
|
|
35
|
+
NORMAL = 2
|
|
36
|
+
HIGH = 1
|
|
37
|
+
CRITICAL = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TaskResult:
|
|
42
|
+
task_id: str
|
|
43
|
+
name: str
|
|
44
|
+
state: TaskState
|
|
45
|
+
result: Any = None
|
|
46
|
+
error: Optional[Exception] = None
|
|
47
|
+
started_at: float = 0.0
|
|
48
|
+
finished_at: float = 0.0
|
|
49
|
+
attempts: int = 0
|
|
50
|
+
run_index: int = 0 # periyodik için kaçıncı çalışma
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def duration(self) -> float:
|
|
54
|
+
return self.finished_at - self.started_at if self.finished_at else 0.0
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def success(self) -> bool:
|
|
58
|
+
return self.state == TaskState.SUCCESS
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ─────────────────────────────────────────────────────────────
|
|
62
|
+
# TASK TANIMI
|
|
63
|
+
# ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class TaskDef:
|
|
67
|
+
"""Bir görevin tüm tanımını ve runtime durumunu tutar."""
|
|
68
|
+
name: str
|
|
69
|
+
code: str # derlenmiş kaynak kod (string)
|
|
70
|
+
func: Callable[..., Awaitable[Any]] # derlenmiş fonksiyon
|
|
71
|
+
|
|
72
|
+
# Zamanlama
|
|
73
|
+
priority: TaskPriority = TaskPriority.NORMAL
|
|
74
|
+
delay: float = 0.0
|
|
75
|
+
timeout: Optional[float] = None
|
|
76
|
+
interval: Optional[float] = None
|
|
77
|
+
max_runs: Optional[int] = None
|
|
78
|
+
retries: int = 0
|
|
79
|
+
retry_delay: float = 1.0
|
|
80
|
+
retry_backoff: float = 1.0
|
|
81
|
+
|
|
82
|
+
# Kontrol
|
|
83
|
+
auto_start: bool = True
|
|
84
|
+
enabled: bool = True # stop_task / cont_task için ana bayrak
|
|
85
|
+
depends_on: List[str] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
# Callback
|
|
88
|
+
on_success: Optional[Callable] = None
|
|
89
|
+
on_failure: Optional[Callable] = None
|
|
90
|
+
on_complete: Optional[Callable] = None
|
|
91
|
+
on_retry: Optional[Callable] = None
|
|
92
|
+
|
|
93
|
+
# Meta
|
|
94
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
95
|
+
tags: List[str] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
# Runtime
|
|
98
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
99
|
+
state: TaskState = TaskState.PENDING
|
|
100
|
+
run_count: int = 0
|
|
101
|
+
last_result: Optional[TaskResult] = None
|
|
102
|
+
|
|
103
|
+
_asyncio_task: Optional[asyncio.Task] = None
|
|
104
|
+
_stop_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
105
|
+
_wake_event: asyncio.Event = field(default_factory=asyncio.Event) # periyodik bekleme için
|
|
106
|
+
_started_at: float = 0.0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ─────────────────────────────────────────────────────────────
|
|
110
|
+
# SCHEDULER
|
|
111
|
+
# ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
class TaskScheduler:
|
|
114
|
+
"""
|
|
115
|
+
Kod string'i ile dinamik task oluşturma + isimle yönetim.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
max_concurrent: int = 10,
|
|
121
|
+
auto_start: bool = True,
|
|
122
|
+
default_timeout: Optional[float] = None,
|
|
123
|
+
namespace: Optional[Dict[str, Any]] = None,
|
|
124
|
+
):
|
|
125
|
+
self.max_concurrent = max_concurrent
|
|
126
|
+
self.auto_start = auto_start
|
|
127
|
+
self.default_timeout = default_timeout
|
|
128
|
+
self.namespace = namespace or {} # kod içinden erişilebilen global isim alanı
|
|
129
|
+
|
|
130
|
+
self._sem = asyncio.Semaphore(max_concurrent)
|
|
131
|
+
self._tasks: Dict[str, TaskDef] = {} # name -> TaskDef
|
|
132
|
+
self._aliases: Dict[str, str] = {} # alias/id -> name
|
|
133
|
+
self._results: Dict[str, List[TaskResult]] = {}
|
|
134
|
+
self._listeners: Dict[str, List[Callable]] = {}
|
|
135
|
+
self._shutdown = False
|
|
136
|
+
|
|
137
|
+
# ─────────────────────────────────────────────────────────
|
|
138
|
+
# KOD DERLEME
|
|
139
|
+
# ─────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
def _compile(self, task_name: str, code: str) -> Callable:
|
|
142
|
+
"""
|
|
143
|
+
String halindeki Python kodunu derleyip callable'a çevirir.
|
|
144
|
+
Kod şu iki formdan biri olabilir:
|
|
145
|
+
1) 'async def foo(...): ...' → fonksiyon adı task_name olarak aranır
|
|
146
|
+
2) direkt ifade / fonksiyon bloğu (son tanımlı 'async def' kullanılır)
|
|
147
|
+
"""
|
|
148
|
+
env: Dict[str, Any] = {
|
|
149
|
+
**self.namespace,
|
|
150
|
+
"asyncio": asyncio,
|
|
151
|
+
"logging": logging,
|
|
152
|
+
"logger": logger,
|
|
153
|
+
"__builtins__": __builtins__,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
compiled = compile(code, f"<task:{task_name}>", "exec")
|
|
158
|
+
exec(compiled, env)
|
|
159
|
+
except SyntaxError as e:
|
|
160
|
+
raise ValueError(f"[{task_name}] Code compilation error: {e}") from e
|
|
161
|
+
|
|
162
|
+
# Fonksiyonu bul: task_name > ilk async callable
|
|
163
|
+
func = env.get(task_name)
|
|
164
|
+
if not (callable(func) and inspect.iscoroutinefunction(func)):
|
|
165
|
+
func = next(
|
|
166
|
+
(v for v in env.values()
|
|
167
|
+
if callable(v) and inspect.iscoroutinefunction(v)),
|
|
168
|
+
None,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if func is None:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"[{task_name}] Code does not contain an 'async def' function."
|
|
174
|
+
)
|
|
175
|
+
return func
|
|
176
|
+
|
|
177
|
+
# ─────────────────────────────────────────────────────────
|
|
178
|
+
# TASK() — YENİ GÖREV OLUŞTUR
|
|
179
|
+
# ─────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
def task(
|
|
182
|
+
self,
|
|
183
|
+
task_name: str,
|
|
184
|
+
pycode: str,
|
|
185
|
+
priority: TaskPriority = TaskPriority.NORMAL,
|
|
186
|
+
delay: float = 0.0,
|
|
187
|
+
timeout: Optional[float] = None,
|
|
188
|
+
interval: Optional[float] = None,
|
|
189
|
+
max_runs: Optional[int] = None,
|
|
190
|
+
retries: int = 0,
|
|
191
|
+
retry_delay: float = 1.0,
|
|
192
|
+
retry_backoff: float = 1.0,
|
|
193
|
+
auto_start: bool = True,
|
|
194
|
+
depends_on: Optional[List[str]] = None,
|
|
195
|
+
on_success: Optional[Callable] = None,
|
|
196
|
+
on_failure: Optional[Callable] = None,
|
|
197
|
+
on_complete: Optional[Callable] = None,
|
|
198
|
+
on_retry: Optional[Callable] = None,
|
|
199
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
200
|
+
tags: Optional[List[str]] = None,
|
|
201
|
+
overwrite: bool = False,
|
|
202
|
+
**extra_context, # kod içine enjekte edilecek ek değişkenler
|
|
203
|
+
) -> TaskDef:
|
|
204
|
+
"""
|
|
205
|
+
Yeni bir task oluşturur ve (auto_start ise) hemen çalıştırır.
|
|
206
|
+
|
|
207
|
+
Örnek:
|
|
208
|
+
sched.task(
|
|
209
|
+
"fetch_users",
|
|
210
|
+
'''
|
|
211
|
+
async def fetch_users(api):
|
|
212
|
+
await asyncio.sleep(1)
|
|
213
|
+
return [{"id": i} for i in range(3)]
|
|
214
|
+
''',
|
|
215
|
+
priority=TaskPriority.HIGH,
|
|
216
|
+
timeout=5, retries=2,
|
|
217
|
+
)
|
|
218
|
+
"""
|
|
219
|
+
if task_name in self._tasks and not overwrite:
|
|
220
|
+
raise ValueError(f"Task '{task_name}' already exists. Use overwrite=True.")
|
|
221
|
+
|
|
222
|
+
# Kodu derle
|
|
223
|
+
if extra_context:
|
|
224
|
+
self.namespace.update(extra_context)
|
|
225
|
+
func = self._compile(task_name, pycode)
|
|
226
|
+
|
|
227
|
+
td = TaskDef(
|
|
228
|
+
name=task_name,
|
|
229
|
+
code=pycode,
|
|
230
|
+
func=func,
|
|
231
|
+
priority=priority,
|
|
232
|
+
delay=delay,
|
|
233
|
+
timeout=timeout if timeout is not None else self.default_timeout,
|
|
234
|
+
interval=interval,
|
|
235
|
+
max_runs=max_runs,
|
|
236
|
+
retries=retries,
|
|
237
|
+
retry_delay=retry_delay,
|
|
238
|
+
retry_backoff=retry_backoff,
|
|
239
|
+
auto_start=auto_start,
|
|
240
|
+
depends_on=depends_on or [],
|
|
241
|
+
on_success=on_success,
|
|
242
|
+
on_failure=on_failure,
|
|
243
|
+
on_complete=on_complete,
|
|
244
|
+
on_retry=on_retry,
|
|
245
|
+
metadata=metadata or {},
|
|
246
|
+
tags=tags or [],
|
|
247
|
+
)
|
|
248
|
+
self._tasks[task_name] = td
|
|
249
|
+
self._results[task_name] = []
|
|
250
|
+
|
|
251
|
+
logger.info(f"[+] Task created: {task_name} ({td.id})")
|
|
252
|
+
|
|
253
|
+
if self.auto_start and auto_start:
|
|
254
|
+
self._spawn(td)
|
|
255
|
+
|
|
256
|
+
return td
|
|
257
|
+
|
|
258
|
+
# ─────────────────────────────────────────────────────────
|
|
259
|
+
# CONFIG_TASK() — RUNTIME KONFİGÜRASYON
|
|
260
|
+
# ─────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
def config_task(
|
|
263
|
+
self,
|
|
264
|
+
task_name: str,
|
|
265
|
+
*,
|
|
266
|
+
pycode: Optional[str] = None,
|
|
267
|
+
priority: Optional[TaskPriority] = None,
|
|
268
|
+
delay: Optional[float] = None,
|
|
269
|
+
timeout: Optional[float] = None,
|
|
270
|
+
interval: Optional[float] = None,
|
|
271
|
+
max_runs: Optional[int] = None,
|
|
272
|
+
retries: Optional[int] = None,
|
|
273
|
+
retry_delay: Optional[float] = None,
|
|
274
|
+
retry_backoff: Optional[float] = None,
|
|
275
|
+
enabled: Optional[bool] = None,
|
|
276
|
+
on_success: Optional[Callable] = None,
|
|
277
|
+
on_failure: Optional[Callable] = None,
|
|
278
|
+
on_complete: Optional[Callable] = None,
|
|
279
|
+
on_retry: Optional[Callable] = None,
|
|
280
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
281
|
+
tags: Optional[List[str]] = None,
|
|
282
|
+
restart: bool = False,
|
|
283
|
+
) -> TaskDef:
|
|
284
|
+
"""
|
|
285
|
+
Mevcut bir task'ı runtime'da yeniden yapılandırır.
|
|
286
|
+
Sadece verilen alanlar güncellenir.
|
|
287
|
+
restart=True ise task yeniden başlatılır (kod değişikliği için gerekli).
|
|
288
|
+
"""
|
|
289
|
+
td = self._require(task_name)
|
|
290
|
+
|
|
291
|
+
if pycode is not None:
|
|
292
|
+
td.code = pycode
|
|
293
|
+
td.func = self._compile(task_name, pycode)
|
|
294
|
+
restart = True
|
|
295
|
+
|
|
296
|
+
if priority is not None: td.priority = priority
|
|
297
|
+
if delay is not None: td.delay = delay
|
|
298
|
+
if timeout is not None: td.timeout = timeout
|
|
299
|
+
if interval is not None: td.interval = interval
|
|
300
|
+
if max_runs is not None: td.max_runs = max_runs
|
|
301
|
+
if retries is not None: td.retries = retries
|
|
302
|
+
if retry_delay is not None: td.retry_delay = retry_delay
|
|
303
|
+
if retry_backoff is not None: td.retry_backoff = retry_backoff
|
|
304
|
+
if enabled is not None: td.enabled = enabled
|
|
305
|
+
if on_success is not None: td.on_success = on_success
|
|
306
|
+
if on_failure is not None: td.on_failure = on_failure
|
|
307
|
+
if on_complete is not None: td.on_complete = on_complete
|
|
308
|
+
if on_retry is not None: td.on_retry = on_retry
|
|
309
|
+
if metadata is not None: td.metadata.update(metadata)
|
|
310
|
+
if tags is not None: td.tags = tags
|
|
311
|
+
|
|
312
|
+
logger.info(f"[~] Task configured: {task_name}")
|
|
313
|
+
|
|
314
|
+
if restart:
|
|
315
|
+
self._restart(td)
|
|
316
|
+
|
|
317
|
+
return td
|
|
318
|
+
|
|
319
|
+
# ─────────────────────────────────────────────────────────
|
|
320
|
+
# STOP_TASK() — periyodik döngüyü durdur (ama task silinmez)
|
|
321
|
+
# ─────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
def stop_task(self, task_name: str, *, wait_current: bool = False) -> bool:
|
|
324
|
+
"""
|
|
325
|
+
Task'ı durdurur.
|
|
326
|
+
- Periyodik task ise: bir sonraki döngüye geçmez.
|
|
327
|
+
- Çalışan task ise: wait_current=False ise iptal eder, True ise mevcut
|
|
328
|
+
çalışmanın bitmesini bekler.
|
|
329
|
+
- Task hâlâ kayıtlı kalır → cont_task ile devam ettirilebilir.
|
|
330
|
+
"""
|
|
331
|
+
td = self._require(task_name)
|
|
332
|
+
|
|
333
|
+
td.enabled = False
|
|
334
|
+
td._stop_event.set()
|
|
335
|
+
td._wake_event.set() # periyodik beklemeyi uyandır
|
|
336
|
+
|
|
337
|
+
at = td._asyncio_task
|
|
338
|
+
if at and not at.done():
|
|
339
|
+
if wait_current:
|
|
340
|
+
td.state = TaskState.STOPPED
|
|
341
|
+
else:
|
|
342
|
+
at.cancel()
|
|
343
|
+
td.state = TaskState.STOPPED
|
|
344
|
+
else:
|
|
345
|
+
td.state = TaskState.STOPPED
|
|
346
|
+
|
|
347
|
+
logger.info(f"[■] Task stopped: {task_name}")
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
# ─────────────────────────────────────────────────────────
|
|
351
|
+
# CONT_TASK() — durdurulan task'ı devam ettir
|
|
352
|
+
# ─────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
def cont_task(self, task_name: str) -> bool:
|
|
355
|
+
"""
|
|
356
|
+
Durdurulmuş task'ı yeniden başlatır (kaldığı yerden değil, baştan).
|
|
357
|
+
Tek seferlik task için de çalışır.
|
|
358
|
+
"""
|
|
359
|
+
td = self._require(task_name)
|
|
360
|
+
|
|
361
|
+
td.enabled = True
|
|
362
|
+
td._stop_event = asyncio.Event()
|
|
363
|
+
td._wake_event = asyncio.Event()
|
|
364
|
+
td.state = TaskState.PENDING
|
|
365
|
+
|
|
366
|
+
self._spawn(td)
|
|
367
|
+
logger.info(f"[▶] Task continued: {task_name}")
|
|
368
|
+
return True
|
|
369
|
+
|
|
370
|
+
# ─────────────────────────────────────────────────────────
|
|
371
|
+
# RESTART / REMOVE / TRIGGER
|
|
372
|
+
# ─────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
def _restart(self, td: TaskDef):
|
|
375
|
+
self.stop_task(td.name)
|
|
376
|
+
td._stop_event = asyncio.Event()
|
|
377
|
+
td._wake_event = asyncio.Event()
|
|
378
|
+
td.run_count = 0
|
|
379
|
+
td.enabled = True
|
|
380
|
+
td.state = TaskState.PENDING
|
|
381
|
+
self._spawn(td)
|
|
382
|
+
|
|
383
|
+
def restart_task(self, task_name: str) -> bool:
|
|
384
|
+
"""Task'ı sıfırdan yeniden başlatır."""
|
|
385
|
+
td = self._require(task_name)
|
|
386
|
+
self._restart(td)
|
|
387
|
+
logger.info(f"[↻] Task restarted: {task_name}")
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
def remove_task(self, task_name: str, *, cancel_running: bool = True) -> bool:
|
|
391
|
+
"""Task'ı tamamen siler."""
|
|
392
|
+
td = self._tasks.get(task_name)
|
|
393
|
+
if td is None:
|
|
394
|
+
return False
|
|
395
|
+
if cancel_running:
|
|
396
|
+
self.stop_task(task_name, wait_current=False)
|
|
397
|
+
self._tasks.pop(task_name, None)
|
|
398
|
+
self._results.pop(task_name, None)
|
|
399
|
+
logger.info(f"[-] Task removed: {task_name}")
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
def trigger_task(self, task_name: str, *args, **kwargs) -> asyncio.Task:
|
|
403
|
+
"""
|
|
404
|
+
Periyodik task'ı beklemeden hemen bir kez çalıştırır
|
|
405
|
+
(ana döngüyü etkilemez).
|
|
406
|
+
"""
|
|
407
|
+
td = self._require(task_name)
|
|
408
|
+
|
|
409
|
+
async def _once():
|
|
410
|
+
return await self._execute_once(td, override_args=args, override_kwargs=kwargs)
|
|
411
|
+
|
|
412
|
+
return asyncio.get_event_loop().create_task(_once())
|
|
413
|
+
|
|
414
|
+
# ─────────────────────────────────────────────────────────
|
|
415
|
+
# DURUM / LİSTE
|
|
416
|
+
# ─────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
def get_task(self, task_name: str) -> Optional[TaskDef]:
|
|
419
|
+
return self._tasks.get(task_name)
|
|
420
|
+
|
|
421
|
+
def list_tasks(self) -> Dict[str, TaskState]:
|
|
422
|
+
return {n: t.state for n, t in self._tasks.items()}
|
|
423
|
+
|
|
424
|
+
def get_results(self, task_name: str) -> List[TaskResult]:
|
|
425
|
+
return list(self._results.get(task_name, []))
|
|
426
|
+
|
|
427
|
+
def summary(self) -> Dict[str, int]:
|
|
428
|
+
s: Dict[str, int] = {}
|
|
429
|
+
for t in self._tasks.values():
|
|
430
|
+
s[t.state.name] = s.get(t.state.name, 0) + 1
|
|
431
|
+
return s
|
|
432
|
+
|
|
433
|
+
# ─────────────────────────────────────────────────────────
|
|
434
|
+
# EVENT
|
|
435
|
+
# ─────────────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
def on(self, event: str, cb: Callable):
|
|
438
|
+
self._listeners.setdefault(event, []).append(cb)
|
|
439
|
+
|
|
440
|
+
async def _emit(self, event: str, payload: Any):
|
|
441
|
+
for cb in self._listeners.get(event, []):
|
|
442
|
+
try:
|
|
443
|
+
r = cb(payload)
|
|
444
|
+
if inspect.isawaitable(r):
|
|
445
|
+
await r
|
|
446
|
+
except Exception:
|
|
447
|
+
logger.exception(f"Listener error: {event}")
|
|
448
|
+
|
|
449
|
+
# ─────────────────────────────────────────────────────────
|
|
450
|
+
# DAHİLİ ÇALIŞTIRMA
|
|
451
|
+
# ─────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
def _spawn(self, td: TaskDef):
|
|
454
|
+
loop = asyncio.get_event_loop()
|
|
455
|
+
td._asyncio_task = loop.create_task(self._runner(td))
|
|
456
|
+
|
|
457
|
+
async def _runner(self, td: TaskDef):
|
|
458
|
+
"""Periyodik veya tek seferlik akışı yönetir."""
|
|
459
|
+
if td.delay > 0:
|
|
460
|
+
try:
|
|
461
|
+
await asyncio.wait_for(td._wake_event.wait(), timeout=td.delay)
|
|
462
|
+
return # stop edildi
|
|
463
|
+
except asyncio.TimeoutError:
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
if td.interval is None:
|
|
467
|
+
# Tek seferlik
|
|
468
|
+
await self._execute_once(td)
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
# Periyodik
|
|
472
|
+
while td.enabled and not td._stop_event.is_set():
|
|
473
|
+
if td.max_runs is not None and td.run_count >= td.max_runs:
|
|
474
|
+
logger.info(f"[✓] max_runs completed: {td.name}")
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
await self._execute_once(td)
|
|
478
|
+
td.run_count += 1
|
|
479
|
+
|
|
480
|
+
if not td.enabled or td._stop_event.is_set():
|
|
481
|
+
break
|
|
482
|
+
|
|
483
|
+
# interval kadar bekle (stop/cont ile erken uyandırılabilir)
|
|
484
|
+
td._wake_event.clear()
|
|
485
|
+
try:
|
|
486
|
+
await asyncio.wait_for(td._wake_event.wait(), timeout=td.interval)
|
|
487
|
+
break # uyandırıldı → stop
|
|
488
|
+
except asyncio.TimeoutError:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
async def _execute_once(
|
|
492
|
+
self, td: TaskDef,
|
|
493
|
+
override_args: tuple = (),
|
|
494
|
+
override_kwargs: Optional[Dict[str, Any]] = None,
|
|
495
|
+
) -> TaskResult:
|
|
496
|
+
"""Retry + timeout dahil tek çalıştırma."""
|
|
497
|
+
async with self._sem:
|
|
498
|
+
attempt = 0
|
|
499
|
+
last_err: Optional[Exception] = None
|
|
500
|
+
override_kwargs = override_kwargs or {}
|
|
501
|
+
|
|
502
|
+
while attempt <= td.retries:
|
|
503
|
+
attempt += 1
|
|
504
|
+
td.state = TaskState.RUNNING
|
|
505
|
+
started = time.time()
|
|
506
|
+
td._started_at = started
|
|
507
|
+
await self._emit("task_started", td)
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
coro = td.func(*override_args, **override_kwargs)
|
|
511
|
+
if td.timeout:
|
|
512
|
+
res = await asyncio.wait_for(coro, timeout=td.timeout)
|
|
513
|
+
else:
|
|
514
|
+
res = await coro
|
|
515
|
+
|
|
516
|
+
tr = TaskResult(
|
|
517
|
+
task_id=td.id, name=td.name, state=TaskState.SUCCESS,
|
|
518
|
+
result=res, started_at=started, finished_at=time.time(),
|
|
519
|
+
attempts=attempt, run_index=td.run_count,
|
|
520
|
+
)
|
|
521
|
+
td.state = TaskState.SUCCESS
|
|
522
|
+
td.last_result = tr
|
|
523
|
+
self._results[td.name].append(tr)
|
|
524
|
+
|
|
525
|
+
await self._emit("task_success", tr)
|
|
526
|
+
await self._safe(td.on_success, tr)
|
|
527
|
+
await self._safe(td.on_complete, tr)
|
|
528
|
+
return tr
|
|
529
|
+
|
|
530
|
+
except asyncio.CancelledError:
|
|
531
|
+
td.state = TaskState.STOPPED
|
|
532
|
+
raise
|
|
533
|
+
|
|
534
|
+
except asyncio.TimeoutError as e:
|
|
535
|
+
last_err = e
|
|
536
|
+
logger.warning(f"[!] Timeout: {td.name} (try {attempt})")
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
last_err = e
|
|
540
|
+
logger.exception(f"[!] Error: {td.name} (try {attempt})")
|
|
541
|
+
|
|
542
|
+
if attempt <= td.retries:
|
|
543
|
+
td.state = TaskState.RETRYING
|
|
544
|
+
tr = TaskResult(
|
|
545
|
+
task_id=td.id, name=td.name, state=TaskState.RETRYING,
|
|
546
|
+
error=last_err, started_at=started, finished_at=time.time(),
|
|
547
|
+
attempts=attempt, run_index=td.run_count,
|
|
548
|
+
)
|
|
549
|
+
await self._emit("task_retry", tr)
|
|
550
|
+
await self._safe(td.on_retry, tr, attempt)
|
|
551
|
+
wait = td.retry_delay * (td.retry_backoff ** (attempt - 1))
|
|
552
|
+
await asyncio.sleep(wait)
|
|
553
|
+
|
|
554
|
+
# Başarısız
|
|
555
|
+
state = (TaskState.TIMEOUT
|
|
556
|
+
if isinstance(last_err, asyncio.TimeoutError)
|
|
557
|
+
else TaskState.FAILED)
|
|
558
|
+
tr = TaskResult(
|
|
559
|
+
task_id=td.id, name=td.name, state=state,
|
|
560
|
+
error=last_err, started_at=td._started_at,
|
|
561
|
+
finished_at=time.time(), attempts=attempt,
|
|
562
|
+
run_index=td.run_count,
|
|
563
|
+
)
|
|
564
|
+
td.state = state
|
|
565
|
+
td.last_result = tr
|
|
566
|
+
self._results[td.name].append(tr)
|
|
567
|
+
|
|
568
|
+
await self._emit("task_failed", tr)
|
|
569
|
+
await self._safe(td.on_failure, tr)
|
|
570
|
+
await self._safe(td.on_complete, tr)
|
|
571
|
+
return tr
|
|
572
|
+
|
|
573
|
+
async def _safe(self, fn, *args):
|
|
574
|
+
if fn is None:
|
|
575
|
+
return
|
|
576
|
+
try:
|
|
577
|
+
r = fn(*args)
|
|
578
|
+
if inspect.isawaitable(r):
|
|
579
|
+
await r
|
|
580
|
+
except Exception:
|
|
581
|
+
logger.exception("Callback error")
|
|
582
|
+
|
|
583
|
+
def _require(self, task_name: str) -> TaskDef:
|
|
584
|
+
td = self._tasks.get(task_name)
|
|
585
|
+
if td is None:
|
|
586
|
+
raise KeyError(f"Task not found: {task_name}")
|
|
587
|
+
return td
|
|
588
|
+
|
|
589
|
+
# ─────────────────────────────────────────────────────────
|
|
590
|
+
# TOPLU KAPATMA
|
|
591
|
+
# ─────────────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
async def shutdown(self, timeout: float = 5.0):
|
|
594
|
+
self._shutdown = True
|
|
595
|
+
for name in list(self._tasks):
|
|
596
|
+
self.stop_task(name, wait_current=False)
|
|
597
|
+
pending = [
|
|
598
|
+
t._asyncio_task for t in self._tasks.values()
|
|
599
|
+
if t._asyncio_task and not t._asyncio_task.done()
|
|
600
|
+
]
|
|
601
|
+
if pending:
|
|
602
|
+
await asyncio.wait(pending, timeout=timeout)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# ─────────────────────────────────────────────────────────────
|
|
606
|
+
# GLOBAL VARSAYILAN SCHEDULER
|
|
607
|
+
# ─────────────────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
_default = TaskScheduler()
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def task(task_name: str, pycode: str, **kwargs) -> TaskDef:
|
|
613
|
+
return _default.task(task_name, pycode, **kwargs)
|
|
614
|
+
|
|
615
|
+
def config_task(task_name: str, **kwargs) -> TaskDef:
|
|
616
|
+
return _default.config_task(task_name, **kwargs)
|
|
617
|
+
|
|
618
|
+
def stop_task(task_name: str, **kwargs) -> bool:
|
|
619
|
+
return _default.stop_task(task_name, **kwargs)
|
|
620
|
+
|
|
621
|
+
def cont_task(task_name: str) -> bool:
|
|
622
|
+
return _default.cont_task(task_name)
|
|
623
|
+
|
|
624
|
+
def restart_task(task_name: str) -> bool:
|
|
625
|
+
return _default.restart_task(task_name)
|
|
626
|
+
|
|
627
|
+
def remove_task(task_name: str, **kwargs) -> bool:
|
|
628
|
+
return _default.remove_task(task_name, **kwargs)
|
|
629
|
+
|
|
630
|
+
def trigger_task(task_name: str, *a, **kw) -> asyncio.Task:
|
|
631
|
+
return _default.trigger_task(task_name, *a, **kw)
|
|
632
|
+
|
|
633
|
+
def get_scheduler() -> TaskScheduler:
|
|
634
|
+
return _default
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
py2autotask/__init__.py,sha256=bXmVkuMVN1qYhSdESz-B0fldccRjbTTPin5ef-hVizA,792
|
|
2
|
+
py2autotask/cli.py,sha256=6MmJ1LdEqiYjwLzwoy_F_Z7tTuk28xDQYZ-iGyk7rcU,13261
|
|
3
|
+
py2autotask/task.py,sha256=6lt-pxzp5eujztPfCUtipoNM3P5s4EdpVH7OJ2DNyQc,25282
|
|
4
|
+
py2autotask-2.0.1.dist-info/METADATA,sha256=6-M8zy6eUGL9GQRpeCP6VdezCqRd7abde1ufX_Hb848,169
|
|
5
|
+
py2autotask-2.0.1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
6
|
+
py2autotask-2.0.1.dist-info/entry_points.txt,sha256=RmO2qI-9pfsl0yaoBzn0Fv5UKUZDvkkWO4sF3y42x6w,51
|
|
7
|
+
py2autotask-2.0.1.dist-info/RECORD,,
|