prefect-client 3.1.5__py3-none-any.whl → 3.1.6__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 +3 -0
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/_internal/concurrency/api.py +52 -52
- prefect/_internal/concurrency/calls.py +59 -35
- prefect/_internal/concurrency/cancellation.py +34 -18
- prefect/_internal/concurrency/event_loop.py +7 -6
- prefect/_internal/concurrency/threads.py +41 -33
- prefect/_internal/concurrency/waiters.py +28 -21
- prefect/_internal/pydantic/v1_schema.py +2 -2
- prefect/_internal/pydantic/v2_schema.py +10 -9
- prefect/_internal/schemas/bases.py +9 -7
- prefect/_internal/schemas/validators.py +2 -1
- prefect/_version.py +3 -3
- prefect/automations.py +53 -47
- prefect/blocks/abstract.py +12 -10
- prefect/blocks/core.py +4 -2
- prefect/cache_policies.py +11 -11
- prefect/client/__init__.py +3 -1
- prefect/client/base.py +36 -37
- prefect/client/cloud.py +26 -19
- prefect/client/collections.py +2 -2
- prefect/client/orchestration.py +342 -273
- prefect/client/schemas/__init__.py +24 -0
- prefect/client/schemas/actions.py +123 -116
- prefect/client/schemas/objects.py +110 -81
- prefect/client/schemas/responses.py +18 -18
- prefect/client/schemas/schedules.py +136 -93
- prefect/client/subscriptions.py +28 -14
- prefect/client/utilities.py +32 -36
- prefect/concurrency/asyncio.py +6 -9
- prefect/concurrency/sync.py +35 -5
- prefect/context.py +39 -31
- prefect/deployments/flow_runs.py +3 -5
- prefect/docker/__init__.py +1 -1
- prefect/events/schemas/events.py +25 -20
- prefect/events/utilities.py +1 -2
- prefect/filesystems.py +3 -3
- prefect/flow_engine.py +61 -21
- prefect/flow_runs.py +3 -3
- prefect/flows.py +214 -170
- prefect/logging/configuration.py +1 -1
- prefect/logging/highlighters.py +1 -2
- prefect/logging/loggers.py +30 -20
- prefect/main.py +17 -24
- prefect/runner/runner.py +43 -21
- prefect/runner/server.py +30 -32
- prefect/runner/submit.py +3 -6
- prefect/runner/utils.py +6 -6
- prefect/runtime/flow_run.py +7 -0
- prefect/settings/constants.py +2 -2
- prefect/settings/legacy.py +1 -1
- prefect/settings/models/server/events.py +10 -0
- prefect/task_engine.py +72 -19
- prefect/task_runners.py +2 -2
- prefect/tasks.py +46 -33
- prefect/telemetry/bootstrap.py +15 -2
- prefect/telemetry/run_telemetry.py +107 -0
- prefect/transactions.py +14 -14
- prefect/types/__init__.py +1 -4
- prefect/utilities/_engine.py +96 -0
- prefect/utilities/annotations.py +25 -18
- prefect/utilities/asyncutils.py +126 -140
- prefect/utilities/callables.py +87 -78
- prefect/utilities/collections.py +278 -117
- prefect/utilities/compat.py +13 -21
- prefect/utilities/context.py +6 -5
- prefect/utilities/dispatch.py +23 -12
- prefect/utilities/dockerutils.py +33 -32
- prefect/utilities/engine.py +126 -239
- prefect/utilities/filesystem.py +18 -15
- prefect/utilities/hashing.py +10 -11
- prefect/utilities/importtools.py +40 -27
- prefect/utilities/math.py +9 -5
- prefect/utilities/names.py +3 -3
- prefect/utilities/processutils.py +121 -57
- prefect/utilities/pydantic.py +41 -36
- prefect/utilities/render_swagger.py +22 -12
- prefect/utilities/schema_tools/__init__.py +2 -1
- prefect/utilities/schema_tools/hydration.py +50 -43
- prefect/utilities/schema_tools/validation.py +52 -42
- prefect/utilities/services.py +13 -12
- prefect/utilities/templating.py +45 -45
- prefect/utilities/text.py +2 -1
- prefect/utilities/timeout.py +4 -4
- prefect/utilities/urls.py +9 -4
- prefect/utilities/visualization.py +46 -24
- prefect/variables.py +9 -8
- prefect/workers/base.py +15 -8
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/METADATA +4 -2
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/RECORD +93 -91
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/top_level.txt +0 -0
@@ -12,14 +12,15 @@ import signal
|
|
12
12
|
import sys
|
13
13
|
import threading
|
14
14
|
import time
|
15
|
-
from
|
15
|
+
from types import TracebackType
|
16
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, overload
|
16
17
|
|
17
18
|
import anyio
|
18
19
|
|
19
20
|
from prefect._internal.concurrency import logger
|
20
21
|
from prefect._internal.concurrency.event_loop import get_running_loop
|
21
22
|
|
22
|
-
_THREAD_SHIELDS:
|
23
|
+
_THREAD_SHIELDS: dict[threading.Thread, "ThreadShield"] = {}
|
23
24
|
_THREAD_SHIELDS_LOCK = threading.Lock()
|
24
25
|
|
25
26
|
|
@@ -42,14 +43,14 @@ class ThreadShield:
|
|
42
43
|
# Uses the Python implementation of the RLock instead of the C implementation
|
43
44
|
# because we need to inspect `_count` directly to check if the lock is active
|
44
45
|
# which is needed for delayed exception raising during alarms
|
45
|
-
self._lock = threading._RLock()
|
46
|
+
self._lock = threading._RLock() # type: ignore # yes, we want the private version
|
46
47
|
self._exception = None
|
47
48
|
self._owner = owner
|
48
49
|
|
49
50
|
def __enter__(self) -> None:
|
50
51
|
self._lock.__enter__()
|
51
52
|
|
52
|
-
def __exit__(self, *exc_info):
|
53
|
+
def __exit__(self, *exc_info: Any):
|
53
54
|
retval = self._lock.__exit__(*exc_info)
|
54
55
|
|
55
56
|
# Raise the exception if this is the last shield to exit in the owner thread
|
@@ -65,14 +66,14 @@ class ThreadShield:
|
|
65
66
|
|
66
67
|
return retval
|
67
68
|
|
68
|
-
def set_exception(self, exc:
|
69
|
+
def set_exception(self, exc: BaseException):
|
69
70
|
self._exception = exc
|
70
71
|
|
71
72
|
def active(self) -> bool:
|
72
73
|
"""
|
73
74
|
Returns true if the shield is active.
|
74
75
|
"""
|
75
|
-
return self._lock
|
76
|
+
return getattr(self._lock, "_count") > 0
|
76
77
|
|
77
78
|
|
78
79
|
class CancelledError(asyncio.CancelledError):
|
@@ -82,7 +83,7 @@ class CancelledError(asyncio.CancelledError):
|
|
82
83
|
pass
|
83
84
|
|
84
85
|
|
85
|
-
def _get_thread_shield(thread) -> ThreadShield:
|
86
|
+
def _get_thread_shield(thread: threading.Thread) -> ThreadShield:
|
86
87
|
with _THREAD_SHIELDS_LOCK:
|
87
88
|
if thread not in _THREAD_SHIELDS:
|
88
89
|
_THREAD_SHIELDS[thread] = ThreadShield(thread)
|
@@ -139,7 +140,7 @@ class CancelScope(abc.ABC):
|
|
139
140
|
self._end_time = None
|
140
141
|
self._timeout = timeout
|
141
142
|
self._lock = threading.Lock()
|
142
|
-
self._callbacks = []
|
143
|
+
self._callbacks: list[Callable[[], None]] = []
|
143
144
|
super().__init__()
|
144
145
|
|
145
146
|
def __enter__(self):
|
@@ -151,7 +152,9 @@ class CancelScope(abc.ABC):
|
|
151
152
|
logger.debug("%r entered", self)
|
152
153
|
return self
|
153
154
|
|
154
|
-
def __exit__(
|
155
|
+
def __exit__(
|
156
|
+
self, exc_type: type[BaseException], exc_val: Exception, exc_tb: TracebackType
|
157
|
+
) -> Optional[bool]:
|
155
158
|
with self._lock:
|
156
159
|
if not self._cancelled:
|
157
160
|
self._completed = True
|
@@ -195,7 +198,7 @@ class CancelScope(abc.ABC):
|
|
195
198
|
throw the cancelled error.
|
196
199
|
"""
|
197
200
|
with self._lock:
|
198
|
-
if not self.
|
201
|
+
if not self._started:
|
199
202
|
raise RuntimeError("Scope has not been entered.")
|
200
203
|
|
201
204
|
if self._completed:
|
@@ -247,7 +250,6 @@ class AsyncCancelScope(CancelScope):
|
|
247
250
|
self, name: Optional[str] = None, timeout: Optional[float] = None
|
248
251
|
) -> None:
|
249
252
|
super().__init__(name=name, timeout=timeout)
|
250
|
-
self.loop = None
|
251
253
|
|
252
254
|
def __enter__(self):
|
253
255
|
self.loop = asyncio.get_running_loop()
|
@@ -262,7 +264,9 @@ class AsyncCancelScope(CancelScope):
|
|
262
264
|
|
263
265
|
return self
|
264
266
|
|
265
|
-
def __exit__(
|
267
|
+
def __exit__(
|
268
|
+
self, exc_type: type[BaseException], exc_val: Exception, exc_tb: TracebackType
|
269
|
+
) -> bool:
|
266
270
|
if self._anyio_scope.cancel_called:
|
267
271
|
# Mark as cancelled
|
268
272
|
self.cancel(throw=False)
|
@@ -310,7 +314,7 @@ class NullCancelScope(CancelScope):
|
|
310
314
|
super().__init__(name, timeout)
|
311
315
|
self.reason = reason or "null cancel scope"
|
312
316
|
|
313
|
-
def cancel(self):
|
317
|
+
def cancel(self, throw: bool = True) -> bool:
|
314
318
|
logger.warning("%r cannot cancel %s.", self, self.reason)
|
315
319
|
return False
|
316
320
|
|
@@ -355,7 +359,7 @@ class AlarmCancelScope(CancelScope):
|
|
355
359
|
|
356
360
|
return self
|
357
361
|
|
358
|
-
def _sigalarm_to_error(self, *args):
|
362
|
+
def _sigalarm_to_error(self, *args: object) -> None:
|
359
363
|
logger.debug("%r captured alarm raising as cancelled error", self)
|
360
364
|
if self.cancel(throw=False):
|
361
365
|
shield = _get_thread_shield(threading.main_thread())
|
@@ -365,11 +369,13 @@ class AlarmCancelScope(CancelScope):
|
|
365
369
|
else:
|
366
370
|
raise CancelledError()
|
367
371
|
|
368
|
-
def __exit__(self, *_):
|
372
|
+
def __exit__(self, *_: Any) -> Optional[bool]:
|
369
373
|
retval = super().__exit__(*_)
|
370
374
|
|
371
375
|
if self.timeout is not None:
|
372
376
|
# Restore the previous timer
|
377
|
+
if TYPE_CHECKING:
|
378
|
+
assert self._previous_timer is not None
|
373
379
|
signal.setitimer(signal.ITIMER_REAL, *self._previous_timer)
|
374
380
|
|
375
381
|
# Restore the previous signal handler
|
@@ -417,7 +423,7 @@ class WatcherThreadCancelScope(CancelScope):
|
|
417
423
|
|
418
424
|
return self
|
419
425
|
|
420
|
-
def __exit__(self, *_):
|
426
|
+
def __exit__(self, *_: Any) -> Optional[bool]:
|
421
427
|
retval = super().__exit__(*_)
|
422
428
|
self._event.set()
|
423
429
|
if self._enforcer_thread:
|
@@ -466,7 +472,17 @@ class WatcherThreadCancelScope(CancelScope):
|
|
466
472
|
return True
|
467
473
|
|
468
474
|
|
469
|
-
|
475
|
+
@overload
|
476
|
+
def get_deadline(timeout: float) -> float:
|
477
|
+
...
|
478
|
+
|
479
|
+
|
480
|
+
@overload
|
481
|
+
def get_deadline(timeout: None) -> None:
|
482
|
+
...
|
483
|
+
|
484
|
+
|
485
|
+
def get_deadline(timeout: Optional[float]) -> Optional[float]:
|
470
486
|
"""
|
471
487
|
Compute an deadline given a timeout.
|
472
488
|
|
@@ -572,7 +588,7 @@ def cancel_sync_after(timeout: Optional[float], name: Optional[str] = None):
|
|
572
588
|
yield scope
|
573
589
|
|
574
590
|
|
575
|
-
def _send_exception_to_thread(thread: threading.Thread, exc_type:
|
591
|
+
def _send_exception_to_thread(thread: threading.Thread, exc_type: type[BaseException]):
|
576
592
|
"""
|
577
593
|
Raise an exception in a thread.
|
578
594
|
|
@@ -5,7 +5,8 @@ Thread-safe utilities for working with asynchronous event loops.
|
|
5
5
|
import asyncio
|
6
6
|
import concurrent.futures
|
7
7
|
import functools
|
8
|
-
from
|
8
|
+
from collections.abc import Coroutine
|
9
|
+
from typing import Any, Callable, Optional, TypeVar
|
9
10
|
|
10
11
|
from typing_extensions import ParamSpec
|
11
12
|
|
@@ -13,7 +14,7 @@ P = ParamSpec("P")
|
|
13
14
|
T = TypeVar("T")
|
14
15
|
|
15
16
|
|
16
|
-
def get_running_loop() -> Optional[asyncio.
|
17
|
+
def get_running_loop() -> Optional[asyncio.AbstractEventLoop]:
|
17
18
|
"""
|
18
19
|
Get the current running loop.
|
19
20
|
|
@@ -30,7 +31,7 @@ def call_soon_in_loop(
|
|
30
31
|
__fn: Callable[P, T],
|
31
32
|
*args: P.args,
|
32
33
|
**kwargs: P.kwargs,
|
33
|
-
) -> concurrent.futures.Future:
|
34
|
+
) -> concurrent.futures.Future[T]:
|
34
35
|
"""
|
35
36
|
Run a synchronous call in an event loop's thread from another thread.
|
36
37
|
|
@@ -38,7 +39,7 @@ def call_soon_in_loop(
|
|
38
39
|
|
39
40
|
Returns a future that can be used to retrieve the result of the call.
|
40
41
|
"""
|
41
|
-
future = concurrent.futures.Future()
|
42
|
+
future: concurrent.futures.Future[T] = concurrent.futures.Future()
|
42
43
|
|
43
44
|
@functools.wraps(__fn)
|
44
45
|
def wrapper() -> None:
|
@@ -62,8 +63,8 @@ def call_soon_in_loop(
|
|
62
63
|
|
63
64
|
|
64
65
|
async def run_coroutine_in_loop_from_async(
|
65
|
-
__loop: asyncio.AbstractEventLoop, __coro: Coroutine
|
66
|
-
) ->
|
66
|
+
__loop: asyncio.AbstractEventLoop, __coro: Coroutine[Any, Any, T]
|
67
|
+
) -> T:
|
67
68
|
"""
|
68
69
|
Run an asynchronous call in an event loop from an asynchronous context.
|
69
70
|
|
@@ -8,7 +8,9 @@ import concurrent.futures
|
|
8
8
|
import itertools
|
9
9
|
import queue
|
10
10
|
import threading
|
11
|
-
from typing import
|
11
|
+
from typing import Any, Optional
|
12
|
+
|
13
|
+
from typing_extensions import TypeVar
|
12
14
|
|
13
15
|
from prefect._internal.concurrency import logger
|
14
16
|
from prefect._internal.concurrency.calls import Call, Portal
|
@@ -16,6 +18,8 @@ from prefect._internal.concurrency.cancellation import CancelledError
|
|
16
18
|
from prefect._internal.concurrency.event_loop import get_running_loop
|
17
19
|
from prefect._internal.concurrency.primitives import Event
|
18
20
|
|
21
|
+
T = TypeVar("T", infer_variance=True)
|
22
|
+
|
19
23
|
|
20
24
|
class WorkerThread(Portal):
|
21
25
|
"""
|
@@ -33,7 +37,7 @@ class WorkerThread(Portal):
|
|
33
37
|
self.thread = threading.Thread(
|
34
38
|
name=name, daemon=daemon, target=self._entrypoint
|
35
39
|
)
|
36
|
-
self._queue = queue.Queue()
|
40
|
+
self._queue: queue.Queue[Optional[Call[Any]]] = queue.Queue()
|
37
41
|
self._run_once: bool = run_once
|
38
42
|
self._started: bool = False
|
39
43
|
self._submitted_count: int = 0
|
@@ -42,7 +46,7 @@ class WorkerThread(Portal):
|
|
42
46
|
if not daemon:
|
43
47
|
atexit.register(self.shutdown)
|
44
48
|
|
45
|
-
def start(self):
|
49
|
+
def start(self) -> None:
|
46
50
|
"""
|
47
51
|
Start the worker thread.
|
48
52
|
"""
|
@@ -51,7 +55,7 @@ class WorkerThread(Portal):
|
|
51
55
|
self._started = True
|
52
56
|
self.thread.start()
|
53
57
|
|
54
|
-
def submit(self, call: Call) -> Call:
|
58
|
+
def submit(self, call: Call[T]) -> Call[T]:
|
55
59
|
if self._submitted_count > 0 and self._run_once:
|
56
60
|
raise RuntimeError(
|
57
61
|
"Worker configured to only run once. A call has already been submitted."
|
@@ -83,7 +87,7 @@ class WorkerThread(Portal):
|
|
83
87
|
def name(self) -> str:
|
84
88
|
return self.thread.name
|
85
89
|
|
86
|
-
def _entrypoint(self):
|
90
|
+
def _entrypoint(self) -> None:
|
87
91
|
"""
|
88
92
|
Entrypoint for the thread.
|
89
93
|
"""
|
@@ -129,12 +133,14 @@ class EventLoopThread(Portal):
|
|
129
133
|
self.thread = threading.Thread(
|
130
134
|
name=name, daemon=daemon, target=self._entrypoint
|
131
135
|
)
|
132
|
-
self._ready_future
|
136
|
+
self._ready_future: concurrent.futures.Future[
|
137
|
+
bool
|
138
|
+
] = concurrent.futures.Future()
|
133
139
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
134
140
|
self._shutdown_event: Event = Event()
|
135
141
|
self._run_once: bool = run_once
|
136
142
|
self._submitted_count: int = 0
|
137
|
-
self._on_shutdown:
|
143
|
+
self._on_shutdown: list[Call[Any]] = []
|
138
144
|
self._lock = threading.Lock()
|
139
145
|
|
140
146
|
if not daemon:
|
@@ -149,7 +155,7 @@ class EventLoopThread(Portal):
|
|
149
155
|
self.thread.start()
|
150
156
|
self._ready_future.result()
|
151
157
|
|
152
|
-
def submit(self, call: Call) -> Call:
|
158
|
+
def submit(self, call: Call[T]) -> Call[T]:
|
153
159
|
if self._loop is None:
|
154
160
|
self.start()
|
155
161
|
|
@@ -167,6 +173,7 @@ class EventLoopThread(Portal):
|
|
167
173
|
call.set_runner(self)
|
168
174
|
|
169
175
|
# Submit the call to the event loop
|
176
|
+
assert self._loop is not None
|
170
177
|
asyncio.run_coroutine_threadsafe(self._run_call(call), self._loop)
|
171
178
|
|
172
179
|
self._submitted_count += 1
|
@@ -180,15 +187,16 @@ class EventLoopThread(Portal):
|
|
180
187
|
Shutdown the worker thread. Does not wait for the thread to stop.
|
181
188
|
"""
|
182
189
|
with self._lock:
|
183
|
-
if self._shutdown_event is None:
|
184
|
-
return
|
185
|
-
|
186
190
|
self._shutdown_event.set()
|
187
191
|
|
188
192
|
@property
|
189
193
|
def name(self) -> str:
|
190
194
|
return self.thread.name
|
191
195
|
|
196
|
+
@property
|
197
|
+
def running(self) -> bool:
|
198
|
+
return not self._shutdown_event.is_set()
|
199
|
+
|
192
200
|
def _entrypoint(self):
|
193
201
|
"""
|
194
202
|
Entrypoint for the thread.
|
@@ -218,12 +226,12 @@ class EventLoopThread(Portal):
|
|
218
226
|
# Empty the list to allow calls to be garbage collected. Issue #10338.
|
219
227
|
self._on_shutdown = []
|
220
228
|
|
221
|
-
async def _run_call(self, call: Call) -> None:
|
229
|
+
async def _run_call(self, call: Call[Any]) -> None:
|
222
230
|
task = call.run()
|
223
231
|
if task is not None:
|
224
232
|
await task
|
225
233
|
|
226
|
-
def add_shutdown_call(self, call: Call) -> None:
|
234
|
+
def add_shutdown_call(self, call: Call[Any]) -> None:
|
227
235
|
self._on_shutdown.append(call)
|
228
236
|
|
229
237
|
def __enter__(self):
|
@@ -235,9 +243,9 @@ class EventLoopThread(Portal):
|
|
235
243
|
|
236
244
|
|
237
245
|
# the GLOBAL LOOP is used for background services, like logs
|
238
|
-
|
246
|
+
_global_loop: Optional[EventLoopThread] = None
|
239
247
|
# the RUN SYNC LOOP is used exclusively for running async functions in a sync context via asyncutils.run_sync
|
240
|
-
|
248
|
+
_run_sync_loop: Optional[EventLoopThread] = None
|
241
249
|
|
242
250
|
|
243
251
|
def get_global_loop() -> EventLoopThread:
|
@@ -246,29 +254,29 @@ def get_global_loop() -> EventLoopThread:
|
|
246
254
|
|
247
255
|
Creates a new one if there is not one available.
|
248
256
|
"""
|
249
|
-
global
|
257
|
+
global _global_loop
|
250
258
|
|
251
259
|
# Create a new worker on first call or if the existing worker is dead
|
252
260
|
if (
|
253
|
-
|
254
|
-
or not
|
255
|
-
or
|
261
|
+
_global_loop is None
|
262
|
+
or not _global_loop.thread.is_alive()
|
263
|
+
or not _global_loop.running
|
256
264
|
):
|
257
|
-
|
258
|
-
|
265
|
+
_global_loop = EventLoopThread(daemon=True, name="GlobalEventLoopThread")
|
266
|
+
_global_loop.start()
|
259
267
|
|
260
|
-
return
|
268
|
+
return _global_loop
|
261
269
|
|
262
270
|
|
263
271
|
def in_global_loop() -> bool:
|
264
272
|
"""
|
265
273
|
Check if called from the global loop.
|
266
274
|
"""
|
267
|
-
if
|
275
|
+
if _global_loop is None:
|
268
276
|
# Avoid creating a global loop if there isn't one
|
269
277
|
return False
|
270
278
|
|
271
|
-
return get_global_loop()
|
279
|
+
return getattr(get_global_loop(), "_loop") == get_running_loop()
|
272
280
|
|
273
281
|
|
274
282
|
def get_run_sync_loop() -> EventLoopThread:
|
@@ -277,29 +285,29 @@ def get_run_sync_loop() -> EventLoopThread:
|
|
277
285
|
|
278
286
|
Creates a new one if there is not one available.
|
279
287
|
"""
|
280
|
-
global
|
288
|
+
global _run_sync_loop
|
281
289
|
|
282
290
|
# Create a new worker on first call or if the existing worker is dead
|
283
291
|
if (
|
284
|
-
|
285
|
-
or not
|
286
|
-
or
|
292
|
+
_run_sync_loop is None
|
293
|
+
or not _run_sync_loop.thread.is_alive()
|
294
|
+
or not _run_sync_loop.running
|
287
295
|
):
|
288
|
-
|
289
|
-
|
296
|
+
_run_sync_loop = EventLoopThread(daemon=True, name="RunSyncEventLoopThread")
|
297
|
+
_run_sync_loop.start()
|
290
298
|
|
291
|
-
return
|
299
|
+
return _run_sync_loop
|
292
300
|
|
293
301
|
|
294
302
|
def in_run_sync_loop() -> bool:
|
295
303
|
"""
|
296
304
|
Check if called from the global loop.
|
297
305
|
"""
|
298
|
-
if
|
306
|
+
if _run_sync_loop is None:
|
299
307
|
# Avoid creating a global loop if there isn't one
|
300
308
|
return False
|
301
309
|
|
302
|
-
return get_run_sync_loop()
|
310
|
+
return getattr(get_run_sync_loop(), "_loop") == get_running_loop()
|
303
311
|
|
304
312
|
|
305
313
|
def wait_for_global_loop_exit(timeout: Optional[float] = None) -> None:
|
@@ -10,7 +10,8 @@ import inspect
|
|
10
10
|
import queue
|
11
11
|
import threading
|
12
12
|
from collections import deque
|
13
|
-
from
|
13
|
+
from collections.abc import Awaitable
|
14
|
+
from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar
|
14
15
|
from weakref import WeakKeyDictionary
|
15
16
|
|
16
17
|
import anyio
|
@@ -24,12 +25,12 @@ T = TypeVar("T")
|
|
24
25
|
|
25
26
|
|
26
27
|
# Waiters are stored in a stack for each thread
|
27
|
-
_WAITERS_BY_THREAD: "WeakKeyDictionary[threading.Thread, deque[Waiter]]" = (
|
28
|
+
_WAITERS_BY_THREAD: "WeakKeyDictionary[threading.Thread, deque[Waiter[Any]]]" = (
|
28
29
|
WeakKeyDictionary()
|
29
30
|
)
|
30
31
|
|
31
32
|
|
32
|
-
def add_waiter_for_thread(waiter: "Waiter", thread: threading.Thread):
|
33
|
+
def add_waiter_for_thread(waiter: "Waiter[Any]", thread: threading.Thread) -> None:
|
33
34
|
"""
|
34
35
|
Add a waiter for a thread.
|
35
36
|
"""
|
@@ -62,7 +63,7 @@ class Waiter(Portal, abc.ABC, Generic[T]):
|
|
62
63
|
return self._call.future.done()
|
63
64
|
|
64
65
|
@abc.abstractmethod
|
65
|
-
def wait(self) ->
|
66
|
+
def wait(self) -> T:
|
66
67
|
"""
|
67
68
|
Wait for the call to finish.
|
68
69
|
|
@@ -71,7 +72,7 @@ class Waiter(Portal, abc.ABC, Generic[T]):
|
|
71
72
|
raise NotImplementedError()
|
72
73
|
|
73
74
|
@abc.abstractmethod
|
74
|
-
def add_done_callback(self, callback: Call) ->
|
75
|
+
def add_done_callback(self, callback: Call[Any]) -> None:
|
75
76
|
"""
|
76
77
|
Schedule a call to run when the waiter is done waiting.
|
77
78
|
|
@@ -91,11 +92,11 @@ class SyncWaiter(Waiter[T]):
|
|
91
92
|
|
92
93
|
def __init__(self, call: Call[T]) -> None:
|
93
94
|
super().__init__(call=call)
|
94
|
-
self._queue: queue.Queue = queue.Queue()
|
95
|
-
self._done_callbacks = []
|
95
|
+
self._queue: queue.Queue[Optional[Call[T]]] = queue.Queue()
|
96
|
+
self._done_callbacks: list[Call[Any]] = []
|
96
97
|
self._done_event = threading.Event()
|
97
98
|
|
98
|
-
def submit(self, call: Call):
|
99
|
+
def submit(self, call: Call[T]) -> Call[T]:
|
99
100
|
"""
|
100
101
|
Submit a callback to execute while waiting.
|
101
102
|
"""
|
@@ -109,7 +110,7 @@ class SyncWaiter(Waiter[T]):
|
|
109
110
|
def _handle_waiting_callbacks(self):
|
110
111
|
logger.debug("Waiter %r watching for callbacks", self)
|
111
112
|
while True:
|
112
|
-
callback
|
113
|
+
callback = self._queue.get()
|
113
114
|
if callback is None:
|
114
115
|
break
|
115
116
|
|
@@ -130,13 +131,13 @@ class SyncWaiter(Waiter[T]):
|
|
130
131
|
if callback:
|
131
132
|
callback.run()
|
132
133
|
|
133
|
-
def add_done_callback(self, callback: Call):
|
134
|
+
def add_done_callback(self, callback: Call[Any]) -> None:
|
134
135
|
if self._done_event.is_set():
|
135
136
|
raise RuntimeError("Cannot add done callbacks to done waiters.")
|
136
137
|
else:
|
137
138
|
self._done_callbacks.append(callback)
|
138
139
|
|
139
|
-
def wait(self) -> T:
|
140
|
+
def wait(self) -> Call[T]:
|
140
141
|
# Stop watching for work once the future is done
|
141
142
|
self._call.future.add_done_callback(lambda _: self._queue.put_nowait(None))
|
142
143
|
self._call.future.add_done_callback(lambda _: self._done_event.set())
|
@@ -159,13 +160,13 @@ class AsyncWaiter(Waiter[T]):
|
|
159
160
|
|
160
161
|
# Delay instantiating loop and queue as there may not be a loop present yet
|
161
162
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
162
|
-
self._queue: Optional[asyncio.Queue] = None
|
163
|
-
self._early_submissions:
|
164
|
-
self._done_callbacks = []
|
163
|
+
self._queue: Optional[asyncio.Queue[Optional[Call[T]]]] = None
|
164
|
+
self._early_submissions: list[Call[T]] = []
|
165
|
+
self._done_callbacks: list[Call[Any]] = []
|
165
166
|
self._done_event = Event()
|
166
167
|
self._done_waiting = False
|
167
168
|
|
168
|
-
def submit(self, call: Call):
|
169
|
+
def submit(self, call: Call[T]) -> Call[T]:
|
169
170
|
"""
|
170
171
|
Submit a callback to execute while waiting.
|
171
172
|
"""
|
@@ -180,11 +181,15 @@ class AsyncWaiter(Waiter[T]):
|
|
180
181
|
return call
|
181
182
|
|
182
183
|
# We must put items in the queue from the event loop that owns it
|
184
|
+
if TYPE_CHECKING:
|
185
|
+
assert self._loop is not None
|
183
186
|
call_soon_in_loop(self._loop, self._queue.put_nowait, call)
|
184
187
|
return call
|
185
188
|
|
186
|
-
def _resubmit_early_submissions(self):
|
187
|
-
|
189
|
+
def _resubmit_early_submissions(self) -> None:
|
190
|
+
if TYPE_CHECKING:
|
191
|
+
assert self._queue is not None
|
192
|
+
assert self._loop is not None
|
188
193
|
for call in self._early_submissions:
|
189
194
|
# We must put items in the queue from the event loop that owns it
|
190
195
|
call_soon_in_loop(self._loop, self._queue.put_nowait, call)
|
@@ -192,11 +197,11 @@ class AsyncWaiter(Waiter[T]):
|
|
192
197
|
|
193
198
|
async def _handle_waiting_callbacks(self):
|
194
199
|
logger.debug("Waiter %r watching for callbacks", self)
|
195
|
-
tasks = []
|
200
|
+
tasks: list[Awaitable[None]] = []
|
196
201
|
|
197
202
|
try:
|
198
203
|
while True:
|
199
|
-
callback
|
204
|
+
callback = await self._queue.get()
|
200
205
|
if callback is None:
|
201
206
|
break
|
202
207
|
|
@@ -228,12 +233,12 @@ class AsyncWaiter(Waiter[T]):
|
|
228
233
|
with anyio.CancelScope(shield=True):
|
229
234
|
await self._run_done_callback(callback)
|
230
235
|
|
231
|
-
async def _run_done_callback(self, callback: Call):
|
236
|
+
async def _run_done_callback(self, callback: Call[Any]) -> None:
|
232
237
|
coro = callback.run()
|
233
238
|
if coro:
|
234
239
|
await coro
|
235
240
|
|
236
|
-
def add_done_callback(self, callback: Call):
|
241
|
+
def add_done_callback(self, callback: Call[Any]) -> None:
|
237
242
|
if self._done_event.is_set():
|
238
243
|
raise RuntimeError("Cannot add done callbacks to done waiters.")
|
239
244
|
else:
|
@@ -243,6 +248,8 @@ class AsyncWaiter(Waiter[T]):
|
|
243
248
|
# Only send a `None` to the queue if the waiter is still blocked reading from
|
244
249
|
# the queue. Otherwise, it's possible that the event loop is stopped.
|
245
250
|
if not self._done_waiting:
|
251
|
+
assert self._loop is not None
|
252
|
+
assert self._queue is not None
|
246
253
|
call_soon_in_loop(self._loop, self._queue.put_nowait, None)
|
247
254
|
|
248
255
|
async def wait(self) -> Call[T]:
|
@@ -6,7 +6,7 @@ import pydantic
|
|
6
6
|
from pydantic.v1 import BaseModel as V1BaseModel
|
7
7
|
|
8
8
|
|
9
|
-
def is_v1_model(v) -> bool:
|
9
|
+
def is_v1_model(v: typing.Any) -> bool:
|
10
10
|
with warnings.catch_warnings():
|
11
11
|
warnings.filterwarnings(
|
12
12
|
"ignore", category=pydantic.warnings.PydanticDeprecatedSince20
|
@@ -23,7 +23,7 @@ def is_v1_model(v) -> bool:
|
|
23
23
|
return False
|
24
24
|
|
25
25
|
|
26
|
-
def is_v1_type(v) -> bool:
|
26
|
+
def is_v1_type(v: typing.Any) -> bool:
|
27
27
|
if is_v1_model(v):
|
28
28
|
return True
|
29
29
|
|
@@ -16,7 +16,7 @@ from prefect._internal.pydantic.annotations.pendulum import (
|
|
16
16
|
from prefect._internal.pydantic.schemas import GenerateEmptySchemaForUserClasses
|
17
17
|
|
18
18
|
|
19
|
-
def is_v2_model(v) -> bool:
|
19
|
+
def is_v2_model(v: t.Any) -> bool:
|
20
20
|
if isinstance(v, V2BaseModel):
|
21
21
|
return True
|
22
22
|
try:
|
@@ -28,7 +28,7 @@ def is_v2_model(v) -> bool:
|
|
28
28
|
return False
|
29
29
|
|
30
30
|
|
31
|
-
def is_v2_type(v) -> bool:
|
31
|
+
def is_v2_type(v: t.Any) -> bool:
|
32
32
|
if is_v2_model(v):
|
33
33
|
return True
|
34
34
|
|
@@ -56,9 +56,9 @@ def process_v2_params(
|
|
56
56
|
param: inspect.Parameter,
|
57
57
|
*,
|
58
58
|
position: int,
|
59
|
-
docstrings:
|
60
|
-
aliases:
|
61
|
-
) ->
|
59
|
+
docstrings: dict[str, str],
|
60
|
+
aliases: dict[str, str],
|
61
|
+
) -> tuple[str, t.Any, t.Any]:
|
62
62
|
"""
|
63
63
|
Generate a sanitized name, type, and pydantic.Field for a given parameter.
|
64
64
|
|
@@ -72,7 +72,7 @@ def process_v2_params(
|
|
72
72
|
else:
|
73
73
|
name = param.name
|
74
74
|
|
75
|
-
type_ = t.Any if param.annotation is inspect.
|
75
|
+
type_ = t.Any if param.annotation is inspect.Parameter.empty else param.annotation
|
76
76
|
|
77
77
|
# Replace pendulum type annotations with our own so that they are pydantic compatible
|
78
78
|
if type_ == pendulum.DateTime:
|
@@ -95,12 +95,13 @@ def process_v2_params(
|
|
95
95
|
def create_v2_schema(
|
96
96
|
name_: str,
|
97
97
|
model_cfg: t.Optional[ConfigDict] = None,
|
98
|
-
model_base: t.Optional[
|
99
|
-
|
100
|
-
):
|
98
|
+
model_base: t.Optional[type[V2BaseModel]] = None,
|
99
|
+
model_fields: t.Optional[dict[str, t.Any]] = None,
|
100
|
+
) -> dict[str, t.Any]:
|
101
101
|
"""
|
102
102
|
Create a pydantic v2 model and craft a v1 compatible schema from it.
|
103
103
|
"""
|
104
|
+
model_fields = model_fields or {}
|
104
105
|
model = create_model(
|
105
106
|
name_, __config__=model_cfg, __base__=model_base, **model_fields
|
106
107
|
)
|