atlan-application-sdk 0.1.1rc35__py3-none-any.whl → 0.1.1rc36__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/lock_management.py +110 -0
- application_sdk/clients/redis.py +443 -0
- application_sdk/clients/temporal.py +31 -187
- application_sdk/common/error_codes.py +24 -3
- application_sdk/constants.py +18 -1
- application_sdk/decorators/__init__.py +0 -0
- application_sdk/decorators/locks.py +42 -0
- application_sdk/handlers/base.py +18 -1
- application_sdk/interceptors/__init__.py +0 -0
- application_sdk/interceptors/events.py +193 -0
- application_sdk/interceptors/lock.py +139 -0
- application_sdk/version.py +1 -1
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/METADATA +4 -2
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/RECORD +17 -10
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Redis lock interceptor for Temporal workflows.
|
|
2
|
+
|
|
3
|
+
Manages distributed locks for activities decorated with @needs_lock using
|
|
4
|
+
separate lock acquisition and release activities to avoid workflow deadlocks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import Any, Dict, Optional, Type
|
|
9
|
+
|
|
10
|
+
from temporalio import workflow
|
|
11
|
+
from temporalio.common import RetryPolicy
|
|
12
|
+
from temporalio.worker import (
|
|
13
|
+
Interceptor,
|
|
14
|
+
StartActivityInput,
|
|
15
|
+
WorkflowInboundInterceptor,
|
|
16
|
+
WorkflowInterceptorClassInput,
|
|
17
|
+
WorkflowOutboundInterceptor,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from application_sdk.common.error_codes import WorkflowError
|
|
21
|
+
from application_sdk.constants import (
|
|
22
|
+
APPLICATION_NAME,
|
|
23
|
+
IS_LOCKING_DISABLED,
|
|
24
|
+
LOCK_METADATA_KEY,
|
|
25
|
+
)
|
|
26
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
27
|
+
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RedisLockInterceptor(Interceptor):
|
|
32
|
+
"""Main interceptor class for Redis distributed locking."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, activities: Dict[str, Any]):
|
|
35
|
+
"""Initialize Redis lock interceptor.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
activities: Dictionary mapping activity names to activity functions
|
|
39
|
+
"""
|
|
40
|
+
self.activities = activities
|
|
41
|
+
|
|
42
|
+
def workflow_interceptor_class(
|
|
43
|
+
self, input: WorkflowInterceptorClassInput
|
|
44
|
+
) -> Optional[Type[WorkflowInboundInterceptor]]:
|
|
45
|
+
activities = self.activities
|
|
46
|
+
|
|
47
|
+
class RedisLockWorkflowInboundInterceptor(WorkflowInboundInterceptor):
|
|
48
|
+
"""Inbound interceptor that manages Redis locks for activities."""
|
|
49
|
+
|
|
50
|
+
def init(self, outbound: WorkflowOutboundInterceptor) -> None:
|
|
51
|
+
"""Initialize with Redis lock outbound interceptor."""
|
|
52
|
+
lock_outbound = RedisLockOutboundInterceptor(outbound, activities)
|
|
53
|
+
super().init(lock_outbound)
|
|
54
|
+
|
|
55
|
+
return RedisLockWorkflowInboundInterceptor
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RedisLockOutboundInterceptor(WorkflowOutboundInterceptor):
|
|
59
|
+
"""Outbound interceptor that acquires Redis locks before activity execution."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, next: WorkflowOutboundInterceptor, activities: Dict[str, Any]):
|
|
62
|
+
super().__init__(next)
|
|
63
|
+
self.activities = activities
|
|
64
|
+
|
|
65
|
+
async def start_activity( # type: ignore[override]
|
|
66
|
+
self, input: StartActivityInput
|
|
67
|
+
) -> workflow.ActivityHandle[Any]:
|
|
68
|
+
"""Start activity with distributed lock if required."""
|
|
69
|
+
|
|
70
|
+
# Check if activity needs locking
|
|
71
|
+
activity_fn = self.activities.get(input.activity)
|
|
72
|
+
if (
|
|
73
|
+
not activity_fn
|
|
74
|
+
or not hasattr(activity_fn, LOCK_METADATA_KEY)
|
|
75
|
+
or IS_LOCKING_DISABLED
|
|
76
|
+
):
|
|
77
|
+
return await self.next.start_activity(input)
|
|
78
|
+
|
|
79
|
+
lock_config = getattr(activity_fn, LOCK_METADATA_KEY)
|
|
80
|
+
lock_name = lock_config.get("lock_name", input.activity)
|
|
81
|
+
max_locks = lock_config.get("max_locks", 5)
|
|
82
|
+
if not input.schedule_to_close_timeout:
|
|
83
|
+
logger.error(
|
|
84
|
+
f"Activity '{input.activity}' with @needs_lock decorator requires schedule_to_close_timeout"
|
|
85
|
+
)
|
|
86
|
+
raise WorkflowError(
|
|
87
|
+
f"{WorkflowError.WORKFLOW_CONFIG_ERROR}: Activity '{input.activity}' with @needs_lock decorator must be called with schedule_to_close_timeout parameter. "
|
|
88
|
+
f"Example: workflow.execute_activity('{input.activity}', schedule_to_close_timeout=timedelta(minutes=10))"
|
|
89
|
+
)
|
|
90
|
+
ttl_seconds = int(input.schedule_to_close_timeout.total_seconds())
|
|
91
|
+
|
|
92
|
+
# Orchestrate lock acquisition -> business activity -> lock release
|
|
93
|
+
return await self._execute_with_lock_orchestration(
|
|
94
|
+
input, lock_name, max_locks, ttl_seconds
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def _execute_with_lock_orchestration(
|
|
98
|
+
self,
|
|
99
|
+
input: StartActivityInput,
|
|
100
|
+
lock_name: str,
|
|
101
|
+
max_locks: int,
|
|
102
|
+
ttl_seconds: int,
|
|
103
|
+
) -> workflow.ActivityHandle[Any]:
|
|
104
|
+
"""Execute activity with distributed lock orchestration."""
|
|
105
|
+
owner_id = f"{APPLICATION_NAME}:{workflow.info().run_id}"
|
|
106
|
+
lock_result = None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Step 1: Acquire lock via dedicated activity (can take >2s safely)
|
|
110
|
+
start_to_close_timeout = workflow.info().execution_timeout
|
|
111
|
+
lock_result = await workflow.execute_activity(
|
|
112
|
+
"acquire_distributed_lock",
|
|
113
|
+
args=[lock_name, max_locks, ttl_seconds, owner_id],
|
|
114
|
+
start_to_close_timeout=start_to_close_timeout,
|
|
115
|
+
retry_policy=RetryPolicy(maximum_attempts=1),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
logger.debug(f"Lock acquired: {lock_result}, executing {input.activity}")
|
|
119
|
+
|
|
120
|
+
# Step 2: Execute the business activity and return its handle
|
|
121
|
+
return await self.next.start_activity(input)
|
|
122
|
+
|
|
123
|
+
finally:
|
|
124
|
+
# Step 3: Release lock (fire-and-forget with short timeout)
|
|
125
|
+
if lock_result is not None:
|
|
126
|
+
try:
|
|
127
|
+
await workflow.execute_local_activity(
|
|
128
|
+
"release_distributed_lock",
|
|
129
|
+
args=[lock_result["resource_id"], lock_result["owner_id"]],
|
|
130
|
+
start_to_close_timeout=timedelta(seconds=5),
|
|
131
|
+
retry_policy=RetryPolicy(maximum_attempts=1),
|
|
132
|
+
)
|
|
133
|
+
logger.debug(f"Lock released: {lock_result['resource_id']}")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
# Silent failure - TTL will handle cleanup
|
|
136
|
+
logger.warning(
|
|
137
|
+
f"Lock release failed for {lock_result['resource_id']}: {e}. "
|
|
138
|
+
f"TTL will handle cleanup."
|
|
139
|
+
)
|
application_sdk/version.py
CHANGED
{atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.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.1rc36
|
|
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
|
|
@@ -26,12 +26,14 @@ Requires-Dist: fastapi[standard]>=0.115.0
|
|
|
26
26
|
Requires-Dist: loguru>=0.7.3
|
|
27
27
|
Requires-Dist: opentelemetry-exporter-otlp>=1.27.0
|
|
28
28
|
Requires-Dist: psutil>=7.0.0
|
|
29
|
-
Requires-Dist: pyatlan>=8.0.
|
|
29
|
+
Requires-Dist: pyatlan>=8.0.2
|
|
30
30
|
Requires-Dist: pydantic>=2.10.6
|
|
31
31
|
Requires-Dist: python-dotenv>=1.1.0
|
|
32
32
|
Requires-Dist: uvloop>=0.21.0; sys_platform != 'win32'
|
|
33
33
|
Provides-Extra: daft
|
|
34
34
|
Requires-Dist: daft>=0.4.12; extra == 'daft'
|
|
35
|
+
Provides-Extra: distributed-lock
|
|
36
|
+
Requires-Dist: redis[hiredis]>=5.2.0; extra == 'distributed-lock'
|
|
35
37
|
Provides-Extra: iam-auth
|
|
36
38
|
Requires-Dist: boto3>=1.38.6; extra == 'iam-auth'
|
|
37
39
|
Provides-Extra: iceberg
|
{atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
application_sdk/__init__.py,sha256=2e2mvmLJ5dxmJGPELtb33xwP-j6JMdoIuqKycEn7hjg,151
|
|
2
|
-
application_sdk/constants.py,sha256=
|
|
3
|
-
application_sdk/version.py,sha256=
|
|
2
|
+
application_sdk/constants.py,sha256=GzwZO0pa9M-FgibmfIs1lh-Fwo06K9Tk6WzGqMyJgpI,10362
|
|
3
|
+
application_sdk/version.py,sha256=SlVLwzlzsYJZi1Wtxmscry6pqrSiOm-LICxqBBQXx70,88
|
|
4
4
|
application_sdk/worker.py,sha256=i5f0AeKI39IfsLO05QkwC6uMz0zDPSJqP7B2byri1VI,7489
|
|
5
5
|
application_sdk/activities/__init__.py,sha256=QaXLOBYbb0zPOY5kfDQh56qbXQFaYNXOjJ5PCvatiZ4,9530
|
|
6
|
+
application_sdk/activities/lock_management.py,sha256=L__GZ9BsArwU1ntYwAgCKsSjCqN6QBeOfT-OT4WyD4Y,3983
|
|
6
7
|
application_sdk/activities/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
8
|
application_sdk/activities/common/models.py,sha256=305WdrZB7EAtCOAU_q9hMw81XowUdCeuFs9zfzb-MHQ,1196
|
|
8
9
|
application_sdk/activities/common/utils.py,sha256=F4Fq9Gl_gvUQj_fSdwzTU7obqUnemYL1dgb_yS34vTM,6967
|
|
@@ -19,16 +20,19 @@ application_sdk/clients/async_atlan.py,sha256=RTgRbMw6zJWcv1C-7cU4ccaSW5XZsB5dcA
|
|
|
19
20
|
application_sdk/clients/atlan.py,sha256=f2-Uk5KiPIDJEhGkfYctA_f3CwoVB_mWNBMVvxeLuY4,2684
|
|
20
21
|
application_sdk/clients/atlan_auth.py,sha256=D7FuNqv81ohNXLJtdx1AFw_jU6a3g0Pw6149ia4ucFY,8930
|
|
21
22
|
application_sdk/clients/base.py,sha256=TIn3pG89eXUc1XSYf4jk66m1vajWp0WxcCQOOltdazA,14021
|
|
23
|
+
application_sdk/clients/redis.py,sha256=IfAD32vLp88BCvsDTaQtxFHxzHlEx4V7TK7h1HwDDBg,15917
|
|
22
24
|
application_sdk/clients/sql.py,sha256=tW89SHuuWdU5jv8lDUP5AUCEpR2CF_5TyUvYDCBHses,17880
|
|
23
|
-
application_sdk/clients/temporal.py,sha256=
|
|
25
|
+
application_sdk/clients/temporal.py,sha256=D5tr6i6DfkTQ--kZYU-sz7yOEsxigf-g3NDnwpOhR2I,18217
|
|
24
26
|
application_sdk/clients/utils.py,sha256=zLFOJbTr_6TOqnjfVFGY85OtIXZ4FQy_rquzjaydkbY,779
|
|
25
27
|
application_sdk/clients/workflow.py,sha256=6bSqmA3sNCk9oY68dOjBUDZ9DhNKQxPD75qqE0cfldc,6104
|
|
26
28
|
application_sdk/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
29
|
application_sdk/common/aws_utils.py,sha256=aeL3BTMzv1UWJ4KxfwY5EsfYnxtS1FKNJ4xKdHeoTjc,3438
|
|
28
30
|
application_sdk/common/dapr_utils.py,sha256=0yHqDP6qNb1OT-bX2XRYQPZ5xkGkV13nyRw6GkPlHs8,1136
|
|
29
31
|
application_sdk/common/dataframe_utils.py,sha256=PId9vT6AUoq3tesiTd4sSUvW7RUhPWdAAEBLuOprks4,1262
|
|
30
|
-
application_sdk/common/error_codes.py,sha256=
|
|
32
|
+
application_sdk/common/error_codes.py,sha256=bxgvugN_0H5b8VXfJw-44mybgX5I9lRJbRdYjtPjqDI,14561
|
|
31
33
|
application_sdk/common/utils.py,sha256=ktCZLp-AEiyd-IPOgbD83Dg9qa8Z0Sj_mJmmdSzpOak,14759
|
|
34
|
+
application_sdk/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
application_sdk/decorators/locks.py,sha256=-cdbICCMns3lkqZ4CCQabW1du8cEu9XSWlwzWTTbIPk,1411
|
|
32
36
|
application_sdk/docgen/__init__.py,sha256=Gr_3uVEnSspKd_-R1YRsDABI-iP4170Dvg5jM2oD76A,7352
|
|
33
37
|
application_sdk/docgen/exporters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
38
|
application_sdk/docgen/exporters/mkdocs.py,sha256=vxYjKLz-7z7rBUSkOGTdNVlPdx_VPfFfJ31HHNgMCeM,4024
|
|
@@ -47,13 +51,16 @@ application_sdk/docgen/parsers/manifest.py,sha256=3NP-dBTpHAUQa477usMIDaKSb_9xfL
|
|
|
47
51
|
application_sdk/events/__init__.py,sha256=OcbVWDF4ZKRTJXK9UaFVtYEwu-3DHE77S-Sn6jNafUs,204
|
|
48
52
|
application_sdk/events/models.py,sha256=7Esqp3WlbriT2EqT4kNiY_sHtRXRPLj27b8SbeC5Sb0,5121
|
|
49
53
|
application_sdk/handlers/__init__.py,sha256=U7kKwVWK0FZz1uIJ2ANN0C5tD83k_9Nyz0ns6ttr92g,1152
|
|
50
|
-
application_sdk/handlers/base.py,sha256=
|
|
54
|
+
application_sdk/handlers/base.py,sha256=ieWFbv8Gm7vfrrpS-mdMSm-mHGuQY02qiAVX2qPdj3w,2467
|
|
51
55
|
application_sdk/handlers/sql.py,sha256=oeB-sgWwPYo31xaD87TyMc0h51Sary1F-CmhExt9_Pk,16100
|
|
52
56
|
application_sdk/inputs/__init__.py,sha256=_d-cUhcDyoJTJR3PdQkC831go6VDw9AM6Bg7-qm3NHI,1900
|
|
53
57
|
application_sdk/inputs/iceberg.py,sha256=xiv1kNtVx1k0h3ZJbJeXjZwdfBGSy9j9orYP_AyCYlI,2756
|
|
54
58
|
application_sdk/inputs/json.py,sha256=Yv70Y9YuutN2trqK5-z2UNtBL0895ZbdEiBDt9cYM9s,6216
|
|
55
59
|
application_sdk/inputs/parquet.py,sha256=KPcfqXOnK4C2orBCAPIO0DZgw1sYMC69MRNrqXPbCBU,6704
|
|
56
60
|
application_sdk/inputs/sql_query.py,sha256=1EREgea6kKNaMIyX2HLJgbJ07rtAgLasd9NyvDcdZok,10636
|
|
61
|
+
application_sdk/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
|
+
application_sdk/interceptors/events.py,sha256=Kh0dEsc6q7YtlN9cxatiL_ZrmBxriv55r9lxvIKGg3A,6548
|
|
63
|
+
application_sdk/interceptors/lock.py,sha256=Xe9TSjYKtDZUB94hbV7rHG_9rgKUJPTACeB8z8xsJ0w,5577
|
|
57
64
|
application_sdk/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
65
|
application_sdk/observability/logger_adaptor.py,sha256=WTqnNg78W2SRGOQVhELVLn6KMRsurkG1kc7essL08Lk,29529
|
|
59
66
|
application_sdk/observability/metrics_adaptor.py,sha256=4TYPNn38zLeqxwf7cUbe8wh_zwQlr-nyiXjJsiEhTEM,16445
|
|
@@ -135,8 +142,8 @@ application_sdk/workflows/metadata_extraction/__init__.py,sha256=jHUe_ZBQ66jx8bg
|
|
|
135
142
|
application_sdk/workflows/metadata_extraction/sql.py,sha256=_NhszxIgmcQI6lVpjJoyJRFLwPYvJw1Dyqox_m9K2RA,11947
|
|
136
143
|
application_sdk/workflows/query_extraction/__init__.py,sha256=n066_CX5RpJz6DIxGMkKS3eGSRg03ilaCtsqfJWQb7Q,117
|
|
137
144
|
application_sdk/workflows/query_extraction/sql.py,sha256=kT_JQkLCRZ44ZpaC4QvPL6DxnRIIVh8gYHLqRbMI-hA,4826
|
|
138
|
-
atlan_application_sdk-0.1.
|
|
139
|
-
atlan_application_sdk-0.1.
|
|
140
|
-
atlan_application_sdk-0.1.
|
|
141
|
-
atlan_application_sdk-0.1.
|
|
142
|
-
atlan_application_sdk-0.1.
|
|
145
|
+
atlan_application_sdk-0.1.1rc36.dist-info/METADATA,sha256=KbWZqqRTyfyif21aRpmdiE2LWuZ0LBr9q33-DMXFZB0,5567
|
|
146
|
+
atlan_application_sdk-0.1.1rc36.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
147
|
+
atlan_application_sdk-0.1.1rc36.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
148
|
+
atlan_application_sdk-0.1.1rc36.dist-info/licenses/NOTICE,sha256=A-XVVGt3KOYuuMmvSMIFkg534F1vHiCggEBp4Ez3wGk,1041
|
|
149
|
+
atlan_application_sdk-0.1.1rc36.dist-info/RECORD,,
|
{atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|