prefect-client 2.16.5__py3-none-any.whl → 2.16.7__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 (49) hide show
  1. prefect/_internal/pydantic/__init__.py +21 -1
  2. prefect/_internal/pydantic/_base_model.py +16 -0
  3. prefect/_internal/pydantic/_compat.py +352 -68
  4. prefect/_internal/pydantic/_flags.py +15 -0
  5. prefect/_internal/pydantic/v1_schema.py +48 -0
  6. prefect/_internal/pydantic/v2_schema.py +6 -2
  7. prefect/_internal/schemas/validators.py +582 -9
  8. prefect/artifacts.py +179 -70
  9. prefect/client/cloud.py +4 -1
  10. prefect/client/orchestration.py +1 -1
  11. prefect/client/schemas/actions.py +2 -2
  12. prefect/client/schemas/objects.py +13 -24
  13. prefect/client/schemas/schedules.py +18 -80
  14. prefect/deployments/deployments.py +22 -86
  15. prefect/deployments/runner.py +8 -11
  16. prefect/events/__init__.py +40 -1
  17. prefect/events/clients.py +17 -20
  18. prefect/events/filters.py +5 -6
  19. prefect/events/related.py +1 -1
  20. prefect/events/schemas/__init__.py +5 -0
  21. prefect/events/schemas/automations.py +303 -0
  22. prefect/events/{schemas.py → schemas/deployment_triggers.py} +146 -270
  23. prefect/events/schemas/events.py +285 -0
  24. prefect/events/schemas/labelling.py +106 -0
  25. prefect/events/utilities.py +2 -2
  26. prefect/events/worker.py +1 -1
  27. prefect/filesystems.py +8 -37
  28. prefect/flows.py +4 -4
  29. prefect/infrastructure/kubernetes.py +12 -56
  30. prefect/infrastructure/provisioners/__init__.py +1 -0
  31. prefect/pydantic/__init__.py +4 -0
  32. prefect/pydantic/main.py +15 -0
  33. prefect/runner/runner.py +2 -2
  34. prefect/runner/server.py +1 -1
  35. prefect/serializers.py +13 -61
  36. prefect/settings.py +35 -13
  37. prefect/task_server.py +21 -7
  38. prefect/utilities/asyncutils.py +1 -1
  39. prefect/utilities/callables.py +2 -2
  40. prefect/utilities/context.py +33 -1
  41. prefect/utilities/schema_tools/hydration.py +14 -6
  42. prefect/workers/base.py +1 -2
  43. prefect/workers/block.py +3 -7
  44. {prefect_client-2.16.5.dist-info → prefect_client-2.16.7.dist-info}/METADATA +2 -2
  45. {prefect_client-2.16.5.dist-info → prefect_client-2.16.7.dist-info}/RECORD +48 -40
  46. prefect/utilities/validation.py +0 -63
  47. {prefect_client-2.16.5.dist-info → prefect_client-2.16.7.dist-info}/LICENSE +0 -0
  48. {prefect_client-2.16.5.dist-info → prefect_client-2.16.7.dist-info}/WHEEL +0 -0
  49. {prefect_client-2.16.5.dist-info → prefect_client-2.16.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,285 @@
1
+ import copy
2
+ from collections import defaultdict
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ Iterable,
7
+ List,
8
+ Mapping,
9
+ Optional,
10
+ Sequence,
11
+ Tuple,
12
+ Union,
13
+ cast,
14
+ )
15
+ from uuid import UUID, uuid4
16
+
17
+ import pendulum
18
+ from pydantic import Field, root_validator, validator
19
+
20
+ from prefect._internal.pydantic import HAS_PYDANTIC_V2
21
+ from prefect._internal.schemas.bases import PrefectBaseModel
22
+ from prefect._internal.schemas.fields import DateTimeTZ
23
+ from prefect.logging import get_logger
24
+ from prefect.settings import (
25
+ PREFECT_EVENTS_MAXIMUM_LABELS_PER_RESOURCE,
26
+ PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES,
27
+ )
28
+
29
+ if HAS_PYDANTIC_V2:
30
+ from pydantic.v1 import Field, root_validator, validator
31
+ else:
32
+ from pydantic import Field, root_validator, validator
33
+
34
+ from .labelling import Labelled
35
+
36
+ logger = get_logger(__name__)
37
+
38
+
39
+ class Resource(Labelled):
40
+ """An observable business object of interest to the user"""
41
+
42
+ @root_validator(pre=True)
43
+ def enforce_maximum_labels(cls, values: Dict[str, Any]):
44
+ labels = values.get("__root__")
45
+ if not isinstance(labels, dict):
46
+ return values
47
+
48
+ if len(labels) > PREFECT_EVENTS_MAXIMUM_LABELS_PER_RESOURCE.value():
49
+ raise ValueError(
50
+ "The maximum number of labels per resource "
51
+ f"is {PREFECT_EVENTS_MAXIMUM_LABELS_PER_RESOURCE.value()}"
52
+ )
53
+
54
+ return values
55
+
56
+ @root_validator(pre=True)
57
+ def requires_resource_id(cls, values: Dict[str, Any]):
58
+ labels = values.get("__root__")
59
+ if not isinstance(labels, dict):
60
+ return values
61
+
62
+ labels = cast(Dict[str, str], labels)
63
+
64
+ if "prefect.resource.id" not in labels:
65
+ raise ValueError("Resources must include the prefect.resource.id label")
66
+ if not labels["prefect.resource.id"]:
67
+ raise ValueError("The prefect.resource.id label must be non-empty")
68
+
69
+ return values
70
+
71
+ @property
72
+ def id(self) -> str:
73
+ return self["prefect.resource.id"]
74
+
75
+ @property
76
+ def name(self) -> Optional[str]:
77
+ return self.get("prefect.resource.name")
78
+
79
+
80
+ class RelatedResource(Resource):
81
+ """A Resource with a specific role in an Event"""
82
+
83
+ @root_validator(pre=True)
84
+ def requires_resource_role(cls, values: Dict[str, Any]):
85
+ labels = values.get("__root__")
86
+ if not isinstance(labels, dict):
87
+ return values
88
+
89
+ labels = cast(Dict[str, str], labels)
90
+
91
+ if "prefect.resource.role" not in labels:
92
+ raise ValueError(
93
+ "Related Resources must include the prefect.resource.role label"
94
+ )
95
+ if not labels["prefect.resource.role"]:
96
+ raise ValueError("The prefect.resource.role label must be non-empty")
97
+
98
+ return values
99
+
100
+ @property
101
+ def role(self) -> str:
102
+ return self["prefect.resource.role"]
103
+
104
+
105
+ class Event(PrefectBaseModel):
106
+ """The client-side view of an event that has happened to a Resource"""
107
+
108
+ occurred: DateTimeTZ = Field(
109
+ default_factory=lambda: pendulum.now("UTC"),
110
+ description="When the event happened from the sender's perspective",
111
+ )
112
+ event: str = Field(
113
+ description="The name of the event that happened",
114
+ )
115
+ resource: Resource = Field(
116
+ description="The primary Resource this event concerns",
117
+ )
118
+ related: List[RelatedResource] = Field(
119
+ default_factory=list,
120
+ description="A list of additional Resources involved in this event",
121
+ )
122
+ payload: Dict[str, Any] = Field(
123
+ default_factory=dict,
124
+ description="An open-ended set of data describing what happened",
125
+ )
126
+ id: UUID = Field(
127
+ default_factory=uuid4,
128
+ description="The client-provided identifier of this event",
129
+ )
130
+ follows: Optional[UUID] = Field(
131
+ None,
132
+ description=(
133
+ "The ID of an event that is known to have occurred prior to this one. "
134
+ "If set, this may be used to establish a more precise ordering of causally-"
135
+ "related events when they occur close enough together in time that the "
136
+ "system may receive them out-of-order."
137
+ ),
138
+ )
139
+
140
+ @property
141
+ def involved_resources(self) -> Sequence[Resource]:
142
+ return [self.resource] + list(self.related)
143
+
144
+ @property
145
+ def resource_in_role(self) -> Mapping[str, RelatedResource]:
146
+ """Returns a mapping of roles to the first related resource in that role"""
147
+ return {related.role: related for related in reversed(self.related)}
148
+
149
+ @property
150
+ def resources_in_role(self) -> Mapping[str, Sequence[RelatedResource]]:
151
+ """Returns a mapping of roles to related resources in that role"""
152
+ resources: Dict[str, List[RelatedResource]] = defaultdict(list)
153
+ for related in self.related:
154
+ resources[related.role].append(related)
155
+ return resources
156
+
157
+ @validator("related")
158
+ def enforce_maximum_related_resources(cls, value: List[RelatedResource]):
159
+ if len(value) > PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES.value():
160
+ raise ValueError(
161
+ "The maximum number of related resources "
162
+ f"is {PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES.value()}"
163
+ )
164
+
165
+ return value
166
+
167
+ def find_resource_label(self, label: str) -> Optional[str]:
168
+ """Finds the value of the given label in this event's resource or one of its
169
+ related resources. If the label starts with `related:<role>:`, search for the
170
+ first matching label in a related resource with that role."""
171
+ directive, _, related_label = label.rpartition(":")
172
+ directive, _, role = directive.partition(":")
173
+ if directive == "related":
174
+ for related in self.related:
175
+ if related.role == role:
176
+ return related.get(related_label)
177
+ return self.resource.get(label)
178
+
179
+
180
+ class ReceivedEvent(Event):
181
+ """The server-side view of an event that has happened to a Resource after it has
182
+ been received by the server"""
183
+
184
+ class Config:
185
+ orm_mode = True
186
+
187
+ received: DateTimeTZ = Field(
188
+ ...,
189
+ description="When the event was received by Prefect Cloud",
190
+ )
191
+
192
+
193
+ def matches(expected: str, value: Optional[str]) -> bool:
194
+ """Returns true if the given value matches the expected string, which may
195
+ include wildcards"""
196
+ if value is None:
197
+ return False
198
+
199
+ # TODO: handle wildcards/globs better than this
200
+ if expected.endswith("*"):
201
+ return value.startswith(expected[:-1])
202
+
203
+ return value == expected
204
+
205
+
206
+ class ResourceSpecification(PrefectBaseModel):
207
+ """A specification that may match zero, one, or many resources, used to target or
208
+ select a set of resources in a query or automation. A resource must match at least
209
+ one value of all of the provided labels"""
210
+
211
+ __root__: Dict[str, Union[str, List[str]]]
212
+
213
+ def matches_every_resource(self) -> bool:
214
+ return len(self) == 0
215
+
216
+ def matches_every_resource_of_kind(self, prefix: str) -> bool:
217
+ if self.matches_every_resource():
218
+ return True
219
+
220
+ if len(self.__root__) == 1:
221
+ if resource_id := self.__root__.get("prefect.resource.id"):
222
+ values = [resource_id] if isinstance(resource_id, str) else resource_id
223
+ return any(value == f"{prefix}.*" for value in values)
224
+
225
+ return False
226
+
227
+ def includes(self, candidates: Iterable[Resource]) -> bool:
228
+ if self.matches_every_resource():
229
+ return True
230
+
231
+ for candidate in candidates:
232
+ if self.matches(candidate):
233
+ return True
234
+
235
+ return False
236
+
237
+ def matches(self, resource: Resource) -> bool:
238
+ for label, expected in self.items():
239
+ value = resource.get(label)
240
+ if not any(matches(candidate, value) for candidate in expected):
241
+ return False
242
+ return True
243
+
244
+ def items(self) -> Iterable[Tuple[str, List[str]]]:
245
+ return [
246
+ (label, [value] if isinstance(value, str) else value)
247
+ for label, value in self.__root__.items()
248
+ ]
249
+
250
+ def __contains__(self, key: str) -> bool:
251
+ return self.__root__.__contains__(key)
252
+
253
+ def __getitem__(self, key: str) -> List[str]:
254
+ value = self.__root__[key]
255
+ if not value:
256
+ return []
257
+ if not isinstance(value, list):
258
+ value = [value]
259
+ return value
260
+
261
+ def pop(
262
+ self, key: str, default: Optional[Union[str, List[str]]] = None
263
+ ) -> Optional[List[str]]:
264
+ value = self.__root__.pop(key, default)
265
+ if not value:
266
+ return []
267
+ if not isinstance(value, list):
268
+ value = [value]
269
+ return value
270
+
271
+ def get(
272
+ self, key: str, default: Optional[Union[str, List[str]]] = None
273
+ ) -> Optional[List[str]]:
274
+ value = self.__root__.get(key, default)
275
+ if not value:
276
+ return []
277
+ if not isinstance(value, list):
278
+ value = [value]
279
+ return value
280
+
281
+ def __len__(self) -> int:
282
+ return len(self.__root__)
283
+
284
+ def deepcopy(self) -> "ResourceSpecification":
285
+ return ResourceSpecification(__root__=copy.deepcopy(self.__root__))
@@ -0,0 +1,106 @@
1
+ from typing import Dict, Iterable, Iterator, List, Optional, Tuple
2
+
3
+ from prefect._internal.schemas.bases import PrefectBaseModel
4
+
5
+
6
+ class LabelDiver:
7
+ """The LabelDiver supports templating use cases for any Labelled object, by
8
+ presenting the labels as a graph of objects that may be accessed by attribute. For
9
+ example:
10
+
11
+ diver = LabelDiver({
12
+ 'hello.world': 'foo',
13
+ 'hello.world.again': 'bar'
14
+ })
15
+
16
+ assert str(diver.hello.world) == 'foo'
17
+ assert str(diver.hello.world.again) == 'bar'
18
+
19
+ """
20
+
21
+ _value: str
22
+ _divers: Dict[str, "LabelDiver"]
23
+ _labels: Dict[str, str]
24
+
25
+ def __init__(self, labels: Dict[str, str], value: str = ""):
26
+ self._labels = labels.copy()
27
+ self._value = value
28
+
29
+ divers: Dict[str, Dict[str, str]] = {}
30
+ values: Dict[str, str] = {}
31
+
32
+ for key, value in labels.items():
33
+ head, _, tail = key.partition(".")
34
+ if tail:
35
+ if head not in divers:
36
+ divers[head] = {}
37
+
38
+ divers[head][tail] = labels[key]
39
+ else:
40
+ values[head] = value
41
+
42
+ # start with keys that had sub-divers...
43
+ self._divers: Dict[str, LabelDiver] = {
44
+ k: LabelDiver(v, value=values.pop(k, "")) for k, v in divers.items()
45
+ }
46
+ # ...then mix in any remaining keys that _only_ had values
47
+ self._divers.update(**{k: LabelDiver({}, value=v) for k, v in values.items()})
48
+
49
+ def __str__(self) -> str:
50
+ return self._value or ""
51
+
52
+ def __repr__(self) -> str:
53
+ return f"LabelDiver(divers={self._divers!r}, value={self._value!r})"
54
+
55
+ def __len__(self) -> int:
56
+ return len(self._labels)
57
+
58
+ def __iter__(self) -> Iterator[Tuple[str, str]]:
59
+ return iter(self._labels.items())
60
+
61
+ def __getitem__(self, key: str) -> str:
62
+ return self._labels[key]
63
+
64
+ def __getattr__(self, name: str) -> "LabelDiver":
65
+ if name.startswith("_"):
66
+ raise AttributeError
67
+
68
+ try:
69
+ return self._divers[name]
70
+ except KeyError:
71
+ raise AttributeError
72
+
73
+
74
+ class Labelled(PrefectBaseModel, extra="ignore"):
75
+ """An object defined by string labels and values"""
76
+
77
+ __root__: Dict[str, str]
78
+
79
+ def keys(self) -> Iterable[str]:
80
+ return self.__root__.keys()
81
+
82
+ def items(self) -> Iterable[Tuple[str, str]]:
83
+ return self.__root__.items()
84
+
85
+ def __getitem__(self, label: str) -> str:
86
+ return self.__root__[label]
87
+
88
+ def __setitem__(self, label: str, value: str) -> str:
89
+ self.__root__[label] = value
90
+ return value
91
+
92
+ def __contains__(self, key: str) -> bool:
93
+ return key in self.__root__
94
+
95
+ def get(self, label: str, default: Optional[str] = None) -> Optional[str]:
96
+ return self.__root__.get(label, default)
97
+
98
+ def as_label_value_array(self) -> List[Dict[str, str]]:
99
+ return [{"label": label, "value": value} for label, value in self.items()]
100
+
101
+ @property
102
+ def labels(self) -> LabelDiver:
103
+ return LabelDiver(self.__root__)
104
+
105
+ def has_all_labels(self, labels: Dict[str, str]) -> bool:
106
+ return all(self.__root__.get(label) == value for label, value in labels.items())
@@ -7,7 +7,7 @@ import pendulum
7
7
  from prefect._internal.schemas.fields import DateTimeTZ
