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/

tasks_dir
def save(self, task: tiphys.scheduler.models.ScheduledTask) -> None:
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.

def load(self, task_id: str) -> tiphys.scheduler.models.ScheduledTask | None:
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.

def list_all(self) -> list[tiphys.scheduler.models.ScheduledTask]:
 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.

def list_by_group(self, group: str) -> list[tiphys.scheduler.models.ScheduledTask]:
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.