prefect-client 2.18.2__py3-none-any.whl → 2.19.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 (41) hide show
  1. prefect/__init__.py +1 -15
  2. prefect/_internal/concurrency/cancellation.py +2 -0
  3. prefect/_internal/schemas/validators.py +10 -0
  4. prefect/_vendor/starlette/testclient.py +1 -1
  5. prefect/blocks/notifications.py +6 -6
  6. prefect/client/base.py +244 -1
  7. prefect/client/cloud.py +4 -2
  8. prefect/client/orchestration.py +515 -106
  9. prefect/client/schemas/actions.py +58 -8
  10. prefect/client/schemas/objects.py +15 -1
  11. prefect/client/schemas/responses.py +19 -0
  12. prefect/client/schemas/schedules.py +1 -1
  13. prefect/client/utilities.py +2 -2
  14. prefect/concurrency/asyncio.py +34 -4
  15. prefect/concurrency/sync.py +40 -6
  16. prefect/context.py +2 -2
  17. prefect/engine.py +17 -1
  18. prefect/events/clients.py +2 -2
  19. prefect/flows.py +91 -17
  20. prefect/infrastructure/process.py +0 -17
  21. prefect/logging/formatters.py +1 -4
  22. prefect/new_flow_engine.py +166 -161
  23. prefect/new_task_engine.py +137 -202
  24. prefect/runner/__init__.py +1 -1
  25. prefect/runner/runner.py +2 -107
  26. prefect/settings.py +11 -0
  27. prefect/tasks.py +76 -57
  28. prefect/types/__init__.py +27 -5
  29. prefect/utilities/annotations.py +1 -8
  30. prefect/utilities/asyncutils.py +4 -0
  31. prefect/utilities/engine.py +106 -1
  32. prefect/utilities/schema_tools/__init__.py +6 -1
  33. prefect/utilities/schema_tools/validation.py +25 -8
  34. prefect/utilities/timeout.py +34 -0
  35. prefect/workers/base.py +7 -3
  36. prefect/workers/process.py +0 -17
  37. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/METADATA +1 -1
  38. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/RECORD +41 -40
  39. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/LICENSE +0 -0
  40. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/WHEEL +0 -0
  41. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,14 @@
1
- import asyncio
2
1
  import inspect
3
2
  import logging
4
- from contextlib import asynccontextmanager, contextmanager
3
+ import time
4
+ from contextlib import contextmanager
5
5
  from dataclasses import dataclass, field
