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}")
