1+ """
2+ Azure client implementation for the application-sdk framework.
3+
4+ This module provides the main AzureClient class that serves as a unified interface
5+ for connecting to and interacting with Azure Storage services. It supports Service Principal
6+ authentication and provides service-specific subclients.
7+ """
8+
9+ import asyncio
10+ from concurrent .futures import ThreadPoolExecutor
11+ from typing import Any , Dict , Optional , Type
12+
13+ from azure .core .exceptions import AzureError , ClientAuthenticationError
14+ from azure .identity import DefaultAzureCredential
15+
16+ from application_sdk .clients import ClientInterface
17+ from application_sdk .clients .azure .azure_auth import AzureAuthProvider
18+ from application_sdk .clients .azure .azure_services import AzureStorageClient
19+ from application_sdk .common .error_codes import ClientError , CommonError
20+ from application_sdk .common .credential_utils import resolve_credentials
21+ from application_sdk .observability .logger_adaptor import get_logger
22+
23+ logger = get_logger (__name__ )
24+
25+
26+ class AzureClient (ClientInterface ):
27+ """
28+ Main Azure client for the application-sdk framework.
29+
30+ This client provides a unified interface for connecting to and interacting
31+ with Azure Storage services. It supports Service Principal authentication
32+ and provides service-specific subclients for different Azure services.
33+
34+ Attributes:
35+ credentials (Dict[str, Any]): Azure connection credentials
36+ resolved_credentials (Dict[str, Any]): Resolved credentials after processing
37+ credential (DefaultAzureCredential): Azure credential instance
38+ auth_provider (AzureAuthProvider): Authentication provider instance
39+ _services (Dict[str, Any]): Cache of service clients
40+ _executor (ThreadPoolExecutor): Thread pool for async operations
41+ _connection_health (bool): Connection health status
42+ """
43+
44+ def __init__ (
45+ self ,
46+ credentials : Optional [Dict [str , Any ]] = None ,
47+ max_workers : int = 10 ,
48+ ** kwargs : Any ,
49+ ):
50+ """
51+ Initialize the Azure client.
52+
53+ Args:
54+ credentials (Optional[Dict[str, Any]]): Azure Service Principal credentials.
55+ Must include tenant_id, client_id, and client_secret.
56+ max_workers (int): Maximum number of worker threads for async operations.
57+ **kwargs: Additional keyword arguments passed to service clients.
58+ """
59+ self .credentials = credentials or {}
60+ self .resolved_credentials : Dict [str , Any ] = {}
61+ self .credential : Optional [DefaultAzureCredential ] = None
62+ self .auth_provider = AzureAuthProvider ()
63+ self ._services : Dict [str , Any ] = {}
64+ self ._executor = ThreadPoolExecutor (max_workers = max_workers )
65+ self ._connection_health = False
66+ self ._kwargs = kwargs
67+
68+ async def load (self , credentials : Optional [Dict [str , Any ]] = None ) -> None :
69+ """
70+ Load and establish Azure connection using Service Principal authentication.
71+
72+ Args:
73+ credentials (Optional[Dict[str, Any]]): Azure Service Principal credentials.
74+ If provided, will override the credentials passed to __init__.
75+ Must include tenant_id, client_id, and client_secret.
76+
77+ Raises:
78+ ClientError: If connection fails due to authentication or connection issues
79+ """
80+ if credentials :
81+ self .credentials = credentials
82+
83+ try :
84+ logger .info ("Loading Azure client..." )
85+
86+ # Resolve credentials using framework's credential resolution
87+ self .resolved_credentials = await resolve_credentials (self .credentials )
88+
89+ # Create Azure credential using Service Principal authentication
90+ self .credential = await self .auth_provider .create_credential (
91+ auth_type = "service_principal" ,
92+ credentials = self .resolved_credentials
93+ )
94+
95+ # Test the connection
96+ await self ._test_connection ()
97+
98+ self ._connection_health = True
99+ logger .info ("Azure client loaded successfully" )
100+
101+ except ClientAuthenticationError as e :
102+ logger .error (f"Azure authentication failed: { str (e )} " )
103+ raise ClientError (f"{ ClientError .CLIENT_AUTH_ERROR } : { str (e )} " )
104+ except AzureError as e :
105+ logger .error (f"Azure connection error: { str (e )} " )
106+ raise ClientError (f"{ ClientError .CLIENT_AUTH_ERROR } : { str (e )} " )
107+ except Exception as e :
108+ logger .error (f"Unexpected error loading Azure client: { str (e )} " )
109+ raise ClientError (f"{ ClientError .CLIENT_AUTH_ERROR } : { str (e )} " )
110+
111+ async def close (self ) -> None :
112+ """Close Azure connections and clean up resources."""
113+ try :
114+ logger .info ("Closing Azure client..." )
115+
116+ # Close all service clients
117+ for service_name , service_client in self ._services .items ():
118+ try :
119+ if hasattr (service_client , 'close' ):
120+ await service_client .close ()
121+ elif hasattr (service_client , 'disconnect' ):
122+ await service_client .disconnect ()
123+ except Exception as e :
124+ logger .warning (f"Error closing { service_name } client: { str (e )} " )
125+
126+ # Clear service cache
127+ self ._services .clear ()
128+
129+ # Shutdown executor
130+ self ._executor .shutdown (wait = True )
131+
132+ # Reset connection health
133+ self ._connection_health = False
134+
135+ logger .info ("Azure client closed successfully" )
136+
137+ except Exception as e :
138+ logger .error (f"Error closing Azure client: { str (e )} " )
139+
140+ async def get_storage_client (self ) -> AzureStorageClient :
141+ """
142+ Get Azure Storage service client.
143+
144+ Returns:
145+ AzureStorageClient: Configured Azure Storage client.
146+
147+ Raises:
148+ ClientError: If client is not loaded or storage client creation fails.
149+ """
150+ if not self ._connection_health :
151+ raise ClientError (f"{ ClientError .CLIENT_AUTH_ERROR } : Client not loaded" )
152+
153+ if "storage" not in self ._services :
154+ try :
155+ self ._services ["storage" ] = AzureStorageClient (
156+ credential = self .credential ,
157+ ** self ._kwargs
158+ )
159+ await self ._services ["storage" ].load (self .resolved_credentials )
160+ except Exception as e :
161+ logger .error (f"Failed to create storage client: { str (e )} " )
162+ raise ClientError (f"{ ClientError .CLIENT_AUTH_ERROR } : { str (e )} " )
163+
164+ return self ._services ["storage" ]
165+
166+ async def get_service_client (self , service_type : str ) -> Any :
167+ """
168+ Get a service client by type.
169+
170+ Args:
171+ service_type (str): Type of service client to retrieve.
172+ Supported values: 'storage'.
173+
174+ Returns:
175+ Any: The requested service client.
176+
177+ Raises:
178+ ValueError: If service_type is not supported.
179+ ClientError: If client creation fails.
180+ """
181+ service_mapping = {
182+ "storage" : self .get_storage_client ,
183+ }
184+
185+ if service_type not in service_mapping :
186+ raise ValueError (f"Unsupported service type: { service_type } " )
187+
188+ return await service_mapping [service_type ]()
189+
190+ async def health_check (self ) -> Dict [str , Any ]:
191+ """
192+ Perform health check on Azure connection and services.
193+
194+ Returns:
195+ Dict[str, Any]: Health status information.
196+ """
197+ health_status = {
198+ "connection_health" : self ._connection_health ,
199+ "services" : {},
200+ "overall_health" : False
201+ }
202+
203+ if not self ._connection_health :
204+ return health_status
205+
206+ # Check each service
207+ for service_name , service_client in self ._services .items ():
208+ try :
209+ if hasattr (service_client , 'health_check' ):
210+ service_health = await service_client .health_check ()
211+ else :
212+ service_health = {"status" : "unknown" }
213+
214+ health_status ["services" ][service_name ] = service_health
215+ except Exception as e :
216+ health_status ["services" ][service_name ] = {
217+ "status" : "error" ,
218+ "error" : str (e )
219+ }
220+
221+ # Overall health is True if connection is healthy and at least one service is available
222+ health_status ["overall_health" ] = (
223+ self ._connection_health and
224+ len (health_status ["services" ]) > 0
225+ )
226+
227+ return health_status
228+
229+ async def _test_connection (self ) -> None :
230+ """
231+ Test the Azure connection by attempting to get a token.
232+
233+ Raises:
234+ ClientAuthenticationError: If connection test fails.
235+ """
236+ try :
237+ # Test the credential by getting a token
238+ await asyncio .get_event_loop ().run_in_executor (
239+ self ._executor ,
240+ self .credential .get_token ,
241+ "https://management.azure.com/.default"
242+ )
243+ except Exception as e :
244+ raise ClientAuthenticationError (f"Connection test failed: { str (e )} " )
245+
246+ def __enter__ (self ):
247+ """Context manager entry."""
248+ return self
249+
250+ def __exit__ (self , exc_type , exc_val , exc_tb ):
251+ """Context manager exit."""
252+ asyncio .create_task (self .close ())
0 commit comments