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
@@ -6,6 +6,7 @@ import signal
6
6
  import time
7
7
  from functools import partial
8
8
  from typing import (
9
+ TYPE_CHECKING,
9
10
  Any,
10
11
  Callable,
11
12
  Dict,
@@ -24,7 +25,6 @@ import prefect
24
25
  import prefect.context
25
26
  import prefect.plugins
26
27
  from prefect._internal.concurrency.cancellation import get_deadline
27
- from prefect.client.orchestration import PrefectClient, SyncPrefectClient
28
28
  from prefect.client.schemas import OrchestrationResult, TaskRun
29
29
  from prefect.client.schemas.objects import (
30
30
  StateType,
@@ -44,6 +44,7 @@ from prefect.exceptions import (
44
44
  )
45
45
  from prefect.flows import Flow
46
46
  from prefect.futures import PrefectFuture
47
+ from prefect.futures import PrefectFuture as NewPrefectFuture
47
48
  from prefect.logging.loggers import (
48
49
  get_logger,
49
50
  task_run_logger,
@@ -55,17 +56,19 @@ from prefect.settings import (
55
56
  from prefect.states import (
56
57
  State,
57
58
  get_state_exception,
58
- is_state,
59
59
  )
60
60
  from prefect.tasks import Task
61
61
  from prefect.utilities.annotations import allow_failure, quote
62
62
  from prefect.utilities.asyncutils import (
63
63
  gather,
64
- run_sync,
64
+ run_coro_as_sync,
65
65
  )
66
66
  from prefect.utilities.collections import StopVisiting, visit_collection
67
67
  from prefect.utilities.text import truncated_to
68
68
 
69
+ if TYPE_CHECKING:
70
+ from prefect.client.orchestration import PrefectClient, SyncPrefectClient
71
+
69
72
  API_HEALTHCHECKS = {}
70
73
  UNTRACKABLE_TYPES = {bool, type(None), type(...), type(NotImplemented)}
71
74
  engine_logger = get_logger("engine")
@@ -93,7 +96,7 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> Set[TaskRun
93
96
  # We need to wait for futures to be submitted before we can get the task
94
97
  # run id but we want to do so asynchronously
95
98
  futures.add(obj)
96
- elif is_state(obj):
99
+ elif isinstance(obj, State):
97
100
  if obj.state_details.task_run_id:
98
101
  inputs.add(TaskRunResult(id=obj.state_details.task_run_id))
99
102
  # Expressions inside quotes should not be traversed
@@ -118,8 +121,49 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> Set[TaskRun
118
121
  return inputs
119
122
 
120
123
 
124
+ def collect_task_run_inputs_sync(
125
+ expr: Any, future_cls: Any = NewPrefectFuture, max_depth: int = -1
126
+ ) -> Set[TaskRunInput]:
127
+ """
128
+ This function recurses through an expression to generate a set of any discernible
129
+ task run inputs it finds in the data structure. It produces a set of all inputs
130
+ found.
131
+
132
+ Examples:
133
+ >>> task_inputs = {
134
+ >>> k: collect_task_run_inputs(v) for k, v in parameters.items()
135
+ >>> }
136
+ """
137
+ # TODO: This function needs to be updated to detect parameters and constants
138
+
139
+ inputs = set()
140
+
141
+ def add_futures_and_states_to_inputs(obj):
142
+ if isinstance(obj, future_cls) and hasattr(obj, "task_run_id"):
143
+ inputs.add(TaskRunResult(id=obj.task_run_id))
144
+ elif isinstance(obj, State):
145
+ if obj.state_details.task_run_id:
146
+ inputs.add(TaskRunResult(id=obj.state_details.task_run_id))
147
+ # Expressions inside quotes should not be traversed
148
+ elif isinstance(obj, quote):
149
+ raise StopVisiting
150
+ else:
151
+ state = get_state_for_result(obj)
152
+ if state and state.state_details.task_run_id:
153
+ inputs.add(TaskRunResult(id=state.state_details.task_run_id))
154
+
155
+ visit_collection(
156
+ expr,
157
+ visit_fn=add_futures_and_states_to_inputs,
158
+ return_data=False,
159
+ max_depth=max_depth,
160
+ )
161
+
162
+ return inputs
163
+
164
+
121
165
  async def wait_for_task_runs_and_report_crashes(
122
- task_run_futures: Iterable[PrefectFuture], client: PrefectClient
166
+ task_run_futures: Iterable[PrefectFuture], client: "PrefectClient"
123
167
  ) -> Literal[True]:
124
168
  crash_exceptions = []
125
169
 
@@ -225,7 +269,7 @@ async def resolve_inputs(
225
269
 
226
270
  if isinstance(expr, PrefectFuture):
227
271
  futures.add(expr)
228
- if is_state(expr):
272
+ if isinstance(expr, State):
229
273
  states.add(expr)
230
274
 
231
275
  return expr
@@ -264,7 +308,7 @@ async def resolve_inputs(
264
308
 
265
309
  if isinstance(expr, PrefectFuture):
266
310
  state = expr._final_state
267
- elif is_state(expr):
311
+ elif isinstance(expr, State):
268
312
  state = expr
269
313
  else:
270
314
  return expr
@@ -311,7 +355,7 @@ async def resolve_inputs(
311
355
 
312
356
 
313
357
  async def propose_state(
314
- client: PrefectClient,
358
+ client: "PrefectClient",
315
359
  state: State[object],
316
360
  force: bool = False,
317
361
  task_run_id: Optional[UUID] = None,
@@ -412,7 +456,7 @@ async def propose_state(
412
456
 
413
457
 
414
458
  def propose_state_sync(
415
- client: SyncPrefectClient,
459
+ client: "SyncPrefectClient",
416
460
  state: State[object],
417
461
  force: bool = False,
418
462
  task_run_id: Optional[UUID] = None,
@@ -459,7 +503,7 @@ def propose_state_sync(
459
503
  # the purpose of disabling `cache_result_in_memory`
460
504
  result = state.result(raise_on_failure=False, fetch=True)
461
505
  if inspect.isawaitable(result):
462
- result = run_sync(result)
506
+ result = run_coro_as_sync(result)
463
507
  else:
464
508
  result = state.data
465
509
 
@@ -515,7 +559,9 @@ def propose_state_sync(
515
559
 
516
560
 
517
561
  def _dynamic_key_for_task_run(context: FlowRunContext, task: Task) -> int:
518
- if context.flow_run is None: # this is an autonomous task run
562
+ if context.detached: # this task is running on remote infrastructure
563
+ return str(uuid4())
564
+ elif context.flow_run is None: # this is an autonomous task run
519
565
  context.task_run_dynamic_keys[task.task_key] = getattr(
520
566
  task, "dynamic_key", str(uuid4())
521
567
  )
@@ -528,14 +574,6 @@ def _dynamic_key_for_task_run(context: FlowRunContext, task: Task) -> int:
528
574
  return context.task_run_dynamic_keys[task.task_key]
529
575
 
530
576
 
531
- def _observed_flow_pauses(context: FlowRunContext) -> int:
532
- if "counter" not in context.observed_flow_pauses:
533
- context.observed_flow_pauses["counter"] = 1
534
- else:
535
- context.observed_flow_pauses["counter"] += 1
536
- return context.observed_flow_pauses["counter"]
537
-
538
-
539
577
  def get_state_for_result(obj: Any) -> Optional[State]:
540
578
  """
541
579
  Get the state related to a result object.
@@ -664,7 +702,7 @@ def _get_hook_name(hook: Callable) -> str:
664
702
  )
665
703
 
666
704
 
667
- async def check_api_reachable(client: PrefectClient, fail_message: str):
705
+ async def check_api_reachable(client: "PrefectClient", fail_message: str):
668
706
  # Do not perform a healthcheck if it exists and is not expired
669
707
  api_url = str(client.api_url)
670
708
  if api_url in API_HEALTHCHECKS:
@@ -734,3 +772,85 @@ def emit_task_run_state_change_event(
734
772
  },
735
773
  follows=follows,
736
774
  )
775
+
776
+
777
+ def resolve_to_final_result(expr, context):
778
+ """
779
+ Resolve any `PrefectFuture`, or `State` types nested in parameters into
780
+ data. Designed to be use with `visit_collection`.
781
+ """
782
+ state = None
783
+
784
+ # Expressions inside quotes should not be modified
785
+ if isinstance(context.get("annotation"), quote):
786
+ raise StopVisiting()
787
+
788
+ if isinstance(expr, NewPrefectFuture):
789
+ expr.wait()
790
+ state = expr.state
791
+ elif isinstance(expr, State):
792
+ state = expr
793
+ else:
794
+ return expr
795
+
796
+ assert state
797
+
798
+ # Do not allow uncompleted upstreams except failures when `allow_failure` has
799
+ # been used
800
+ if not state.is_completed() and not (
801
+ # TODO: Note that the contextual annotation here is only at the current level
802
+ # if `allow_failure` is used then another annotation is used, this will
803
+ # incorrectly evaluate to false — to resolve this, we must track all
804
+ # annotations wrapping the current expression but this is not yet
805
+ # implemented.
806
+ isinstance(context.get("annotation"), allow_failure) and state.is_failed()
807
+ ):
808
+ raise UpstreamTaskError(
809
+ f"Upstream task run '{state.state_details.task_run_id}' did not reach a"
810
+ " 'COMPLETED' state."
811
+ )
812
+
813
+ _result = state.result(raise_on_failure=False, fetch=True)
814
+ if inspect.isawaitable(_result):
815
+ _result = run_coro_as_sync(_result)
816
+ return _result
817
+
818
+
819
+ def resolve_inputs_sync(
820
+ parameters: Dict[str, Any], return_data: bool = True, max_depth: int = -1
821
+ ) -> Dict[str, Any]:
822
+ """
823
+ Resolve any `Quote`, `PrefectFuture`, or `State` types nested in parameters into
824
+ data.
825
+
826
+ Returns:
827
+ A copy of the parameters with resolved data
828
+
829
+ Raises:
830
+ UpstreamTaskError: If any of the upstream states are not `COMPLETED`
831
+ """
832
+
833
+ if not parameters:
834
+ return {}
835
+
836
+ resolved_parameters = {}
837
+ for parameter, value in parameters.items():
838
+ try:
839
+ resolved_parameters[parameter] = visit_collection(
840
+ value,
841
+ visit_fn=resolve_to_final_result,
842
+ return_data=return_data,
843
+ max_depth=max_depth,
844
+ remove_annotations=True,
845
+ context={},
846
+ )
847
+ except UpstreamTaskError:
848
+ raise
849
+ except Exception as exc:
850
+ raise PrefectException(
851
+ f"Failed to resolve inputs in parameter {parameter!r}. If your"
852
+ " parameter type is not supported, consider using the `quote`"
853
+ " annotation to skip resolution of inputs."
854
+ ) from exc
855
+
856
+ return resolved_parameters
@@ -1,9 +1,10 @@
1
+ import ast
1
2
  import importlib
2
3
  import importlib.util
3
- import inspect
4
4
  import os
5
5
  import runpy
6
6
  import sys
7
+ import warnings
7
8
  from importlib.abc import Loader, MetaPathFinder
8
9
  from importlib.machinery import ModuleSpec
9
10
  from pathlib import Path
@@ -14,8 +15,11 @@ from typing import Any, Callable, Dict, Iterable, NamedTuple, Optional, Union
14
15
  import fsspec
15
16
 
16
17
  from prefect.exceptions import ScriptError
18
+ from prefect.logging.loggers import get_logger
17
19
  from prefect.utilities.filesystem import filename, is_local_path, tmpchdir
18
20
 
21
+ logger = get_logger(__name__)
22
+
19
23
 
20
24
  def to_qualified_name(obj: Any) -> str:
21
25
  """
@@ -224,24 +228,18 @@ class DelayedImportErrorModule(ModuleType):
224
228
  [1]: https://github.com/scientific-python/lazy_loader
225
229
  """
226
230
 
227
- def __init__(self, frame_data, help_message, *args, **kwargs):
228
- self.__frame_data = frame_data
231
+ def __init__(self, error_message, help_message, *args, **kwargs):
232
+ self.__error_message = error_message
229
233
  self.__help_message = (
230
234
  help_message or "Import errors for this module are only reported when used."
231
235
  )
232
236
  super().__init__(*args, **kwargs)
233
237
 
234
238
  def __getattr__(self, attr):
235
- if attr in ("__class__", "__file__", "__frame_data", "__help_message"):
239
+ if attr in ("__class__", "__file__", "__help_message"):
236
240
  super().__getattr__(attr)
237
241
  else:
238
- fd = self.__frame_data
239
- raise ModuleNotFoundError(
240
- f"No module named '{fd['spec']}'\n\nThis module was originally imported"
241
- f" at:\n File \"{fd['filename']}\", line {fd['lineno']}, in"
242
- f" {fd['function']}\n\n {''.join(fd['code_context']).strip()}\n"
243
- + self.__help_message
244
- )
242
+ raise ModuleNotFoundError(self.__error_message)
245
243
 
246
244
 
247
245
  def lazy_import(
@@ -252,6 +250,13 @@ def lazy_import(
252
250
  Use this to retain module-level imports for libraries that we don't want to
253
251
  actually import until they are needed.
254
252
 
253
+ NOTE: Lazy-loading a subpackage can cause the subpackage to be imported
254
+ twice if another non-lazy import also imports the subpackage. For example,
255
+ using both `lazy_import("docker.errors")` and `import docker.errors` in the
256
+ same codebase will import `docker.errors` twice and can lead to unexpected
257
+ behavior, e.g. type check failures and import-time side effects running
258
+ twice.
259
+
255
260
  Adapted from the [Python documentation][1] and [lazy_loader][2]
256
261
 
257
262
  [1]: https://docs.python.org/3/library/importlib.html#implementing-lazy-imports
@@ -263,25 +268,23 @@ def lazy_import(
263
268
  except KeyError:
264
269
  pass
265
270
 
271
+ if "." in name:
272
+ warnings.warn(
273
+ "Lazy importing subpackages can lead to unexpected behavior.",
274
+ RuntimeWarning,
275
+ )
276
+
266
277
  spec = importlib.util.find_spec(name)
278
+
267
279
  if spec is None:
280
+ import_error_message = f"No module named '{name}'.\n{help_message}"
281
+
268
282
  if error_on_import:
269
- raise ModuleNotFoundError(f"No module named '{name}'.\n{help_message}")
270
- else:
271
- try:
272
- parent = inspect.stack()[1]
273
- frame_data = {
274
- "spec": name,
275
- "filename": parent.filename,
276
- "lineno": parent.lineno,
277
- "function": parent.function,
278
- "code_context": parent.code_context,
279
- }
280
- return DelayedImportErrorModule(
281
- frame_data, help_message, "DelayedImportErrorModule"
282
- )
283
- finally:
284
- del parent
283
+ raise ModuleNotFoundError(import_error_message)
284
+
285
+ return DelayedImportErrorModule(
286
+ import_error_message, help_message, "DelayedImportErrorModule"
287
+ )
285
288
 
286
289
  module = importlib.util.module_from_spec(spec)
287
290
  sys.modules[name] = module
@@ -356,3 +359,70 @@ class AliasedModuleLoader(Loader):
356
359
  if self.callback is not None:
357
360
  self.callback(self.alias)
358
361
  sys.modules[self.alias] = root_module
362
+
363
+
364
+ def safe_load_namespace(source_code: str):
365
+ """
366
+ Safely load a namespace from source code.
367
+
368
+ This function will attempt to import all modules and classes defined in the source
369
+ code. If an import fails, the error is caught and the import is skipped. This function
370
+ will also attempt to compile and evaluate class and function definitions locally.
371
+
372
+ Args:
373
+ source_code: The source code to load
374
+
375
+ Returns:
376
+ The namespace loaded from the source code. Can be used when evaluating source
377
+ code.
378
+ """
379
+ parsed_code = ast.parse(source_code)
380
+
381
+ namespace = {}
382
+
383
+ # Walk through the AST and find all import statements
384
+ for node in ast.walk(parsed_code):
385
+ if isinstance(node, ast.Import):
386
+ for alias in node.names:
387
+ module_name = alias.name
388
+ as_name = alias.asname if alias.asname else module_name
389
+ try:
390
+ # Attempt to import the module
391
+ namespace[as_name] = importlib.import_module(module_name)
392
+ logger.debug("Successfully imported %s", module_name)
393
+ except ImportError as e:
394
+ logger.debug(f"Failed to import {module_name}: {e}")
395
+ elif isinstance(node, ast.ImportFrom):
396
+ module_name = node.module
397
+ if module_name is None:
398
+ continue
399
+ try:
400
+ module = importlib.import_module(module_name)
401
+ for alias in node.names:
402
+ name = alias.name
403
+ asname = alias.asname if alias.asname else name
404
+ try:
405
+ # Get the specific attribute from the module
406
+ attribute = getattr(module, name)
407
+ namespace[asname] = attribute
408
+ except AttributeError as e:
409
+ logger.debug(
410
+ "Failed to retrieve %s from %s: %s", name, module_name, e
411
+ )
412
+ except ImportError as e:
413
+ logger.debug("Failed to import from %s: %s", node.module, e)
414
+
415
+ # Handle local class definitions
416
+ for node in ast.walk(parsed_code):
417
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
418
+ try:
419
+ # Compile and execute each class and function definition locally
420
+ code = compile(
421
+ ast.Module(body=[node], type_ignores=[]),
422
+ filename="<ast>",
423
+ mode="exec",
424
+ )
425
+ exec(code, namespace)
426
+ except Exception as e:
427
+ logger.debug("Failed to compile class definition: %s", e)
428
+ return namespace