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)