prefect-client 3.0.0rc2__py3-none-any.whl → 3.0.0rc4__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.
- prefect/__init__.py +0 -1
- prefect/_internal/compatibility/migration.py +124 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pytz.py +1 -1
- prefect/blocks/core.py +1 -1
- prefect/client/orchestration.py +96 -22
- prefect/client/schemas/actions.py +1 -1
- prefect/client/schemas/filters.py +6 -0
- prefect/client/schemas/objects.py +10 -3
- prefect/client/subscriptions.py +6 -5
- prefect/context.py +1 -27
- prefect/deployments/__init__.py +3 -0
- prefect/deployments/base.py +4 -2
- prefect/deployments/deployments.py +3 -0
- prefect/deployments/steps/pull.py +1 -0
- prefect/deployments/steps/utility.py +2 -1
- prefect/engine.py +3 -0
- prefect/events/cli/automations.py +1 -1
- prefect/events/clients.py +7 -1
- prefect/exceptions.py +9 -0
- prefect/filesystems.py +22 -11
- prefect/flow_engine.py +195 -153
- prefect/flows.py +95 -36
- prefect/futures.py +9 -1
- prefect/infrastructure/provisioners/container_instance.py +1 -0
- prefect/infrastructure/provisioners/ecs.py +2 -2
- prefect/input/__init__.py +4 -0
- prefect/logging/formatters.py +2 -2
- prefect/logging/handlers.py +2 -2
- prefect/logging/loggers.py +1 -1
- prefect/plugins.py +1 -0
- prefect/records/cache_policies.py +3 -3
- prefect/records/result_store.py +10 -3
- prefect/results.py +47 -73
- prefect/runner/runner.py +1 -1
- prefect/runner/server.py +1 -1
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +1 -0
- prefect/runtime/task_run.py +1 -0
- prefect/settings.py +16 -3
- prefect/states.py +15 -4
- prefect/task_engine.py +195 -39
- prefect/task_runners.py +9 -3
- prefect/task_runs.py +26 -12
- prefect/task_worker.py +149 -20
- prefect/tasks.py +153 -71
- prefect/transactions.py +85 -15
- prefect/types/__init__.py +10 -3
- prefect/utilities/asyncutils.py +3 -3
- prefect/utilities/callables.py +16 -4
- prefect/utilities/collections.py +120 -57
- prefect/utilities/dockerutils.py +5 -3
- prefect/utilities/engine.py +11 -0
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +29 -0
- prefect/utilities/services.py +2 -2
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +4 -0
- prefect/workers/base.py +35 -0
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/METADATA +2 -2
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/RECORD +68 -66
- prefect/blocks/kubernetes.py +0 -115
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/top_level.txt +0 -0
prefect/transactions.py
CHANGED
@@ -7,17 +7,22 @@ from typing import (
|
|
7
7
|
List,
|
8
8
|
Optional,
|
9
9
|
Type,
|
10
|
-
TypeVar,
|
11
10
|
)
|
12
11
|
|
13
12
|
from pydantic import Field
|
13
|
+
from typing_extensions import Self
|
14
14
|
|
15
|
-
from prefect.context import ContextModel
|
15
|
+
from prefect.context import ContextModel, FlowRunContext, TaskRunContext
|
16
16
|
from prefect.records import RecordStore
|
17
|
+
from prefect.records.result_store import ResultFactoryStore
|
18
|
+
from prefect.results import (
|
19
|
+
BaseResult,
|
20
|
+
ResultFactory,
|
21
|
+
get_or_create_default_result_storage,
|
22
|
+
)
|
23
|
+
from prefect.utilities.asyncutils import run_coro_as_sync
|
17
24
|
from prefect.utilities.collections import AutoEnum
|
18
25
|
|
19
|
-
T = TypeVar("T")
|
20
|
-
|
21
26
|
|
22
27
|
class IsolationLevel(AutoEnum):
|
23
28
|
READ_COMMITTED = AutoEnum.auto()
|
@@ -54,7 +59,7 @@ class Transaction(ContextModel):
|
|
54
59
|
)
|
55
60
|
overwrite: bool = False
|
56
61
|
_staged_value: Any = None
|
57
|
-
__var__ = ContextVar("transaction")
|
62
|
+
__var__: ContextVar = ContextVar("transaction")
|
58
63
|
|
59
64
|
def is_committed(self) -> bool:
|
60
65
|
return self.state == TransactionState.COMMITTED
|
@@ -92,7 +97,8 @@ class Transaction(ContextModel):
|
|
92
97
|
self._token = self.__var__.set(self)
|
93
98
|
return self
|
94
99
|
|
95
|
-
def __exit__(self,
|
100
|
+
def __exit__(self, *exc_info):
|
101
|
+
exc_type, exc_val, _ = exc_info
|
96
102
|
if not self._token:
|
97
103
|
raise RuntimeError(
|
98
104
|
"Asymmetric use of context. Context exit called without an enter."
|
@@ -123,11 +129,19 @@ class Transaction(ContextModel):
|
|
123
129
|
def begin(self):
|
124
130
|
# currently we only support READ_COMMITTED isolation
|
125
131
|
# i.e., no locking behavior
|
126
|
-
if
|
132
|
+
if (
|
133
|
+
not self.overwrite
|
134
|
+
and self.store
|
135
|
+
and self.key
|
136
|
+
and self.store.exists(key=self.key)
|
137
|
+
):
|
127
138
|
self.state = TransactionState.COMMITTED
|
128
139
|
|
129
|
-
def read(self) ->
|
130
|
-
|
140
|
+
def read(self) -> BaseResult:
|
141
|
+
if self.store and self.key:
|
142
|
+
return self.store.read(key=self.key)
|
143
|
+
else:
|
144
|
+
return {} # TODO: Determine what this should be
|
131
145
|
|
132
146
|
def reset(self) -> None:
|
133
147
|
parent = self.get_parent()
|
@@ -136,8 +150,9 @@ class Transaction(ContextModel):
|
|
136
150
|
# parent takes responsibility
|
137
151
|
parent.add_child(self)
|
138
152
|
|
139
|
-
self.
|
140
|
-
|
153
|
+
if self._token:
|
154
|
+
self.__var__.reset(self._token)
|
155
|
+
self._token = None
|
141
156
|
|
142
157
|
# do this below reset so that get_transaction() returns the relevant txn
|
143
158
|
if parent and self.state == TransactionState.ROLLED_BACK:
|
@@ -165,7 +180,7 @@ class Transaction(ContextModel):
|
|
165
180
|
for hook in self.on_commit_hooks:
|
166
181
|
hook(self)
|
167
182
|
|
168
|
-
if self.store:
|
183
|
+
if self.store and self.key:
|
169
184
|
self.store.write(key=self.key, value=self._staged_value)
|
170
185
|
self.state = TransactionState.COMMITTED
|
171
186
|
return True
|
@@ -174,11 +189,17 @@ class Transaction(ContextModel):
|
|
174
189
|
return False
|
175
190
|
|
176
191
|
def stage(
|
177
|
-
self,
|
192
|
+
self,
|
193
|
+
value: BaseResult,
|
194
|
+
on_rollback_hooks: Optional[List] = None,
|
195
|
+
on_commit_hooks: Optional[List] = None,
|
178
196
|
) -> None:
|
179
197
|
"""
|
180
198
|
Stage a value to be committed later.
|
181
199
|
"""
|
200
|
+
on_commit_hooks = on_commit_hooks or []
|
201
|
+
on_rollback_hooks = on_rollback_hooks or []
|
202
|
+
|
182
203
|
if self.state != TransactionState.COMMITTED:
|
183
204
|
self._staged_value = value
|
184
205
|
self.on_rollback_hooks += on_rollback_hooks
|
@@ -203,11 +224,11 @@ class Transaction(ContextModel):
|
|
203
224
|
return False
|
204
225
|
|
205
226
|
@classmethod
|
206
|
-
def get_active(cls: Type[
|
227
|
+
def get_active(cls: Type[Self]) -> Optional[Self]:
|
207
228
|
return cls.__var__.get(None)
|
208
229
|
|
209
230
|
|
210
|
-
def get_transaction() -> Transaction:
|
231
|
+
def get_transaction() -> Optional[Transaction]:
|
211
232
|
return Transaction.get_active()
|
212
233
|
|
213
234
|
|
@@ -218,6 +239,55 @@ def transaction(
|
|
218
239
|
commit_mode: CommitMode = CommitMode.LAZY,
|
219
240
|
overwrite: bool = False,
|
220
241
|
) -> Generator[Transaction, None, None]:
|
242
|
+
"""
|
243
|
+
A context manager for opening and managing a transaction.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
- key: An identifier to use for the transaction
|
247
|
+
- store: The store to use for persisting the transaction result. If not provided,
|
248
|
+
a default store will be used based on the current run context.
|
249
|
+
- commit_mode: The commit mode controlling when the transaction and
|
250
|
+
child transactions are committed
|
251
|
+
- overwrite: Whether to overwrite an existing transaction record in the store
|
252
|
+
|
253
|
+
Yields:
|
254
|
+
- Transaction: An object representing the transaction state
|
255
|
+
"""
|
256
|
+
# if there is no key, we won't persist a record
|
257
|
+
if key and not store:
|
258
|
+
flow_run_context = FlowRunContext.get()
|
259
|
+
task_run_context = TaskRunContext.get()
|
260
|
+
existing_factory = getattr(task_run_context, "result_factory", None) or getattr(
|
261
|
+
flow_run_context, "result_factory", None
|
262
|
+
)
|
263
|
+
|
264
|
+
if existing_factory and existing_factory.storage_block_id:
|
265
|
+
new_factory = existing_factory.model_copy(
|
266
|
+
update={
|
267
|
+
"persist_result": True,
|
268
|
+
}
|
269
|
+
)
|
270
|
+
else:
|
271
|
+
default_storage = get_or_create_default_result_storage(_sync=True)
|
272
|
+
if existing_factory:
|
273
|
+
new_factory = existing_factory.model_copy(
|
274
|
+
update={
|
275
|
+
"persist_result": True,
|
276
|
+
"storage_block": default_storage,
|
277
|
+
"storage_block_id": default_storage._block_document_id,
|
278
|
+
}
|
279
|
+
)
|
280
|
+
else:
|
281
|
+
new_factory = run_coro_as_sync(
|
282
|
+
ResultFactory.default_factory(
|
283
|
+
persist_result=True,
|
284
|
+
result_storage=default_storage,
|
285
|
+
)
|
286
|
+
)
|
287
|
+
store = ResultFactoryStore(
|
288
|
+
result_factory=new_factory,
|
289
|
+
)
|
290
|
+
|
221
291
|
with Transaction(
|
222
292
|
key=key, store=store, commit_mode=commit_mode, overwrite=overwrite
|
223
293
|
) as txn:
|
prefect/types/__init__.py
CHANGED
@@ -15,12 +15,19 @@ from zoneinfo import available_timezones
|
|
15
15
|
MAX_VARIABLE_NAME_LENGTH = 255
|
16
16
|
MAX_VARIABLE_VALUE_LENGTH = 5000
|
17
17
|
|
18
|
-
timezone_set = available_timezones()
|
19
|
-
|
20
18
|
NonNegativeInteger = Annotated[int, Field(ge=0)]
|
21
19
|
PositiveInteger = Annotated[int, Field(gt=0)]
|
22
20
|
NonNegativeFloat = Annotated[float, Field(ge=0.0)]
|
23
|
-
|
21
|
+
|
22
|
+
TimeZone = Annotated[
|
23
|
+
str,
|
24
|
+
Field(
|
25
|
+
default="UTC",
|
26
|
+
pattern="|".join(
|
27
|
+
[z for z in sorted(available_timezones()) if "localtime" not in z]
|
28
|
+
),
|
29
|
+
),
|
30
|
+
]
|
24
31
|
|
25
32
|
|
26
33
|
BANNED_CHARACTERS = ["/", "%", "&", ">", "<"]
|
prefect/utilities/asyncutils.py
CHANGED
@@ -314,7 +314,7 @@ def sync_compatible(async_fn: T, force_sync: bool = False) -> T:
|
|
314
314
|
"""
|
315
315
|
|
316
316
|
@wraps(async_fn)
|
317
|
-
def coroutine_wrapper(*args, _sync: bool = None, **kwargs):
|
317
|
+
def coroutine_wrapper(*args, _sync: Optional[bool] = None, **kwargs):
|
318
318
|
from prefect.context import MissingContextError, get_run_context
|
319
319
|
from prefect.settings import (
|
320
320
|
PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT,
|
@@ -376,8 +376,8 @@ def sync_compatible(async_fn: T, force_sync: bool = False) -> T:
|
|
376
376
|
|
377
377
|
|
378
378
|
@asynccontextmanager
|
379
|
-
async def asyncnullcontext():
|
380
|
-
yield
|
379
|
+
async def asyncnullcontext(value=None):
|
380
|
+
yield value
|
381
381
|
|
382
382
|
|
383
383
|
def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T:
|
prefect/utilities/callables.py
CHANGED
@@ -44,11 +44,23 @@ def get_call_parameters(
|
|
44
44
|
apply_defaults: bool = True,
|
45
45
|
) -> Dict[str, Any]:
|
46
46
|
"""
|
47
|
-
Bind a call to a function to get parameter/value mapping. Default values on
|
48
|
-
signature will be included if not overridden.
|
49
|
-
|
50
|
-
|
47
|
+
Bind a call to a function to get parameter/value mapping. Default values on
|
48
|
+
the signature will be included if not overridden.
|
49
|
+
|
50
|
+
If the function has a `__prefect_self__` attribute, it will be included as
|
51
|
+
the first parameter. This attribute is set when Prefect decorates a bound
|
52
|
+
method, so this approach allows Prefect to work with bound methods in a way
|
53
|
+
that is consistent with how Python handles them (i.e. users don't have to
|
54
|
+
pass the instance argument to the method) while still making the implicit self
|
55
|
+
argument visible to all of Prefect's parameter machinery (such as cache key
|
56
|
+
functions).
|
57
|
+
|
58
|
+
Raises a ParameterBindError if the arguments/kwargs are not valid for the
|
59
|
+
function
|
51
60
|
"""
|
61
|
+
if hasattr(fn, "__prefect_self__"):
|
62
|
+
call_args = (fn.__prefect_self__,) + call_args
|
63
|
+
|
52
64
|
try:
|
53
65
|
bound_signature = inspect.signature(fn).bind(*call_args, **call_kwargs)
|
54
66
|
except TypeError as exc:
|
prefect/utilities/collections.py
CHANGED
@@ -4,6 +4,7 @@ Utilities for extensions of and operations on Python collections.
|
|
4
4
|
|
5
5
|
import io
|
6
6
|
import itertools
|
7
|
+
import types
|
7
8
|
import warnings
|
8
9
|
from collections import OrderedDict, defaultdict
|
9
10
|
from collections.abc import Iterator as IteratorABC
|
@@ -220,25 +221,31 @@ class StopVisiting(BaseException):
|
|
220
221
|
|
221
222
|
|
222
223
|
def visit_collection(
|
223
|
-
expr,
|
224
|
-
visit_fn: Union[Callable[[Any, dict], Any], Callable[[Any], Any]],
|
224
|
+
expr: Any,
|
225
|
+
visit_fn: Union[Callable[[Any, Optional[dict]], Any], Callable[[Any], Any]],
|
225
226
|
return_data: bool = False,
|
226
227
|
max_depth: int = -1,
|
227
228
|
context: Optional[dict] = None,
|
228
229
|
remove_annotations: bool = False,
|
229
|
-
|
230
|
+
_seen: Optional[Set[int]] = None,
|
231
|
+
) -> Any:
|
230
232
|
"""
|
231
|
-
|
232
|
-
is a Python collection, it will be visited recursively. If an element is not a
|
233
|
-
collection, `visit_fn` will be called with the element. The return value of
|
234
|
-
`visit_fn` can be used to alter the element if `return_data` is set.
|
233
|
+
Visits and potentially transforms every element of an arbitrary Python collection.
|
235
234
|
|
236
|
-
|
237
|
-
|
238
|
-
|
235
|
+
If an element is a Python collection, it will be visited recursively. If an element
|
236
|
+
is not a collection, `visit_fn` will be called with the element. The return value of
|
237
|
+
`visit_fn` can be used to alter the element if `return_data` is set to `True`.
|
238
|
+
|
239
|
+
Note:
|
240
|
+
- When `return_data` is `True`, a copy of each collection is created only if
|
241
|
+
`visit_fn` modifies an element within that collection. This approach minimizes
|
242
|
+
performance penalties by avoiding unnecessary copying.
|
243
|
+
- When `return_data` is `False`, no copies are created, and only side effects from
|
244
|
+
`visit_fn` are applied. This mode is faster and should be used when no transformation
|
245
|
+
of the collection is required, because it never has to copy any data.
|
239
246
|
|
240
247
|
Supported types:
|
241
|
-
- List
|
248
|
+
- List (including iterators)
|
242
249
|
- Tuple
|
243
250
|
- Set
|
244
251
|
- Dict (note: keys are also visited recursively)
|
@@ -246,32 +253,41 @@ def visit_collection(
|
|
246
253
|
- Pydantic model
|
247
254
|
- Prefect annotations
|
248
255
|
|
256
|
+
Note that visit_collection will not consume generators or async generators, as it would prevent
|
257
|
+
the caller from iterating over them.
|
258
|
+
|
249
259
|
Args:
|
250
|
-
expr (Any):
|
251
|
-
visit_fn (Callable[[Any, Optional[dict]],
|
252
|
-
will be applied to every non-collection element of expr
|
253
|
-
accept one or two arguments. If two arguments are accepted, the second
|
254
|
-
|
255
|
-
return_data (bool):
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
descend to N layers deep. If set to any negative integer, no limit will be
|
260
|
+
expr (Any): A Python object or expression.
|
261
|
+
visit_fn (Callable[[Any, Optional[dict]], Any] or Callable[[Any], Any]): A function
|
262
|
+
that will be applied to every non-collection element of `expr`. The function can
|
263
|
+
accept one or two arguments. If two arguments are accepted, the second argument
|
264
|
+
will be the context dictionary.
|
265
|
+
return_data (bool): If `True`, a copy of `expr` containing data modified by `visit_fn`
|
266
|
+
will be returned. This is slower than `return_data=False` (the default).
|
267
|
+
max_depth (int): Controls the depth of recursive visitation. If set to zero, no
|
268
|
+
recursion will occur. If set to a positive integer `N`, visitation will only
|
269
|
+
descend to `N` layers deep. If set to any negative integer, no limit will be
|
261
270
|
enforced and recursion will continue until terminal items are reached. By
|
262
271
|
default, recursion is unlimited.
|
263
|
-
context: An optional dictionary. If passed, the context will be sent
|
264
|
-
call to the `visit_fn`. The context can be mutated by each visitor and
|
265
|
-
be available for later visits to expressions at the given depth. Values
|
272
|
+
context (Optional[dict]): An optional dictionary. If passed, the context will be sent
|
273
|
+
to each call to the `visit_fn`. The context can be mutated by each visitor and
|
274
|
+
will be available for later visits to expressions at the given depth. Values
|
266
275
|
will not be available "up" a level from a given expression.
|
267
|
-
|
268
276
|
The context will be automatically populated with an 'annotation' key when
|
269
|
-
visiting collections within a `BaseAnnotation` type. This requires the
|
270
|
-
|
271
|
-
remove_annotations: If set, annotations will be replaced by their contents. By
|
277
|
+
visiting collections within a `BaseAnnotation` type. This requires the caller to
|
278
|
+
pass `context={}` and will not be activated by default.
|
279
|
+
remove_annotations (bool): If set, annotations will be replaced by their contents. By
|
272
280
|
default, annotations are preserved but their contents are visited.
|
281
|
+
_seen (Optional[Set[int]]): A set of object ids that have already been visited. This
|
282
|
+
prevents infinite recursion when visiting recursive data structures.
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
Any: The modified collection if `return_data` is `True`, otherwise `None`.
|
273
286
|
"""
|
274
287
|
|
288
|
+
if _seen is None:
|
289
|
+
_seen = set()
|
290
|
+
|
275
291
|
def visit_nested(expr):
|
276
292
|
# Utility for a recursive call, preserving options and updating the depth.
|
277
293
|
return visit_collection(
|
@@ -282,6 +298,7 @@ def visit_collection(
|
|
282
298
|
max_depth=max_depth - 1,
|
283
299
|
# Copy the context on nested calls so it does not "propagate up"
|
284
300
|
context=context.copy() if context is not None else None,
|
301
|
+
_seen=_seen,
|
285
302
|
)
|
286
303
|
|
287
304
|
def visit_expression(expr):
|
@@ -290,7 +307,7 @@ def visit_collection(
|
|
290
307
|
else:
|
291
308
|
return visit_fn(expr)
|
292
309
|
|
293
|
-
# Visit every expression
|
310
|
+
# --- 1. Visit every expression
|
294
311
|
try:
|
295
312
|
result = visit_expression(expr)
|
296
313
|
except StopVisiting:
|
@@ -298,47 +315,92 @@ def visit_collection(
|
|
298
315
|
result = expr
|
299
316
|
|
300
317
|
if return_data:
|
301
|
-
# Only mutate the expression
|
318
|
+
# Only mutate the root expression if the user indicated we're returning data,
|
319
|
+
# otherwise the function could return null and we have no collection to check
|
302
320
|
expr = result
|
303
321
|
|
304
|
-
#
|
322
|
+
# --- 2. Visit every child of the expression recursively
|
305
323
|
|
306
|
-
# If we have reached the maximum depth
|
307
|
-
if
|
324
|
+
# If we have reached the maximum depth or we have already visited this object,
|
325
|
+
# return the result if we are returning data, otherwise return None
|
326
|
+
if max_depth == 0 or id(expr) in _seen:
|
308
327
|
return result if return_data else None
|
328
|
+
else:
|
329
|
+
_seen.add(id(expr))
|
309
330
|
|
310
331
|
# Get the expression type; treat iterators like lists
|
311
332
|
typ = list if isinstance(expr, IteratorABC) and isiterable(expr) else type(expr)
|
312
333
|
typ = cast(type, typ) # mypy treats this as 'object' otherwise and complains
|
313
334
|
|
314
335
|
# Then visit every item in the expression if it is a collection
|
315
|
-
|
336
|
+
|
337
|
+
# presume that the result is the original expression.
|
338
|
+
# in each of the following cases, we will update the result if we need to.
|
339
|
+
result = expr
|
340
|
+
|
341
|
+
# --- Generators
|
342
|
+
|
343
|
+
if isinstance(expr, (types.GeneratorType, types.AsyncGeneratorType)):
|
344
|
+
# Do not attempt to iterate over generators, as it will exhaust them
|
345
|
+
pass
|
346
|
+
|
347
|
+
# --- Mocks
|
348
|
+
|
349
|
+
elif isinstance(expr, Mock):
|
316
350
|
# Do not attempt to recurse into mock objects
|
317
|
-
|
351
|
+
pass
|
352
|
+
|
353
|
+
# --- Annotations (unmapped, quote, etc.)
|
318
354
|
|
319
355
|
elif isinstance(expr, BaseAnnotation):
|
320
356
|
if context is not None:
|
321
357
|
context["annotation"] = expr
|
322
|
-
|
358
|
+
unwrapped = expr.unwrap()
|
359
|
+
value = visit_nested(unwrapped)
|
323
360
|
|
324
|
-
if
|
325
|
-
|
326
|
-
|
327
|
-
|
361
|
+
if return_data:
|
362
|
+
# if we are removing annotations, return the value
|
363
|
+
if remove_annotations:
|
364
|
+
result = value
|
365
|
+
# if the value was modified, rewrap it
|
366
|
+
elif value is not unwrapped:
|
367
|
+
result = expr.rewrap(value)
|
368
|
+
# otherwise return the expr
|
369
|
+
|
370
|
+
# --- Sequences
|
328
371
|
|
329
372
|
elif typ in (list, tuple, set):
|
330
373
|
items = [visit_nested(o) for o in expr]
|
331
|
-
|
374
|
+
if return_data:
|
375
|
+
modified = any(item is not orig for item, orig in zip(items, expr))
|
376
|
+
if modified:
|
377
|
+
result = typ(items)
|
378
|
+
|
379
|
+
# --- Dictionaries
|
332
380
|
|
333
381
|
elif typ in (dict, OrderedDict):
|
334
382
|
assert isinstance(expr, (dict, OrderedDict)) # typecheck assertion
|
335
383
|
items = [(visit_nested(k), visit_nested(v)) for k, v in expr.items()]
|
336
|
-
|
384
|
+
if return_data:
|
385
|
+
modified = any(
|
386
|
+
k1 is not k2 or v1 is not v2
|
387
|
+
for (k1, v1), (k2, v2) in zip(items, expr.items())
|
388
|
+
)
|
389
|
+
if modified:
|
390
|
+
result = typ(items)
|
391
|
+
|
392
|
+
# --- Dataclasses
|
337
393
|
|
338
394
|
elif is_dataclass(expr) and not isinstance(expr, type):
|
339
395
|
values = [visit_nested(getattr(expr, f.name)) for f in fields(expr)]
|
340
|
-
|
341
|
-
|
396
|
+
if return_data:
|
397
|
+
modified = any(
|
398
|
+
getattr(expr, f.name) is not v for f, v in zip(fields(expr), values)
|
399
|
+
)
|
400
|
+
if modified:
|
401
|
+
result = typ(**{f.name: v for f, v in zip(fields(expr), values)})
|
402
|
+
|
403
|
+
# --- Pydantic models
|
342
404
|
|
343
405
|
elif isinstance(expr, pydantic.BaseModel):
|
344
406
|
typ = cast(Type[pydantic.BaseModel], typ)
|
@@ -355,20 +417,21 @@ def visit_collection(
|
|
355
417
|
}
|
356
418
|
|
357
419
|
if return_data:
|
358
|
-
|
359
|
-
|
360
|
-
|
420
|
+
modified = any(
|
421
|
+
getattr(expr, field) is not updated_data[field]
|
422
|
+
for field in model_fields
|
361
423
|
)
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
424
|
+
if modified:
|
425
|
+
# Use construct to avoid validation and handle immutability
|
426
|
+
model_instance = typ.model_construct(
|
427
|
+
_fields_set=expr.model_fields_set, **updated_data
|
428
|
+
)
|
429
|
+
for private_attr in expr.__private_attributes__:
|
430
|
+
setattr(model_instance, private_attr, getattr(expr, private_attr))
|
431
|
+
result = model_instance
|
367
432
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
return result
|
433
|
+
if return_data:
|
434
|
+
return result
|
372
435
|
|
373
436
|
|
374
437
|
def remove_nested_keys(keys_to_remove: List[Hashable], obj):
|
prefect/utilities/dockerutils.py
CHANGED
@@ -41,7 +41,9 @@ def python_version_micro() -> str:
|
|
41
41
|
|
42
42
|
|
43
43
|
def get_prefect_image_name(
|
44
|
-
prefect_version: str = None,
|
44
|
+
prefect_version: Optional[str] = None,
|
45
|
+
python_version: Optional[str] = None,
|
46
|
+
flavor: Optional[str] = None,
|
45
47
|
) -> str:
|
46
48
|
"""
|
47
49
|
Get the Prefect image name matching the current Prefect and Python versions.
|
@@ -138,7 +140,7 @@ def build_image(
|
|
138
140
|
dockerfile: str = "Dockerfile",
|
139
141
|
tag: Optional[str] = None,
|
140
142
|
pull: bool = False,
|
141
|
-
platform: str = None,
|
143
|
+
platform: Optional[str] = None,
|
142
144
|
stream_progress_to: Optional[TextIO] = None,
|
143
145
|
**kwargs,
|
144
146
|
) -> str:
|
@@ -209,7 +211,7 @@ class ImageBuilder:
|
|
209
211
|
self,
|
210
212
|
base_image: str,
|
211
213
|
base_directory: Path = None,
|
212
|
-
platform: str = None,
|
214
|
+
platform: Optional[str] = None,
|
213
215
|
context: Path = None,
|
214
216
|
):
|
215
217
|
"""Create an ImageBuilder
|
prefect/utilities/engine.py
CHANGED
@@ -786,6 +786,17 @@ def resolve_to_final_result(expr, context):
|
|
786
786
|
raise StopVisiting()
|
787
787
|
|
788
788
|
if isinstance(expr, NewPrefectFuture):
|
789
|
+
upstream_task_run = context.get("current_task_run")
|
790
|
+
upstream_task = context.get("current_task")
|
791
|
+
if (
|
792
|
+
upstream_task
|
793
|
+
and upstream_task_run
|
794
|
+
and expr.task_run_id == upstream_task_run.id
|
795
|
+
):
|
796
|
+
raise ValueError(
|
797
|
+
f"Discovered a task depending on itself. Raising to avoid a deadlock. Please inspect the inputs and dependencies of {upstream_task.name}."
|
798
|
+
)
|
799
|
+
|
789
800
|
expr.wait()
|
790
801
|
state = expr.state
|
791
802
|
elif isinstance(expr, State):
|
prefect/utilities/filesystem.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
Utilities for working with file systems
|
3
3
|
"""
|
4
|
+
|
4
5
|
import os
|
5
6
|
import pathlib
|
6
7
|
import threading
|
7
8
|
from contextlib import contextmanager
|
8
9
|
from pathlib import Path, PureWindowsPath
|
9
|
-
from typing import Union
|
10
|
+
from typing import Optional, Union
|
10
11
|
|
11
12
|
import fsspec
|
12
13
|
import pathspec
|
@@ -32,7 +33,7 @@ def create_default_ignore_file(path: str) -> bool:
|
|
32
33
|
|
33
34
|
|
34
35
|
def filter_files(
|
35
|
-
root: str = ".", ignore_patterns: list = None, include_dirs: bool = True
|
36
|
+
root: str = ".", ignore_patterns: Optional[list] = None, include_dirs: bool = True
|
36
37
|
) -> set:
|
37
38
|
"""
|
38
39
|
This function accepts a root directory path and a list of file patterns to ignore, and returns
|
@@ -40,9 +41,7 @@ def filter_files(
|
|
40
41
|
|
41
42
|
The specification matches that of [.gitignore files](https://git-scm.com/docs/gitignore).
|
42
43
|
"""
|
43
|
-
|
44
|
-
ignore_patterns = []
|
45
|
-
spec = pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns)
|
44
|
+
spec = pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns or [])
|
46
45
|
ignored_files = {p.path for p in spec.match_tree_entries(root)}
|
47
46
|
if include_dirs:
|
48
47
|
all_files = {p.path for p in pathspec.util.iter_tree_entries(root)}
|
prefect/utilities/importtools.py
CHANGED
@@ -380,6 +380,15 @@ def safe_load_namespace(source_code: str):
|
|
380
380
|
|
381
381
|
namespace = {"__name__": "prefect_safe_namespace_loader"}
|
382
382
|
|
383
|
+
# Remove the body of the if __name__ == "__main__": block from the AST to prevent
|
384
|
+
# execution of guarded code
|
385
|
+
new_body = []
|
386
|
+
for node in parsed_code.body:
|
387
|
+
if _is_main_block(node):
|
388
|
+
continue
|
389
|
+
new_body.append(node)
|
390
|
+
parsed_code.body = new_body
|
391
|
+
|
383
392
|
# Walk through the AST and find all import statements
|
384
393
|
for node in ast.walk(parsed_code):
|
385
394
|
if isinstance(node, ast.Import):
|
@@ -426,3 +435,23 @@ def safe_load_namespace(source_code: str):
|
|
426
435
|
except Exception as e:
|
427
436
|
logger.debug("Failed to compile: %s", e)
|
428
437
|
return namespace
|
438
|
+
|
439
|
+
|
440
|
+
def _is_main_block(node: ast.AST):
|
441
|
+
"""
|
442
|
+
Check if the node is an `if __name__ == "__main__":` block.
|
443
|
+
"""
|
444
|
+
if isinstance(node, ast.If):
|
445
|
+
try:
|
446
|
+
# Check if the condition is `if __name__ == "__main__":`
|
447
|
+
if (
|
448
|
+
isinstance(node.test, ast.Compare)
|
449
|
+
and isinstance(node.test.left, ast.Name)
|
450
|
+
and node.test.left.id == "__name__"
|
451
|
+
and isinstance(node.test.comparators[0], ast.Constant)
|
452
|
+
and node.test.comparators[0].value == "__main__"
|
453
|
+
):
|
454
|
+
return True
|
455
|
+
except AttributeError:
|
456
|
+
pass
|
457
|
+
return False
|
prefect/utilities/services.py
CHANGED
@@ -2,7 +2,7 @@ import sys
|
|
2
2
|
from collections import deque
|
3
3
|
from traceback import format_exception
|
4
4
|
from types import TracebackType
|
5
|
-
from typing import Callable, Coroutine, Deque, Tuple
|
5
|
+
from typing import Callable, Coroutine, Deque, Optional, Tuple
|
6
6
|
|
7
7
|
import anyio
|
8
8
|
import httpx
|
@@ -22,7 +22,7 @@ async def critical_service_loop(
|
|
22
22
|
backoff: int = 1,
|
23
23
|
printer: Callable[..., None] = print,
|
24
24
|
run_once: bool = False,
|
25
|
-
jitter_range: float = None,
|
25
|
+
jitter_range: Optional[float] = None,
|
26
26
|
):
|
27
27
|
"""
|
28
28
|
Runs the given `workload` function on the specified `interval`, while being
|