prefect-client 2.19.3__py3-none-any.whl → 3.0.0rc1__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 (239) hide show
  1. prefect/__init__.py +8 -56
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/concurrency/api.py +0 -34
  5. prefect/_internal/concurrency/calls.py +0 -6
  6. prefect/_internal/concurrency/cancellation.py +0 -3
  7. prefect/_internal/concurrency/event_loop.py +0 -20
  8. prefect/_internal/concurrency/inspection.py +3 -3
  9. prefect/_internal/concurrency/threads.py +35 -0
  10. prefect/_internal/concurrency/waiters.py +0 -28
  11. prefect/_internal/pydantic/__init__.py +0 -45
  12. prefect/_internal/pydantic/v1_schema.py +21 -22
  13. prefect/_internal/pydantic/v2_schema.py +0 -2
  14. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  15. prefect/_internal/schemas/bases.py +44 -177
  16. prefect/_internal/schemas/fields.py +1 -43
  17. prefect/_internal/schemas/validators.py +60 -158
  18. prefect/artifacts.py +161 -14
  19. prefect/automations.py +39 -4
  20. prefect/blocks/abstract.py +1 -1
  21. prefect/blocks/core.py +268 -148
  22. prefect/blocks/fields.py +2 -57
  23. prefect/blocks/kubernetes.py +8 -12
  24. prefect/blocks/notifications.py +40 -20
  25. prefect/blocks/system.py +22 -11
  26. prefect/blocks/webhook.py +2 -9
  27. prefect/client/base.py +4 -4
  28. prefect/client/cloud.py +8 -13
  29. prefect/client/orchestration.py +347 -341
  30. prefect/client/schemas/actions.py +92 -86
  31. prefect/client/schemas/filters.py +20 -40
  32. prefect/client/schemas/objects.py +147 -145
  33. prefect/client/schemas/responses.py +16 -24
  34. prefect/client/schemas/schedules.py +47 -35
  35. prefect/client/subscriptions.py +2 -2
  36. prefect/client/utilities.py +5 -2
  37. prefect/concurrency/asyncio.py +3 -1
  38. prefect/concurrency/events.py +1 -1
  39. prefect/concurrency/services.py +6 -3
  40. prefect/context.py +195 -27
  41. prefect/deployments/__init__.py +5 -6
  42. prefect/deployments/base.py +7 -5
  43. prefect/deployments/flow_runs.py +185 -0
  44. prefect/deployments/runner.py +50 -45
  45. prefect/deployments/schedules.py +28 -23
  46. prefect/deployments/steps/__init__.py +0 -1
  47. prefect/deployments/steps/core.py +1 -0
  48. prefect/deployments/steps/pull.py +7 -21
  49. prefect/engine.py +12 -2422
  50. prefect/events/actions.py +17 -23
  51. prefect/events/cli/automations.py +19 -6
  52. prefect/events/clients.py +14 -37
  53. prefect/events/filters.py +14 -18
  54. prefect/events/related.py +2 -2
  55. prefect/events/schemas/__init__.py +0 -5
  56. prefect/events/schemas/automations.py +55 -46
  57. prefect/events/schemas/deployment_triggers.py +7 -197
  58. prefect/events/schemas/events.py +34 -65
  59. prefect/events/schemas/labelling.py +10 -14
  60. prefect/events/utilities.py +2 -3
  61. prefect/events/worker.py +2 -3
  62. prefect/filesystems.py +6 -517
  63. prefect/{new_flow_engine.py → flow_engine.py} +313 -72
  64. prefect/flow_runs.py +377 -5
  65. prefect/flows.py +248 -165
  66. prefect/futures.py +186 -345
  67. prefect/infrastructure/__init__.py +0 -27
  68. prefect/infrastructure/provisioners/__init__.py +5 -3
  69. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  70. prefect/infrastructure/provisioners/container_instance.py +11 -7
  71. prefect/infrastructure/provisioners/ecs.py +6 -4
  72. prefect/infrastructure/provisioners/modal.py +8 -5
  73. prefect/input/actions.py +2 -4
  74. prefect/input/run_input.py +5 -7
  75. prefect/logging/formatters.py +0 -2
  76. prefect/logging/handlers.py +3 -11
  77. prefect/logging/loggers.py +2 -2
  78. prefect/manifests.py +2 -1
  79. prefect/records/__init__.py +1 -0
  80. prefect/records/result_store.py +42 -0
  81. prefect/records/store.py +9 -0
  82. prefect/results.py +43 -39
  83. prefect/runner/runner.py +9 -9
  84. prefect/runner/server.py +6 -10
  85. prefect/runner/storage.py +3 -8
  86. prefect/runner/submit.py +2 -2
  87. prefect/runner/utils.py +2 -2
  88. prefect/serializers.py +24 -35
  89. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  90. prefect/settings.py +70 -133
  91. prefect/states.py +17 -47
  92. prefect/task_engine.py +697 -58
  93. prefect/task_runners.py +269 -301
  94. prefect/task_server.py +53 -34
  95. prefect/tasks.py +327 -337
  96. prefect/transactions.py +220 -0
  97. prefect/types/__init__.py +61 -82
  98. prefect/utilities/asyncutils.py +195 -136
  99. prefect/utilities/callables.py +121 -41
  100. prefect/utilities/collections.py +23 -38
  101. prefect/utilities/dispatch.py +11 -3
  102. prefect/utilities/dockerutils.py +4 -0
  103. prefect/utilities/engine.py +140 -20
  104. prefect/utilities/importtools.py +26 -27
  105. prefect/utilities/pydantic.py +128 -38
  106. prefect/utilities/schema_tools/hydration.py +5 -1
  107. prefect/utilities/templating.py +12 -2
  108. prefect/variables.py +78 -61
  109. prefect/workers/__init__.py +0 -1
  110. prefect/workers/base.py +15 -17
  111. prefect/workers/process.py +3 -8
  112. prefect/workers/server.py +2 -2
  113. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
  114. prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
  115. prefect/_internal/pydantic/_base_model.py +0 -51
  116. prefect/_internal/pydantic/_compat.py +0 -82
  117. prefect/_internal/pydantic/_flags.py +0 -20
  118. prefect/_internal/pydantic/_types.py +0 -8
  119. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  120. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  121. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  122. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  123. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  124. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  125. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  126. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  127. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  128. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  129. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  130. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  131. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  132. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  133. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  134. prefect/_vendor/__init__.py +0 -0
  135. prefect/_vendor/fastapi/__init__.py +0 -25
  136. prefect/_vendor/fastapi/applications.py +0 -946
  137. prefect/_vendor/fastapi/background.py +0 -3
  138. prefect/_vendor/fastapi/concurrency.py +0 -44
  139. prefect/_vendor/fastapi/datastructures.py +0 -58
  140. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  141. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  142. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  143. prefect/_vendor/fastapi/encoders.py +0 -177
  144. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  145. prefect/_vendor/fastapi/exceptions.py +0 -46
  146. prefect/_vendor/fastapi/logger.py +0 -3
  147. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  148. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  149. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  150. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  151. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  152. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  153. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  154. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  155. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  156. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  157. prefect/_vendor/fastapi/openapi/models.py +0 -480
  158. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  159. prefect/_vendor/fastapi/param_functions.py +0 -340
  160. prefect/_vendor/fastapi/params.py +0 -453
  161. prefect/_vendor/fastapi/requests.py +0 -4
  162. prefect/_vendor/fastapi/responses.py +0 -40
  163. prefect/_vendor/fastapi/routing.py +0 -1331
  164. prefect/_vendor/fastapi/security/__init__.py +0 -15
  165. prefect/_vendor/fastapi/security/api_key.py +0 -98
  166. prefect/_vendor/fastapi/security/base.py +0 -6
  167. prefect/_vendor/fastapi/security/http.py +0 -172
  168. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  169. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  170. prefect/_vendor/fastapi/security/utils.py +0 -10
  171. prefect/_vendor/fastapi/staticfiles.py +0 -1
  172. prefect/_vendor/fastapi/templating.py +0 -3
  173. prefect/_vendor/fastapi/testclient.py +0 -1
  174. prefect/_vendor/fastapi/types.py +0 -3
  175. prefect/_vendor/fastapi/utils.py +0 -235
  176. prefect/_vendor/fastapi/websockets.py +0 -7
  177. prefect/_vendor/starlette/__init__.py +0 -1
  178. prefect/_vendor/starlette/_compat.py +0 -28
  179. prefect/_vendor/starlette/_exception_handler.py +0 -80
  180. prefect/_vendor/starlette/_utils.py +0 -88
  181. prefect/_vendor/starlette/applications.py +0 -261
  182. prefect/_vendor/starlette/authentication.py +0 -159
  183. prefect/_vendor/starlette/background.py +0 -43
  184. prefect/_vendor/starlette/concurrency.py +0 -59
  185. prefect/_vendor/starlette/config.py +0 -151
  186. prefect/_vendor/starlette/convertors.py +0 -87
  187. prefect/_vendor/starlette/datastructures.py +0 -707
  188. prefect/_vendor/starlette/endpoints.py +0 -130
  189. prefect/_vendor/starlette/exceptions.py +0 -60
  190. prefect/_vendor/starlette/formparsers.py +0 -276
  191. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  192. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  193. prefect/_vendor/starlette/middleware/base.py +0 -220
  194. prefect/_vendor/starlette/middleware/cors.py +0 -176
  195. prefect/_vendor/starlette/middleware/errors.py +0 -265
  196. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  197. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  198. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  199. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  200. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  201. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  202. prefect/_vendor/starlette/requests.py +0 -328
  203. prefect/_vendor/starlette/responses.py +0 -347
  204. prefect/_vendor/starlette/routing.py +0 -933
  205. prefect/_vendor/starlette/schemas.py +0 -154
  206. prefect/_vendor/starlette/staticfiles.py +0 -248
  207. prefect/_vendor/starlette/status.py +0 -199
  208. prefect/_vendor/starlette/templating.py +0 -231
  209. prefect/_vendor/starlette/testclient.py +0 -804
  210. prefect/_vendor/starlette/types.py +0 -30
  211. prefect/_vendor/starlette/websockets.py +0 -193
  212. prefect/agent.py +0 -698
  213. prefect/deployments/deployments.py +0 -1042
  214. prefect/deprecated/__init__.py +0 -0
  215. prefect/deprecated/data_documents.py +0 -350
  216. prefect/deprecated/packaging/__init__.py +0 -12
  217. prefect/deprecated/packaging/base.py +0 -96
  218. prefect/deprecated/packaging/docker.py +0 -146
  219. prefect/deprecated/packaging/file.py +0 -92
  220. prefect/deprecated/packaging/orion.py +0 -80
  221. prefect/deprecated/packaging/serializers.py +0 -171
  222. prefect/events/instrument.py +0 -135
  223. prefect/infrastructure/base.py +0 -323
  224. prefect/infrastructure/container.py +0 -818
  225. prefect/infrastructure/kubernetes.py +0 -920
  226. prefect/infrastructure/process.py +0 -289
  227. prefect/new_task_engine.py +0 -423
  228. prefect/pydantic/__init__.py +0 -76
  229. prefect/pydantic/main.py +0 -39
  230. prefect/software/__init__.py +0 -2
  231. prefect/software/base.py +0 -50
  232. prefect/software/conda.py +0 -199
  233. prefect/software/pip.py +0 -122
  234. prefect/software/python.py +0 -52
  235. prefect/workers/block.py +0 -218
  236. prefect_client-2.19.3.dist-info/RECORD +0 -292
  237. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
  238. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
  239. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -3,15 +3,13 @@ Utilities for interoperability with async functions and workers from various con
