This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new 4959f7c4e feat(portal): repoint project reads to gRPC + fix SDK 
protobuf floor (Track D, D2) (#158)
4959f7c4e is described below

commit 4959f7c4ec237ed87a79416d265ae684e2315f8d
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Mon Jun 8 16:21:54 2026 -0400

    feat(portal): repoint project reads to gRPC + fix SDK protobuf floor (Track 
D, D2) (#158)
    
    First view migration off Thrift onto the new gRPC facade, establishing the
    reusable D2 pattern:
    
    - ProjectViewSet reads (get_list, get_instance, list_all) now call
      request.airavata.research.get_user_projects/get_project instead of the 
Thrift
      request.airavata_client. Writes (create/update) stay on Thrift for D3.
    - grpc_adapters.project(): adapts the gRPC Project protobuf to the Thrift
      attribute names the existing ProjectSerializer reads 
(project_id->projectID,
      creation_time->creationTime, ...), so the portal's REST contract with the 
Vue
      frontend is byte-for-byte unchanged (serializer reused as-is). Verified: 
a real
      Project protobuf maps onto all 8 Thrift Project fields.
    - exceptions.custom_exception_handler: map gRPC StatusCode -> HTTP 
(NOT_FOUND 404,
      PERMISSION_DENIED 403, UNAUTHENTICATED 401, INVALID_ARGUMENT 400,
      ALREADY_EXISTS 409, UNAVAILABLE -> apiServerDown 500), mirroring the 
Thrift
      handling so migrated views behave identically. Reused by every later 
migration.
    
    Requirements fix (D1 #157 got the floor wrong): the generated stubs use
    protobuf's runtime_version (added in 5.26), so protobuf>=5.26,<7 
(googleapis-common-protos
    caps <7). Removed the unused google-api-python-client whose google-api-core
    transitive hard-pinned protobuf<4 — nothing in the portal imports it.
    
    Verified: pip check clean, generated stubs import, manage.py check clean, 
and
    the adapter+serializer reproduce the existing JSON shape.
---
 .../django_airavata/apps/api/exceptions.py         | 30 ++++++++++++++++++++++
 .../django_airavata/apps/api/grpc_adapters.py      | 30 ++++++++++++++++++++++
 .../django_airavata/apps/api/views.py              | 19 +++++++++-----
 airavata-django-portal/requirements.txt            |  9 ++++---
 4 files changed, 78 insertions(+), 10 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/api/exceptions.py 
b/airavata-django-portal/django_airavata/apps/api/exceptions.py
index 7d26a5711..b14bce2c7 100644
--- a/airavata-django-portal/django_airavata/apps/api/exceptions.py
+++ b/airavata-django-portal/django_airavata/apps/api/exceptions.py
@@ -1,6 +1,7 @@
 import logging
 import sys
 
+import grpc
 from airavata.api.error.ttypes import (
     AuthorizationException,
     ExperimentNotFoundException
@@ -16,12 +17,41 @@ from thrift.transport import TTransport
 
 log = logging.getLogger(__name__)
 
+# Track D: map new-stack gRPC status codes to HTTP responses, mirroring the
+# Thrift exception handling below so migrated views behave identically.
+GRPC_STATUS_TO_HTTP = {
+    grpc.StatusCode.NOT_FOUND: status.HTTP_404_NOT_FOUND,
+    grpc.StatusCode.PERMISSION_DENIED: status.HTTP_403_FORBIDDEN,
+    grpc.StatusCode.UNAUTHENTICATED: status.HTTP_401_UNAUTHORIZED,
+    grpc.StatusCode.INVALID_ARGUMENT: status.HTTP_400_BAD_REQUEST,
+    grpc.StatusCode.FAILED_PRECONDITION: status.HTTP_400_BAD_REQUEST,
+    grpc.StatusCode.ALREADY_EXISTS: status.HTTP_409_CONFLICT,
+    grpc.StatusCode.UNIMPLEMENTED: status.HTTP_501_NOT_IMPLEMENTED,
+}
+
 
 def custom_exception_handler(exc, context):
     # Call REST framework's default exception handler first,
     # to get the standard error response.
     response = exception_handler(exc, context)
 
+    if isinstance(exc, grpc.RpcError):
+        code = exc.code()
+        detail = exc.details() or str(exc)
+        if code == grpc.StatusCode.UNAVAILABLE:
+            log.warning("gRPC UNAVAILABLE", exc_info=exc)
+            return Response(
+                {'detail': detail, 'apiServerDown': True},
+                status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+        http_status = GRPC_STATUS_TO_HTTP.get(
+            code, status.HTTP_500_INTERNAL_SERVER_ERROR)
+        if http_status >= 500:
+            log.error("gRPC error %s", code, exc_info=exc,
+                      extra={'request': context['request']})
+        else:
+            log.warning("gRPC error %s", code, exc_info=exc)
+        return Response({'detail': detail}, status=http_status)
+
     if isinstance(exc, AuthorizationException):
         log.warning("AuthorizationException", exc_info=exc)
         return Response(
diff --git a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py 
b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
new file mode 100644
index 000000000..1933ca489
--- /dev/null
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
@@ -0,0 +1,30 @@
+"""Adapters from gRPC protobuf messages to the attribute shape the existing
+DRF serializers read.
+
+Track D: while ``apps/api`` views are repointed from the Thrift API to the gRPC
+facade (``request.airavata``), the portal keeps its REST contract with the Vue
+frontend unchanged by reusing the existing serializers. Those serializers were
+generated from the Thrift models, so they read Thrift attribute names
+(``projectID``, ``creationTime``, ...). These adapters expose the corresponding
+protobuf fields (``project_id``, ``creation_time``, ...) under those Thrift 
names,
+so serializer output is identical by construction. They are removed once the
+serializers are made protobuf-native.
+"""
+
+from types import SimpleNamespace
+
+
+def project(pb):
+    """gRPC ``Project`` protobuf -> ``ProjectSerializer`` (Thrift ``Project``) 
shape."""
+    return SimpleNamespace(
+        projectID=pb.project_id,
+        owner=pb.owner,
+        gatewayId=pb.gateway_id,
+        name=pb.name,
+        description=pb.description,
+        # int64 epoch millis, like the Thrift field; 0 (unset) -> None for the
+        # serializer's allow_null creationTime.
+        creationTime=pb.creation_time or None,
+        sharedUsers=list(pb.shared_users),
+        sharedGroups=list(pb.shared_groups),
+    )
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py 
b/airavata-django-portal/django_airavata/apps/api/views.py
index 1cca33e26..bf6e07cfa 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -65,6 +65,7 @@ from django_airavata.apps.auth.models import EmailVerification
 
 from . import (
     exceptions,
+    grpc_adapters,
     helpers,
     models,
     output_views,
@@ -156,14 +157,16 @@ class ProjectViewSet(APIBackedViewSet):
 
         class ProjectResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
-                return view.request.airavata_client.getUserProjects(
-                    view.authz_token, view.gateway_id, view.username, limit, 
offset)
+                projects = view.request.airavata.research.get_user_projects(
+                    gateway_id=view.gateway_id, user_name=view.username,
+                    limit=limit, offset=offset)
+                return [grpc_adapters.project(p) for p in projects]
 
         return ProjectResultIterator()
 
     def get_instance(self, lookup_value):
-        return self.request.airavata_client.getProject(
-            self.authz_token, lookup_value)
+        return grpc_adapters.project(
+            self.request.airavata.research.get_project(lookup_value))
 
     def perform_create(self, serializer):
         project = serializer.save(
@@ -182,8 +185,12 @@ class ProjectViewSet(APIBackedViewSet):
 
     @action(detail=False)
     def list_all(self, request):
-        projects = self.request.airavata_client.getUserProjects(
-            self.authz_token, self.gateway_id, self.username, -1, 0)
+        projects = [
+            grpc_adapters.project(p)
+            for p in self.request.airavata.research.get_user_projects(
+                gateway_id=self.gateway_id, user_name=self.username,
+                limit=-1, offset=0)
+        ]
         serializer = serializers.ProjectSerializer(
             projects, many=True, context={'request': request})
         return Response(serializer.data)
diff --git a/airavata-django-portal/requirements.txt 
b/airavata-django-portal/requirements.txt
index 32e8aadd1..13442761d 100644
--- a/airavata-django-portal/requirements.txt
+++ b/airavata-django-portal/requirements.txt
@@ -13,12 +13,13 @@ jupyter==1.0.0
 papermill==1.0.1
 
 # gRPC libs. Bumped for the new Airavata gRPC SDK (airavata-python-sdk 3.0.0,
-# Track D): grpcio>=1.60 and protobuf>=4.25. google-api-python-client is bumped
-# off 1.12.8 because its old google-api-core pinned protobuf<4.
-google-api-python-client>=2.0
+# Track D): grpcio>=1.60 and protobuf>=5.26 (the generated stubs use
+# protobuf's runtime_version, added in 5.26). The unused 
google-api-python-client
+# was removed — its google-api-core transitive hard-pinned protobuf<4, which is
+# incompatible with the new stubs (and nothing in the portal imports it).
 grpcio>=1.60.0
 grpcio-tools>=1.60.0
-protobuf>=4.25.0
+protobuf>=5.26.0,<7.0.0
 googleapis-common-protos>=1.62.0
 
 # Legacy Thrift SDK — still required while apps/api views are repointed from

Reply via email to