prefect-client 3.1.9__py3-none-any.whl → 3.1.11__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 (113) hide show
  1. prefect/_experimental/lineage.py +7 -8
  2. prefect/_internal/_logging.py +15 -3
  3. prefect/_internal/compatibility/async_dispatch.py +22 -16
  4. prefect/_internal/compatibility/deprecated.py +42 -18
  5. prefect/_internal/compatibility/migration.py +2 -2
  6. prefect/_internal/concurrency/inspection.py +12 -14
  7. prefect/_internal/concurrency/primitives.py +2 -2
  8. prefect/_internal/concurrency/services.py +154 -80
  9. prefect/_internal/concurrency/waiters.py +13 -9
  10. prefect/_internal/pydantic/annotations/pendulum.py +7 -7
  11. prefect/_internal/pytz.py +4 -3
  12. prefect/_internal/retries.py +10 -5
  13. prefect/_internal/schemas/bases.py +19 -10
  14. prefect/_internal/schemas/validators.py +227 -388
  15. prefect/_version.py +3 -3
  16. prefect/artifacts.py +61 -74
  17. prefect/automations.py +27 -7
  18. prefect/blocks/core.py +3 -3
  19. prefect/client/{orchestration.py → orchestration/__init__.py} +38 -701
  20. prefect/client/orchestration/_artifacts/__init__.py +0 -0
  21. prefect/client/orchestration/_artifacts/client.py +239 -0
  22. prefect/client/orchestration/_concurrency_limits/__init__.py +0 -0
  23. prefect/client/orchestration/_concurrency_limits/client.py +762 -0
  24. prefect/client/orchestration/_logs/__init__.py +0 -0
  25. prefect/client/orchestration/_logs/client.py +95 -0
  26. prefect/client/orchestration/_variables/__init__.py +0 -0
  27. prefect/client/orchestration/_variables/client.py +157 -0
  28. prefect/client/orchestration/base.py +46 -0
  29. prefect/client/orchestration/routes.py +145 -0
  30. prefect/client/schemas/actions.py +2 -2
  31. prefect/client/schemas/filters.py +5 -0
  32. prefect/client/schemas/objects.py +3 -10
  33. prefect/client/schemas/schedules.py +22 -10
  34. prefect/concurrency/_asyncio.py +87 -0
  35. prefect/concurrency/{events.py → _events.py} +10 -10
  36. prefect/concurrency/asyncio.py +20 -104
  37. prefect/concurrency/context.py +6 -4
  38. prefect/concurrency/services.py +26 -74
  39. prefect/concurrency/sync.py +23 -44
  40. prefect/concurrency/v1/_asyncio.py +63 -0
  41. prefect/concurrency/v1/{events.py → _events.py} +13 -15
  42. prefect/concurrency/v1/asyncio.py +27 -80
  43. prefect/concurrency/v1/context.py +6 -4
  44. prefect/concurrency/v1/services.py +33 -79
  45. prefect/concurrency/v1/sync.py +18 -37
  46. prefect/context.py +66 -70
  47. prefect/deployments/base.py +4 -144
  48. prefect/deployments/flow_runs.py +12 -2
  49. prefect/deployments/runner.py +11 -3
  50. prefect/deployments/steps/pull.py +13 -0
  51. prefect/events/clients.py +7 -1
  52. prefect/events/schemas/events.py +3 -2
  53. prefect/flow_engine.py +54 -47
  54. prefect/flows.py +2 -1
  55. prefect/futures.py +42 -27
  56. prefect/input/run_input.py +2 -1
  57. prefect/locking/filesystem.py +8 -7
  58. prefect/locking/memory.py +5 -3
  59. prefect/locking/protocol.py +1 -1
  60. prefect/main.py +1 -3
  61. prefect/plugins.py +12 -10
  62. prefect/results.py +3 -308
  63. prefect/runner/storage.py +87 -21
  64. prefect/serializers.py +32 -25
  65. prefect/settings/legacy.py +4 -4
  66. prefect/settings/models/api.py +3 -3
  67. prefect/settings/models/cli.py +3 -3
  68. prefect/settings/models/client.py +5 -3
  69. prefect/settings/models/cloud.py +3 -3
  70. prefect/settings/models/deployments.py +3 -3
  71. prefect/settings/models/experiments.py +4 -2
  72. prefect/settings/models/flows.py +3 -3
  73. prefect/settings/models/internal.py +4 -2
  74. prefect/settings/models/logging.py +4 -3
  75. prefect/settings/models/results.py +3 -3
  76. prefect/settings/models/root.py +3 -2
  77. prefect/settings/models/runner.py +4 -4
  78. prefect/settings/models/server/api.py +3 -3
  79. prefect/settings/models/server/database.py +11 -4
  80. prefect/settings/models/server/deployments.py +6 -2
  81. prefect/settings/models/server/ephemeral.py +4 -2
  82. prefect/settings/models/server/events.py +3 -2
  83. prefect/settings/models/server/flow_run_graph.py +6 -2
  84. prefect/settings/models/server/root.py +3 -3
  85. prefect/settings/models/server/services.py +26 -11
  86. prefect/settings/models/server/tasks.py +6 -3
  87. prefect/settings/models/server/ui.py +3 -3
  88. prefect/settings/models/tasks.py +5 -5
  89. prefect/settings/models/testing.py +3 -3
  90. prefect/settings/models/worker.py +5 -3
  91. prefect/settings/profiles.py +15 -2
  92. prefect/states.py +4 -7
  93. prefect/task_engine.py +54 -75
  94. prefect/tasks.py +84 -32
  95. prefect/telemetry/processors.py +6 -6
  96. prefect/telemetry/run_telemetry.py +13 -8
  97. prefect/telemetry/services.py +32 -31
  98. prefect/transactions.py +4 -15
  99. prefect/utilities/_git.py +34 -0
  100. prefect/utilities/asyncutils.py +1 -1
  101. prefect/utilities/engine.py +3 -19
  102. prefect/utilities/generics.py +18 -0
  103. prefect/workers/__init__.py +2 -0
  104. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/METADATA +1 -1
  105. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/RECORD +108 -99
  106. prefect/records/__init__.py +0 -1
  107. prefect/records/base.py +0 -235
  108. prefect/records/filesystem.py +0 -213
  109. prefect/records/memory.py +0 -184
  110. prefect/records/result_store.py +0 -70
  111. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/LICENSE +0 -0
  112. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/WHEEL +0 -0
  113. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/top_level.txt +0 -0
