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
@@ -2,36 +2,39 @@
2
2
  Utilities for working with Python callables.
3
3
  """
4
4
 
5
+ import ast
6
+ import importlib.util
5
7
  import inspect
8
+ import warnings
6
9
  from functools import partial
10
+ from pathlib import Path
7
11
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
8
12
 
9
13
  import cloudpickle
10
-
11
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
12
- from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
13
-
14
- if HAS_PYDANTIC_V2:
15
- import pydantic.v1 as pydantic
16
-
17
- from prefect._internal.pydantic.v2_schema import (
18
- create_v2_schema,
19
- process_v2_params,
20
- )
21
- else:
22
- import pydantic
23
-
14
+ import pydantic
24
15
  from griffe.dataclasses import Docstring
25
16
  from griffe.docstrings.dataclasses import DocstringSectionKind
26
17
  from griffe.docstrings.parsers import Parser, parse
27
18
  from typing_extensions import Literal
28
19
 
20
+ from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
21
+ from prefect._internal.pydantic.v2_schema import (
22
+ create_v2_schema,
23
+ process_v2_params,
24
+ )
29
25
  from prefect.exceptions import (
26
+ MappingLengthMismatch,
27
+ MappingMissingIterable,
30
28
  ParameterBindError,
31
29
  ReservedArgumentError,
32
30
  SignatureMismatchError,
33
31
  )
34
- from prefect.logging.loggers import disable_logger
32
+ from prefect.logging.loggers import disable_logger, get_logger
33
+ from prefect.utilities.annotations import allow_failure, quote, unmapped
34
+ from prefect.utilities.collections import isiterable
35
+ from prefect.utilities.importtools import safe_load_namespace
36
+
37
+ logger = get_logger(__name__)
35
38
 
36
39
 
37
40
  def get_call_parameters(
@@ -224,15 +227,14 @@ class ParameterSchema(pydantic.BaseModel):
224
227
  title: Literal["Parameters"] = "Parameters"
225
228
  type: Literal["object"] = "object"
226
229
  properties: Dict[str, Any] = pydantic.Field(default_factory=dict)
227
- required: List[str] = None
228
- definitions: Optional[Dict[str, Any]] = None
230
+ required: List[str] = pydantic.Field(default_factory=list)
231
+ definitions: Dict[str, Any] = pydantic.Field(default_factory=dict)
229
232
 
230
- def dict(self, *args, **kwargs):
231
- """Exclude `None` fields by default to comply with
232
- the OpenAPI spec.
233
- """
234
- kwargs.setdefault("exclude_none", True)
235
- return super().dict(*args, **kwargs)
233
+ def model_dump_for_openapi(self) -> Dict[str, Any]:
234
+ result = self.model_dump(mode="python", exclude_none=True)
235
+ if "required" in result and not result["required"]:
236
+ del result["required"]
237
+ return result
236
238
 
237
239
 
238
240
  def parameter_docstrings(docstring: Optional[str]) -> Dict[str, str]:
@@ -280,21 +282,31 @@ def process_v1_params(
280
282
  name = param.name
281
283
 
282
284
  type_ = Any if param.annotation is inspect._empty else param.annotation
283
- field = pydantic.Field(
284
- default=... if param.default is param.empty else param.default,
285
- title=param.name,
286
- description=docstrings.get(param.name, None),
287
- alias=aliases.get(name),
288
- position=position,
289
- )
285
+
286
+ with warnings.catch_warnings():
287
+ warnings.filterwarnings(
288
+ "ignore", category=pydantic.warnings.PydanticDeprecatedSince20
289
+ )
290
+ field = pydantic.Field(
291
+ default=... if param.default is param.empty else param.default,
292
+ title=param.name,
293
+ description=docstrings.get(param.name, None),
294
+ alias=aliases.get(name),
295
+ position=position,
296
+ )
290
297
  return name, type_, field
291
298
 
292
299
 
293
300
  def create_v1_schema(name_: str, model_cfg, **model_fields):
294
- model: "pydantic.BaseModel" = pydantic.create_model(
295
- name_, __config__=model_cfg, **model_fields
296
- )
297
- return model.schema(by_alias=True)
301
+ with warnings.catch_warnings():
302
+ warnings.filterwarnings(
303
+ "ignore", category=pydantic.warnings.PydanticDeprecatedSince20
304
+ )
305
+
306
+ model: "pydantic.BaseModel" = pydantic.create_model(
307
+ name_, __config__=model_cfg, **model_fields
308
+ )
309
+ return model.schema(by_alias=True)
298
310
 
299
311
 
300
312
  def parameter_schema(fn: Callable) -> ParameterSchema:
@@ -318,20 +330,77 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
318
330
  # `eval_str` is not available in Python < 3.10
319
331
  signature = inspect.signature(fn)
320
332
 
321
- model_fields = {}
322
- aliases = {}
323
333
  docstrings = parameter_docstrings(inspect.getdoc(fn))
324
334
 
325
- class ModelConfig:
326
- arbitrary_types_allowed = True
335
+ return generate_parameter_schema(signature, docstrings)
336
+
337
+
338
+ def parameter_schema_from_entrypoint(entrypoint: str) -> ParameterSchema:
339
+ """
340
+ Generate a parameter schema from an entrypoint string.
341
+
342
+ Will load the source code of the function and extract the signature and docstring
343
+ to generate the schema.
327
344
 
