prefect-client 2.20.4__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 +405 -153
  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 +650 -442
  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 -2475
  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 +117 -47
  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 +137 -45
  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.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.4.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.4.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.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
@@ -5,34 +5,31 @@ Utilities for working with Python callables.
5
5
  import ast
6
6
  import importlib.util
7
7
  import inspect
8
+ import warnings
8
9
  from functools import partial
9
10
  from pathlib import Path
10
11
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
11
12
 
12
13
  import cloudpickle
13
-
14
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
15
- from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
16
-
17
- if HAS_PYDANTIC_V2:
18
- import pydantic.v1 as pydantic
19
-
20
- from prefect._internal.pydantic.v2_schema import (
21
- create_v2_schema,
22
- process_v2_params,
23
- )
24
- else:
25
- import pydantic
26
-
14
+ import pydantic
27
15
  from griffe import Docstring, DocstringSectionKind, Parser, parse
28
16
  from typing_extensions import Literal
29
17
 
18
+ from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
19
+ from prefect._internal.pydantic.v2_schema import (
20
+ create_v2_schema,
21
+ process_v2_params,
22
+ )
30
23
  from prefect.exceptions import (
24
+ MappingLengthMismatch,
25
+ MappingMissingIterable,
31
26
  ParameterBindError,
32
27
  ReservedArgumentError,
33
28
  SignatureMismatchError,
34
29
  )
35
30
  from prefect.logging.loggers import disable_logger, get_logger
31
+ from prefect.utilities.annotations import allow_failure, quote, unmapped
32
+ from prefect.utilities.collections import isiterable
36
33
  from prefect.utilities.importtools import safe_load_namespace
37
34
 
38
35
  logger = get_logger(__name__)
@@ -45,11 +42,23 @@ def get_call_parameters(
45
42
  apply_defaults: bool = True,
46
43
  ) -> Dict[str, Any]:
47
44
  """
48
- Bind a call to a function to get parameter/value mapping. Default values on the
49
- signature will be included if not overridden.
50
-
51
- Raises a ParameterBindError if the arguments/kwargs are not valid for the function
45
+ Bind a call to a function to get parameter/value mapping. Default values on
46
+ the signature will be included if not overridden.
47
+
48
+ If the function has a `__prefect_self__` attribute, it will be included as
49
+ the first parameter. This attribute is set when Prefect decorates a bound
50
+ method, so this approach allows Prefect to work with bound methods in a way
51
+ that is consistent with how Python handles them (i.e. users don't have to
52
+ pass the instance argument to the method) while still making the implicit self
53
+ argument visible to all of Prefect's parameter machinery (such as cache key
54
+ functions).
55
+
56
+ Raises a ParameterBindError if the arguments/kwargs are not valid for the
57
+ function
52
58
  """
59
+ if hasattr(fn, "__prefect_self__"):
60
+ call_args = (fn.__prefect_self__,) + call_args
61
+
53
62
  try:
54
63
  bound_signature = inspect.signature(fn).bind(*call_args, **call_kwargs)
55
64
  except TypeError as exc:
@@ -228,15 +237,14 @@ class ParameterSchema(pydantic.BaseModel):
228
237
  title: Literal["Parameters"] = "Parameters"
229
238
  type: Literal["object"] = "object"
230
239
  properties: Dict[str, Any] = pydantic.Field(default_factory=dict)
231
- required: List[str] = None
232
- definitions: Optional[Dict[str, Any]] = None
240
+ required: List[str] = pydantic.Field(default_factory=list)
241
+ definitions: Dict[str, Any] = pydantic.Field(default_factory=dict)
233
242
 
234
- def dict(self, *args, **kwargs):
235
- """Exclude `None` fields by default to comply with
236
- the OpenAPI spec.
237
- """
238
- kwargs.setdefault("exclude_none", True)
239
- return super().dict(*args, **kwargs)
243
+ def model_dump_for_openapi(self) -> Dict[str, Any]:
244
+ result = self.model_dump(mode="python", exclude_none=True)
245
+ if "required" in result and not result["required"]:
246
+ del result["required"]
247
+ return result
240
248
 