6
6
  from typing import (
7
7
  Any,
8
8
  Callable,
9
9
  Coroutine,
10
10
  Dict,
11
+ Generator,
11
12
  Generic,
12
13
  Iterable,
13
14
  Literal,
@@ -16,20 +17,13 @@ from typing import (
16
17
  Union,
17
18
  cast,
18
19
  )
19
- from uuid import uuid4
20
20
 
21
21
  import pendulum
22
22
  from typing_extensions import ParamSpec
23
23
 
24
24
  from prefect import Task, get_client
25
- from prefect._internal.concurrency.cancellation import (
26
- AlarmCancelScope,
27
- AsyncCancelScope,
28
- CancelledError,
29
- )
30
- from prefect.client.orchestration import PrefectClient
25
+ from prefect.client.orchestration import SyncPrefectClient
31
26
  from prefect.client.schemas import TaskRun
32
- from prefect.client.schemas.objects import TaskRunResult
33
27
  from prefect.context import FlowRunContext, TaskRunContext
34
28
  from prefect.futures import PrefectFuture, resolve_futures_to_states
35
29
  from prefect.logging.loggers import get_logger, task_run_logger
@@ -37,7 +31,6 @@ from prefect.results import ResultFactory
37
31
  from prefect.server.schemas.states import State
38
32
  from prefect.settings import PREFECT_TASKS_REFRESH_CACHE
39
33
  from prefect.states import (
40
- Pending,
41
34
  Retrying,
42
35
  Running,
43
36
  StateDetails,
@@ -45,38 +38,18 @@ from prefect.states import (
45
38
  exception_to_failed_state,
46
39
  return_value_to_state,
47
40
  )
48
- from prefect.utilities.asyncutils import A, Async, is_async_fn, run_sync
41
+ from prefect.utilities.asyncutils import A, Async, run_sync
49
42
  from prefect.utilities.callables import parameters_to_args_kwargs
50
43
  from prefect.utilities.engine import (
51
- _dynamic_key_for_task_run,
52
44
  _get_hook_name,
53
- _resolve_custom_task_run_name,
54
- collect_task_run_inputs,
55
- propose_state,
45
+ propose_state_sync,
56
46
  )
47
+ from prefect.utilities.timeout import timeout, timeout_async
57
48
 
58
49
  P = ParamSpec("P")
59
50
  R = TypeVar("R")
60
51
 
61
52
 
62
- @asynccontextmanager
63
- async def timeout(seconds: Optional[float] = None):
64
- try:
65
- with AsyncCancelScope(timeout=seconds):
66
- yield
67
- except CancelledError:
68
- raise TimeoutError(f"Task timed out after {seconds} second(s).")
69
-
70
-
71
- @contextmanager
72
- def timeout_sync(seconds: Optional[float] = None):
73
- try:
74
- with AlarmCancelScope(timeout=seconds):
75
- yield
76
- except CancelledError:
77
- raise TimeoutError(f"Task timed out after {seconds} second(s).")
78
-
79
-
80
53
  @dataclass
81
54
  class TaskRunEngine(Generic[P, R]):
82
55
  task: Union[Task[P, R], Task[P, Coroutine[Any, Any, R]]]
@@ -85,14 +58,14 @@ class TaskRunEngine(Generic[P, R]):
85
58
  task_run: Optional[TaskRun] = None
86
59
  retries: int = 0
87
60
  _is_started: bool = False
88
- _client: Optional[PrefectClient] = None
61
+ _client: Optional[SyncPrefectClient] = None
89
62
 
90
63
  def __post_init__(self):
91
64
  if self.parameters is None:
92
65
  self.parameters = {}
93
66
 
94
67
  @property
95
- def client(self) -> PrefectClient:
68
+ def client(self) -> SyncPrefectClient:
96
69
  if not self._is_started or self._client is None:
97
70
  raise RuntimeError("Engine has not started.")
98
71
  return self._client
@@ -114,10 +87,7 @@ class TaskRunEngine(Generic[P, R]):
114
87
  self.task, self.task_run, self.state
115
88
  )
116
89
 
117
- async def _run_hooks(self, state: State) -> None:
118
- """Run the on_failure and on_completion hooks for a task, making sure to
119
- catch and log any errors that occur.
120
- """
90
+ def get_hooks(self, state: State, as_async: bool = False) -> Iterable[Callable]:
121
91
  task = self.task
122
92
  task_run = self.task_run
123
93
 
@@ -130,18 +100,17 @@ class TaskRunEngine(Generic[P, R]):
130
100
  elif state.is_completed() and task.on_completion:
131
101
  hooks = task.on_completion
132
102
 
133
- if hooks:
134
- for hook in hooks:
135
- hook_name = _get_hook_name(hook)
103
+ for hook in hooks or []:
104
+ hook_name = _get_hook_name(hook)
105
+
106
+ @contextmanager
107
+ def hook_context():
136
108
  try:
137
109
  self.logger.info(
138
110
  f"Running hook {hook_name!r} in response to entering state"
139
111
  f" {state.name!r}"
140
112
  )
141
- if is_async_fn(hook):
142
- await hook(task, task_run, state)
143
- else:
144
- hook(task, task_run, state)
113
+ yield
145
114
  except Exception:
146
115
  self.logger.error(
147
116
  f"An error was encountered while running hook {hook_name!r}",
@@ -152,6 +121,21 @@ class TaskRunEngine(Generic[P, R]):
152
121
  f"Hook {hook_name!r} finished running successfully"
153
122
  )
154
123
 
124
+ if as_async:
125
+
126
+ async def _hook_fn():
127
+ with hook_context():
128
+ result = hook(task, task_run, state)
129
+ if inspect.isawaitable(result):
130
+ await result
131
+ else:
132
+
133
+ def _hook_fn():
134
+ with hook_context():
135
+ hook(task, task_run, state)
136
+
137
+ yield _hook_fn
138
+
155
139
  def _compute_state_details(
156
140
  self, include_cache_expiration: bool = False
157
141
  ) -> StateDetails:
@@ -187,152 +171,92 @@ class TaskRunEngine(Generic[P, R]):
187
171
  cache_expiration=cache_expiration,
188
172
  )