328
- if HAS_PYDANTIC_V2 and not has_v1_type_as_param(signature):
345
+ Useful for generating a schema for a function when instantiating the function may
346
+ not be possible due to missing imports or other issues.
347
+
348
+ Args:
349
+ entrypoint: A string representing the entrypoint to a function. The string
350
+ should be in the format of `module.path.to.function:do_stuff`.
351
+
352
+ Returns:
353
+ ParameterSchema: The parameter schema for the function.
354
+ """
355
+ if ":" in entrypoint:
356
+ # split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
357
+ path, func_name = entrypoint.rsplit(":", maxsplit=1)
358
+ source_code = Path(path).read_text()
359
+ else:
360
+ path, func_name = entrypoint.rsplit(".", maxsplit=1)
361
+ spec = importlib.util.find_spec(path)
362
+ if not spec or not spec.origin:
363
+ raise ValueError(f"Could not find module {path!r}")
364
+ source_code = Path(spec.origin).read_text()
365
+ signature = _generate_signature_from_source(source_code, func_name)
366
+ docstring = _get_docstring_from_source(source_code, func_name)
367
+ return generate_parameter_schema(signature, parameter_docstrings(docstring))
368
+
369
+
370
+ def generate_parameter_schema(
371
+ signature: inspect.Signature, docstrings: Dict[str, str]
372
+ ) -> ParameterSchema:
373
+ """
374
+ Generate a parameter schema from a function signature and docstrings.
375
+
376
+ To get a signature from a function, use `inspect.signature(fn)` or
377
+ `_generate_signature_from_source(source_code, func_name)`.
378
+
379
+ Args:
380
+ signature: The function signature.
381
+ docstrings: A dictionary mapping parameter names to docstrings.
382
+
383
+ Returns:
384
+ ParameterSchema: The parameter schema.
385
+ """
386
+
387
+ model_fields = {}
388
+ aliases = {}
389
+
390
+ if not has_v1_type_as_param(signature):
329
391
  create_schema = create_v2_schema
330
392
  process_params = process_v2_params
393
+
394
+ config = pydantic.ConfigDict(arbitrary_types_allowed=True)
331
395
  else:
332
396
  create_schema = create_v1_schema
333
397
  process_params = process_v1_params
334
398
 
399
+ class ModelConfig:
400
+ arbitrary_types_allowed = True
401
+
402
+ config = ModelConfig
403
+
335
404
  for position, param in enumerate(signature.parameters.values()):
336
405
  name, type_, field = process_params(
337
406
  param, position=position, docstrings=docstrings, aliases=aliases
@@ -339,16 +408,14 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
339
408
  # Generate a Pydantic model at each step so we can check if this parameter
340
409
  # type supports schema generation
341
410
  try:
342
- create_schema(
343
- "CheckParameter", model_cfg=ModelConfig, **{name: (type_, field)}
344
- )
411
+ create_schema("CheckParameter", model_cfg=config, **{name: (type_, field)})
345
412
  except (ValueError, TypeError):
346
413
  # This field's type is not valid for schema creation, update it to `Any`
347
414
  type_ = Any
348
415
  model_fields[name] = (type_, field)
349
416
 
350
417
  # Generate the final model and schema
351
- schema = create_schema("Parameters", model_cfg=ModelConfig, **model_fields)
418
+ schema = create_schema("Parameters", model_cfg=config, **model_fields)
352
419
  return ParameterSchema(**schema)
353
420
 
354
421
 
@@ -362,3 +429,204 @@ def raise_for_reserved_arguments(fn: Callable, reserved_arguments: Iterable[str]
362
429
  raise ReservedArgumentError(
363
430
  f"{argument!r} is a reserved argument name and cannot be used."
364
431
  )
432
+
433
+
434
+ def _generate_signature_from_source(
435
+ source_code: str, func_name: str
436
+ ) -> inspect.Signature:
437
+ """
438
+ Extract the signature of a function from its source code.
439
+
440
+ Will ignore missing imports and exceptions while loading local class definitions.
441
+
442
+ Args:
443
+ source_code: The source code where the function named `func_name` is declared.
444
+ func_name: The name of the function.
445
+
446
+ Returns:
447
+ The signature of the function.
448
+ """
449
+ # Load the namespace from the source code. Missing imports and exceptions while
450
+ # loading local class definitions are ignored.
451
+ namespace = safe_load_namespace(source_code)
452
+ # Parse the source code into an AST
453
+ parsed_code = ast.parse(source_code)
454
+
455
+ func_def = next(
456
+ (
457
+ node
458
+ for node in ast.walk(parsed_code)
459
+ if isinstance(node, ast.FunctionDef) and node.name == func_name
460
+ ),
461
+ None,
462
+ )
463
+ if func_def is None:
464
+ raise ValueError(f"Function {func_name} not found in source code")
465
+ parameters = []
466
+
467
+ for arg in func_def.args.args:
468
+ name = arg.arg
469
+ annotation = arg.annotation
470
+ if annotation is not None:
471
+ try:
472
+ # Compile and evaluate the annotation
473
+ ann_code = compile(ast.Expression(annotation), "<string>", "eval")
474
+ annotation = eval(ann_code, namespace)
475
+ except Exception as e:
476
+ # Don't raise an error if the annotation evaluation fails. Set the
477
+ # annotation to `inspect.Parameter.empty` instead which is equivalent to
478
+ # not having an annotation.
479
+ logger.debug("Failed to evaluate annotation for %s: %s", name, e)
480
+ annotation = inspect.Parameter.empty
481
+ else:
482
+ annotation = inspect.Parameter.empty
483
+
484
+ param = inspect.Parameter(
485
+ name, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=annotation
486
+ )
487
+ parameters.append(param)
488
+
489
+ defaults = [None] * (
490
+ len(func_def.args.args) - len(func_def.args.defaults)
491
+ ) + func_def.args.defaults
492
+ for param, default in zip(parameters, defaults):
493
+ if default is not None:
494
+ try:
495
+ def_code = compile(ast.Expression(default), "<string>", "eval")
496
+ default = eval(def_code, namespace)
497
+ except Exception as e:
498
+ logger.debug(
499
+ "Failed to evaluate default value for %s: %s", param.name, e
500
+ )
501
+ default = None # Set to None if evaluation fails
502
+ parameters[parameters.index(param)] = param.replace(default=default)
503
+
504
+ if func_def.args.vararg:
505
+ parameters.append(
506
+ inspect.Parameter(
507
+ func_def.args.vararg.arg, inspect.Parameter.VAR_POSITIONAL
508
+ )
509
+ )
510
+ if func_def.args.kwarg:
511
+ parameters.append(
512
+ inspect.Parameter(func_def.args.kwarg.arg, inspect.Parameter.VAR_KEYWORD)
513
+ )
514
+
515
+ # Handle return annotation
516
+ return_annotation = func_def.returns
517
+ if return_annotation is not None:
518
+ try:
519
+ ret_ann_code = compile(
520
+ ast.Expression(return_annotation), "<string>", "eval"
521
+ )
522
+ return_annotation = eval(ret_ann_code, namespace)
523
+ except Exception as e:
524
+ logger.debug("Failed to evaluate return annotation: %s", e)
525
+ return_annotation = inspect.Signature.empty
526
+
527
+ return inspect.Signature(parameters, return_annotation=return_annotation)
528
+
529
+
530
+ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str]:
531
+ """
532
+ Extract the docstring of a function from its source code.
533
+
534
+ Args:
535
+ source_code (str): The source code of the function.
536
+ func_name (str): The name of the function.
537
+
538
+ Returns:
539
+ The docstring of the function. If the function has no docstring, returns None.
540
+ """
541
+ parsed_code = ast.parse(source_code)
542
+
543
+ func_def = next(
544
+ (
545
+ node
546
+ for node in ast.walk(parsed_code)
547
+ if isinstance(node, ast.FunctionDef) and node.name == func_name
548
+ ),
549
+ None,
550
+ )
551
+ if func_def is None:
552
+ raise ValueError(f"Function {func_name} not found in source code")
553
+
554
+ if (
555
+ func_def.body
556
+ and isinstance(func_def.body[0], ast.Expr)
557
+ and isinstance(func_def.body[0].value, ast.Constant)
558
+ ):
559
+ return func_def.body[0].value.value
560
+ return None
561
+
562
+
563
+ def expand_mapping_parameters(
564
+ func: Callable, parameters: Dict[str, Any]
565
+ ) -> List[Dict[str, Any]]:
566
+ """
567
+ Generates a list of call parameters to be used for individual calls in a mapping
568
+ operation.
569
+
570
+ Args:
571
+ func: The function to be called
572
+ parameters: A dictionary of parameters with iterables to be mapped over
573
+
574
+ Returns:
575
+ List: A list of dictionaries to be used as parameters for each
576
+ call in the mapping operation
577
+ """
578
+ # Ensure that any parameters in kwargs are expanded before this check
579
+ parameters = explode_variadic_parameter(func, parameters)
580
+
581
+ iterable_parameters = {}
582
+ static_parameters = {}
583
+ annotated_parameters = {}
584
+ for key, val in parameters.items():
585
+ if isinstance(val, (allow_failure, quote)):
586
+ # Unwrap annotated parameters to determine if they are iterable
587
+ annotated_parameters[key] = val
588
+ val = val.unwrap()
589
+
590
+ if isinstance(val, unmapped):
591
+ static_parameters[key] = val.value
592
+ elif isiterable(val):
593
+ iterable_parameters[key] = list(val)
594
+ else:
595
+ static_parameters[key] = val
596
+
597
+ if not len(iterable_parameters):
598
+ raise MappingMissingIterable(
599
+ "No iterable parameters were received. Parameters for map must "
600
+ f"include at least one iterable. Parameters: {parameters}"
601
+ )
602
+
603
+ iterable_parameter_lengths = {
604
+ key: len(val) for key, val in iterable_parameters.items()
605
+ }
606
+ lengths = set(iterable_parameter_lengths.values())
607
+ if len(lengths) > 1:
608
+ raise MappingLengthMismatch(
609
+ "Received iterable parameters with different lengths. Parameters for map"
610
+ f" must all be the same length. Got lengths: {iterable_parameter_lengths}"
611
+ )
612
+
613
+ map_length = list(lengths)[0]
614
+
615
+ call_parameters_list = []
616
+ for i in range(map_length):
617
+ call_parameters = {key: value[i] for key, value in iterable_parameters.items()}
618
+ call_parameters.update({key: value for key, value in static_parameters.items()})
619
+
620
+ # Add default values for parameters; these are skipped earlier since they should
621
+ # not be mapped over
622
+ for key, value in get_parameter_defaults(func).items():
623
+ call_parameters.setdefault(key, value)
624
+
625
+ # Re-apply annotations to each key again
626
+ for key, annotation in annotated_parameters.items():
627
+ call_parameters[key] = annotation.rewrap(call_parameters[key])
628
+
629
+ # Collapse any previously exploded kwargs
630
+ call_parameters_list.append(collapse_variadic_parameters(func, call_parameters))
631
+
632
+ return call_parameters_list
@@ -4,6 +4,7 @@ Utilities for extensions of and operations on Python collections.
4
4
 
5
5
  import io
6
6
  import itertools
7
+ import warnings
7
8
  from collections import OrderedDict, defaultdict
8
9
  from collections.abc import Iterator as IteratorABC
9
10
  from collections.abc import Sequence
@@ -28,12 +29,7 @@ from typing import (
28
29
  )
29
30
  from unittest.mock import Mock
30
31
 
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
32
+ import pydantic
37
33
 
38
34
  # Quote moved to `prefect.utilities.annotations` but preserved here for compatibility
39
35
  from prefect.utilities.annotations import BaseAnnotation, Quote, quote # noqa
@@ -225,7 +221,7 @@ class StopVisiting(BaseException):
225
221
 
226
222
  def visit_collection(
227
223
  expr,
228
- visit_fn: Callable[[Any], Any],
224
+ visit_fn: Union[Callable[[Any, dict], Any], Callable[[Any], Any]],
229
225
  return_data: bool = False,
230
226
  max_depth: int = -1,
231
227
  context: Optional[dict] = None,
@@ -252,8 +248,10 @@ def visit_collection(
252
248
 
253
249
  Args:
254
250
  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.
251
+ visit_fn (Callable[[Any, Optional[dict]], Awaitable[Any]]): a function that
252
+ will be applied to every non-collection element of expr. The function can
253
+ accept one or two arguments. If two arguments are accepted, the second
254
+ argument will be the context dictionary.
257
255
  return_data (bool): if `True`, a copy of `expr` containing data modified
258
256
  by `visit_fn` will be returned. This is slower than `return_data=False`
259
257
  (the default).
@@ -343,39 +341,26 @@ def visit_collection(
343
341
  result = typ(**items) if return_data else None
344
342
 
345
343
  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]
344
+ typ = cast(Type[pydantic.BaseModel], typ)
354
345
 
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
- }
346
+ # when extra=allow, fields not in model_fields may be in model_fields_set
347
+ model_fields = expr.model_fields_set.union(expr.model_fields.keys())
362
348
 
363
- model_instance = typ(
364
- **{
365
- aliases.get(key) or key: value
366
- for key, value in zip(model_fields, items)
367
- }
368
- )
349
+ # We may encounter a deprecated field here, but this isn't the caller's fault
350
+ with warnings.catch_warnings():
351
+ warnings.simplefilter("ignore", category=DeprecationWarning)
369
352
 
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))
353
+ updated_data = {
354
+ field: visit_nested(getattr(expr, field)) for field in model_fields
355
+ }
376
356
 
377
- # Preserve data about which fields were explicitly set on the original model
378
- object.__setattr__(model_instance, "__fields_set__", expr.__fields_set__)
357
+ if return_data:
358
+ # Use construct to avoid validation and handle immutability
359
+ model_instance = typ.model_construct(
360
+ _fields_set=expr.model_fields_set, **updated_data
361
+ )
362
+ for private_attr in expr.__private_attributes__:
363
+ setattr(model_instance, private_attr, getattr(expr, private_attr))
379
364
  result = model_instance
380
365
  else:
381
366
  result = None
@@ -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
 
@@ -27,6 +27,10 @@ import prefect
27
27
  from prefect.utilities.importtools import lazy_import
28
28
  from prefect.utilities.slugify import slugify
29
29
 
30
+ CONTAINER_LABELS = {
31
+ "io.prefect.version": prefect.__version__,
32
+ }
33
+
30
34
 
31
35
  def python_version_minor() -> str:
32
36
  return f"{sys.version_info.major}.{sys.version_info.minor}"