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
prefect/task_runners.py CHANGED
@@ -1,365 +1,461 @@
1
- """
2
- Interface and implementations of various task runners.
3
-
4
- [Task Runners](/concepts/task-runners/) in Prefect are responsible for managing the execution of Prefect task runs. Generally speaking, users are not expected to interact with task runners outside of configuring and initializing them for a flow.
5
-
6
- Example:
7
- ```
8
- >>> from prefect import flow, task
9
- >>> from prefect.task_runners import SequentialTaskRunner
10
- >>> from typing import List
11
- >>>
12
- >>> @task
13
- >>> def say_hello(name):
14
- ... print(f"hello {name}")
15
- >>>
16
- >>> @task
17
- >>> def say_goodbye(name):
18
- ... print(f"goodbye {name}")
19
- >>>
20
- >>> @flow(task_runner=SequentialTaskRunner())
21
- >>> def greetings(names: List[str]):
22
- ... for name in names:
23
- ... say_hello(name)
24
- ... say_goodbye(name)
25
- >>>
26
- >>> greetings(["arthur", "trillian", "ford", "marvin"])
27
- hello arthur
28
- goodbye arthur
29
- hello trillian
30
- goodbye trillian
31
- hello ford
32
- goodbye ford
33
- hello marvin
34
- goodbye marvin
35
- ```
36
-
37
- Switching to a `DaskTaskRunner`:
38
- ```
39
- >>> from prefect_dask.task_runners import DaskTaskRunner
40
- >>> flow.task_runner = DaskTaskRunner()
41
- >>> greetings(["arthur", "trillian", "ford", "marvin"])
42
- hello arthur
43
- goodbye arthur
44
- hello trillian
45
- hello ford
46
- goodbye marvin
47
- hello marvin
48
- goodbye ford
49
- goodbye trillian
50
- ```
51
-
52
- For usage details, see the [Task Runners](/concepts/task-runners/) documentation.
53
- """
54
1
  import abc
55
- from contextlib import AsyncExitStack, asynccontextmanager
2
+ import asyncio
3
+ import sys
4
+ import threading
5
+ import uuid
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from contextvars import copy_context
56
8
  from typing import (
57
9
  TYPE_CHECKING,
58
10
  Any,
59
- AsyncIterator,
60
- Awaitable,
61
- Callable,
11
+ Coroutine,
62
12
  Dict,
13
+ Generic,
14
+ Iterable,
15
+ List,
63
16
  Optional,
64
17
  Set,
65
- TypeVar,
18
+ overload,
66
19
  )
67
- from uuid import UUID
68
20
 
69
- import anyio
21
+ from typing_extensions import ParamSpec, Self, TypeVar
70
22
 
71
- from prefect._internal.concurrency.primitives import Event
72
- from prefect.client.schemas.objects import State
73
- from prefect.logging import get_logger
74
- from prefect.states import exception_to_crashed_state
75
- from prefect.utilities.collections import AutoEnum
23
+ from prefect.client.schemas.objects import TaskRunInput
24
+ from prefect.exceptions import MappingLengthMismatch, MappingMissingIterable
25
+ from prefect.futures import (
26
+ PrefectConcurrentFuture,
27
+ PrefectDistributedFuture,
28
+ PrefectFuture,
29
+ PrefectFutureList,
30
+ )
31
+ from prefect.logging.loggers import get_logger, get_run_logger
32
+ from prefect.utilities.annotations import allow_failure, quote, unmapped
33
+ from prefect.utilities.callables import (
34
+ collapse_variadic_parameters,
35
+ explode_variadic_parameter,
36
+ get_parameter_defaults,
37
+ )
38
+ from prefect.utilities.collections import isiterable
76
39
 
77
40
  if TYPE_CHECKING:
78
- import anyio.abc
41
+ from prefect.tasks import Task
79
42
 
80
-
81
- T = TypeVar("T", bound="BaseTaskRunner")
43
+ P = ParamSpec("P")
44
+ T = TypeVar("T")
82
45
  R = TypeVar("R")
46
+ F = TypeVar("F", bound=PrefectFuture, default=PrefectConcurrentFuture)
83
47
 
84
48
 
