prefect-client 2.20.2__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +423 -164
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +667 -440
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2466
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +124 -51
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +138 -48
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.2.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.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,25 +18,31 @@ from typing import (
20
18
  Dict,
21
19
  List,
22
20
  Optional,
23
- Type,
24
21
  TypeVar,
25
22
  Union,
26
23
  cast,
24
+ overload,
27
25
  )
28
26
  from uuid import UUID, uuid4
29
27
 
30
28
  import anyio
31
29
  import anyio.abc
30
+ import anyio.from_thread
32
31
  import anyio.to_thread
33
32
  import sniffio
34
- from anyio.from_thread import start_blocking_portal
35
33
  from typing_extensions import Literal, ParamSpec, TypeGuard
36
34
 
35
+ from prefect._internal.concurrency.api import _cast_to_call, from_sync
36
+ from prefect._internal.concurrency.threads import (
37
+ get_run_sync_loop,
38
+ in_run_sync_loop,
39
+ )
37
40
  from prefect.logging import get_logger
38
41
 
39
42
  T = TypeVar("T")
40
43
  P = ParamSpec("P")
41
44
  R = TypeVar("R")
45
+ F = TypeVar("F", bound=Callable[..., Any])
42
46
  Async = Literal[True]
43
47
  Sync = Literal[False]
44
48
  A = TypeVar("A", Async, Sync, covariant=True)
@@ -48,6 +52,14 @@ EVENT_LOOP_GC_REFS = {}
48
52
 
49
53
  PREFECT_THREAD_LIMITER: Optional[anyio.CapacityLimiter] = None
50
54
 
55
+ RUNNING_IN_RUN_SYNC_LOOP_FLAG = ContextVar("running_in_run_sync_loop", default=False)
56
+ RUNNING_ASYNC_FLAG = ContextVar("run_async", default=False)
57
+ BACKGROUND_TASKS: set[asyncio.Task] = set()
58
+ background_task_lock = threading.Lock()
59
+
60
+ # Thread-local storage to keep track of worker thread state
61
+ _thread_local = threading.local()
62
+
51
63
  logger = get_logger()
52
64
 
53
65
 
@@ -84,12 +96,47 @@ def is_async_gen_fn(func):
84
96
  return inspect.isasyncgenfunction(func)
85
97
 
86
98
 
87
- def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
99
+ def create_task(coroutine: Coroutine) -> asyncio.Task:
88
100
  """
89
- Runs a coroutine from a synchronous context. A thread will be spawned
90
- to run the event loop if necessary, which allows coroutines to run in
91
- environments like Jupyter notebooks where the event loop runs on the main
92
- thread.
101
+ Replacement for asyncio.create_task that will ensure that tasks aren't
102
+ garbage collected before they complete. Allows for "fire and forget"
103
+ behavior in which tasks can be created and the application can move on.
104
+ Tasks can also be awaited normally.
105
+
106
+ See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
107
+ for details (and essentially this implementation)
108
+ """
109
+
110
+ task = asyncio.create_task(coroutine)
111
+
112
+ # Add task to the set. This creates a strong reference.
113
+ # Take a lock because this might be done from multiple threads.
114
+ with background_task_lock:
115
+ BACKGROUND_TASKS.add(task)
116
+
117
+ # To prevent keeping references to finished tasks forever,
118
+ # make each task remove its own reference from the set after
119
+ # completion:
120
+ task.add_done_callback(BACKGROUND_TASKS.discard)
121
+
122
+ return task
123
+
124
+
125
+ def _run_sync_in_new_thread(coroutine: Coroutine[Any, Any, T]) -> T:
126
+ """
127
+ Note: this is an OLD implementation of `run_coro_as_sync` which liberally created
128
+ new threads and new loops. This works, but prevents sharing any objects
129
+ across coroutines, in particular httpx clients, which are very expensive to
130
+ instantiate.
131
+
132
+ This is here for historical purposes and can be removed if/when it is no
133
+ longer needed for reference.
134
+
135
+ ---
136
+
137
+ Runs a coroutine from a synchronous context. A thread will be spawned to run
138
+ the event loop if necessary, which allows coroutines to run in environments
139
+ like Jupyter notebooks where the event loop runs on the main thread.
93
140
 
94
141
  Args:
95
142
  coroutine: The coroutine to run.
@@ -98,15 +145,25 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
98
145
  The return value of the coroutine.
99
146
 
100
147
  Example:
101
- Basic usage:
102
- ```python
103
- async def my_async_function(x: int) -> int:
148
+ Basic usage: ```python async def my_async_function(x: int) -> int:
104
149
  return x + 1
105
150
 
106
- run_sync(my_async_function(1))
107
- ```
151
+ run_sync(my_async_function(1)) ```
108
152
  """
