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.
- prefect/__init__.py +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +405 -153
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +650 -442
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2475
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +117 -47
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +137 -45
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/py.typed +0 -0
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/py.typed +0 -0
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/blocks/kubernetes.py +0 -119
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/task_server.py +0 -322
- prefect_client-2.20.4.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
prefect/utilities/callables.py
CHANGED
@@ -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
|
49
|
-
signature will be included if not overridden.
|
50
|
-
|
51
|
-
|
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] =
|
232
|
-
definitions:
|
240
|
+
required: List[str] = pydantic.Field(default_factory=list)
|
241
|
+
definitions: Dict[str, Any] = pydantic.Field(default_factory=dict)
|
233
242
|
|
234
|
-
def
|
235
|
-
""
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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
|
-
|
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=
|
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
|
prefect/utilities/collections.py
CHANGED
@@ -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
|
-
|
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
|
-
|
236
|
-
|
237
|
-
collection,
|
238
|
-
`visit_fn`
|
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
|
241
|
-
|
242
|
-
|
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):
|
255
|
-
visit_fn (Callable[[Any],
|
256
|
-
will be applied to every non-collection element of expr
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
266
|
-
call to the `visit_fn`. The context can be mutated by each visitor and
|
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
|
-
|
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
|
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
|
-
#
|
322
|
+
# --- 2. Visit every child of the expression recursively
|
307
323
|
|
308
|
-
# If we have reached the maximum depth
|
309
|
-
if
|
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
|
-
|
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
|
-
|
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
|
-
|
358
|
+
unwrapped = expr.unwrap()
|
359
|
+
value = visit_nested(unwrapped)
|
325
360
|
|
326
|
-
if
|
327
|
-
|
328
|
-
|
329
|
-
|
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
|
-
|
370
|
+
# --- Sequences
|
371
|
+
|
372
|
+
elif isinstance(expr, (list, tuple, set)):
|
332
373
|
items = [visit_nested(o) for o in expr]
|
333
|
-
|
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
|
-
|
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
|
-
|
343
|
-
|
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
|
-
|
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
|
-
|
356
|
-
|
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
|
-
|
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
|
-
|
371
|
-
|
372
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
else:
|
381
|
-
result = None
|
415
|
+
updated_data = {
|
416
|
+
field: visit_nested(getattr(expr, field)) for field in model_fields
|
417
|
+
}
|
382
418
|
|
383
|
-
|
384
|
-
|
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
|
-
|
433
|
+
if return_data:
|
434
|
+
return result
|
387
435
|
|
388
436
|
|
389
437
|
def remove_nested_keys(keys_to_remove: List[Hashable], obj):
|
prefect/utilities/dispatch.py
CHANGED
@@ -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
|
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
|
-
|
118
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|