85
- class TaskConcurrencyType(AutoEnum):
86
- SEQUENTIAL = AutoEnum.auto()
87
- CONCURRENT = AutoEnum.auto()
88
- PARALLEL = AutoEnum.auto()
89
-
49
+ class TaskRunner(abc.ABC, Generic[F]):
50
+ """
51
+ Abstract base class for task runners.
90
52
 
91
- CONCURRENCY_MESSAGES = {
92
- TaskConcurrencyType.SEQUENTIAL: "sequentially",
93
- TaskConcurrencyType.CONCURRENT: "concurrently",
94
- TaskConcurrencyType.PARALLEL: "in parallel",
95
- }
53
+ A task runner is responsible for submitting tasks to the task run engine running
54
+ in an execution environment. Submitted tasks are non-blocking and return a future
55
+ object that can be used to wait for the task to complete and retrieve the result.
96
56
 
57
+ Task runners are context managers and should be used in a `with` block to ensure
58
+ proper cleanup of resources.
59
+ """
97
60
 
98
- class BaseTaskRunner(metaclass=abc.ABCMeta):
99
- def __init__(self) -> None:
61
+ def __init__(self):
100
62
  self.logger = get_logger(f"task_runner.{self.name}")
101
- self._started: bool = False
102
-
103
- @property
104
- @abc.abstractmethod
105
- def concurrency_type(self) -> TaskConcurrencyType:
106
- pass # noqa
63
+ self._started = False
107
64
 
108
65
  @property
109
66
  def name(self):
67
+ """The name of this task runner"""
110
68
  return type(self).__name__.lower().replace("taskrunner", "")
111
69
 