153
+
109
154
  # ensure context variables are properly copied to the async frame
155
+ async def context_local_wrapper():
156
+ """
157
+ Wrapper that is submitted using copy_context().run to ensure
158
+ the RUNNING_ASYNC_FLAG mutations are tightly scoped to this coroutine's frame.
159
+ """
160
+ token = RUNNING_ASYNC_FLAG.set(True)
161
+ try:
162
+ result = await coroutine
163
+ finally:
164
+ RUNNING_ASYNC_FLAG.reset(token)
165
+ return result
166
+
110
167
  context = copy_context()
111
168
  try:
112
169
  loop = asyncio.get_running_loop()
@@ -115,102 +172,111 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
115
172
 
116
173
  if loop and loop.is_running():
117
174
  with ThreadPoolExecutor() as executor:
118
- future = executor.submit(context.run, asyncio.run, coroutine)
119
- return cast(T, future.result())
175
+ future = executor.submit(context.run, asyncio.run, context_local_wrapper())
176
+ result = cast(T, future.result())
120
177
  else:
121
- return context.run(asyncio.run, coroutine)
178
+ result = context.run(asyncio.run, context_local_wrapper())
179
+ return result
122
180
 
123
181
 
124
- async def run_sync_in_worker_thread(
125
- __fn: Callable[..., T], *args: Any, **kwargs: Any
126
- ) -> T:
182
+ def run_coro_as_sync(
183
+ coroutine: Awaitable[R],
184
+ force_new_thread: bool = False,
185
+ wait_for_result: bool = True,
186
+ ) -> Union[R, None]:
127
187
  """
128
- Runs a sync function in a new worker thread so that the main thread's event loop
129
- is not blocked
188
+ Runs a coroutine from a synchronous context, as if it were a synchronous
189
+ function.
130
190
 
131
- Unlike the anyio function, this defaults to a cancellable thread and does not allow
132
- passing arguments to the anyio function so users can pass kwargs to their function.
191
+ The coroutine is scheduled to run in the "run sync" event loop, which is
192
+ running in its own thread and is started the first time it is needed. This
193
+ allows us to share objects like async httpx clients among all coroutines
194
+ running in the loop.
133
195
 
134
- Note that cancellation of threads will not result in interrupted computation, the
135
- thread may continue running the outcome will just be ignored.
136
- """
137
- call = partial(__fn, *args, **kwargs)
138
- return await anyio.to_thread.run_sync(
139
- call, abandon_on_cancel=True, limiter=get_thread_limiter()
140
- )
196
+ If run_sync is called from within the run_sync loop, it will run the
197
+ coroutine in a new thread, because otherwise a deadlock would occur. Note
198
+ that this behavior should not appear anywhere in the Prefect codebase or in
199
+ user code.
141
200
 
201
+ Args:
202
+ coroutine (Awaitable): The coroutine to be run as a synchronous function.
203
+ force_new_thread (bool, optional): If True, the coroutine will always be run in a new thread.
204
+ Defaults to False.
205
+ wait_for_result (bool, optional): If True, the function will wait for the coroutine to complete
206
+ and return the result. If False, the function will submit the coroutine to the "run sync"
207
+ event loop and return immediately, where it will eventually be run. Defaults to True.
142
208
 
