From 0ff1b7ee84dc7ebd2bd805a678661a10d0ecbce1 Mon Sep 17 00:00:00 2001 From: dev-hari-prasad Date: Sat, 29 Aug 2026 17:26:08 +0530 Subject: [PATCH] Defer cloud SDK imports across AWS RDS, Azure, and Google Cloud --- web/pgadmin/misc/cloud/azure/__init__.py | 61 ++++++++++++++++------- web/pgadmin/misc/cloud/google/__init__.py | 58 +++++++++++++-------- web/pgadmin/misc/cloud/rds/__init__.py | 6 ++- 3 files changed, 83 insertions(+), 42 deletions(-) diff --git a/web/pgadmin/misc/cloud/azure/__init__.py b/web/pgadmin/misc/cloud/azure/__init__.py index 60d7c3cd942..845a69d240d 100644 --- a/web/pgadmin/misc/cloud/azure/__init__.py +++ b/web/pgadmin/misc/cloud/azure/__init__.py @@ -25,15 +25,6 @@ import os -from azure.mgmt.rdbms.postgresql_flexibleservers import \ - PostgreSQLManagementClient -from azure.identity import AzureCliCredential, DeviceCodeCredential,\ - AuthenticationRecord -from azure.mgmt.resource import ResourceManagementClient -from azure.mgmt.subscription import SubscriptionClient -from azure.mgmt.rdbms.postgresql_flexibleservers.models import \ - NameAvailabilityRequest - MODULE_NAME = 'azure' @@ -265,6 +256,34 @@ def clear_session(): return make_json_response(success=1) +def _azure_sdk(): + """Defer heavy Azure SDK imports until required by user actions. + Repeat calls are cheap via sys.modules caching. + """ + from types import SimpleNamespace + from azure.identity import ( + AzureCliCredential, DeviceCodeCredential, AuthenticationRecord + ) + from azure.mgmt.rdbms.postgresql_flexibleservers import ( + PostgreSQLManagementClient + ) + from azure.mgmt.rdbms.postgresql_flexibleservers.models import ( + NameAvailabilityRequest + ) + from azure.mgmt.resource import ResourceManagementClient + from azure.mgmt.subscription import SubscriptionClient + + return SimpleNamespace( + AzureCliCredential=AzureCliCredential, + DeviceCodeCredential=DeviceCodeCredential, + AuthenticationRecord=AuthenticationRecord, + PostgreSQLManagementClient=PostgreSQLManagementClient, + ResourceManagementClient=ResourceManagementClient, + SubscriptionClient=SubscriptionClient, + NameAvailabilityRequest=NameAvailabilityRequest, + ) + + class Azure: def __init__(self, interactive_browser_credential, tenant_id=None, session_token=None, region='eastus'): @@ -367,7 +386,8 @@ def _get_azure_credentials(self): def _azure_cli_auth(self): if self._cli_credentials is None: - self._cli_credentials = AzureCliCredential() + sdk = _azure_sdk() + self._cli_credentials = sdk.AzureCliCredential() self.list_subscriptions() return self._cli_credentials @@ -380,8 +400,9 @@ def _azure_interactive_auth_prompt_callback( session['azure']['azure_auth_code'] = azure_auth_code def _azure_interactive_auth(self): + sdk = _azure_sdk() if self.authentication_record_json is None: - _interactive_credential = DeviceCodeCredential( + _interactive_credential = sdk.DeviceCodeCredential( tenant_id=self._tenant_id, timeout=180, prompt_callback=self._azure_interactive_auth_prompt_callback, @@ -392,9 +413,9 @@ def _azure_interactive_auth(self): _auth_record = _interactive_credential.authenticate() self.authentication_record_json = _auth_record.serialize() else: - deserialized_auth_record = AuthenticationRecord.deserialize( + deserialized_auth_record = sdk.AuthenticationRecord.deserialize( self.authentication_record_json) - _interactive_credential = DeviceCodeCredential( + _interactive_credential = sdk.DeviceCodeCredential( tenant_id=self._tenant_id, timeout=180, prompt_callback=self._azure_interactive_auth_prompt_callback, @@ -411,15 +432,16 @@ def _get_azure_client(self, type): return self._clients[type] _, _credentials = self._get_azure_credentials() + sdk = _azure_sdk() if type == 'postgresql': - client = PostgreSQLManagementClient(_credentials, - self.subscription_id) + client = sdk.PostgreSQLManagementClient(_credentials, + self.subscription_id) elif type == 'resource': - client = ResourceManagementClient(_credentials, - self.subscription_id) + client = sdk.ResourceManagementClient(_credentials, + self.subscription_id) elif type == 'subscription': - client = SubscriptionClient(_credentials) + client = sdk.SubscriptionClient(_credentials) self._clients[type] = client return self._clients[type] @@ -430,8 +452,9 @@ def check_cluster_name_availability(self, cluster_name): :param cluster_name """ postgresql_client = self._get_azure_client('postgresql') + sdk = _azure_sdk() res = postgresql_client.check_name_availability.execute( - NameAvailabilityRequest( + sdk.NameAvailabilityRequest( name=cluster_name, type='Microsoft.DBforPostgreSQL/flexibleServers')) res = res.__dict__ diff --git a/web/pgadmin/misc/cloud/google/__init__.py b/web/pgadmin/misc/cloud/google/__init__.py index 20fb883593c..57aff041757 100644 --- a/web/pgadmin/misc/cloud/google/__init__.py +++ b/web/pgadmin/misc/cloud/google/__init__.py @@ -38,11 +38,6 @@ # fall back to google-auth cleanly. See issue #10110. sys.modules.setdefault('oauth2client', None) -from googleapiclient import discovery -from googleapiclient.errors import HttpError -from google_auth_oauthlib.flow import InstalledAppFlow -from google.auth.transport.requests import Request - MODULE_NAME = 'google' os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # Required for Oauth2 @@ -374,6 +369,17 @@ def clear_google_session(): session.pop('google') +def _google_sdk(): + """Defer heavy Google API client imports until required by user actions. + Repeat calls are cheap via sys.modules caching. + """ + from types import SimpleNamespace + from googleapiclient import discovery + from googleapiclient.errors import HttpError + + return SimpleNamespace(discovery=discovery, HttpError=HttpError) + + class Google: def __init__(self, client_config=None): # Google cloud sql api versions @@ -468,6 +474,8 @@ def get_auth_url(self, host_url): self._verification_error = None try: self._redirect_url = host_url + 'google/callback' + # Defer InstalledAppFlow (heavy import, user action only, cached) + from google_auth_oauthlib.flow import InstalledAppFlow flow = InstalledAppFlow.from_client_config( client_config=self._client_config, scopes=self._scopes, redirect_uri=self._redirect_url) @@ -491,6 +499,8 @@ def callback(self, flask_request): if session['state'] != flask_request.args.get('state', None): self._verification_successful = False, self._verification_error = 'Invalid state parameter' + # Defer InstalledAppFlow (heavy import, user action only, cached) + from google_auth_oauthlib.flow import InstalledAppFlow flow = InstalledAppFlow.from_client_config( client_config=self._client_config, scopes=self._scopes, redirect_uri=self._redirect_url) @@ -532,6 +542,8 @@ def _get_credentials(self, scopes): if self._credentials and self._credentials.expired and \ self._credentials.refresh_token and \ self._credentials.has_scopes(scopes): + # Defer Request (heavy import, user action only, cached) + from google.auth.transport.requests import Request self._credentials.refresh(Request()) return self._credentials return self._credentials @@ -543,17 +555,18 @@ def get_projects(self): """ projects = [] error = None + sdk = _google_sdk() credentials = self._get_credentials(self._scopes) - service = discovery.build('cloudresourcemanager', - self._cloud_resource_manager_api_version, - credentials=credentials) + service = sdk.discovery.build('cloudresourcemanager', + self._cloud_resource_manager_api_version, + credentials=credentials) try: req = service.projects().list() res = req.execute() for project in res.get('projects', []): projects.append({'label': project['projectId'], 'value': project['projectId']}) - except HttpError as e: + except sdk.HttpError as e: error = e.reason except Exception as e: error = str(e) @@ -566,10 +579,11 @@ def get_regions(self, project): :return: """ self._project_id = project + sdk = _google_sdk() credentials = self._get_credentials(self._scopes) - service = discovery.build('compute', - self._compute_api_version, - credentials=credentials) + service = sdk.discovery.build('compute', + self._compute_api_version, + credentials=credentials) error = None try: req = service.regions().list(project=project) @@ -582,7 +596,7 @@ def get_regions(self, project): region_zones = list( map(lambda region: region.split('/')[-1], region_zones)) self._availability_zones[region_name] = region_zones - except HttpError as e: + except sdk.HttpError as e: error = e.reason except Exception as e: error = str(e) @@ -611,10 +625,11 @@ def get_instance_types(self, project, region): high_mem = [] instance_types = {} error = None + sdk = _google_sdk() credentials = self._get_credentials(self._scopes) - service = discovery.build('sqladmin', - self._sqladmin_api_version, - credentials=credentials) + service = sdk.discovery.build('sqladmin', + self._sqladmin_api_version, + credentials=credentials) try: req = service.tiers().list(project=project) res = req.execute() @@ -645,7 +660,7 @@ def get_instance_types(self, project, region): instance_types = {'standard': standard_instances, 'highmem': high_mem, 'shared': shared_instances} - except HttpError as e: + except sdk.HttpError as e: error = e.reason except Exception as e: error = str(e) @@ -659,10 +674,11 @@ def get_database_versions(self): pg_database_versions = [] database_versions = [] error = None + sdk = _google_sdk() credentials = self._get_credentials(self._scopes) - service = discovery.build('sqladmin', - self._sqladmin_api_version, - credentials=credentials) + service = sdk.discovery.build('sqladmin', + self._sqladmin_api_version, + credentials=credentials) try: req = service.flags().list() res = req.execute() @@ -673,7 +689,7 @@ def get_database_versions(self): label = (version.title().split('_')[0])[0:7] \ + 'SQL ' + version.split('_')[1] database_versions.append({'label': label, 'value': version}) - except HttpError as e: + except sdk.HttpError as e: error = e.reason except Exception as e: error = str(e) diff --git a/web/pgadmin/misc/cloud/rds/__init__.py b/web/pgadmin/misc/cloud/rds/__init__.py index 62c92add6c8..cd0ba507345 100644 --- a/web/pgadmin/misc/cloud/rds/__init__.py +++ b/web/pgadmin/misc/cloud/rds/__init__.py @@ -10,9 +10,7 @@ # AWS RDS Cloud Deployment Implementation import requests -import boto3 import json -from boto3.session import Session from flask_babel import gettext from flask import session, current_app, request from pgadmin.user_login_check import pga_login_required @@ -152,6 +150,8 @@ def get_regions(): """GET Regions for AWS.""" try: clear_aws_session() + # Defer boto3.session (heavy import, user action only, cached) + from boto3.session import Session _session = Session() res = _session.get_available_regions('rds') regions = [] @@ -192,6 +192,8 @@ def _get_aws_client(self, type): if type in self._clients: return self._clients[type] + # Defer boto3 (heavy import, user action only, cached) + import boto3 session = boto3.Session( aws_access_key_id=self._access_key, aws_secret_access_key=self._secret_key,