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.
@@ -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
+ )
@@ -2,4 +2,4 @@
2
2
  Version information for the application_sdk package.
3
3
  """
4
4
 
5
- __version__ = "0.1.1rc35"
5
+ __version__ = "0.1.1rc36"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atlan-application-sdk
3
- Version: 0.1.1rc35
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.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
@@ -1,8 +1,9 @@
1
1
  application_sdk/__init__.py,sha256=2e2mvmLJ5dxmJGPELtb33xwP-j6JMdoIuqKycEn7hjg,151
2
- application_sdk/constants.py,sha256=_Fmk9PgpM68chPDHHkgrs4Zg2KK4UCqqg7Oj9_u3WVo,9486
3
- application_sdk/version.py,sha256=z0Ar6Kx6DAuBs3JjgjdT2lqiLuVvhu2S7idSn3AwbsU,88
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=nzHZMq6mAOYk2E9tpgjzztW2-ztXrFXwV8TiOrSfbNw,23503
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=uROVfOOMrGPO8JroWB3vs5rIEhr0GfcPqXAK9wdhcVQ,13742
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=6GF0a4p9mbdVoBltkzXwt6i0vzOvOKUHcB6fBiqi7v0,1883
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.1rc35.dist-info/METADATA,sha256=bQqHjrAoOeWNf2kR6F4CoHgeK-fQb3wRgkDxcvx-6SM,5468
139
- atlan_application_sdk-0.1.1rc35.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
140
- atlan_application_sdk-0.1.1rc35.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
141
- atlan_application_sdk-0.1.1rc35.dist-info/licenses/NOTICE,sha256=A-XVVGt3KOYuuMmvSMIFkg534F1vHiCggEBp4Ez3wGk,1041
142
- atlan_application_sdk-0.1.1rc35.dist-info/RECORD,,
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,,