3
3
  """
4
4
 
5
5
  import asyncio
6
- import ctypes
7
6
  import inspect
8
7
  import threading
9
8
  import warnings
10
9
  from concurrent.futures import ThreadPoolExecutor
11
10
  from contextlib import asynccontextmanager
12
- from contextvars import copy_context
11
+ from contextvars import ContextVar, copy_context
13
12
  from functools import partial, wraps
14
- from threading import Thread
15
13
  from typing import (
16
14
  Any,
17
15
  Awaitable,
@@ -20,7 +18,6 @@ from typing import (
20
18
  Dict,
21
19
  List,
22
20
  Optional,
23
- Type,
24
21
  TypeVar,
25
22
  Union,
26
23
  cast,
@@ -29,9 +26,16 @@ from uuid import UUID, uuid4
29
26
 
30
27
  import anyio
31
28
  import anyio.abc
29
+ import anyio.from_thread
30
+ import anyio.to_thread
32
31
  import sniffio
33
32
  from typing_extensions import Literal, ParamSpec, TypeGuard
34
33
 
34
+ from prefect._internal.concurrency.api import _cast_to_call, from_sync
35
+ from prefect._internal.concurrency.threads import (
36
+ get_run_sync_loop,
37
+ in_run_sync_loop,
38
+ )
35
39
  from prefect.logging import get_logger
36
40
 
37
41
  T = TypeVar("T")
@@ -46,6 +50,14 @@ EVENT_LOOP_GC_REFS = {}
46
50
 
47
51
  PREFECT_THREAD_LIMITER: Optional[anyio.CapacityLimiter] = None
48
52
 
53
+ RUNNING_IN_RUN_SYNC_LOOP_FLAG = ContextVar("running_in_run_sync_loop", default=False)
54
+ RUNNING_ASYNC_FLAG = ContextVar("run_async", default=False)
55
+ BACKGROUND_TASKS: set[asyncio.Task] = set()
56
+ background_task_lock = threading.Lock()
57
+
58
+ # Thread-local storage to keep track of worker thread state
59
+ _thread_local = threading.local()
60
+
49
61
  logger = get_logger()
50
62
 
51
63
 
@@ -82,12 +94,47 @@ def is_async_gen_fn(func):
82
94
  return inspect.isasyncgenfunction(func)
83
95
 
84
96
 
85
- def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
97
+ def create_task(coroutine: Coroutine) -> asyncio.Task:
98
+ """
99
+ Replacement for asyncio.create_task that will ensure that tasks aren't
100
+ garbage collected before they complete. Allows for "fire and forget"
101
+ behavior in which tasks can be created and the application can move on.
102
+ Tasks can also be awaited normally.
103
+
104
+ See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
105
+ for details (and essentially this implementation)
86
106
  """
87
- Runs a coroutine from a synchronous context. A thread will be spawned
88
- to run the event loop if necessary, which allows coroutines to run in
89
- environments like Jupyter notebooks where the event loop runs on the main
90
- thread.
107
+
108
+ task = asyncio.create_task(coroutine)
109
+
110
+ # Add task to the set. This creates a strong reference.
111
+ # Take a lock because this might be done from multiple threads.
112
+ with background_task_lock:
113
+ BACKGROUND_TASKS.add(task)
114
+
115
+ # To prevent keeping references to finished tasks forever,
116
+ # make each task remove its own reference from the set after
117
+ # completion:
118
+ task.add_done_callback(BACKGROUND_TASKS.discard)
119
+
120
+ return task
121
+
122
+
123
+ def _run_sync_in_new_thread(coroutine: Coroutine[Any, Any, T]) -> T:
124
+ """
125
+ Note: this is an OLD implementation of `run_coro_as_sync` which liberally created
126
+ new threads and new loops. This works, but prevents sharing any objects
127
+ across coroutines, in particular httpx clients, which are very expensive to
128
+ instantiate.
129
+
130
+ This is here for historical purposes and can be removed if/when it is no
131
+ longer needed for reference.
132
+
133
+ ---
134
+
135
+ Runs a coroutine from a synchronous context. A thread will be spawned to run
136
+ the event loop if necessary, which allows coroutines to run in environments
137
+ like Jupyter notebooks where the event loop runs on the main thread.
91
138
 
