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 bf3cc12a7 refactor(portal): make CredentialSummarySerializer 
proto-native (Track D) (#195)
bf3cc12a7 is described below

commit bf3cc12a7e2a70ab7729e53208b9c2e3315ba917
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 03:37:26 2026 -0400

    refactor(portal): make CredentialSummarySerializer proto-native (Track D) 
(#195)
    
    Rewrite CredentialSummarySerializer to read the gRPC CredentialSummary 
protobuf
    directly and emit the same Thrift-named JSON keys, REST contract 
byte-for-byte
    unchanged.
    
    - Repoint CredentialSummaryViewSet (list/instance/ssh/password/create_ssh/
      create_password/destroy) to pass protobuf through directly and dispatch 
on the
      proto SummaryType enum, dropping the grpc_adapters.credential_summary 
roundtrip
      and grpc_adapters.proto_summary_type.
    - New reusable ProtoEnumNameField (+ proto_enum_name_field factory): 
renders a
      proto enum field as the member name, matching the old ThriftEnumField 
output.
      The factory snapshots the proto enum descriptor into plain dicts so the 
field
      is deep-copyable when DRF binds it.
    - Remove grpc_adapters.credential_summary / proto_summary_type and the 
Thrift
      CredentialSummary/SummaryType imports.
    
    Validated byte-for-byte (ssh/password/minimal — incl. the proto-0 
persistedTime
    that renders as the epoch, not null) vs the old adapter+serializer path.
    manage.py check green; api test failures unchanged vs origin/main.
---
 .../django_airavata/apps/api/grpc_adapters.py      | 35 ----------
 .../django_airavata/apps/api/serializers.py        | 79 +++++++++++++++++++---
 .../django_airavata/apps/api/views.py              | 40 ++++++-----
 3 files changed, 90 insertions(+), 64 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py 
b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
index 72bede0c8..febcb2f20 100644
--- a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
@@ -28,7 +28,6 @@ from airavata.model.appcatalog.parallelism.ttypes import (
 )
 from airavata.model.appcatalog.parser.ttypes import IOType as _ThriftIOType
 from airavata.model.application.io.ttypes import DataType as _ThriftDataType
-from airavata.model.credential.store.ttypes import SummaryType as 
_ThriftSummaryType
 from airavata.model.data.replica.ttypes import (
     DataProductType as _ThriftDataProductType,
     ReplicaLocationCategory as _ThriftReplicaLocationCategory,
@@ -240,22 +239,6 @@ def compute_resource(pb):
     )
 
 
-def proto_summary_type(thrift_summary_type):
-    """Thrift ``SummaryType`` -> proto ``SummaryType`` enum value (by name).
-
-    The credential facade's request messages take the proto enum value, so 
views
-    that still speak in Thrift ``SummaryType`` (e.g. for delete dispatch) 
convert
-    through here. Imported lazily so this module stays importable without the
-    gRPC SDK on the path (the SDK is required only once ``request.airavata`` is
-    actually used).
-    """
-    from airavata_sdk.generated.org.apache.airavata.model.credential.store 
import (
-        credential_store_pb2,
-    )
-    return credential_store_pb2.SummaryType.Value(
-        _ThriftSummaryType(thrift_summary_type).name)
-
-
 def experiment_summary(pb):
     """gRPC ``ExperimentSummaryModel`` protobuf -> 
``ExperimentSummarySerializer`` shape."""
     return SimpleNamespace(
@@ -295,24 +278,6 @@ def experiment_statistics(pb):
     )
 
 
-def credential_summary(pb):
-    """gRPC ``CredentialSummary`` protobuf -> ``CredentialSummarySerializer`` 
shape."""
-    return SimpleNamespace(
-        # proto/Thrift SummaryType have different ints per name -> bridge by 
name
-        # so the serializer's ThriftEnumField labels it correctly and
-        # perform_destroy's ``instance.type == SummaryType.SSH`` (Thrift) 
holds.
-        type=_thrift_enum(pb, 'type', _ThriftSummaryType),
-        gatewayId=pb.gateway_id,
-        username=pb.username,
-        publicKey=pb.public_key,
-        # int64 epoch millis, like the Thrift field; the serializer's
-        # UTCPosixTimestampDateTimeField divides by 1000, so keep it an int.
-        persistedTime=pb.persisted_time,
-        token=pb.token,
-        description=pb.description,
-    )
-
-
 def _input_data_object(pb):
     """gRPC ``InputDataObjectType`` -> ``InputDataObjectTypeSerializer`` 
shape."""
     return SimpleNamespace(
diff --git a/airavata-django-portal/django_airavata/apps/api/serializers.py 
b/airavata-django-portal/django_airavata/apps/api/serializers.py
index 14b6cd3a8..9754e7edc 100644
--- a/airavata-django-portal/django_airavata/apps/api/serializers.py
+++ b/airavata-django-portal/django_airavata/apps/api/serializers.py
@@ -39,10 +39,6 @@ from airavata.model.application.io.ttypes import (
     InputDataObjectType,
     OutputDataObjectType
 )
-from airavata.model.credential.store.ttypes import (
-    CredentialSummary,
-    SummaryType
-)
 from airavata.model.data.replica.ttypes import (
     DataProductModel,
     DataReplicaLocationModel
@@ -166,6 +162,58 @@ class ProtoTimestampField(UTCPosixTimestampDateTimeField):
         return super().to_representation(obj)
 
 
+class ProtoEnumNameField(serializers.Field):
+    """Renders a protobuf enum field as the enum member NAME, the same string 
the
+    Thrift-generated ``ThriftEnumField`` / ``EnumChoiceField`` emitted.
+
+    The instance is the protobuf message and ``source`` is the proto enum field
+    name; ``to_representation`` receives that field's integer value and 
resolves
+    it to the member name. Construct via :func:`proto_enum_name_field`, which
+    snapshots the proto enum descriptor into the plain 
``by_number``/``by_name``
+    dicts this field holds (the descriptor itself cannot be deep-copied, and 
DRF
+    deep-copies field instances when binding them). ``proto_prefix`` strips a
+    proto-only member prefix (proto3 namespaces members that would otherwise
+    collide, e.g. ``NOTIFICATION_PRIORITY_LOW`` -> ``LOW``); the bare-named
+    ``*_UNKNOWN`` zero sentinel renders ``None`` to match the old nullable 
fields.
+    """
+
+    def __init__(self, by_number, by_name, proto_prefix='', **kwargs):
+        self._by_number = by_number
+        self._by_name = by_name
+        self.proto_prefix = proto_prefix
+        super().__init__(**kwargs)
+
+    def to_representation(self, value):
+        name = self._by_number[value]
+        if self.proto_prefix and name.startswith(self.proto_prefix):
+            name = name[len(self.proto_prefix):]
+        if name.endswith('UNKNOWN') and value == 0:
+            return None
+        return name
+
+    def to_internal_value(self, data):
+        # Writes pass the member name; map back to the proto integer value.
+        name = data
+        if self.proto_prefix and (self.proto_prefix + name) in self._by_name:
+            name = self.proto_prefix + name
+        try:
+            return self._by_name[name]
+        except KeyError:
+            self.fail('invalid_choice', input=data)
+
+
+def proto_enum_name_field(enum_descriptor, proto_prefix='', **kwargs):
+    """Build a :class:`ProtoEnumNameField` from a proto enum descriptor.
+
+    Snapshots the descriptor into plain int<->name dicts so the resulting field
+    is deep-copyable (DRF deep-copies fields when binding them to a 
serializer).
+    """
+    return ProtoEnumNameField(
+        by_number={v.number: v.name for v in enum_descriptor.values},
+        by_name={v.name: v.number for v in enum_descriptor.values},
+        proto_prefix=proto_prefix, **kwargs)
+
+
 class StoredJSONField(serializers.JSONField):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -1889,10 +1937,25 @@ class SharedEntitySerializer(serializers.Serializer):
             request, shared_entity['entityId'], "MANAGE_SHARING")
 
 
-class CredentialSummarySerializer(
-        thrift_utils.create_serializer_class(CredentialSummary)):
-    type = thrift_utils.ThriftEnumField(SummaryType)
-    persistedTime = UTCPosixTimestampDateTimeField()
+def _credential_store_pb2():
+    from airavata_sdk.generated.org.apache.airavata.model.credential.store 
import (
+        credential_store_pb2,
+    )
+    return credential_store_pb2
+
+
+class CredentialSummarySerializer(serializers.Serializer):
+    """Proto-native serializer for the gRPC ``CredentialSummary`` message."""
+
+    type = proto_enum_name_field(
+        _credential_store_pb2().SummaryType.DESCRIPTOR, read_only=True)
+    gatewayId = serializers.CharField(source='gateway_id', read_only=True)
+    username = serializers.CharField(read_only=True)
+    publicKey = serializers.CharField(source='public_key', read_only=True)
+    persistedTime = ProtoTimestampField(
+        source='persisted_time', read_only=True)
+    token = serializers.CharField(read_only=True)
+    description = serializers.CharField(read_only=True)
     userHasWriteAccess = serializers.SerializerMethodField()
 
     def get_userHasWriteAccess(self, credential_summary):
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py 
b/airavata-django-portal/django_airavata/apps/api/views.py
index b59337bd4..3a82a67f6 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -14,7 +14,6 @@ from airavata.model.appcatalog.computeresource.ttypes import (
     UnicoreJobSubmission
 )
 from airavata.model.application.io.ttypes import DataType
-from airavata.model.credential.store.ttypes import SummaryType
 from airavata.model.data.movement.ttypes import (
     GridFTPDataMovement,
     LOCALDataMovement,
@@ -1426,31 +1425,31 @@ class CredentialSummaryViewSet(APIBackedViewSet):
     serializer_class = serializers.CredentialSummarySerializer
 
     def _credential_summaries(self, summary_type):
-        return [
-            grpc_adapters.credential_summary(s)
-            for s in 
self.request.airavata.credential.get_all_credential_summaries(
-                self.gateway_id, 
grpc_adapters.proto_summary_type(summary_type))
-        ]
+        return list(
+            self.request.airavata.credential.get_all_credential_summaries(
+                self.gateway_id, summary_type))
 
     def get_list(self):
-        return (self._credential_summaries(SummaryType.SSH) +
-                self._credential_summaries(SummaryType.PASSWD))
+        pb2 = serializers._credential_store_pb2()
+        return (self._credential_summaries(pb2.SummaryType.SSH) +
+                self._credential_summaries(pb2.SummaryType.PASSWD))
 
     def get_instance(self, lookup_value):
-        return grpc_adapters.credential_summary(
-            self.request.airavata.credential.get_credential_summary(
-                lookup_value, self.gateway_id))
+        return self.request.airavata.credential.get_credential_summary(
+            lookup_value, self.gateway_id)
 
     @action(detail=False)
     def ssh(self, request):
+        pb2 = serializers._credential_store_pb2()
         serializer = self.get_serializer(
-            self._credential_summaries(SummaryType.SSH), many=True)
+            self._credential_summaries(pb2.SummaryType.SSH), many=True)
         return Response(serializer.data)
 
     @action(detail=False)
     def password(self, request):
+        pb2 = serializers._credential_store_pb2()
         serializer = self.get_serializer(
-            self._credential_summaries(SummaryType.PASSWD), many=True)
+            self._credential_summaries(pb2.SummaryType.PASSWD), many=True)
         return Response(serializer.data)
 
     @action(methods=['post'], detail=False)
@@ -1460,9 +1459,8 @@ class CredentialSummaryViewSet(APIBackedViewSet):
         description = request.data.get('description')
         token_id = request.airavata.credential.generate_and_register_ssh_keys(
             self.gateway_id, self.username, description)
-        credential_summary = grpc_adapters.credential_summary(
-            request.airavata.credential.get_credential_summary(
-                token_id, self.gateway_id))
+        credential_summary = 
request.airavata.credential.get_credential_summary(
+            token_id, self.gateway_id)
         serializer = self.get_serializer(credential_summary)
         return Response(serializer.data)
 
@@ -1480,17 +1478,17 @@ class CredentialSummaryViewSet(APIBackedViewSet):
             self.gateway_id,
             grpc_requests.password_credential(
                 self.gateway_id, self.username, username, password, 
description))
-        credential_summary = grpc_adapters.credential_summary(
-            request.airavata.credential.get_credential_summary(
-                token_id, self.gateway_id))
+        credential_summary = 
request.airavata.credential.get_credential_summary(
+            token_id, self.gateway_id)
         serializer = self.get_serializer(credential_summary)
         return Response(serializer.data)
 
     def perform_destroy(self, instance):
-        if instance.type == SummaryType.SSH:
+        pb2 = serializers._credential_store_pb2()
+        if instance.type == pb2.SummaryType.SSH:
             self.request.airavata.credential.delete_ssh_pub_key(
                 instance.token, self.gateway_id)
-        elif instance.type == SummaryType.PASSWD:
+        elif instance.type == pb2.SummaryType.PASSWD:
             self.request.airavata.credential.delete_pwd_credential(
                 instance.token, self.gateway_id)
 

Reply via email to