Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changes/456.new.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for unpacking user-supplied data in the `GPPTooViewSet` create method and introduced serializers for new input types. The API endpoint handles brightnesses, elevation ranges, and exposure modes. These changes bring us closer to enabling the submission of ToOs to GPP.
112 changes: 110 additions & 2 deletions src/goats_tom/api_views/gpp/toos.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@
from django.conf import settings
from gpp_client import GPPClient
from gpp_client.api.enums import ObservationWorkflowState
from gpp_client.api.input_types import ObservationPropertiesInput, TargetPropertiesInput
from gpp_client.api.input_types import (
BandBrightnessIntegratedInput,
ElevationRangeInput,
ExposureTimeModeInput,
ObservationPropertiesInput,
TargetPropertiesInput,
)
from rest_framework import permissions, status
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, mixins

from goats_tom.serializers import (
GPPBrightnessesSerializer,
GPPElevationRangeSerializer,
GPPExposureModeSerializer,
)


class GPPTooViewSet(GenericViewSet, mixins.CreateModelMixin):
serializer_class = None
Expand Down Expand Up @@ -46,14 +58,110 @@ def create(self, request: Request, *args, **kwargs) -> Response:
)
credentials = request.user.gpplogin

# Setup client to communicate with GPP.
print(request.data)
try:
# Setup client to communicate with GPP.
_ = GPPClient(url=settings.GPP_URL, token=credentials.token)

# TODO: Format brightnesses from request data.
# TODO: Format exposure mode from request data.
# TODO: Format elevation range from request data.

return Response({"detail": "Not yet implemented."})

except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)

def _format_elevation_range_properties(
self, data: dict[str, Any]
) -> ElevationRangeInput | None:
"""Format elevation range properties from the request data.

Parameters
----------
data : dict[str, Any]
The request data containing elevation range fields.

Returns
-------
ElevationRangeInput | None
An ElevationRangeInput instance or ``None`` if no elevation range is
provided.

Raises
------
serializers.ValidationError
If any error occurs during parsing or validation of elevation range values.
"""

elevation_range = GPPElevationRangeSerializer(data=data)
elevation_range.is_valid(raise_exception=True)
return (
ElevationRangeInput(**elevation_range.validated_data)
if elevation_range.validated_data
else None
)

def _format_brightnesses_properties(
self, data: dict[str, Any]
) -> list[BandBrightnessIntegratedInput] | None:
"""Format brightnesses properties from the request data.

Parameters
----------
data : dict[str, Any]
The request data containing brightness fields.

Returns
-------
list[BandBrightnessIntegratedInput] | None
A list of BandBrightnessIntegratedInput instances or ``None`` if no
brightnesses are provided.

Raises
------
serializers.ValidationError
If any error occurs during parsing or validation of brightness values.
"""
brightnesses = GPPBrightnessesSerializer(data=data)
brightnesses.is_valid(raise_exception=True)
brightnesses_data = brightnesses.validated_data.get("brightnesses", None)
return (
[BandBrightnessIntegratedInput(**b) for b in brightnesses_data]
if brightnesses_data
else None
)

def _format_exposure_mode_properties(
self, data: dict[str, Any]
) -> ExposureTimeModeInput | None:
"""Format exposure mode properties from the request data.

Parameters
----------
data : dict[str, Any]
The request data containing exposure mode fields.

Returns
-------
ExposureTimeModeInput | None
An ExposureTimeModeInput instance or ``None`` if no exposure mode is
provided.

Raises
------
serializers.ValidationError
If any error occurs during parsing or validation of exposure mode values.
"""

exposure_mode = GPPExposureModeSerializer(data=data)
exposure_mode.is_valid(raise_exception=True)
return (
ExposureTimeModeInput(**exposure_mode.validated_data)
if exposure_mode.validated_data
else None
)

def _clone_target(
self, client: GPPClient, properties: TargetPropertiesInput, target_id: str
) -> dict[str, Any]:
Expand Down
8 changes: 8 additions & 0 deletions src/goats_tom/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
DRAGONSReduceUpdateSerializer,
)
from .dragons_run import DRAGONSRunFilterSerializer, DRAGONSRunSerializer
from .gpp import (
GPPBrightnessesSerializer,
GPPElevationRangeSerializer,
GPPExposureModeSerializer,
)
from .header import HeaderSerializer
from .recipes_module import RecipesModuleSerializer
from .run_processor import RunProcessorSerializer
Expand All @@ -37,4 +42,7 @@
"Antares2GoatsSerializer",
"HeaderSerializer",
"AstroDatalabSerializer",
"GPPBrightnessesSerializer",
"GPPExposureModeSerializer",
"GPPElevationRangeSerializer",
]
9 changes: 9 additions & 0 deletions src/goats_tom/serializers/gpp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .brightnesses import GPPBrightnessesSerializer
from .elevation_range import GPPElevationRangeSerializer
from .exposure_mode import GPPExposureModeSerializer

__all__ = [
"GPPBrightnessesSerializer",
"GPPExposureModeSerializer",
"GPPElevationRangeSerializer",
]
108 changes: 108 additions & 0 deletions src/goats_tom/serializers/gpp/brightnesses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
__all__ = ["GPPBrightnessesSerializer"]

