atlan-application-sdk 0.1.1rc34__py3-none-any.whl → 0.1.1rc35__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.
- application_sdk/activities/__init__.py +3 -2
- application_sdk/activities/common/utils.py +21 -1
- application_sdk/activities/metadata_extraction/base.py +4 -2
- application_sdk/activities/metadata_extraction/sql.py +13 -12
- application_sdk/activities/query_extraction/sql.py +24 -20
- application_sdk/clients/atlan_auth.py +2 -2
- application_sdk/clients/temporal.py +6 -10
- application_sdk/inputs/json.py +6 -4
- application_sdk/inputs/parquet.py +16 -13
- application_sdk/outputs/__init__.py +6 -3
- application_sdk/outputs/json.py +9 -6
- application_sdk/outputs/parquet.py +10 -36
- application_sdk/server/fastapi/__init__.py +4 -5
- application_sdk/services/__init__.py +18 -0
- application_sdk/{outputs → services}/atlan_storage.py +64 -16
- application_sdk/{outputs → services}/eventstore.py +68 -6
- application_sdk/services/objectstore.py +407 -0
- application_sdk/services/secretstore.py +344 -0
- application_sdk/services/statestore.py +267 -0
- application_sdk/version.py +1 -1
- application_sdk/worker.py +1 -1
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/METADATA +1 -1
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/RECORD +26 -29
- application_sdk/common/credential_utils.py +0 -85
- application_sdk/inputs/objectstore.py +0 -238
- application_sdk/inputs/secretstore.py +0 -130
- application_sdk/inputs/statestore.py +0 -101
- application_sdk/outputs/objectstore.py +0 -125
- application_sdk/outputs/secretstore.py +0 -38
- application_sdk/outputs/statestore.py +0 -113
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Unified secret store service for the application."""
|
|
2
|
+
|
|
3
|
+
import collections.abc
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
from dapr.clients import DaprClient
|
|
10
|
+
|
|
11
|
+
from application_sdk.common.dapr_utils import is_component_registered
|
|
12
|
+
from application_sdk.common.error_codes import CommonError
|
|
13
|
+
from application_sdk.constants import (
|
|
14
|
+
DEPLOYMENT_NAME,
|
|
15
|
+
DEPLOYMENT_SECRET_PATH,
|
|
16
|
+
DEPLOYMENT_SECRET_STORE_NAME,
|
|
17
|
+
LOCAL_ENVIRONMENT,
|
|
18
|
+
SECRET_STORE_NAME,
|
|
19
|
+
)
|
|
20
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
21
|
+
from application_sdk.services.statestore import StateStore, StateType
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SecretStore:
|
|
27
|
+
"""Unified secret store service for handling secret management."""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
async def get_credentials(cls, credential_guid: str) -> Dict[str, Any]:
|
|
31
|
+
"""Resolve credentials based on credential source with automatic secret substitution.
|
|
32
|
+
|
|
33
|
+
This method retrieves credential configuration from the state store and resolves
|
|
34
|
+
any secret references by fetching actual values from the secret store.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
credential_guid (str): The unique GUID of the credential configuration to resolve.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dict[str, Any]: Complete credential data with secrets resolved.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
CommonError: If credential resolution fails due to missing configuration,
|
|
44
|
+
secret store errors, or invalid credential format.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
>>> # Resolve database credentials
|
|
48
|
+
>>> creds = await SecretStore.get_credentials("db-cred-abc123")
|
|
49
|
+
>>> print(f"Connecting to {creds['host']}:{creds['port']}")
|
|
50
|
+
>>> # Password is automatically resolved from secret store
|
|
51
|
+
|
|
52
|
+
>>> # Handle resolution errors
|
|
53
|
+
>>> try:
|
|
54
|
+
... creds = await SecretStore.get_credentials("invalid-guid")
|
|
55
|
+
>>> except CommonError as e:
|
|
56
|
+
... logger.error(f"Failed to resolve credentials: {e}")
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
async def _get_credentials_async(credential_guid: str) -> Dict[str, Any]:
|
|
60
|
+
"""Async helper function to perform async I/O operations."""
|
|
61
|
+
credential_config = await StateStore.get_state(
|
|
62
|
+
credential_guid, StateType.CREDENTIALS
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Fetch secret data from secret store
|
|
66
|
+
secret_key = credential_config.get("secret-path", credential_guid)
|
|
67
|
+
secret_data = SecretStore.get_secret(secret_key=secret_key)
|
|
68
|
+
|
|
69
|
+
# Resolve credentials
|
|
70
|
+
credential_source = credential_config.get("credentialSource", "direct")
|
|
71
|
+
if credential_source == "direct":
|
|
72
|
+
credential_config.update(secret_data)
|
|
73
|
+
return credential_config
|
|
74
|
+
else:
|
|
75
|
+
return cls.resolve_credentials(credential_config, secret_data)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Run async operations directly
|
|
79
|
+
return await _get_credentials_async(credential_guid)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Error resolving credentials: {str(e)}")
|
|
82
|
+
raise CommonError(
|
|
83
|
+
CommonError.CREDENTIALS_RESOLUTION_ERROR,
|
|
84
|
+
f"Failed to resolve credentials: {str(e)}",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def resolve_credentials(
|
|
89
|
+
cls, credential_config: Dict[str, Any], secret_data: Dict[str, Any]
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""Resolve credentials by substituting secret references with actual values.
|
|
92
|
+
|
|
93
|
+
This method processes credential configuration and replaces any reference
|
|
94
|
+
values with corresponding secrets from the secret data.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
credential_config (Dict[str, Any]): Base credential configuration with potential references.
|
|
98
|
+
secret_data (Dict[str, Any]): Secret data containing actual secret values.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict[str, Any]: Credential configuration with all secret references resolved.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> # Basic secret resolution
|
|
105
|
+
>>> config = {"host": "db.example.com", "password": "db_password_key"}
|
|
106
|
+
>>> secrets = {"db_password_key": "actual_secret_password"}
|
|
107
|
+
>>> resolved = SecretStore.resolve_credentials(config, secrets)
|
|
108
|
+
>>> print(resolved) # {"host": "db.example.com", "password": "actual_secret_password"}
|
|
109
|
+
|
|
110
|
+
>>> # Resolution with nested 'extra' fields
|
|
111
|
+
>>> config = {
|
|
112
|
+
... "host": "db.example.com",
|
|
113
|
+
... "extra": {"ssl_cert": "cert_key"}
|
|
114
|
+
... }
|
|
115
|
+
>>> secrets = {"cert_key": "-----BEGIN CERTIFICATE-----..."}
|
|
116
|
+
>>> resolved = SecretStore.resolve_credentials(config, secrets)
|
|
117
|
+
"""
|
|
118
|
+
credentials = copy.deepcopy(credential_config)
|
|
119
|
+
|
|
120
|
+
# Replace values with secret values
|
|
121
|
+
for key, value in list(credentials.items()):
|
|
122
|
+
if isinstance(value, str) and value in secret_data:
|
|
123
|
+
credentials[key] = secret_data[value]
|
|
124
|
+
|
|
125
|
+
# Apply the same substitution to the 'extra' dictionary if it exists
|
|
126
|
+
if "extra" in credentials and isinstance(credentials["extra"], dict):
|
|
127
|
+
for key, value in list(credentials["extra"].items()):
|
|
128
|
+
if isinstance(value, str):
|
|
129
|
+
if value in secret_data:
|
|
130
|
+
credentials["extra"][key] = secret_data[value]
|
|
131
|
+
elif value in secret_data.get("extra", {}):
|
|
132
|
+
credentials["extra"][key] = secret_data["extra"][value]
|
|
133
|
+
|
|
134
|
+
return credentials
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def get_deployment_secret(cls) -> Dict[str, Any]:
|
|
138
|
+
"""Get deployment configuration from the deployment secret store.
|
|
139
|
+
|
|
140
|
+
Validates that the deployment secret store component is registered
|
|
141
|
+
before attempting to fetch secrets to prevent errors. This method
|
|
142
|
+
is commonly used to retrieve environment-specific configuration.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dict[str, Any]: Deployment configuration data, or empty dict if
|
|
146
|
+
component is unavailable or fetch fails.
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
>>> # Get deployment configuration
|
|
150
|
+
>>> config = SecretStore.get_deployment_secret()
|
|
151
|
+
>>> if config:
|
|
152
|
+
... print(f"Environment: {config.get('environment')}")
|
|
153
|
+
... print(f"Region: {config.get('region')}")
|
|
154
|
+
>>> else:
|
|
155
|
+
... print("No deployment configuration available")
|
|
156
|
+
|
|
157
|
+
>>> # Use in application initialization
|
|
158
|
+
>>> deployment_config = SecretStore.get_deployment_secret()
|
|
159
|
+
>>> if deployment_config.get('debug_mode'):
|
|
160
|
+
... logging.getLogger().setLevel(logging.DEBUG)
|
|
161
|
+
"""
|
|
162
|
+
if not is_component_registered(DEPLOYMENT_SECRET_STORE_NAME):
|
|
163
|
+
logger.warning(
|
|
164
|
+
f"Deployment secret store component '{DEPLOYMENT_SECRET_STORE_NAME}' is not registered"
|
|
165
|
+
)
|
|
166
|
+
return {}
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
return cls.get_secret(DEPLOYMENT_SECRET_PATH, DEPLOYMENT_SECRET_STORE_NAME)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Failed to fetch deployment config: {e}")
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def get_secret(
|
|
176
|
+
cls, secret_key: str, component_name: str = SECRET_STORE_NAME
|
|
177
|
+
) -> Dict[str, Any]:
|
|
178
|
+
"""Get secret from the Dapr secret store component.
|
|
179
|
+
|
|
180
|
+
Retrieves secret data from the specified Dapr component and processes
|
|
181
|
+
it into a standardized dictionary format.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
secret_key (str): Key of the secret to fetch from the secret store.
|
|
185
|
+
component_name (str, optional): Name of the Dapr component to fetch from.
|
|
186
|
+
Defaults to SECRET_STORE_NAME.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Dict[str, Any]: Processed secret data as a dictionary.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
Exception: If the secret cannot be retrieved from the component.
|
|
193
|
+
|
|
194
|
+
Note:
|
|
195
|
+
In local development environment, returns empty dict to avoid
|
|
196
|
+
secret store dependency.
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
>>> # Get database credentials
|
|
200
|
+
>>> db_secret = SecretStore.get_secret("database-credentials")
|
|
201
|
+
>>> print(f"Host: {db_secret.get('host')}")
|
|
202
|
+
|
|
203
|
+
>>> # Get from specific component
|
|
204
|
+
>>> api_secret = SecretStore.get_secret(
|
|
205
|
+
... "api-keys",
|
|
206
|
+
... component_name="external-secrets"
|
|
207
|
+
... )
|
|
208
|
+
"""
|
|
209
|
+
if DEPLOYMENT_NAME == LOCAL_ENVIRONMENT:
|
|
210
|
+
return {}
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
with DaprClient() as client:
|
|
214
|
+
dapr_secret_object = client.get_secret(
|
|
215
|
+
store_name=component_name, key=secret_key
|
|
216
|
+
)
|
|
217
|
+
return cls._process_secret_data(dapr_secret_object.secret)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(
|
|
220
|
+
f"Failed to fetch secret using component {component_name}: {str(e)}"
|
|
221
|
+
)
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def _process_secret_data(cls, secret_data: Any) -> Dict[str, Any]:
|
|
226
|
+
"""Process raw secret data into a standardized dictionary format.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
secret_data: Raw secret data from various sources.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dict[str, Any]: Processed secret data as a dictionary.
|
|
233
|
+
"""
|
|
234
|
+
# Convert ScalarMapContainer to dict if needed
|
|
235
|
+
if isinstance(secret_data, collections.abc.Mapping):
|
|
236
|
+
secret_data = dict(secret_data)
|
|
237
|
+
|
|
238
|
+
# If the dict has a single key and its value is a JSON string, parse it
|
|
239
|
+
if len(secret_data) == 1 and isinstance(next(iter(secret_data.values())), str):
|
|
240
|
+
try:
|
|
241
|
+
parsed = json.loads(next(iter(secret_data.values())))
|
|
242
|
+
if isinstance(parsed, dict):
|
|
243
|
+
secret_data = parsed
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"Failed to parse secret data: {e}")
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
return secret_data
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def apply_secret_values(
|
|
252
|
+
cls, source_data: Dict[str, Any], secret_data: Dict[str, Any]
|
|
253
|
+
) -> Dict[str, Any]:
|
|
254
|
+
"""Apply secret values to source data by substituting references.
|
|
255
|
+
|
|
256
|
+
This function replaces values in the source data with actual secret values
|
|
257
|
+
when the source value exists as a key in the secret data. It supports
|
|
258
|
+
nested structures and preserves the original data structure.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
source_data (Dict[str, Any]): Original data with potential references to secrets.
|
|
262
|
+
secret_data (Dict[str, Any]): Secret data containing actual secret values.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Dict[str, Any]: Deep copy of source data with secret references resolved.
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
>>> # Simple secret substitution
|
|
269
|
+
>>> source = {
|
|
270
|
+
... "database_url": "postgresql://user:${db_password}@localhost/app",
|
|
271
|
+
... "api_key": "api_key_ref"
|
|
272
|
+
... }
|
|
273
|
+
>>> secrets = {
|
|
274
|
+
... "api_key_ref": "sk-1234567890abcdef",
|
|
275
|
+
... "db_password": "secure_db_password"
|
|
276
|
+
... }
|
|
277
|
+
>>> resolved = SecretStore.apply_secret_values(source, secrets)
|
|
278
|
+
|
|
279
|
+
>>> # With nested extra fields
|
|
280
|
+
>>> source = {
|
|
281
|
+
... "host": "api.example.com",
|
|
282
|
+
... "extra": {"token": "auth_token_ref"}
|
|
283
|
+
... }
|
|
284
|
+
>>> secrets = {"auth_token_ref": "bearer_token_123"}
|
|
285
|
+
>>> resolved = SecretStore.apply_secret_values(source, secrets)
|
|
286
|
+
"""
|
|
287
|
+
result_data = copy.deepcopy(source_data)
|
|
288
|
+
|
|
289
|
+
# Replace values with secret values
|
|
290
|
+
for key, value in list(result_data.items()):
|
|
291
|
+
if isinstance(value, str) and value in secret_data:
|
|
292
|
+
result_data[key] = secret_data[value]
|
|
293
|
+
|
|
294
|
+
# Apply the same substitution to the 'extra' dictionary if it exists
|
|
295
|
+
if "extra" in result_data and isinstance(result_data["extra"], dict):
|
|
296
|
+
for key, value in list(result_data["extra"].items()):
|
|
297
|
+
if isinstance(value, str) and value in secret_data:
|
|
298
|
+
result_data["extra"][key] = secret_data[value]
|
|
299
|
+
|
|
300
|
+
return result_data
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
async def save_secret(cls, config: Dict[str, Any]) -> str:
|
|
304
|
+
"""Store credentials in the state store (development environment only).
|
|
305
|
+
|
|
306
|
+
This method is designed for development and testing purposes only.
|
|
307
|
+
In production environments, secrets should be managed through proper
|
|
308
|
+
secret management systems.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
config (Dict[str, Any]): The credential configuration to store.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
str: The generated credential GUID that can be used to retrieve the credentials.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ValueError: If called in production environment (non-local deployment).
|
|
318
|
+
Exception: If there's an error storing the credentials.
|
|
319
|
+
|
|
320
|
+
Examples:
|
|
321
|
+
>>> # Development environment only
|
|
322
|
+
>>> config = {
|
|
323
|
+
... "host": "localhost",
|
|
324
|
+
... "port": 5432,
|
|
325
|
+
... "username": "dev_user",
|
|
326
|
+
... "password": "dev_password",
|
|
327
|
+
... "database": "app_dev"
|
|
328
|
+
... }
|
|
329
|
+
>>> guid = await SecretStore.save_secret(config)
|
|
330
|
+
>>> print(f"Stored credentials with GUID: {guid}")
|
|
331
|
+
|
|
332
|
+
>>> # Later retrieve these credentials
|
|
333
|
+
>>> retrieved = await SecretStore.get_credentials(guid)
|
|
334
|
+
"""
|
|
335
|
+
if DEPLOYMENT_NAME == LOCAL_ENVIRONMENT:
|
|
336
|
+
# NOTE: (development) temporary solution to store the credentials in the state store.
|
|
337
|
+
# In production, dapr doesn't support creating secrets.
|
|
338
|
+
credential_guid = str(uuid.uuid4())
|
|
339
|
+
await StateStore.save_state_object(
|
|
340
|
+
id=credential_guid, value=config, type=StateType.CREDENTIALS
|
|
341
|
+
)
|
|
342
|
+
return credential_guid
|
|
343
|
+
else:
|
|
344
|
+
raise ValueError("Storing credentials is not supported in production.")
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Unified state store service for the application."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from temporalio import activity
|
|
9
|
+
|
|
10
|
+
from application_sdk.activities.common.utils import get_object_store_prefix
|
|
11
|
+
from application_sdk.constants import (
|
|
12
|
+
APPLICATION_NAME,
|
|
13
|
+
STATE_STORE_PATH_TEMPLATE,
|
|
14
|
+
TEMPORARY_PATH,
|
|
15
|
+
UPSTREAM_OBJECT_STORE_NAME,
|
|
16
|
+
)
|
|
17
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
18
|
+
from application_sdk.services.objectstore import ObjectStore
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
activity.logger = logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StateType(Enum):
|
|
25
|
+
WORKFLOWS = "workflows"
|
|
26
|
+
CREDENTIALS = "credentials"
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def is_member(cls, type: str) -> bool:
|
|
30
|
+
"""Check if a string value is a valid StateType member.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
type (str): The string value to check.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
bool: True if the value is a valid StateType, False otherwise.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> StateType.is_member("workflows")
|
|
40
|
+
True
|
|
41
|
+
>>> StateType.is_member("invalid")
|
|
42
|
+
False
|
|
43
|
+
"""
|
|
44
|
+
return type in cls._value2member_map_
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_state_store_path(id: str, state_type: StateType) -> str:
|
|
48
|
+
"""Build the state file path for the given id and type.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
id (str): The unique identifier for the state.
|
|
52
|
+
state_type (StateType): The type of state (WORKFLOWS or CREDENTIALS).
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: The constructed state file path.
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
>>> from application_sdk.services.statestore import build_state_store_path, StateType
|
|
59
|
+
|
|
60
|
+
>>> # Workflow state path
|
|
61
|
+
>>> path = build_state_store_path("workflow-123", StateType.WORKFLOWS)
|
|
62
|
+
>>> print(path)
|
|
63
|
+
'./local/tmp/persistent-artifacts/apps/appName/workflows/workflow-123/config.json'
|
|
64
|
+
|
|
65
|
+
>>> # Credential state path
|
|
66
|
+
>>> cred_path = build_state_store_path("db-cred-456", StateType.CREDENTIALS)
|
|
67
|
+
>>> print(cred_path)
|
|
68
|
+
'./local/tmp/persistent-artifacts/apps/appName/credentials/db-cred-456/config.json'
|
|
69
|
+
"""
|
|
70
|
+
return os.path.join(
|
|
71
|
+
TEMPORARY_PATH,
|
|
72
|
+
STATE_STORE_PATH_TEMPLATE.format(
|
|
73
|
+
application_name=APPLICATION_NAME, state_type=state_type.value, id=id
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class StateStore:
|
|
79
|
+
"""Unified state store service for handling state management."""
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
async def get_state(cls, id: str, type: StateType) -> Dict[str, Any]:
|
|
83
|
+
"""Get state from the store.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
id (str): The unique identifier to retrieve the state for.
|
|
87
|
+
type (StateType): The type of state to retrieve (WORKFLOWS or CREDENTIALS).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict[str, Any]: The retrieved state data. Returns empty dict if no state found.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
IOError: If there's an error with the object store operations.
|
|
94
|
+
Exception: If there's an unexpected error during state retrieval.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
>>> from application_sdk.services.statestore import StateStore, StateType
|
|
98
|
+
|
|
99
|
+
>>> # Get workflow state
|
|
100
|
+
>>> state = await StateStore.get_state("workflow-123", StateType.WORKFLOWS)
|
|
101
|
+
>>> print(f"Current status: {state.get('status', 'unknown')}")
|
|
102
|
+
|
|
103
|
+
>>> # Get credential configuration
|
|
104
|
+
>>> creds = await StateStore.get_state("db-cred-456", StateType.CREDENTIALS)
|
|
105
|
+
>>> print(f"Database: {creds.get('database')}")
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
state_file_path = build_state_store_path(id, type)
|
|
109
|
+
state = {}
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
logger.info(f"Trying to download state object for {id} with type {type}")
|
|
113
|
+
await ObjectStore.download_file(
|
|
114
|
+
source=get_object_store_prefix(state_file_path),
|
|
115
|
+
destination=state_file_path,
|
|
116
|
+
store_name=UPSTREAM_OBJECT_STORE_NAME,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
with open(state_file_path, "r") as file:
|
|
120
|
+
state = json.load(file)
|
|
121
|
+
|
|
122
|
+
logger.info(f"State object downloaded for {id} with type {type}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
# local error message is "file not found", while in object store it is "object not found"
|
|
125
|
+
if "not found" in str(e).lower():
|
|
126
|
+
logger.info(
|
|
127
|
+
f"No state found for {type.value} with id '{id}', returning empty dict"
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
logger.error(f"Failed to extract state: {str(e)}")
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
return state
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
async def save_state(cls, key: str, value: Any, id: str, type: StateType) -> None:
|
|
137
|
+
"""Save a single state value to the store.
|
|
138
|
+
|
|
139
|
+
This method updates a specific key within the state object, merging with existing state.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
key (str): The key to store the state value under.
|
|
143
|
+
value (Any): The value to store (can be any JSON-serializable type).
|
|
144
|
+
id (str): The unique identifier for the state object.
|
|
145
|
+
type (StateType): The type of state (WORKFLOWS or CREDENTIALS).
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
Exception: If there's an error with the object store operations.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
>>> from application_sdk.services.statestore import StateStore, StateType
|
|
152
|
+
|
|
153
|
+
>>> # Update workflow progress
|
|
154
|
+
>>> await StateStore.save_state(
|
|
155
|
+
... key="progress",
|
|
156
|
+
... value=75,
|
|
157
|
+
... id="workflow-123",
|
|
158
|
+
... type=StateType.WORKFLOWS
|
|
159
|
+
... )
|
|
160
|
+
|
|
161
|
+
>>> # Update workflow status with dict
|
|
162
|
+
>>> await StateStore.save_state(
|
|
163
|
+
... key="execution_info",
|
|
164
|
+
... value={"started_at": "2024-01-15T10:00:00Z", "worker_id": "worker-1"},
|
|
165
|
+
... id="workflow-123",
|
|
166
|
+
... type=StateType.WORKFLOWS
|
|
167
|
+
... )
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
# get the current state from object store
|
|
171
|
+
current_state = await cls.get_state(id, type)
|
|
172
|
+
state_file_path = build_state_store_path(id, type)
|
|
173
|
+
|
|
174
|
+
# update the state with the new value
|
|
175
|
+
current_state[key] = value
|
|
176
|
+
|
|
177
|
+
os.makedirs(os.path.dirname(state_file_path), exist_ok=True)
|
|
178
|
+
|
|
179
|
+
# save the state to a local file
|
|
180
|
+
with open(state_file_path, "w") as file:
|
|
181
|
+
json.dump(current_state, file)
|
|
182
|
+
|
|
183
|
+
# save the state to the object store
|
|
184
|
+
await ObjectStore.upload_file(
|
|
185
|
+
source=state_file_path,
|
|
186
|
+
destination=get_object_store_prefix(state_file_path),
|
|
187
|
+
store_name=UPSTREAM_OBJECT_STORE_NAME,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Failed to store state: {str(e)}")
|
|
192
|
+
raise e
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
async def save_state_object(
|
|
196
|
+
cls, id: str, value: Dict[str, Any], type: StateType
|
|
197
|
+
) -> Dict[str, Any]:
|
|
198
|
+
"""Save the entire state object to the object store.
|
|
199
|
+
|
|
200
|
+
This method merges the provided value with existing state and saves the complete object.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
id (str): The unique identifier for the state object.
|
|
204
|
+
value (Dict[str, Any]): The state data to save/merge.
|
|
205
|
+
type (StateType): The type of state (WORKFLOWS or CREDENTIALS).
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dict[str, Any]: The complete updated state after merge.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
Exception: If there's an error with the object store operations.
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
>>> from application_sdk.services.statestore import StateStore, StateType
|
|
215
|
+
|
|
216
|
+
>>> # Save complete workflow state
|
|
217
|
+
>>> workflow_state = {
|
|
218
|
+
... "status": "running",
|
|
219
|
+
... "current_step": "data_processing",
|
|
220
|
+
... "progress": 50,
|
|
221
|
+
... "config": {"batch_size": 1000}
|
|
222
|
+
... }
|
|
223
|
+
>>> updated = await StateStore.save_state_object(
|
|
224
|
+
... id="workflow-123",
|
|
225
|
+
... value=workflow_state,
|
|
226
|
+
... type=StateType.WORKFLOWS
|
|
227
|
+
... )
|
|
228
|
+
>>> print(f"Final state has {len(updated)} keys")
|
|
229
|
+
|
|
230
|
+
>>> # Save credential configuration
|
|
231
|
+
>>> cred_config = {
|
|
232
|
+
... "credential_type": "database",
|
|
233
|
+
... "host": "db.example.com",
|
|
234
|
+
... "port": 5432
|
|
235
|
+
... }
|
|
236
|
+
>>> await StateStore.save_state_object(
|
|
237
|
+
... id="db-cred-456",
|
|
238
|
+
... value=cred_config,
|
|
239
|
+
... type=StateType.CREDENTIALS
|
|
240
|
+
... )
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
logger.info(f"Saving state object for {id} with type {type}")
|
|
244
|
+
# get the current state from object store
|
|
245
|
+
current_state = await cls.get_state(id, type)
|
|
246
|
+
state_file_path = build_state_store_path(id, type)
|
|
247
|
+
|
|
248
|
+
# update the state with the new value
|
|
249
|
+
current_state.update(value)
|
|
250
|
+
|
|
251
|
+
os.makedirs(os.path.dirname(state_file_path), exist_ok=True)
|
|
252
|
+
|
|
253
|
+
# save the state to a local file
|
|
254
|
+
with open(state_file_path, "w") as file:
|
|
255
|
+
json.dump(current_state, file)
|
|
256
|
+
|
|
257
|
+
# save the state to the object store
|
|
258
|
+
await ObjectStore.upload_file(
|
|
259
|
+
source=state_file_path,
|
|
260
|
+
destination=get_object_store_prefix(state_file_path),
|
|
261
|
+
store_name=UPSTREAM_OBJECT_STORE_NAME,
|
|
262
|
+
)
|
|
263
|
+
logger.info(f"State object saved for {id} with type {type}")
|
|
264
|
+
return current_state
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Failed to store state: {str(e)}")
|
|
267
|
+
raise e
|
application_sdk/version.py
CHANGED
application_sdk/worker.py
CHANGED
|
@@ -22,7 +22,7 @@ from application_sdk.events.models import (
|
|
|
22
22
|
WorkerStartEventData,
|
|
23
23
|
)
|
|
24
24
|
from application_sdk.observability.logger_adaptor import get_logger
|
|
25
|
-
from application_sdk.
|
|
25
|
+
from application_sdk.services.eventstore import EventStore
|
|
26
26
|
|
|
27
27
|
logger = get_logger(__name__)
|
|
28
28
|
|
{atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlan-application-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1rc35
|
|
4
4
|
Summary: Atlan Application SDK is a Python library for developing applications on the Atlan Platform
|
|
5
5
|
Project-URL: Repository, https://github.com/atlanhq/application-sdk
|
|
6
6
|
Project-URL: Documentation, https://github.com/atlanhq/application-sdk/README.md
|