92
139
  Args:
93
140
  coroutine: The coroutine to run.
@@ -96,15 +143,25 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
96
143
  The return value of the coroutine.
97
144
 
98
145
  Example:
99
- Basic usage:
100
- ```python
101
- async def my_async_function(x: int) -> int:
146
+ Basic usage: ```python async def my_async_function(x: int) -> int:
102
147
  return x + 1
103
148
 
104
- run_sync(my_async_function(1))
105
- ```
149
+ run_sync(my_async_function(1)) ```
106
150
  """
151
+
107
152
  # ensure context variables are properly copied to the async frame
153
+ async def context_local_wrapper():
154
+ """
155
+ Wrapper that is submitted using copy_context().run to ensure
156
+ the RUNNING_ASYNC_FLAG mutations are tightly scoped to this coroutine's frame.
157
+ """
158
+ token = RUNNING_ASYNC_FLAG.set(True)
159
+ try:
160
+ result = await coroutine
161
+ finally:
162
+ RUNNING_ASYNC_FLAG.reset(token)
163
+ return result
164
+
108
165
  context = copy_context()
109
166
  try:
110
167
  loop = asyncio.get_running_loop()
@@ -113,102 +170,99 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
113
170
 
114
171
  if loop and loop.is_running():
115
172
  with ThreadPoolExecutor() as executor:
116
- future = executor.submit(context.run, asyncio.run, coroutine)
117
- return cast(T, future.result())
173
+ future = executor.submit(context.run, asyncio.run, context_local_wrapper())
174
+ result = cast(T, future.result())
118
175
  else:
119
- return context.run(asyncio.run, coroutine)
176
+ result = context.run(asyncio.run, context_local_wrapper())
177
+ return result
120
178
 
121
179
 
122
- async def run_sync_in_worker_thread(
123
- __fn: Callable[..., T], *args: Any, **kwargs: Any
124
- ) -> T:
180
+ def run_coro_as_sync(
181
+ coroutine: Awaitable[R],
182
+ force_new_thread: bool = False,
183
+ wait_for_result: bool = True,
184
+ ) -> R:
125
185
  """
