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):

Reply via email to