prefect-client 2.20.2__py3-none-any.whl → 3.0.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.
Files changed (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +423 -164
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +667 -440
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2466
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +124 -51
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +138 -48
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.2.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,207 @@
1
+ import json
2
+ import time
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ import pendulum
7
+ from typing_extensions import TypedDict
8
+
9
+ from prefect.logging.loggers import get_logger
10
+ from prefect.records.base import RecordStore, TransactionRecord
11
+ from prefect.results import BaseResult
12
+ from prefect.transactions import IsolationLevel
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class _LockInfo(TypedDict):
18
+ """
19
+ A dictionary containing information about a lock.
20
+
21
+ Attributes:
22
+ holder: The holder of the lock.
23
+ expiration: Datetime when the lock expires.
24
+ path: Path to the lock file.
25
+ """
26
+
27
+ holder: str
28
+ expiration: Optional[pendulum.DateTime]
29
+ path: Path
30
+
31
+
32
+ class FileSystemRecordStore(RecordStore):
33
+ """
34
+ A record store that stores data on the local filesystem.
35
+
36
+ Locking is implemented using a lock file with the same name as the record file,
37
+ but with a `.lock` extension.
38
+
39
+ Attributes:
40
+ records_directory: the directory where records are stored; defaults to
41
+ `{PREFECT_HOME}/records`
42
+ """
43
+
44
+ def __init__(self, records_directory: Path):
45
+ self.records_directory = records_directory
46
+ self._locks: Dict[str, _LockInfo] = {}
47
+
48
+ def _ensure_records_directory_exists(self):
49
+ self.records_directory.mkdir(parents=True, exist_ok=True)
50
+
51
+ def _lock_path_for_key(self, key: str) -> Path:
52
+ if (lock_info := self._locks.get(key)) is not None:
53
+ return lock_info["path"]
54
+ return self.records_directory.joinpath(key).with_suffix(".lock")
55
+
56
+ def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
57
+ if use_cache:
58
+ if (lock_info := self._locks.get(key)) is not None:
59
+ print("Got lock info from cache")
60
+ return lock_info
61
+
62
+ lock_path = self._lock_path_for_key(key)
63
+
64
+ try:
65
+ with open(lock_path, "r") as lock_file:
66
+ lock_info = json.load(lock_file)
67
+ lock_info["path"] = lock_path
68
+ expiration = lock_info.get("expiration")
69
+ lock_info["expiration"] = (
70
+ pendulum.parse(expiration) if expiration is not None else None
71
+ )
72
+ self._locks[key] = lock_info
73
+ print("Got lock info from file")
74
+ return lock_info
75
+ except FileNotFoundError:
76
+ return None
77
+
78
+ def read(
79
+ self, key: str, holder: Optional[str] = None
80
+ ) -> Optional[TransactionRecord]:
81
+ if not self.exists(key):
82
+ return None
83
+
84
+ holder = holder or self.generate_default_holder()
85
+
86
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
87
+ self.wait_for_lock(key)
88
+ record_data = self.records_directory.joinpath(key).read_text()
89
+ return TransactionRecord(
90
+ key=key, result=BaseResult.model_validate_json(record_data)
91
+ )
92
+
93
+ def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
94
+ self._ensure_records_directory_exists()
95
+
96
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
97
+ raise ValueError(
98
+ f"Cannot write to transaction with key {key} because it is locked by another holder."
99
+ )
100
+
101
+ record_path = self.records_directory.joinpath(key)
102
+ record_path.touch(exist_ok=True)
103
+ record_data = result.model_dump_json()
104
+ record_path.write_text(record_data)
105
+
106
+ def exists(self, key: str) -> bool:
107
+ return self.records_directory.joinpath(key).exists()
108
+
109
+ def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
110
+ return isolation_level in {
111
+ IsolationLevel.READ_COMMITTED,
112
+ IsolationLevel.SERIALIZABLE,
113
+ }
114
+
115
+ def acquire_lock(
116
+ self,
117
+ key: str,
118
+ holder: Optional[str] = None,
119
+ acquire_timeout: Optional[float] = None,
120
+ hold_timeout: Optional[float] = None,
121
+ ) -> bool:
122
+ holder = holder or self.generate_default_holder()
123
+
124
+ self._ensure_records_directory_exists()
125
+ lock_path = self._lock_path_for_key(key)
126
+
127
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
128
+ lock_free = self.wait_for_lock(key, acquire_timeout)
129
+ if not lock_free:
130
+ return False
131
+
132
+ try:
133
+ Path(lock_path).touch(exist_ok=False)
134
+ except FileExistsError:
135
+ if not self.is_lock_holder(key, holder):
136
+ logger.debug(
137
+ f"Another actor acquired the lock for record with key {key}. Trying again."
138
+ )
139
+ return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
140
+ expiration = (
141
+ pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
142
+ if hold_timeout is not None
143
+ else None
144
+ )
145
+
146
+ with open(Path(lock_path), "w") as lock_file:
147
+ json.dump(
148
+ {
149
+ "holder": holder,
150
+ "expiration": str(expiration) if expiration is not None else None,
151
+ },
152
+ lock_file,
153
+ )
154
+
155
+ self._locks[key] = {
156
+ "holder": holder,
157
+ "expiration": expiration,
158
+ "path": lock_path,
159
+ }
160
+
161
+ return True
162
+
163
+ def release_lock(self, key: str, holder: Optional[str] = None) -> None:
164
+ holder = holder or self.generate_default_holder()
165
+ lock_path = self._lock_path_for_key(key)
166
+ if not self.is_locked(key):
167
+ ValueError(f"No lock for transaction with key {key}")
168
+ if self.is_lock_holder(key, holder):
169
+ Path(lock_path).unlink(missing_ok=True)
170
+ self._locks.pop(key, None)
171
+ else:
172
+ raise ValueError(f"No lock held by {holder} for transaction with key {key}")
173
+
174
+ def is_locked(self, key: str, use_cache: bool = False) -> bool:
175
+ if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
176
+ return False
177
+
178
+ if (expiration := lock_info.get("expiration")) is None:
179
+ return True
180
+
181
+ expired = expiration < pendulum.now("utc")
182
+ if expired:
183
+ Path(lock_info["path"]).unlink()
184
+ self._locks.pop(key, None)
185
+ return False
186
+ else:
187
+ return True
188
+
189
+ def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
190
+ if not self.is_locked(key):
191
+ return False
192
+
193
+ holder = holder or self.generate_default_holder()
194
+ if not self.is_locked(key):
195
+ return False
196
+ if (lock_info := self._get_lock_info(key)) is None:
197
+ return False
198
+ return lock_info["holder"] == holder
199
+
200
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
201
+ seconds_waited = 0
202
+ while self.is_locked(key, use_cache=False):
203
+ if timeout and seconds_waited >= timeout:
204
+ return False
205
+ seconds_waited += 0.1
206
+ time.sleep(0.1)
207
+ return True
@@ -0,0 +1,178 @@
1
+ import threading
2
+ from typing import Dict, Optional, TypedDict
3
+
4
+ from prefect.results import BaseResult
5
+ from prefect.transactions import IsolationLevel
6
+
7
+ from .base import RecordStore, TransactionRecord
8
+
9
+
10
+ class _LockInfo(TypedDict):
11
+ """
12
+ A dictionary containing information about a lock.
13
+
14
+ Attributes:
15
+ holder: The holder of the lock.
16
+ lock: The lock object.
17
+ expiration_timer: The timer for the lock expiration
18
+ """
19
+
20
+ holder: str
21
+ lock: threading.Lock
22
+ expiration_timer: Optional[threading.Timer]
23
+
24
+
25
+ class MemoryRecordStore(RecordStore):
26
+ """
27
+ A record store that stores data in memory.
28
+ """
29
+
30
+ _instance = None
31
+
32
+ def __new__(cls, *args, **kwargs):
33
+ if cls._instance is None:
34
+ cls._instance = super().__new__(cls)
35
+ return cls._instance
36
+
37
+ def __init__(self):
38
+ self._locks_dict_lock = threading.Lock()
39
+ self._locks: Dict[str, _LockInfo] = {}
40
+ self._records: Dict[str, TransactionRecord] = {}
41
+
42
+ def read(
43
+ self, key: str, holder: Optional[str] = None
44
+ ) -> Optional[TransactionRecord]:
45
+ holder = holder or self.generate_default_holder()
46
+
47
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
48
+ self.wait_for_lock(key)
49
+ return self._records.get(key)
50
+
51
+ def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
52
+ holder = holder or self.generate_default_holder()
53
+
54
+ with self._locks_dict_lock:
55
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
56
+ raise ValueError(
57
+ f"Cannot write to transaction with key {key} because it is locked by another holder."
58
+ )
59
+ self._records[key] = TransactionRecord(key=key, result=result)
60
+
61
+ def exists(self, key: str) -> bool:
62
+ return key in self._records
63
+
64
+ def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
65
+ return isolation_level in {
66
+ IsolationLevel.READ_COMMITTED,
67
+ IsolationLevel.SERIALIZABLE,
68
+ }
69
+
70
+ def _expire_lock(self, key: str):
71
+ """
72
+ Expire the lock for the given key.
73
+
74
+ Used as a callback for the expiration timer of a lock.
75
+
76
+ Args:
77
+ key: The key of the lock to expire.
78
+ """
79
+ with self._locks_dict_lock:
80
+ if key in self._locks:
81
+ lock_info = self._locks[key]
82
+ if lock_info["lock"].locked():
83
+ lock_info["lock"].release()
84
+ if lock_info["expiration_timer"]:
85
+ lock_info["expiration_timer"].cancel()
86
+ del self._locks[key]
87
+
88
+ def acquire_lock(
89
+ self,
90
+ key: str,
91
+ holder: Optional[str] = None,
92
+ acquire_timeout: Optional[float] = None,
93
+ hold_timeout: Optional[float] = None,
94
+ ) -> bool:
95
+ holder = holder or self.generate_default_holder()
96
+ with self._locks_dict_lock:
97
+ if key not in self._locks:
98
+ lock = threading.Lock()
99
+ lock.acquire()
100
+ expiration_timer = None
101
+ if hold_timeout is not None:
102
+ expiration_timer = threading.Timer(
103
+ hold_timeout, self._expire_lock, args=(key,)
104
+ )
105
+ expiration_timer.start()
106
+ self._locks[key] = _LockInfo(
107
+ holder=holder, lock=lock, expiration_timer=expiration_timer
108
+ )
109
+ return True
110
+ elif self._locks[key]["holder"] == holder:
111
+ return True
112
+ else:
113
+ existing_lock_info = self._locks[key]
114
+
115
+ if acquire_timeout is not None:
116
+ existing_lock_acquired = existing_lock_info["lock"].acquire(
117
+ timeout=acquire_timeout
118
+ )
119
+ else:
120
+ existing_lock_acquired = existing_lock_info["lock"].acquire()
121
+
122
+ if existing_lock_acquired:
123
+ with self._locks_dict_lock:
124
+ if (
125
+ expiration_timer := existing_lock_info["expiration_timer"]
126
+ ) is not None:
127
+ expiration_timer.cancel()
128
+ expiration_timer = None
129
+ if hold_timeout is not None:
130
+ expiration_timer = threading.Timer(
131
+ hold_timeout, self._expire_lock, args=(key,)
132
+ )
133
+ expiration_timer.start()
134
+ self._locks[key] = _LockInfo(
135
+ holder=holder,
136
+ lock=existing_lock_info["lock"],
137
+ expiration_timer=expiration_timer,
138
+ )
139
+ return True
140
+ return False
141
+
142
+ def release_lock(self, key: str, holder: Optional[str] = None) -> None:
143
+ holder = holder or self.generate_default_holder()
144
+ with self._locks_dict_lock:
145
+ if key in self._locks and self._locks[key]["holder"] == holder:
146
+ if (
147
+ expiration_timer := self._locks[key]["expiration_timer"]
148
+ ) is not None:
149
+ expiration_timer.cancel()
150
+ self._locks[key]["lock"].release()
151
+ del self._locks[key]
152
+ else:
153
+ raise ValueError(
154
+ f"No lock held by {holder} for transaction with key {key}"
155
+ )
156
+
157
+ def is_locked(self, key: str) -> bool:
158
+ return key in self._locks and self._locks[key]["lock"].locked()
159
+
160
+ def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
161
+ holder = holder or self.generate_default_holder()
162
+ lock_info = self._locks.get(key)
163
+ return (
164
+ lock_info is not None
165
+ and lock_info["lock"].locked()
166
+ and lock_info["holder"] == holder
167
+ )
168
+
169
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
170
+ if lock := self._locks.get(key, {}).get("lock"):
171
+ if timeout is not None:
172
+ lock_acquired = lock.acquire(timeout=timeout)
173
+ else:
174
+ lock_acquired = lock.acquire()
175
+ if lock_acquired:
176
+ lock.release()
177
+ return lock_acquired
178
+ return True
@@ -0,0 +1,64 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Optional
3
+
4
+ import pendulum
5
+
6
+ from prefect.results import BaseResult, PersistedResult, ResultStore
7
+ from prefect.transactions import IsolationLevel
8
+ from prefect.utilities.asyncutils import run_coro_as_sync
9
+
10
+ from .base import RecordStore, TransactionRecord
11
+
12
+
13
+ @dataclass
14
+ class ResultRecordStore(RecordStore):
15
+ """
16
+ A record store for result records.
17
+
18
+ Collocates result metadata with result data.
19
+ """
20
+
21
+ result_store: ResultStore
22
+ cache: Optional[PersistedResult] = None
23
+
24
+ def exists(self, key: str) -> bool:
25
+ try:
26
+ record = self.read(key)
27
+ if not record:
28
+ return False
29
+ result = record.result
30
+ result.get(_sync=True)
31
+ if result.expiration:
32
+ # if the result has an expiration,
33
+ # check if it is still in the future
34
+ exists = result.expiration > pendulum.now("utc")
35
+ else:
36
+ exists = True
37
+ self.cache = result
38
+ return exists
39
+ except Exception:
40
+ return False
41
+
42
+ def read(self, key: str, holder: Optional[str] = None) -> TransactionRecord:
43
+ if self.cache:
44
+ return TransactionRecord(key=key, result=self.cache)
45
+ try:
46
+ result = PersistedResult(
47
+ serializer_type=self.result_store.serializer.type,
48
+ storage_block_id=self.result_store.result_storage_block_id,
49
+ storage_key=key,
50
+ )
51
+ return TransactionRecord(key=key, result=result)
52
+ except Exception:
53
+ # this is a bit of a bandaid for functionality
54
+ raise ValueError("Result could not be read")
55
+
56
+ def write(self, key: str, result: Any, holder: Optional[str] = None) -> None:
57
+ if isinstance(result, PersistedResult):
58
+ # if the value is already a persisted result, write it
59
+ result.write(_sync=True)
60
+ elif not isinstance(result, BaseResult):
61
+ run_coro_as_sync(self.result_store.create_result(obj=result, key=key))
62
+
63
+ def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
64
+ return isolation_level == IsolationLevel.READ_COMMITTED