112
- def duplicate(self):
113
- """
114
- Return a new task runner instance with the same options.
115
- """
116
- # The base class returns `NotImplemented` to indicate that this is not yet
117
- # implemented by a given task runner.
118
- return NotImplemented
119
-
120
- def __eq__(self, other: object) -> bool:
121
- """
122
- Returns true if the task runners use the same options.
123
- """
124
- if type(other) == type(self) and (
125
- # Compare public attributes for naive equality check
126
- # Subclasses should implement this method with a check init option equality
127
- {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
128
- == {k: v for k, v in other.__dict__.items() if not k.startswith("_")}
129
- ):
130
- return True
131
- else:
132
- return NotImplemented
70
+ @abc.abstractmethod
71
+ def duplicate(self) -> Self:
72
+ """Return a new instance of this task runner with the same configuration."""
73
+ ...
133
74
 
134
75
  @abc.abstractmethod
135
- async def submit(
76
+ def submit(
136
77
  self,
137
- key: UUID,
138
- call: Callable[..., Awaitable[State[R]]],
139
- ) -> None:
78
+ task: "Task",
79
+ parameters: Dict[str, Any],
80
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
81
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
82
+ ) -> F:
140
83
  """
141
- Submit a call for execution and return a `PrefectFuture` that can be used to
142
- get the call result.
84
+ Submit a task to the task run engine.
143
85
 
144
86
  Args:
145
- task_run: The task run being submitted.
146
- task_key: A unique key for this orchestration run of the task. Can be used
147
- for caching.
148
- call: The function to be executed
149
- run_kwargs: A dict of keyword arguments to pass to `call`
87
+ task: The task to submit.
88
+ parameters: The parameters to use when running the task.
89
+ wait_for: A list of futures that the task depends on.
150
90
 
151
91
  Returns:
152
- A future representing the result of `call` execution
92
+ A future object that can be used to wait for the task to complete and
93
+ retrieve the result.
153
94
  """
154
- raise NotImplementedError()
95
+ ...
155
96
 
156
- @abc.abstractmethod
157
- async def wait(self, key: UUID, timeout: float = None) -> Optional[State]:
97
+ def map(
98
+ self,
99
+ task: "Task",
100
+ parameters: Dict[str, Any],
101
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
102
+ ) -> PrefectFutureList[F]:
158
103
  """
159
- Given a `PrefectFuture`, wait for its return state up to `timeout` seconds.
160
- If it is not finished after the timeout expires, `None` should be returned.
104
+ Submit multiple tasks to the task run engine.
161
105
 
162
- Implementers should be careful to ensure that this function never returns or
163
- raises an exception.
164
- """
165
- raise NotImplementedError()
106
+ Args:
107
+ task: The task to submit.
108
+ parameters: The parameters to use when running the task.
109
+ wait_for: A list of futures that the task depends on.
166
110
 
167
- @asynccontextmanager
168
- async def start(
169
- self: T,
170
- ) -> AsyncIterator[T]:
111
+ Returns:
112
+ An iterable of future objects that can be used to wait for the tasks to
113
+ complete and retrieve the results.
171
114
  """
172
- Start the task runner, preparing any resources necessary for task submission.
115
+ if not self._started:
116
+ raise RuntimeError(
117
+ "The task runner must be started before submitting work."
118
+ )
173
119
 
174
- Children should implement `_start` to prepare and clean up resources.
120
+ from prefect.utilities.engine import (
121
+ collect_task_run_inputs_sync,
122
+ resolve_inputs_sync,
123
+ )
175
124
 
176
- Yields:
177
- The prepared task runner
178
- """
179
- if self._started:
180
- raise RuntimeError("The task runner is already started!")
181
-
182
- async with AsyncExitStack() as exit_stack:
183
- self.logger.debug("Starting task runner...")
184
- try:
185
- await self._start(exit_stack)
186
- self._started = True
187
- yield self
188
- finally:
189
- self.logger.debug("Shutting down task runner...")
190
- self._started = False
191
-
192
- async def _start(self, exit_stack: AsyncExitStack) -> None:
193
- """
194
- Create any resources required for this task runner to submit work.
125
+ # We need to resolve some futures to map over their data, collect the upstream
126
+ # links beforehand to retain relationship tracking.
127
+ task_inputs = {
128
+ k: collect_task_run_inputs_sync(v, max_depth=0)
129
+ for k, v in parameters.items()
130
+ }
131
+
132
+ # Resolve the top-level parameters in order to get mappable data of a known length.
133
+ # Nested parameters will be resolved in each mapped child where their relationships
134
+ # will also be tracked.
135
+ parameters = resolve_inputs_sync(parameters, max_depth=0)
136
+
137
+ # Ensure that any parameters in kwargs are expanded before this check
138
+ parameters = explode_variadic_parameter(task.fn, parameters)
139
+
140
+ iterable_parameters = {}
141
+ static_parameters = {}
142
+ annotated_parameters = {}
143
+ for key, val in parameters.items():
144
+ if isinstance(val, (allow_failure, quote)):
145
+ # Unwrap annotated parameters to determine if they are iterable
146
+ annotated_parameters[key] = val
147
+ val = val.unwrap()
148
+
149
+ if isinstance(val, unmapped):
150
+ static_parameters[key] = val.value
151
+ elif isiterable(val):
152
+ iterable_parameters[key] = list(val)
153
+ else:
154
+ static_parameters[key] = val
155
+
156
+ if not len(iterable_parameters):
157
+ raise MappingMissingIterable(
158
+ "No iterable parameters were received. Parameters for map must "
159
+ f"include at least one iterable. Parameters: {parameters}"
160
+ )
195
161
 
196
- Cleanup of resources should be submitted to the `exit_stack`.
197
- """
198
- pass # noqa
162
+ iterable_parameter_lengths = {
163
+ key: len(val) for key, val in iterable_parameters.items()
164
+ }
165
+ lengths = set(iterable_parameter_lengths.values())
166
+ if len(lengths) > 1:
167
+ raise MappingLengthMismatch(
168
+ "Received iterable parameters with different lengths. Parameters for map"
169
+ f" must all be the same length. Got lengths: {iterable_parameter_lengths}"
170
+ )
199
171
 
200
- def __str__(self) -> str:
201
- return type(self).__name__
172
+ map_length = list(lengths)[0]
202
173
 
174
+ futures: List[PrefectFuture] = []
175
+ for i in range(map_length):
176
+ call_parameters = {
177
+ key: value[i] for key, value in iterable_parameters.items()
178
+ }
179
+ call_parameters.update(
180
+ {key: value for key, value in static_parameters.items()}
181
+ )
203
182
 
204
- class SequentialTaskRunner(BaseTaskRunner):
205
- """
206
- A simple task runner that executes calls as they are submitted.
183
+ # Add default values for parameters; these are skipped earlier since they should
184
+ # not be mapped over
185
+ for key, value in get_parameter_defaults(task.fn).items():
186
+ call_parameters.setdefault(key, value)
187
+
188
+ # Re-apply annotations to each key again
189
+ for key, annotation in annotated_parameters.items():
190
+ call_parameters[key] = annotation.rewrap(call_parameters[key])
191
+
192
+ # Collapse any previously exploded kwargs
193
+ call_parameters = collapse_variadic_parameters(task.fn, call_parameters)
194
+
195
+ futures.append(
196
+ self.submit(
197
+ task=task,
198
+ parameters=call_parameters,
199
+ wait_for=wait_for,
200
+ dependencies=task_inputs,
201
+ )
202
+ )
207
203
 
208
- If writing synchronous tasks, this runner will always execute tasks sequentially.
209
- If writing async tasks, this runner will execute tasks sequentially unless grouped
210
- using `anyio.create_task_group` or `asyncio.gather`.
211
- """
204
+ return PrefectFutureList(futures)
212
205
 
213
- def __init__(self) -> None:
214
- super().__init__()
215
- self._results: Dict[str, State] = {}
206
+ def __enter__(self):
207
+ if self._started:
208
+ raise RuntimeError("This task runner is already started")
216
209
 
217
- @property
218
- def concurrency_type(self) -> TaskConcurrencyType:
219
- return TaskConcurrencyType.SEQUENTIAL
210
+ self.logger.debug("Starting task runner")
211
+ self._started = True
212
+ return self
220
213
 
221
- def duplicate(self):
222
- return type(self)()
214
+ def __exit__(self, exc_type, exc_value, traceback):
215
+ self.logger.debug("Stopping task runner")
216
+ self._started = False
223
217
 
224
- async def submit(
225
- self,
226
- key: UUID,
227
- call: Callable[..., Awaitable[State[R]]],
228
- ) -> None:
229
- # Run the function immediately and store the result in memory
230
- try:
231
- result = await call()
232
- except BaseException as exc:
233
- result = await exception_to_crashed_state(exc)
234
218
 
235
- self._results[key] = result
219
+ class ThreadPoolTaskRunner(TaskRunner[PrefectConcurrentFuture]):
220
+ def __init__(self, max_workers: Optional[int] = None):
221
+ super().__init__()
222
+ self._executor: Optional[ThreadPoolExecutor] = None
223
+ self._max_workers = sys.maxsize if max_workers is None else max_workers
224
+ self._cancel_events: Dict[uuid.UUID, threading.Event] = {}
236
225
 
237
- async def wait(self, key: UUID, timeout: float = None) -> Optional[State]:
238
- return self._results[key]
226
+ def duplicate(self) -> "ThreadPoolTaskRunner":
227
+ return type(self)(max_workers=self._max_workers)
239
228
 
229
+ @overload
230
+ def submit(
231
+ self,
232
+ task: "Task[P, Coroutine[Any, Any, R]]",
233
+ parameters: Dict[str, Any],
234
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
235
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
236
+ ) -> PrefectConcurrentFuture[R]:
237
+ ...
238
+
239
+ @overload
240
+ def submit(
241
+ self,
242
+ task: "Task[Any, R]",
243
+ parameters: Dict[str, Any],
244
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
245
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
246
+ ) -> PrefectConcurrentFuture[R]:
247
+ ...
248
+
249
+ def submit(
250
+ self,
251
+ task: "Task",
252
+ parameters: Dict[str, Any],
253
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
254
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
255
+ ):
256
+ """
257
+ Submit a task to the task run engine running in a separate thread.
240
258
 
241
- class ConcurrentTaskRunner(BaseTaskRunner):
242
- """
243
- A concurrent task runner that allows tasks to switch when blocking on IO.
244
- Synchronous tasks will be submitted to a thread pool maintained by `anyio`.
245
-
246
- Example:
247
- ```
248
- Using a thread for concurrency:
249
- >>> from prefect import flow
250
- >>> from prefect.task_runners import ConcurrentTaskRunner
251
- >>> @flow(task_runner=ConcurrentTaskRunner)
252
- >>> def my_flow():
253
- >>> ...
254
- ```
255
- """
259
+ Args:
260
+ task: The task to submit.
261
+ parameters: The parameters to use when running the task.
262
+ wait_for: A list of futures that the task depends on.
256
263
 
257
- def __init__(self):
258
- # TODO: Consider adding `max_workers` support using anyio capacity limiters
264
+ Returns:
265
+ A future object that can be used to wait for the task to complete and
266
+ retrieve the result.
267
+ """
268
+ if not self._started or self._executor is None:
269
+ raise RuntimeError("Task runner is not started")
259
270
 
260
- # Runtime attributes
261
- self._task_group: anyio.abc.TaskGroup = None
262
- self._result_events: Dict[UUID, Event] = {}
263
- self._results: Dict[UUID, Any] = {}
264
- self._keys: Set[UUID] = set()
271
+ from prefect.context import FlowRunContext
272
+ from prefect.task_engine import run_task_async, run_task_sync
265
273
 
266
- super().__init__()
274
+ task_run_id = uuid.uuid4()
275
+ cancel_event = threading.Event()
276
+ self._cancel_events[task_run_id] = cancel_event
277
+ context = copy_context()
267
278
 
268
- @property
269
- def concurrency_type(self) -> TaskConcurrencyType:
270
- return TaskConcurrencyType.CONCURRENT
279
+ flow_run_ctx = FlowRunContext.get()
280
+ if flow_run_ctx:
281
+ get_run_logger(flow_run_ctx).info(
282
+ f"Submitting task {task.name} to thread pool executor..."
283
+ )
284
+ else:
285
+ self.logger.info(f"Submitting task {task.name} to thread pool executor...")
286
+
287
+ submit_kwargs = dict(
288
+ task=task,
289
+ task_run_id=task_run_id,
290
+ parameters=parameters,
291
+ wait_for=wait_for,
292
+ return_type="state",
293
+ dependencies=dependencies,
294
+ context=dict(cancel_event=cancel_event),
295
+ )
271
296
 
272
- def duplicate(self):
273
- return type(self)()
297
+ if task.isasync:
298
+ # TODO: Explore possibly using a long-lived thread with an event loop
299
+ # for better performance
300
+ future = self._executor.submit(
301
+ context.run,
302
+ asyncio.run,
303
+ run_task_async(**submit_kwargs),
304
+ )
305
+ else:
306
+ future = self._executor.submit(
307
+ context.run,
308
+ run_task_sync,
309
+ **submit_kwargs,
310
+ )
311
+ prefect_future = PrefectConcurrentFuture(
312
+ task_run_id=task_run_id, wrapped_future=future
313
+ )
314
+ return prefect_future
274
315
 
275
- async def submit(
316
+ @overload
317
+ def map(
276
318
  self,
277
- key: UUID,
278
- call: Callable[[], Awaitable[State[R]]],
279
- ) -> None:
280
- if not self._started:
281
- raise RuntimeError(
282
- "The task runner must be started before submitting work."
283
- )
319
+ task: "Task[P, Coroutine[Any, Any, R]]",
320
+ parameters: Dict[str, Any],
321
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
322
+ ) -> PrefectFutureList[PrefectConcurrentFuture[R]]:
323
+ ...
324
+
325
+ @overload
326
+ def map(
327
+ self,
328
+ task: "Task[Any, R]",
329
+ parameters: Dict[str, Any],
330
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
331
+ ) -> PrefectFutureList[PrefectConcurrentFuture[R]]:
332
+ ...
284
333
 
285
- if not self._task_group:
286
- raise RuntimeError(
287
- "The concurrent task runner cannot be used to submit work after "
288
- "serialization."
289
- )
334
+ def map(
335
+ self,
336
+ task: "Task",
337
+ parameters: Dict[str, Any],
338
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
339
+ ):
340
+ return super().map(task, parameters, wait_for)
290
341
 
291
- # Create an event to set on completion
292
- self._result_events[key] = Event()
342
+ def cancel_all(self):
343
+ for event in self._cancel_events.values():
344
+ event.set()
345
+ self.logger.debug("Set cancel event")
293
346
 
294
- # Rely on the event loop for concurrency
295
- self._task_group.start_soon(self._run_and_store_result, key, call)
347
+ if self._executor is not None:
348
+ self._executor.shutdown(cancel_futures=True)
349
+ self._executor = None
296
350
 
297
- async def wait(
298
- self,
299
- key: UUID,
300
- timeout: float = None,
301
- ) -> Optional[State]:
302
- if not self._task_group:
303
- raise RuntimeError(
304
- "The concurrent task runner cannot be used to wait for work after "
305
- "serialization."
306
- )
351
+ def __enter__(self):
352
+ super().__enter__()
353
+ self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
354
+ return self
307
355
 
308
- return await self._get_run_result(key, timeout)
356
+ def __exit__(self, exc_type, exc_value, traceback):
357
+ self.cancel_all()
358
+ if self._executor is not None:
359
+ self._executor.shutdown(cancel_futures=True)
360
+ self._executor = None
361
+ super().__exit__(exc_type, exc_value, traceback)
309
362
 
310
- async def _run_and_store_result(
311
- self, key: UUID, call: Callable[[], Awaitable[State[R]]]
312
- ):
313
- """
314
- Simple utility to store the orchestration result in memory on completion
363
+ def __eq__(self, value: object) -> bool:
364
+ if not isinstance(value, ThreadPoolTaskRunner):
365
+ return False
366
+ return self._max_workers == value._max_workers
315
367
 
316
- Since this run is occurring on the main thread, we capture exceptions to prevent
317
- task crashes from crashing the flow run.
318
- """
319
- try:
320
- result = await call()
321
- except BaseException as exc:
322
- result = await exception_to_crashed_state(exc)
323
368
 
324
- self._results[key] = result
325
- self._result_events[key].set()
369
+ # Here, we alias ConcurrentTaskRunner to ThreadPoolTaskRunner for backwards compatibility
370
+ ConcurrentTaskRunner = ThreadPoolTaskRunner
326
371
 
327
- async def _get_run_result(
328
- self, key: UUID, timeout: float = None
329
- ) -> Optional[State]:
330
- """
331
- Block until the run result has been populated.
332
- """
333
- result = None # retval on timeout
334
372
 
335
- # Note we do not use `asyncio.wrap_future` and instead use an `Event` to avoid
336
- # stdlib behavior where the wrapped future is cancelled if the parent future is
337
- # cancelled (as it would be during a timeout here)
338
- with anyio.move_on_after(timeout):
339
- await self._result_events[key].wait()
340
- result = self._results[key]
373
+ class PrefectTaskRunner(TaskRunner[PrefectDistributedFuture]):
374
+ def __init__(self):
375
+ super().__init__()
341
376
 
342
- return result # timeout reached
377
+ def duplicate(self) -> "PrefectTaskRunner":
378
+ return type(self)()
343
379
 
344
- async def _start(self, exit_stack: AsyncExitStack):
380
+ @overload
381
+ def submit(
382
+ self,
383
+ task: "Task[P, Coroutine[Any, Any, R]]",
384
+ parameters: Dict[str, Any],
385
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
386
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
387
+ ) -> PrefectDistributedFuture[R]:
388
+ ...
389
+
390
+ @overload
391
+ def submit(
392
+ self,
393
+ task: "Task[Any, R]",
394
+ parameters: Dict[str, Any],
395
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
396
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
397
+ ) -> PrefectDistributedFuture[R]:
398
+ ...
399
+
400
+ def submit(
401
+ self,
402
+ task: "Task",
403
+ parameters: Dict[str, Any],
404
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
405
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
406
+ ):
345
407
  """
346
- Start the process pool
408
+ Submit a task to the task run engine running in a separate thread.
409
+
410
+ Args:
411
+ task: The task to submit.
412
+ parameters: The parameters to use when running the task.
413
+ wait_for: A list of futures that the task depends on.
414
+
415
+ Returns:
416
+ A future object that can be used to wait for the task to complete and
417
+ retrieve the result.
347
418
  """
348
- self._task_group = await exit_stack.enter_async_context(
349
- anyio.create_task_group()
419
+ if not self._started:
420
+ raise RuntimeError("Task runner is not started")
421
+ from prefect.context import FlowRunContext
422
+
423
+ flow_run_ctx = FlowRunContext.get()
424
+ if flow_run_ctx:
425
+ get_run_logger(flow_run_ctx).info(
426
+ f"Submitting task {task.name} to for execution by a Prefect task worker..."
427
+ )
428
+ else:
429
+ self.logger.info(
430
+ f"Submitting task {task.name} to for execution by a Prefect task worker..."
431
+ )
432
+
433
+ return task.apply_async(
434
+ kwargs=parameters, wait_for=wait_for, dependencies=dependencies
350
435
  )
351
436
 
352
- def __getstate__(self):
353
- """
354
- Allow the `ConcurrentTaskRunner` to be serialized by dropping the task group.
355
- """
356
- data = self.__dict__.copy()
357
- data.update({k: None for k in {"_task_group"}})
358
- return data
437
+ @overload
438
+ def map(
439
+ self,
440
+ task: "Task[P, Coroutine[Any, Any, R]]",
441
+ parameters: Dict[str, Any],
442
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
443
+ ) -> PrefectFutureList[PrefectDistributedFuture[R]]:
444
+ ...
445
+
446
+ @overload
447
+ def map(
448
+ self,
449
+ task: "Task[Any, R]",
450
+ parameters: Dict[str, Any],
451
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
452
+ ) -> PrefectFutureList[PrefectDistributedFuture[R]]:
453
+ ...
359
454
 
360
- def __setstate__(self, data: dict):
361
- """
362
- When deserialized, we will no longer have a reference to the task group.
363
- """
364
- self.__dict__.update(data)
365
- self._task_group = None
455
+ def map(
456
+ self,
457
+ task: "Task",
458
+ parameters: Dict[str, Any],
459
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
460
+ ):
461
+ return super().map(task, parameters, wait_for)