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