126
- Runs a sync function in a new worker thread so that the main thread's event loop
127
- is not blocked
186
+ Runs a coroutine from a synchronous context, as if it were a synchronous
187
+ function.
128
188
 
129
- Unlike the anyio function, this defaults to a cancellable thread and does not allow
130
- passing arguments to the anyio function so users can pass kwargs to their function.
189
+ The coroutine is scheduled to run in the "run sync" event loop, which is
190
+ running in its own thread and is started the first time it is needed. This
191
+ allows us to share objects like async httpx clients among all coroutines
192
+ running in the loop.
131
193
 
132
- Note that cancellation of threads will not result in interrupted computation, the
133
- thread may continue running the outcome will just be ignored.
134
- """
135
- call = partial(__fn, *args, **kwargs)
136
- return await anyio.to_thread.run_sync(
137
- call, cancellable=True, limiter=get_thread_limiter()
138
- )
194
+ If run_sync is called from within the run_sync loop, it will run the
195
+ coroutine in a new thread, because otherwise a deadlock would occur. Note
196
+ that this behavior should not appear anywhere in the Prefect codebase or in
197
+ user code.
139
198
 
199
+ Args:
200
+ coroutine (Awaitable): The coroutine to be run as a synchronous function.
201
+ force_new_thread (bool, optional): If True, the coroutine will always be run in a new thread.
202
+ Defaults to False.
203
+ wait_for_result (bool, optional): If True, the function will wait for the coroutine to complete
204
+ and return the result. If False, the function will submit the coroutine to the "run sync"
205
+ event loop and return immediately, where it will eventually be run. Defaults to True.
140
206
 
141
- def raise_async_exception_in_thread(thread: Thread, exc_type: Type[BaseException]):
207
+ Returns:
208
+ The result of the coroutine if wait_for_result is True, otherwise None.
142
209
  """
