prefect-client 3.0.1__py3-none-any.whl → 3.0.2__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 (40) hide show
  1. prefect/_internal/compatibility/deprecated.py +1 -1
  2. prefect/blocks/notifications.py +21 -0
  3. prefect/blocks/webhook.py +8 -0
  4. prefect/client/orchestration.py +39 -20
  5. prefect/client/schemas/actions.py +2 -2
  6. prefect/client/schemas/objects.py +24 -6
  7. prefect/client/types/flexible_schedule_list.py +1 -1
  8. prefect/concurrency/asyncio.py +45 -6
  9. prefect/concurrency/services.py +1 -1
  10. prefect/concurrency/sync.py +21 -27
  11. prefect/concurrency/v1/asyncio.py +3 -0
  12. prefect/concurrency/v1/sync.py +4 -5
  13. prefect/context.py +5 -1
  14. prefect/deployments/runner.py +1 -0
  15. prefect/events/actions.py +6 -0
  16. prefect/flow_engine.py +12 -4
  17. prefect/locking/filesystem.py +243 -0
  18. prefect/logging/handlers.py +0 -2
  19. prefect/logging/loggers.py +0 -18
  20. prefect/logging/logging.yml +1 -0
  21. prefect/main.py +19 -5
  22. prefect/records/base.py +12 -0
  23. prefect/records/filesystem.py +6 -2
  24. prefect/records/memory.py +6 -0
  25. prefect/records/result_store.py +6 -0
  26. prefect/results.py +169 -25
  27. prefect/runner/runner.py +74 -5
  28. prefect/settings.py +1 -1
  29. prefect/states.py +34 -17
  30. prefect/task_engine.py +31 -37
  31. prefect/transactions.py +105 -50
  32. prefect/utilities/engine.py +16 -8
  33. prefect/utilities/importtools.py +1 -0
  34. prefect/utilities/urls.py +70 -12
  35. prefect/workers/base.py +14 -6
  36. {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/METADATA +1 -1
  37. {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/RECORD +40 -39
  38. {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/LICENSE +0 -0
  39. {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/WHEEL +0 -0
  40. {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/top_level.txt +0 -0
prefect/flow_engine.py CHANGED
@@ -47,7 +47,12 @@ from prefect.logging.loggers import (
47
47
  get_run_logger,
48
48
  patch_print,
49
49
  )
50
- from prefect.results import BaseResult, ResultStore, get_current_result_store
50
+ from prefect.results import (
51
+ BaseResult,
52
+ ResultStore,
53
+ get_result_store,
54
+ should_persist_result,
55
+ )
51
56
  from prefect.settings import PREFECT_DEBUG_MODE
52
57
  from prefect.states import (
53
58
  Failed,
@@ -202,7 +207,7 @@ class FlowRunEngine(Generic[P, R]):
202
207
  self.handle_exception(
203
208
  exc,
204
209
  msg=message,
205
- result_store=get_current_result_store().update_for_flow(
210
+ result_store=get_result_store().update_for_flow(
206
211
  self.flow, _sync=True
207
212
  ),
208
213
  )
@@ -271,7 +276,7 @@ class FlowRunEngine(Generic[P, R]):
271
276
  return_value_to_state(
272
277
  resolved_result,
273
278
  result_store=result_store,
274
- write_result=True,
279
+ write_result=should_persist_result(),
275
280
  )
276
281
  )
277
282
  self.set_state(terminal_state)
@@ -507,10 +512,13 @@ class FlowRunEngine(Generic[P, R]):
507
512
  flow_run=self.flow_run,
508
513
  parameters=self.parameters,
509
514
  client=client,
510
- result_store=get_current_result_store().update_for_flow(
515
+ result_store=get_result_store().update_for_flow(
511
516
  self.flow, _sync=True
512
517
  ),
513
518
  task_runner=task_runner,
519
+ persist_result=self.flow.persist_result
520
+ if self.flow.persist_result is not None
521
+ else should_persist_result(),
514
522
  )
515
523
  )
516
524
  stack.enter_context(ConcurrencyContextV1())
@@ -0,0 +1,243 @@
1
+ import time
2
+ from pathlib import Path
3
+ from typing import Dict, Optional
4
+
5
+ import anyio
6
+ import pendulum
7
+ import pydantic_core
8
+ from typing_extensions import TypedDict
9
+
10
+ from prefect.logging.loggers import get_logger
11
+
12
+ from .protocol import LockManager
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 FileSystemLockManager(LockManager):
33
+ """
34
+ A lock manager that implements locking using local files.
35
+
36
+ Attributes:
37
+ lock_files_directory: the directory where lock files are stored
38
+ """
39
+
40
+ def __init__(self, lock_files_directory: Path):
41
+ self.lock_files_directory = lock_files_directory
42
+ self._locks: Dict[str, _LockInfo] = {}
43
+
44
+ def _ensure_records_directory_exists(self):
45
+ self.lock_files_directory.mkdir(parents=True, exist_ok=True)
46
+
47
+ def _lock_path_for_key(self, key: str) -> Path:
48
+ if (lock_info := self._locks.get(key)) is not None:
49
+ return lock_info["path"]
50
+ return self.lock_files_directory.joinpath(key).with_suffix(".lock")
51
+
52
+ def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
53
+ if use_cache:
54
+ if (lock_info := self._locks.get(key)) is not None:
55
+ return lock_info
56
+
57
+ lock_path = self._lock_path_for_key(key)
58
+
59
+ try:
60
+ with open(lock_path, "rb") as lock_file:
61
+ lock_info = pydantic_core.from_json(lock_file.read())
62
+ lock_info["path"] = lock_path
63
+ expiration = lock_info.get("expiration")
64
+ lock_info["expiration"] = (
65
+ pendulum.parse(expiration) if expiration is not None else None
66
+ )
67
+ self._locks[key] = lock_info
68
+ return lock_info
69
+ except FileNotFoundError:
70
+ return None
71
+
72
+ async def _aget_lock_info(
73
+ self, key: str, use_cache: bool = True
74
+ ) -> Optional[_LockInfo]:
75
+ if use_cache:
76
+ if (lock_info := self._locks.get(key)) is not None:
77
+ return lock_info
78
+
79
+ lock_path = self._lock_path_for_key(key)
80
+
81
+ try:
82
+ lock_info_bytes = await anyio.Path(lock_path).read_bytes()
83
+ lock_info = pydantic_core.from_json(lock_info_bytes)
84
+ lock_info["path"] = lock_path
85
+ expiration = lock_info.get("expiration")
86
+ lock_info["expiration"] = (
87
+ pendulum.parse(expiration) if expiration is not None else None
88
+ )
89
+ self._locks[key] = lock_info
90
+ return lock_info
91
+ except FileNotFoundError:
92
+ return None
93
+
94
+ def acquire_lock(
95
+ self,
96
+ key: str,
97
+ holder: str,
98
+ acquire_timeout: Optional[float] = None,
99
+ hold_timeout: Optional[float] = None,
100
+ ) -> bool:
101
+ self._ensure_records_directory_exists()
102
+ lock_path = self._lock_path_for_key(key)
103
+
104
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
105
+ lock_free = self.wait_for_lock(key, acquire_timeout)
106
+ if not lock_free:
107
+ return False
108
+
109
+ try:
110
+ Path(lock_path).touch(exist_ok=False)
111
+ except FileExistsError:
112
+ if not self.is_lock_holder(key, holder):
113
+ logger.debug(
114
+ f"Another actor acquired the lock for record with key {key}. Trying again."
115
+ )
116
+ return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
117
+ expiration = (
118
+ pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
119
+ if hold_timeout is not None
120
+ else None
121
+ )
122
+
123
+ with open(Path(lock_path), "wb") as lock_file:
124
+ lock_file.write(
125
+ pydantic_core.to_json(
126
+ {
127
+ "holder": holder,
128
+ "expiration": str(expiration)
129
+ if expiration is not None
130
+ else None,
131
+ },
132
+ )
133
+ )
134
+
135
+ self._locks[key] = {
136
+ "holder": holder,
137
+ "expiration": expiration,
138
+ "path": lock_path,
139
+ }
140
+
141
+ return True
142
+
143
+ async def aacquire_lock(
144
+ self,
145
+ key: str,
146
+ holder: str,
147
+ acquire_timeout: Optional[float] = None,
148
+ hold_timeout: Optional[float] = None,
149
+ ) -> bool:
150
+ await anyio.Path(self.lock_files_directory).mkdir(parents=True, exist_ok=True)
151
+ lock_path = self._lock_path_for_key(key)
152
+
153
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
154
+ lock_free = await self.await_for_lock(key, acquire_timeout)
155
+ if not lock_free:
156
+ return False
157
+
158
+ try:
159
+ await anyio.Path(lock_path).touch(exist_ok=False)
160
+ except FileExistsError:
161
+ if not self.is_lock_holder(key, holder):
162
+ logger.debug(
163
+ f"Another actor acquired the lock for record with key {key}. Trying again."
164
+ )
165
+ return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
166
+ expiration = (
167
+ pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
168
+ if hold_timeout is not None
169
+ else None
170
+ )
171
+
172
+ async with await anyio.Path(lock_path).open("wb") as lock_file:
173
+ await lock_file.write(
174
+ pydantic_core.to_json(
175
+ {
176
+ "holder": holder,
177
+ "expiration": str(expiration)
178
+ if expiration is not None
179
+ else None,
180
+ },
181
+ )
182
+ )
183
+
184
+ self._locks[key] = {
185
+ "holder": holder,
186
+ "expiration": expiration,
187
+ "path": lock_path,
188
+ }
189
+
190
+ return True
191
+
192
+ def release_lock(self, key: str, holder: str) -> None:
193
+ lock_path = self._lock_path_for_key(key)
194
+ if not self.is_locked(key):
195
+ ValueError(f"No lock for transaction with key {key}")
196
+ if self.is_lock_holder(key, holder):
197
+ Path(lock_path).unlink(missing_ok=True)
198
+ self._locks.pop(key, None)
199
+ else:
200
+ raise ValueError(f"No lock held by {holder} for transaction with key {key}")
201
+
202
+ def is_locked(self, key: str, use_cache: bool = False) -> bool:
203
+ if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
204
+ return False
205
+
206
+ if (expiration := lock_info.get("expiration")) is None:
207
+ return True
208
+
209
+ expired = expiration < pendulum.now("utc")
210
+ if expired:
211
+ Path(lock_info["path"]).unlink()
212
+ self._locks.pop(key, None)
213
+ return False
214
+ else:
215
+ return True
216
+
217
+ def is_lock_holder(self, key: str, holder: str) -> bool:
218
+ if not self.is_locked(key):
219
+ return False
220
+
221
+ if not self.is_locked(key):
222
+ return False
223
+ if (lock_info := self._get_lock_info(key)) is None:
224
+ return False
225
+ return lock_info["holder"] == holder
226
+
227
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
228
+ seconds_waited = 0
229
+ while self.is_locked(key, use_cache=False):
230
+ if timeout and seconds_waited >= timeout:
231
+ return False
232
+ seconds_waited += 0.1
233
+ time.sleep(0.1)
234
+ return True
235
+
236
+ async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
237
+ seconds_waited = 0
238
+ while self.is_locked(key, use_cache=False):
239
+ if timeout and seconds_waited >= timeout:
240
+ return False
241
+ seconds_waited += 0.1
242
+ await anyio.sleep(0.1)
243
+ return True
@@ -138,8 +138,6 @@ class APILogHandler(logging.Handler):
138
138
  return # Respect the global settings toggle
139
139
  if not getattr(record, "send_to_api", True):
140
140
  return # Do not send records that have opted out
141
- if not getattr(record, "send_to_orion", True):
142
- return # Backwards compatibility
143
141
 
144
142
  log = self.prepare(record)
145
143
  APILogWorker.instance().send(log)
@@ -1,7 +1,6 @@
1
1
  import io
2
2
  import logging
3
3
  import sys
4
- import warnings
5
4
  from builtins import print
6
5
  from contextlib import contextmanager
7
6
  from functools import lru_cache
@@ -34,23 +33,6 @@ class PrefectLogAdapter(logging.LoggerAdapter):
34
33
 
35
34
  def process(self, msg, kwargs):
36
35
  kwargs["extra"] = {**(self.extra or {}), **(kwargs.get("extra") or {})}
37
-
38
- from prefect._internal.compatibility.deprecated import (
39
- PrefectDeprecationWarning,
40
- generate_deprecation_message,
41
- )
42
-
43
- if "send_to_orion" in kwargs["extra"]:
44
- warnings.warn(
45
- generate_deprecation_message(
46
- 'The "send_to_orion" option',
47
- start_date="May 2023",
48
- help='Use "send_to_api" instead.',
49
- ),
50
- PrefectDeprecationWarning,
51
- stacklevel=4,
52
- )
53
-
54
36
  return (msg, kwargs)
55
37
 
56
38
  def getChild(
@@ -77,6 +77,7 @@ loggers:
77
77
  prefect.extra:
78
78
  level: "${PREFECT_LOGGING_LEVEL}"
79
79
  handlers: [api]
80
+ propagate: false
80
81
 
81
82
  prefect.flow_runs:
82
83
  level: NOTSET
prefect/main.py CHANGED
@@ -7,7 +7,7 @@ from prefect.transactions import Transaction
7
7
  from prefect.tasks import task, Task
8
8
  from prefect.context import tags
9
9
  from prefect.utilities.annotations import unmapped, allow_failure
10
- from prefect.results import BaseResult
10
+ from prefect.results import BaseResult, ResultRecordMetadata
11
11
  from prefect.flow_runs import pause_flow_run, resume_flow_run, suspend_flow_run
12
12
  from prefect.client.orchestration import get_client, PrefectClient
13
13
  from prefect.client.cloud import get_cloud_client, CloudClient
@@ -26,12 +26,26 @@ import prefect.context
26
26
  import prefect.client.schemas
27
27
 
28
28
  prefect.context.FlowRunContext.model_rebuild(
29
- _types_namespace={"Flow": Flow, "BaseResult": BaseResult}
29
+ _types_namespace={
30
+ "Flow": Flow,
31
+ "BaseResult": BaseResult,
32
+ "ResultRecordMetadata": ResultRecordMetadata,
33
+ }
34
+ )
35
+ prefect.context.TaskRunContext.model_rebuild(
36
+ _types_namespace={"Task": Task, "BaseResult": BaseResult}
37
+ )
38
+ prefect.client.schemas.State.model_rebuild(
39
+ _types_namespace={
40
+ "BaseResult": BaseResult,
41
+ "ResultRecordMetadata": ResultRecordMetadata,
42
+ }
30
43
  )
31
- prefect.context.TaskRunContext.model_rebuild(_types_namespace={"Task": Task})
32
- prefect.client.schemas.State.model_rebuild(_types_namespace={"BaseResult": BaseResult})
33
44
  prefect.client.schemas.StateCreate.model_rebuild(
34
- _types_namespace={"BaseResult": BaseResult}
45
+ _types_namespace={
46
+ "BaseResult": BaseResult,
47
+ "ResultRecordMetadata": ResultRecordMetadata,
48
+ }
35
49
  )
36
50
  Transaction.model_rebuild()
37
51
 
prefect/records/base.py CHANGED
@@ -6,11 +6,18 @@ from contextlib import contextmanager
6
6
  from dataclasses import dataclass
7
7
  from typing import TYPE_CHECKING, Optional
8
8
 
9
+ from prefect._internal.compatibility import deprecated
10
+
9
11
  if TYPE_CHECKING:
10
12
  from prefect.results import BaseResult
11
13
  from prefect.transactions import IsolationLevel
12
14
 
13
15
 
16
+ @deprecated.deprecated_class(
17
+ start_date="Sep 2024",
18
+ end_date="Nov 2024",
19
+ help="Use `ResultRecord` instead to represent a result and its associated metadata.",
20
+ )
14
21
  @dataclass
15
22
  class TransactionRecord:
16
23
  """
@@ -21,6 +28,11 @@ class TransactionRecord:
21
28
  result: "BaseResult"
22
29
 
23
30
 
31
+ @deprecated.deprecated_class(
32
+ start_date="Sep 2024",
33
+ end_date="Nov 2024",
34
+ help="Use `ResultStore` and provide a `WritableFileSystem` for `metadata_storage` instead.",
35
+ )
24
36
  class RecordStore(abc.ABC):
25
37
  @abc.abstractmethod
26
38
  def read(
@@ -6,6 +6,7 @@ from typing import Dict, Optional
6
6
  import pendulum
7
7
  from typing_extensions import TypedDict
8
8
 
9
+ from prefect._internal.compatibility import deprecated
9
10
  from prefect.logging.loggers import get_logger
10
11
  from prefect.records.base import RecordStore, TransactionRecord
11
12
  from prefect.results import BaseResult
@@ -29,6 +30,11 @@ class _LockInfo(TypedDict):
29
30
  path: Path
30
31
 
31
32
 
33
+ @deprecated.deprecated_class(
34
+ start_date="Sep 2024",
35
+ end_date="Nov 2024",
36
+ help="Use `ResultStore` with a `LocalFileSystem` for `metadata_storage` and a `FileSystemLockManager` instead.",
37
+ )
32
38
  class FileSystemRecordStore(RecordStore):
33
39
  """
34
40
  A record store that stores data on the local filesystem.
@@ -56,7 +62,6 @@ class FileSystemRecordStore(RecordStore):
56
62
  def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
57
63
  if use_cache:
58
64
  if (lock_info := self._locks.get(key)) is not None:
59
- print("Got lock info from cache")
60
65
  return lock_info
61
66
 
62
67
  lock_path = self._lock_path_for_key(key)
@@ -70,7 +75,6 @@ class FileSystemRecordStore(RecordStore):
70
75
  pendulum.parse(expiration) if expiration is not None else None
71
76
  )
72
77
  self._locks[key] = lock_info
73
- print("Got lock info from file")
74
78
  return lock_info
75
79
  except FileNotFoundError:
76
80
  return None
prefect/records/memory.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import threading
2
2
  from typing import Dict, Optional, TypedDict
3
3
 
4
+ from prefect._internal.compatibility import deprecated
4
5
  from prefect.results import BaseResult
5
6
  from prefect.transactions import IsolationLevel
6
7
 
@@ -22,6 +23,11 @@ class _LockInfo(TypedDict):
22
23
  expiration_timer: Optional[threading.Timer]
23
24
 
24
25
 
26
+ @deprecated.deprecated_class(
27
+ start_date="Sep 2024",
28
+ end_date="Nov 2024",
29
+ help="Use `ResultStore` with a `MemoryLockManager` instead.",
30
+ )
25
31
  class MemoryRecordStore(RecordStore):
26
32
  """
27
33
  A record store that stores data in memory.
@@ -3,6 +3,7 @@ from typing import Any, Optional
3
3
 
4
4
  import pendulum
5
5
 
6
+ from prefect._internal.compatibility import deprecated
6
7
  from prefect.results import BaseResult, PersistedResult, ResultStore
7
8
  from prefect.transactions import IsolationLevel
8
9
  from prefect.utilities.asyncutils import run_coro_as_sync
@@ -10,6 +11,11 @@ from prefect.utilities.asyncutils import run_coro_as_sync
10
11
  from .base import RecordStore, TransactionRecord
11
12
 
12
13
 
14
+ @deprecated.deprecated_class(
15
+ start_date="Sep 2024",
16
+ end_date="Nov 2024",
17
+ help="Use `ResultStore` directly instead.",
18
+ )
13
19
  @dataclass
14
20
  class ResultRecordStore(RecordStore):
15
21
  """