fix: normalize unicode escape sequences and reformat multi-line expressions across codebase
This commit is contained in:
@@ -5,8 +5,16 @@ from __future__ import annotations
|
||||
from ..actions.spec import Action, Param
|
||||
|
||||
|
||||
def field(name: str, description: str, required: bool = False, kind: str = "string") -> Param:
|
||||
return Param(name=name, location="body", description=description, required=required, type=kind)
|
||||
def field(
|
||||
name: str, description: str, required: bool = False, kind: str = "string"
|
||||
) -> Param:
|
||||
return Param(
|
||||
name=name,
|
||||
location="body",
|
||||
description=description,
|
||||
required=required,
|
||||
type=kind,
|
||||
)
|
||||
|
||||
|
||||
SCHEDULE_FIELDS: tuple[Param, ...] = (
|
||||
@@ -20,8 +28,16 @@ SCHEDULE_FIELDS: tuple[Param, ...] = (
|
||||
"run_at",
|
||||
"For kind=once: absolute UTC time in ISO 8601, e.g. 2026-06-09T14:30:00.",
|
||||
),
|
||||
field("delay_seconds", "For kind=once: run this many seconds from now.", kind="integer"),
|
||||
field("every_seconds", "For kind=interval: number of seconds between runs.", kind="integer"),
|
||||
field(
|
||||
"delay_seconds",
|
||||
"For kind=once: run this many seconds from now.",
|
||||
kind="integer",
|
||||
),
|
||||
field(
|
||||
"every_seconds",
|
||||
"For kind=interval: number of seconds between runs.",
|
||||
kind="integer",
|
||||
),
|
||||
field(
|
||||
"start_at",
|
||||
"For kind=interval: optional ISO 8601 UTC time of the first run. "
|
||||
@@ -73,7 +89,9 @@ TASK_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
params=(
|
||||
field("enabled_only", "Only return enabled tasks.", kind="boolean"),
|
||||
field("status", "Filter by status: pending, running, done, error, disabled."),
|
||||
field(
|
||||
"status", "Filter by status: pending, running, done, error, disabled."
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
|
||||
@@ -15,7 +15,15 @@ from .store import TaskStore
|
||||
|
||||
logger = logging.getLogger("devii.tasks.controller")
|
||||
|
||||
SCHEDULE_KEYS = ("kind", "run_at", "delay_seconds", "every_seconds", "start_at", "cron", "max_runs")
|
||||
SCHEDULE_KEYS = (
|
||||
"kind",
|
||||
"run_at",
|
||||
"delay_seconds",
|
||||
"every_seconds",
|
||||
"start_at",
|
||||
"cron",
|
||||
"max_runs",
|
||||
)
|
||||
RESULT_PREVIEW_CHARS = 500
|
||||
TRUTHY = {"1", "true", "yes", "on"}
|
||||
|
||||
@@ -105,7 +113,10 @@ class TaskController:
|
||||
)
|
||||
rows.sort(key=lambda row: row.get("next_run_at") or "")
|
||||
return json.dumps(
|
||||
{"count": len(rows), "tasks": [_serialize(row, preview=True) for row in rows]},
|
||||
{
|
||||
"count": len(rows),
|
||||
"tasks": [_serialize(row, preview=True) for row in rows],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
@@ -127,7 +138,9 @@ class TaskController:
|
||||
if "enabled" in arguments:
|
||||
changes["enabled"] = _as_bool(arguments.get("enabled"))
|
||||
|
||||
if any(key in arguments and arguments[key] is not None for key in SCHEDULE_KEYS):
|
||||
if any(
|
||||
key in arguments and arguments[key] is not None for key in SCHEDULE_KEYS
|
||||
):
|
||||
merged = {key: row.get(key) for key in SCHEDULE_KEYS}
|
||||
for key in SCHEDULE_KEYS:
|
||||
if key in arguments and arguments[key] is not None:
|
||||
@@ -137,10 +150,16 @@ class TaskController:
|
||||
changes["next_run_at"] = to_iso(schedule.first_run(now_utc()))
|
||||
changes["status"] = "pending"
|
||||
|
||||
if changes.get("enabled") and row.get("status") in ("done", "disabled", "error"):
|
||||
if changes.get("enabled") and row.get("status") in (
|
||||
"done",
|
||||
"disabled",
|
||||
"error",
|
||||
):
|
||||
changes.setdefault("status", "pending")
|
||||
if not changes.get("next_run_at") and not row.get("next_run_at"):
|
||||
schedule = self._build_schedule({key: row.get(key) for key in SCHEDULE_KEYS})
|
||||
schedule = self._build_schedule(
|
||||
{key: row.get(key) for key in SCHEDULE_KEYS}
|
||||
)
|
||||
changes["next_run_at"] = to_iso(schedule.first_run(now_utc()))
|
||||
if changes.get("enabled") is False:
|
||||
changes["status"] = "disabled"
|
||||
@@ -150,7 +169,10 @@ class TaskController:
|
||||
|
||||
self._store.update(row["uid"], changes)
|
||||
return json.dumps(
|
||||
{"status": "updated", "task": _serialize(self._store.get(row["uid"]), preview=True)},
|
||||
{
|
||||
"status": "updated",
|
||||
"task": _serialize(self._store.get(row["uid"]), preview=True),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
@@ -168,12 +190,18 @@ class TaskController:
|
||||
{"enabled": True, "status": "pending", "next_run_at": to_iso(now_utc())},
|
||||
)
|
||||
return json.dumps(
|
||||
{"status": "queued", "uid": row["uid"], "note": "Will execute on the next scheduler tick."},
|
||||
{
|
||||
"status": "queued",
|
||||
"uid": row["uid"],
|
||||
"note": "Will execute on the next scheduler tick.",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
def _build_schedule(self, source: dict[str, Any]) -> Schedule:
|
||||
payload = {key: source.get(key) for key in SCHEDULE_KEYS if source.get(key) is not None}
|
||||
payload = {
|
||||
key: source.get(key) for key in SCHEDULE_KEYS if source.get(key) is not None
|
||||
}
|
||||
try:
|
||||
return Schedule(**payload)
|
||||
except ValidationError as exc:
|
||||
@@ -195,10 +223,14 @@ class TaskController:
|
||||
return row
|
||||
|
||||
|
||||
def compute_followup(row: dict[str, Any], reference: Any) -> tuple[dict[str, Any], bool]:
|
||||
def compute_followup(
|
||||
row: dict[str, Any], reference: Any
|
||||
) -> tuple[dict[str, Any], bool]:
|
||||
run_count = int(row.get("run_count") or 0) + 1
|
||||
max_runs = row.get("max_runs")
|
||||
upcoming = next_run(row.get("kind"), row.get("every_seconds"), row.get("cron"), reference)
|
||||
upcoming = next_run(
|
||||
row.get("kind"), row.get("every_seconds"), row.get("cron"), reference
|
||||
)
|
||||
|
||||
changes: dict[str, Any] = {"run_count": run_count, "last_run_at": to_iso(reference)}
|
||||
if upcoming is None or (max_runs is not None and run_count >= int(max_runs)):
|
||||
|
||||
@@ -50,7 +50,9 @@ def _parse_field(field: str, low: int, high: int) -> set[int]:
|
||||
def cron_next(expr: str, after: datetime) -> datetime:
|
||||
fields = expr.split()
|
||||
if len(fields) != 5:
|
||||
raise ValueError("Cron expression must have 5 fields: minute hour dom month dow")
|
||||
raise ValueError(
|
||||
"Cron expression must have 5 fields: minute hour dom month dow"
|
||||
)
|
||||
minutes = _parse_field(fields[0], 0, 59)
|
||||
hours = _parse_field(fields[1], 0, 23)
|
||||
days = _parse_field(fields[2], 1, 31)
|
||||
@@ -139,7 +141,9 @@ class Schedule(BaseModel):
|
||||
}
|
||||
|
||||
|
||||
def next_run(kind: str, every_seconds: Optional[int], cron: Optional[str], reference: datetime) -> Optional[datetime]:
|
||||
def next_run(
|
||||
kind: str, every_seconds: Optional[int], cron: Optional[str], reference: datetime
|
||||
) -> Optional[datetime]:
|
||||
if kind == "interval" and every_seconds:
|
||||
return reference + timedelta(seconds=int(every_seconds))
|
||||
if kind == "cron" and cron:
|
||||
|
||||
@@ -77,7 +77,11 @@ class Scheduler:
|
||||
logger.exception("Task uid=%s crashed", uid)
|
||||
self._store.update(
|
||||
uid,
|
||||
{"status": "error", "last_error": str(exc), "last_run_at": to_iso(now_utc())},
|
||||
{
|
||||
"status": "error",
|
||||
"last_error": str(exc),
|
||||
"last_run_at": to_iso(now_utc()),
|
||||
},
|
||||
)
|
||||
self._on_event("error", row, str(exc))
|
||||
return
|
||||
|
||||
@@ -10,7 +10,13 @@ import dataset
|
||||
logger = logging.getLogger("devii.tasks.store")
|
||||
|
||||
TABLE = "devii_tasks"
|
||||
INDEXED_COLUMNS = (["owner_kind", "owner_id"], ["uid"], ["enabled"], ["next_run_at"], ["status"])
|
||||
INDEXED_COLUMNS = (
|
||||
["owner_kind", "owner_id"],
|
||||
["uid"],
|
||||
["enabled"],
|
||||
["next_run_at"],
|
||||
["status"],
|
||||
)
|
||||
|
||||
|
||||
def memory_db() -> Any:
|
||||
@@ -41,12 +47,19 @@ class TaskStore:
|
||||
|
||||
def create(self, record: dict[str, Any]) -> None:
|
||||
self._table.insert({**record, **self._scope})
|
||||
logger.info("Task created uid=%s owner=%s/%s", record.get("uid"), self._owner_kind, self._owner_id)
|
||||
logger.info(
|
||||
"Task created uid=%s owner=%s/%s",
|
||||
record.get("uid"),
|
||||
self._owner_kind,
|
||||
self._owner_id,
|
||||
)
|
||||
|
||||
def get(self, uid: str) -> dict[str, Any] | None:
|
||||
return self._table.find_one(uid=uid, **self._scope)
|
||||
|
||||
def list(self, enabled_only: bool = False, status: str | None = None) -> list[dict[str, Any]]:
|
||||
def list(
|
||||
self, enabled_only: bool = False, status: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
criteria: dict[str, Any] = dict(self._scope)
|
||||
if enabled_only:
|
||||
criteria["enabled"] = True
|
||||
@@ -69,12 +82,18 @@ class TaskStore:
|
||||
return 0
|
||||
stuck = list(self._table.find(status="running", **self._scope))
|
||||
for row in stuck:
|
||||
self._table.update({"uid": row["uid"], "status": "pending", **self._scope},
|
||||
["uid", "owner_kind", "owner_id"])
|
||||
self._table.update(
|
||||
{"uid": row["uid"], "status": "pending", **self._scope},
|
||||
["uid", "owner_kind", "owner_id"],
|
||||
)
|
||||
if stuck:
|
||||
logger.info("Recovered %d task(s) stuck in running", len(stuck))
|
||||
return len(stuck)
|
||||
|
||||
def due(self, now_iso: str) -> list[dict[str, Any]]:
|
||||
rows = self._table.find(enabled=True, status="pending", **self._scope)
|
||||
return [row for row in rows if row.get("next_run_at") and row["next_run_at"] <= now_iso]
|
||||
return [
|
||||
row
|
||||
for row in rows
|
||||
if row.get("next_run_at") and row["next_run_at"] <= now_iso
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user