diff --git a/src/vocutouts/models/parameters.py b/src/vocutouts/models/parameters.py index 6e4fe55..beaba5e 100644 --- a/src/vocutouts/models/parameters.py +++ b/src/vocutouts/models/parameters.py @@ -7,7 +7,7 @@ from ..exceptions import InvalidCutoutParameterError from ..uws.models import UWSJobParameter -from .stencils import Stencil, parse_stencil +from .stencils import CircleStencil, PolygonStencil, RangeStencil, Stencil @dataclass @@ -46,8 +46,9 @@ def from_job_parameters(cls, params: list[UWSJobParameter]) -> Self: if param.parameter_id == "id": ids.append(param.value) else: - f = parse_stencil(param.parameter_id.upper(), param.value) - stencils.append(f) + stencil_type = param.parameter_id.upper() + stencil = cls._parse_stencil(stencil_type, param.value) + stencils.append(stencil) except Exception as e: msg = f"Invalid cutout parameter: {type(e).__name__}: {e!s}" raise InvalidCutoutParameterError(msg, params) from e @@ -58,3 +59,18 @@ def from_job_parameters(cls, params: list[UWSJobParameter]) -> Self: "No cutout stencil given", params ) return cls(ids=ids, stencils=stencils) + + @staticmethod + def _parse_stencil(stencil_type: str, params: str) -> Stencil: + """Convert a string stencil parameter to its representation.""" + if stencil_type == "POS": + stencil_type, params = params.split(None, 1) + match stencil_type: + case "CIRCLE": + return CircleStencil.from_string(params) + case "POLYGON": + return PolygonStencil.from_string(params) + case "RANGE": + return RangeStencil.from_string(params) + case _: + raise ValueError(f"Unknown stencil type {stencil_type}") diff --git a/src/vocutouts/models/stencils.py b/src/vocutouts/models/stencils.py index 792588e..1d0e8a4 100644 --- a/src/vocutouts/models/stencils.py +++ b/src/vocutouts/models/stencils.py @@ -4,12 +4,21 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Self +from typing import Any, Self, TypeAlias from astropy import units as u from astropy.coordinates import Angle, SkyCoord -Range = tuple[float, float] +Range: TypeAlias = tuple[float, float] +"""Type representing a range of a coordinate.""" + +__all__ = [ + "CircleStencil", + "PolygonStencil", + "Range", + "RangeStencil", + "Stencil", +] class Stencil(ABC): @@ -22,7 +31,7 @@ def from_string(cls, params: str) -> Self: @abstractmethod def to_dict(self) -> dict[str, Any]: - """Convert the stencil to a JSON-serializable form for queuing.""" + """Convert the stencil to a dictionary for logging.""" @dataclass @@ -30,7 +39,10 @@ class CircleStencil(Stencil): """Represents a ``CIRCLE`` or ``POS=CIRCLE`` stencil.""" center: SkyCoord + """Center of the circle.""" + radius: Angle + """Radius of the circle.""" @classmethod def from_string(cls, params: str) -> Self: @@ -60,6 +72,7 @@ class PolygonStencil(Stencil): """ vertices: SkyCoord + """Vertices of the polygon.""" @classmethod def from_string(cls, params: str) -> Self: @@ -90,7 +103,10 @@ class RangeStencil(Stencil): """Represents a ``POS=RANGE`` stencil.""" ra: Range + """Range of ra values.""" + dec: Range + """Range of dec values.""" @classmethod def from_string(cls, params: str) -> Self: @@ -106,17 +122,3 @@ def to_dict(self) -> dict[str, Any]: "ra": self.ra, "dec": self.dec, } - - -def parse_stencil(stencil_type: str, params: str) -> Stencil: - """Convert a string stencil parameter to its internal representation.""" - if stencil_type == "POS": - stencil_type, params = params.split(None, 1) - if stencil_type == "CIRCLE": - return CircleStencil.from_string(params) - elif stencil_type == "POLYGON": - return PolygonStencil.from_string(params) - elif stencil_type == "RANGE": - return RangeStencil.from_string(params) - else: - raise ValueError(f"Unknown stencil type {stencil_type}") diff --git a/src/vocutouts/policy.py b/src/vocutouts/policy.py index ca1bcf8..65bcda4 100644 --- a/src/vocutouts/policy.py +++ b/src/vocutouts/policy.py @@ -33,16 +33,16 @@ def __init__(self, arq: ArqQueue, logger: BoundLogger) -> None: super().__init__(arq) self._logger = logger - async def dispatch(self, job: UWSJob, access_token: str) -> JobMetadata: + async def dispatch(self, job: UWSJob, token: str) -> JobMetadata: """Dispatch a cutout request to the backend. Parameters ---------- job The submitted job description. - access_token - Gafaelfawr access token used to authenticate to Butler server - in the backend. + token + Gafaelfawr token used to authenticate to the Butler server in the + backend. Returns ------- @@ -54,13 +54,9 @@ async def dispatch(self, job: UWSJob, access_token: str) -> JobMetadata: Currently, only one dataset ID and only one stencil are supported. This limitation is expected to be relaxed in a later version. """ - cutout_params = CutoutParameters.from_job_parameters(job.parameters) + params = CutoutParameters.from_job_parameters(job.parameters) return await self.arq.enqueue( - "cutout", - job.job_id, - cutout_params.ids, - [s.to_dict() for s in cutout_params.stencils], - access_token, + "cutout", job.job_id, params.ids, params.stencils, token=token ) def validate_params(self, params: list[UWSJobParameter]) -> None: diff --git a/src/vocutouts/uws/policy.py b/src/vocutouts/uws/policy.py index 2deea01..f3073c8 100644 --- a/src/vocutouts/uws/policy.py +++ b/src/vocutouts/uws/policy.py @@ -40,7 +40,7 @@ def __init__(self, arq: ArqQueue) -> None: self.arq = arq @abstractmethod - async def dispatch(self, job: UWSJob, access_token: str) -> JobMetadata: + async def dispatch(self, job: UWSJob, token: str) -> JobMetadata: """Dispatch a job to a backend worker. This method is responsible for converting UWS job parameters to the @@ -51,9 +51,9 @@ async def dispatch(self, job: UWSJob, access_token: str) -> JobMetadata: ---------- job Job to start. - access_token - Gafaelfawr access token used to authenticate to services used - by the backend on the user's behalf. + token + Gafaelfawr token used to authenticate to services used by the + backend on the user's behalf. Returns ------- diff --git a/src/vocutouts/uws/service.py b/src/vocutouts/uws/service.py index 9b3f883..9cdeb3b 100644 --- a/src/vocutouts/uws/service.py +++ b/src/vocutouts/uws/service.py @@ -226,9 +226,7 @@ async def list_jobs( user, phases=phases, after=after, count=count ) - async def start( - self, user: str, job_id: str, access_token: str - ) -> JobMetadata: + async def start(self, user: str, job_id: str, token: str) -> JobMetadata: """Start execution of a job. Parameters @@ -237,9 +235,9 @@ async def start( User on behalf of whom this operation is performed. job_id Identifier of the job to start. - access_token - Gafaelfawr access token used to authenticate to services used - by the backend on the user's behalf. + token + Gafaelfawr token used to authenticate to services used by the + backend on the user's behalf. Returns ------- @@ -257,7 +255,7 @@ async def start( raise PermissionDeniedError(f"Access to job {job_id} denied") if job.phase not in (ExecutionPhase.PENDING, ExecutionPhase.HELD): raise InvalidPhaseError("Cannot start job in phase {job.phase}") - metadata = await self._policy.dispatch(job, access_token) + metadata = await self._policy.dispatch(job, token) await self._storage.mark_queued(job_id, metadata) return metadata diff --git a/src/vocutouts/workers/cutout.py b/src/vocutouts/workers/cutout.py index 0d1872b..de18f05 100644 --- a/src/vocutouts/workers/cutout.py +++ b/src/vocutouts/workers/cutout.py @@ -8,13 +8,10 @@ import os from datetime import timedelta -from typing import Any from urllib.parse import urlparse from uuid import UUID -import astropy.units as u import structlog -from astropy.coordinates import Angle, SkyCoord from lsst.afw.geom import SinglePolygonException from lsst.daf.butler import LabeledButlerFactory from lsst.image_cutout_backend import ImageCutoutBackend, projection_finders @@ -23,6 +20,7 @@ from safir.logging import configure_logging from structlog.stdlib import BoundLogger +from ..models.stencils import CircleStencil, PolygonStencil, Stencil from ..uws.exceptions import TaskFatalError, TaskUserError from ..uws.models import ErrorCode, UWSJobResult from ..uws.workers import UWSWorkerConfig, build_worker @@ -33,7 +31,7 @@ __all__ = ["WorkerSettings"] -def _get_backend(butler_label: str, access_token: str) -> ImageCutoutBackend: +def _get_backend(butler_label: str, token: str) -> ImageCutoutBackend: """Given the Butler label, retrieve or build a backend. The dataset ID will be a URI of the form ``butler://