prefect-client 2.16.6__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.
- prefect/_internal/pydantic/__init__.py +21 -1
- prefect/_internal/pydantic/_base_model.py +16 -0
- prefect/_internal/pydantic/_compat.py +325 -74
- prefect/_internal/pydantic/_flags.py +15 -0
- prefect/_internal/schemas/validators.py +582 -9
- prefect/artifacts.py +179 -70
- prefect/client/orchestration.py +1 -1
- prefect/client/schemas/actions.py +2 -2
- prefect/client/schemas/objects.py +13 -24
- prefect/client/schemas/schedules.py +18 -80
- prefect/deployments/deployments.py +22 -86
- prefect/deployments/runner.py +8 -11
- prefect/events/__init__.py +40 -1
- prefect/events/clients.py +17 -20
- prefect/events/filters.py +5 -6
- prefect/events/related.py +1 -1
- prefect/events/schemas/__init__.py +5 -0
- prefect/events/schemas/automations.py +303 -0
- prefect/events/{schemas.py → schemas/deployment_triggers.py} +146 -270
- prefect/events/schemas/events.py +285 -0
- prefect/events/schemas/labelling.py +106 -0
- prefect/events/utilities.py +2 -2
- prefect/events/worker.py +1 -1
- prefect/filesystems.py +8 -37
- prefect/flows.py +4 -4
- prefect/infrastructure/kubernetes.py +12 -56
- prefect/infrastructure/provisioners/__init__.py +1 -0
- prefect/pydantic/__init__.py +4 -0
- prefect/pydantic/main.py +15 -0
- prefect/runner/runner.py +2 -2
- prefect/runner/server.py +1 -1
- prefect/serializers.py +13 -61
- prefect/settings.py +34 -12
- prefect/task_server.py +21 -7
- prefect/utilities/asyncutils.py +1 -1
- prefect/utilities/context.py +33 -1
- prefect/workers/base.py +1 -2
- prefect/workers/block.py +3 -7
- {prefect_client-2.16.6.dist-info → prefect_client-2.16.7.dist-info}/METADATA +2 -2
- {prefect_client-2.16.6.dist-info → prefect_client-2.16.7.dist-info}/RECORD +43 -36
- prefect/utilities/validation.py +0 -63
- {prefect_client-2.16.6.dist-info → prefect_client-2.16.7.dist-info}/LICENSE +0 -0
- {prefect_client-2.16.6.dist-info → prefect_client-2.16.7.dist-info}/WHEEL +0 -0
- {prefect_client-2.16.6.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())
|
prefect/events/utilities.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
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[
|
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[
|
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[
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
prefect/pydantic/main.py
ADDED
@@ -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
|
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[
|
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,
|