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 3bc56cb272009ca920a117cff9956b827b27009d
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 21:36:19 2026 -0400

    feat(launcher): listing endpoints (apps, profile, storages, projects)
    
    Adds five GET endpoints under /api/launcher/ as thin proxies over
    launcher_client: applications list (with category/search filtering),
    application detail, project resource profile, user storages, and
    projects list. All require authentication (403 for anonymous requests).
---
 .../django_airavata/apps/api/launcher_views.py     | 47 ++++++++++++++
 .../apps/api/tests/test_launcher_views.py          | 74 ++++++++++++++++++++++
 .../django_airavata/apps/api/urls.py               | 20 ++++++
 3 files changed, 141 insertions(+)

diff --git a/airavata-django-portal/django_airavata/apps/api/launcher_views.py 
b/airavata-django-portal/django_airavata/apps/api/launcher_views.py
new file mode 100644
index 000000000..7c23d0a69
--- /dev/null
+++ b/airavata-django-portal/django_airavata/apps/api/launcher_views.py
@@ -0,0 +1,47 @@
+from rest_framework import permissions, status
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.request import Request
+from rest_framework.response import Response
+
+from django_airavata.apps.api import launcher_client
+
+
+def _client(request: Request) -> launcher_client.LauncherClient:
+    token = request.session.get("ACCESS_TOKEN", "")
+    return launcher_client.get_client(user_token=token)
+
+
+@api_view(["GET"])
+@permission_classes([permissions.IsAuthenticated])
+def applications_list(request: Request) -> Response:
+    category = request.query_params.get("category") or None
+    search = request.query_params.get("search") or None
+    results = _client(request).list_applications(category=category, 
search=search)
+    return Response({"results": results})
+
+
+@api_view(["GET"])
+@permission_classes([permissions.IsAuthenticated])
+def application_detail(request: Request, app_id: str) -> Response:
+    try:
+        return Response(_client(request).get_application(app_id=app_id))
+    except LookupError:
+        return Response({"detail": "not found"}, 
status=status.HTTP_404_NOT_FOUND)
+
+
+@api_view(["GET"])
+@permission_classes([permissions.IsAuthenticated])
+def project_resource_profile(request: Request, project_id: str) -> Response:
+    return 
Response(_client(request).get_project_resource_profile(project_id=project_id))
+
+
+@api_view(["GET"])
+@permission_classes([permissions.IsAuthenticated])
+def user_storages(request: Request) -> Response:
+    return Response({"results": _client(request).list_user_storages()})
+
+
+@api_view(["GET"])
+@permission_classes([permissions.IsAuthenticated])
+def projects_list(request: Request) -> Response:
+    return Response({"results": _client(request).list_projects()})
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
new file mode 100644
index 000000000..ae4e35c9c
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_views.py
@@ -0,0 +1,74 @@
+from django.contrib.auth import get_user_model
+from django.test import override_settings
+from rest_framework.test import APITestCase
+
+
+class LauncherListingViewsTest(APITestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.user = User.objects.create_user(username="alice", password="pw")
+        # Use DRF force_authenticate: bypasses session/CSRF and directly sets
+        # request._force_auth_user so the view sees an authenticated user.
+        # We don't need a real session for these thin-proxy views because the
+        # ACCESS_TOKEN is only read by _client() to construct the 
LauncherClient,
+        # and the stub client ignores the token entirely.
+        self.client.force_authenticate(user=self.user)
+
+    @override_settings(LAUNCHER_CLIENT_STUB=True)
+    def test_applications_list_default(self):
+        resp = self.client.get("/api/launcher/applications/")
+        self.assertEqual(resp.status_code, 200)
+        body = resp.json()
+        self.assertIn("results", body)
+        self.assertTrue(len(body["results"]) >= 1)
+
+    @override_settings(LAUNCHER_CLIENT_STUB=True)
+    def test_applications_list_filters_by_search(self):
+        resp = self.client.get("/api/launcher/applications/?search=namd")
+        self.assertEqual(resp.status_code, 200)
+        names = [a["name"] for a in resp.json()["results"]]
+        self.assertIn("NAMD", names)
+
+    @override_settings(LAUNCHER_CLIENT_STUB=True)
+    def test_application_detail(self):
+        resp = self.client.get("/api/launcher/applications/namd/")
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(resp.json()["app_id"], "namd")
+
+    @override_settings(LAUNCHER_CLIENT_STUB=True)
+    def test_resource_profile_for_project(self):
+        resp = 
self.client.get("/api/launcher/projects/proj-1/resource-profile/")
+        self.assertEqual(resp.status_code, 200)
+        body = resp.json()
+        self.assertIn("compute_resources", body)
+        self.assertIn("allocation_id", body)
+
+    @override_settings(LAUNCHER_CLIENT_STUB=True)
+    def test_user_storages(self):
+        resp = self.client.get("/api/launcher/storages/")
+        self.assertEqual(resp.status_code, 200)
+        body = resp.json()
+        self.assertIn("results", body)
+        self.assertTrue(any(s.get("is_primary") for s in body["results"]))
+
+    @override_settings(LAUNCHER_CLIENT_STUB=True)
+    def test_projects_list(self):
+        resp = self.client.get("/api/launcher/projects/")
+        self.assertEqual(resp.status_code, 200)
+        body = resp.json()
+        self.assertIn("results", body)
+        self.assertTrue(len(body["results"]) >= 1)
+        self.assertIn("project_id", body["results"][0])
+        self.assertIn("name", body["results"][0])
+
+    def test_endpoints_require_auth(self):
+        # Clear the forced authentication so the requests are anonymous.
+        self.client.force_authenticate(user=None)
+        for url in [
+            "/api/launcher/applications/",
+            "/api/launcher/applications/namd/",
+            "/api/launcher/projects/proj-1/resource-profile/",
+            "/api/launcher/storages/",
+            "/api/launcher/projects/",
+        ]:
+            self.assertEqual(self.client.get(url).status_code, 403)
diff --git a/airavata-django-portal/django_airavata/apps/api/urls.py 
b/airavata-django-portal/django_airavata/apps/api/urls.py
index 5a34cb2f0..3997fca61 100644
--- a/airavata-django-portal/django_airavata/apps/api/urls.py
+++ b/airavata-django-portal/django_airavata/apps/api/urls.py
@@ -4,6 +4,7 @@ from django.urls import re_path
 from rest_framework import routers
 from rest_framework.urlpatterns import format_suffix_patterns
 
+from . import launcher_views
 from . import views
 from . import views_ssh
 
@@ -91,6 +92,25 @@ urlpatterns = [
 
 urlpatterns = router.urls + format_suffix_patterns(urlpatterns)
 
+# Launcher endpoints — thin proxies over launcher_client.
+# The parameterized project route must be listed before the bare projects list
+# so Django's regex matcher resolves it first.
+urlpatterns += [
+    re_path(r"^launcher/applications/$", launcher_views.applications_list, 
name="launcher_applications_list"),
+    re_path(
+        r"^launcher/applications/(?P<app_id>[^/]+)/$",
+        launcher_views.application_detail,
+        name="launcher_application_detail",
+    ),
+    re_path(
+        r"^launcher/projects/(?P<project_id>[^/]+)/resource-profile/$",
+        launcher_views.project_resource_profile,
+        name="launcher_project_resource_profile",
+    ),
+    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"),
+]
+
 if logger.isEnabledFor(logging.DEBUG):
     for router_url in router.urls:
         logger.debug(f"router url: {router_url}")

Reply via email to