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 126fa75bb feat(portal): stand up the new gRPC AiravataClient (Track D,
D1) (#157)
126fa75bb is described below
commit 126fa75bb413b9578513c6663d46c1d08b7afb42
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Mon Jun 8 16:07:43 2026 -0400
feat(portal): stand up the new gRPC AiravataClient (Track D, D1) (#157)
First step of repointing the portal from the legacy Thrift API to the new
Airavata gRPC/REST backend. Additive and non-breaking: introduces the new
gRPC client (airavata-python-sdk 3.0.0 AiravataClient) alongside the
existing
Thrift client so apps/api views can be migrated resource family by resource
family.
- django_airavata/airavata_grpc.py: build an AiravataClient from a request's
Keycloak access token (GRPC_API_HOST/PORT/SECURE + GATEWAY_ID); the SDK is
imported lazily.
- middleware.airavata_grpc_client: attach request.airavata as a
SimpleLazyObject
so the client (and the airavata_sdk import) is built only when a view
first
uses it, and the channel is closed after the response. Registered after
authz_token_middleware. Coexists with request.airavata_client (Thrift).
- settings: GRPC_API_HOST/PORT/SECURE defaults targeting the tilt server on
:9090.
Requirements: grpcio>=1.60 and protobuf>=4.25 for the new SDK;
googleapis-common-protos added; google-api-python-client bumped off 1.12.8
(its old google-api-core pinned protobuf<4). The new gRPC SDK shares the
airavata-python-sdk PyPI/import name with the legacy 2.2.7 Thrift SDK, so
during
the transition it is provided on PYTHONPATH from the local apache/airavata
checkout; both legacy SDKs are dropped at D6. protobuf>=4 breaks the legacy
MFT
storage stubs (regenerated in D4); the dev DjangoFileSystemProvider path is
unaffected.
---
.../django_airavata/airavata_grpc.py | 52 ++++++++++++++++++++++
.../django_airavata/middleware.py | 35 +++++++++++++++
airavata-django-portal/django_airavata/settings.py | 13 ++++++
airavata-django-portal/requirements.txt | 23 +++++++---
4 files changed, 117 insertions(+), 6 deletions(-)
diff --git a/airavata-django-portal/django_airavata/airavata_grpc.py
b/airavata-django-portal/django_airavata/airavata_grpc.py
new file mode 100644
index 000000000..203d80e80
--- /dev/null
+++ b/airavata-django-portal/django_airavata/airavata_grpc.py
@@ -0,0 +1,52 @@
+"""New-stack gRPC Airavata client (airavata-python-sdk ``AiravataClient``).
+
+Track D: the portal is migrating from the legacy Thrift API to the new Airavata
+gRPC/REST server. This module builds the gRPC ``AiravataClient`` from a
request's
+Keycloak access token. It is intentionally additive — the gRPC client
+(``request.airavata``) coexists with the legacy Thrift client
+(``request.airavata_client``) while ``apps/api`` views are repointed resource
+family by resource family. The Thrift client and ``thrift_utils`` are removed
once
+nothing depends on them.
+
+Per the migration principle, the "talk to Airavata + transform" grunt belongs
in
+``airavata-python-sdk`` (the facade sub-clients on ``AiravataClient``),
keeping the
+portal a thin adapter.
+"""
+
+import logging
+
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+
+def build_airavata_client(access_token, gateway_id=None, claims=None):
+ """Build an :class:`AiravataClient` for the given Keycloak access token.
+
+ The SDK is imported lazily so importing this module does not require the
new
+ SDK to be installed (it is provided on the path during the transition).
+ """
+ from airavata_sdk.client import AiravataClient
+
+ gateway_id = gateway_id or settings.GATEWAY_ID
+ return AiravataClient(
+ host=settings.GRPC_API_HOST,
+ port=settings.GRPC_API_PORT,
+ token=access_token,
+ gateway_id=gateway_id,
+ secure=settings.GRPC_API_SECURE,
+ claims=claims,
+ )
+
+
+def airavata_client_for_request(request):
+ """Build an :class:`AiravataClient` from ``request.authz_token``.
+
+ Returns ``None`` for unauthenticated requests (no ``authz_token``). The
caller
+ is responsible for closing the returned client.
+ """
+ authz_token = getattr(request, "authz_token", None)
+ if authz_token is None:
+ return None
+ claims = dict(authz_token.claimsMap) if authz_token.claimsMap else None
+ return build_airavata_client(authz_token.accessToken, claims=claims)
diff --git a/airavata-django-portal/django_airavata/middleware.py
b/airavata-django-portal/django_airavata/middleware.py
index 8c18c81d7..cc1eb7462 100644
--- a/airavata-django-portal/django_airavata/middleware.py
+++ b/airavata-django-portal/django_airavata/middleware.py
@@ -33,6 +33,41 @@ class AiravataClientMiddleware:
return None
+def airavata_grpc_client(get_response):
+ """Attach the new-stack gRPC ``AiravataClient`` as ``request.airavata``.
+
+ Track D: additive — coexists with the legacy Thrift
``request.airavata_client``
+ while ``apps/api`` views are repointed from Thrift to gRPC.
``request.airavata``
+ is a lazy object: the client (and the ``airavata_sdk`` import) is built
only
+ when a view first accesses it, carrying the user's Keycloak token from
+ ``request.authz_token``. The channel is closed after the response if it was
+ used. Views that never touch ``request.airavata`` incur no cost and do not
+ require the SDK to be importable.
+
+ Usage in a view::
+
+ experiments = request.airavata.research.get_user_experiments(
+ gateway_id=settings.GATEWAY_ID, user_name=request.user.username)
+ """
+ from django.utils.functional import SimpleLazyObject, empty
+
+ from .airavata_grpc import airavata_client_for_request
+
+ def middleware(request):
+ request.airavata = SimpleLazyObject(
+ lambda: airavata_client_for_request(request))
+ try:
+ return get_response(request)
+ finally:
+ lazy = request.__dict__.get('airavata')
+ if isinstance(lazy, SimpleLazyObject) and lazy._wrapped is not
empty:
+ client = lazy._wrapped
+ if client is not None:
+ client.close()
+
+ return middleware
+
+
def profile_service_client(get_response):
"""Open and close Profile Service client for each request.
diff --git a/airavata-django-portal/django_airavata/settings.py
b/airavata-django-portal/django_airavata/settings.py
index b96e74be0..d33cccdeb 100644
--- a/airavata-django-portal/django_airavata/settings.py
+++ b/airavata-django-portal/django_airavata/settings.py
@@ -72,6 +72,10 @@ MIDDLEWARE = [
'django_airavata.apps.auth.middleware.authz_token_middleware',
'django_airavata.middleware.AiravataClientMiddleware',
'django_airavata.middleware.profile_service_client',
+ # Track D: new gRPC AiravataClient (request.airavata), additive alongside
the
+ # Thrift request.airavata_client. Must come after authz_token_middleware
(uses
+ # request.authz_token for the access token).
+ 'django_airavata.middleware.airavata_grpc_client',
# Needs to come after authz_token_middleware, airavata_client and
# profile_service_client
'django_airavata.apps.auth.middleware.gateway_groups_middleware',
@@ -398,6 +402,15 @@ LOGGING = {
+# New gRPC backend (Track D). The portal is migrating from the legacy Thrift
API
+# to the new Airavata gRPC/REST server (airavata-python-sdk AiravataClient).
These
+# defaults target the tilt-managed server on :9090 and may be overridden via
env
+# vars or settings_local.py. The gRPC client coexists with the Thrift client
while
+# apps/api views are repointed resource-family by resource-family.
+GRPC_API_HOST = os.environ.get('GRPC_API_HOST', 'localhost')
+GRPC_API_PORT = int(os.environ.get('GRPC_API_PORT', 9090))
+GRPC_API_SECURE = os.environ.get('GRPC_API_SECURE', 'false').lower() == 'true'
+
# Allow all settings to be overridden by settings_local.py file
try:
from django_airavata.settings_local import * # noqa
diff --git a/airavata-django-portal/requirements.txt
b/airavata-django-portal/requirements.txt
index d8133f9d3..32e8aadd1 100644
--- a/airavata-django-portal/requirements.txt
+++ b/airavata-django-portal/requirements.txt
@@ -12,13 +12,24 @@ zipstream-new==1.1.8
jupyter==1.0.0
papermill==1.0.1
-# gRPC libs
-google-api-python-client==1.12.8
-grpcio-tools==1.48.2 ; python_version < "3.7"
-grpcio-tools==1.51.1 ; python_version >= "3.7"
-grpcio==1.48.2 ; python_version < "3.7"
-grpcio==1.53.2 ; python_version >= "3.7"
+# 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
+grpcio>=1.60.0
+grpcio-tools>=1.60.0
+protobuf>=4.25.0
+googleapis-common-protos>=1.62.0
+# Legacy Thrift SDK — still required while apps/api views are repointed from
+# Thrift (request.airavata_client) to gRPC (request.airavata) resource family
by
+# resource family. The NEW gRPC SDK (import name airavata_sdk) cannot be pip
+# co-installed because it shares this PyPI/import name; during the Track D
+# transition it is provided on PYTHONPATH from the local apache/airavata
checkout
+# (PYTHONPATH=/path/to/airavata/airavata-python-sdk). Both are dropped at D6.
+# NOTE: protobuf>=4 breaks the legacy MFT storage stubs
(airavata-django-portal-sdk,
+# generated for protobuf<=3.20); the dev DjangoFileSystemProvider path is
+# unaffected and MFT regen is part of D4 (storage).
airavata-django-portal-sdk==1.8.4
airavata-python-sdk==2.2.7