from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from pydantic import Field, PrivateAttr, ConfigDict
from pyobs.robotic.scheduler import DataProvider
from pyobs.robotic.scheduler.targets import Target
from pyobs.robotic.scripts import Script
from pyobs.utils.time import Time
if TYPE_CHECKING:
from pyobs.robotic.observationarchive import ObservationArchive
from pyobs.robotic.taskarchive import TaskArchive
from pyobs.robotic.scheduler.constraints import Constraint
from pyobs.robotic.scheduler.merits import Merit
from pyobs.utils.serialization import BaseModel
[docs]
@dataclass
class TaskData:
task: Task
observation_archive: ObservationArchive | None = None
task_archive: TaskArchive | None = None
[docs]
class Task(BaseModel):
id: Any | None = None
name: str = Field(default="")
project: str = Field(default="")
duration: float = Field(ge=0.0, le=84000.0, default=0.0)
priority: float | None = Field(ge=0.0, le=9999.0, default=1.0)
constraints: list[Constraint] = Field(default_factory=list)
merits: list[Merit] = Field(default_factory=list)
static_target: Target | None = Field(default=None, alias="target")
script: dict[str, Any] = Field(default_factory=dict)
active: bool = True
_resolved_target: Target | None = PrivateAttr(default=None)
model_config = ConfigDict(populate_by_name=True)
def __str__(self) -> str:
s = f"Task {self.id}: {self.name} (duration: {self.duration}s"
if self.priority is not None:
s += f", priority: {self.priority}"
if self.target is not None:
s += f", target: {self.target.name}"
s += ")"
return s
def create_script(self) -> Script:
return self.pyobs_model_validate(Script, self.script, by_alias=True)
[docs]
async def can_run(self, data: TaskData | None) -> bool:
"""Checks whether this task could run now.
Returns:
True, if the task can run now.
"""
if self.script is not None:
return await self.create_script().can_run(data)
return True
@property
def can_start_late(self) -> bool:
"""Whether this tasks is allowed to start later than the user-set time, e.g. for flatfields.
Returns:
True, if task can start late.
"""
return False
[docs]
async def run(self, data: TaskData | None) -> None:
"""Run a task"""
if self.script is not None:
await self.create_script().run(data)
[docs]
def is_finished(self) -> bool:
"""Whether task is finished."""
return False
def estimate_duration(self) -> float:
if self.script:
return self.create_script().estimate_duration()
return self.duration
[docs]
async def resolve_target(self, time: Time, task: Task, data: DataProvider) -> bool:
"""Resolve dynamic target. Returns False if no valid target found."""
if self.static_target is None:
return True
if self._resolved_target is not None:
return True
self._resolved_target = await self.static_target.resolve(time, self, data)
return self._resolved_target is not None
[docs]
def set_resolved_target(self, target: Target | None) -> None:
"""Set the resolved target if not already set, e.g. when restoring from an observation."""
if self._resolved_target is None:
self._resolved_target = target
@property
def target(self) -> Target | None:
"""The resolved target, or the static target if not dynamic."""
if self._resolved_target is not None:
return self._resolved_target
return self.static_target
[docs]
class Project(BaseModel):
code: str
name: str = ""
priority: float | None = Field(ge=0.0, le=9999.0, default=1.0)
__all__ = ["Task", "TaskData", "Project"]