Skip to content

Commit 97b3f1b

Browse files
authored
GOATS-743: Extend API to support fetching observations from GPP. (#332)
1 parent 762526d commit 97b3f1b

8 files changed

Lines changed: 164 additions & 5 deletions

File tree

docs/changes/332.new.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Extended API to fetch observations from GPP.

src/goats_tom/api_views/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .dragons_recipes import DRAGONSRecipesViewSet
1010
from .dragons_reduce import DRAGONSReduceViewSet
1111
from .dragons_runs import DRAGONSRunsViewSet
12-
from .gpp import GPPProgramViewSet
12+
from .gpp import GPPObservationViewSet, GPPProgramViewSet
1313
from .recipes_module import RecipesModuleViewSet
1414
from .reduceddatum import ReducedDatumViewSet
1515
from .run_processor import RunProcessorViewSet
@@ -30,4 +30,5 @@
3030
"ReducedDatumViewSet",
3131
"AstroDatalabViewSet",
3232
"GPPProgramViewSet",
33+
"GPPObservationViewSet",
3334
]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .observations import GPPObservationViewSet
12
from .programs import GPPProgramViewSet
23

3-
__all__ = ["GPPProgramViewSet"]
4+
__all__ = ["GPPProgramViewSet", "GPPObservationViewSet"]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Handles grabbing observations and observation details from GPP."""
2+
3+
__all__ = ["GPPObservationViewSet"]
4+
5+
from asgiref.sync import async_to_sync
6+
from django.conf import settings
7+
from gpp_client import GPPClient
8+
from rest_framework import permissions
9+
from rest_framework.exceptions import PermissionDenied
10+
from rest_framework.request import Request
11+
from rest_framework.response import Response
12+
from rest_framework.viewsets import GenericViewSet, mixins
13+
14+
15+
class GPPObservationViewSet(
16+
GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin
17+
):
18+
serializer_class = None
19+
permission_classes = [permissions.IsAuthenticated]
20+
queryset = None
21+
22+
def list(self, request: Request, *args, **kwargs) -> Response:
23+
"""Return a list of GPP observations associated with the authenticated user.
24+
25+
Parameters
26+
----------
27+
request : Request
28+
The HTTP request object, including user context.
29+
30+
Returns
31+
-------
32+
Response
33+
A DRF Response object containing a list of GPP observations.
34+
35+
Raises
36+
------
37+
PermissionDenied
38+
If the authenticated user has not configured GPP login credentials.
39+
"""
40+
if not hasattr(request.user, "gpplogin"):
41+
raise PermissionDenied(
42+
"GPP login credentials are not configured for this user."
43+
)
44+
credentials = request.user.gpplogin
45+
46+
# Setup client to communicate with GPP.
47+
client = GPPClient(url=settings.GPP_URL, token=credentials.token)
48+
observations = async_to_sync(client.observation.get_all)()
49+
50+
return Response(observations)
51+
52+
def retrieve(self, request: Request, *args, **kwargs) -> Response:
53+
"""Return details for a specific GPP observation by observation ID.
54+
55+
Parameters
56+
----------
57+
request : Request
58+
The HTTP request object, including user context.
59+
60+
Returns
61+
-------
62+
Response
63+
A DRF Response object containing the details of the requested observation.
64+
65+
Raises
66+
------
67+
PermissionDenied
68+
If the authenticated user has not configured GPP login credentials.
69+
KeyError
70+
If 'pk' (the observation ID) is not present in kwargs.
71+
"""
72+
observation_id = kwargs["pk"]
73+
74+
if not hasattr(request.user, "gpplogin"):
75+
raise PermissionDenied(
76+
"GPP login credentials are not configured for this user."
77+
)
78+
credentials = request.user.gpplogin
79+
80+
# Setup client to communicate with GPP.
81+
client = GPPClient(url=settings.GPP_URL, token=credentials.token)
82+
observation = async_to_sync(client.observation.get_by_id)(
83+
observation_id=observation_id
84+
)
85+
86+
return Response(observation)

src/goats_tom/api_views/gpp/programs.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ def retrieve(self, request: Request, *args, **kwargs) -> Response:
7171
"""
7272
program_id = kwargs["pk"]
7373

74-
# Check if GPP credentials have been added.
7574
if not hasattr(request.user, "gpplogin"):
76-
# Return with the proper error
7775
raise PermissionDenied(
7876
"GPP login credentials are not configured for this user."
7977
)