189
173
 
190
- async def begin_run(self):
174
+ def begin_run(self):
191
175
  state_details = self._compute_state_details()
192
176
  new_state = Running(state_details=state_details)
193
- state = await self.set_state(new_state)
177
+ state = self.set_state(new_state)
194
178
  while state.is_pending():
195
- await asyncio.sleep(1)
196
- state = await self.set_state(new_state)
179
+ time.sleep(0.2)
180
+ state = self.set_state(new_state)
197
181
 
198
- async def set_state(self, state: State, force: bool = False) -> State:
182
+ def set_state(self, state: State, force: bool = False) -> State:
199
183
  if not self.task_run:
200
184
  raise ValueError("Task run is not set")
201
- new_state = await propose_state(
185
+ new_state = propose_state_sync(
202
186
  self.client, state, task_run_id=self.task_run.id, force=force
203
- ) # type: ignore
187
+ )
188
+ # type: ignore
204
189
 
205
190
  # currently this is a hack to keep a reference to the state object
206
191
  # that has an in-memory result attached to it; using the API state
207
192
  # could result in losing that reference
208
193
  self.task_run.state = new_state
209
- if new_state.is_final():
210
- await self._run_hooks(new_state)
211
194
  return new_state
212
195
 
213
- async def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
196
+ def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
214
197
  _result = self.state.result(raise_on_failure=raise_on_failure, fetch=True)
215
198
  # state.result is a `sync_compatible` function that may or may not return an awaitable
216
199
  # depending on whether the parent frame is sync or not
217
200
  if inspect.isawaitable(_result):
218
- _result = await _result
201
+ _result = run_sync(_result)
219
202
  return _result
220
203
 
221
- async def handle_success(self, result: R) -> R:
204
+ def handle_success(self, result: R) -> R:
222
205
  result_factory = getattr(TaskRunContext.get(), "result_factory", None)
223
206
  if result_factory is None:
224
207
  raise ValueError("Result factory is not set")
