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 e3e30ffbc refactor(portal): make experiment-summary + statistics
serializers proto-native (Track D) (#196)
e3e30ffbc is described below
commit e3e30ffbce10796f818be5604745a2d36e2dc901
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 03:42:38 2026 -0400
refactor(portal): make experiment-summary + statistics serializers
proto-native (Track D) (#196)
Rewrite BaseExperimentSummarySerializer, ExperimentSummarySerializer, and
ExperimentStatisticsSerializer to read the gRPC ExperimentSummaryModel /
ExperimentStatistics protobuf directly, emitting the same Thrift-named JSON.
- Preserve a pre-existing quirk byte-for-byte: the experiment-search
subclass
(ExperimentSummarySerializer) renders creationTime/statusUpdateTime as raw
epoch-millis ints (the old Thrift metaclass regenerated them as
IntegerField on
the subclass, shadowing the base's ISO field), while the base used by the
statistics summary lists renders them as ISO. New ProtoIntOrNoneField
(raw int,
proto-0 -> None) backs the subclass.
- Repoint ExperimentSearchViewSet + ExperimentStatisticsView to pass
protobuf
through directly, dropping grpc_adapters.experiment_summary /
experiment_statistics + the Thrift
ExperimentSummaryModel/ExperimentStatistics
imports.
Validated byte-for-byte (search full/min, base, statistics) vs the old
adapter+serializer path. manage.py check green; api test failures unchanged.
---
.../django_airavata/apps/api/grpc_adapters.py | 39 ---------
.../django_airavata/apps/api/serializers.py | 95 ++++++++++++++++++----
.../django_airavata/apps/api/views.py | 16 ++--
3 files changed, 84 insertions(+), 66 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 febcb2f20..47c5774ac 100644
--- a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
@@ -239,45 +239,6 @@ def compute_resource(pb):
)
-def experiment_summary(pb):
- """gRPC ``ExperimentSummaryModel`` protobuf ->
``ExperimentSummarySerializer`` shape."""
- return SimpleNamespace(
- experimentId=pb.experiment_id,
- projectId=pb.project_id,
- gatewayId=pb.gateway_id,
- creationTime=pb.creation_time or None,
- userName=pb.user_name,
- name=pb.name,
- description=pb.description,
- executionId=pb.execution_id,
- resourceHostId=pb.resource_host_id,
- experimentStatus=pb.experiment_status,
- statusUpdateTime=pb.status_update_time or None,
- )
-
-
-def experiment_statistics(pb):
- """gRPC ``ExperimentStatistics`` -> ``ExperimentStatisticsSerializer``
shape.
-
- A wrapper of per-state counts plus per-state experiment-summary lists, each
- list adapted with :func:`experiment_summary`.
- """
- return SimpleNamespace(
- allExperimentCount=pb.all_experiment_count,
- completedExperimentCount=pb.completed_experiment_count,
- cancelledExperimentCount=pb.cancelled_experiment_count,
- failedExperimentCount=pb.failed_experiment_count,
- createdExperimentCount=pb.created_experiment_count,
- runningExperimentCount=pb.running_experiment_count,
- allExperiments=[experiment_summary(s) for s in pb.all_experiments],
- completedExperiments=[experiment_summary(s) for s in
pb.completed_experiments],
- failedExperiments=[experiment_summary(s) for s in
pb.failed_experiments],
- cancelledExperiments=[experiment_summary(s) for s in
pb.cancelled_experiments],
- createdExperiments=[experiment_summary(s) for s in
pb.created_experiments],
- runningExperiments=[experiment_summary(s) for s in
pb.running_experiments],
- )
-
-
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 9754e7edc..661d7a69d 100644
--- a/airavata-django-portal/django_airavata/apps/api/serializers.py
+++ b/airavata-django-portal/django_airavata/apps/api/serializers.py
@@ -44,9 +44,7 @@ from airavata.model.data.replica.ttypes import (
DataReplicaLocationModel
)
from airavata.model.experiment.ttypes import (
- ExperimentModel,
- ExperimentStatistics,
- ExperimentSummaryModel
+ ExperimentModel
)
from airavata.model.group.ttypes import GroupModel, ResourcePermissionType
from airavata.model.job.ttypes import JobModel
@@ -214,6 +212,23 @@ def proto_enum_name_field(enum_descriptor,
proto_prefix='', **kwargs):
proto_prefix=proto_prefix, **kwargs)
+class ProtoIntOrNoneField(serializers.IntegerField):
+ """Renders a protobuf int field as a raw integer, mapping the proto-0
default
+ to ``None`` to match the old auto-generated ``IntegerField`` whose adapter
fed
+ it ``pb.<field> or None``.
+ """
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('allow_null', True)
+ kwargs.setdefault('read_only', True)
+ super().__init__(*args, **kwargs)
+
+ def to_representation(self, value):
+ if not value:
+ return None
+ return super().to_representation(value)
+
+
class StoredJSONField(serializers.JSONField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -852,25 +867,50 @@ class FullExperimentSerializer(serializers.Serializer):
raise Exception("Not implemented")
-class BaseExperimentSummarySerializer(
- thrift_utils.create_serializer_class(ExperimentSummaryModel)):
- creationTime = UTCPosixTimestampDateTimeField()
- statusUpdateTime = UTCPosixTimestampDateTimeField()
+class BaseExperimentSummarySerializer(serializers.Serializer):
+ """Proto-native serializer for the gRPC ``ExperimentSummaryModel`` message.
+
+ Read-only; used directly for the experiment-statistics summary lists (where
+ the timestamps render as ISO strings — see
:class:`ExperimentSummarySerializer`
+ for the experiment-search variant's int rendering).
+ """
+
+ experimentId = serializers.CharField(source='experiment_id',
read_only=True)
+ projectId = serializers.CharField(source='project_id', read_only=True)
+ gatewayId = serializers.CharField(source='gateway_id', read_only=True)
+ creationTime = ProtoTimestampField(
+ source='creation_time', null_if_zero=True, read_only=True)
+ userName = serializers.CharField(source='user_name', read_only=True)
+ name = serializers.CharField(read_only=True)
+ description = serializers.CharField(read_only=True)
+ executionId = serializers.CharField(source='execution_id', read_only=True)
+ resourceHostId = serializers.CharField(
+ source='resource_host_id', read_only=True)
+ experimentStatus = serializers.CharField(
+ source='experiment_status', read_only=True)
+ statusUpdateTime = ProtoTimestampField(
+ source='status_update_time', null_if_zero=True, read_only=True)
url = FullyEncodedHyperlinkedIdentityField(
view_name='django_airavata_api:experiment-detail',
- lookup_field='experimentId',
+ lookup_field='experiment_id',
lookup_url_kwarg='experiment_id')
project = FullyEncodedHyperlinkedIdentityField(
view_name='django_airavata_api:project-detail',
- lookup_field='projectId',
+ lookup_field='project_id',
lookup_url_kwarg='project_id')
class ExperimentSummarySerializer(BaseExperimentSummarySerializer):
+ # The experiment-search list historically rendered these timestamps as raw
+ # epoch-millis ints (the Thrift metaclass regenerated them as IntegerField
on
+ # this subclass, shadowing the base's ISO field); preserve that exactly.
+ creationTime = ProtoIntOrNoneField(source='creation_time')
+ statusUpdateTime = ProtoIntOrNoneField(source='status_update_time')
userHasWriteAccess = serializers.SerializerMethodField()
def get_userHasWriteAccess(self, experiment):
- return user_has_access(self.context['request'],
experiment.experimentId)
+ return user_has_access(
+ self.context['request'], experiment.experiment_id)
class UserProfileSerializer(
@@ -2231,14 +2271,33 @@ class
NotificationSerializer(thrift_utils.create_serializer_class(Notification))
)
-class ExperimentStatisticsSerializer(
- thrift_utils.create_serializer_class(ExperimentStatistics)):
- allExperiments = BaseExperimentSummarySerializer(many=True)
- completedExperiments = BaseExperimentSummarySerializer(many=True)
- failedExperiments = BaseExperimentSummarySerializer(many=True)
- cancelledExperiments = BaseExperimentSummarySerializer(many=True)
- createdExperiments = BaseExperimentSummarySerializer(many=True)
- runningExperiments = BaseExperimentSummarySerializer(many=True)
+class ExperimentStatisticsSerializer(serializers.Serializer):
+ """Proto-native serializer for the gRPC ``ExperimentStatistics``
message."""
+
+ allExperimentCount = serializers.IntegerField(
+ source='all_experiment_count', read_only=True)
+ completedExperimentCount = serializers.IntegerField(
+ source='completed_experiment_count', read_only=True)
+ cancelledExperimentCount = serializers.IntegerField(
+ source='cancelled_experiment_count', read_only=True)
+ failedExperimentCount = serializers.IntegerField(
+ source='failed_experiment_count', read_only=True)
+ createdExperimentCount = serializers.IntegerField(
+ source='created_experiment_count', read_only=True)
+ runningExperimentCount = serializers.IntegerField(
+ source='running_experiment_count', read_only=True)
+ allExperiments = BaseExperimentSummarySerializer(
+ source='all_experiments', many=True, read_only=True)
+ completedExperiments = BaseExperimentSummarySerializer(
+ source='completed_experiments', many=True, read_only=True)
+ failedExperiments = BaseExperimentSummarySerializer(
+ source='failed_experiments', many=True, read_only=True)
+ cancelledExperiments = BaseExperimentSummarySerializer(
+ source='cancelled_experiments', many=True, read_only=True)
+ createdExperiments = BaseExperimentSummarySerializer(
+ source='created_experiments', many=True, read_only=True)
+ runningExperiments = BaseExperimentSummarySerializer(
+ source='running_experiments', many=True, read_only=True)
class UnverifiedEmailUserProfile(serializers.Serializer):
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py
b/airavata-django-portal/django_airavata/apps/api/views.py
index 3a82a67f6..99be9e76e 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -363,10 +363,9 @@ class ExperimentSearchViewSet(mixins.ListModelMixin,
GenericAPIBackedViewSet):
class ExperimentSearchResultIterator(APIResultIterator):
def get_results(self, limit=-1, offset=0):
- summaries = view.request.airavata.research.search_experiments(
+ return list(view.request.airavata.research.search_experiments(
gateway_id=view.gateway_id, user_name=view.username,
- filters=filters, limit=limit, offset=offset)
- return [grpc_adapters.experiment_summary(s) for s in summaries]
+ filters=filters, limit=limit, offset=offset))
# Preserve query parameters when moving to next and previous links
return
ExperimentSearchResultIterator(query_params=self.request.query_params.copy())
@@ -1996,15 +1995,14 @@ class ExperimentStatisticsView(APIView):
limit = int(request.GET.get('limit', '50'))
offset = int(request.GET.get('offset', '0'))
- statistics = grpc_adapters.experiment_statistics(
- request.airavata.research.get_experiment_statistics(
- settings.GATEWAY_ID, from_time, to_time,
- username or "", application_name or "", resource_hostname or
"",
- limit, offset))
+ statistics = request.airavata.research.get_experiment_statistics(
+ settings.GATEWAY_ID, from_time, to_time,
+ username or "", application_name or "", resource_hostname or "",
+ limit, offset)
serializer = self.serializer_class(statistics, context={'request':
request})
paginator = pagination.LimitOffsetPagination()
- paginator.count = statistics.allExperimentCount
+ paginator.count = statistics.all_experiment_count
paginator.limit = limit
paginator.offset = offset
paginator.request = request