pakt 0.2.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.
pakt/scheduler.py ADDED
@@ -0,0 +1,153 @@
1
+ """Scheduler for automatic sync operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Any, Callable
8
+
9
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
10
+ from apscheduler.triggers.interval import IntervalTrigger
11
+
12
+ if TYPE_CHECKING:
13
+ from pakt.config import Config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class SyncScheduler:
19
+ """Manages scheduled sync operations."""
20
+
21
+ def __init__(
22
+ self,
23
+ config: Config,
24
+ sync_func: Callable[[], Any],
25
+ is_running_func: Callable[[], bool],
26
+ ) -> None:
27
+ """Initialize the scheduler.
28
+
29
+ Args:
30
+ config: Application configuration
31
+ sync_func: Async function to run sync
32
+ is_running_func: Function to check if sync is currently running
33
+ """
34
+ self.config = config
35
+ self.sync_func = sync_func
36
+ self.is_running_func = is_running_func
37
+ self.scheduler = AsyncIOScheduler()
38
+ self._job_id = "pakt_sync"
39
+ self._last_run: datetime | None = None
40
+ self._next_run: datetime | None = None
41
+
42
+ def start(self) -> None:
43
+ """Start the scheduler if enabled in config."""
44
+ if not self.config.scheduler.enabled:
45
+ logger.info("Scheduler disabled in config")
46
+ return
47
+
48
+ if self.config.scheduler.interval_hours <= 0:
49
+ logger.warning("Scheduler interval must be > 0, scheduler disabled")
50
+ return
51
+
52
+ self._add_job()
53
+ self.scheduler.start()
54
+ logger.info(
55
+ f"Scheduler started with {self.config.scheduler.interval_hours}h interval"
56
+ )
57
+
58
+ def stop(self) -> None:
59
+ """Stop the scheduler."""
60
+ if self.scheduler.running:
61
+ self.scheduler.shutdown(wait=False)
62
+ logger.info("Scheduler stopped")
63
+
64
+ def _add_job(self) -> None:
65
+ """Add or update the sync job."""
66
+ trigger = IntervalTrigger(hours=self.config.scheduler.interval_hours)
67
+
68
+ if self.scheduler.get_job(self._job_id):
69
+ self.scheduler.reschedule_job(self._job_id, trigger=trigger)
70
+ else:
71
+ self.scheduler.add_job(
72
+ self._run_sync,
73
+ trigger=trigger,
74
+ id=self._job_id,
75
+ name="Pakt Sync",
76
+ replace_existing=True,
77
+ )
78
+
79
+ job = self.scheduler.get_job(self._job_id)
80
+ if job:
81
+ self._next_run = job.next_run_time
82
+
83
+ async def _run_sync(self) -> None:
84
+ """Execute the sync if not already running."""
85
+ if self.is_running_func():
86
+ logger.info("Scheduled sync skipped - sync already running")
87
+ return
88
+
89
+ logger.info("Starting scheduled sync")
90
+ self._last_run = datetime.now()
91
+
92
+ try:
93
+ await self.sync_func()
94
+ except Exception as e:
95
+ logger.error(f"Scheduled sync failed: {e}")
96
+ finally:
97
+ job = self.scheduler.get_job(self._job_id)
98
+ if job:
99
+ self._next_run = job.next_run_time
100
+
101
+ def update_config(self, enabled: bool, interval_hours: int) -> None:
102
+ """Update scheduler configuration.
103
+
104
+ Args:
105
+ enabled: Whether scheduler should be enabled
106
+ interval_hours: Hours between syncs
107
+ """
108
+ self.config.scheduler.enabled = enabled
109
+ self.config.scheduler.interval_hours = interval_hours
110
+
111
+ if not enabled:
112
+ if self.scheduler.get_job(self._job_id):
113
+ self.scheduler.remove_job(self._job_id)
114
+ self._next_run = None
115
+ logger.info("Scheduler disabled")
116
+ return
117
+
118
+ if interval_hours <= 0:
119
+ logger.warning("Invalid interval, scheduler disabled")
120
+ return
121
+
122
+ if not self.scheduler.running:
123
+ self._add_job()
124
+ self.scheduler.start()
125
+ else:
126
+ self._add_job()
127
+
128
+ logger.info(f"Scheduler updated: {interval_hours}h interval")
129
+
130
+ @property
131
+ def next_run(self) -> datetime | None:
132
+ """Get the next scheduled run time."""
133
+ job = self.scheduler.get_job(self._job_id)
134
+ return job.next_run_time if job else None
135
+
136
+ @property
137
+ def last_run(self) -> datetime | None:
138
+ """Get the last run time."""
139
+ return self._last_run
140
+
141
+ @property
142
+ def is_enabled(self) -> bool:
143
+ """Check if scheduler is enabled and running."""
144
+ return self.scheduler.running and self.scheduler.get_job(self._job_id) is not None
145
+
146
+ def get_status(self) -> dict[str, Any]:
147
+ """Get scheduler status."""
148
+ return {
149
+ "enabled": self.is_enabled,
150
+ "interval_hours": self.config.scheduler.interval_hours,
151
+ "next_run": self.next_run.isoformat() if self.next_run else None,
152
+ "last_run": self._last_run.isoformat() if self._last_run else None,
153
+ }