diff --git a/.gitignore b/.gitignore index 222ff34..5f3138a 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ cython_debug/ # Direnv .envrc .zed/ +build/ diff --git a/Justfile b/Justfile index 07e305f..32c847a 100644 --- a/Justfile +++ b/Justfile @@ -26,6 +26,10 @@ update-blanco: update-saao: curl https://ocsio.saao.ac.za/api/instruments/ | codegen/lco/generator.py SAAO > src/aeonlib/ocs/saao/instruments.py +# Update the CFHT generated models via swagger +update-cfht: + codegen/cfht/generator.py > src/aeonlib/cfht/models.py + # Update all generated instrument files update-all: update-lco update-soar update-saao update-blanco @echo "All updates completed" diff --git a/codegen/cfht/generator.py b/codegen/cfht/generator.py new file mode 100755 index 0000000..11797e2 --- /dev/null +++ b/codegen/cfht/generator.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import sys +from typing import Any + +import httpx +import yaml +from datamodel_code_generator import ( + DataModelType, + Formatter, + GenerateConfig, + InputFileType, + OpenAPIScope, + TargetPydanticVersion, + generate, +) +from datamodel_code_generator import ( + Error as DataModelCodegenError, +) + +OPENAPI_SPEC_URL = "https://hou-stage.cfht.hawaii.edu/api-docs/pi_api.yaml" +GENERATED_HEADER = "# Auto generated file, do not edit\n# ruff: noqa: E741\n" + + +def generate_models(openapi_document: dict[str, Any]) -> str: + config = GenerateConfig( + input_file_type=InputFileType.OpenAPI, + input_filename="cfhtopenapi.yaml", + openapi_scopes=[OpenAPIScope.Schemas], + output_model_type=DataModelType.PydanticV2BaseModel, + base_class="aeonlib.cfht.base_model.CFHTBaseModel", + target_pydantic_version=TargetPydanticVersion.V2_11, + use_annotated=True, + field_constraints=True, + set_default_enum_member=True, + snake_case_field=True, + allow_population_by_field_name=True, + type_overrides={"DoubleValue": "aeonlib.cfht.types.DoubleValue"}, + formatters=[Formatter.BUILTIN], + ) + + result = generate(openapi_document, config=config) + if not isinstance(result, str): + raise TypeError( + "Expected string output from datamodel-code-generator " + f"got {type(result).__name__}." + ) + + return GENERATED_HEADER + result + "\n" + + +def main() -> int: + response = httpx.get(OPENAPI_SPEC_URL) + response.raise_for_status() + document = yaml.safe_load(response.content) + generated = generate_models(document) + _ = sys.stdout.write(generated) + + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (yaml.YAMLError, DataModelCodegenError, OSError, TypeError) as exc: + print(f"CFHT model generation failed: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/examples/cfht.ipynb b/examples/cfht.ipynb new file mode 100644 index 0000000..90160ee --- /dev/null +++ b/examples/cfht.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5accbf56-995b-4a45-9d64-701be9fd9b49", + "metadata": {}, + "source": [ + "# CFHT AEONLib Demonstration Notebook\n", + "The online tests are also a good reference and are more extensive. See tests/cfht/test_online.py" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "08a7f23c-7c4e-4a1f-9481-562a2a391ad0", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "# Import generic Aeonlib models, as well as CFHT facilties\n", + "from aeonlib.cfht.facility import CFHTFacility\n", + "from aeonlib.cfht.conversions import target_data_from_aeon\n", + "from aeonlib.cfht.models import Instrument, TargetDataMagnitude\n", + "from aeonlib.conf import settings\n", + "from aeonlib.models import SiderealTarget" + ] + }, + { + "cell_type": "markdown", + "id": "a7097b42-3a61-4a13-bc61-1543c231037f", + "metadata": {}, + "source": [ + "## Initialize the CFHT facility and select an observing program" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c10a85c8-613b-4a57-8de6-2c0c107f2b51", + "metadata": {}, + "outputs": [], + "source": [ + "# Normally credentials are automatically picked up by the environment. For this notebook we supply them directly\n", + "settings.cfht_access_token = \"\"\n", + "settings.cfht_api_root = \"https://api-stage.cfht.hawaii.edu/\"\n", + "facility = CFHTFacility(settings=settings)\n", + "\n", + "# Get a list of Programs available to us, and select the one we wish to use. Alternatively, the facility can be\n", + "# instatiated with a program token directly, which is probably what you'd want to use in normal circumstances.\n", + "programs = facility.programs()\n", + "if not programs:\n", + " raise ValueError(\"No programs found. Check the Kealahou Phase2 Tool\")\n", + "# Set the first available program as the active one\n", + "program = programs[0]\n", + "facility.select_program(program)" + ] + }, + { + "cell_type": "markdown", + "id": "44a88310-a8b5-4bd2-a0d2-5885728572d1", + "metadata": {}, + "source": [ + "## Define a target\n", + "We use a generic AEONLib target as a starting point here, as it makes it easier to share between other non-CFHT facilities. If CFHT is the only facility you will be using, it might be easier to construct an aeonlib.cfht.models.TargetData instance directly." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8707a1a0-fddc-41f0-b931-049c56703129", + "metadata": {}, + "outputs": [], + "source": [ + "# Start with the AEONLib target\n", + "sidereal_target = SiderealTarget(\n", + " name=\"AEONLib CFHT Notebook Target\",\n", + " type=\"ICRS\",\n", + " ra=320.11,\n", + " dec=-42.0\n", + ")\n", + "# Use it to bootstrap a full CFHT TargetData\n", + "target_data = target_data_from_aeon(sidereal_target)\n", + "# Fill in CFHT specific data\n", + "target_data.token = f\"{facility.program_token}-{random.randint(1000000000, 9999999999)}\"\n", + "target_data.magnitude = TargetDataMagnitude(ab=10.0)\n", + "target_data.temperature_effective = 1234.5\n", + "target_data.standard_star = False\n", + "target_data.pointing_offset_token = f\"00AZ00-PO+{Instrument.megacam.value}+1\"" + ] + }, + { + "cell_type": "markdown", + "id": "2ac0a48f-bd0f-43a0-912f-6c4e7d0340a9", + "metadata": {}, + "source": [ + "## Create and list targets" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7df70290-4fd6-4958-b59e-cb6aee52d32c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "token='25BE25-4399809431' name='AEONLib CFHT Notebook Target' label=5 version=1 fixed_target=TargetDataFixedTarget(coordinate=SkyCoordinate(ra=320.11, dec=-42.0), proper_motion=FixedTargetProperMotion(ra_mas=None, dec_mas=None), computed_coordinate=None, estimated_radial_velocity_kmps=None) moving_target=None magnitude=TargetDataMagnitude(u=None, b=None, v=None, r=None, i=None, g=None, j=None, h=None, k=None, uu=None, gg=None, rr=None, ii=None, zz=None, ab=10.0) temperature_effective=1234.5 standard_star=None linked_target_identifiers=None finding_chart=[] pointing_offset_token='00AZ00-PO+MEGACAM+1' pointing_offset=PointingOffsetData(token='00AZ00-PO+MEGACAM+1', name='1', offset=OffsetCoordinate(ra_offset=None, dec_offset=None, exposure_number=None), label=None, version=None, user_token='SYSTEM', instrument=, is_system=True)\n" + ] + } + ], + "source": [ + "target = facility.create_or_update_target(target_data, Instrument.megacam)\n", + "all_targets = facility.targets()\n", + "try:\n", + " found_target = next(t for t in all_targets if t.token == target.token)\n", + " print(found_target)\n", + "except StopIteration:\n", + " raise ValueError(\"Created target did not appear in target list!\")" + ] + }, + { + "cell_type": "markdown", + "id": "728dc77e-24ce-411d-a549-c00e3eb85492", + "metadata": {}, + "source": [ + "## Delete created target\n", + "Now would be a good time to check the K2 tool in your browser to confirm the target appears in the targets section before it is deleted." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "828e4200-232c-4e82-b69b-f10ed839a6eb", + "metadata": {}, + "outputs": [], + "source": [ + "if target.token is not None:\n", + " facility.delete_target(target.token)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51eb8bf2-0f7c-4a01-b990-04d6bcc4a402", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 4aade38..dd484b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ build-backend = "hatchling.build" codegen = [ "jinja2>=3.1.6", "textcase>=0.2.1", + "datamodel-code-generator>=0.66.3", ] dev = [ @@ -55,6 +56,7 @@ log_cli_format = "%(levelname)s [%(name)s %(filename)s:%(lineno)s %(funcName)s() extend-exclude = [ "*.ipynb", "src/aeonlib/ocs/**/instruments.py", + "src/aeonlib/cfht/models.py" ] force-exclude = true diff --git a/src/aeonlib/cfht/base_model.py b/src/aeonlib/cfht/base_model.py new file mode 100644 index 0000000..3d8c17d --- /dev/null +++ b/src/aeonlib/cfht/base_model.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, ConfigDict + + +class CFHTBaseModel(BaseModel): + """Base model for CFHT generated schemas. + This allows us to configure all derived classes + if necessary + """ + + model_config = ConfigDict(validate_assignment=True) + + def api_dump(self, **kwargs): + return self.model_dump( + mode="json", + by_alias=True, + exclude_none=True, + **kwargs, + ) diff --git a/src/aeonlib/cfht/conversions.py b/src/aeonlib/cfht/conversions.py new file mode 100644 index 0000000..8a36754 --- /dev/null +++ b/src/aeonlib/cfht/conversions.py @@ -0,0 +1,60 @@ +from functools import singledispatch +from typing import Any, overload + +from aeonlib.models import TARGET_TYPES, SiderealTarget + +from .models import TargetData, TargetDataFixedTarget + +# This is a bit over-engineered at the moment. I started writing +# it before I realized that non-sidereal targets were going to require +# a lot more work. Well, the infrastructure is here if we ever implement +# non-sidereal target conversions. + + +class FixedTargetData(TargetData): + """CFHT target data guaranteed to contain sidereal target fields.""" + + fixed_target: TargetDataFixedTarget + + +def _sidereal_target_payload(target: SiderealTarget) -> dict[str, Any]: + return { + "name": target.name, + "fixed_target": { + "coordinate": { + "ra": target.ra.to_value("deg"), + "dec": target.dec.to_value("deg"), + }, + "proper_motion": { + "ra_mas": target.proper_motion_ra, + "dec_mas": target.proper_motion_dec, + }, + }, + } + + +@singledispatch +def _target_data_from_aeon(target: object) -> TargetData: + raise TypeError(f"Cannot convert {type(target).__name__} to CFHT TargetData") + + +@_target_data_from_aeon.register +def _convert_sidereal_target_data(target: SiderealTarget) -> FixedTargetData: + return FixedTargetData.model_validate(_sidereal_target_payload(target)) + + +# TODO: Register non-sidereal target conversions here. +# They will require a JPL Horizons client or API call because CFHT +# uses a series of astrometric coordinates instead of orbital elements. + + +@overload +def target_data_from_aeon(target: SiderealTarget) -> FixedTargetData: ... + + +@overload +def target_data_from_aeon(target: TARGET_TYPES) -> TargetData: ... + + +def target_data_from_aeon(target: TARGET_TYPES) -> TargetData: + return _target_data_from_aeon(target) diff --git a/src/aeonlib/cfht/facility.py b/src/aeonlib/cfht/facility.py new file mode 100644 index 0000000..07a9013 --- /dev/null +++ b/src/aeonlib/cfht/facility.py @@ -0,0 +1,179 @@ +from pprint import pprint +from typing import Any + +import httpx + +from aeonlib.conf import Settings +from aeonlib.conf import settings as default_settings + +from .models import ( + ExposureData, + Instrument, + ObservingGroupData, + ObservingTemplateData, + ProgramInfo, + TargetData, +) + + +class VersionMismatchError(ValueError): + """Raised when the version of the target does not match the server""" + + +class EntityNotFoundError(ValueError): + """Raised when the entity is not found""" + + +class InvalidResponseError(ValueError): + """Raised when the response is invalid""" + + +class ServerError(RuntimeError): + """Raised when the server returns an error""" + + +class CFHTFacility: + """CFHT Facility class""" + + def __init__( + self, settings: Settings = default_settings, program_token: str | None = None + ): + base_url = settings.cfht_api_root + if not base_url: + raise ValueError("AEON_CFHT_API_ROOT is not set") + access_token = settings.cfht_access_token + if not access_token: + raise ValueError("AEON_CFHT_ACCESS_TOKEN token is not set") + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + self._client = httpx.Client(base_url=base_url, headers=headers) + self.program_token = program_token + + def _request( + self, + method: str, + url: str, + *, + response_key: str = "entity", + **kwargs: Any, + ) -> Any: + response = self._client.request(method, url, **kwargs) + if response.status_code == httpx.codes.CONFLICT: + raise VersionMismatchError( + f"Version mismatch while requesting {response.request.url}" + ) + if response.status_code == httpx.codes.NOT_FOUND: + raise EntityNotFoundError(f"Entity not found: {response.request.url}") + try: + response.raise_for_status() + except httpx.HTTPStatusError as exc: + raise ServerError(f"CFHT API error: {exc}") from exc + if method == "DELETE": + return None + + try: + return response.json()[response_key] + except (ValueError, TypeError, KeyError) as exc: + raise InvalidResponseError( + f"CFHT API response from {response.request.url} did not contain " + f"the expected {response_key!r} key" + ) from exc + + def _program_request( + self, + method: str, + url: str, + *, + response_key: str = "entity", + **kwargs: Any, + ) -> Any: + if not self.program_token: + raise ValueError( + "Program must be set. Initialize the facility with program_token or use `select_program`" + ) + return self._request( + method, + f"programs/{self.program_token}/{url.lstrip('/')}", + response_key=response_key, + **kwargs, + ) + + def select_program(self, program: ProgramInfo) -> None: + if program.program_data is None: + raise ValueError("Program data is not set") + token = program.program_data.token + if not token: + raise ValueError("Program token is not set") + self.program_token = token + + def programs(self) -> list[ProgramInfo]: + """Get the list of observing programs""" + entities = self._request("GET", "programs/") + return [ProgramInfo.model_validate(entity) for entity in entities] + + def instruments(self) -> set[Instrument]: + """Return the instruments allocated to the selected program""" + if not self.program_token: + raise ValueError( + "Program must be set. Initialize the facility with program_token or use `select_program`" + ) + for program in self.programs(): + program_data = program.program_data + if program_data is None or program_data.token != self.program_token: + continue + + return { + allocation.instrument + for allocation in program_data.time_allocation or [] + if allocation.instrument is not None + } + + raise EntityNotFoundError(f"Program not found: {self.program_token}") + + def targets(self) -> list[TargetData]: + """Get the list of targets for a given program""" + entities = self._program_request("GET", "targets/") + return [TargetData.model_validate(target) for target in entities] + + def get_target(self, target_token: str) -> TargetData: + entity = self._program_request("GET", f"targets/{target_token}") + return TargetData.model_validate(entity) + + def delete_target(self, target_token: str) -> None: + self._program_request("DELETE", f"targets/{target_token}") + + def create_or_update_target( + self, target: TargetData, instrument: Instrument + ) -> TargetData: + version = {"value": target.version} if target.version is not None else None + data = { + "entity": target.api_dump(), + "lock_version": version, + "instrument": instrument.value, + } + entity = self._program_request("PUT", f"targets/{target.token}/", json=data) + return TargetData.model_validate(entity) + + def observing_templates(self) -> list[ObservingTemplateData]: + entities = self._program_request("GET", "observing-templates/") + return [ObservingTemplateData.model_validate(entity) for entity in entities] + + def create_observing_group( + self, observing_group: ObservingGroupData + ) -> ObservingGroupData: + data = {"entity": observing_group.api_dump()} + entity = self._program_request( + "PUT", f"observing-groups/{observing_group.token}/", json=data + ) + return ObservingGroupData.model_validate(entity) + + def delete_observing_group(self, observing_group_token: str) -> None: + self._program_request("DELETE", f"observing-groups/{observing_group_token}") + + def exposures(self) -> list[ExposureData]: + exposures = self._program_request("GET", "exposures/", response_key="exposure") + print(pprint(exposures)) + return [ExposureData.model_validate(exposure) for exposure in exposures] diff --git a/src/aeonlib/cfht/models.py b/src/aeonlib/cfht/models.py new file mode 100644 index 0000000..50378f7 --- /dev/null +++ b/src/aeonlib/cfht/models.py @@ -0,0 +1,2716 @@ +# Auto generated file, do not edit +# ruff: noqa: E741 +# generated by datamodel-codegen: +# filename: cfhtopenapi.yaml +# timestamp: 2026-08-21T16:42:09+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated + +from aeonlib.cfht.base_model import CFHTBaseModel +from aeonlib.cfht.types import DoubleValue +from pydantic import ConfigDict, Field + + +class AddSingleToSnrBlockRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier of the science program.') + ] = None + snr_block_token: Annotated[ + str | None, + Field( + description='Unique identifier of an existing SNR block to create the new observing\n group for.' + ), + ] = None + observing_group_token: Annotated[ + str | None, + Field( + description='Unique identifier of the observing group. If an observing group target\n with this token already exists, it will be updated, otherwise it will be\n created.\n\n Must begin with the program token followed by a dash, e.g.\n 20AE01-0123456789' + ), + ] = None + + +class Instrument(Enum): + unknown = 'UNKNOWN' + cfh12_k = 'CFH12K' + megacam = 'MEGACAM' + wircam = 'WIRCAM' + espadons = 'ESPADONS' + sitelle = 'SITELLE' + spirou = 'SPIROU' + wenaokeao = 'WENAOKEAO' + + +class AllocationDataAgency(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + letter: str | None = None + name: str | None = None + + +class BigInteger(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + value: str | None = None + + +class CatalogIdentifier(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + catalog: Annotated[str | None, Field(description='Catalog to reference.')] = None + identifier: Annotated[ + str | None, + Field( + description='Valid identifier in the catalog.\n\n Note: this ID will contain a catalog name, because spacing matters in catalogs, the full ID for an object is like\n M 4. Catalog is M, id is identifier could be 4, but to reference m4, you need the spaces.\n\n NB: These identity values can not be set in fits headers because they do not have to conform to\n the length restrictions.' + ), + ] = None + + +class ConfigDithering(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + is_staring: bool | None = None + + +class StartSequence(Enum): + object = 'OBJECT' + target = 'TARGET' + sky = 'SKY' + dark = 'DARK' + flat = 'FLAT' + snap = 'SNAP' + bias = 'BIAS' + focus = 'FOCUS' + align = 'ALIGN' + comparison = 'COMPARISON' + acquire = 'ACQUIRE' + fabry_perot = 'FABRY_PEROT' + twilight_flats = 'TWILIGHT_FLATS' + photometry = 'PHOTOMETRY' + sfocus = 'SFOCUS' + + +class ConfigNodding(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + start_sequence: StartSequence | None = None + target_db: float | None = None + + +class DoubleMinMax(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + min: float | None = None + max: float | None = None + + +class DisplayUnit(Enum): + days = 'DAYS' + hours = 'HOURS' + minutes = 'MINUTES' + seconds = 'SECONDS' + + +class Duration(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + millis: int | None = None + display_unit: Annotated[ + DisplayUnit | None, + Field( + description='Only needed for rendering UI, should display the duration using the units specified.\n If unset, assume DAYS' + ), + ] = None + + +class DynamicExposureTimeLimit(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + minimum_multiplier: float | None = None + maximum_multiplier: float | None = None + + +class Errors(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + messages: list[str] | None = None + + +class EspadonsStatusExposureMeterProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + snr: float | None = None + + +class EspadonsStatusGuiderSeeingProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + guider_seeing: float | None = None + + +class ExposureDataAttribution(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: str | None = None + observing_block_label: Annotated[ + str | None, + Field( + description='If displaying the observing block to the user, this uniquely identifies it in the context\n of an observing group, i.e. OG3-2' + ), + ] = None + observing_block_label_int: Annotated[ + int | None, Field(description='Label of the observing block') + ] = None + observing_block_token: Annotated[ + str | None, + Field( + description='Token of the observing block this exposure is attributed to.' + ), + ] = None + observing_group_label: Annotated[ + int | None, + Field( + description='Label of the observing group this exposure is attributed to.' + ), + ] = None + observing_group_token: Annotated[ + str | None, + Field( + description='Token of the observing group this exposure is attributed to.' + ), + ] = None + observing_component_token: Annotated[ + str | None, + Field( + description='Token of the observing component this exposure is attributed to.' + ), + ] = None + exposure_number: Annotated[ + int | None, Field(description='exposure number this exposure is attributed to.') + ] = None + instrument_run: Annotated[ + str | None, Field(description='the queue run this exposure was taken under') + ] = None + attributing_agency: Annotated[str | None, Field(description='the agency id')] = None + tac_rank: int | None = None + tac_grade: str | None = None + + +class ExpStatus(Enum): + unobserved = 'UNOBSERVED' + observed = 'OBSERVED' + processed = 'PROCESSED' + graded = 'GRADED' + validated = 'VALIDATED' + rejected = 'REJECTED' + + +class ExposureDataWenaStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + + +class ExposureDataWircamStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + iq_uncertainty: float | None = None + absorption: float | None = None + absorption_uncertainty: float | None = None + number_of_stars_iq: int | None = None + number_of_stars_absorption: int | None = None + absolute_sky_level: float | None = None + elongation: float | None = None + mdcoords: int | None = None + mdrepeat: int | None = None + actual_filter: str | None = None + iq: float | None = None + + +class ExposureStatusFile(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + absolute_path: Annotated[ + str | None, + Field( + description='Absolute path to directory file is homed in. Starts with / and ends with /.' + ), + ] = None + base_name: Annotated[ + str | None, Field(description='Name of file without extension') + ] = None + ext: Annotated[str | None, Field(description='Name of extenion including .')] = None + + +class ExposureTime(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + desired_snr: float | None = None + exposure_time_ms: int | None = None + + +class FindingChartDataApproval(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + approved_at_millis: int | None = None + approved_by_usertoken: str | None = None + + +class FixedTargetProperMotion(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + ra_mas: Annotated[ + float | None, + Field( + description='Motion in the right ascension axis in milliarcseconds/year.' + ), + ] = None + dec_mas: Annotated[ + float | None, + Field(description='Motion in the declination axis in milliarcseconds/year.'), + ] = None + + +class GoogleProtobufAny(CFHTBaseModel): + model_config = ConfigDict( + extra='allow', + validate_by_name=True, + ) + field_type: Annotated[ + str | None, + Field(alias='@type', description='The type of the serialized message.'), + ] = None + + +class ITimeData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + exposure_time_millis: Annotated[ + int | None, + Field(description='Time spent collecting photons (in milliseconds).'), + ] = None + overhead_time_millis: Annotated[ + int | None, + Field( + description='All other times required for taking the exposure (in milliseconds).' + ), + ] = None + + +class Tracking(Enum): + unknown = 'Unknown' + sid = 'SID' + nonsid = 'NONSID' + sid_nog = 'SID_NOG' + nonsid_g = 'NONSID_G' + + +class ObservingMode(Enum): + polarimetry = 'Polarimetry' + spectroscopy_star_only = 'Spectroscopy_star_only' + spectroscopy_star_sky = 'Spectroscopy_star_sky' + + +class ReadoutMode(Enum): + normal = 'Normal' + slow = 'Slow' + fast = 'Fast' + + +class StokesParameter(Enum): + q = 'Q' + u = 'U' + v = 'V' + w = 'W' + i = 'I' + + +class InstrumentConfigurationDataEspadonsConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observing_mode: ObservingMode | None = None + readout_mode: ReadoutMode | None = None + stokes_parameter: StokesParameter | None = None + number_of_sequences: int | None = None + number_of_exposures: int | None = None + needs_snr: bool | None = None + snr_wave: float | None = None + snr: float | None = None + + +class Filter(Enum): + unknown = 'Unknown' + u = 'u' + g = 'g' + i = 'i' + r = 'r' + z = 'z' + gri = 'gri' + ca_hk = 'CaHK' + ha = 'Ha' + ha_off = 'HaOFF' + oiii = 'OIII' + oiiioff = 'OIIIOFF' + u_s = 'uS' + g_s = 'gS' + r_s = 'rS' + i_s = 'iS' + z_s = 'zS' + n393_s = 'N393S' + ha_s = 'HaS' + ha_offs = 'HaOFFS' + ti_os = 'TiOS' + cns = 'CNS' + oiiis = 'OIIIS' + phgs = 'PHGS' + m4112 = 'M4112' + m4376 = 'M4376' + + +class Binning(Enum): + one_one = 'one_one' + two_two = 'two_two' + three_three = 'three_three' + four_four = 'four_four' + + +class SnrMode(Enum): + none = 'NONE' + standard = 'STANDARD' + cumulative = 'CUMULATIVE' + zero_point = 'ZERO_POINT' + + +class Filter1(Enum): + unknown = 'UNKNOWN' + none = 'NONE' + c1 = 'C1' + c2 = 'C2' + c3 = 'C3' + c4 = 'C4' + sn1 = 'SN1' + sn2 = 'SN2' + sn3 = 'SN3' + sn4 = 'SN4' + sn5 = 'SN5' + sn6 = 'SN6' + + +class Binning1(Enum): + one_one = 'one_one' + two_two = 'two_two' + three_three = 'three_three' + + +class ResolutionMode(Enum): + no_resolution_mode = 'no_resolution_mode' + hires = 'hires' + medres = 'medres' + + +class ObservingMode1(Enum): + polarimetry = 'Polarimetry' + star = 'Star' + + +class Mode(Enum): + dark = 'Dark' + fabry_perot = 'Fabry_Perot' + wave_hc1 = 'Wave_Hc1' + wave_hc2 = 'Wave_Hc2' + flat = 'FLAT' + + +class SkyObservation(Enum): + none = 'None' + before = 'Before' + after = 'After' + before_and_after = 'BeforeAndAfter' + + +class InstrumentConfigurationDataWenaConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + + +class Filter2(Enum): + unknown = 'UNKNOWN' + w = 'W' + h = 'H' + j = 'J' + ks = 'Ks' + y = 'Y' + h2 = 'H2' + k_cont = 'KCont' + ch4_on = 'CH4On' + ch4_off = 'CH4Off' + low_oh1 = 'LowOH1' + low_oh2 = 'LowOH2' + br_g = 'BrG' + co = 'CO' + c_onar = 'COnar' + + +class InstrumentConfigurationDataWircamConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + filter: Filter2 | None = None + is_microdithering: bool | None = None + exposures_per_dp_position: Annotated[ + int | None, Field(description='Must always be > 0') + ] = None + accurate_pointing: bool | None = None + defocus: float | None = None + is_staring: bool | None = None + + +class Int32Value(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + value: Annotated[int | None, Field(description='The int32 value.')] = None + + +class Int64Value(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + value: Annotated[int | None, Field(description='The int64 value.')] = None + + +class LegacyReel(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + reel_og_token: Annotated[ + str | None, Field(description='Token of the linked observing group.') + ] = None + delay: Annotated[ + Duration | None, + Field( + description='Required wait after the observation of the linked observing group before\n this observing group can be observed.' + ), + ] = None + window: Annotated[ + Duration | None, + Field( + description='Maximum allowed wait after the delay until this observing group can no\n longer be observed.' + ), + ] = None + reel_og_label: Annotated[ + int | None, + Field( + description='Label of the observing group which must be observed before this one.\n\n storage: transient\n api_visibility: read only' + ), + ] = None + + +class ListOfIdentifiers(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + catalog_identifier: list[CatalogIdentifier] | None = None + + +class MegacamStatusElixirProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + iq: float | None = None + sky_background: float | None = None + snr: float | None = None + zeropoint: float | None = None + + +class ObservingBlockConditionRequisite(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + requisite_observing_block_index: Annotated[ + int | None, + Field( + description='This should match the index of the observing block in the\n read_observing_block list.' + ), + ] = None + wait_period_min_millis: Annotated[ + int | None, + Field( + description='Minimum time to wait after requisite observing block exposure has\n been taken.' + ), + ] = None + wait_period_millis: Annotated[ + int | None, + Field( + description='Optimal to wait after requisite observing block exposure has\n been taken.' + ), + ] = None + wait_period_max_millis: Annotated[ + int | None, + Field( + description='Minimum time to wait after requisite observing block exposure has\n been taken.' + ), + ] = None + unbounded: Annotated[ + bool | None, + Field( + description='If this observation can happen any time after the requisite.' + ), + ] = None + + +class ObservingBlockConditionWindow(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observe_after: Annotated[ + int | None, + Field( + description='Start of the observation window, in Unix time milliseconds.' + ), + ] = None + observe_before: Annotated[ + int | None, + Field(description='End of the observation window, in Unix time milliseconds.'), + ] = None + + +class ObservingBlockObservingComponent(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + target_token: Annotated[ + str | None, + Field( + description='Unique identifier of the target used in this observing component.\n\n When creating or updating an observing group, this must be an\n existing target, or on the map of the request to be created.' + ), + ] = None + observing_template_token: Annotated[ + str | None, + Field( + description='Unique identifier of the observing template used in this\n observing component.\n\n When creating or updating an observing group, this must be an\n existing observing template, or on the map of the request to be\n created.' + ), + ] = None + target_label: Annotated[ + int | None, + Field( + description='Label of the target used in this observing component.\n This value is only set on the read_observing_block.\n\n api_visibility: read only' + ), + ] = None + observing_template_label: Annotated[ + int | None, + Field( + description='Label of the observing template used in this observing component.\n This value is only set on the read_observing_block.\n\n api_visibility: read only' + ), + ] = None + snr_block_token: Annotated[ + str | None, + Field( + description='Unique identifier of the SNR block used in this observing\n component, if any. Must be set if and only if the observing\n component uses a MegaCam cumulative SNR mode observing template.\n\n If this is set when creating or updating an observing group,\n the observing component will be tied to the SNR block with this\n token if it already exists, otherwise a new SNR block will be\n created using this token.' + ), + ] = None + + +class ObservingGroupPriority(Enum): + unknown_og_priority = 'UNKNOWN_OG_PRIORITY' + high = 'HIGH' + medium = 'MEDIUM' + low = 'LOW' + inactive = 'INACTIVE' + + +class ObservingGroupState(Enum): + not_started = 'NOT_STARTED' + observed = 'OBSERVED' + started = 'STARTED' + validated = 'VALIDATED' + + +class ExposureType(Enum): + object = 'OBJECT' + target = 'TARGET' + sky = 'SKY' + dark = 'DARK' + flat = 'FLAT' + snap = 'SNAP' + bias = 'BIAS' + focus = 'FOCUS' + align = 'ALIGN' + comparison = 'COMPARISON' + acquire = 'ACQUIRE' + fabry_perot = 'FABRY_PEROT' + twilight_flats = 'TWILIGHT_FLATS' + photometry = 'PHOTOMETRY' + sfocus = 'SFOCUS' + + +class OgPriority(Enum): + unknown_og_priority = 'UNKNOWN_OG_PRIORITY' + high = 'HIGH' + medium = 'MEDIUM' + low = 'LOW' + inactive = 'INACTIVE' + + +class State(Enum): + not_started = 'NOT_STARTED' + observed = 'OBSERVED' + started = 'STARTED' + validated = 'VALIDATED' + + +class TargetType(Enum): + object = 'OBJECT' + target = 'TARGET' + sky = 'SKY' + dark = 'DARK' + flat = 'FLAT' + snap = 'SNAP' + bias = 'BIAS' + focus = 'FOCUS' + align = 'ALIGN' + comparison = 'COMPARISON' + acquire = 'ACQUIRE' + fabry_perot = 'FABRY_PEROT' + twilight_flats = 'TWILIGHT_FLATS' + photometry = 'PHOTOMETRY' + sfocus = 'SFOCUS' + + +class DitherScheme(Enum): + none = 'NONE' + standard = 'STANDARD' + group = 'GROUP' + + +class ObservingGroupDataObservingBlock(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observing_component: Annotated[ + list[ObservingBlockObservingComponent] | None, + Field( + description='List of observing components that comprise this observing block.' + ), + ] = None + token: Annotated[ + str | None, + Field( + description='Unique identifier for the observing block.\n\n api_visibility: operations read only' + ), + ] = None + label: Annotated[ + int | None, + Field( + description='Index of the observing block within the program, set by the system\n automatically. Used for reference in communications with QSO staff.\n\n storage: transient\n api_visibility: read only' + ), + ] = None + + +class ObservingGroupDeleteResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + + +class ObservingTemplateDeleteResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + + +class OffsetCoordinate(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + ra_offset: Annotated[ + float | None, + Field( + description='Offset in the Right Ascension axis in arcsec of degrees.', + ge=-1296000.0, + le=1296000.0, + title='Right Ascension Offset', + ), + ] = None + dec_offset: Annotated[ + float | None, + Field( + description='Offset in the Declination axis in arcsec of degrees.', + ge=-324000.0, + le=324000.0, + title='Declination Offset', + ), + ] = None + exposure_number: Annotated[ + int | None, Field(description='Exposure number', le=99, title='Exposure number') + ] = None + + +class PhaseScheduleObservingGroupPhaseSchedule(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + earliest_observe_date_millis: Annotated[ + int | None, + Field(description='The earliest moment to observe, in Unix time milliseconds.'), + ] = None + tolerance: Annotated[ + Duration | None, + Field( + description='The amount of time before or after earliest_observe_date_millis that\n the initial observation can be made.' + ), + ] = None + period: Annotated[ + Duration | None, + Field(description='The amount of time to wait between each iteration.'), + ] = None + period_tolerance: Annotated[ + Duration | None, + Field( + description='The amount of time before or after the period that each iteration\n can be made.' + ), + ] = None + + +class PhotometryAperture(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + optimal_aperture: bool | None = None + fixed_aperture: Annotated[ + float | None, Field(description='Radius of the aperture in arcsec') + ] = None + ninety_six_percent_flux_aperture: bool | None = None + + +class PointingOffsetData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + token: str | None = None + name: str | None = None + offset: OffsetCoordinate | None = None + label: int | None = None + version: int | None = None + user_token: Annotated[ + str | None, + Field( + description='this is the PI of the program that this entity will be recorded for\nif null should be derived using the program_data.contact_info.user_token\nif null, then a system ?? gross' + ), + ] = None + instrument: Annotated[ + Instrument | None, Field(description='the instrument for the pointing') + ] = None + is_system: bool | None = None + + +class PointingOffsetDeleteResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + + +class PointingOffsetGetResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + entity: PointingOffsetData | None = None + error: Errors | None = None + + +class PointingOffsetListResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + entity: list[PointingOffsetData] | None = None + error: Errors | None = None + + +class PointingOffsetUpdateRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier your science program.') + ] = None + token: str | None = None + lock_version: Int32Value | None = None + entity: PointingOffsetData | None = None + + +class PointingOffsetUpdateResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + entity: PointingOffsetData | None = None + + +class ProgramType(Enum): + regular = 'REGULAR' + target_of_opportunity = 'TARGET_OF_OPPORTUNITY' + calibration = 'CALIBRATION' + snapshot = 'SNAPSHOT' + + +class ProgramDataComments(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + question: str | None = None + comment: str | None = None + + +class ProgramDataContactInfo(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + pi_first_name: str | None = None + pi_last_name: str | None = None + pi_email: str | None = None + user_token: str | None = None + + +class ProgramInfoPiInfo(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + ldap_username: str | None = None + email_address: str | None = None + first_name: str | None = None + last_name: str | None = None + legacy_username: str | None = None + user_token: str | None = None + + +class RollupValue(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + requested: int | None = None + started: int | None = None + observed: int | None = None + validated: int | None = None + + +class SingleObservingGroup(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observing_block: ObservingGroupDataObservingBlock | None = None + + +class CalibrationPosition(Enum): + no_calibration_position = 'no_calibration_position' + zenith = 'zenith' + target = 'target' + + +class CalibrationMode(Enum): + no_calibration_mode = 'no_calibration_mode' + laser = 'laser' + phase = 'phase' + + +class SitelleConfigurationDataCalibration(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + calibration_position: CalibrationPosition | None = None + calibration_mode: CalibrationMode | None = None + + +class SitelleStatusCamera(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + iq: float | None = None + iq_rms: float | None = None + pixel_shift_x: float | None = None + pixel_shift_y: float | None = None + number_of_stars: int | None = None + me: float | None = None + + +class SkyCoordinate(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + ra: Annotated[ + float | None, + Field( + description='Right Ascension in degrees.', le=360.0, title='Right Ascension' + ), + ] = None + dec: Annotated[ + float | None, + Field( + description='Declination in degrees.', + ge=-90.0, + le=90.0, + title='Declination', + ), + ] = None + + +class SpirouStatusExposureMeterProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + flux: float | None = None + snr: float | None = None + mid_exposure_date_millis: float | None = None + spemsnr: Annotated[float | None, Field(description='Calculated SNR')] = None + spemsnrc: float | None = None + + +class SpirouStatusGuiderProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + etime: float | None = None + isustate: str | None = None + sbdens_p: float | None = None + hole_position_offset_x: float | None = None + hole_position_offset_y: float | None = None + star_to_null_position_offset_x: float | None = None + star_to_null_position_offset_y: float | None = None + magnitude_estimate: float | None = None + seeing_estimate: float | None = None + sgisust: Annotated[str | None, Field(description='per 160676372')] = None + sggdst: str | None = None + sgfrmrt: float | None = None + sgetime: float | None = None + sgnullx: float | None = None + sgnully: float | None = None + sgwinx0: float | None = None + sgwiny0: float | None = None + sgwinx1: float | None = None + sgwiny1: float | None = None + sgoffx0: float | None = None + sgoffy0: float | None = None + sgoffx1: float | None = None + sgoffy1: float | None = None + sgholex: float | None = None + sgholey: float | None = None + sgcholex: float | None = None + sgcholey: float | None = None + sgeholex: float | None = None + sgeholey: float | None = None + sgcstarx: float | None = None + sgcstary: float | None = None + sgestarx: float | None = None + sgestary: float | None = None + sgcnoffx: float | None = None + sgcnoffy: float | None = None + sgcsee: float | None = None + sgesee: float | None = None + sgcmagn: float | None = None + sgemagn: float | None = None + sgssee: float | None = None + trg_type: str | None = None + + +class SpirouStatusPipelineProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + snr10: float | None = None + snr34: float | None = None + snr44: float | None = None + dprtype: str | None = None + ccfmask: float | None = None + ccfmacpp: float | None = None + ccfrv: float | None = None + ccfcontr: float | None = None + ccfrvc: float | None = None + ccffwhm: float | None = None + ccfmask_str: str | None = None + + +class Status(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + code: Annotated[ + int | None, + Field( + description='The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].' + ), + ] = None + message: Annotated[ + str | None, + Field( + description='A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.' + ), + ] = None + details: Annotated[ + list[GoogleProtobufAny] | None, + Field( + description='A list of messages that carry the error details. There is a common set of message types for APIs to use.' + ), + ] = None + + +class SubTotalsData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + etime: RollupValue | None = None + overheadtime: RollupValue | None = None + exposure_count: RollupValue | None = None + child_count: RollupValue | None = None + etime_configured: RollupValue | None = None + overhead_time_configured: RollupValue | None = None + completeness: Annotated[ + float | None, Field(description='Percentage completed for the entity.') + ] = None + + +class TargetDataFixedTarget(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + coordinate: Annotated[ + SkyCoordinate | None, + Field(description='Coordinate for this target, specified in ICRS (ep=J2000).'), + ] = None + proper_motion: Annotated[ + FixedTargetProperMotion | None, + Field(description='Proper motions for this target.'), + ] = None + computed_coordinate: Annotated[ + SkyCoordinate | None, + Field( + description='Internal use only.\n The coordinate of the target at the time of observation,\n specified in ICRS (ep=J2000).\n\n storage: transient\n api_visibility: operations read only' + ), + ] = None + estimated_radial_velocity_kmps: Annotated[ + DoubleValue | None, + Field( + description='Expected radial velocity of the target in kilometers/second.\n\n Used for targets observed with SPIRou.' + ), + ] = None + + +class TargetDataMagnitude(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + u: Annotated[DoubleValue | None, Field(alias='U', description='U-band magnitude')] = ( + None + ) + b: Annotated[DoubleValue | None, Field(alias='B', description='B-band magnitude')] = ( + None + ) + v: Annotated[ + DoubleValue | None, + Field( + alias='V', + description='V-band magnitude\n\n Required for targets observed with ESPaDonS.', + ), + ] = None + r: Annotated[DoubleValue | None, Field(alias='R', description='R-band magnitude')] = ( + None + ) + i: Annotated[DoubleValue | None, Field(alias='I', description='I-band magnitude')] = ( + None + ) + g: Annotated[DoubleValue | None, Field(alias='G', description='G-band magnitude')] = ( + None + ) + j: Annotated[DoubleValue | None, Field(alias='J', description='J-band magnitude')] = ( + None + ) + h: Annotated[ + DoubleValue | None, + Field( + alias='H', + description='H-band magnitude\n\n Required for targets observed with SPIRou.', + ), + ] = None + k: Annotated[DoubleValue | None, Field(alias='K', description='K-band magnitude')] = ( + None + ) + uu: Annotated[DoubleValue | None, Field(description='u-band magnitude')] = None + gg: Annotated[DoubleValue | None, Field(description='g-band magnitude')] = None + rr: Annotated[DoubleValue | None, Field(description='r-band magnitude')] = None + ii: Annotated[DoubleValue | None, Field(description='i-band magnitude')] = None + zz: Annotated[DoubleValue | None, Field(description='z-band magnitude')] = None + ab: Annotated[ + DoubleValue | None, + Field( + alias='AB', + description='AB magnitude\n\n Required for targets observed with MegaCam.', + ), + ] = None + + +class TargetDeleteResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + + +class TargetIdentifiers(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + catalog_identifier: Annotated[ + dict[str, ListOfIdentifiers] | None, + Field( + description='All catalogs resolved by simbad for this target\n\n key: catalog name, catalog_identifier.key === catalog_identifier.get(key).catalog\n value: catalog identifier.' + ), + ] = None + two_mass: CatalogIdentifier | None = None + gaia1: CatalogIdentifier | None = None + gaia2: CatalogIdentifier | None = None + resolved_catalog_identifier: Annotated[ + CatalogIdentifier | None, + Field( + description='Contains the identifier returned by the resolution service which it determined was the\n catalog the lookup was searching in.' + ), + ] = None + + +class TargetMonitoringObservingGroupTimeConstraint(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + number_of_iterations: Annotated[ + int | None, Field(description='How many observations to take.') + ] = None + minimum_number_of_iterations: Annotated[ + int | None, + Field( + description='Minimum number of observations which still provides science value.\n\n If weather becomes an issue, this is used to determine if additional\n observations should be tried, or the whole observing group aborted\n to prioritize other observations.\n\n Cannot be greater than requested number of iterations.' + ), + ] = None + time_interval: Annotated[ + Duration | None, Field(description='The time interval between observations.') + ] = None + time_interval_tolerance_min: Annotated[ + Duration | None, + Field( + description='How much earlier an iteration can be observed before the time\n interval has elapsed.\n\n At least one of tolerance before or after must be non-zero.' + ), + ] = None + time_interval_tolerance_max: Annotated[ + Duration | None, + Field( + description='How much later an iteration can be observed after the time interval\n has elapsed.\n\n At least one of tolerance before or after must be non-zero.' + ), + ] = None + t0_millis: Annotated[ + int | None, + Field( + description="Time the observing group should start being observed, in Unix time\n milliseconds.\n\n If omitted, CFHT will start observing at the QC's discretion." + ), + ] = None + within_same_camera_run: Annotated[ + bool | None, + Field( + description='Whether all iterations need to occur within the same camera run.' + ), + ] = None + + +class View(Enum): + requested = 'REQUESTED' + actual = 'ACTUAL' + + +class TargetTimeData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + requested_integration_time: ITimeData | None = None + actual_integration_time: ITimeData | None = None + view: Annotated[ + View | None, + Field( + description='Which time clients should prefer showing users when presenting a single integration time for this data.\n\nREQUESTED - prefer requested_integration_time\nACTUAL - prefer actual_integration_time' + ), + ] = None + exposure_count: Annotated[ + int | None, + Field( + description='The total number of exposures represented by the values here.\n\nThis value is the same for both views.' + ), + ] = None + child_count: Annotated[ + int | None, + Field( + description='The total number of child entities represented by the values here.\n\nIf this is a program, these are observing groups.\nIf this is an observing group, these are observing blocks.\nIf this is an observing block, these are exposures.\n\nUseful for e.g. seeing the number of iterations configuredor completed on an Observing Group.\nThis value is the same for both views.' + ), + ] = None + + +class TelescopePatternDataConfig(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + name: str | None = None + description: str | None = None + charge_model: Annotated[ + str | None, Field(description='Used for calculating cost for this pattern') + ] = None + dithering: ConfigDithering | None = None + nodding: ConfigNodding | None = None + scale: float | None = None + sybase_pat_id: BigInteger | None = None + is_system: bool | None = None + + +class Type(Enum): + object = 'OBJECT' + target = 'TARGET' + sky = 'SKY' + dark = 'DARK' + flat = 'FLAT' + snap = 'SNAP' + bias = 'BIAS' + focus = 'FOCUS' + align = 'ALIGN' + comparison = 'COMPARISON' + acquire = 'ACQUIRE' + fabry_perot = 'FABRY_PEROT' + twilight_flats = 'TWILIGHT_FLATS' + photometry = 'PHOTOMETRY' + sfocus = 'SFOCUS' + + +class TelescopePatternDataOffset(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + exposure_number: Annotated[ + int | None, + Field( + description='TODO: deprecate, no need to have duplicate information here.' + ), + ] = None + offset: OffsetCoordinate | None = None + type: Annotated[ + Type | None, Field(description='For dithering patterns type is always target') + ] = None + + +class TelescopePatternDeleteResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + + +class TimeAccountingStatus(Enum): + not_started = 'NOT_STARTED' + started = 'STARTED' + observed = 'OBSERVED' + validated = 'VALIDATED' + + +class TimeAccountingData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + configured_time: TargetTimeData | None = None + charged_time: TargetTimeData | None = None + validated_time_on_sky: TargetTimeData | None = None + time_on_sky: TargetTimeData | None = None + time_accounting_status: Annotated[ + TimeAccountingStatus | None, + Field(description='Lifecycle state this entity is in.'), + ] = None + completion_ratio: Annotated[ + float | None, + Field( + description='Entity completion ratio, representing how much of the time configured has been charged.\n\nEqual to total charged time (as requested) divided by total configured time. For programs, if the total configured time is greater than the time allocated to the program, we divide by the time allocated instead.\nNote that if a program has INACTIVE observing groups that already have charged time, this could result in an inflated value here.', + le=1.0, + ), + ] = None + + +class TimeConstraint(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + window: Annotated[ + list[ObservingBlockConditionWindow] | None, + Field( + description='Time windows during which the observation can be made.\n\n Windows can not overlap, and will always be stored in temporal order.' + ), + ] = None + + +class UpdateProgramCommentRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier for your science program') + ] = None + comment: Annotated[ + str | None, Field(description='Comment set by the PI for the CFHT QSO Team') + ] = None + + +class UpdateProgramCommentsRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier for your science program') + ] = None + program_comment: Annotated[ + str | None, Field(description='Comment set by the PI for the CFHT QSO Team') + ] = None + operations_comment: Annotated[ + str | None, Field(description='Comment set by the CFHT QSO Team') + ] = None + + +class UpenaProcessingResultH2oMm(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + raw_density: float | None = None + at_zenith: float | None = None + + +class AllocationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + run_id: str | None = None + time_allocated_millis: int | None = None + tac_rank: int | None = None + tac_grade: str | None = None + instrument: Instrument | None = None + agency: AllocationDataAgency | None = None + cfht_rank: int | None = None + + +class Constraint(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + image_quality: DoubleMinMax | None = None + sky_background_max: float | None = None + airmass_max: float | None = None + extinction_max: float | None = None + moon_distance_arc_length_min: float | None = None + photometric: bool | None = None + name: str | None = None + sybase_cons_id: BigInteger | None = None + h2o_vapor_max: float | None = None + read_sky_background_name: str | None = None + sky_background_name: str | None = None + + +class EspadonsStatusUpenaProcessingResult(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + iq: float | None = None + h2o_column_density_from_telluric_lines: float | None = None + radial_velocity_corrections: float | None = None + snr_per_ccd_bin: float | None = None + snr_per_spec_bin: float | None = None + actual_wavelength: float | None = None + snr_per_ccd_bin_polar: float | None = None + snr_per_spec_bin_polar: float | None = None + h2o_mm_data: UpenaProcessingResultH2oMm | None = None + + +class ExposureDataEspadonsStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + instr_mode: str | None = None + actual_wavelength: float | None = None + snr_exp_meter: float | None = None + snr_wave_meas_int: float | None = None + snr_wave_meas_pol: float | None = None + snr_meas_int_per_ccd_bin: float | None = None + snr_meas_int_per_spec_bin: float | None = None + snr_meas_polar_ccd_bin: float | None = None + snr_meas_polar_spec_bin: float | None = None + trg_type: str | None = None + guider_seeing: float | None = None + guider_seeing_processing_results: ( + EspadonsStatusGuiderSeeingProcessingResult | None + ) = None + exposure_meter_processing_results: ( + EspadonsStatusExposureMeterProcessingResult | None + ) = None + upena_processing_results: EspadonsStatusUpenaProcessingResult | None = None + observation_type: str | None = None + + +class ExposureDataMegacamStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + iq: float | None = None + skybg: float | None = None + elixir_eval: int | None = None + elixir_seval: str | None = None + snr: float | None = None + atmospheric_transmission: float | None = None + elixir_zeropoint: float | None = None + actual_filter: str | None = None + elixir_processing_result: MegacamStatusElixirProcessingResult | None = None + + +class ExposureDataSitelleStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + camera_a: SitelleStatusCamera | None = None + camera_b: SitelleStatusCamera | None = None + relative_extinction: float | None = None + relative_extinction_rms: float | None = None + actual_filter: str | None = None + + +class ExposureDataSpirouStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + acqtime: float | None = None + acqtime1: str | None = None + asic_num: str | None = None + sca_id: str | None = None + muxtype: int | None = None + noutputs: int | None = None + nadcs: int | None = None + pddector: int | None = None + clkoff: int | None = None + warmtst: int | None = None + clocking: int | None = None + glbreset: int | None = None + frmode: int | None = None + camlink: int | None = None + expmode: int | None = None + nresets: int | None = None + frmtime: float | None = None + exptime: float | None = None + acqtype: int | None = None + datamode: int | None = None + datlevel: int | None = None + asicgain: int | None = None + nomgain: int | None = None + ampreset: int | None = None + ktcremov: int | None = None + srccur: int | None = None + ampinput: int | None = None + v4v3v2v1: str | None = None + units: str | None = None + tstation: str | None = None + hxrgver: str | None = None + mclk: float | None = None + inttime: float | None = None + frameno: int | None = None + sb_adc_s: str | None = None + sbadc1_p: float | None = None + sbadc2_p: float | None = None + sbcali_p: str | None = None + sbdens_p: float | None = None + sbrhb1_p: str | None = None + sbrhb2_p: str | None = None + sbetem_p: str | None = None + sb_inj_t: float | None = None + sb_pol_t: float | None = None + sb_cs1_t: float | None = None + sb_cs2_t: float | None = None + sb_cb1_t: float | None = None + sb_cb2_t: float | None = None + sbiavl1s: str | None = None + sbiavl2s: str | None = None + sbetel_s: str | None = None + sb_csc_s: str | None = None + sbcref_p: str | None = None + sbccas_p: str | None = None + sbcden_p: float | None = None + sbagit_s: str | None = None + sbcmir_p: str | None = None + sbclhc1s: str | None = None + sbclhc1w: str | None = None + sbclhc1c: float | None = None + sbclhc2s: str | None = None + sbclhc2w: str | None = None + sbclhc2c: float | None = None + sbclwl_s: str | None = None + sbclwl_c: float | None = None + sbwlsh_s: str | None = None + sbctec_s: str | None = None + sbctecct: float | None = None + sbctecht: float | None = None + sbccprit: float | None = None + sbcccu_t: float | None = None + sbccprot: float | None = None + sbcfbi_t: float | None = None + sbcwls_t: float | None = None + sbclsuat: float | None = None + sbcfpi_t: float | None = None + sbcfpe_t: float | None = None + sbcfpb_p: float | None = None + sbvga_cp: float | None = None + sbvga_ip: float | None = None + sbvgb_cp: float | None = None + sbvgb_ip: float | None = None + sbmk00_t: float | None = None + sbmk01_t: float | None = None + sbmk02_t: float | None = None + sbmk03_t: float | None = None + sbmk04_t: float | None = None + sbmk05_t: float | None = None + sbmk06_t: float | None = None + sbmk07_t: float | None = None + sbls000t: float | None = None + sbls001t: float | None = None + sbls002t: float | None = None + sbls003t: float | None = None + sbls004t: float | None = None + sbls005t: float | None = None + sbls006t: float | None = None + sbls007t: float | None = None + sbls008t: float | None = None + sbls009t: float | None = None + sbls010t: float | None = None + sbls011t: float | None = None + sbls100t: float | None = None + sbls101t: float | None = None + sbls102t: float | None = None + sbls103t: float | None = None + sbls104t: float | None = None + sbls105t: float | None = None + sbls106t: float | None = None + sbls107t: float | None = None + sbls108t: float | None = None + sbls109t: float | None = None + sbls110t: float | None = None + sbls111t: float | None = None + sbl00i_t: float | None = None + sbl00h_v: float | None = None + sbl01i_t: float | None = None + sbl01h_v: float | None = None + sbl02i_t: float | None = None + sbl02h_v: float | None = None + sbl03i_t: float | None = None + sbl03h_v: float | None = None + sbl04i_t: float | None = None + sbl04h_v: float | None = None + sbl05i_t: float | None = None + sbl05h_v: float | None = None + sbl06i_t: float | None = None + sbl06h_v: float | None = None + sbl07i_t: float | None = None + sbl07h_v: float | None = None + sbl08i_t: float | None = None + sbl08h_v: float | None = None + sbl09i_t: float | None = None + sbl09h_v: float | None = None + sbl10i_t: float | None = None + sbl10h_v: float | None = None + sbl11i_t: float | None = None + sbl11h_v: float | None = None + sbl12i_t: float | None = None + sbl12h_v: float | None = None + sbl13i_t: float | None = None + sbl13h_v: float | None = None + seadc1_p: float | None = None + seadc2_p: float | None = None + sedens_p: int | None = None + sedens_p_float: float | None = None + se_inj_t: float | None = None + se_pol_t: float | None = None + se_cs1_t: float | None = None + se_cs2_t: float | None = None + se_cb1_t: float | None = None + se_cb2_t: float | None = None + sectecct: float | None = None + sectecht: float | None = None + seccprit: float | None = None + secccu_t: float | None = None + seccprot: float | None = None + secfbi_t: float | None = None + secwls_t: float | None = None + seclhc1c: float | None = None + seclhc2c: float | None = None + seclwl_c: float | None = None + seclsuat: float | None = None + secfpi_t: float | None = None + secfpe_t: float | None = None + secfpb_p: float | None = None + sevga_cp: float | None = None + sevga_ip: float | None = None + sevgb_cp: float | None = None + sevgb_ip: float | None = None + semk00_t: float | None = None + semk01_t: float | None = None + semk02_t: float | None = None + semk03_t: float | None = None + semk04_t: float | None = None + semk05_t: float | None = None + semk06_t: float | None = None + semk07_t: float | None = None + sels000t: float | None = None + sels001t: float | None = None + sels002t: float | None = None + sels003t: float | None = None + sels004t: float | None = None + sels005t: float | None = None + sels006t: float | None = None + sels007t: float | None = None + sels008t: float | None = None + sels009t: float | None = None + sels010t: float | None = None + sels011t: float | None = None + sels100t: float | None = None + sels101t: float | None = None + sels102t: float | None = None + sels103t: float | None = None + sels104t: float | None = None + sels105t: float | None = None + sels106t: float | None = None + sels107t: float | None = None + sels108t: float | None = None + sels109t: float | None = None + sels110t: float | None = None + sels111t: float | None = None + sel00i_t: float | None = None + sel00h_v: float | None = None + sel01i_t: float | None = None + sel01h_v: float | None = None + sel02i_t: float | None = None + sel02h_v: float | None = None + sel03i_t: float | None = None + sel03h_v: float | None = None + sel04i_t: float | None = None + sel04h_v: float | None = None + sel05i_t: float | None = None + sel05h_v: float | None = None + sel06i_t: float | None = None + sel06h_v: float | None = None + sel07i_t: float | None = None + sel07h_v: float | None = None + sel08i_t: float | None = None + sel08h_v: float | None = None + sel09i_t: float | None = None + sel09h_v: float | None = None + sel10i_t: float | None = None + sel10h_v: float | None = None + sel11i_t: float | None = None + sel11h_v: float | None = None + sel12i_t: float | None = None + sel12h_v: float | None = None + sel13i_t: float | None = None + sel13h_v: float | None = None + guider_processing: SpirouStatusGuiderProcessingResult | None = None + exposure_meter_processing: SpirouStatusExposureMeterProcessingResult | None = None + pipeline_processing: SpirouStatusPipelineProcessingResult | None = None + + +class FindingChartData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + comment: str | None = None + url: str | None = None + token: str | None = None + staff_approval: FindingChartDataApproval | None = None + pi_approval: FindingChartDataApproval | None = None + target_token: str | None = None + + +class InstrumentConfigurationDataSitelleConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + filter: Filter1 | None = None + binning: Binning1 | None = None + resolution: int | None = None + snrwavelength: Annotated[ + float | None, + Field(description='This should be on target too , sitell, espedons, spirou'), + ] = None + calibration: SitelleConfigurationDataCalibration | None = None + resolution_mode: ResolutionMode | None = None + needs_precision: bool | None = None + needs_flux_calibration: bool | None = None + needs_target_calibration: bool | None = None + + +class InstrumentConfigurationDataSpirouConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + template_name: Annotated[str | None, Field(description='Deprecated fields')] = None + observing_mode: ObservingMode1 | None = None + stokes_parameter: StokesParameter | None = None + mode: Mode | None = None + sky_observation: SkyObservation | None = None + number_of_sequences: Annotated[ + int | None, + Field( + description='Each sequence is 1 exposure for Star mode and 4 exposures for ObservingMode' + ), + ] = None + use_snr: Annotated[ + bool | None, Field(description='when true - use SNR for stoping exposures') + ] = None + sky_etime_millis: Annotated[ + Int64Value | None, + Field(description='Used to override the etime for the sky_observation'), + ] = None + + +class MegacamConfigurationDataPhotometry(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + aperture: PhotometryAperture | None = None + psf: bool | None = None + + +class MovingTargetEphemeris(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + mjd: Annotated[ + float | None, + Field( + description='Modified Julian date when the target is at this coordinate.' + ), + ] = None + coordinate: Annotated[ + SkyCoordinate | None, + Field( + description='Coordinate for this target at this time, specified in\n ICRS (ep=J2000).' + ), + ] = None + computed_coordinate: Annotated[ + SkyCoordinate | None, + Field( + description='Internal use only.\n The coordinate of the target at the time of observation,\n specified in ICRS (ep=J2000).\n\n storage: transient\n api_visibility: operations read only' + ), + ] = None + + +class ObservingBlockCondition(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + pre_requisite: Annotated[ + list[ObservingBlockConditionRequisite] | None, + Field( + description='Pre-requisite conditions before each observing block can be started.\n The index of items in this list corresponds directly to the index of the\n observing block on the observing group i.e. this list is one-to-one with\n read_observing_block.' + ), + ] = None + window: Annotated[ + list[ObservingBlockConditionWindow] | None, + Field( + description='Time windows during which the observing block can be observed.\n\n Windows can not overlap, and will always be stored in temporal order.' + ), + ] = None + + +class ObservingGroupContext(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + iteration_count: Annotated[ + int | None, + Field( + description='The iteration number this object was part of in the observing group' + ), + ] = None + iteration_total: Annotated[ + int | None, + Field( + description='The total number of iterations requested for the observing group' + ), + ] = None + iteration_minimum: Annotated[ + int | None, + Field( + description='Minimum number of iterations this OG needs to provide science value' + ), + ] = None + window: Annotated[ + list[ObservingBlockConditionWindow] | None, + Field(description='Sorted by from past -> future on the observe_after key'), + ] = None + observing_group_token: str | None = None + observing_group_priority: ObservingGroupPriority | None = None + observing_group_state: ObservingGroupState | None = None + observing_group_label: int | None = None + observing_group_version: Annotated[ + int | None, + Field(description='Increments every time the observing group changes.'), + ] = None + legacy_reel: LegacyReel | None = None + single: bool | None = None + target_monitoring: TargetMonitoringObservingGroupTimeConstraint | None = None + phase_schedule: PhaseScheduleObservingGroupPhaseSchedule | None = None + time_accounting: Annotated[ + TimeAccountingData | None, + Field( + description='*\n TimeAccounting for the observing group.\n\n api_visibility: readonly' + ), + ] = None + exposure_type: Annotated[ExposureType | None, Field(alias='exposureType')] = None + use_group_dither_scheme: bool | None = None + + +class PhaseScheduleObservingGroup(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observing_block: Annotated[ + ObservingGroupDataObservingBlock | None, + Field(description='The observing block that is added once per iteration.'), + ] = None + phase_schedule: Annotated[ + PhaseScheduleObservingGroupPhaseSchedule | None, + Field(description='The schedule that defines when each iteration can occur.'), + ] = None + + +class ProgramData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + title: str | None = None + abstract: str | None = None + contact_info: ProgramDataContactInfo | None = None + comment: list[ProgramDataComments] | None = None + sybase_pi_user_id: Annotated[str | None, Field(alias='sybasePiUserId')] = None + sub_totals: SubTotalsData | None = None + token: Annotated[ + str | None, + Field( + description='Unique identifier for the program. Used interchangeably with "runid".' + ), + ] = None + time_allocation: list[AllocationData] | None = None + release_date_millis: int | None = None + metadata_release_date_millis: int | None = None + comment_field: str | None = None + program_type: ProgramType | None = None + distribution_hash: str | None = None + time_accounting: Annotated[ + TimeAccountingData | None, + Field( + description='TimeAccounting for the program.\n\n api_visibility: readonly' + ), + ] = None + operations_comment_field: str | None = None + last_data_acquisition_notification_date_millis: int | None = None + dynamic_exposure_time_limit: DynamicExposureTimeLimit | None = None + + +class RollupFields(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + i_time: RollupValue | None = None + i_time_configured: RollupValue | None = None + sub_totals: SubTotalsData | None = None + + +class TargetDataLinkedTargetIdentifiers(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + pi_confirmed_identifiers: Annotated[ + TargetIdentifiers | None, + Field( + description='Ids confirmed by the PI through the K2 web UI, or other\n confirmation step.\n\n When absent, the PI never confirmed a linking for this target.' + ), + ] = None + custom_target: Annotated[ + bool | None, + Field( + description='Used to indicate this is a target without known identifiers.' + ), + ] = None + + +class TargetDataMovingTarget(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + ephemeris_point: Annotated[ + list[MovingTargetEphemeris] | None, + Field( + description="The ephemerides that define the path of motion for the target.\n\n Must be provided using topocentric astrometric coordinates.\n Do not provide apparent coordinates.\n CFHT strongly recommends the use of JPL Horizons System for\n generating the proper coordinates. CFHT's observatory code is T14.\n\n At least five distinct positions must be provided that span the\n desired observing period. The first position should be well in\n advance of the observation. The positions do not need to be\n uniformly sampled throughout the desired observing period.\n\n The points will be interpolated to determine the position of the\n target at the time of observation, at which point there must be\n at least one ephemeris point defined in the past, and at least\n four defined in the future." + ), + ] = None + + +class TargetMonitoringObservingGroup(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observing_block: Annotated[ + ObservingGroupDataObservingBlock | None, + Field(description='The observing block that is added once per iteration.'), + ] = None + time_constraint: Annotated[ + TargetMonitoringObservingGroupTimeConstraint | None, + Field(description='The schedule that defines when each iteration can occur.'), + ] = None + + +class TelescopePatternData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + offsets: Annotated[ + list[TelescopePatternDataOffset] | None, + Field( + description='Should only be set if manually specifying custom dithering patterns' + ), + ] = None + config: TelescopePatternDataConfig | None = None + token: str | None = None + label: int | None = None + version: int | None = None + user_token: str | None = None + instrument: Instrument | None = None + is_system: bool | None = None + name: str | None = None + offset_coordinate: list[OffsetCoordinate] | None = None + + +class TelescopePatternListResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + telescope_pattern: list[TelescopePatternData] | None = None + error: Errors | None = None + + +class TelescopePatternUpdateRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: str | None = None + telescope_pattern_token: str | None = None + lock_version: Int32Value | None = None + telescope_pattern: TelescopePatternData | None = None + + +class TelescopePatternUpdateResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + telescope_pattern: TelescopePatternData | None = None + + +class UpdateProgramCommentResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + program_data: ProgramData | None = None + error: Errors | None = None + + +class UpdateProgramCommentsResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + program_data: ProgramData | None = None + error: Errors | None = None + + +class EphemeridesUpdateRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier of the science program.') + ] = None + token: Annotated[str | None, Field(description='Unique identifier of the target.')] = ( + None + ) + ephemeris: Annotated[ + list[MovingTargetEphemeris] | None, + Field(description='List of ephemerides to set on the target.'), + ] = None + + +class ExposureDataExposureStatus(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + megacam_status: ExposureDataMegacamStatus | None = None + wircam_status: ExposureDataWircamStatus | None = None + espadons_status: ExposureDataEspadonsStatus | None = None + sitelle_status: ExposureDataSitelleStatus | None = None + spirou_status: ExposureDataSpirouStatus | None = None + wena_status: ExposureDataWenaStatus | None = None + exposure_time: ExposureTime | None = None + exp_date_mjd: float | None = None + queue_name: str | None = None + exp_type: str | None = None + exp_status: ExpStatus | None = None + is_photometric: bool | None = None + obs_comment: str | None = None + seq_comment: str | None = None + actual_pointing: Annotated[ + SkyCoordinate | None, + Field( + description='TODO: come up with word which means it was read from instruments, measured, observed, reported, etc.' + ), + ] = None + actual_sky: float | None = None + actual_airmass: float | None = None + actual_moondist: float | None = None + observer_comment: str | None = None + qc_comment: str | None = None + file: Annotated[ + ExposureStatusFile | None, + Field(description='To read a file, simply absolute_path + base_name + ext.'), + ] = None + credited_qcoordinator: str | None = None + credited_observer: str | None = None + grade: int | None = None + read_obs_date_utc: str | None = None + read_obs_date_hst: str | None = None + read_actual_sky_name: str | None = None + ra: Annotated[str | None, Field(description='Values from headers')] = None + dec: str | None = None + cmmtobs: str | None = None + cmmtseq: str | None = None + pi_name: str | None = None + nexp: str | None = None + cmpltexp: str | None = None + ha: str | None = None + read_obs_date_millis: int | None = None + object: str | None = None + + +class InstrumentConfigurationDataMegacamConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + filter: Filter | None = None + binning: Binning | None = None + is_dynamic_exposure_time: Annotated[ + bool | None, + Field( + description="TODO: is this needed billy? Can we use presence of snr or not to decide?\n I THINK SO. We have the capability to calculate SNR on every exposure, but we aren't necessarily doing SNR mode observations with everything" + ), + ] = None + minimum_exposures: int | None = None + min_exptime_ms: int | None = None + max_exptime_ms: int | None = None + mag_ab: Annotated[ + float | None, + Field( + alias='magAB', + description='TODO: interesting field here because associated with the target, but in the legacy mp snr mode, the mag is carried with the IC! so have to go back to this :(', + ), + ] = None + snr_mode: SnrMode | None = None + etc_photometry: MegacamConfigurationDataPhotometry | None = None + + +class ObservingGroupData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + token: Annotated[ + str | None, Field(description='Unique identifier for the observing group.') + ] = None + label: Annotated[ + int | None, + Field( + description='Index of the observing group within the program, set by the system\n automatically. Used for reference in communications with QSO staff.\n\n storage: transient\n api_visibility: read only' + ), + ] = None + og_priority: Annotated[ + OgPriority | None, + Field( + description='Priority of observing this observing group, relative to other observing\n groups within the same program.' + ), + ] = None + single_observing_group: Annotated[ + SingleObservingGroup | None, + Field(description='The observing group has a single observing block.'), + ] = None + target_monitoring_observing_group: Annotated[ + TargetMonitoringObservingGroup | None, + Field( + description='The observing group has multiple observing blocks driven by target\n monitoring constraints.' + ), + ] = None + phase_schedule_observing_group: Annotated[ + PhaseScheduleObservingGroup | None, + Field( + description='The observing group multiple observing blocks driven by a phase\n schedule.' + ), + ] = None + read_observing_block: Annotated[ + list[ObservingGroupDataObservingBlock] | None, + Field( + description='The observing blocks that have been created based on the entered\n configuration.\n\n api_visibility: read only' + ), + ] = None + observing_block_condition: Annotated[ + dict[str, ObservingBlockCondition] | None, + Field( + description='The requirements that have been set for observing each observing blocks\n based on the entered configuration.\n\n api_visibility: read only' + ), + ] = None + minimum_observing_block_count: Annotated[ + int | None, + Field( + description='This field is unused, please ignore.\n\n api_visibility: read only' + ), + ] = None + time_accounting: Annotated[ + TimeAccountingData | None, + Field( + description='Time accounting information for the observing group.\n\n See the Time Accounting documentation for more details.\n\n api_visibility: readonly' + ), + ] = None + state: Annotated[ + State | None, + Field( + description='State of the observing group, based off of the time accounting status.' + ), + ] = None + version: Annotated[ + int | None, + Field( + description='Increments when this object updates.\n\n storage: transient\n api_visibility: read only, incremented by the server' + ), + ] = None + legacy_reel: Annotated[ + LegacyReel | None, + Field( + description='Conditions the observation of this observing group on the observation of\n another observing group first.' + ), + ] = None + time_constraint: Annotated[ + TimeConstraint | None, + Field(description='Constraints on when the observing group can be observed.'), + ] = None + target_type: Annotated[ + TargetType | None, + Field( + description='This can be used to configure calibration observing groups. For internal\n use only.\n\n api_visibility: operations write only' + ), + ] = None + dither_scheme: Annotated[ + DitherScheme | None, + Field( + description='Used to customize the order of observation when observing multiple\n targets with the same telescope pattern within this observing group.' + ), + ] = None + low_surface_brightness: Annotated[ + bool | None, + Field( + description='Indicates the observing group uses low surface brightness (LSB) mode.with' + ), + ] = None + + +class ObservingGroupListResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + entity: list[ObservingGroupData] | None = None + error: Errors | None = None + + +class ObservingGroupUpdateResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + entity: ObservingGroupData | None = None + + +class ProgramInfo(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + program_data: ProgramData | None = None + rollup_fields: RollupFields | None = None + time_accounting: TimeAccountingData | None = None + pi_info: ProgramInfoPiInfo | None = None + version: int | None = None + + +class TargetData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + token: Annotated[ + str | None, + Field( + description='Unique identifier for the target.\n\n When creating a new target, this will be inferred from the request data\n if left blank here.\n\n Must begin with the program token followed by a dash, e.g.\n 20AE01-0123456789' + ), + ] = None + name: Annotated[ + str | None, + Field( + description='Name for the target entered by PI, or primary name as resolved by CDS.\n\n Will become OBJECT in the FITS header.\n\n Required.\n Length limited to 39 characters.\n Allowed characters are letters, numbers, spaces, or any of:\n !@#$%^&*()_-+.,?/[]<>' + ), + ] = None + label: Annotated[ + int | None, + Field( + description='Index of the target within the program, set by the system automatically.\n Used for reference to targets in communications with QSO staff.\n\n storage: transient\n api_visibility: read only' + ), + ] = None + version: Annotated[ + int | None, + Field( + description='Increments when this object updates.\n\n storage: transient\n api_visibility: read only, incremented by the server' + ), + ] = None + fixed_target: Annotated[ + TargetDataFixedTarget | None, + Field( + description='Used for a target at fixed coordinates, including any provided\n proper motion.' + ), + ] = None + moving_target: Annotated[ + TargetDataMovingTarget | None, + Field( + description='Used for a target with motion defined by a list of ephemerides.' + ), + ] = None + magnitude: Annotated[ + TargetDataMagnitude | None, + Field(description='The magnitude of the target for bands which are known.'), + ] = None + temperature_effective: Annotated[ + float | None, + Field( + description='The estimated/measured effective temperature, in Kelvin.\n\n Required for targets observed with SPIRou or ESPaDOnS.\n Not used for MegaCam.' + ), + ] = None + standard_star: Annotated[ + bool | None, + Field( + description='Whether the star is a "standard" star, to be observed each time other\n science targets are observed.\n\n Not used for MegaCam.' + ), + ] = None + linked_target_identifiers: Annotated[ + TargetDataLinkedTargetIdentifiers | None, + Field(description='Identifiers linked to this target.'), + ] = None + finding_chart: Annotated[ + list[FindingChartData] | None, + Field( + description='A read-only view of the finding charts for this target.\n To create or update finding charts, see the FindingChartService.\n\n storage: transient\n api_visibility: read only' + ), + ] = None + pointing_offset_token: Annotated[ + str | None, + Field( + description='The token of a pointing offset which should be used when observing\n this target.\n\n To view or manage pointing offsets, see the PointingOffsetService.' + ), + ] = None + pointing_offset: Annotated[ + PointingOffsetData | None, + Field( + description='A read-only view of the pointing offset which has been specified by\n pointing_offset_token.\n\n To view or manage pointing offsets, see the PointingOffsetService.\n\n storage: transient\n api_visibility: read only' + ), + ] = None + + +class TargetListResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + entity: list[TargetData] | None = None + error: Errors | None = None + + +class TargetShowResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + entity: TargetData | None = None + + +class TargetUpdateRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier of the science program.') + ] = None + entity: Annotated[ + TargetData | None, Field(description='Target data to be created or updated.') + ] = None + token: Annotated[ + str | None, + Field( + description='Unique identifier of the target. If a target with this token already\n exists, it will be updated, otherwise it will be created.\n\n Must begin with the program token followed by a dash, e.g.\n 20AE01-0123456789' + ), + ] = None + lock_version: Annotated[ + Int32Value | None, + Field( + description='If specified, the endpoint will use optimistic locking instead of last\n write wins. This should be set to the current version on entity.' + ), + ] = None + instrument: Annotated[ + Instrument | None, + Field( + description='If specified, instrument-specific validation checks will be performed.' + ), + ] = None + + +class TargetUpdateResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + entity: TargetData | None = None + + +class EphemeridesUpdateResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + entity: TargetData | None = None + + +class InstrumentConfigurationData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + espadons_configuration: ( + InstrumentConfigurationDataEspadonsConfigurationData | None + ) = None + megacam_configuration: ( + InstrumentConfigurationDataMegacamConfigurationData | None + ) = None + wircam_configuration: InstrumentConfigurationDataWircamConfigurationData | None = ( + None + ) + sitelle_configuration: ( + InstrumentConfigurationDataSitelleConfigurationData | None + ) = None + spirou_configuration: InstrumentConfigurationDataSpirouConfigurationData | None = ( + None + ) + wena_configuration: InstrumentConfigurationDataWenaConfigurationData | None = None + sybase_ic_id: BigInteger | None = None + etime_or_snr: ExposureTime | None = None + sybase_cons_id: BigInteger | None = None + tracking: Tracking | None = None + + +class ListProgramsResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + entity: list[ProgramInfo] | None = None + error: Errors | None = None + + +class ObservingTemplateData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + instrument_configuration: Annotated[ + InstrumentConfigurationData | None, Field(description='Next available ID 17') + ] = None + constraint: Constraint | None = None + telescope_offset: TelescopePatternData | None = None + charge_model: Annotated[ + str | None, + Field( + description='Used for calculating cost for this pattern\n Invokes code which reads the ExposureInstructions and\n attributes a cost to it.' + ), + ] = None + itime: int | None = None + calculate_etime: Annotated[ + ExposureTime | None, + Field( + description='int calculated_etime = 6; /* from the ETC, can be different than the input etime */' + ), + ] = None + acquisition_time: Annotated[ + ExposureTime | None, + Field( + description='SNR or E-Time per exposure => therefore we do not need the ExposureTime message in each IC? question above as well' + ), + ] = None + token: str | None = None + associated_observing_blocks: Annotated[ + list[BigInteger] | None, + Field( + description='When just creating an OT, this list is empty\n When this OT belongs to an OG, then any OBs in sybase that this OT is related too must be filled in.\n select * from blah where (select id from prg where prg\n Query to find related OBs is, select ob.* from ob, prg where ob.prg_id = prg.id and prg.runid=? instrument_configuration.sybase_ic_id, constraint.sybase_cons_id\n\nselect ob.* from ob, prg, icseq where ob.prg_id = prg.id\nand prg.runid=\nand icseq.ob_id=ob.id and ob.con_id = \nand icseq.ic_id = ;' + ), + ] = None + name: str | None = None + total_exposure_count: int | None = None + label: int | None = None + version: Annotated[ + int | None, + Field( + description='Increments when this object updates\n\n storage: transient\n api_visibility: read only, incremented by the server' + ), + ] = None + telescope_pattern_token: str | None = None + telescope_pattern_scale: float | None = None + megacam_cumulative_snr_system_generated: bool | None = None + + +class ObservingTemplateListResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + entity: list[ObservingTemplateData] | None = None + error: Errors | None = None + + +class ObservingTemplateUpdateRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: str | None = None + observing_template_token: str | None = None + observing_template: ObservingTemplateData | None = None + lock_version: Annotated[ + Int32Value | None, + Field( + description='If specified, the endpoint will use optimistic locking instead of last write wins' + ), + ] = None + + +class ObservingTemplateUpdateResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + observing_template: ObservingTemplateData | None = None + + +class ExposureData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + token: str | None = None + obsid: str | None = None + exposure_number: int | None = None + iteration: int | None = None + parent_exposure_token: str | None = None + exposure_status: ExposureDataExposureStatus | None = None + observing_template: ObservingTemplateData | None = None + target_data: TargetData | None = None + observing_group_context: Annotated[ + ObservingGroupContext | None, + Field( + description='Contains live data for the overall state of the observing group.' + ), + ] = None + all_derivations_complete: Annotated[ + bool | None, + Field( + description='When false, there is data which is still being calculated for this exposure.\n\n Data can still be displayed, but the client should attempt to refresh the data.' + ), + ] = None + queue_run_id: str | None = None + camera_run_id: str | None = None + prg_run_id: str | None = None + observing_component_token: str | None = None + sub_totals: SubTotalsData | None = None + attribution: Annotated[ + ExposureDataAttribution | None, + Field( + description='Allows exposures to be moved to different observing blocks\n\n As soon as an exposure is marked observed, this attribution is available' + ), + ] = None + version: Annotated[ + int | None, + Field( + description='Increments when this object updates\n\n storage: transient\n api_visibility: read only, incremented by the server' + ), + ] = None + + +class ObservingGroupUpdateRequest(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + runid: Annotated[ + str | None, Field(description='Unique identifier of the science program.') + ] = None + token: Annotated[ + str | None, + Field( + description='Unique identifier of the observing group. If an observing group target\n with this token already exists, it will be updated, otherwise it will be\n created.\n\n Must begin with the program token followed by a dash, e.g.\n 20AE01-0123456789' + ), + ] = None + entity: Annotated[ + ObservingGroupData | None, + Field(description='Observing group data to be created or updated.'), + ] = None + target: Annotated[ + dict[str, TargetData] | None, + Field( + description='If creating or updating targets with this request, set the data here.\n\n Key is the unique identifier of the target and must be prefixed with the runid.\n Value is the target data and must be valid. Otherwise, the whole request will fail.\n\n Observing components can then reference the unique identifier (the map key) in this same request.' + ), + ] = None + observing_template: Annotated[ + dict[str, ObservingTemplateData] | None, + Field( + description='If creating or updating observing templates with this request, set the data here.\n\n Key is the unique identifier of the observing template and must be prefixed with the runid.\n Value is the observing template data and must be valid. Otherwise, the whole request will fail.\n\n Observing components can then reference the unique identifier (the map key) in this same request.' + ), + ] = None + lock_version: Annotated[ + Int32Value | None, + Field( + description='If specified, the endpoint will use optimistic locking instead of last\n write wins. This should be set to the current version on entity.' + ), + ] = None + + +class SnrBlockDataSnrBlockObservingComponents(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + observing_component_token: Annotated[ + str | None, Field(description='the observing component') + ] = None + observing_group_token: Annotated[ + str | None, Field(description='should not be here, should be in the parent') + ] = None + is_primary: Annotated[bool | None, Field(description='is it the original oc?')] = ( + None + ) + configured_exposures: int | None = None + exposures: list[ExposureData] | None = None + ob_label: int | None = None + target_token: str | None = None + observing_template_token: str | None = None + + +class SnrBlockData(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + token: str | None = None + label: int | None = None + cumulative_snr: Annotated[ + float | None, + Field( + description='the total snr achieved of all exposures\n of all observing components of this snr block' + ), + ] = None + carry_forward_snr: Annotated[ + float | None, + Field( + description='if the program is copied to a new program possibly for a new semester\n the cumulative snr will be moved to the carry_forward_snr and the\n carry forward snr will be counted in the cumulative snr of the new program' + ), + ] = None + observing_components: list[SnrBlockDataSnrBlockObservingComponents] | None = None + version: int | None = None + requested_target_magnitude: Annotated[ + float | None, Field(description='this will come from the target') + ] = None + requested_cumulative_snr: Annotated[ + float | None, Field(description='this will come from the OT') + ] = None + requested_minimum_exposures: int | None = None + total_exposures: Annotated[ + int | None, + Field( + description='counting the total validated exposures for this OC (OT/TARGET)' + ), + ] = None + primary_observing_group_token: str | None = None + primary_observing_component_token: str | None = None + primary_observing_group_label: int | None = None + total_configured_exposures: Annotated[ + int | None, + Field(description='counting the total configured exposures for this OC'), + ] = None + + +class SnrBlockListResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + entity: list[SnrBlockData] | None = None + + +class AddSingleToSnrBlockResponse(CFHTBaseModel): + model_config = ConfigDict( + validate_by_name=True, + ) + success: bool | None = None + error: Errors | None = None + snr_block: SnrBlockData | None = None + observing_group: ObservingGroupData | None = None diff --git a/src/aeonlib/cfht/types.py b/src/aeonlib/cfht/types.py new file mode 100644 index 0000000..fa3aa19 --- /dev/null +++ b/src/aeonlib/cfht/types.py @@ -0,0 +1,20 @@ +from typing import Annotated, Any + +from pydantic import BeforeValidator, PlainSerializer + + +def _unwrap_double_value(value: Any) -> Any: + if isinstance(value, dict): + return value.get("value") + return value + + +def _wrap_double_value(value: float | None) -> dict[str, float | None]: + return {"value": value} + + +DoubleValue = Annotated[ + float | None, + BeforeValidator(_unwrap_double_value), + PlainSerializer(_wrap_double_value, return_type=dict[str, float | None]), +] diff --git a/src/aeonlib/conf.py b/src/aeonlib/conf.py index e02d58a..2ccdd45 100644 --- a/src/aeonlib/conf.py +++ b/src/aeonlib/conf.py @@ -39,5 +39,9 @@ class Settings(BaseSettings): salt_username: str = "" salt_password: str = "" + # Canada/France/Hawaii Telescope + cfht_api_root: str = "https://api-stage.cfht.hawaii.edu/" + cfht_access_token: str = "" + settings = Settings() diff --git a/src/aeonlib/types.py b/src/aeonlib/types.py index 9ac4cbd..9761f5c 100644 --- a/src/aeonlib/types.py +++ b/src/aeonlib/types.py @@ -243,9 +243,6 @@ def __get_pydantic_json_schema__( } -Time = Annotated[astropy.time.Time | datetime, _AstropyTimeType] -TimeMJD = Annotated[astropy.time.Time | datetime | float, _AstropyTimeMJDType] -Angle = Annotated[ - astropy.coordinates.Angle | Quantity | str | float, - _AstropyAngleType, -] +Time = Annotated[astropy.time.Time, _AstropyTimeType] +TimeMJD = Annotated[astropy.time.Time, _AstropyTimeMJDType] +Angle = Annotated[astropy.coordinates.Angle, _AstropyAngleType] diff --git a/tests/cfht/test_models.py b/tests/cfht/test_models.py new file mode 100644 index 0000000..02c4fcd --- /dev/null +++ b/tests/cfht/test_models.py @@ -0,0 +1,82 @@ +import pytest + +from aeonlib.cfht.conversions import ( + FixedTargetData, + _sidereal_target_payload, + target_data_from_aeon, +) +from aeonlib.cfht.models import ( + TargetData, + TargetDataFixedTarget, + TargetDataMagnitude, +) +from aeonlib.models import SiderealTarget + + +@pytest.fixture(scope="module") +def sidereal_target(): + return SiderealTarget( + name="test", + type="ICRS", + ra=12.3, + dec=45.6, + proper_motion_ra=1.0, + proper_motion_dec=0.1, + ) + + +def test_sidereal_target_payload(sidereal_target): + payload = _sidereal_target_payload(sidereal_target) + assert payload["name"] == "test" + assert payload["fixed_target"]["coordinate"]["ra"] == 12.3 + assert payload["fixed_target"]["coordinate"]["dec"] == 45.6 + assert payload["fixed_target"]["proper_motion"]["ra_mas"] == 1.0 + assert payload["fixed_target"]["proper_motion"]["dec_mas"] == 0.1 + + +def test_sidereal_target_to_target_data(sidereal_target): + result = target_data_from_aeon(sidereal_target) + assert isinstance(result, FixedTargetData) + assert isinstance(result, TargetData) + assert result.name == "test" + assert result.fixed_target + assert result.moving_target is None + assert result.fixed_target.coordinate + assert result.fixed_target.coordinate.ra == 12.3 + assert result.fixed_target.coordinate.dec == 45.6 + assert result.fixed_target.proper_motion + assert result.fixed_target.proper_motion.ra_mas == 1.0 + assert result.fixed_target.proper_motion.dec_mas == 0.1 + + +def test_double_value_field_accepts_float(): + fixed_target = TargetDataFixedTarget(estimated_radial_velocity_kmps=234.0) + + assert fixed_target.estimated_radial_velocity_kmps == 234.0 + assert fixed_target.api_dump() == { + "estimated_radial_velocity_kmps": {"value": 234.0} + } + + +def test_aliased_double_value_field_accepts_float(): + magnitude = TargetDataMagnitude(v=10.0) + + assert magnitude.v == 10.0 + assert magnitude.api_dump() == {"V": {"value": 10.0}} + + +def test_double_value_field_accepts_wrapped_api_input(): + magnitude = TargetDataMagnitude.model_validate({"v": {"value": 10.0}}) + assert magnitude.v == 10.0 + + null_magnitude = TargetDataMagnitude.model_validate({"v": {"value": None}}) + assert null_magnitude.v is None + + +def test_double_value_field_accepts_float_assignment(): + magnitude = TargetDataMagnitude() + + magnitude.ab = 10.0 + + assert magnitude.ab == 10.0 + assert magnitude.api_dump() == {"AB": {"value": 10.0}} diff --git a/tests/cfht/test_online.py b/tests/cfht/test_online.py new file mode 100644 index 0000000..93d92db --- /dev/null +++ b/tests/cfht/test_online.py @@ -0,0 +1,286 @@ +import random +import uuid +from collections.abc import Iterator + +import pytest + +from aeonlib.cfht.conversions import target_data_from_aeon +from aeonlib.cfht.facility import ( + CFHTFacility, + EntityNotFoundError, + ServerError, + VersionMismatchError, +) +from aeonlib.cfht.models import ( + ExposureData, + Instrument, + MovingTargetEphemeris, # TODO: replace with common non-sidereal model + ObservingBlockObservingComponent, + ObservingGroupData, + ObservingGroupDataObservingBlock, + ObservingTemplateData, + OgPriority, + ProgramInfo, + SingleObservingGroup, + SkyCoordinate, + TargetData, + TargetDataMagnitude, + TargetDataMovingTarget, # TODO: replace with common non-sidereal target model + TargetType, +) +from aeonlib.models import SiderealTarget + +pytestmark = pytest.mark.online + +example_mag_by_instrument: dict[Instrument, str] = { + Instrument.spirou: "h", + Instrument.espadons: "v", + Instrument.megacam: "ab", +} + + +@pytest.fixture(scope="module") +def facility() -> CFHTFacility: + return CFHTFacility() + + +@pytest.fixture(scope="module") +def test_run_id() -> str: + return uuid.uuid4().hex[:8] + + +@pytest.fixture(scope="module") +def program_facilities(facility: CFHTFacility) -> list[CFHTFacility]: + facilities = [ + CFHTFacility(program_token=data.token) + for program in facility.programs() + if (data := program.program_data) + if data.token + ] + assert facilities, "No programs with tokens were returned" + return facilities + + +def program_instruments( + facilities: list[CFHTFacility], +) -> Iterator[tuple[CFHTFacility, str, Instrument]]: + for facility in facilities: + program_token = facility.program_token + assert program_token is not None + instruments = facility.instruments() + assert instruments, f"No instruments allocated to {program_token}" + unsupported = instruments.difference(example_mag_by_instrument) + assert not unsupported, ( + "Example required mag not defined " + f"for: {', '.join(sorted(i.value for i in unsupported))}" + ) + for instrument in instruments: + yield facility, program_token, instrument + + +def example_fixed_target( + program_token: str, instrument: Instrument, test_run_id: str +) -> TargetData: + # Start with an Aeonlib common SiderealTarget + sidereal_target = SiderealTarget( + name=f"my new aeonlib test target {test_run_id}", + type="ICRS", + ra=random.uniform(0, 359.9999), + dec=random.uniform(-90, 90), + ) + # Get a CFHT TargetData + target_data = target_data_from_aeon(sidereal_target) + + # Add CFHT specific fields + target_data.token = f"{program_token}-{random.randint(1000000000, 9999999999)}" + target_data.fixed_target.estimated_radial_velocity_kmps = ( + 234.0 if instrument == Instrument.spirou else None + ) + target_data.magnitude = TargetDataMagnitude( + **{example_mag_by_instrument[instrument]: 10.0} + ) + target_data.temperature_effective = 1234.5 + target_data.standard_star = False + target_data.pointing_offset_token = f"00AZ00-PO+{instrument.value}+1" + + return target_data + + +def example_moving_target( + program_token: str, instrument: Instrument, test_run_id: str +) -> TargetData: + # For now constructing a moving target needs to be done manually as + # generating the ephemeris points automatically is not yet supported + return TargetData( + token=f"{program_token}-{random.randint(1000000000, 9999999999)}", + name=f"my new aeonlib moving target {test_run_id}", + moving_target=TargetDataMovingTarget( + ephemeris_point=[ + MovingTargetEphemeris( + mjd=61041.0 + i, + coordinate=SkyCoordinate( + ra=random.uniform(0, 359.9999), + dec=random.uniform(-90, 90), + ), + ) + for i in range(5) + ] + ), + magnitude=TargetDataMagnitude(**{example_mag_by_instrument[instrument]: 10.0}), + temperature_effective=1234.5, + standard_star=False, + pointing_offset_token=f"00AZ00-PO+{instrument.value}+1", + ) + + +def test_programs(facility: CFHTFacility): + programs = facility.programs() + assert isinstance(programs, list) + assert all(isinstance(program, ProgramInfo) for program in programs) + + +def test_get_targets(program_facilities: list[CFHTFacility]): + for facility in program_facilities: + targets = facility.targets() + assert isinstance(targets, list) + assert all(isinstance(target, TargetData) for target in targets) + + +@pytest.mark.side_effect +def test_create_fixed_targets(program_facilities: list[CFHTFacility], test_run_id: str): + for facility, program_token, instrument in program_instruments(program_facilities): + new_target = example_fixed_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + try: + assert target.version == 1 + finally: + if target.token is not None: + facility.delete_target(target.token) + + +@pytest.mark.side_effect +def test_create_moving_targets( + program_facilities: list[CFHTFacility], test_run_id: str +): + for facility, program_token, instrument in program_instruments(program_facilities): + new_target = example_moving_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + try: + assert target.version == 1 + finally: + if target.token is not None: + facility.delete_target(target.token) + + +@pytest.mark.side_effect +def test_get_target(program_facilities: list[CFHTFacility], test_run_id: str): + for facility, program_token, instrument in program_instruments(program_facilities): + new_target = example_fixed_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + try: + assert target.token is not None + fetched_target = facility.get_target(target.token) + assert fetched_target.token == target.token + finally: + if target.token is not None: + facility.delete_target(target.token) + + +@pytest.mark.side_effect +def test_delete_target(program_facilities: list[CFHTFacility], test_run_id: str): + for facility, program_token, instrument in program_instruments(program_facilities): + new_target = example_fixed_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + assert target.token is not None + facility.delete_target(target.token) + with pytest.raises(EntityNotFoundError): + facility.get_target(target.token) + + +@pytest.mark.side_effect +def test_update_fixed_targets(program_facilities: list[CFHTFacility], test_run_id: str): + for facility, program_token, instrument in program_instruments(program_facilities): + new_target = example_fixed_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + try: + assert target.version == 1 + updated_name = f"updates aeonlib target {test_run_id}" + target.name = updated_name + target = facility.create_or_update_target(target, instrument) + assert target.name == updated_name + assert target.version == 2 + finally: + if target.token is not None: + facility.delete_target(target.token) + + +@pytest.mark.side_effect +def test_target_bad_version(program_facilities: list[CFHTFacility], test_run_id: str): + facility, program_token, instrument = next(program_instruments(program_facilities)) + new_target = example_fixed_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + try: + assert target.version == 1 + target.version = 0 + with pytest.raises(VersionMismatchError): + facility.create_or_update_target(target, instrument) + finally: + if target.token is not None: + facility.delete_target(target.token) + + +def test_get_observing_templates(program_facilities: list[CFHTFacility]): + for facility in program_facilities: + templates = facility.observing_templates() + assert isinstance(templates, list) + assert all( + isinstance(template, ObservingTemplateData) for template in templates + ) + + +@pytest.mark.side_effect +def test_create_observing_group( + program_facilities: list[CFHTFacility], test_run_id: str +): + facility, program_token, instrument = next(program_instruments(program_facilities)) + templates = facility.observing_templates() + assert templates, "No observing templates available" + first_ot = templates[0] + new_target = example_fixed_target(program_token, instrument, test_run_id) + target = facility.create_or_update_target(new_target, instrument) + new_observing_group = ObservingGroupData( + token=f"{program_token}-{random.randint(1000000000, 9999999999)}", + og_priority=OgPriority.medium, + target_type=TargetType.object, + single_observing_group=SingleObservingGroup( + observing_block=ObservingGroupDataObservingBlock( + observing_component=[ + ObservingBlockObservingComponent( + target_token=target.token, + observing_template_token=first_ot.token, + ) + ] + ) + ), + ) + observing_group = facility.create_observing_group(new_observing_group) + assert isinstance(observing_group, ObservingGroupData) + try: + assert observing_group.token + assert observing_group.label + + # Attempt to delete the target that is used in an observing group + with pytest.raises(ServerError): + if target.token is not None: + facility.delete_target(target.token) + finally: + facility.delete_observing_group(observing_group.token) + if target.token is not None: + facility.delete_target(target.token) + + +def test_get_exposures(program_facilities: list[CFHTFacility]): + for facility in program_facilities: + exposures = facility.exposures() + assert isinstance(exposures, list) + assert all(isinstance(exposure, ExposureData) for exposure in exposures) diff --git a/uv.lock b/uv.lock index b6c5415..94a0703 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] [[package]] name = "aeonlib" @@ -31,6 +35,7 @@ salt = [ [package.dev-dependencies] codegen = [ + { name = "datamodel-code-generator" }, { name = "jinja2" }, { name = "textcase" }, ] @@ -59,6 +64,7 @@ provides-extras = ["eso", "lt", "salt"] [package.metadata.requires-dev] codegen = [ + { name = "datamodel-code-generator", specifier = ">=0.66.3" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "textcase", specifier = ">=0.2.1" }, ] @@ -90,6 +96,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] +[[package]] +name = "argcomplete" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/c0/c8e94135e66fabf89a120d9b4b123fe6993506beca6c1938a74c24cfa5fd/argcomplete-3.7.0.tar.gz", hash = "sha256:afde224f753f874807b1dc1414e883ab8fe0cda9c04807b6047dcb8e1ac23913", size = 73284, upload-time = "2026-06-30T22:28:22.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/f6/5b8ec087cd9cfa9449491ec83f76fb6b7006b4dff57d2ba8aaab330fe8e4/argcomplete-3.7.0-py3-none-any.whl", hash = "sha256:d8f0f22d2a8a7caa383be1e22b6caf1ecaf0ebd10d8f83cc125e36540c95830c", size = 42575, upload-time = "2026-06-30T22:28:20.547Z" }, +] + [[package]] name = "astropy" version = "8.0.0" @@ -135,6 +150,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + [[package]] name = "certifi" version = "2026.6.17" @@ -254,6 +301,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -307,6 +366,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, ] +[[package]] +name = "datamodel-code-generator" +version = "0.66.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "black", marker = "sys_platform != 'emscripten'" }, + { name = "genson" }, + { name = "inflect" }, + { name = "isort", marker = "sys_platform != 'emscripten'" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/4f/78f654273af65bd65f55b1eb670b23e56a8d1b227129bc17d96c4803495d/datamodel_code_generator-0.66.3.tar.gz", hash = "sha256:739f36b42d8131359a82cb25b704444581114e14611344538a61465d1743c6ec", size = 1516025, upload-time = "2026-07-01T16:03:37.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/ef/78fcf2a043e4c541827f156c98034bc6f2d4f511ec750f58f6f757985f66/datamodel_code_generator-0.66.3-py3-none-any.whl", hash = "sha256:7c1b44910951efea0e109f64243acb9721f6f7cd45b15635df17bd0829b24244", size = 418011, upload-time = "2026-07-01T16:03:35.348Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -316,6 +394,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "genson" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919, upload-time = "2024-05-15T22:08:49.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -362,6 +449,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -371,6 +471,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -603,6 +712,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "numpy" version = "2.4.6" @@ -686,6 +804,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -874,6 +1010,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pywin32-ctypes" version = "0.2.3" @@ -1047,6 +1212,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, ] +[[package]] +name = "typeguard" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/dfba5c4633cafc4c701f237d2ba63b416805047fd6d96aab4cfc40969f98/typeguard-4.5.2.tar.gz", hash = "sha256:5a16dcac23502039299c97c8941651bc33d7ea8cc4b2f7d6bbb1b528f6eea423", size = 80240, upload-time = "2026-05-14T12:59:40.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl", hash = "sha256:fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf", size = 36748, upload-time = "2026-05-14T12:59:39.473Z" }, +] + [[package]] name = "types-defusedxml" version = "0.7.0.20260504"