225
- terminal_state = await return_value_to_state(
226
- await resolve_futures_to_states(result),
227
- result_factory=result_factory,
208
+ terminal_state = run_sync(
209
+ return_value_to_state(
210
+ run_sync(resolve_futures_to_states(result)),
211
+ result_factory=result_factory,
212
+ )
228
213
  )
229
214
  terminal_state.state_details = self._compute_state_details(
230
215
  include_cache_expiration=True
231
216
  )
232
- await self.set_state(terminal_state)
217
+ self.set_state(terminal_state)
233
218
  return result
234
219
 
235
- async def handle_retry(self, exc: Exception) -> bool:
220
+ def handle_retry(self, exc: Exception) -> bool:
236
221
  """
237
222
  If the task has retries left, and the retry condition is met, set the task to retrying.
238
223
  - If the task has no retries left, or the retry condition is not met, return False.
239
224
  - If the task has retries left, and the retry condition is met, return True.
240
225
  """
241
226
  if self.retries < self.task.retries and self.can_retry:
242
- await self.set_state(Retrying(), force=True)
227
+ self.set_state(Retrying(), force=True)
243
228
  self.retries = self.retries + 1
244
229
  return True
245
230
  return False
246
231
 
247
- async def handle_exception(self, exc: Exception) -> None:
232
+ def handle_exception(self, exc: Exception) -> None:
248
233
  # If the task fails, and we have retries left, set the task to retrying.
249
- if not await self.handle_retry(exc):
234
+ if not self.handle_retry(exc):
250
235
  # If the task has no retries left, or the retry condition is not met, set the task to failed.
251
236
  context = TaskRunContext.get()
252
- state = await exception_to_failed_state(
253
- exc,
254
- message="Task run encountered an exception",
255
- result_factory=getattr(context, "result_factory", None),
237
+ state = run_sync(
238
+ exception_to_failed_state(
239
+ exc,
240
+ message="Task run encountered an exception",
241
+ result_factory=getattr(context, "result_factory", None),
242
+ )
256
243
  )
257
- await self.set_state(state)
244
+ self.set_state(state)
258
245
 
259
- async def handle_crash(self, exc: BaseException) -> None:
260
- state = await exception_to_crashed_state(exc)
246
+ def handle_crash(self, exc: BaseException) -> None:
247
+ state = run_sync(exception_to_crashed_state(exc))
261
248
  self.logger.error(f"Crash detected! {state.message}")
262
249
  self.logger.debug("Crash details:", exc_info=exc)
263
- await self.set_state(state, force=True)
264
-
265
- async def create_task_run(self, client: PrefectClient) -> TaskRun:
266
- flow_run_ctx = FlowRunContext.get()
267
- parameters = self.parameters or {}
268
- try:
269
- task_run_name = _resolve_custom_task_run_name(self.task, parameters)
270
- except TypeError:
271
- task_run_name = None
272
-
273
- # prep input tracking
274
- task_inputs = {
275
- k: await collect_task_run_inputs(v) for k, v in parameters.items()
276
- }
277
-
278
- # anticipate nested runs
279
- task_run_ctx = TaskRunContext.get()
280
- if task_run_ctx:
281
- task_inputs["wait_for"] = [TaskRunResult(id=task_run_ctx.task_run.id)] # type: ignore
282
-
283
- # TODO: implement wait_for
284
- # if wait_for:
285
- # task_inputs["wait_for"] = await collect_task_run_inputs(wait_for)
286
-
287
- if flow_run_ctx:
288
- dynamic_key = _dynamic_key_for_task_run(
289
- context=flow_run_ctx, task=self.task
290
- )
291
- else:
292
- dynamic_key = uuid4().hex
293
- task_run = await client.create_task_run(
294
- task=self.task, # type: ignore
295
- name=task_run_name,
296
- flow_run_id=(
297
- getattr(flow_run_ctx.flow_run, "id", None)
298
- if flow_run_ctx and flow_run_ctx.flow_run
299
- else None
300
- ),
301
- dynamic_key=str(dynamic_key),
302
- state=Pending(),
303
- task_inputs=task_inputs, # type: ignore
304
- )
305
- return task_run
306
-
307
- @asynccontextmanager
308
- async def enter_run_context(self, client: Optional[PrefectClient] = None):
309
- if client is None:
310
- client = self.client
311
-
312
- if not self.task_run:
313
- raise ValueError("Task run is not set")
314
-
315
- self.task_run = await client.read_task_run(self.task_run.id)
316
-
317
- with TaskRunContext(
318
- task=self.task,
319
- log_prints=self.task.log_prints or False,
320
- task_run=self.task_run,
321
- parameters=self.parameters,
322
- result_factory=await ResultFactory.from_autonomous_task(self.task), # type: ignore
323
- client=client,
324
- ):
325
- self.logger = task_run_logger(task_run=self.task_run, task=self.task) # type: ignore
326
- yield
250
+ self.set_state(state, force=True)
327
251
 
328
252
  @contextmanager
329
- def enter_run_context_sync(self, client: Optional[PrefectClient] = None):
253
+ def enter_run_context(self, client: Optional[SyncPrefectClient] = None):
330
254
  if client is None:
331
255
  client = self.client
332
256
  if not self.task_run:
333
257
  raise ValueError("Task run is not set")
334
258
 
335
- self.task_run = run_sync(client.read_task_run(self.task_run.id))
259
+ self.task_run = client.read_task_run(self.task_run.id)
336
260
 
337
261
  with TaskRunContext(
338
262
  task=self.task,
@@ -342,58 +266,45 @@ class TaskRunEngine(Generic[P, R]):
342
266
  result_factory=run_sync(ResultFactory.from_autonomous_task(self.task)), # type: ignore
343
267
  client=client,
344
268
  ):
345
- self.logger = task_run_logger(task_run=self.task_run, task=self.task) # type: ignore
346
- yield
269
+ # set the logger to the task run logger
270
+ current_logger = self.logger
271
+ try:
272
+ self.logger = task_run_logger(task_run=self.task_run, task=self.task) # type: ignore
273
+ yield
274
+ finally:
275
+ self.logger = current_logger
347
276
 
348
- @asynccontextmanager
349
- async def start(self):
277
+ @contextmanager
278
+ def start(self) -> Generator["TaskRunEngine", Any, Any]:
350
279
  """
351
280
  Enters a client context and creates a task run if needed.
352
281
  """
353
- async with get_client() as client:
282
+ with get_client(sync_client=True) as client:
354
283
  self._client = client
355
284
  self._is_started = True
356
285
  try:
357
286
  if not self.task_run:
358
- self.task_run = await self.create_task_run(client)
287
+ self.task_run = run_sync(
288
+ self.task.create_run(
289
+ client=client,
290
+ parameters=self.parameters,
291
+ flow_run_context=FlowRunContext.get(),
292
+ parent_task_run_context=TaskRunContext.get(),
293
+ )
294
+ )
295
+
359
296
  yield self
360
297
  except Exception:
361
298
  # regular exceptions are caught and re-raised to the user
362
299
  raise
363
300
  except BaseException as exc:
364
301
  # BaseExceptions are caught and handled as crashes
365
- await self.handle_crash(exc)
302
+ self.handle_crash(exc)
366
303
  raise
367
304
  finally:
368
305
  self._is_started = False
369
306
  self._client = None
370
307
 
371
- @contextmanager
372
- def start_sync(self):
373
- """
374
- Enters a client context and creates a task run if needed.
375
- """
376
- client = get_client()
377
- run_sync(client.__aenter__())
378
- self._client = client
379
- self._is_started = True
380
- try:
381
- if not self.task_run:
382
- self.task_run = run_sync(self.create_task_run(client))
383
- yield self
384
- except Exception:
385
- # regular exceptions are caught and re-raised to the user
386
- raise
387
- except BaseException as exc:
388
- # BaseExceptions are caught and handled as crashes
389
- run_sync(self.handle_crash(exc))
390
- raise
391
- finally:
392
- # quickly close client
393
- run_sync(client.__aexit__(None, None, None))
394
- self._is_started = False
395
- self._client = None
396
-
397
308
  async def get_client(self):
398
309
  if not self._is_started:
399
310
  raise RuntimeError("Engine has not started.")
@@ -411,78 +322,102 @@ class TaskRunEngine(Generic[P, R]):
411
322
  return getattr(self, "task_run").state.is_pending()
412
323
 
413
324
 
414
- async def run_task(
415
- task: Task[P, Coroutine[Any, Any, R]],
325
+ def run_task_sync(
326
+ task: Task[P, R],
416
327
  task_run: Optional[TaskRun] = None,
417
328
  parameters: Optional[Dict[str, Any]] = None,
418
329
  wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
419
330
  return_type: Literal["state", "result"] = "result",
420
331
  ) -> Union[R, State, None]:
421
- """
422
- Runs a task against the API.
423
-
424
- We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
425
- """
426
332
  engine = TaskRunEngine[P, R](task=task, parameters=parameters, task_run=task_run)
427
333
 
428
334
  # This is a context manager that keeps track of the run of the task run.
429
- async with engine.start() as run:
430
- await run.begin_run()
335
+ with engine.start() as run:
336
+ run.begin_run()
431
337
 
432
338
  while run.is_running():
433
- async with run.enter_run_context():
339
+ with run.enter_run_context():
434
340
  try:
435
341
  # This is where the task is actually run.
436
- async with timeout(seconds=run.task.timeout_seconds):
342
+ with timeout(seconds=run.task.timeout_seconds):
437
343
  call_args, call_kwargs = parameters_to_args_kwargs(
438
344
  task.fn, run.parameters or {}
439
345
  )
440
- result = cast(R, await task.fn(*call_args, **call_kwargs)) # type: ignore
346
+ result = cast(R, task.fn(*call_args, **call_kwargs)) # type: ignore
441
347
 
442
348
  # If the task run is successful, finalize it.
443
- await run.handle_success(result)
444
- if return_type == "result":
445
- return result
349
+ run.handle_success(result)
446
350
 
447
351
  except Exception as exc:
448
- await run.handle_exception(exc)
352
+ run.handle_exception(exc)
353
+
354
+ if run.state.is_final():
355
+ for hook in run.get_hooks(run.state):
356
+ hook()
449
357
 
450
358
  if return_type == "state":
451
359
  return run.state
452
- return await run.result()
360
+ return run.result()
453
361
 
454
362
 
455
- def run_task_sync(
456
- task: Task[P, R],
363
+ async def run_task_async(
364
+ task: Task[P, Coroutine[Any, Any, R]],
457
365
  task_run: Optional[TaskRun] = None,
458
366
  parameters: Optional[Dict[str, Any]] = None,
459
367
  wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
460
368
  return_type: Literal["state", "result"] = "result",
461
369
  ) -> Union[R, State, None]:
370
+ """
371
+ Runs a task against the API.
372
+
373
+ We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
374
+ """
462
375
  engine = TaskRunEngine[P, R](task=task, parameters=parameters, task_run=task_run)
463
376
 
464
377
  # This is a context manager that keeps track of the run of the task run.
465
- with engine.start_sync() as run:
466
- run_sync(run.begin_run())
378
+ with engine.start() as run:
379
+ run.begin_run()
467
380
 
468
381
  while run.is_running():
469
- with run.enter_run_context_sync():
382
+ with run.enter_run_context():
470
383
  try:
471
384
  # This is where the task is actually run.
472
- with timeout_sync(seconds=run.task.timeout_seconds):
385
+ with timeout_async(seconds=run.task.timeout_seconds):
473
386
  call_args, call_kwargs = parameters_to_args_kwargs(
474
387
  task.fn, run.parameters or {}
475
388
  )
476
- result = cast(R, task.fn(*call_args, **call_kwargs)) # type: ignore
389
+ result = cast(R, await task.fn(*call_args, **call_kwargs)) # type: ignore
477
390
 
478
391
  # If the task run is successful, finalize it.
479
- run_sync(run.handle_success(result))
480
- if return_type == "result":
481
- return result
392
+ run.handle_success(result)
482
393
 
483
394
  except Exception as exc:
484
- run_sync(run.handle_exception(exc))
395
+ run.handle_exception(exc)
396
+
397
+ if run.state.is_final():
398
+ for hook in run.get_hooks(run.state, as_async=True):
399
+ await hook()
485
400
 
486
401
  if return_type == "state":
487
402
  return run.state
488
- return run_sync(run.result())
403
+ return run.result()
404
+
405
+
406
+ def run_task(
407
+ task: Task[P, R],
408
+ task_run: Optional[TaskRun] = None,
409
+ parameters: Optional[Dict[str, Any]] = None,
410
+ wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
411
+ return_type: Literal["state", "result"] = "result",
412
+ ) -> Union[R, State, None]:
413
+ kwargs = dict(
414
+ task=task,
415
+ task_run=task_run,
416
+ parameters=parameters,
417
+ wait_for=wait_for,
418
+ return_type=return_type,
419
+ )
420
+ if task.isasync:
421
+ return run_task_async(**kwargs)
422
+ else:
423
+ return run_task_sync(**kwargs)
@@ -1,2 +1,2 @@
1
- from .runner import Runner, serve
1
+ from .runner import Runner
2
2
  from .submit import submit_to_runner, wait_for_submitted_runs