prefect-client 2.19.2__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 +151 -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 +307 -166
  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 +19 -15
  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 +311 -43
  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 +97 -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.2.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.2.dist-info/RECORD +0 -292
  237. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
  238. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
  239. {prefect_client-2.19.2.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.