241
249
 
242
250
  def parameter_docstrings(docstring: Optional[str]) -> Dict[str, str]:
@@ -284,21 +292,31 @@ def process_v1_params(
284
292
  name = param.name
285
293
 
286
294
  type_ = Any if param.annotation is inspect._empty else param.annotation
287
- field = pydantic.Field(
288
- default=... if param.default is param.empty else param.default,
289
- title=param.name,
290
- description=docstrings.get(param.name, None),
291
- alias=aliases.get(name),
292
- position=position,
293
- )
295
+
296
+ with warnings.catch_warnings():
297
+ warnings.filterwarnings(
298
+ "ignore", category=pydantic.warnings.PydanticDeprecatedSince20
299
+ )
300
+ field = pydantic.Field(
301
+ default=... if param.default is param.empty else param.default,
302
+ title=param.name,
303
+ description=docstrings.get(param.name, None),
304
+ alias=aliases.get(name),
305
+ position=position,
306
+ )
294
307
  return name, type_, field
295
308
 
296
309
 
297
310
  def create_v1_schema(name_: str, model_cfg, **model_fields):
298
- model: "pydantic.BaseModel" = pydantic.create_model(
299
- name_, __config__=model_cfg, **model_fields
300
- )
301
- return model.schema(by_alias=True)
311
+ with warnings.catch_warnings():
312
+ warnings.filterwarnings(
313
+ "ignore", category=pydantic.warnings.PydanticDeprecatedSince20
314
+ )
315
+
316
+ model: "pydantic.BaseModel" = pydantic.create_model(
317
+ name_, __config__=model_cfg, **model_fields
318
+ )
319
+ return model.schema(by_alias=True)
302
320
 
303
321
 
304
322
  def parameter_schema(fn: Callable) -> ParameterSchema:
@@ -381,16 +399,20 @@ def generate_parameter_schema(
381
399
  model_fields = {}
382
400
  aliases = {}
383
401
 
384
- class ModelConfig:
385
- arbitrary_types_allowed = True
386
-
387
- if HAS_PYDANTIC_V2 and not has_v1_type_as_param(signature):
402
+ if not has_v1_type_as_param(signature):
388
403
  create_schema = create_v2_schema
389
404
  process_params = process_v2_params
405
+
406
+ config = pydantic.ConfigDict(arbitrary_types_allowed=True)
390
407
  else:
391
408
  create_schema = create_v1_schema
392
409
  process_params = process_v1_params
393
410
 
411
+ class ModelConfig:
412
+ arbitrary_types_allowed = True
413
+
414
+ config = ModelConfig
415
+
394
416
  for position, param in enumerate(signature.parameters.values()):
395
417
  name, type_, field = process_params(
396
418
  param, position=position, docstrings=docstrings, aliases=aliases
@@ -398,16 +420,14 @@ def generate_parameter_schema(
398
420
  # Generate a Pydantic model at each step so we can check if this parameter
399
421
  # type supports schema generation
400
422
  try:
401
- create_schema(
402
- "CheckParameter", model_cfg=ModelConfig, **{name: (type_, field)}
403
- )
423
+ create_schema("CheckParameter", model_cfg=config, **{name: (type_, field)})
404
424
  except (ValueError, TypeError):
405
425
  # This field's type is not valid for schema creation, update it to `Any`
406
426
  type_ = Any
407
427
  model_fields[name] = (type_, field)
408
428
 
409
429
  # Generate the final model and schema
410
- schema = create_schema("Parameters", model_cfg=ModelConfig, **model_fields)
430
+ schema = create_schema("Parameters", model_cfg=config, **model_fields)
411
431
  return ParameterSchema(**schema)
412
432
 
413
433
 
@@ -621,3 +641,75 @@ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str
621
641
  ):
622
642
  return func_def.body[0].value.value
623
643
  return None
644
+
645
+
646
+ def expand_mapping_parameters(
647
+ func: Callable, parameters: Dict[str, Any]
648
+ ) -> List[Dict[str, Any]]:
649
+ """
650
+ Generates a list of call parameters to be used for individual calls in a mapping
651
+ operation.
652
+
653
+ Args:
654
+ func: The function to be called
655
+ parameters: A dictionary of parameters with iterables to be mapped over
656
+
657
+ Returns:
658
+ List: A list of dictionaries to be used as parameters for each
659
+ call in the mapping operation
660
+ """
661
+ # Ensure that any parameters in kwargs are expanded before this check
662
+ parameters = explode_variadic_parameter(func, parameters)
663
+
664
+ iterable_parameters = {}
665
+ static_parameters = {}
666
+ annotated_parameters = {}
667
+ for key, val in parameters.items():
668
+ if isinstance(val, (allow_failure, quote)):
669
+ # Unwrap annotated parameters to determine if they are iterable
670
+ annotated_parameters[key] = val
671
+ val = val.unwrap()
672
+
673
+ if isinstance(val, unmapped):
674
+ static_parameters[key] = val.value
675
+ elif isiterable(val):
676
+ iterable_parameters[key] = list(val)
677
+ else:
678
+ static_parameters[key] = val
679
+
680
+ if not len(iterable_parameters):
681
+ raise MappingMissingIterable(
682
+ "No iterable parameters were received. Parameters for map must "
683
+ f"include at least one iterable. Parameters: {parameters}"
684
+ )
685
+
686
+ iterable_parameter_lengths = {
687
+ key: len(val) for key, val in iterable_parameters.items()
688
+ }
689
+ lengths = set(iterable_parameter_lengths.values())
690
+ if len(lengths) > 1:
691
+ raise MappingLengthMismatch(
692
+ "Received iterable parameters with different lengths. Parameters for map"
693
+ f" must all be the same length. Got lengths: {iterable_parameter_lengths}"
694
+ )
695
+
696
+ map_length = list(lengths)[0]
697
+
698
+ call_parameters_list = []
699
+ for i in range(map_length):
700
+ call_parameters = {key: value[i] for key, value in iterable_parameters.items()}
701
+ call_parameters.update({key: value for key, value in static_parameters.items()})
702
+
703
+ # Add default values for parameters; these are skipped earlier since they should
704
+ # not be mapped over
705
+ for key, value in get_parameter_defaults(func).items():
706
+ call_parameters.setdefault(key, value)
707
+
708
+ # Re-apply annotations to each key again
709
+ for key, annotation in annotated_parameters.items():
710
+ call_parameters[key] = annotation.rewrap(call_parameters[key])
711
+
712
+ # Collapse any previously exploded kwargs
713
+ call_parameters_list.append(collapse_variadic_parameters(func, call_parameters))
714
+
715
+ return call_parameters_list
@@ -4,6 +4,8 @@ Utilities for extensions of and operations on Python collections.
4
4
 
5
5
  import io
6
6
  import itertools
7
+ import types
8
+ import warnings
7
9
  from collections import OrderedDict, defaultdict
8
10
  from collections.abc import Iterator as IteratorABC
9
11
  from collections.abc import Sequence
@@ -28,12 +30,7 @@ from typing import (
28
30
  )
29
31
  from unittest.mock import Mock
30
32
 
31
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
32
-
33
- if HAS_PYDANTIC_V2:
34
- import pydantic.v1 as pydantic
35
- else:
36
- import pydantic
33
+ import pydantic
37
34
 
38
35
  # Quote moved to `prefect.utilities.annotations` but preserved here for compatibility
39
36
  from prefect.utilities.annotations import BaseAnnotation, Quote, quote # noqa
@@ -224,25 +221,31 @@ class StopVisiting(BaseException):
224
221
 
225
222
 
226
223
  def visit_collection(
227
- expr,
228
- visit_fn: Callable[[Any], Any],
224
+ expr: Any,
225
+ visit_fn: Union[Callable[[Any, Optional[dict]], Any], Callable[[Any], Any]],
229
226
  return_data: bool = False,
230
227
  max_depth: int = -1,
231
228
  context: Optional[dict] = None,
232
229
  remove_annotations: bool = False,
233
- ):
230
+ _seen: Optional[Set[int]] = None,
231
+ ) -> Any:
234
232
  """
235
- This function visits every element of an arbitrary Python collection. If an element
236
- is a Python collection, it will be visited recursively. If an element is not a
237
- collection, `visit_fn` will be called with the element. The return value of
238
- `visit_fn` can be used to alter the element if `return_data` is set.
233
+ Visits and potentially transforms every element of an arbitrary Python collection.
234
+
235
+ If an element is a Python collection, it will be visited recursively. If an element
236
+ is not a collection, `visit_fn` will be called with the element. The return value of
237
+ `visit_fn` can be used to alter the element if `return_data` is set to `True`.
239
238
 
240
- Note that when using `return_data` a copy of each collection is created to avoid
241
- mutating the original object. This may have significant performance penalties and
242
- should only be used if you intend to transform the collection.
239
+ Note:
240
+ - When `return_data` is `True`, a copy of each collection is created only if
241
+ `visit_fn` modifies an element within that collection. This approach minimizes
242
+ performance penalties by avoiding unnecessary copying.
243
+ - When `return_data` is `False`, no copies are created, and only side effects from
244
+ `visit_fn` are applied. This mode is faster and should be used when no transformation
245
+ of the collection is required, because it never has to copy any data.
243
246
 
244
247
  Supported types:
245
- - List
248
+ - List (including iterators)
246
249
  - Tuple
247
250
  - Set
248
251
  - Dict (note: keys are also visited recursively)
@@ -250,30 +253,41 @@ def visit_collection(
250
253
  - Pydantic model
251
254
  - Prefect annotations
252
255
 
256
+ Note that visit_collection will not consume generators or async generators, as it would prevent
257
+ the caller from iterating over them.
258
+
253
259
  Args:
254
- expr (Any): a Python object or expression
255
- visit_fn (Callable[[Any], Awaitable[Any]]): an async function that
256
- will be applied to every non-collection element of expr.
257
- return_data (bool): if `True`, a copy of `expr` containing data modified
258
- by `visit_fn` will be returned. This is slower than `return_data=False`
259
- (the default).
260
- max_depth: Controls the depth of recursive visitation. If set to zero, no
261
- recursion will occur. If set to a positive integer N, visitation will only
262
- descend to N layers deep. If set to any negative integer, no limit will be
260
+ expr (Any): A Python object or expression.
261
+ visit_fn (Callable[[Any, Optional[dict]], Any] or Callable[[Any], Any]): A function
262
+ that will be applied to every non-collection element of `expr`. The function can
263
+ accept one or two arguments. If two arguments are accepted, the second argument
264
+ will be the context dictionary.
265
+ return_data (bool): If `True`, a copy of `expr` containing data modified by `visit_fn`
266
+ will be returned. This is slower than `return_data=False` (the default).
267
+ max_depth (int): Controls the depth of recursive visitation. If set to zero, no
268
+ recursion will occur. If set to a positive integer `N`, visitation will only
269
+ descend to `N` layers deep. If set to any negative integer, no limit will be
263
270
  enforced and recursion will continue until terminal items are reached. By
264
271
  default, recursion is unlimited.
265
- context: An optional dictionary. If passed, the context will be sent to each
266
- call to the `visit_fn`. The context can be mutated by each visitor and will
267
- be available for later visits to expressions at the given depth. Values
272
+ context (Optional[dict]): An optional dictionary. If passed, the context will be sent
273
+ to each call to the `visit_fn`. The context can be mutated by each visitor and
274
+ will be available for later visits to expressions at the given depth. Values
268
275
  will not be available "up" a level from a given expression.
269
-
270
276
  The context will be automatically populated with an 'annotation' key when
271
- visiting collections within a `BaseAnnotation` type. This requires the
272
- caller to pass `context={}` and will not be activated by default.
273
- remove_annotations: If set, annotations will be replaced by their contents. By
277
+ visiting collections within a `BaseAnnotation` type. This requires the caller to
278
+ pass `context={}` and will not be activated by default.
279
+ remove_annotations (bool): If set, annotations will be replaced by their contents. By
274
280
  default, annotations are preserved but their contents are visited.
281
+ _seen (Optional[Set[int]]): A set of object ids that have already been visited. This
282
+ prevents infinite recursion when visiting recursive data structures.
283
+
284
+ Returns:
285
+ Any: The modified collection if `return_data` is `True`, otherwise `None`.
275
286
  """
276
287
 
288
+ if _seen is None:
289
+ _seen = set()
290
+
277
291
  def visit_nested(expr):
278
292
  # Utility for a recursive call, preserving options and updating the depth.
279
293
  return visit_collection(
@@ -284,6 +298,7 @@ def visit_collection(
284
298
  max_depth=max_depth - 1,
285
299
  # Copy the context on nested calls so it does not "propagate up"
286
300
  context=context.copy() if context is not None else None,
301
+ _seen=_seen,
287
302
  )
288
303
 
289
304
  def visit_expression(expr):
@@ -292,7 +307,7 @@ def visit_collection(
292
307
  else:
293
308
  return visit_fn(expr)
294
309
 
295
- # Visit every expression
310
+ # --- 1. Visit every expression
296
311
  try:
297
312
  result = visit_expression(expr)
298
313
  except StopVisiting:
@@ -300,90 +315,123 @@ def visit_collection(
300
315
  result = expr
301
316
 
302
317
  if return_data:
303
- # Only mutate the expression while returning data, otherwise it could be null
318
+ # Only mutate the root expression if the user indicated we're returning data,
319
+ # otherwise the function could return null and we have no collection to check
304
320
  expr = result
305
321
 
306
- # Then, visit every child of the expression recursively
322
+ # --- 2. Visit every child of the expression recursively
307
323
 
308
- # If we have reached the maximum depth, do not perform any recursion
309
- if max_depth == 0:
324
+ # If we have reached the maximum depth or we have already visited this object,
325
+ # return the result if we are returning data, otherwise return None
326
+ if max_depth == 0 or id(expr) in _seen:
310
327
  return result if return_data else None
328
+ else:
329
+ _seen.add(id(expr))
311
330
 
312
331
  # Get the expression type; treat iterators like lists
313
332
  typ = list if isinstance(expr, IteratorABC) and isiterable(expr) else type(expr)
314
333
  typ = cast(type, typ) # mypy treats this as 'object' otherwise and complains
315
334
 
316
335
  # Then visit every item in the expression if it is a collection
317
- if isinstance(expr, Mock):
336
+
337
+ # presume that the result is the original expression.
338
+ # in each of the following cases, we will update the result if we need to.
339
+ result = expr
340
+
341
+ # --- Generators
342
+
343
+ if isinstance(expr, (types.GeneratorType, types.AsyncGeneratorType)):
344
+ # Do not attempt to iterate over generators, as it will exhaust them
345
+ pass
346
+
347
+ # --- Mocks
348
+
349
+ elif isinstance(expr, Mock):
318
350
  # Do not attempt to recurse into mock objects
319
- result = expr
351
+ pass
352
+
353
+ # --- Annotations (unmapped, quote, etc.)
320
354
 
321
355
  elif isinstance(expr, BaseAnnotation):
322
356
  if context is not None:
323
357
  context["annotation"] = expr
324
- value = visit_nested(expr.unwrap())
358
+ unwrapped = expr.unwrap()
359
+ value = visit_nested(unwrapped)
325
360
 
326
- if remove_annotations:
327
- result = value if return_data else None
328
- else:
329
- result = expr.rewrap(value) if return_data else None
361
+ if return_data:
362
+ # if we are removing annotations, return the value
363
+ if remove_annotations:
364
+ result = value
365
+ # if the value was modified, rewrap it
366
+ elif value is not unwrapped:
367
+ result = expr.rewrap(value)
368
+ # otherwise return the expr
330
369
 
331
- elif typ in (list, tuple, set):
370
+ # --- Sequences
371
+
372
+ elif isinstance(expr, (list, tuple, set)):
332
373
  items = [visit_nested(o) for o in expr]
333
- result = typ(items) if return_data else None
374
+ if return_data:
375
+ modified = any(item is not orig for item, orig in zip(items, expr))
376
+ if modified:
377
+ result = typ(items)
378
+
379
+ # --- Dictionaries
334
380
 
335
381
  elif typ in (dict, OrderedDict):
336
382
  assert isinstance(expr, (dict, OrderedDict)) # typecheck assertion
337
383
  items = [(visit_nested(k), visit_nested(v)) for k, v in expr.items()]
338
- result = typ(items) if return_data else None
384
+ if return_data:
385
+ modified = any(
386
+ k1 is not k2 or v1 is not v2
387
+ for (k1, v1), (k2, v2) in zip(items, expr.items())
388
+ )
389
+ if modified:
390
+ result = typ(items)
391
+
392
+ # --- Dataclasses
339
393
 
340
394
  elif is_dataclass(expr) and not isinstance(expr, type):
341
395
  values = [visit_nested(getattr(expr, f.name)) for f in fields(expr)]
342
- items = {field.name: value for field, value in zip(fields(expr), values)}
343
- result = typ(**items) if return_data else None
396
+ if return_data:
397
+ modified = any(
398
+ getattr(expr, f.name) is not v for f, v in zip(fields(expr), values)
399
+ )
400
+ if modified:
401
+ result = typ(**{f.name: v for f, v in zip(fields(expr), values)})
344
402
 
345
- elif isinstance(expr, pydantic.BaseModel):
346
- # NOTE: This implementation *does not* traverse private attributes
347
- # Pydantic does not expose extras in `__fields__` so we use `__fields_set__`
348
- # as well to get all of the relevant attributes
349
- # Check for presence of attrs even if they're in the field set due to pydantic#4916
350
- model_fields = {
351
- f for f in expr.__fields_set__.union(expr.__fields__) if hasattr(expr, f)
352
- }
353
- items = [visit_nested(getattr(expr, key)) for key in model_fields]
403
+ # --- Pydantic models
354
404
 
355
- if return_data:
356
- # Collect fields with aliases so reconstruction can use the correct field name
357
- aliases = {
358
- key: value.alias
359
- for key, value in expr.__fields__.items()
360
- if value.has_alias
361
- }
405
+ elif isinstance(expr, pydantic.BaseModel):
406
+ typ = cast(Type[pydantic.BaseModel], typ)
362
407
 
363
- model_instance = typ(
364
- **{
365
- aliases.get(key) or key: value
366
- for key, value in zip(model_fields, items)
367
- }
368
- )
408
+ # when extra=allow, fields not in model_fields may be in model_fields_set
409
+ model_fields = expr.model_fields_set.union(expr.model_fields.keys())
369
410
 
370
- # Private attributes are not included in `__fields_set__` but we do not want
371
- # to drop them from the model so we restore them after constructing a new
372
- # model
373
- for attr in expr.__private_attributes__:
374
- # Use `object.__setattr__` to avoid errors on immutable models
375
- object.__setattr__(model_instance, attr, getattr(expr, attr))
411
+ # We may encounter a deprecated field here, but this isn't the caller's fault
412
+ with warnings.catch_warnings():
413
+ warnings.simplefilter("ignore", category=DeprecationWarning)
376
414
 
377
- # Preserve data about which fields were explicitly set on the original model
378
- object.__setattr__(model_instance, "__fields_set__", expr.__fields_set__)
379
- result = model_instance
380
- else:
381
- result = None
415
+ updated_data = {
416
+ field: visit_nested(getattr(expr, field)) for field in model_fields
417
+ }
382
418
 
383
- else:
384
- result = result if return_data else None
419
+ if return_data:
420
+ modified = any(
421
+ getattr(expr, field) is not updated_data[field]
422
+ for field in model_fields
423
+ )
424
+ if modified:
425
+ # Use construct to avoid validation and handle immutability
426
+ model_instance = typ.model_construct(
427
+ _fields_set=expr.model_fields_set, **updated_data
428
+ )
429
+ for private_attr in expr.__private_attributes__:
430
+ setattr(model_instance, private_attr, getattr(expr, private_attr))
431
+ result = model_instance
385
432
 
386
- return result
433
+ if return_data:
434
+ return result
387
435
 
388
436
 
389
437
  def remove_nested_keys(keys_to_remove: List[Hashable], obj):
@@ -90,8 +90,10 @@ def get_dispatch_key(
90
90
 
91
91
  @classmethod
92
92
  def _register_subclass_of_base_type(cls, **kwargs):
93
- if cls.__init_subclass_original__:
93
+ if hasattr(cls, "__init_subclass_original__"):
94
94
  cls.__init_subclass_original__(**kwargs)
95
+ elif hasattr(cls, "__pydantic_init_subclass_original__"):
96
+ cls.__pydantic_init_subclass_original__(**kwargs)
95
97
 
96
98
  # Do not register abstract base classes
97
99
  if abc.ABC in getattr(cls, "__bases__", []):
@@ -114,8 +116,14 @@ def register_base_type(cls: T) -> T:
114
116
  registry[base_key] = cls
115
117
 
116
118
  # Add automatic subtype registration
117
- cls.__init_subclass_original__ = getattr(cls, "__init_subclass__")
118
- cls.__init_subclass__ = _register_subclass_of_base_type
119
+ if hasattr(cls, "__pydantic_init_subclass__"):
120
+ cls.__pydantic_init_subclass_original__ = getattr(
121
+ cls, "__pydantic_init_subclass__"
122
+ )
123
+ cls.__pydantic_init_subclass__ = _register_subclass_of_base_type
124
+ else:
125
+ cls.__init_subclass_original__ = getattr(cls, "__init_subclass__")
126
+ cls.__init_subclass__ = _register_subclass_of_base_type
119
127
 
120
128
  return cls
121
129
 
@@ -154,17 +162,22 @@ def register_type(cls: T) -> T:
154
162
  key = get_dispatch_key(cls)
155
163
  existing_value = registry.get(key)
156
164
  if existing_value is not None and id(existing_value) != id(cls):
157
- # Get line numbers for debugging
158
- file = inspect.getsourcefile(cls)
159
- line_number = inspect.getsourcelines(cls)[1]
160
- existing_file = inspect.getsourcefile(existing_value)
161
- existing_line_number = inspect.getsourcelines(existing_value)[1]
162
- warnings.warn(
163
- f"Type {cls.__name__!r} at {file}:{line_number} has key {key!r} that "
164
- f"matches existing registered type {existing_value.__name__!r} from "
165
- f"{existing_file}:{existing_line_number}. The existing type will be "
166
- "overridden."
167
- )
165
+ try:
166
+ # Get line numbers for debugging
167
+ file = inspect.getsourcefile(cls)
168
+ line_number = inspect.getsourcelines(cls)[1]
169
+ existing_file = inspect.getsourcefile(existing_value)
170
+ existing_line_number = inspect.getsourcelines(existing_value)[1]
171
+ warnings.warn(
172
+ f"Type {cls.__name__!r} at {file}:{line_number} has key {key!r} that "
173
+ f"matches existing registered type {existing_value.__name__!r} from "
174
+ f"{existing_file}:{existing_line_number}. The existing type will be "
175
+ "overridden."
176
+ )
177
+ except OSError:
178
+ # If we can't get the source, another actor is loading this class via eval
179
+ # and we shouldn't update the registry
180
+ return cls
168
181
 
169
182
  # Add to the registry
170
183
  registry[key] = cls