@@ -1,213 +0,0 @@
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._internal.compatibility import deprecated
10
- from prefect.logging.loggers import get_logger
11
- from prefect.records.base import RecordStore, TransactionRecord
12
- from prefect.results import BaseResult
13
- from prefect.transactions import IsolationLevel
14
-
15
- logger = get_logger(__name__)
16
-
17
-
18
- class _LockInfo(TypedDict):
19
- """
20
- A dictionary containing information about a lock.
21
-
22
- Attributes:
23
- holder: The holder of the lock.
24
- expiration: Datetime when the lock expires.
25
- path: Path to the lock file.
26
- """
27
-
28
- holder: str
29
- expiration: Optional[pendulum.DateTime]
30
- path: Path
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
- )
38
- class FileSystemRecordStore(RecordStore):
39
- """
40
- A record store that stores data on the local filesystem.
41
-
42
- Locking is implemented using a lock file with the same name as the record file,
43
- but with a `.lock` extension.
44
-
45
- Attributes:
46
- records_directory: the directory where records are stored; defaults to
47
- `{PREFECT_HOME}/records`
48
- """
49
-
50
- def __init__(self, records_directory: Path):
51
- self.records_directory = records_directory
52
- self._locks: Dict[str, _LockInfo] = {}
53
-
54
- def _ensure_records_directory_exists(self):
55
- self.records_directory.mkdir(parents=True, exist_ok=True)
56
-
57
- def _lock_path_for_key(self, key: str) -> Path:
58
- if (lock_info := self._locks.get(key)) is not None:
59
- return lock_info["path"]
60
- return self.records_directory.joinpath(key).with_suffix(".lock")
61
-
62
- def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
63
- if use_cache:
64
- if (lock_info := self._locks.get(key)) is not None:
65
- return lock_info
66
-
67
- lock_path = self._lock_path_for_key(key)
68
-
69
- try:
70
- with open(lock_path, "r") as lock_file:
71
- lock_info = json.load(lock_file)
72
- lock_info["path"] = lock_path
73
- expiration = lock_info.get("expiration")
74
- lock_info["expiration"] = (
75
- pendulum.parse(expiration) if expiration is not None else None
76
- )
77
- self._locks[key] = lock_info
78
- return lock_info
79
- except FileNotFoundError:
80
- return None
81
-
82
- def read(
83
- self, key: str, holder: Optional[str] = None, timeout: Optional[float] = None
84
- ) -> Optional[TransactionRecord]:
85
- if not self.exists(key):
86
- return None
87
-
88
- holder = holder or self.generate_default_holder()
89
-
90
- if self.is_locked(key) and not self.is_lock_holder(key, holder):
91
- unlocked = self.wait_for_lock(key, timeout=timeout)
92
- if not unlocked:
93
- return None
94
- record_data = self.records_directory.joinpath(key).read_text()
95
- return TransactionRecord(
96
- key=key, result=BaseResult.model_validate_json(record_data)
97
- )
98
-
99
- def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
100
- self._ensure_records_directory_exists()
101
-
102
- if self.is_locked(key) and not self.is_lock_holder(key, holder):
103
- raise ValueError(
104
- f"Cannot write to transaction with key {key} because it is locked by another holder."
105
- )
106
-
107
- record_path = self.records_directory.joinpath(key)
108
- record_path.touch(exist_ok=True)
109
- record_data = result.model_dump_json()
110
- record_path.write_text(record_data)
111
-
112
- def exists(self, key: str) -> bool:
113
- return self.records_directory.joinpath(key).exists()
114
-
115
- def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
116
- return isolation_level in {
117
- IsolationLevel.READ_COMMITTED,
118
- IsolationLevel.SERIALIZABLE,
119
- }
120
-
121
- def acquire_lock(
122
- self,
123
- key: str,
124
- holder: Optional[str] = None,
125
- acquire_timeout: Optional[float] = None,
126
- hold_timeout: Optional[float] = None,
127
- ) -> bool:
128
- holder = holder or self.generate_default_holder()
129
-
130
- self._ensure_records_directory_exists()
131
- lock_path = self._lock_path_for_key(key)
132
-
133
- if self.is_locked(key) and not self.is_lock_holder(key, holder):
134
- lock_free = self.wait_for_lock(key, acquire_timeout)
135
- if not lock_free:
136
- return False
137
-
138
- try:
139
- Path(lock_path).touch(exist_ok=False)
140
- except FileExistsError:
141
- if not self.is_lock_holder(key, holder):
142
- logger.debug(
143
- f"Another actor acquired the lock for record with key {key}. Trying again."
144
- )
145
- return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
146
- expiration = (
147
- pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
148
- if hold_timeout is not None
149
- else None
150
- )
151
-
152
- with open(Path(lock_path), "w") as lock_file:
153
- json.dump(
154
- {
155
- "holder": holder,
156
- "expiration": str(expiration) if expiration is not None else None,
157
- },
158
- lock_file,
159
- )
160
-
161
- self._locks[key] = {
162
- "holder": holder,
163
- "expiration": expiration,
164
- "path": lock_path,
165
- }
166
-
167
- return True
168
-
169
- def release_lock(self, key: str, holder: Optional[str] = None) -> None:
170
- holder = holder or self.generate_default_holder()
171
- lock_path = self._lock_path_for_key(key)
172
- if not self.is_locked(key):
173
- ValueError(f"No lock for transaction with key {key}")
174
- if self.is_lock_holder(key, holder):
175
- Path(lock_path).unlink(missing_ok=True)
176
- self._locks.pop(key, None)
177
- else:
178
- raise ValueError(f"No lock held by {holder} for transaction with key {key}")
179
-
180
- def is_locked(self, key: str, use_cache: bool = False) -> bool:
181
- if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
182
- return False
183
-
184
- if (expiration := lock_info.get("expiration")) is None:
185
- return True
186
-
187
- expired = expiration < pendulum.now("utc")
188
- if expired:
189
- Path(lock_info["path"]).unlink()
190
- self._locks.pop(key, None)
191
- return False
192
- else:
193
- return True
194
-
195
- def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
196
- if not self.is_locked(key):
197
- return False
198
-
199
- holder = holder or self.generate_default_holder()
200
- if not self.is_locked(key):
201
- return False
202
- if (lock_info := self._get_lock_info(key)) is None:
203
- return False
204
- return lock_info["holder"] == holder
205
-
206
- def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
207
- seconds_waited = 0
208
- while self.is_locked(key, use_cache=False):
209
- if timeout and seconds_waited >= timeout:
210
- return False
211
- seconds_waited += 0.1
212
- time.sleep(0.1)
213
- return True
prefect/records/memory.py DELETED
@@ -1,184 +0,0 @@
1
- import threading
2
- from typing import Dict, Optional, TypedDict
3
-
4
- from prefect._internal.compatibility import deprecated
5
- from prefect.results import BaseResult
6
- from prefect.transactions import IsolationLevel
7
-
8
- from .base import RecordStore, TransactionRecord
9
-
10
-
11
- class _LockInfo(TypedDict):
12
- """
13
- A dictionary containing information about a lock.
14
-
15
- Attributes:
16
- holder: The holder of the lock.
17
- lock: The lock object.
18
- expiration_timer: The timer for the lock expiration
19
- """
20
-
21
- holder: str
22
- lock: threading.Lock
23
- expiration_timer: Optional[threading.Timer]
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
- )
31
- class MemoryRecordStore(RecordStore):
32
- """
33
- A record store that stores data in memory.
34
- """
35
-
36
- _instance = None
37
-
38
- def __new__(cls, *args, **kwargs):
39
- if cls._instance is None:
40
- cls._instance = super().__new__(cls)
41
- return cls._instance
42
-
43
- def __init__(self):
44
- self._locks_dict_lock = threading.Lock()
45
- self._locks: Dict[str, _LockInfo] = {}
46
- self._records: Dict[str, TransactionRecord] = {}
47
-
48
- def read(
49
- self, key: str, holder: Optional[str] = None
50
- ) -> Optional[TransactionRecord]:
51
- holder = holder or self.generate_default_holder()
52
-
53
- if self.is_locked(key) and not self.is_lock_holder(key, holder):
54
- self.wait_for_lock(key)
55
- return self._records.get(key)
56
-
57
- def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
58
- holder = holder or self.generate_default_holder()
59
-
60
- with self._locks_dict_lock:
61
- if self.is_locked(key) and not self.is_lock_holder(key, holder):
62
- raise ValueError(
63
- f"Cannot write to transaction with key {key} because it is locked by another holder."
64
- )
65
- self._records[key] = TransactionRecord(key=key, result=result)
66
-
67
- def exists(self, key: str) -> bool:
68
- return key in self._records
69
-
70
- def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
71
- return isolation_level in {
72
- IsolationLevel.READ_COMMITTED,
73
- IsolationLevel.SERIALIZABLE,
74
- }
75
-
76
- def _expire_lock(self, key: str):
77
- """
78
- Expire the lock for the given key.
79
-
80
- Used as a callback for the expiration timer of a lock.
81
-
82
- Args:
83
- key: The key of the lock to expire.
84
- """
85
- with self._locks_dict_lock:
86
- if key in self._locks:
87
- lock_info = self._locks[key]
88
- if lock_info["lock"].locked():
89
- lock_info["lock"].release()
90
- if lock_info["expiration_timer"]:
91
- lock_info["expiration_timer"].cancel()
92
- del self._locks[key]
93
-
94
- def acquire_lock(
95
- self,
96
- key: str,
97
- holder: Optional[str] = None,
98
- acquire_timeout: Optional[float] = None,
99
- hold_timeout: Optional[float] = None,
100
- ) -> bool:
101
- holder = holder or self.generate_default_holder()
102
- with self._locks_dict_lock:
103
- if key not in self._locks:
104
- lock = threading.Lock()
105
- lock.acquire()
106
- expiration_timer = None
107
- if hold_timeout is not None:
108
- expiration_timer = threading.Timer(
109
- hold_timeout, self._expire_lock, args=(key,)
110
- )
111
- expiration_timer.start()
112
- self._locks[key] = _LockInfo(
113
- holder=holder, lock=lock, expiration_timer=expiration_timer
114
- )
115
- return True
116
- elif self._locks[key]["holder"] == holder:
117
- return True
118
- else:
119
- existing_lock_info = self._locks[key]
120
-
121
- if acquire_timeout is not None:
122
- existing_lock_acquired = existing_lock_info["lock"].acquire(
123
- timeout=acquire_timeout
124
- )
125
- else:
126
- existing_lock_acquired = existing_lock_info["lock"].acquire()
127
-
128
- if existing_lock_acquired:
129
- with self._locks_dict_lock:
130
- if (
131
- expiration_timer := existing_lock_info["expiration_timer"]
132
- ) is not None:
133
- expiration_timer.cancel()
134
- expiration_timer = None
135
- if hold_timeout is not None:
136
- expiration_timer = threading.Timer(
137
- hold_timeout, self._expire_lock, args=(key,)
138
- )
139
- expiration_timer.start()
140
- self._locks[key] = _LockInfo(
141
- holder=holder,
142
- lock=existing_lock_info["lock"],
143
- expiration_timer=expiration_timer,
144
- )
145
- return True
146
- return False
147
-
148
- def release_lock(self, key: str, holder: Optional[str] = None) -> None:
149
- holder = holder or self.generate_default_holder()
150
- with self._locks_dict_lock:
151
- if key in self._locks and self._locks[key]["holder"] == holder:
152
- if (
153
- expiration_timer := self._locks[key]["expiration_timer"]
154
- ) is not None:
155
- expiration_timer.cancel()
156
- self._locks[key]["lock"].release()
157
- del self._locks[key]
158
- else:
159
- raise ValueError(
160
- f"No lock held by {holder} for transaction with key {key}"
161
- )
162
-
163
- def is_locked(self, key: str) -> bool:
164
- return key in self._locks and self._locks[key]["lock"].locked()
165
-
166
- def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
167
- holder = holder or self.generate_default_holder()
168
- lock_info = self._locks.get(key)
169
- return (
170
- lock_info is not None
171
- and lock_info["lock"].locked()
172
- and lock_info["holder"] == holder
173
- )
174
-
175
- def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
176
- if lock := self._locks.get(key, {}).get("lock"):
177
- if timeout is not None:
178
- lock_acquired = lock.acquire(timeout=timeout)
179
- else:
180
- lock_acquired = lock.acquire()
181
- if lock_acquired:
182
- lock.release()
183
- return lock_acquired
184
- return True
@@ -1,70 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Any, Optional
3
-
4
- import pendulum
5
-
6
- from prefect._internal.compatibility import deprecated
7
- from prefect.results import BaseResult, PersistedResult, ResultStore
8
- from prefect.transactions import IsolationLevel
9
- from prefect.utilities.asyncutils import run_coro_as_sync
10
-
11
- from .base import RecordStore, TransactionRecord
12
-
13
-
14
- @deprecated.deprecated_class(
15
- start_date="Sep 2024",
16
- end_date="Nov 2024",
17
- help="Use `ResultStore` directly instead.",
18
- )
19
- @dataclass
20
- class ResultRecordStore(RecordStore):
21
- """
22
- A record store for result records.
23
-
24
- Collocates result metadata with result data.
25
- """
26
-
27
- result_store: ResultStore
28
- cache: Optional[PersistedResult] = None
29
-
30
- def exists(self, key: str) -> bool:
31
- try:
32
- record = self.read(key)
33
- if not record:
34
- return False
35
- result = record.result
36
- result.get(_sync=True)
37
- if result.expiration:
38
- # if the result has an expiration,
39
- # check if it is still in the future
40
- exists = result.expiration > pendulum.now("utc")
41
- else:
42
- exists = True
43
- self.cache = result
44
- return exists
45
- except Exception:
46
- return False
47
-
48
- def read(self, key: str, holder: Optional[str] = None) -> TransactionRecord:
49
- if self.cache:
50
- return TransactionRecord(key=key, result=self.cache)
51
- try:
52
- result = PersistedResult(
53
- serializer_type=self.result_store.serializer.type,
54
- storage_block_id=self.result_store.result_storage_block_id,
55
- storage_key=key,
56
- )
57
- return TransactionRecord(key=key, result=result)
58
- except Exception:
59
- # this is a bit of a bandaid for functionality
60
- raise ValueError("Result could not be read")
61
-
62
- def write(self, key: str, result: Any, holder: Optional[str] = None) -> None:
63
- if isinstance(result, PersistedResult):
64
- # if the value is already a persisted result, write it
65
- result.write(_sync=True)
66
- elif not isinstance(result, BaseResult):
67
- run_coro_as_sync(self.result_store.create_result(obj=result, key=key))
68
-
69
- def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
70
- return isolation_level == IsolationLevel.READ_COMMITTED