atlan-application-sdk 2.1.1__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,20 @@
1
1
  from concurrent.futures import ThreadPoolExecutor
2
2
  from typing import Any, Dict, List, Optional, Tuple, Type
3
3
 
4
+ from typing_extensions import deprecated
5
+
4
6
  from application_sdk.activities import ActivitiesInterface
5
7
  from application_sdk.clients.base import BaseClient
6
8
  from application_sdk.clients.utils import get_workflow_client
7
- from application_sdk.constants import ENABLE_MCP
9
+ from application_sdk.constants import APPLICATION_MODE, ENABLE_MCP, ApplicationMode
8
10
  from application_sdk.handlers.base import BaseHandler
9
11
  from application_sdk.interceptors.models import EventRegistration
12
+ from application_sdk.observability.decorators.observability_decorator import (
13
+ observability,
14
+ )
10
15
  from application_sdk.observability.logger_adaptor import get_logger
16
+ from application_sdk.observability.metrics_adaptor import get_metrics
17
+ from application_sdk.observability.traces_adaptor import get_traces
11
18
  from application_sdk.server import ServerInterface
12
19
  from application_sdk.server.fastapi import APIServer, HttpWorkflowTrigger
13
20
  from application_sdk.server.fastapi.models import EventWorkflowTrigger
@@ -15,6 +22,8 @@ from application_sdk.worker import Worker
15
22
  from application_sdk.workflows import WorkflowInterface
16
23
 
17
24
  logger = get_logger(__name__)
25
+ metrics = get_metrics()
26
+ traces = get_traces()
18
27
 
19
28
 
20
29
  class BaseApplication:
@@ -110,6 +119,40 @@ class BaseApplication:
110
119
 
111
120
  self.event_subscriptions[event_id].workflow_class = workflow_class
112
121
 