src/goats_tom/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
router.register(
99
r"gpp/programs", api_views.GPPProgramViewSet, basename="gppprograms"
1010
)
11+
router.register(
12+
r"gpp/observations", api_views.GPPObservationViewSet, basename="gppobservations"
13+
)
1114
router.register(
1215
r"reduceddatums", api_views.ReducedDatumViewSet, basename="reduceddatums"
1316
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import pytest
2+
from rest_framework.test import APIRequestFactory, force_authenticate
3+
from rest_framework import status
4+
from unittest.mock import AsyncMock
5+
6+
from goats_tom.tests.factories import UserFactory, GPPLoginFactory
7+
from goats_tom.api_views import GPPObservationViewSet
8+
9+
10+
@pytest.mark.django_db
11+
class TestGPPObservationViewSet:
12+
def setup_method(self):
13+
self.factory = APIRequestFactory()
14+
self.list_view = GPPObservationViewSet.as_view({'get': 'list'})
15+
self.retrieve_view = GPPObservationViewSet.as_view({'get': 'retrieve'})
16+
17+
self.observation_id = "o-23e1"
18+
self.observation_data = {"observation_id": self.observation_id, "name": "m27"}
19+
self.observations_url = "/api/gpp/observations/"
20+
self.observation_detail_url = f"/api/gpp/observations/{self.observation_id}/"
21+
22+
# Setup users.
23+
self.user_with_login = UserFactory()
24+
GPPLoginFactory(user=self.user_with_login)
25+
self.user_without_login = UserFactory()
26+
27+
def test_list_observations_success(self, mocker):
28+
mock_client = mocker.patch("goats_tom.api_views.gpp.observations.GPPClient")
29+
mock_client.return_value.observation.get_all = AsyncMock(return_value=[self.observation_data])
30+
31+
request = self.factory.get(self.observations_url)
32+
force_authenticate(request, user=self.user_with_login)
33+
34+
response = self.list_view(request)
35+
36+
assert response.status_code == status.HTTP_200_OK
37+
assert response.data == [self.observation_data]
38+
mock_client.return_value.observation.get_all.assert_called_once()
39+
40+
def test_list_observations_missing_gpplogin(self):
41+
request = self.factory.get(self.observations_url)
42+
force_authenticate(request, user=self.user_without_login)
43+
44+
response = self.list_view(request)
45+
46+
assert response.status_code == status.HTTP_403_FORBIDDEN
47+
assert response.data["detail"] == "GPP login credentials are not configured for this user."
48+
49+
def test_retrieve_observation_success(self, mocker):
50+
mock_client = mocker.patch("goats_tom.api_views.gpp.observations.GPPClient")
51+
mock_client.return_value.observation.get_by_id = AsyncMock(return_value=self.observation_data)
52+
53+
request = self.factory.get(self.observation_detail_url)
54+
force_authenticate(request, user=self.user_with_login)
55+
56+
response = self.retrieve_view(request, pk=self.observation_id)
57+
58+
assert response.status_code == status.HTTP_200_OK
59+
assert response.data == self.observation_data
60+
mock_client.return_value.observation.get_by_id.assert_called_once_with(observation_id=self.observation_id)
61+
62+
def test_retrieve_observation_missing_gpplogin(self):
63+
request = self.factory.get(self.observation_detail_url)
64+
force_authenticate(request, user=self.user_without_login)
65+
66+
response = self.retrieve_view(request, pk=self.observation_id)
67+
68+
assert response.status_code == status.HTTP_403_FORBIDDEN
69+
assert response.data["detail"] == "GPP login credentials are not configured for this user."

tests/goats_tom/api_views/gpp/test_programs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def setup_method(self):
1414
self.list_view = GPPProgramViewSet.as_view({'get': 'list'})
1515
self.retrieve_view = GPPProgramViewSet.as_view({'get': 'retrieve'})
1616

17-
self.program_id = "GN-2025A-Q-101"
17+
self.program_id = "p-230e"
1818
self.program_data = {"program_id": self.program_id, "title": "Test Program"}
1919
self.programs_url = "/api/gpp/programs/"
2020
self.program_detail_url = f"/api/gpp/programs/{self.program_id}/"

0 commit comments

Comments
 (0)