prefect-client 2.16.8__py3-none-any.whl → 2.17.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 +0 -18
- prefect/_internal/compatibility/deprecated.py +108 -5
- prefect/_internal/compatibility/experimental.py +9 -8
- prefect/_internal/concurrency/api.py +23 -42
- prefect/_internal/concurrency/waiters.py +25 -22
- prefect/_internal/pydantic/__init__.py +16 -3
- prefect/_internal/pydantic/_base_model.py +39 -4
- prefect/_internal/pydantic/_compat.py +69 -452
- prefect/_internal/pydantic/_flags.py +5 -0
- prefect/_internal/pydantic/_types.py +8 -0
- prefect/_internal/pydantic/utilities/__init__.py +0 -0
- prefect/_internal/pydantic/utilities/config_dict.py +72 -0
- prefect/_internal/pydantic/utilities/field_validator.py +135 -0
- prefect/_internal/pydantic/utilities/model_construct.py +56 -0
- prefect/_internal/pydantic/utilities/model_copy.py +55 -0
- prefect/_internal/pydantic/utilities/model_dump.py +136 -0
- prefect/_internal/pydantic/utilities/model_dump_json.py +112 -0
- prefect/_internal/pydantic/utilities/model_fields.py +50 -0
- prefect/_internal/pydantic/utilities/model_fields_set.py +29 -0
- prefect/_internal/pydantic/utilities/model_json_schema.py +82 -0
- prefect/_internal/pydantic/utilities/model_rebuild.py +80 -0
- prefect/_internal/pydantic/utilities/model_validate.py +75 -0
- prefect/_internal/pydantic/utilities/model_validate_json.py +68 -0
- prefect/_internal/pydantic/utilities/model_validator.py +79 -0
- prefect/_internal/pydantic/utilities/type_adapter.py +71 -0
- prefect/_internal/schemas/bases.py +1 -17
- prefect/_internal/schemas/validators.py +425 -4
- prefect/agent.py +1 -1
- prefect/blocks/kubernetes.py +7 -3
- prefect/blocks/notifications.py +18 -18
- prefect/blocks/webhook.py +1 -1
- prefect/client/base.py +7 -0
- prefect/client/cloud.py +1 -1
- prefect/client/orchestration.py +51 -11
- prefect/client/schemas/actions.py +367 -297
- prefect/client/schemas/filters.py +28 -28
- prefect/client/schemas/objects.py +78 -147
- prefect/client/schemas/responses.py +240 -60
- prefect/client/schemas/schedules.py +6 -8
- prefect/concurrency/events.py +2 -2
- prefect/context.py +4 -2
- prefect/deployments/base.py +6 -13
- prefect/deployments/deployments.py +34 -9
- prefect/deployments/runner.py +9 -27
- prefect/deprecated/packaging/base.py +5 -6
- prefect/deprecated/packaging/docker.py +19 -25
- prefect/deprecated/packaging/file.py +10 -5
- prefect/deprecated/packaging/orion.py +9 -4
- prefect/deprecated/packaging/serializers.py +8 -58
- prefect/engine.py +55 -618
- prefect/events/actions.py +16 -1
- prefect/events/clients.py +45 -13
- prefect/events/filters.py +19 -2
- prefect/events/related.py +4 -4
- prefect/events/schemas/automations.py +13 -2
- prefect/events/schemas/deployment_triggers.py +73 -5
- prefect/events/schemas/events.py +1 -1
- prefect/events/utilities.py +12 -4
- prefect/events/worker.py +26 -8
- prefect/exceptions.py +3 -8
- prefect/filesystems.py +7 -7
- prefect/flows.py +7 -3
- prefect/infrastructure/provisioners/ecs.py +1 -0
- prefect/logging/configuration.py +2 -2
- prefect/manifests.py +1 -8
- prefect/profiles.toml +1 -1
- prefect/pydantic/__init__.py +74 -2
- prefect/pydantic/main.py +26 -2
- prefect/serializers.py +6 -31
- prefect/settings.py +72 -26
- prefect/software/python.py +3 -5
- prefect/task_server.py +2 -2
- prefect/utilities/callables.py +1 -1
- prefect/utilities/collections.py +2 -1
- prefect/utilities/dispatch.py +1 -0
- prefect/utilities/engine.py +629 -0
- prefect/utilities/pydantic.py +1 -1
- prefect/utilities/schema_tools/validation.py +2 -2
- prefect/utilities/visualization.py +1 -1
- prefect/variables.py +88 -12
- prefect/workers/base.py +20 -11
- prefect/workers/block.py +4 -8
- prefect/workers/process.py +2 -5
- {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/METADATA +4 -3
- {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/RECORD +88 -72
- prefect/_internal/schemas/transformations.py +0 -106
- {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/WHEEL +0 -0
- {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,33 @@
|
|
1
|
+
"""
|
2
|
+
This module contains a collection of functions that are used to validate the
|
3
|
+
values of fields in Pydantic models. These functions are used as validators in
|
4
|
+
Pydantic models to ensure that the values of fields conform to the expected
|
5
|
+
format.
|
6
|
+
This will be subject to consolidation and refactoring over the next few months.
|
7
|
+
"""
|
8
|
+
|
1
9
|
import datetime
|
2
10
|
import json
|
3
11
|
import logging
|
4
12
|
import re
|
13
|
+
import sys
|
5
14
|
import urllib.parse
|
15
|
+
import warnings
|
16
|
+
from copy import copy
|
6
17
|
from pathlib import Path
|
7
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
18
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union
|
8
19
|
|
9
20
|
import jsonschema
|
10
21
|
import pendulum
|
22
|
+
import yaml
|
11
23
|
|
12
24
|
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
25
|
+
from prefect._internal.pydantic._flags import USE_PYDANTIC_V2
|
13
26
|
from prefect._internal.schemas.fields import DateTimeTZ
|
14
27
|
from prefect.exceptions import InvalidNameError, InvalidRepositoryURLError
|
15
28
|
from prefect.utilities.annotations import NotSet
|
29
|
+
from prefect.utilities.dockerutils import get_prefect_image_name
|
30
|
+
from prefect.utilities.filesystem import relative_path_to_current_platform
|
16
31
|
from prefect.utilities.importtools import from_qualified_name
|
17
32
|
from prefect.utilities.names import generate_slug
|
18
33
|
from prefect.utilities.pydantic import JsonPatch
|
@@ -27,9 +42,11 @@ if TYPE_CHECKING:
|
|
27
42
|
from prefect.utilities.callables import ParameterSchema
|
28
43
|
|
29
44
|
if HAS_PYDANTIC_V2:
|
30
|
-
|
31
|
-
|
32
|
-
|
45
|
+
if USE_PYDANTIC_V2:
|
46
|
+
# TODO: we need to account for rewriting the validator to not use ModelField
|
47
|
+
pass
|
48
|
+
if not USE_PYDANTIC_V2:
|
49
|
+
from pydantic.v1.fields import ModelField
|
33
50
|
|
34
51
|
|
35
52
|
def raise_on_name_with_banned_characters(name: str) -> str:
|
@@ -191,6 +208,30 @@ def handle_openapi_schema(value: Optional["ParameterSchema"]) -> "ParameterSchem
|
|
191
208
|
return value
|
192
209
|
|
193
210
|
|
211
|
+
def validate_parameters_conform_to_schema(value: dict, values: dict) -> dict:
|
212
|
+
"""Validate that the parameters conform to the parameter schema."""
|
213
|
+
if values.get("enforce_parameter_schema"):
|
214
|
+
validate_values_conform_to_schema(
|
215
|
+
value, values.get("parameter_openapi_schema"), ignore_required=True
|
216
|
+
)
|
217
|
+
return value
|
218
|
+
|
219
|
+
|
220
|
+
def validate_parameter_openapi_schema(value: dict, values: dict) -> dict:
|
221
|
+
"""Validate that the parameter_openapi_schema is a valid json schema."""
|
222
|
+
if values.get("enforce_parameter_schema"):
|
223
|
+
validate_schema(value)
|
224
|
+
return value
|
225
|
+
|
226
|
+
|
227
|
+
def return_none_schedule(v: Optional[Union[str, dict]]) -> Optional[Union[str, dict]]:
|
228
|
+
from prefect.client.schemas.schedules import NoSchedule
|
229
|
+
|
230
|
+
if isinstance(v, NoSchedule):
|
231
|
+
return None
|
232
|
+
return v
|
233
|
+
|
234
|
+
|
194
235
|
### SCHEDULE SCHEMA VALIDATORS ###
|
195
236
|
|
196
237
|
|
@@ -246,6 +287,91 @@ def reconcile_schedules(cls, values: dict) -> dict:
|
|
246
287
|
return values
|
247
288
|
|
248
289
|
|
290
|
+
# TODO: consolidate with above if possible
|
291
|
+
def reconcile_schedules_runner(values: dict) -> dict:
|
292
|
+
"""
|
293
|
+
Similar to above, we reconcile the `schedule` and `schedules` fields in a deployment.
|
294
|
+
"""
|
295
|
+
from prefect.deployments.schedules import (
|
296
|
+
create_minimal_deployment_schedule,
|
297
|
+
normalize_to_minimal_deployment_schedules,
|
298
|
+
)
|
299
|
+
|
300
|
+
schedule = values.get("schedule")
|
301
|
+
schedules = values.get("schedules")
|
302
|
+
|
303
|
+
if schedules is None and schedule is not None:
|
304
|
+
values["schedules"] = [create_minimal_deployment_schedule(schedule)]
|
305
|
+
elif schedules is not None and len(schedules) > 0:
|
306
|
+
values["schedules"] = normalize_to_minimal_deployment_schedules(schedules)
|
307
|
+
|
308
|
+
return values
|
309
|
+
|
310
|
+
|
311
|
+
def set_deployment_schedules(values: dict) -> dict:
|
312
|
+
from prefect.server.schemas.actions import DeploymentScheduleCreate
|
313
|
+
|
314
|
+
if not values.get("schedules") and values.get("schedule"):
|
315
|
+
values["schedules"] = [
|
316
|
+
DeploymentScheduleCreate(
|
317
|
+
schedule=values["schedule"],
|
318
|
+
active=values["is_schedule_active"],
|
319
|
+
)
|
320
|
+
]
|
321
|
+
|
322
|
+
return values
|
323
|
+
|
324
|
+
|
325
|
+
def remove_old_deployment_fields(values: dict) -> dict:
|
326
|
+
# 2.7.7 removed worker_pool_queue_id in lieu of worker_pool_name and
|
327
|
+
# worker_pool_queue_name. Those fields were later renamed to work_pool_name
|
328
|
+
# and work_queue_name. This validator removes old fields provided
|
329
|
+
# by older clients to avoid 422 errors.
|
330
|
+
values_copy = copy(values)
|
331
|
+
worker_pool_queue_id = values_copy.pop("worker_pool_queue_id", None)
|
332
|
+
worker_pool_name = values_copy.pop("worker_pool_name", None)
|
333
|
+
worker_pool_queue_name = values_copy.pop("worker_pool_queue_name", None)
|
334
|
+
work_pool_queue_name = values_copy.pop("work_pool_queue_name", None)
|
335
|
+
if worker_pool_queue_id:
|
336
|
+
warnings.warn(
|
337
|
+
(
|
338
|
+
"`worker_pool_queue_id` is no longer supported for creating or updating "
|
339
|
+
"deployments. Please use `work_pool_name` and "
|
340
|
+
"`work_queue_name` instead."
|
341
|
+
),
|
342
|
+
UserWarning,
|
343
|
+
)
|
344
|
+
if worker_pool_name or worker_pool_queue_name or work_pool_queue_name:
|
345
|
+
warnings.warn(
|
346
|
+
(
|
347
|
+
"`worker_pool_name`, `worker_pool_queue_name`, and "
|
348
|
+
"`work_pool_name` are"
|
349
|
+
"no longer supported for creating or updating "
|
350
|
+
"deployments. Please use `work_pool_name` and "
|
351
|
+
"`work_queue_name` instead."
|
352
|
+
),
|
353
|
+
UserWarning,
|
354
|
+
)
|
355
|
+
return values_copy
|
356
|
+
|
357
|
+
|
358
|
+
def reconcile_paused_deployment(values):
|
359
|
+
paused = values.get("paused")
|
360
|
+
is_schedule_active = values.get("is_schedule_active")
|
361
|
+
|
362
|
+
if paused is not None:
|
363
|
+
values["paused"] = paused
|
364
|
+
values["is_schedule_active"] = not paused
|
365
|
+
elif is_schedule_active is not None:
|
366
|
+
values["paused"] = not is_schedule_active
|
367
|
+
values["is_schedule_active"] = is_schedule_active
|
368
|
+
else:
|
369
|
+
values["paused"] = False
|
370
|
+
values["is_schedule_active"] = True
|
371
|
+
|
372
|
+
return values
|
373
|
+
|
374
|
+
|
249
375
|
def interval_schedule_must_be_positive(v: datetime.timedelta) -> datetime.timedelta:
|
250
376
|
if v.total_seconds() <= 0:
|
251
377
|
raise ValueError("The interval must be positive")
|
@@ -473,6 +599,9 @@ def set_default_image(values: dict) -> dict:
|
|
473
599
|
return values
|
474
600
|
|
475
601
|
|
602
|
+
### STATE SCHEMA VALIDATORS ###
|
603
|
+
|
604
|
+
|
476
605
|
def get_or_create_state_name(v: str, values: dict) -> str:
|
477
606
|
"""If a name is not provided, use the type"""
|
478
607
|
|
@@ -483,6 +612,23 @@ def get_or_create_state_name(v: str, values: dict) -> str:
|
|
483
612
|
return v
|
484
613
|
|
485
614
|
|
615
|
+
def set_default_scheduled_time(cls, values: dict) -> dict:
|
616
|
+
"""
|
617
|
+
TODO: This should throw an error instead of setting a default but is out of
|
618
|
+
scope for https://github.com/PrefectHQ/orion/pull/174/ and can be rolled
|
619
|
+
into work refactoring state initialization
|
620
|
+
"""
|
621
|
+
from prefect.server.schemas.states import StateType
|
622
|
+
|
623
|
+
if values.get("type") == StateType.SCHEDULED:
|
624
|
+
state_details = values.setdefault(
|
625
|
+
"state_details", cls.__fields__["state_details"].get_default()
|
626
|
+
)
|
627
|
+
if not state_details.scheduled_time:
|
628
|
+
state_details.scheduled_time = pendulum.now("utc")
|
629
|
+
return values
|
630
|
+
|
631
|
+
|
486
632
|
def get_or_create_run_name(name):
|
487
633
|
return name or generate_slug(2)
|
488
634
|
|
@@ -558,6 +704,48 @@ def validate_picklelib(value: str) -> str:
|
|
558
704
|
return value
|
559
705
|
|
560
706
|
|
707
|
+
def validate_picklelib_version(values: dict) -> dict:
|
708
|
+
"""
|
709
|
+
Infers a default value for `picklelib_version` if null or ensures it matches
|
710
|
+
the version retrieved from the `pickelib`.
|
711
|
+
"""
|
712
|
+
picklelib = values.get("picklelib")
|
713
|
+
picklelib_version = values.get("picklelib_version")
|
714
|
+
|
715
|
+
if not picklelib:
|
716
|
+
raise ValueError("Unable to check version of unrecognized picklelib module")
|
717
|
+
|
718
|
+
pickler = from_qualified_name(picklelib)
|
719
|
+
pickler_version = getattr(pickler, "__version__", None)
|
720
|
+
|
721
|
+
if not picklelib_version:
|
722
|
+
values["picklelib_version"] = pickler_version
|
723
|
+
elif picklelib_version != pickler_version:
|
724
|
+
warnings.warn(
|
725
|
+
(
|
726
|
+
f"Mismatched {picklelib!r} versions. Found {pickler_version} in the"
|
727
|
+
f" environment but {picklelib_version} was requested. This may"
|
728
|
+
" cause the serializer to fail."
|
729
|
+
),
|
730
|
+
RuntimeWarning,
|
731
|
+
stacklevel=3,
|
732
|
+
)
|
733
|
+
|
734
|
+
return values
|
735
|
+
|
736
|
+
|
737
|
+
def validate_picklelib_and_modules(values: dict) -> dict:
|
738
|
+
"""
|
739
|
+
Prevents modules from being specified if picklelib is not cloudpickle
|
740
|
+
"""
|
741
|
+
if values.get("picklelib") != "cloudpickle" and values.get("pickle_modules"):
|
742
|
+
raise ValueError(
|
743
|
+
"`pickle_modules` cannot be used without 'cloudpickle'. Got"
|
744
|
+
f" {values.get('picklelib')!r}."
|
745
|
+
)
|
746
|
+
return values
|
747
|
+
|
748
|
+
|
561
749
|
def validate_dump_kwargs(value: dict) -> dict:
|
562
750
|
# `default` is set by `object_encoder`. A user provided callable would make this
|
563
751
|
# class unserializable anyway.
|
@@ -607,3 +795,236 @@ def validate_compressionlib(value: str) -> str:
|
|
607
795
|
)
|
608
796
|
|
609
797
|
return value
|
798
|
+
|
799
|
+
|
800
|
+
# TODO: if we use this elsewhere we can change the error message to be more generic
|
801
|
+
def list_length_50_or_less(v: Optional[List[float]]) -> Optional[List[float]]:
|
802
|
+
if isinstance(v, list) and (len(v) > 50):
|
803
|
+
raise ValueError("Can not configure more than 50 retry delays per task.")
|
804
|
+
return v
|
805
|
+
|
806
|
+
|
807
|
+
# TODO: if we use this elsewhere we can change the error message to be more generic
|
808
|
+
def validate_not_negative(v: Optional[float]) -> Optional[float]:
|
809
|
+
if v is not None and v < 0:
|
810
|
+
raise ValueError("`retry_jitter_factor` must be >= 0.")
|
811
|
+
return v
|
812
|
+
|
813
|
+
|
814
|
+
def validate_message_template_variables(v: Optional[str]) -> Optional[str]:
|
815
|
+
from prefect.client.schemas.objects import FLOW_RUN_NOTIFICATION_TEMPLATE_KWARGS
|
816
|
+
|
817
|
+
if v is not None:
|
818
|
+
try:
|
819
|
+
v.format(**{k: "test" for k in FLOW_RUN_NOTIFICATION_TEMPLATE_KWARGS})
|
820
|
+
except KeyError as exc:
|
821
|
+
raise ValueError(f"Invalid template variable provided: '{exc.args[0]}'")
|
822
|
+
return v
|
823
|
+
|
824
|
+
|
825
|
+
def validate_default_queue_id_not_none(v: Optional[str]) -> Optional[str]:
|
826
|
+
if v is None:
|
827
|
+
raise ValueError(
|
828
|
+
"`default_queue_id` is a required field. If you are "
|
829
|
+
"creating a new WorkPool and don't have a queue "
|
830
|
+
"ID yet, use the `actions.WorkPoolCreate` model instead."
|
831
|
+
)
|
832
|
+
return v
|
833
|
+
|
834
|
+
|
835
|
+
def validate_max_metadata_length(
|
836
|
+
v: Optional[Dict[str, Any]],
|
837
|
+
) -> Optional[Dict[str, Any]]:
|
838
|
+
max_metadata_length = 500
|
839
|
+
if not isinstance(v, dict):
|
840
|
+
return v
|
841
|
+
for key in v.keys():
|
842
|
+
if len(str(v[key])) > max_metadata_length:
|
843
|
+
v[key] = str(v[key])[:max_metadata_length] + "..."
|
844
|
+
return v
|
845
|
+
|
846
|
+
|
847
|
+
### DOCKER SCHEMA VALIDATORS ###
|
848
|
+
|
849
|
+
|
850
|
+
def validate_registry_url(value: Optional[str]) -> Optional[str]:
|
851
|
+
if isinstance(value, str):
|
852
|
+
if "://" not in value:
|
853
|
+
return "https://" + value
|
854
|
+
return value
|
855
|
+
|
856
|
+
|
857
|
+
def convert_labels_to_docker_format(labels: Dict[str, str]) -> Dict[str, str]:
|
858
|
+
labels = labels or {}
|
859
|
+
new_labels = {}
|
860
|
+
for name, value in labels.items():
|
861
|
+
if "/" in name:
|
862
|
+
namespace, key = name.split("/", maxsplit=1)
|
863
|
+
new_namespace = ".".join(reversed(namespace.split(".")))
|
864
|
+
new_labels[f"{new_namespace}.{key}"] = value
|
865
|
+
else:
|
866
|
+
new_labels[name] = value
|
867
|
+
return new_labels
|
868
|
+
|
869
|
+
|
870
|
+
def check_volume_format(volumes: List[str]) -> List[str]:
|
871
|
+
for volume in volumes:
|
872
|
+
if ":" not in volume:
|
873
|
+
raise ValueError(
|
874
|
+
"Invalid volume specification. "
|
875
|
+
f"Expected format 'path:container_path', but got {volume!r}"
|
876
|
+
)
|
877
|
+
|
878
|
+
return volumes
|
879
|
+
|
880
|
+
|
881
|
+
def assign_default_base_image(values: Mapping[str, Any]) -> Mapping[str, Any]:
|
882
|
+
from prefect.software.conda import CondaEnvironment
|
883
|
+
|
884
|
+
if not values.get("base_image") and not values.get("dockerfile"):
|
885
|
+
values["base_image"] = get_prefect_image_name(
|
886
|
+
flavor=(
|
887
|
+
"conda"
|
888
|
+
if isinstance(values.get("python_environment"), CondaEnvironment)
|
889
|
+
else None
|
890
|
+
)
|
891
|
+
)
|
892
|
+
return values
|
893
|
+
|
894
|
+
|
895
|
+
def base_image_xor_dockerfile(values: Mapping[str, Any]):
|
896
|
+
if values.get("base_image") and values.get("dockerfile"):
|
897
|
+
raise ValueError(
|
898
|
+
"Either `base_image` or `dockerfile` should be provided, but not both"
|
899
|
+
)
|
900
|
+
return values
|
901
|
+
|
902
|
+
|
903
|
+
def set_default_python_environment(values: Mapping[str, Any]) -> Mapping[str, Any]:
|
904
|
+
from prefect.software.python import PythonEnvironment
|
905
|
+
|
906
|
+
if values.get("base_image") and not values.get("python_environment"):
|
907
|
+
values["python_environment"] = PythonEnvironment.from_environment()
|
908
|
+
return values
|
909
|
+
|
910
|
+
|
911
|
+
### SETTINGS SCHEMA VALIDATORS ###
|
912
|
+
|
913
|
+
|
914
|
+
def validate_settings(value: dict) -> dict:
|
915
|
+
from prefect.settings import SETTING_VARIABLES, Setting
|
916
|
+
|
917
|
+
if value is None:
|
918
|
+
return value
|
919
|
+
|
920
|
+
# Cast string setting names to variables
|
921
|
+
validated = {}
|
922
|
+
for setting, val in value.items():
|
923
|
+
if isinstance(setting, str) and setting in SETTING_VARIABLES:
|
924
|
+
validated[SETTING_VARIABLES[setting]] = val
|
925
|
+
elif isinstance(setting, Setting):
|
926
|
+
validated[setting] = val
|
927
|
+
else:
|
928
|
+
raise ValueError(f"Unknown setting {setting!r}.")
|
929
|
+
|
930
|
+
return validated
|
931
|
+
|
932
|
+
|
933
|
+
def validate_yaml(value: Union[str, dict]) -> dict:
|
934
|
+
if isinstance(value, str):
|
935
|
+
return yaml.safe_load(value)
|
936
|
+
return value
|
937
|
+
|
938
|
+
|
939
|
+
### TASK RUN SCHEMA VALIDATORS ###
|
940
|
+
|
941
|
+
|
942
|
+
def validate_cache_key_length(cache_key: Optional[str]) -> Optional[str]:
|
943
|
+
from prefect.settings import (
|
944
|
+
PREFECT_API_TASK_CACHE_KEY_MAX_LENGTH,
|
945
|
+
)
|
946
|
+
|
947
|
+
if cache_key and len(cache_key) > PREFECT_API_TASK_CACHE_KEY_MAX_LENGTH.value():
|
948
|
+
raise ValueError(
|
949
|
+
"Cache key exceeded maximum allowed length of"
|
950
|
+
f" {PREFECT_API_TASK_CACHE_KEY_MAX_LENGTH.value()} characters."
|
951
|
+
)
|
952
|
+
return cache_key
|
953
|
+
|
954
|
+
|
955
|
+
def set_run_policy_deprecated_fields(values: dict) -> dict:
|
956
|
+
"""
|
957
|
+
If deprecated fields are provided, populate the corresponding new fields
|
958
|
+
to preserve orchestration behavior.
|
959
|
+
"""
|
960
|
+
if not values.get("retries", None) and values.get("max_retries", 0) != 0:
|
961
|
+
values["retries"] = values["max_retries"]
|
962
|
+
|
963
|
+
if (
|
964
|
+
not values.get("retry_delay", None)
|
965
|
+
and values.get("retry_delay_seconds", 0) != 0
|
966
|
+
):
|
967
|
+
values["retry_delay"] = values["retry_delay_seconds"]
|
968
|
+
|
969
|
+
return values
|
970
|
+
|
971
|
+
|
972
|
+
### PYTHON ENVIRONMENT SCHEMA VALIDATORS ###
|
973
|
+
|
974
|
+
|
975
|
+
def infer_python_version(value: Optional[str]) -> Optional[str]:
|
976
|
+
if value is None:
|
977
|
+
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
978
|
+
return value
|
979
|
+
|
980
|
+
|
981
|
+
def return_v_or_none(v: Optional[str]) -> Optional[str]:
|
982
|
+
"""Make sure that empty strings are treated as None"""
|
983
|
+
if not v:
|
984
|
+
return None
|
985
|
+
return v
|
986
|
+
|
987
|
+
|
988
|
+
### INFRASTRUCTURE BLOCK SCHEMA VALIDATORS ###
|
989
|
+
|
990
|
+
|
991
|
+
def validate_block_is_infrastructure(v: "Block") -> "Block":
|
992
|
+
from prefect.infrastructure.base import Infrastructure
|
993
|
+
|
994
|
+
print("v: ", v)
|
995
|
+
if not isinstance(v, Infrastructure):
|
996
|
+
raise TypeError("Provided block is not a valid infrastructure block.")
|
997
|
+
|
998
|
+
return v
|
999
|
+
|
1000
|
+
|
1001
|
+
### BLOCK SCHEMA VALIDATORS ###
|
1002
|
+
|
1003
|
+
|
1004
|
+
def validate_parent_and_ref_diff(values: dict) -> dict:
|
1005
|
+
parent_id = values.get("parent_block_document_id")
|
1006
|
+
ref_id = values.get("reference_block_document_id")
|
1007
|
+
if parent_id and ref_id and parent_id == ref_id:
|
1008
|
+
raise ValueError(
|
1009
|
+
"`parent_block_document_id` and `reference_block_document_id` cannot be"
|
1010
|
+
" the same"
|
1011
|
+
)
|
1012
|
+
return values
|
1013
|
+
|
1014
|
+
|
1015
|
+
def validate_name_present_on_nonanonymous_blocks(values: dict) -> dict:
|
1016
|
+
# anonymous blocks may have no name prior to actually being
|
1017
|
+
# stored in the database
|
1018
|
+
if not values.get("is_anonymous") and not values.get("name"):
|
1019
|
+
raise ValueError("Names must be provided for block documents.")
|
1020
|
+
return values
|
1021
|
+
|
1022
|
+
|
1023
|
+
### PROCESS JOB CONFIGURATION VALIDATORS ###
|
1024
|
+
|
1025
|
+
|
1026
|
+
def validate_command(v: str) -> Path:
|
1027
|
+
"""Make sure that the working directory is formatted for the current platform."""
|
1028
|
+
if v:
|
1029
|
+
return relative_path_to_current_platform(v)
|
1030
|
+
return v
|
prefect/agent.py
CHANGED
@@ -445,7 +445,7 @@ class PrefectAgent:
|
|
445
445
|
# attributes of the infrastructure block
|
446
446
|
doc_dict = infra_document.dict()
|
447
447
|
infra_dict = doc_dict.get("data", {})
|
448
|
-
for override, value in (deployment.
|
448
|
+
for override, value in (deployment.job_variables or {}).items():
|
449
449
|
nested_fields = override.split(".")
|
450
450
|
data = infra_dict
|
451
451
|
for field in nested_fields[:-1]:
|
prefect/blocks/kubernetes.py
CHANGED
@@ -3,7 +3,9 @@ from typing import TYPE_CHECKING, Dict, Type
|
|
3
3
|
|
4
4
|
import yaml
|
5
5
|
|
6
|
+
from prefect._internal.compatibility.deprecated import deprecated_class
|
6
7
|
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
8
|
+
from prefect._internal.schemas.validators import validate_yaml
|
7
9
|
|
8
10
|
if HAS_PYDANTIC_V2:
|
9
11
|
from pydantic.v1 import Field, validator
|
@@ -23,6 +25,10 @@ else:
|
|
23
25
|
kubernetes = lazy_import("kubernetes")
|
24
26
|
|
25
27
|
|
28
|
+
@deprecated_class(
|
29
|
+
start_date="Mar 2024",
|
30
|
+
help="Use the KubernetesClusterConfig block from prefect-kubernetes instead.",
|
31
|
+
)
|
26
32
|
class KubernetesClusterConfig(Block):
|
27
33
|
"""
|
28
34
|
Stores configuration for interaction with Kubernetes clusters.
|
@@ -55,9 +61,7 @@ class KubernetesClusterConfig(Block):
|
|
55
61
|
|
56
62
|
@validator("config", pre=True)
|
57
63
|
def parse_yaml_config(cls, value):
|
58
|
-
|
59
|
-
return yaml.safe_load(value)
|
60
|
-
return value
|
64
|
+
return validate_yaml(value)
|
61
65
|
|
62
66
|
@classmethod
|
63
67
|
def from_file(cls: Type[Self], path: Path = None, context_name: str = None) -> Self:
|
prefect/blocks/notifications.py
CHANGED
@@ -77,7 +77,7 @@ class AppriseNotificationBlock(AbstractAppriseNotificationBlock, ABC):
|
|
77
77
|
default=...,
|
78
78
|
title="Webhook URL",
|
79
79
|
description="Incoming webhook URL used to send notifications.",
|
80
|
-
|
80
|
+
examples=["https://hooks.example.com/XXX"],
|
81
81
|
)
|
82
82
|
|
83
83
|
|
@@ -105,7 +105,7 @@ class SlackWebhook(AppriseNotificationBlock):
|
|
105
105
|
default=...,
|
106
106
|
title="Webhook URL",
|
107
107
|
description="Slack incoming webhook URL used to send notifications.",
|
108
|
-
|
108
|
+
examples=["https://hooks.slack.com/XXX"],
|
109
109
|
)
|
110
110
|
|
111
111
|
|
@@ -131,9 +131,9 @@ class MicrosoftTeamsWebhook(AppriseNotificationBlock):
|
|
131
131
|
...,
|
132
132
|
title="Webhook URL",
|
133
133
|
description="The Teams incoming webhook URL used to send notifications.",
|
134
|
-
|
134
|
+
examples=[
|
135
135
|
"https://your-org.webhook.office.com/webhookb2/XXX/IncomingWebhook/YYY/ZZZ"
|
136
|
-
|
136
|
+
],
|
137
137
|
)
|
138
138
|
|
139
139
|
|
@@ -222,7 +222,7 @@ class PagerDutyWebHook(AbstractAppriseNotificationBlock):
|
|
222
222
|
custom_details: Optional[Dict[str, str]] = Field(
|
223
223
|
default=None,
|
224
224
|
description="Additional details to include as part of the payload.",
|
225
|
-
|
225
|
+
examples=['{"disk_space_left": "145GB"}'],
|
226
226
|
)
|
227
227
|
|
228
228
|
def block_initialization(self) -> None:
|
@@ -283,14 +283,14 @@ class TwilioSMS(AbstractAppriseNotificationBlock):
|
|
283
283
|
from_phone_number: str = Field(
|
284
284
|
default=...,
|
285
285
|
description="The valid Twilio phone number to send the message from.",
|
286
|
-
|
286
|
+
examples=["18001234567"],
|
287
287
|
)
|
288
288
|
|
289
289
|
to_phone_numbers: List[str] = Field(
|
290
290
|
default=...,
|
291
291
|
description="A list of valid Twilio phone number(s) to send the message to.",
|
292
292
|
# not wrapped in brackets because of the way UI displays examples; in code should be ["18004242424"]
|
293
|
-
|
293
|
+
examples=["18004242424"],
|
294
294
|
)
|
295
295
|
|
296
296
|
def block_initialization(self) -> None:
|
@@ -366,7 +366,7 @@ class OpsgenieWebhook(AbstractAppriseNotificationBlock):
|
|
366
366
|
"A comma-separated list of tags you can associate with your Opsgenie"
|
367
367
|
" message."
|
368
368
|
),
|
369
|
-
|
369
|
+
examples=['["tag1", "tag2"]'],
|
370
370
|
)
|
371
371
|
|
372
372
|
priority: Optional[str] = Field(
|
@@ -388,7 +388,7 @@ class OpsgenieWebhook(AbstractAppriseNotificationBlock):
|
|
388
388
|
details: Optional[Dict[str, str]] = Field(
|
389
389
|
default=None,
|
390
390
|
description="Additional details composed of key/values pairs.",
|
391
|
-
|
391
|
+
examples=['{"key1": "value1", "key2": "value2"}'],
|
392
392
|
)
|
393
393
|
|
394
394
|
def block_initialization(self) -> None:
|
@@ -445,7 +445,7 @@ class MattermostWebhook(AbstractAppriseNotificationBlock):
|
|
445
445
|
hostname: str = Field(
|
446
446
|
default=...,
|
447
447
|
description="The hostname of your Mattermost server.",
|
448
|
-
|
448
|
+
examples=["Mattermost.example.com"],
|
449
449
|
)
|
450
450
|
|
451
451
|
token: SecretStr = Field(
|
@@ -617,7 +617,7 @@ class CustomWebhookNotificationBlock(NotificationBlock):
|
|
617
617
|
url: str = Field(
|
618
618
|
title="Webhook URL",
|
619
619
|
description="The webhook URL.",
|
620
|
-
|
620
|
+
examples=["https://hooks.slack.com/XXX"],
|
621
621
|
)
|
622
622
|
|
623
623
|
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = Field(
|
@@ -631,10 +631,10 @@ class CustomWebhookNotificationBlock(NotificationBlock):
|
|
631
631
|
default=None,
|
632
632
|
title="JSON Data",
|
633
633
|
description="Send json data as payload.",
|
634
|
-
|
634
|
+
examples=[
|
635
635
|
'{"text": "{{subject}}\\n{{body}}", "title": "{{name}}", "token":'
|
636
636
|
' "{{tokenFromSecrets}}"}'
|
637
|
-
|
637
|
+
],
|
638
638
|
)
|
639
639
|
form_data: Optional[Dict[str, str]] = Field(
|
640
640
|
default=None,
|
@@ -642,10 +642,10 @@ class CustomWebhookNotificationBlock(NotificationBlock):
|
|
642
642
|
description=(
|
643
643
|
"Send form data as payload. Should not be used together with _JSON Data_."
|
644
644
|
),
|
645
|
-
|
645
|
+
examples=[
|
646
646
|
'{"text": "{{subject}}\\n{{body}}", "title": "{{name}}", "token":'
|
647
647
|
' "{{tokenFromSecrets}}"}'
|
648
|
-
|
648
|
+
],
|
649
649
|
)
|
650
650
|
|
651
651
|
headers: Optional[Dict[str, str]] = Field(None, description="Custom headers.")
|
@@ -659,7 +659,7 @@ class CustomWebhookNotificationBlock(NotificationBlock):
|
|
659
659
|
default_factory=lambda: SecretDict(dict()),
|
660
660
|
title="Custom Secret Values",
|
661
661
|
description="A dictionary of secret values to be substituted in other configs.",
|
662
|
-
|
662
|
+
examples=['{"tokenFromSecrets":"SomeSecretToken"}'],
|
663
663
|
)
|
664
664
|
|
665
665
|
def _build_request_args(self, body: str, subject: Optional[str]):
|
@@ -753,14 +753,14 @@ class SendgridEmail(AbstractAppriseNotificationBlock):
|
|
753
753
|
sender_email: str = Field(
|
754
754
|
title="Sender email id",
|
755
755
|
description="The sender email id.",
|
756
|
-
|
756
|
+
examples=["test-support@gmail.com"],
|
757
757
|
)
|
758
758
|
|
759
759
|
to_emails: List[str] = Field(
|
760
760
|
default=...,
|
761
761
|
title="Recipient emails",
|
762
762
|
description="Email ids of all recipients.",
|
763
|
-
|
763
|
+
examples=['"recipient1@gmail.com"'],
|
764
764
|
)
|
765
765
|
|
766
766
|
def block_initialization(self) -> None:
|