143
- def raise_async_exception_in_thread(thread: Thread, exc_type: Type[BaseException]):
209
+ Returns:
210
+ The result of the coroutine if wait_for_result is True, otherwise None.
144
211
  """
145
- Raise an exception in a thread asynchronously.
146
212
 
147
- This will not interrupt long-running system calls like `sleep` or `wait`.
148
- """
149
- ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(
150
- ctypes.c_long(thread.ident), ctypes.py_object(exc_type)
151
- )
152
- if ret == 0:
153
- raise ValueError("Thread not found.")
213
+ async def coroutine_wrapper() -> Union[R, None]:
214
+ """
215
+ Set flags so that children (and grandchildren...) of this task know they are running in a new
216
+ thread and do not try to run on the run_sync thread, which would cause a
217
+ deadlock.
218
+ """
219
+ token1 = RUNNING_IN_RUN_SYNC_LOOP_FLAG.set(True)
220
+ token2 = RUNNING_ASYNC_FLAG.set(True)
221
+ try:
222
+ # use `asyncio.create_task` because it copies context variables automatically
223
+ task = create_task(coroutine)
224
+ if wait_for_result:
225
+ return await task
226
+ finally:
227
+ RUNNING_IN_RUN_SYNC_LOOP_FLAG.reset(token1)
228
+ RUNNING_ASYNC_FLAG.reset(token2)
154
229
 
230
+ # if we are already in the run_sync loop, or a descendent of a coroutine
231
+ # that is running in the run_sync loop, we need to run this coroutine in a
232
+ # new thread
233
+ if in_run_sync_loop() or RUNNING_IN_RUN_SYNC_LOOP_FLAG.get() or force_new_thread:
234
+ return from_sync.call_in_new_thread(coroutine_wrapper)
155
235
 
156
- async def run_sync_in_interruptible_worker_thread(
157
- __fn: Callable[..., T], *args: Any, **kwargs: Any
158
- ) -> T:
159
- """
160
- Runs a sync function in a new interruptible worker thread so that the main
161
- thread's event loop is not blocked
162
-
163
- Unlike the anyio function, this performs best-effort cancellation of the
164
- thread using the C API. Cancellation will not interrupt system calls like
165
- `sleep`.
166
- """
236
+ # otherwise, we can run the coroutine in the run_sync loop
237
+ # and wait for the result
238
+ else:
239
+ call = _cast_to_call(coroutine_wrapper)
240
+ runner = get_run_sync_loop()
241
+ runner.submit(call)
242
+ try:
243
+ return call.result()
244
+ except KeyboardInterrupt:
245
+ call.cancel()
167
246
 
168
- class NotSet:
169
- pass
247
+ logger.debug("Coroutine cancelled due to KeyboardInterrupt.")
248
+ raise
170
249
 
171
- thread: Thread = None
172
- result = NotSet
173
- event = asyncio.Event()
174
- loop = asyncio.get_running_loop()
175
250
 
176
- def capture_worker_thread_and_result():
177
- # Captures the worker thread that AnyIO is using to execute the function so
178
- # the main thread can perform actions on it
179
- nonlocal thread, result
180
- try:
181
- thread = threading.current_thread()
182
- result = __fn(*args, **kwargs)
183
- except BaseException as exc:
184
- result = exc
185
- raise
186
- finally:
187
- loop.call_soon_threadsafe(event.set)
251
+ async def run_sync_in_worker_thread(
252
+ __fn: Callable[..., T], *args: Any, **kwargs: Any
253
+ ) -> T:
254
+ """
255
+ Runs a sync function in a new worker thread so that the main thread's event loop
256
+ is not blocked.
188
257
 
189
- async def send_interrupt_to_thread():
190
- # This task waits until the result is returned from the thread, if cancellation
191
- # occurs during that time, we will raise the exception in the thread as well
192
- try:
193
- await event.wait()
194
- except anyio.get_cancelled_exc_class():
195
- # NOTE: We could send a SIGINT here which allow us to interrupt system
196
- # calls but the interrupt bubbles from the child thread into the main thread
197
- # and there is not a clear way to prevent it.
198
- raise_async_exception_in_thread(thread, anyio.get_cancelled_exc_class())
199
- raise
258
+ Unlike the anyio function, this defaults to a cancellable thread and does not allow
259
+ passing arguments to the anyio function so users can pass kwargs to their function.
200
260
 
201
- async with anyio.create_task_group() as tg:
202
- tg.start_soon(send_interrupt_to_thread)
203
- tg.start_soon(
204
- partial(
205
- anyio.to_thread.run_sync,
206
- capture_worker_thread_and_result,
207
- abandon_on_cancel=True,
208
- limiter=get_thread_limiter(),
209
- )
261
+ Note that cancellation of threads will not result in interrupted computation, the
262
+ thread may continue running — the outcome will just be ignored.
263
+ """
264
+ # When running a sync function in a worker thread, we set this flag so that
265
+ # any root sync compatible functions will run as sync functions
266
+ token = RUNNING_ASYNC_FLAG.set(False)
267
+ try:
268
+ call = partial(__fn, *args, **kwargs)
269
+ result = await anyio.to_thread.run_sync(
270
+ call_with_mark, call, abandon_on_cancel=True, limiter=get_thread_limiter()
210
271
  )
