"""In-process job tracking for long-running HTTP operations.""" from __future__ import annotations import time import traceback import uuid import json from concurrent.futures import Future, ThreadPoolExecutor from dataclasses import dataclass, field from inspect import signature from threading import Lock from typing import Any, Callable from rich.console import Console from owilix.core.types import CommandResult def _jsonable(value: Any) -> Any: try: json.dumps(value, default=str) return value except TypeError: if isinstance(value, list): return [_jsonable(item) for item in value] if isinstance(value, dict): return {str(key): _jsonable(item) for key, item in value.items()} if hasattr(value, "to_dict"): try: return value.to_dict(orient="records") except TypeError: return value.to_dict() if hasattr(value, "metadata"): metadata = getattr(value, "metadata", {}) return { "id": metadata.get("id") or metadata.get("internalID") if hasattr(metadata, "get") else None, "path": getattr(value, "path", None), "access": getattr(value, "access", None), "metadata": metadata.as_json_dict() if hasattr(metadata, "as_json_dict") else dict(metadata), } return str(value) def _command_result_dict(result: CommandResult) -> dict[str, Any]: data = result.as_dict() if data.get("result") is None and result.object is not None: data["result"] = _jsonable(result.object) return data @dataclass class Job: id: str operation: str status: str = "queued" created_at: float = field(default_factory=time.time) started_at: float | None = None finished_at: float | None = None result: dict[str, Any] | None = None error: str | None = None traceback: str | None = None logs: str = "" def public_dict(self) -> dict[str, Any]: return { "id": self.id, "operation": self.operation, "status": self.status, "created_at": int(self.created_at), "started_at": int(self.started_at) if self.started_at else None, "finished_at": int(self.finished_at) if self.finished_at else None, "result": self.result, "error": self.error, "logs": self.logs, } class JobManager: """Run blocking functions in background threads and expose pollable state.""" def __init__(self, max_workers: int = 2): self._executor = ThreadPoolExecutor(max_workers=max_workers) self._lock = Lock() self._jobs: dict[str, Job] = {} self._futures: dict[str, Future] = {} def submit(self, operation: str, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Job: job = Job(id=str(uuid.uuid4()), operation=operation) with self._lock: self._jobs[job.id] = job future = self._executor.submit(self._run, job.id, func, args, kwargs) with self._lock: self._futures[job.id] = future return job def get(self, job_id: str) -> Job | None: with self._lock: return self._jobs.get(job_id) def list(self) -> list[Job]: with self._lock: return list(self._jobs.values()) def _run(self, job_id: str, func: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]) -> None: console = Console(record=True, width=120) with self._lock: job = self._jobs[job_id] job.status = "running" job.started_at = time.time() try: try: accepts_console = "console" in signature(func).parameters except (TypeError, ValueError): accepts_console = False if accepts_console and "console" not in kwargs: kwargs["console"] = console value = func(*args, **kwargs) result = _command_result_dict(value) if isinstance(value, CommandResult) else {"success": True, "result": _jsonable(value)} status = "succeeded" if result.get("success", True) else "failed" with self._lock: job = self._jobs[job_id] job.status = status job.result = result job.finished_at = time.time() job.logs = console.export_text(clear=False) except Exception as exc: with self._lock: job = self._jobs[job_id] job.status = "failed" job.error = str(exc) job.traceback = traceback.format_exc() job.finished_at = time.time() job.logs = console.export_text(clear=False)