This is an automated email from the ASF dual-hosted git repository. yasithdev pushed a commit to branch feat/generic-experiment-launcher in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit cf5be4ed515bc2924fa65cadb5a41ed4452b0908 Author: yasithdev <[email protected]> AuthorDate: Fri Apr 24 21:43:51 2026 -0400 feat(launcher): preview + create endpoints with 502/400 handling --- .../django_airavata/apps/api/launcher_views.py | 30 ++++++++++- .../apps/api/tests/test_launcher_views.py | 58 ++++++++++++++++++++++ .../django_airavata/apps/api/urls.py | 4 ++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/airavata-django-portal/django_airavata/apps/api/launcher_views.py b/airavata-django-portal/django_airavata/apps/api/launcher_views.py index f0b39ba3a..82fe88346 100644 --- a/airavata-django-portal/django_airavata/apps/api/launcher_views.py +++ b/airavata-django-portal/django_airavata/apps/api/launcher_views.py @@ -1,10 +1,10 @@ -from rest_framework import permissions +from rest_framework import permissions, status from rest_framework.decorators import api_view, permission_classes from rest_framework.exceptions import NotFound from rest_framework.request import Request from rest_framework.response import Response -from django_airavata.apps.api import launcher_client +from django_airavata.apps.api import launcher_client, launcher_serializers def _client(request: Request) -> launcher_client.LauncherClient: @@ -46,3 +46,29 @@ def user_storages(request: Request) -> Response: @permission_classes([permissions.IsAuthenticated]) def projects_list(request: Request) -> Response: return Response({"results": _client(request).list_projects()}) + + +@api_view(["POST"]) +@permission_classes([permissions.IsAuthenticated]) +def experiment_draft_preview(request: Request) -> Response: + serializer = launcher_serializers.ExperimentDraftSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + try: + result = _client(request).generate_preview(serializer.validated_data) + except ConnectionError as e: + return Response({"message": str(e)}, status=status.HTTP_502_BAD_GATEWAY) + return Response(result) + + +@api_view(["POST"]) +@permission_classes([permissions.IsAuthenticated]) +def experiment_draft_create(request: Request) -> Response: + serializer = launcher_serializers.ExperimentDraftSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + try: + result = _client(request).launch_experiment(serializer.validated_data) + except ConnectionError as e: + return Response({"message": str(e)}, status=status.HTTP_502_BAD_GATEWAY) + return Response(result, status=status.HTTP_201_CREATED) diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_views.py b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_views.py index e328b936a..f167e63b6 100644 --- a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_views.py +++ b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_views.py @@ -1,7 +1,11 @@ +from unittest.mock import patch + from django.contrib.auth import get_user_model from django.test import override_settings from rest_framework.test import APITestCase +from django_airavata.apps.api import launcher_views # for the patch target + class LauncherListingViewsTest(APITestCase): def setUp(self): @@ -79,3 +83,57 @@ class LauncherListingViewsTest(APITestCase): "/api/launcher/projects/", ]: self.assertEqual(self.client.get(url).status_code, 403) + + +class LauncherWriteViewsTest(APITestCase): + def setUp(self): + User = get_user_model() + self.user = User.objects.create_user(username="alice", password="pw") + self.client.force_authenticate(user=self.user) + self.draft = { + "name": "test-run", + "project_id": "proj-1", + "app_id": "namd", + "interface_name": "run", + "inputs": {"steps": 100}, + "outputs": {"trajectory": {"storage_id": "my-home", "path": "/x/out.dcd"}}, + "runtime": { + "compute_resource_id": "bridges-2", + "partition": "RM", + "walltime": "01:00:00", + "nodes": 1, + "cpus_per_node": 64, + }, + } + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_preview_returns_script_and_command(self): + resp = self.client.post("/api/launcher/experiment-drafts/preview/", self.draft, format="json") + self.assertEqual(resp.status_code, 200, resp.content) + body = resp.json() + self.assertIn("invocation_command", body) + self.assertIn("script_contents", body) + self.assertIn("warnings", body) + self.assertTrue(body["script_contents"].startswith("#!/bin/bash")) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_preview_rejects_invalid_draft(self): + bad = dict(self.draft) + bad["runtime"] = {**bad["runtime"], "walltime": "garbage"} + resp = self.client.post("/api/launcher/experiment-drafts/preview/", bad, format="json") + self.assertEqual(resp.status_code, 400) + self.assertIn("runtime", resp.json()) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_preview_returns_502_when_client_unreachable(self): + with patch.object(launcher_views, "_client") as mock_client: + mock_client.return_value.generate_preview.side_effect = ConnectionError("airavata down") + resp = self.client.post("/api/launcher/experiment-drafts/preview/", self.draft, format="json") + self.assertEqual(resp.status_code, 502) + self.assertIn("message", resp.json()) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_create_returns_experiment_id(self): + resp = self.client.post("/api/launcher/experiment-drafts/", self.draft, format="json") + self.assertEqual(resp.status_code, 201, resp.content) + self.assertIn("experiment_id", resp.json()) diff --git a/airavata-django-portal/django_airavata/apps/api/urls.py b/airavata-django-portal/django_airavata/apps/api/urls.py index 3997fca61..c41e0c235 100644 --- a/airavata-django-portal/django_airavata/apps/api/urls.py +++ b/airavata-django-portal/django_airavata/apps/api/urls.py @@ -109,6 +109,10 @@ urlpatterns += [ ), re_path(r"^launcher/storages/$", launcher_views.user_storages, name="launcher_user_storages"), re_path(r"^launcher/projects/$", launcher_views.projects_list, name="launcher_projects_list"), + re_path(r"^launcher/experiment-drafts/preview/$", + launcher_views.experiment_draft_preview, name="launcher_draft_preview"), + re_path(r"^launcher/experiment-drafts/$", + launcher_views.experiment_draft_create, name="launcher_draft_create"), ] if logger.isEnabledFor(logging.DEBUG):
