Fixed issue with generating multiple OTP - now prints a nice list for Copy/Paste. Began splitting client / server functionality. Demoted selector and setup to util from service. Fixed some typos.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable, Dict, Any, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Registry of functions that can be scheduled
|
||||
FUNCTION_MAP: Dict[str, Callable] = {}
|
||||
|
||||
# Dictionary to manually track scheduled jobs by ID
|
||||
scheduled_jobs: Dict[str, asyncio.TimerHandle] = {}
|
||||
|
||||
# Path to the JSON file for job persistence TODO - pin this to the correct place
|
||||
JOBS_FILE = os.path.join(os.getcwd(), "jobs.json")
|
||||
|
||||
def register_function(name: str, func: Callable):
|
||||
"""
|
||||
Register a function so it can be called by name later.
|
||||
Example:
|
||||
register_function("say_hello", say_hello)
|
||||
"""
|
||||
FUNCTION_MAP[name] = func
|
||||
|
||||
def load_jobs() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Load jobs from the JSON file, or return [] if none exist.
|
||||
"""
|
||||
if not os.path.exists(JOBS_FILE):
|
||||
return []
|
||||
with open(JOBS_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_jobs(jobs: List[Dict[str, Any]]):
|
||||
"""
|
||||
Save jobs to the JSON file (overwrite).
|
||||
"""
|
||||
with open(JOBS_FILE, "w") as f:
|
||||
json.dump(jobs, f, indent=4)
|
||||
|
||||
def cancel_job(job_id: str):
|
||||
"""
|
||||
Cancel a scheduled job by ID and remove it from the registry and persistence.
|
||||
"""
|
||||
handle = scheduled_jobs.pop(job_id, None)
|
||||
if handle:
|
||||
handle.cancel()
|
||||
logger.info(f"Cancelled job '{job_id}'")
|
||||
|
||||
jobs = [j for j in load_jobs() if j.get("id") != job_id]
|
||||
save_jobs(jobs)
|
||||
|
||||
def run_once_job(job_id: str, func_name: str, delay_seconds: float, args=None, kwargs=None, persist=True):
|
||||
"""
|
||||
Schedule a job to run once after a delay (in seconds).
|
||||
"""
|
||||
args = args or []
|
||||
kwargs = kwargs or {}
|
||||
|
||||
def job_wrapper():
|
||||
func = FUNCTION_MAP.get(func_name)
|
||||
if func is None:
|
||||
logger.error(f"Function '{func_name}' is not registered.")
|
||||
return
|
||||
func(*args, **kwargs)
|
||||
cancel_job(job_id)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
handle = loop.call_later(delay_seconds, job_wrapper)
|
||||
scheduled_jobs[job_id] = handle
|
||||
|
||||
if persist:
|
||||
jobs = [j for j in load_jobs() if j.get("id") != job_id]
|
||||
jobs.append({
|
||||
"id": job_id,
|
||||
"type": "once",
|
||||
"delay": delay_seconds,
|
||||
"function": func_name,
|
||||
"args": args,
|
||||
"kwargs": kwargs
|
||||
})
|
||||
save_jobs(jobs)
|
||||
logger.info(f"Scheduled one-time job '{job_id}' to run in {delay_seconds} seconds.")
|
||||
|
||||
def recurring_job(job_id: str, func_name: str, interval: float, args=None, kwargs=None, persist=True):
|
||||
"""
|
||||
Schedule a recurring job.
|
||||
"""
|
||||
args = args or []
|
||||
kwargs = kwargs or {}
|
||||
|
||||
def job_wrapper():
|
||||
func = FUNCTION_MAP.get(func_name)
|
||||
if func is None:
|
||||
logger.error(f"Function '{func_name}' is not registered.")
|
||||
return
|
||||
func(*args, **kwargs)
|
||||
# Reschedule the job
|
||||
handle = asyncio.get_event_loop().call_later(interval, job_wrapper)
|
||||
scheduled_jobs[job_id] = handle
|
||||
|
||||
cancel_job(job_id)
|
||||
handle = asyncio.get_event_loop().call_later(interval, job_wrapper)
|
||||
scheduled_jobs[job_id] = handle
|
||||
|
||||
if persist:
|
||||
jobs = [j for j in load_jobs() if j.get("id") != job_id]
|
||||
jobs.append({
|
||||
"id": job_id,
|
||||
"type": "recurring",
|
||||
"interval": interval,
|
||||
"function": func_name,
|
||||
"args": args,
|
||||
"kwargs": kwargs
|
||||
})
|
||||
save_jobs(jobs)
|
||||
logger.info(f"Scheduled recurring job '{job_id}' every {interval} seconds.")
|
||||
|
||||
def reload_jobs():
|
||||
"""
|
||||
Reload jobs from JSON and reschedule them.
|
||||
"""
|
||||
jobs = load_jobs()
|
||||
for job in jobs:
|
||||
if job["type"] == "once":
|
||||
run_once_job(
|
||||
job["id"],
|
||||
job["function"],
|
||||
job["delay"],
|
||||
job.get("args"),
|
||||
job.get("kwargs"),
|
||||
persist=False
|
||||
)
|
||||
elif job["type"] == "recurring":
|
||||
recurring_job(
|
||||
job["id"],
|
||||
job["function"],
|
||||
job["interval"],
|
||||
job.get("args"),
|
||||
job.get("kwargs"),
|
||||
persist=False
|
||||
)
|
||||
|
||||
async def start_scheduler():
|
||||
"""
|
||||
Start the asynchronous scheduler loop.
|
||||
|
||||
This function is a placeholder to keep the event loop alive.
|
||||
Jobs are scheduled using asyncio.call_later and do not require polling.
|
||||
"""
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
except asyncio.CancelledError:
|
||||
logger.critical("Scheduler stopped.")
|
||||
|
||||
"""
|
||||
Start the asynchronous scheduler loop.
|
||||
|
||||
This function is a placeholder for compatibility. Since we use asyncio.call_later,
|
||||
jobs are scheduled directly on the event loop and no polling is required.
|
||||
|
||||
Usage:
|
||||
# In an async app (e.g., Textual)
|
||||
asyncio.create_task(start_scheduler())
|
||||
|
||||
# Or in a standalone script
|
||||
async def main():
|
||||
await start_scheduler()
|
||||
|
||||
asyncio.run(main())
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(3600) # Sleep indefinitely; jobs run via call_later
|
||||
except asyncio.CancelledError:
|
||||
logger.critical("Scheduler stopped.")
|
||||
Reference in New Issue
Block a user