8
8
 
9
9
  from .clients import AssertingEventsClient, PrefectCloudEventsClient
10
- from .schemas import Event, RelatedResource
10
+ from .schemas.events import Event, RelatedResource
11
11
  from .worker import EventsWorker, emit_events_to_cloud
12
12
 
13
13
  TIGHT_TIMING = timedelta(minutes=5)
@@ -40,7 +40,7 @@ def emit_event(
40
40
 
41
41
  Returns:
42
42
  The event that was emitted if worker is using a client that emit
43
- events, otherwise None.
43
+ events, otherwise None
44
44
  """
45
45
  if not emit_events_to_cloud():
46
46
  return None
prefect/events/worker.py CHANGED
@@ -11,7 +11,7 @@ from prefect.utilities.context import temporary_context
11
11
 
12
12
  from .clients import EventsClient, NullEventsClient, PrefectCloudEventsClient
13
13
  from .related import related_resources_from_run_context
14
- from .schemas import Event
14
+ from .schemas.events import Event
15
15
 
16
16
 
17
17
  def emit_events_to_cloud() -> bool:
prefect/filesystems.py CHANGED
@@ -18,8 +18,12 @@ if HAS_PYDANTIC_V2:
18
18
  else:
19
19
  from pydantic import Field, SecretStr, validator
20
20
 
21
+ from prefect._internal.schemas.validators import (
22
+ stringify_path,
23
+ validate_basepath,
24
+ validate_github_access_token,
25
+ )
21
26
  from prefect.blocks.core import Block
22
- from prefect.exceptions import InvalidRepositoryURLError
23
27
  from prefect.utilities.asyncutils import run_sync_in_worker_thread, sync_compatible
24
28
  from prefect.utilities.compat import copytree
25
29
  from prefect.utilities.filesystem import filter_files
@@ -97,9 +101,7 @@ class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
97
101
 
98
102
  @validator("basepath", pre=True)
99
103
  def cast_pathlib(cls, value):
100
- if isinstance(value, Path):
101
- return str(value)
102
- return value
104
+ return stringify_path(value)
103
105
 
104
106
  def _resolve_path(self, path: str) -> Path:
105
107
  # Only resolve the base path at runtime, default to the current directory
@@ -280,23 +282,7 @@ class RemoteFileSystem(WritableFileSystem, WritableDeploymentStorage):
280
282
 
281
283
  @validator("basepath")
282
284
  def check_basepath(cls, value):
283
- scheme, netloc, _, _, _ = urllib.parse.urlsplit(value)
284
-
285
- if not scheme:
286
- raise ValueError(f"Base path must start with a scheme. Got {value!r}.")
287
-
288
- if not netloc:
289
- raise ValueError(
290
- f"Base path must include a location after the scheme. Got {value!r}."
291
- )
292
-
293
- if scheme == "file":
294
- raise ValueError(
295
- "Base path scheme cannot be 'file'. Use `LocalFileSystem` instead for"
296
- " local file access."
297
- )
298
-
299
- return value
285
+ return validate_basepath(value)
300
286
 
301
287
  def _resolve_path(self, path: str) -> str:
302
288
  base_scheme, base_netloc, base_urlpath, _, _ = urllib.parse.urlsplit(
@@ -945,22 +931,7 @@ class GitHub(ReadableDeploymentStorage):
945
931
 
946
932
  @validator("access_token")
947
933
  def _ensure_credentials_go_with_https(cls, v: str, values: dict) -> str:
948
- """Ensure that credentials are not provided with 'SSH' formatted GitHub URLs.
949
-
950
- Note: validates `access_token` specifically so that it only fires when
951
- private repositories are used.
952
- """
953
- if v is not None:
954
- if urllib.parse.urlparse(values["repository"]).scheme != "https":
955
- raise InvalidRepositoryURLError(
956
- "Crendentials can only be used with GitHub repositories "
957
- "using the 'HTTPS' format. You must either remove the "
958
- "credential if you wish to use the 'SSH' format and are not "
959
- "using a private repository, or you must change the repository "
960
- "URL to the 'HTTPS' format. "
961
- )
962
-
963
- return v
934
+ return validate_github_access_token(v, values)
964
935
 
965
936
  def _create_repo_url(self) -> str:
966
937
  """Format the URL provided to the `git clone` command.
prefect/flows.py CHANGED
@@ -75,7 +75,7 @@ from prefect.client.schemas.objects import Flow as FlowSchema
75
75
  from prefect.client.schemas.objects import FlowRun, MinimalDeploymentSchedule
76
76
  from prefect.client.schemas.schedules import SCHEDULE_TYPES
77
77
  from prefect.context import PrefectObjectRegistry, registry_from_script
78
- from prefect.events.schemas import DeploymentTrigger
78
+ from prefect.events import DeploymentTriggerTypes
79
79
  from prefect.exceptions import (
80
80
  MissingFlowError,
81
81
  ObjectNotFound,
@@ -618,7 +618,7 @@ class Flow(Generic[P, R]):
618
618
  schedule: Optional[SCHEDULE_TYPES] = None,
619
619
  is_schedule_active: Optional[bool] = None,
620
620
  parameters: Optional[dict] = None,
621
- triggers: Optional[List[DeploymentTrigger]] = None,
621
+ triggers: Optional[List[DeploymentTriggerTypes]] = None,
622
622
  description: Optional[str] = None,
623
623
  tags: Optional[List[str]] = None,
624
624
  version: Optional[str] = None,
@@ -748,7 +748,7 @@ class Flow(Generic[P, R]):
748
748
  schedules: Optional[List["FlexibleScheduleList"]] = None,
749
749
  schedule: Optional[SCHEDULE_TYPES] = None,
750
750
  is_schedule_active: Optional[bool] = None,
751
- triggers: Optional[List[DeploymentTrigger]] = None,
751
+ triggers: Optional[List[DeploymentTriggerTypes]] = None,
752
752
  parameters: Optional[dict] = None,
753
753
  description: Optional[str] = None,
754
754
  tags: Optional[List[str]] = None,
@@ -962,7 +962,7 @@ class Flow(Generic[P, R]):
962
962
  schedules: Optional[List[MinimalDeploymentSchedule]] = None,
963
963
  schedule: Optional[SCHEDULE_TYPES] = None,
964
964
  is_schedule_active: Optional[bool] = None,
965
- triggers: Optional[List[DeploymentTrigger]] = None,
965
+ triggers: Optional[List[DeploymentTriggerTypes]] = None,
966
966
  parameters: Optional[dict] = None,
967
967
  description: Optional[str] = None,
968
968
  tags: Optional[List[str]] = None,
@@ -8,7 +8,6 @@ For upgrade instructions, see https://docs.prefect.io/latest/guides/upgrade-guid
8
8
  """
9
9
  import copy
10
10
  import enum
11
- import json
12
11
  import math
13
12
  import os
14
13
  import shlex
@@ -23,6 +22,13 @@ from prefect._internal.compatibility.deprecated import (
23
22
  deprecated_class,
24
23
  )
25
24
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
25
+ from prefect._internal.schemas.validators import (
26
+ cast_k8s_job_customizations,
27
+ set_default_image,
28
+ set_default_namespace,
29
+ validate_k8s_job_compatible_values,
30
+ validate_k8s_job_required_components,
31
+ )
26
32
 
27
33
  if HAS_PYDANTIC_V2:
28
34
  from pydantic.v1 import Field, root_validator, validator
@@ -35,7 +41,6 @@ from prefect.blocks.kubernetes import KubernetesClusterConfig
35
41
  from prefect.exceptions import InfrastructureNotAvailable, InfrastructureNotFound
36
42
  from prefect.infrastructure.base import Infrastructure, InfrastructureResult
37
43
  from prefect.utilities.asyncutils import run_sync_in_worker_thread, sync_compatible
38
- from prefect.utilities.dockerutils import get_prefect_image_name
39
44
  from prefect.utilities.hashing import stable_hash
40
45
  from prefect.utilities.importtools import lazy_import
41
46
  from prefect.utilities.pydantic import JsonPatch
@@ -188,74 +193,25 @@ class KubernetesJob(Infrastructure):
188
193
 
189
194
  @validator("job")
190
195
  def ensure_job_includes_all_required_components(cls, value: KubernetesManifest):
191
- patch = JsonPatch.from_diff(value, cls.base_job_manifest())
192
- missing_paths = sorted([op["path"] for op in patch if op["op"] == "add"])
193
- if missing_paths:
194
- raise ValueError(
195
- "Job is missing required attributes at the following paths: "
196
- f"{', '.join(missing_paths)}"
197
- )
198
- return value
196
+ return validate_k8s_job_required_components(cls, value)
199
197
 
200
198
  @validator("job")
201
199
  def ensure_job_has_compatible_values(cls, value: KubernetesManifest):
202
- patch = JsonPatch.from_diff(value, cls.base_job_manifest())
203
- incompatible = sorted(
204
- [
205
- f"{op['path']} must have value {op['value']!r}"
206
- for op in patch
207
- if op["op"] == "replace"
208
- ]
209
- )
210
- if incompatible:
211
- raise ValueError(
212
- "Job has incompatible values for the following attributes: "
213
- f"{', '.join(incompatible)}"
214
- )
215
- return value
200
+ return validate_k8s_job_compatible_values(cls, value)
216
201
 
217
202
  @validator("customizations", pre=True)
218
203
  def cast_customizations_to_a_json_patch(
219
204
  cls, value: Union[List[Dict], JsonPatch, str]
220
205
  ) -> JsonPatch:
221
- if isinstance(value, list):
222
- return JsonPatch(value)
223
- elif isinstance(value, str):
224
- try:
225
- return JsonPatch(json.loads(value))
226
- except json.JSONDecodeError as exc:
227
- raise ValueError(
228
- f"Unable to parse customizations as JSON: {value}. Please make sure"
229
- " that the provided value is a valid JSON string."
230
- ) from exc
231
- return value
206
+ return cast_k8s_job_customizations(cls, value)
232
207
 
233
208
  @root_validator
234
209
  def default_namespace(cls, values):
235
- job = values.get("job")
236
-
237
- namespace = values.get("namespace")
238
- job_namespace = job["metadata"].get("namespace") if job else None
239
-
240
- if not namespace and not job_namespace:
241
- values["namespace"] = "default"
242
-
243
- return values
210
+ return set_default_namespace(values)
244
211
 
245
212
  @root_validator
246
213
  def default_image(cls, values):
247
- job = values.get("job")
248
- image = values.get("image")
249
- job_image = (
250
- job["spec"]["template"]["spec"]["containers"][0].get("image")
251
- if job
252
- else None
253
- )
254
-
255
- if not image and not job_image:
256
- values["image"] = get_prefect_image_name()
257
-
258
- return values
214
+ return set_default_image(values)
259
215
 
260
216
  # Support serialization of the 'JsonPatch' type
261
217
  class Config:
@@ -9,6 +9,7 @@ import rich.console
9
9
 
10
10
  _provisioners = {
11
11
  "cloud-run:push": CloudRunPushProvisioner,
12
+ "cloud-run-v2:push": CloudRunPushProvisioner,
12
13
  "azure-container-instance:push": ContainerInstancePushProvisioner,
13
14
  "ecs:push": ElasticContainerServicePushProvisioner,
14
15
  "modal:push": ModalPushProvisioner,
@@ -0,0 +1,4 @@
1
+ from .main import BaseModel, PrefectBaseModel
2
+
3
+
4
+ __all__ = ["BaseModel", "PrefectBaseModel"]
@@ -0,0 +1,15 @@
1
+ import typing
2
+
3
+ from prefect._internal.pydantic._compat import BaseModel
4
+
5
+
6
+ class PrefectBaseModel(BaseModel):
7
+ def _reset_fields(self) -> typing.Set[str]:
8
+ """
9
+ A set of field names that are reset when the PrefectBaseModel is copied.
10
+ These fields are also disregarded for equality comparisons.
11
+ """
12
+ return set()
13
+
14
+
15
+ __all__ = ["BaseModel", "PrefectBaseModel"]
prefect/runner/runner.py CHANGED
@@ -80,7 +80,7 @@ from prefect.deployments.runner import (
80
80
  )
81
81
  from prefect.deployments.schedules import FlexibleScheduleList
82
82
  from prefect.engine import propose_state
83
- from prefect.events.schemas import DeploymentTrigger
83
+ from prefect.events import DeploymentTriggerTypes
84
84
  from prefect.exceptions import (
85
85
  Abort,
86
86
  )
@@ -232,7 +232,7 @@ class Runner:
232
232
  schedule: Optional[SCHEDULE_TYPES] = None,
233
233
  is_schedule_active: Optional[bool] = None,
234
234
  parameters: Optional[dict] = None,
235
- triggers: Optional[List[DeploymentTrigger]] = None,
235
+ triggers: Optional[List[DeploymentTriggerTypes]] = None,
236
236
  description: Optional[str] = None,
237
237
  tags: Optional[List[str]] = None,
238
238
  version: Optional[str] = None,