143
- Raise an exception in a thread asynchronously.
144
210
 
145
- This will not interrupt long-running system calls like `sleep` or `wait`.
146
- """
147
- ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(
148
- ctypes.c_long(thread.ident), ctypes.py_object(exc_type)
149
- )
150
- if ret == 0:
151
- raise ValueError("Thread not found.")
211
+ async def coroutine_wrapper():
212
+ """
213
+ Set flags so that children (and grandchildren...) of this task know they are running in a new
214
+ thread and do not try to run on the run_sync thread, which would cause a
215
+ deadlock.
216
+ """
217
+ token1 = RUNNING_IN_RUN_SYNC_LOOP_FLAG.set(True)
218
+ token2 = RUNNING_ASYNC_FLAG.set(True)
219
+ try:
220
+ # use `asyncio.create_task` because it copies context variables automatically
221
+ task = create_task(coroutine)
222
+ if wait_for_result:
223
+ return await task
224
+ finally:
225
+ RUNNING_IN_RUN_SYNC_LOOP_FLAG.reset(token1)
226
+ RUNNING_ASYNC_FLAG.reset(token2)
227
+
228
+ # if we are already in the run_sync loop, or a descendent of a coroutine
229
+ # that is running in the run_sync loop, we need to run this coroutine in a
230
+ # new thread
231
+ if in_run_sync_loop() or RUNNING_IN_RUN_SYNC_LOOP_FLAG.get() or force_new_thread:
232
+ return from_sync.call_in_new_thread(coroutine_wrapper)
233
+
234
+ # otherwise, we can run the coroutine in the run_sync loop
235
+ # and wait for the result
236
+ else:
237
+ call = _cast_to_call(coroutine_wrapper)
238
+ runner = get_run_sync_loop()
239
+ runner.submit(call)
240
+ return call.result()
152
241
 
153
242
 
154
- async def run_sync_in_interruptible_worker_thread(
243
+ async def run_sync_in_worker_thread(
155
244
  __fn: Callable[..., T], *args: Any, **kwargs: Any
156
245
  ) -> T:
157
246
  """
158
- Runs a sync function in a new interruptible worker thread so that the main
159
- thread's event loop is not blocked
160
-
161
- Unlike the anyio function, this performs best-effort cancellation of the
162
- thread using the C API. Cancellation will not interrupt system calls like
163
- `sleep`.
164
- """
165
-
166
- class NotSet:
167
- pass
247
+ Runs a sync function in a new worker thread so that the main thread's event loop
248
+ is not blocked.
168
249
 
169
- thread: Thread = None
170
- result = NotSet
171
- event = asyncio.Event()
172
- loop = asyncio.get_running_loop()
250
+ Unlike the anyio function, this defaults to a cancellable thread and does not allow
251
+ passing arguments to the anyio function so users can pass kwargs to their function.
173
252
 
174
- def capture_worker_thread_and_result():
175
- # Captures the worker thread that AnyIO is using to execute the function so
176
- # the main thread can perform actions on it
177
- nonlocal thread, result
178
- try:
179
- thread = threading.current_thread()
180
- result = __fn(*args, **kwargs)
181
- except BaseException as exc:
182
- result = exc
183
- raise
184
- finally:
185
- loop.call_soon_threadsafe(event.set)
253
+ Note that cancellation of threads will not result in interrupted computation, the
254
+ thread may continue running the outcome will just be ignored.
255
+ """
256
+ call = partial(__fn, *args, **kwargs)
257
+ result = await anyio.to_thread.run_sync(
258
+ call_with_mark, call, abandon_on_cancel=True, limiter=get_thread_limiter()
259
+ )
260
+ return result
186
261
 
187
- async def send_interrupt_to_thread():
188
- # This task waits until the result is returned from the thread, if cancellation
189
- # occurs during that time, we will raise the exception in the thread as well
190
- try:
191
- await event.wait()
192
- except anyio.get_cancelled_exc_class():
193
- # NOTE: We could send a SIGINT here which allow us to interrupt system
194
- # calls but the interrupt bubbles from the child thread into the main thread
195
- # and there is not a clear way to prevent it.
196
- raise_async_exception_in_thread(thread, anyio.get_cancelled_exc_class())
197
- raise
198
-
199
- async with anyio.create_task_group() as tg:
200
- tg.start_soon(send_interrupt_to_thread)
201
- tg.start_soon(
202
- partial(
203
- anyio.to_thread.run_sync,
204
- capture_worker_thread_and_result,
205
- cancellable=True,
206
- limiter=get_thread_limiter(),
207
- )
208
- )
209
262
 