122
+ async def start(
123
+ self,
124
+ workflow_class: Type[WorkflowInterface],
125
+ ui_enabled: bool = True,
126
+ has_configmap: bool = False,
127
+ ):
128
+ """Start the application based on the configured APPLICATION_MODE.
129
+
130
+ Args:
131
+ workflow_class: The workflow class to register with the server.
132
+ ui_enabled: Whether to enable the UI. Defaults to True.
133
+ has_configmap: Whether the application has a configmap. Defaults to False.
134
+
135
+ Behavior based on APPLICATION_MODE:
136
+ - LOCAL: Starts worker in daemon mode and server (for local development)
137
+ - WORKER: Starts only the worker in non-daemon mode (for production worker pods)
138
+ - SERVER: Starts only the server (for production API server pods)
139
+
140
+ Raises:
141
+ ValueError: If APPLICATION_MODE is not a valid ApplicationMode value.
142
+ """
143
+ if APPLICATION_MODE in (ApplicationMode.LOCAL, ApplicationMode.WORKER):
144
+ await self._start_worker(
145
+ daemon=APPLICATION_MODE == ApplicationMode.LOCAL,
146
+ )
147
+
148
+ if APPLICATION_MODE in (ApplicationMode.LOCAL, ApplicationMode.SERVER):
149
+ await self._setup_server(
150
+ workflow_class=workflow_class,
151
+ ui_enabled=ui_enabled,
152
+ has_configmap=has_configmap,
153
+ )
154
+ await self._start_server()
155
+
113
156
  async def setup_workflow(
114
157
  self,
115
158
  workflow_and_activities_classes: List[
@@ -167,7 +210,12 @@ class BaseApplication:
167
210
  raise ValueError("Workflow client not initialized")
168
211
  return await self.workflow_client.start_workflow(workflow_args, workflow_class) # type: ignore
169
212
 
213
+ @deprecated("Use application.start(). Deprecated since v2.3.0.")
214
+ @observability(logger=logger, metrics=metrics, traces=traces)
170
215
  async def start_worker(self, daemon: bool = True):
216
+ return await self._start_worker(daemon=daemon)
217
+
218
+ async def _start_worker(self, daemon: bool = True):
171
219
  """
172
220
  Start the worker for the application.
173
221
 
@@ -178,11 +226,25 @@ class BaseApplication:
178
226
  raise ValueError("Worker not initialized")
179
227
  await self.worker.start(daemon=daemon)
180
228
 
229
+ @deprecated("Use application.start(). Deprecated since v2.3.0.")
230
+ @observability(logger=logger, metrics=metrics, traces=traces)
181
231
  async def setup_server(
182
232
  self,
183
233
  workflow_class: Type[WorkflowInterface],
184
234
  ui_enabled: bool = True,
185
235
  has_configmap: bool = False,
236
+ ):
237
+ return await self._setup_server(
238
+ workflow_class=workflow_class,
239
+ ui_enabled=ui_enabled,
240
+ has_configmap=has_configmap,
241
+ )
242
+
243
+ async def _setup_server(
244
+ self,
245
+ workflow_class: Type[WorkflowInterface],
246
+ ui_enabled: bool = True,
247
+ has_configmap: bool = False,
186
248
  ):
187
249
  """
188
250
  Set up FastAPI server and automatically mount MCP if enabled.
@@ -239,7 +301,12 @@ class BaseApplication:
239
301
  triggers=[HttpWorkflowTrigger()],
240
302
  )
241
303
 
304
+ @deprecated("Use application.start(). Deprecated since v2.3.0.")
305
+ @observability(logger=logger, metrics=metrics, traces=traces)
242
306
  async def start_server(self):
307
+ return await self._start_server()
308
+
309
+ async def _start_server(self):
243
310
  """
244
311
  Start the FastAPI server for the application.
245
312
 
@@ -1,6 +1,8 @@
1
1
  from concurrent.futures import ThreadPoolExecutor
2
2
  from typing import Any, Dict, List, Optional, Tuple, Type
3
3
 
4
+ from typing_extensions import deprecated
5
+
4
6
  from application_sdk.application import BaseApplication
5
7
  from application_sdk.clients.sql import BaseSQLClient
6
8
  from application_sdk.clients.utils import get_workflow_client
@@ -146,8 +148,33 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
146
148
  )
147
149
  return workflow_response
148
150
 
151
+ @observability(logger=logger, metrics=metrics, traces=traces)
152
+ async def start(
153
+ self,
154
+ workflow_class: Type = BaseSQLMetadataExtractionWorkflow,
155
+ ui_enabled: bool = True,
156
+ has_configmap: bool = False,
157
+ ):
158
+ """Start the SQL metadata extraction application.
159
+
160
+ Args:
161
+ workflow_class: The workflow class to register. Defaults to BaseSQLMetadataExtractionWorkflow.
162
+ ui_enabled: Whether to enable the UI. Defaults to True.
163
+ has_configmap: Whether the application has a configmap. Defaults to False.
164
+ """
165
+ await super().start(
166
+ workflow_class=workflow_class,
167
+ ui_enabled=ui_enabled,
168
+ has_configmap=has_configmap,
169
+ )
170
+
171
+ @deprecated("Use application.start(). Deprecated since v2.3.0.")
149
172
  @observability(logger=logger, metrics=metrics, traces=traces)
150
173
  async def start_worker(self, daemon: bool = True):
174
+ return await self._start_worker(daemon=daemon)
175
+
176
+ @observability(logger=logger, metrics=metrics, traces=traces)
177
+ async def _start_worker(self, daemon: bool = True):
151
178
  """
152
179
  Start the worker for the SQL metadata extraction application.
153
180
  """
@@ -155,12 +182,27 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
155
182
  raise ValueError("Worker not initialized")
156
183
  await self.worker.start(daemon=daemon)
157
184
 
185
+ @deprecated("Use application.start(). Deprecated since v2.3.0.")
158
186
  @observability(logger=logger, metrics=metrics, traces=traces)
159
187
  async def setup_server(
188
+ self,
189
+ workflow_class: Type = BaseSQLMetadataExtractionWorkflow,
190
+ ui_enabled: bool = True,
191
+ has_configmap: bool = False,
192
+ ):
193
+ return await self._setup_server(
194
+ workflow_class=workflow_class,
195
+ ui_enabled=ui_enabled,
196
+ has_configmap=has_configmap,
197
+ )
198
+
199
+ @observability(logger=logger, metrics=metrics, traces=traces)
200
+ async def _setup_server(
160
201
  self,
161
202
  workflow_class: Type[
162
203
  BaseSQLMetadataExtractionWorkflow
163
204
  ] = BaseSQLMetadataExtractionWorkflow,
205
+ ui_enabled: bool = True,
164
206
  has_configmap: bool = False,
165
207
  ) -> Any:
166
208
  """
@@ -168,6 +210,7 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
168
210
 
169
211
  Args:
170
212
  workflow_class (Type): Workflow class to register with the server. Defaults to BaseSQLMetadataExtractionWorkflow.
213
+ ui_enabled (bool): Whether to enable the UI. Defaults to True.
171
214
  has_configmap (bool): Whether the application has a configmap. Defaults to False.
172
215
 
173
216
  Returns:
@@ -180,6 +223,7 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
180
223
  self.server = APIServer(
181
224
  handler=self.handler_class(sql_client=self.client_class()),
182
225
  workflow_client=self.workflow_client,
226
+ ui_enabled=ui_enabled,
183
227
  has_configmap=has_configmap,
184
228
  )
185
229
 
@@ -0,0 +1,6 @@
1
+ """Azure client module for the application-sdk framework."""
2
+
3
+ # Azure Management API endpoint for token acquisition
4
+ AZURE_MANAGEMENT_API_ENDPOINT = "https://management.azure.com/.default"
5
+
6
+ __all__ = ["AZURE_MANAGEMENT_API_ENDPOINT"]
@@ -0,0 +1,288 @@
1
+ """
2
+ Azure authentication provider for the application-sdk framework.
3
+
4
+ This module provides the AzureAuthProvider class that handles Azure
5
+ Service Principal authentication for the application-sdk framework.
6
+
7
+ Example:
8
+ >>> from application_sdk.clients.azure.auth import AzureAuthProvider
9
+ >>> import asyncio
10
+ >>>
11
+ >>> # Create authentication provider
12
+ >>> auth_provider = AzureAuthProvider()
13
+ >>>
14
+ >>> # Authenticate with Service Principal credentials
15
+ >>> credentials = {
16
+ ... "tenant_id": "your-tenant-id",
17
+ ... "client_id": "your-client-id",
18
+ ... "client_secret": "your-client-secret"
19
+ ... }
20
+ >>>
21
+ >>> # Create credential
22
+ >>> credential = await auth_provider.create_credential(
23
+ ... auth_type="service_principal",
24
+ ... credentials=credentials
25
+ ... )
26
+ >>>
27
+ >>> # Alternative credential key formats are also supported
28
+ >>> alt_credentials = {
29
+ ... "tenantId": "your-tenant-id", # camelCase
30
+ ... "clientId": "your-client-id", # camelCase
31
+ ... "clientSecret": "your-client-secret" # camelCase
32
+ ... }
33
+ >>>
34
+ >>> credential = await auth_provider.create_credential(
35
+ ... auth_type="service_principal",
36
+ ... credentials=alt_credentials
37
+ ... )
38
+ >>>
39
+ >>> # Error handling for missing credentials
40
+ >>> try:
41
+ ... await auth_provider.create_credential(
42
+ ... auth_type="service_principal",
43
+ ... credentials={"tenant_id": "only-tenant"} # Missing client_id and client_secret
44
+ ... )
45
+ ... except CommonError as e:
46
+ ... print(f"Authentication failed: {e}")
47
+ ... # Output: Authentication failed: Missing required credential keys: client_id, client_secret
48
+ >>>
49
+ >>> # Unsupported authentication type
50
+ >>> try:
51
+ ... await auth_provider.create_credential(
52
+ ... auth_type="unsupported_type",
53
+ ... credentials=credentials
54
+ ... )
55
+ ... except CommonError as e:
56
+ ... print(f"Authentication failed: {e}")
57
+ ... # Output: Authentication failed: Only 'service_principal' authentication is supported. Received: unsupported_type
58
+ """
59
+
60
+ from typing import Any, Dict, Optional
61
+
62
+ from azure.core.credentials import TokenCredential
63
+ from azure.core.exceptions import ClientAuthenticationError
64
+ from azure.identity import ClientSecretCredential
65
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
66
+
67
+ from application_sdk.clients.azure import AZURE_MANAGEMENT_API_ENDPOINT
68
+ from application_sdk.common.error_codes import CommonError
69
+ from application_sdk.common.utils import run_sync
70
+ from application_sdk.observability.logger_adaptor import get_logger
71
+
72
+ logger = get_logger(__name__)
73
+
74
+
75
+ class ServicePrincipalCredentials(BaseModel):
76
+ """
77
+ Pydantic model for Azure Service Principal credentials.
78
+
79
+ Supports both snake_case and camelCase field names through field aliases.
80
+ All fields are required for service principal authentication.
81
+
82
+ Attributes:
83
+ tenant_id: Azure tenant ID (also accepts 'tenantId').
84
+ client_id: Azure client ID (also accepts 'clientId').
85
+ client_secret: Azure client secret (also accepts 'clientSecret').
86
+ """
87
+
88
+ tenant_id: str = Field(
89
+ ...,
90
+ alias="tenantId",
91
+ description="Azure tenant ID for service principal authentication",
92
+ )
93
+ client_id: str = Field(
94
+ ...,
95
+ alias="clientId",
96
+ description="Azure client ID for service principal authentication",
97
+ )
98
+ client_secret: str = Field(
99
+ ...,
100
+ alias="clientSecret",
101
+ description="Azure client secret for service principal authentication",
102
+ )
103
+
104
+ model_config = ConfigDict(
105
+ populate_by_name=True, # Allow both field name and alias
106
+ extra="ignore", # Ignore additional fields (Azure client may need extra fields like storage_account_name, network_config, etc.)
107
+ validate_assignment=True, # Validate on assignment
108
+ )
109
+
110
+
111
+ class AzureAuthProvider:
112
+ """
113
+ Azure authentication provider for handling Service Principal authentication.
114
+
115
+ This class provides a unified interface for creating Azure credentials
116
+ using Service Principal authentication with Azure SDK.
117
+
118
+ Supported authentication method:
119
+ - service_principal: Using client ID, client secret, and tenant ID
120
+ """
121
+
122
+ def __init__(self):
123
+ """Initialize the Azure authentication provider."""
124
+ pass
125
+
126
+ async def create_credential(
127
+ self,
128
+ auth_type: str = "service_principal",
129
+ credentials: Optional[Dict[str, Any]] = None,
130
+ ) -> TokenCredential:
131
+ """
132
+ Create Azure credential using Service Principal authentication.
133
+
134
+ Args:
135
+ auth_type (str): Type of authentication to use.
136
+ Currently only supports 'service_principal'.
137
+ credentials (Optional[Dict[str, Any]]): Service Principal credentials.
138
+ Required fields: tenant_id, client_id, client_secret.
139
+
140
+ Returns:
141
+ TokenCredential: Azure credential instance.
142
+
143
+ Raises:
144
+ CommonError: If authentication type is not supported or credentials are invalid.
145
+ ClientAuthenticationError: If credential creation fails.
146
+ """
147
+ try:
148
+ logger.debug(f"Creating Azure credential with auth type: {auth_type}")
149
+
150
+ if auth_type.lower() != "service_principal":
151
+ raise CommonError(
152
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: "
153
+ f"Only 'service_principal' authentication is supported. "
154
+ f"Received: {auth_type}"
155
+ )
156
+
157
+ if not credentials:
158
+ raise CommonError(
159
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: "
160
+ "Credentials required for service principal authentication"
161
+ )
162
+
163
+ return await self._create_service_principal_credential(credentials)
164
+
165
+ except ClientAuthenticationError as e:
166
+ logger.error(f"Azure authentication failed: {str(e)}")
167
+ raise CommonError(f"{CommonError.AZURE_CREDENTIAL_ERROR}: {str(e)}")
168
+ except ValueError as e:
169
+ logger.error(f"Invalid Azure credential parameters: {str(e)}")
170
+ raise CommonError(
171
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameters - {str(e)}"
172
+ )
173
+ except TypeError as e:
174
+ logger.error(f"Wrong Azure credential parameter types: {str(e)}")
175
+ raise CommonError(
176
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameter types - {str(e)}"
177
+ )
178
+ except Exception as e:
179
+ logger.error(f"Unexpected error creating Azure credential: {str(e)}")
180
+ raise CommonError(
181
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: Unexpected error - {str(e)}"
182
+ )
183
+
184
+ async def _create_service_principal_credential(
185
+ self, credentials: Dict[str, Any]
186
+ ) -> ClientSecretCredential:
187
+ """
188
+ Create service principal credential.
189
+
190
+ Args:
191
+ credentials (Dict[str, Any]): Service principal credentials.
192
+ Must include tenant_id, client_id, and client_secret.
193
+
194
+ Returns:
195
+ ClientSecretCredential: Service principal credential.
196
+
197
+ Raises:
198
+ CommonError: If required credentials are missing or invalid.
199
+ """
200
+ if not credentials:
201
+ raise CommonError(
202
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: "
203
+ "Credentials required for service principal authentication"
204
+ )
205
+
206
+ try:
207
+ # Validate credentials using Pydantic model
208
+ validated_credentials = ServicePrincipalCredentials(**credentials)
209
+ except ValidationError as e:
210
+ # Pydantic provides detailed error messages for all validation errors
211
+ # Format errors into a user-friendly message
212
+ error_details = "; ".join(
213
+ [
214
+ f"{'.'.join(str(loc) for loc in err['loc'])}: {err['msg']}"
215
+ for err in e.errors()
216
+ ]
217
+ )
218
+ error_message = f"Invalid credential parameters: {error_details}"
219
+ logger.error(f"Azure credential validation failed: {error_message}")
220
+ raise CommonError(f"{CommonError.CREDENTIALS_PARSE_ERROR}: {error_message}")
221
+
222
+ logger.debug(
223
+ f"Creating service principal credential for tenant: {validated_credentials.tenant_id}"
224
+ )
225
+
226
+ try:
227
+ return await run_sync(ClientSecretCredential)(
228
+ validated_credentials.tenant_id,
229
+ validated_credentials.client_id,
230
+ validated_credentials.client_secret,
231
+ )
232
+ except ValueError as e:
233
+ logger.error(f"Invalid Azure credential parameters: {str(e)}")
234
+ raise CommonError(
235
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameters - {str(e)}"
236
+ )
237
+ except TypeError as e:
238
+ logger.error(f"Wrong Azure credential parameter types: {str(e)}")
239
+ raise CommonError(
240
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameter types - {str(e)}"
241
+ )
242
+ except ClientAuthenticationError as e:
243
+ logger.error(f"Azure authentication failed: {str(e)}")
244
+ raise CommonError(f"{CommonError.AZURE_CREDENTIAL_ERROR}: {str(e)}")
245
+ except Exception as e:
246
+ logger.error(f"Unexpected error creating Azure credential: {str(e)}")
247
+ raise CommonError(
248
+ f"{CommonError.CREDENTIALS_PARSE_ERROR}: Unexpected error - {str(e)}"
249
+ )
250
+
251
+ async def validate_credential(self, credential: TokenCredential) -> bool:
252
+ """
253
+ Validate Azure credential by attempting to get a token.
254
+
255
+ Args:
256
+ credential (TokenCredential): Azure credential to validate.
257
+
258
+ Returns:
259
+ bool: True if credential is valid, False otherwise.
260
+ """
261
+ try:
262
+ logger.debug("Validating Azure credential")
263
+
264
+ # Try to get a token for Azure Management API
265
+ token = await run_sync(credential.get_token)(AZURE_MANAGEMENT_API_ENDPOINT)
266
+
267
+ if token and hasattr(token, "token"):
268
+ logger.debug("Azure credential validation successful")
269
+ return True
270
+ else:
271
+ logger.warning("Azure credential validation failed: No token received")
272
+ return False
273
+
274
+ except ClientAuthenticationError as e:
275
+ logger.error(
276
+ f"Azure credential validation failed - authentication error: {str(e)}"
277
+ )
278
+ return False
279
+ except ValueError as e:
280
+ logger.error(
281
+ f"Azure credential validation failed - invalid parameters: {str(e)}"
282
+ )
283
+ return False
284
+ except Exception as e:
285
+ logger.error(
286
+ f"Azure credential validation failed - unexpected error: {str(e)}"
287
+ )
288
+ return False