tiphys.scheduler.store
Task store for scheduled tasks.
Persists tasks to disk as JSON files with file locking.
1""" 2Task store for scheduled tasks. 3 4Persists tasks to disk as JSON files with file locking. 5""" 6 7import contextlib 8import json 9from datetime import datetime 10from pathlib import Path 11 12import structlog 13from filelock import FileLock 14 15from tiphys.scheduler.models import ScheduledTask 16 17logger = structlog.get_logger(__name__) 18 19 20class TaskStore: 21 """ 22 Persistent store for scheduled tasks. 23 24 Uses file locking for cross-process safety. 25 """ 26 27 def __init__(self, tasks_dir: Path | None = None): 28 """ 29 Initialize the task store. 30 31 Args: 32 tasks_dir: Directory for task files. 33 Defaults to ~/.tiphys/tasks/ 34 """ 35 if tasks_dir is None: 36 tasks_dir = Path.home() / ".tiphys" / "tasks" 37 self.tasks_dir = tasks_dir 38 self.tasks_dir.mkdir(parents=True, exist_ok=True) 39 40 def _task_path(self, task_id: str) -> Path: 41 """Get path to task file.""" 42 return self.tasks_dir / f"{task_id}.json" 43 44 def _lock_path(self, task_id: str) -> Path: 45 """Get path to lock file.""" 46 return self._task_path(task_id).with_suffix(".lock") 47 48 def save(self, task: ScheduledTask) -> None: 49 """Save a task to disk.""" 50 task.updated_at = datetime.utcnow() 51 path = self._task_path(task.id) 52 53 lock = FileLock(self._lock_path(task.id)) 54 with lock: 55 temp_path = path.with_suffix(".tmp") 56 temp_path.write_text(task.model_dump_json(indent=2)) 57 temp_path.rename(path) 58 59 def load(self, task_id: str) -> ScheduledTask | None: 60 """Load a task from disk.""" 61 path = self._task_path(task_id) 62 if not path.exists(): 63 return None 64 65 lock = FileLock(self._lock_path(task_id)) 66 with lock: 67 try: 68 data = json.loads(path.read_text()) 69 return ScheduledTask.model_validate(data) 70 except (json.JSONDecodeError, FileNotFoundError): 71 return None 72 73 def delete(self, task_id: str) -> bool: 74 """Delete a task from disk.""" 75 path = self._task_path(task_id) 76 lock_path = self._lock_path(task_id) 77 78 if not path.exists(): 79 return False 80 81 lock = FileLock(lock_path) 82 with lock: 83 try: 84 path.unlink() 85 with contextlib.suppress(FileNotFoundError): 86 lock_path.unlink() 87 return True 88 except FileNotFoundError: 89 return False 90 91 def list_all(self) -> list[ScheduledTask]: 92 """List all tasks in the store.""" 93 tasks = [] 94 for path in self.tasks_dir.glob("*.json"): 95 try: 96 data = json.loads(path.read_text()) 97 tasks.append(ScheduledTask.model_validate(data)) 98 except Exception: 99 continue 100 return tasks 101 102 def list_by_group(self, group: str) -> list[ScheduledTask]: 103 """List tasks belonging to a specific group.""" 104 return [t for t in self.list_all() if t.group == group] 105 106 def get_due_tasks(self, now: datetime | None = None) -> list[ScheduledTask]: 107 """Get all active tasks that are due to run.""" 108 if now is None: 109 now = datetime.utcnow() 110 111 return [ 112 t for t in self.list_all() if t.status == "active" and t.next_run and t.next_run <= now 113 ]
logger =
<BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=('tiphys.scheduler.store',))>
class
TaskStore:
21class TaskStore: 22 """ 23 Persistent store for scheduled tasks. 24 25 Uses file locking for cross-process safety. 26 """ 27 28 def __init__(self, tasks_dir: Path | None = None): 29 """ 30 Initialize the task store. 31 32 Args: 33 tasks_dir: Directory for task files. 34 Defaults to ~/.tiphys/tasks/ 35 """ 36 if tasks_dir is None: 37 tasks_dir = Path.home() / ".tiphys" / "tasks" 38 self.tasks_dir = tasks_dir 39 self.tasks_dir.mkdir(parents=True, exist_ok=True) 40 41 def _task_path(self, task_id: str) -> Path: 42 """Get path to task file.""" 43 return self.tasks_dir / f"{task_id}.json" 44 45 def _lock_path(self, task_id: str) -> Path: 46 """Get path to lock file.""" 47 return self._task_path(task_id).with_suffix(".lock") 48 49 def save(self, task: ScheduledTask) -> None: 50 """Save a task to disk.""" 51 task.updated_at = datetime.utcnow() 52 path = self._task_path(task.id) 53 54 lock = FileLock(self._lock_path(task.id)) 55 with lock: 56 temp_path = path.with_suffix(".tmp") 57 temp_path.write_text(task.model_dump_json(indent=2)) 58 temp_path.rename(path) 59 60 def load(self, task_id: str) -> ScheduledTask | None: 61 """Load a task from disk.""" 62 path = self._task_path(task_id) 63 if not path.exists(): 64 return None 65 66 lock = FileLock(self._lock_path(task_id)) 67 with lock: 68 try: 69 data = json.loads(path.read_text()) 70 return ScheduledTask.model_validate(data) 71 except (json.JSONDecodeError, FileNotFoundError): 72 return None 73 74 def delete(self, task_id: str) -> bool: 75 """Delete a task from disk.""" 76 path = self._task_path(task_id) 77 lock_path = self._lock_path(task_id) 78 79 if not path.exists(): 80 return False 81 82 lock = FileLock(lock_path) 83 with lock: 84 try: 85 path.unlink() 86 with contextlib.suppress(FileNotFoundError): 87 lock_path.unlink() 88 return True 89 except FileNotFoundError: 90 return False 91 92 def list_all(self) -> list[ScheduledTask]: 93 """List all tasks in the store.""" 94 tasks = [] 95 for path in self.tasks_dir.glob("*.json"): 96 try: 97 data = json.loads(path.read_text()) 98 tasks.append(ScheduledTask.model_validate(data)) 99 except Exception: 100 continue 101 return tasks 102 103 def list_by_group(self, group: str) -> list[ScheduledTask]: 104 """List tasks belonging to a specific group.""" 105 return [t for t in self.list_all() if t.group == group] 106 107 def get_due_tasks(self, now: datetime | None = None) -> list[ScheduledTask]: 108 """Get all active tasks that are due to run.""" 109 if now is None: 110 now = datetime.utcnow() 111 112 return [ 113 t for t in self.list_all() if t.status == "active" and t.next_run and t.next_run <= now 114 ]
Persistent store for scheduled tasks.
Uses file locking for cross-process safety.
TaskStore(tasks_dir: pathlib.Path | None = None)
28 def __init__(self, tasks_dir: Path | None = None): 29 """ 30 Initialize the task store. 31 32 Args: 33 tasks_dir: Directory for task files. 34 Defaults to ~/.tiphys/tasks/ 35 """ 36 if tasks_dir is None: 37 tasks_dir = Path.home() / ".tiphys" / "tasks" 38 self.tasks_dir = tasks_dir 39 self.tasks_dir.mkdir(parents=True, exist_ok=True)
Initialize the task store.
Args: tasks_dir: Directory for task files. Defaults to ~/.tiphys/tasks/
49 def save(self, task: ScheduledTask) -> None: 50 """Save a task to disk.""" 51 task.updated_at = datetime.utcnow() 52 path = self._task_path(task.id) 53 54 lock = FileLock(self._lock_path(task.id)) 55 with lock: 56 temp_path = path.with_suffix(".tmp") 57 temp_path.write_text(task.model_dump_json(indent=2)) 58 temp_path.rename(path)
Save a task to disk.
60 def load(self, task_id: str) -> ScheduledTask | None: 61 """Load a task from disk.""" 62 path = self._task_path(task_id) 63 if not path.exists(): 64 return None 65 66 lock = FileLock(self._lock_path(task_id)) 67 with lock: 68 try: 69 data = json.loads(path.read_text()) 70 return ScheduledTask.model_validate(data) 71 except (json.JSONDecodeError, FileNotFoundError): 72 return None
Load a task from disk.
def
delete(self, task_id: str) -> bool:
74 def delete(self, task_id: str) -> bool: 75 """Delete a task from disk.""" 76 path = self._task_path(task_id) 77 lock_path = self._lock_path(task_id) 78 79 if not path.exists(): 80 return False 81 82 lock = FileLock(lock_path) 83 with lock: 84 try: 85 path.unlink() 86 with contextlib.suppress(FileNotFoundError): 87 lock_path.unlink() 88 return True 89 except FileNotFoundError: 90 return False
Delete a task from disk.
92 def list_all(self) -> list[ScheduledTask]: 93 """List all tasks in the store.""" 94 tasks = [] 95 for path in self.tasks_dir.glob("*.json"): 96 try: 97 data = json.loads(path.read_text()) 98 tasks.append(ScheduledTask.model_validate(data)) 99 except Exception: 100 continue 101 return tasks
List all tasks in the store.
103 def list_by_group(self, group: str) -> list[ScheduledTask]: 104 """List tasks belonging to a specific group.""" 105 return [t for t in self.list_all() if t.group == group]
List tasks belonging to a specific group.
def
get_due_tasks( self, now: datetime.datetime | None = None) -> list[tiphys.scheduler.models.ScheduledTask]:
107 def get_due_tasks(self, now: datetime | None = None) -> list[ScheduledTask]: 108 """Get all active tasks that are due to run.""" 109 if now is None: 110 now = datetime.utcnow() 111 112 return [ 113 t for t in self.list_all() if t.status == "active" and t.next_run and t.next_run <= now 114 ]
Get all active tasks that are due to run.