import re
from typing import Any

from gpp_client.api.enums import Band, BrightnessIntegratedUnits
from rest_framework import serializers


class BrightnessSerializer(serializers.Serializer):
"""
Serializer for individual brightness entries.

Notes
-----
This serializer is tied to
``gpp_client.api.input_types.BandNormalizedIntegratedInput.`` and will eventually
need to support all types of ``SourceProfileInput``.
"""

band = serializers.ChoiceField(choices=[b.value for b in Band])
value = serializers.FloatField()
unit = serializers.ChoiceField(choices=[u.value for u in BrightnessIntegratedUnits])
error = serializers.FloatField(required=False, allow_null=True)


class GPPBrightnessesSerializer(serializers.Serializer):
"""Serializer to parse and validate brightness entries from flat form data."""

brightnesses = serializers.ListField(
child=BrightnessSerializer(),
allow_empty=True,
allow_null=True,
required=False,
default=None,
)

def to_internal_value(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Parse flat brightness fields into structured brightnesses list.

Parameters
----------
data : dict[str, Any]
The input data potentially containing brightness fields.

Returns
-------
dict[str, Any]
The structured brightnesses list or an error message.

Raises
------
serializers.ValidationError
If any brightness value is invalid or required fields are missing.
"""
brightness_pattern = re.compile(
r"brightness(ValueInput|BandSelect|UnitsSelect)(\d+)"
)
brightnesses_data: dict[int, dict[str, Any]] = {}

# Group brightness fields by their index.
for key, value in data.items():
match = brightness_pattern.match(key)
if not match:
continue

field_type, index = match.groups()
index = int(index)

# Handle list values from form submissions.
raw_value = value[0] if isinstance(value, list) else value
raw_value = raw_value.strip() if raw_value else None

# Initialize dictionary for this index if not already present.
brightnesses_data.setdefault(index, {})[field_type] = raw_value

# Normalize values.
parsed = []
for index, entry in sorted(brightnesses_data.items()):
try:
value = float(entry["ValueInput"])
except (KeyError, TypeError, ValueError):
raise serializers.ValidationError(
"A Brightness value is not a valid number."
)

band = entry.get("BandSelect")
unit = entry.get("UnitsSelect")

# Ensure band and unit are provided.
if not band or not unit:
raise serializers.ValidationError(
"A Brightness is missing a band or unit."
)

parsed.append(
{
"band": band,
"value": value,
"unit": unit,
}
)

# Return structured brightnesses or None if empty.
if not parsed:
return super().to_internal_value({"brightnesses": None})
return super().to_internal_value({"brightnesses": parsed})
82 changes: 82 additions & 0 deletions src/goats_tom/serializers/gpp/elevation_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
__all__ = ["GPPElevationRangeSerializer"]

from typing import Any

from rest_framework import serializers

from .utils import normalize


class GPPElevationRangeSerializer(serializers.Serializer):
"""Serializer to parse and validate elevation range from flat form data."""

elevationRangeSelect = serializers.ChoiceField(choices=["Air Mass", "Hour Angle"])
airMassMinimumInput = serializers.CharField(required=False, allow_blank=True)
airMassMaximumInput = serializers.CharField(required=False, allow_blank=True)
haMinimumInput = serializers.CharField(required=False, allow_blank=True)
haMaximumInput = serializers.CharField(required=False, allow_blank=True)

def validate(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Validate and structure elevation range input into the correct nested model
shape.

Returns
-------
dict[str, Any]
The structured elevation range data.

Raises
------
serializers.ValidationError
If required fields are missing or invalid based on the selected mode.
"""

mode = data["elevationRangeSelect"]

# Handle Air Mass mode.
if mode == "Air Mass":
min_val = normalize(data.get("airMassMinimumInput"))
max_val = normalize(data.get("airMassMaximumInput"))

# At least one of min or max must be provided.
if min_val is None and max_val is None:
raise serializers.ValidationError(
"Air mass range must have at least one value."
)

# Build and return the structured data.
try:
return {
"airMass": {
"min": float(min_val) if min_val is not None else None,
"max": float(max_val) if max_val is not None else None,
}
}
except ValueError:
raise serializers.ValidationError("Air mass values must be numeric.")

# Handle Hour Angle mode.
elif mode == "Hour Angle":
min_val = normalize(data.get("haMinimumInput"))
max_val = normalize(data.get("haMaximumInput"))

# At least one of min or max must be provided.
if min_val is None and max_val is None:
raise serializers.ValidationError(
"Hour angle range must have at least one value."
)

# Build and return the structured data.
try:
return {
"hourAngle": {
"minHours": float(min_val) if min_val is not None else None,
"maxHours": float(max_val) if max_val is not None else None,
}
}
except ValueError:
raise serializers.ValidationError("Hour angle values must be numeric.")

# If mode is neither "Air Mass" nor "Hour Angle", raise an error.
raise serializers.ValidationError("Invalid elevation range mode selected.")
Loading