py2autotask 2.0.1__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.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.5
2
+ Name: py2autotask
3
+ Version: 2.0.1
4
+ Summary: Dynamic asynchronous task library
5
+ Author: 7yokki, brt222
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
@@ -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
@@ -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())
@@ -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,18 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "py2autotask"
7
+ version = "2.0.1"
8
+ description = "Dynamic asynchronous task library"
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "7yokki, brt222" }
13
+ ]
14
+ dependencies = [
15
+ ]
16
+
17
+ [project.scripts]
18
+ pyautotask = "pyautotask.cli:main"