210
- assert result is not NotSet
211
- return result
263
+ def call_with_mark(call):
264
+ mark_as_worker_thread()
265
+ return call()
212
266
 
213
267
 
214
268
  def run_async_from_worker_thread(
@@ -226,13 +280,12 @@ def run_async_in_new_loop(__fn: Callable[..., Awaitable[T]], *args: Any, **kwarg
226
280
  return anyio.run(partial(__fn, *args, **kwargs))
227
281
 
228
282
 
283
+ def mark_as_worker_thread():
284
+ _thread_local.is_worker_thread = True
285
+
286
+
229
287
  def in_async_worker_thread() -> bool:
230
- try:
231
- anyio.from_thread.threadlocals.current_async_module
232
- except AttributeError:
233
- return False
234
- else:
235
- return True
288
+ return getattr(_thread_local, "is_worker_thread", False)
236
289
 
237
290
 
238
291
  def in_async_main_thread() -> bool:
@@ -245,7 +298,7 @@ def in_async_main_thread() -> bool:
245
298
  return not in_async_worker_thread()
246
299
 
247
300
 
248
- def sync_compatible(async_fn: T) -> T:
301
+ def sync_compatible(async_fn: T, force_sync: bool = False) -> T:
249
302
  """
250
303
  Converts an async function into a dual async and sync function.
251
304
 
@@ -261,47 +314,53 @@ def sync_compatible(async_fn: T) -> T:
261
314
  """
262
315
 
263
316
  @wraps(async_fn)
264
- def coroutine_wrapper(*args, **kwargs):
265
- from prefect._internal.concurrency.api import create_call, from_sync
266
- from prefect._internal.concurrency.calls import get_current_call, logger
267
- from prefect._internal.concurrency.event_loop import get_running_loop
268
- from prefect._internal.concurrency.threads import get_global_loop
269
- from prefect.settings import PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT
270
-
271
- if PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT:
272
- return async_fn(*args, **kwargs)
273
-
274
- global_thread_portal = get_global_loop()
275
- current_thread = threading.current_thread()
276
- current_call = get_current_call()
277
- current_loop = get_running_loop()
317
+ def coroutine_wrapper(*args, _sync: bool = None, **kwargs):
318
+ from prefect.context import MissingContextError, get_run_context
319
+ from prefect.settings import (
320
+ PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT,
321
+ )
278
322
 
279
- if current_thread.ident == global_thread_portal.thread.ident:
280
- logger.debug(f"{async_fn} --> return coroutine for internal await")
281
- # In the prefect async context; return the coro for us to await
323
+ if PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT or _sync is False:
282
324
  return async_fn(*args, **kwargs)
283
- elif in_async_main_thread() and (
284
- not current_call or is_async_fn(current_call.fn)
285
- ):
286
- # In the main async context; return the coro for them to await
287
- logger.debug(f"{async_fn} --> return coroutine for user await")
288
- return async_fn(*args, **kwargs)
289
- elif in_async_worker_thread():
290
- # In a sync context but we can access the event loop thread; send the async
291
- # call to the parent
292
- return run_async_from_worker_thread(async_fn, *args, **kwargs)
293
- elif current_loop is not None:
294
- logger.debug(f"{async_fn} --> run async in global loop portal")
295
- # An event loop is already present but we are in a sync context, run the
296
- # call in Prefect's event loop thread
297
- return from_sync.call_soon_in_loop_thread(
298
- create_call(async_fn, *args, **kwargs)
299
- ).result()
325
+
326
+ is_async = True
327
+
328
+ # if _sync is set, we do as we're told
329
+ # otherwise, we make some determinations
330
+ if _sync is None:
331
+ try:
332
+ run_ctx = get_run_context()
333
+ parent_obj = getattr(run_ctx, "task", None)
334
+ if not parent_obj:
335
+ parent_obj = getattr(run_ctx, "flow", None)
336
+ is_async = getattr(parent_obj, "isasync", True)
337
+ except MissingContextError:
338
+ # not in an execution context, make best effort to
339
+ # decide whether to syncify
340
+ try:
341
+ asyncio.get_running_loop()
342
+ is_async = True
343
+ except RuntimeError:
344
+ is_async = False
345
+
346
+ async def ctx_call():
347
+ """
348
+ Wrapper that is submitted using copy_context().run to ensure
349
+ mutations of RUNNING_ASYNC_FLAG are tightly scoped to this coroutine's frame.
350
+ """
351
+ token = RUNNING_ASYNC_FLAG.set(True)
352
+ try:
353
+ result = await async_fn(*args, **kwargs)
354
+ finally:
355
+ RUNNING_ASYNC_FLAG.reset(token)
356
+ return result
357
+
358
+ if _sync is True:
359
+ return run_coro_as_sync(ctx_call())
360
+ elif _sync is False or RUNNING_ASYNC_FLAG.get() or is_async:
361
+ return ctx_call()
300
362
  else:
301
- logger.debug(f"{async_fn} --> run async in new loop")
302
- # Run in a new event loop, but use a `Call` for nested context detection
303
- call = create_call(async_fn, *args, **kwargs)
304
- return call()
363
+ return run_coro_as_sync(ctx_call())
305
364
 
306
365
  # TODO: This is breaking type hints on the callable... mypy is behind the curve
307
366
  # on argument annotations. We can still fix this for editors though.
@@ -5,36 +5,33 @@ Utilities for working with Python callables.
5
5
  import ast
6
6
  import importlib.util
7
7
  import inspect
8
+ import warnings
8
9
  from functools import partial
9
10
  from pathlib import Path
10
11
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
11
12
 
12
13
  import cloudpickle
13
-
14
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
15
- from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
16
-
17
- if HAS_PYDANTIC_V2:
18
- import pydantic.v1 as pydantic
19
-
20
- from prefect._internal.pydantic.v2_schema import (
21
- create_v2_schema,
22
- process_v2_params,
23
- )
24
- else:
25
- import pydantic
26
-
14
+ import pydantic
27
15
  from griffe.dataclasses import Docstring
28
16
  from griffe.docstrings.dataclasses import DocstringSectionKind
29
17
  from griffe.docstrings.parsers import Parser, parse
30
18
  from typing_extensions import Literal
31
19
 
20
+ from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
21
+ from prefect._internal.pydantic.v2_schema import (
22
+ create_v2_schema,
23
+ process_v2_params,
24
+ )
32
25
  from prefect.exceptions import (
26
+ MappingLengthMismatch,
27
+ MappingMissingIterable,
33
28
  ParameterBindError,
34
29
  ReservedArgumentError,
35
30
  SignatureMismatchError,
36
31
  )
37
32
  from prefect.logging.loggers import disable_logger, get_logger
33
+ from prefect.utilities.annotations import allow_failure, quote, unmapped
34
+ from prefect.utilities.collections import isiterable
38
35
  from prefect.utilities.importtools import safe_load_namespace
39
36
 
40
37
  logger = get_logger(__name__)
@@ -230,15 +227,14 @@ class ParameterSchema(pydantic.BaseModel):
230
227
  title: Literal["Parameters"] = "Parameters"
231
228
  type: Literal["object"] = "object"
232
229
  properties: Dict[str, Any] = pydantic.Field(default_factory=dict)
233
- required: List[str] = None
234
- definitions: Optional[Dict[str, Any]] = None
230
+ required: List[str] = pydantic.Field(default_factory=list)
231
+ definitions: Dict[str, Any] = pydantic.Field(default_factory=dict)
235
232
 
236
- def dict(self, *args, **kwargs):
237
- """Exclude `None` fields by default to comply with
238
- the OpenAPI spec.
239
- """
240
- kwargs.setdefault("exclude_none", True)
241
- return super().dict(*args, **kwargs)
233
+ def model_dump_for_openapi(self) -> Dict[str, Any]:
234
+ result = self.model_dump(mode="python", exclude_none=True)
235
+ if "required" in result and not result["required"]:
236
+ del result["required"]
237
+ return result
242
238
 
243
239
 
244
240
  def parameter_docstrings(docstring: Optional[str]) -> Dict[str, str]:
@@ -286,21 +282,31 @@ def process_v1_params(
286
282
  name = param.name
287
283
 
288
284
  type_ = Any if param.annotation is inspect._empty else param.annotation
289
- field = pydantic.Field(
290
- default=... if param.default is param.empty else param.default,
291
- title=param.name,
292
- description=docstrings.get(param.name, None),
293
- alias=aliases.get(name),
294
- position=position,
295
- )
285
+
286
+ with warnings.catch_warnings():
287
+ warnings.filterwarnings(
288
+ "ignore", category=pydantic.warnings.PydanticDeprecatedSince20
289
+ )
290
+ field = pydantic.Field(
291
+ default=... if param.default is param.empty else param.default,
292
+ title=param.name,
293
+ description=docstrings.get(param.name, None),
294
+ alias=aliases.get(name),
295
+ position=position,
296
+ )
296
297
  return name, type_, field
297
298
 
298
299
 
299
300
  def create_v1_schema(name_: str, model_cfg, **model_fields):
300
- model: "pydantic.BaseModel" = pydantic.create_model(
301
- name_, __config__=model_cfg, **model_fields
302
- )
303
- return model.schema(by_alias=True)
301
+ with warnings.catch_warnings():
302
+ warnings.filterwarnings(
303
+ "ignore", category=pydantic.warnings.PydanticDeprecatedSince20
304
+ )
305
+
306
+ model: "pydantic.BaseModel" = pydantic.create_model(
307
+ name_, __config__=model_cfg, **model_fields
308
+ )
309
+ return model.schema(by_alias=True)
304
310
 
305
311
 
306
312
  def parameter_schema(fn: Callable) -> ParameterSchema:
@@ -381,16 +387,20 @@ def generate_parameter_schema(
381
387
  model_fields = {}
382
388
  aliases = {}
383
389
 
384
- class ModelConfig:
385
- arbitrary_types_allowed = True
386
-
387
- if HAS_PYDANTIC_V2 and not has_v1_type_as_param(signature):
390
+ if not has_v1_type_as_param(signature):
388
391
  create_schema = create_v2_schema
389
392
  process_params = process_v2_params
393
+
394
+ config = pydantic.ConfigDict(arbitrary_types_allowed=True)
390
395
  else:
391
396
  create_schema = create_v1_schema
392
397
  process_params = process_v1_params
393
398
 
399
+ class ModelConfig:
400
+ arbitrary_types_allowed = True
401
+
402
+ config = ModelConfig
403
+
394
404
  for position, param in enumerate(signature.parameters.values()):
395
405
  name, type_, field = process_params(
396
406
  param, position=position, docstrings=docstrings, aliases=aliases
@@ -398,16 +408,14 @@ def generate_parameter_schema(
398
408
  # Generate a Pydantic model at each step so we can check if this parameter
399
409
  # type supports schema generation
400
410
  try:
401
- create_schema(
402
- "CheckParameter", model_cfg=ModelConfig, **{name: (type_, field)}
403
- )
411
+ create_schema("CheckParameter", model_cfg=config, **{name: (type_, field)})
404
412
  except (ValueError, TypeError):
405
413
  # This field's type is not valid for schema creation, update it to `Any`
406
414
  type_ = Any
407
415
  model_fields[name] = (type_, field)
408
416
 
409
417
  # Generate the final model and schema
410
- schema = create_schema("Parameters", model_cfg=ModelConfig, **model_fields)
418
+ schema = create_schema("Parameters", model_cfg=config, **model_fields)
411
419
  return ParameterSchema(**schema)
412
420
 
413
421
 
@@ -550,3 +558,75 @@ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str
550
558
  ):
551
559
  return func_def.body[0].value.value
552
560
  return None
561
+
562
+
563
+ def expand_mapping_parameters(
564
+ func: Callable, parameters: Dict[str, Any]
565
+ ) -> List[Dict[str, Any]]:
566
+ """
567
+ Generates a list of call parameters to be used for individual calls in a mapping
568
+ operation.
569
+
570
+ Args:
571
+ func: The function to be called
572
+ parameters: A dictionary of parameters with iterables to be mapped over
573
+
574
+ Returns:
575
+ List: A list of dictionaries to be used as parameters for each
576
+ call in the mapping operation
577
+ """
578
+ # Ensure that any parameters in kwargs are expanded before this check
579
+ parameters = explode_variadic_parameter(func, parameters)
580
+
581
+ iterable_parameters = {}
582
+ static_parameters = {}
583
+ annotated_parameters = {}
584
+ for key, val in parameters.items():
585
+ if isinstance(val, (allow_failure, quote)):
586
+ # Unwrap annotated parameters to determine if they are iterable
587
+ annotated_parameters[key] = val
588
+ val = val.unwrap()
589
+
590
+ if isinstance(val, unmapped):
591
+ static_parameters[key] = val.value
592
+ elif isiterable(val):
593
+ iterable_parameters[key] = list(val)
594
+ else:
595
+ static_parameters[key] = val
596
+
597
+ if not len(iterable_parameters):
598
+ raise MappingMissingIterable(
599
+ "No iterable parameters were received. Parameters for map must "
600
+ f"include at least one iterable. Parameters: {parameters}"
601
+ )
602
+
603
+ iterable_parameter_lengths = {
604
+ key: len(val) for key, val in iterable_parameters.items()
605
+ }
606
+ lengths = set(iterable_parameter_lengths.values())
607
+ if len(lengths) > 1:
608
+ raise MappingLengthMismatch(
609
+ "Received iterable parameters with different lengths. Parameters for map"
610
+ f" must all be the same length. Got lengths: {iterable_parameter_lengths}"
611
+ )
612
+
613
+ map_length = list(lengths)[0]
614
+
615
+ call_parameters_list = []
616
+ for i in range(map_length):
617
+ call_parameters = {key: value[i] for key, value in iterable_parameters.items()}
618
+ call_parameters.update({key: value for key, value in static_parameters.items()})
619
+
620
+ # Add default values for parameters; these are skipped earlier since they should
621
+ # not be mapped over
622
+ for key, value in get_parameter_defaults(func).items():
623
+ call_parameters.setdefault(key, value)
624
+
625
+ # Re-apply annotations to each key again
626
+ for key, annotation in annotated_parameters.items():
627
+ call_parameters[key] = annotation.rewrap(call_parameters[key])
628
+
629
+ # Collapse any previously exploded kwargs
630
+ call_parameters_list.append(collapse_variadic_parameters(func, call_parameters))
631
+
632
+ return call_parameters_list