-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathknowledge_scheduler.py
More file actions
143 lines (123 loc) · 4.66 KB
/
Copy pathknowledge_scheduler.py
File metadata and controls
143 lines (123 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""App-start knowledge refresh scheduling with persistent, opt-in state."""
from __future__ import annotations
import sqlite3
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable
from app_paths import DATABASE_PATH
from database_migrations import ensure_application_schema
TASK_NAME = "knowledge-refresh"
MIN_INTERVAL_HOURS = 6
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class KnowledgeScheduler:
"""Run cached/rate-limited imports when an enabled schedule becomes due."""
def __init__(self, database_path: str | Path = DATABASE_PATH) -> None:
self.database_path = Path(database_path)
with self._connect() as connection:
ensure_application_schema(connection)
connection.execute(
"""
INSERT INTO scheduled_sync_state(
task_name, enabled, interval_hours, updated_at
) VALUES (?, 0, 168, ?)
ON CONFLICT(task_name) DO NOTHING
""",
(TASK_NAME, utc_now().isoformat()),
)
@contextmanager
def _connect(self):
connection = sqlite3.connect(self.database_path)
connection.row_factory = sqlite3.Row
try:
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
def status(self) -> dict[str, Any]:
with self._connect() as connection:
row = connection.execute(
"SELECT * FROM scheduled_sync_state WHERE task_name=?", (TASK_NAME,)
).fetchone()
return dict(row) if row else {}
def configure(self, enabled: bool, interval_hours: int) -> None:
interval = max(MIN_INTERVAL_HOURS, min(int(interval_hours), 24 * 365))
with self._connect() as connection:
connection.execute(
"""
UPDATE scheduled_sync_state
SET enabled=?, interval_hours=?, updated_at=?
WHERE task_name=?
""",
(int(enabled), interval, utc_now().isoformat(), TASK_NAME),
)
def is_due(self, now: datetime | None = None) -> bool:
state = self.status()
if not state or not bool(state["enabled"]):
return False
current = now or utc_now()
completed = _parse_time(state.get("last_completed_at"))
if completed is None:
return True
return current >= completed + timedelta(hours=int(state["interval_hours"]))
def run_if_due(
self,
sync: Callable[[], Any] | None = None,
) -> dict[str, Any] | None:
if not self.is_due():
return None
operation = sync or _sync_all
started = utc_now().isoformat()
with self._connect() as connection:
connection.execute(
"""
UPDATE scheduled_sync_state
SET last_started_at=?, last_status='running', last_error=NULL,
updated_at=? WHERE task_name=?
""",
(started, started, TASK_NAME),
)
try:
result = operation()
except Exception as exc:
with self._connect() as connection:
connection.execute(
"""
UPDATE scheduled_sync_state
SET last_status='failed', last_error=?, updated_at=?
WHERE task_name=?
""",
(str(exc), utc_now().isoformat(), TASK_NAME),
)
raise
completed = utc_now().isoformat()
with self._connect() as connection:
connection.execute(
"""
UPDATE scheduled_sync_state
SET last_completed_at=?, last_status='completed',
last_error=NULL, updated_at=? WHERE task_name=?
""",
(completed, completed, TASK_NAME),
)
return {"completed_at": completed, "result": result}
def _sync_all() -> dict[str, Any]:
from knowledge_sync import sync_consolemods_knowledge, sync_reference_wikis
return {
"consolemods": sync_consolemods_knowledge(),
"wikis": sync_reference_wikis(),
}
def _parse_time(value: Any) -> datetime | None:
if not value:
return None
try:
parsed = datetime.fromisoformat(str(value))
except ValueError:
return None
if parsed.tzinfo is None:
return parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)