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
 

Reply via email to