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.
Files changed (33) hide show
  1. application_sdk/activities/__init__.py +3 -2
  2. application_sdk/activities/common/utils.py +21 -1
  3. application_sdk/activities/metadata_extraction/base.py +4 -2
  4. application_sdk/activities/metadata_extraction/sql.py +13 -12
  5. application_sdk/activities/query_extraction/sql.py +24 -20
  6. application_sdk/clients/atlan_auth.py +2 -2
  7. application_sdk/clients/temporal.py +6 -10
  8. application_sdk/inputs/json.py +6 -4
  9. application_sdk/inputs/parquet.py +16 -13
  10. application_sdk/outputs/__init__.py +6 -3
  11. application_sdk/outputs/json.py +9 -6
  12. application_sdk/outputs/parquet.py +10 -36
  13. application_sdk/server/fastapi/__init__.py +4 -5
  14. application_sdk/services/__init__.py +18 -0
  15. application_sdk/{outputs → services}/atlan_storage.py +64 -16
  16. application_sdk/{outputs → services}/eventstore.py +68 -6
  17. application_sdk/services/objectstore.py +407 -0
  18. application_sdk/services/secretstore.py +344 -0
  19. application_sdk/services/statestore.py +267 -0
  20. application_sdk/version.py +1 -1
  21. application_sdk/worker.py +1 -1
  22. {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/METADATA +1 -1
  23. {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/RECORD +26 -29
  24. application_sdk/common/credential_utils.py +0 -85
  25. application_sdk/inputs/objectstore.py +0 -238
  26. application_sdk/inputs/secretstore.py +0 -130
  27. application_sdk/inputs/statestore.py +0 -101
  28. application_sdk/outputs/objectstore.py +0 -125
  29. application_sdk/outputs/secretstore.py +0 -38
  30. application_sdk/outputs/statestore.py +0 -113
  31. {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/WHEEL +0 -0
  32. {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc35.dist-info}/licenses/LICENSE +0 -0
  33. {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
@@ -2,4 +2,4 @@
2
2
  Version information for the application_sdk package.
3
3
  """
4
4
 
5
- __version__ = "0.1.1rc34"
5
+ __version__ = "0.1.1rc35"
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.outputs.eventstore import EventStore
25
+ from application_sdk.services.eventstore import EventStore
26
26
 
27
27
  logger = get_logger(__name__)
28
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atlan-application-sdk
3
- Version: 0.1.1rc34
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