272
+ return result
273
+ finally:
274
+ RUNNING_ASYNC_FLAG.reset(token)
211
275
 
212
- assert result is not NotSet
213
- return result
276
+
277
+ def call_with_mark(call):
278
+ mark_as_worker_thread()
279
+ return call()
214
280
 
215
281
 
216
282
  def run_async_from_worker_thread(
@@ -228,13 +294,12 @@ def run_async_in_new_loop(__fn: Callable[..., Awaitable[T]], *args: Any, **kwarg
228
294
  return anyio.run(partial(__fn, *args, **kwargs))
229
295
 
230
296
 
297
+ def mark_as_worker_thread():
298
+ _thread_local.is_worker_thread = True
299
+
300
+
231
301
  def in_async_worker_thread() -> bool:
232
- try:
233
- anyio.from_thread.threadlocals.current_async_backend
234
- except AttributeError:
235
- return False
236
- else:
237
- return True
302
+ return getattr(_thread_local, "is_worker_thread", False)
238
303
 
239
304
 
240
305
  def in_async_main_thread() -> bool:
@@ -247,7 +312,23 @@ def in_async_main_thread() -> bool:
247
312
  return not in_async_worker_thread()
248
313
 
249
314
 
250
- def sync_compatible(async_fn: T) -> T:
315
+ @overload
316
+ def sync_compatible(
317
+ async_fn: Callable[..., Coroutine[Any, Any, R]],
318
+ ) -> Callable[..., R]:
319
+ ...
320
+
321
+
322
+ @overload
323
+ def sync_compatible(
324
+ async_fn: Callable[..., Coroutine[Any, Any, R]],
325
+ ) -> Callable[..., Coroutine[Any, Any, R]]:
326
+ ...
327
+
328
+
329
+ def sync_compatible(
330
+ async_fn: Callable[..., Coroutine[Any, Any, R]],
331
+ ) -> Callable[..., Union[R, Coroutine[Any, Any, R]]]:
251
332
  """
252
333
  Converts an async function into a dual async and sync function.
253
334
 
@@ -263,55 +344,53 @@ def sync_compatible(async_fn: T) -> T:
263
344
  """
264
345
 
265
346
  @wraps(async_fn)
266
- def coroutine_wrapper(*args, _sync: Optional[bool] = None, **kwargs):
267
- from prefect._internal.concurrency.api import create_call, from_sync
268
- from prefect._internal.concurrency.calls import get_current_call, logger
269
- from prefect._internal.concurrency.event_loop import get_running_loop
270
- from prefect._internal.concurrency.threads import get_global_loop
271
- from prefect.settings import PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT
272
-
273
- if PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT or _sync is False:
274
- return async_fn(*args, **kwargs)
347
+ def coroutine_wrapper(
348
+ *args: Any, _sync: Optional[bool] = None, **kwargs: Any
349
+ ) -> Union[R, Coroutine[Any, Any, R]]:
350
+ from prefect.context import MissingContextError, get_run_context
275
351
 
276
- global_thread_portal = get_global_loop()
277
- current_thread = threading.current_thread()
278
- current_call = get_current_call()
279
- current_loop = get_running_loop()
280
-
281
- if (
282
- current_thread.ident == global_thread_portal.thread.ident
283
- and _sync is not True
284
- ):
285
- logger.debug(f"{async_fn} --> return coroutine for internal await")
286
- # In the prefect async context; return the coro for us to await
352
+ if _sync is False:
287
353
  return async_fn(*args, **kwargs)
288
- elif (
289
- in_async_main_thread()
290
- and (not current_call or is_async_fn(current_call.fn))
291
- and _sync is not True
292
- ):
293
- # In the main async context; return the coro for them to await
294
- logger.debug(f"{async_fn} --> return coroutine for user await")
295
- return async_fn(*args, **kwargs)
296
- elif in_async_worker_thread():
297
- # In a sync context but we can access the event loop thread; send the async
298
- # call to the parent
299
- return run_async_from_worker_thread(async_fn, *args, **kwargs)
300
- elif current_loop is not None:
301
- logger.debug(f"{async_fn} --> run async in global loop portal")
302
- # An event loop is already present but we are in a sync context, run the
303
- # call in Prefect's event loop thread
304
- return from_sync.call_soon_in_loop_thread(
305
- create_call(async_fn, *args, **kwargs)
306
- ).result()
354
+
355
+ is_async = True
356
+
357
+ # if _sync is set, we do as we're told
358
+ # otherwise, we make some determinations
359
+ if _sync is None:
360
+ try:
361
+ run_ctx = get_run_context()
362
+ parent_obj = getattr(run_ctx, "task", None)
363
+ if not parent_obj:
364
+ parent_obj = getattr(run_ctx, "flow", None)
365
+ is_async = getattr(parent_obj, "isasync", True)
366
+ except MissingContextError:
367
+ # not in an execution context, make best effort to
368
+ # decide whether to syncify
369
+ try:
370
+ asyncio.get_running_loop()
371
+ is_async = True
372
+ except RuntimeError:
373
+ is_async = False
374
+
375
+ async def ctx_call():
376
+ """
377
+ Wrapper that is submitted using copy_context().run to ensure
378
+ mutations of RUNNING_ASYNC_FLAG are tightly scoped to this coroutine's frame.
379
+ """
380
+ token = RUNNING_ASYNC_FLAG.set(True)
381
+ try:
382
+ result = await async_fn(*args, **kwargs)
383
+ finally:
384
+ RUNNING_ASYNC_FLAG.reset(token)
385
+ return result
386
+
387
+ if _sync is True:
388
+ return run_coro_as_sync(ctx_call())
389
+ elif _sync is False or RUNNING_ASYNC_FLAG.get() or is_async:
390
+ return ctx_call()
307
391
  else:
308
- logger.debug(f"{async_fn} --> run async in new loop")
309
- # Run in a new event loop, but use a `Call` for nested context detection
310
- call = create_call(async_fn, *args, **kwargs)
311
- return call()
392
+ return run_coro_as_sync(ctx_call())
312
393
 
313
- # TODO: This is breaking type hints on the callable... mypy is behind the curve
314
- # on argument annotations. We can still fix this for editors though.
315
394
  if is_async_fn(async_fn):
316
395
  wrapper = coroutine_wrapper
317
396
  elif is_async_gen_fn(async_fn):
@@ -319,13 +398,13 @@ def sync_compatible(async_fn: T) -> T:
319
398
  else:
320
399
  raise TypeError("The decorated function must be async.")
321
400
 
322
- wrapper.aio = async_fn
401
+ wrapper.aio = async_fn # type: ignore
323
402
  return wrapper
324
403
 
325
404
 
326
405
  @asynccontextmanager
327
- async def asyncnullcontext():
328
- yield
406
+ async def asyncnullcontext(value=None, *args, **kwargs):
407
+ yield value
329
408
 
330
409
 
331
410
  def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T:
@@ -340,7 +419,7 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
340
419
  "`sync` called from an asynchronous context; "
341
420
  "you should `await` the async function directly instead."
342
421
  )
343
- with start_blocking_portal() as portal:
422
+ with anyio.start_blocking_portal() as portal:
344
423
  return portal.call(partial(__async_fn, *args, **kwargs))
345
424
  elif in_async_worker_thread():
346
425
  # In a sync context but we can access the event loop thread; send the async