prefect-client 2.17.1__py3-none-any.whl → 2.18.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/_internal/compatibility/deprecated.py +2 -0
- prefect/_internal/pydantic/_compat.py +1 -0
- prefect/_internal/pydantic/utilities/field_validator.py +25 -10
- prefect/_internal/pydantic/utilities/model_dump.py +1 -1
- prefect/_internal/pydantic/utilities/model_validate.py +1 -1
- prefect/_internal/pydantic/utilities/model_validator.py +11 -3
- prefect/_internal/schemas/validators.py +0 -6
- prefect/_version.py +97 -38
- prefect/blocks/abstract.py +34 -1
- prefect/blocks/notifications.py +14 -5
- prefect/client/base.py +10 -5
- prefect/client/orchestration.py +125 -66
- prefect/client/schemas/actions.py +4 -3
- prefect/client/schemas/objects.py +6 -5
- prefect/client/schemas/schedules.py +2 -6
- prefect/deployments/__init__.py +0 -2
- prefect/deployments/base.py +2 -144
- prefect/deployments/deployments.py +2 -2
- prefect/deployments/runner.py +2 -2
- prefect/deployments/steps/core.py +3 -3
- prefect/deprecated/packaging/serializers.py +5 -4
- prefect/events/__init__.py +45 -0
- prefect/events/actions.py +250 -19
- prefect/events/cli/__init__.py +0 -0
- prefect/events/cli/automations.py +163 -0
- prefect/events/clients.py +133 -7
- prefect/events/schemas/automations.py +76 -3
- prefect/events/schemas/deployment_triggers.py +17 -59
- prefect/events/utilities.py +2 -0
- prefect/events/worker.py +12 -2
- prefect/exceptions.py +1 -1
- prefect/logging/__init__.py +2 -2
- prefect/logging/loggers.py +64 -1
- prefect/results.py +29 -10
- prefect/serializers.py +62 -31
- prefect/settings.py +6 -10
- prefect/types/__init__.py +90 -0
- prefect/utilities/pydantic.py +34 -15
- prefect/utilities/schema_tools/hydration.py +88 -19
- prefect/variables.py +4 -4
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/METADATA +1 -1
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/RECORD +45 -42
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/WHEEL +0 -0
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/top_level.txt +0 -0
@@ -344,6 +344,8 @@ class DeprecatedInfraOverridesField(BaseModel):
|
|
344
344
|
exclude.add("job_variables")
|
345
345
|
elif exclude_type is dict:
|
346
346
|
exclude["job_variables"] = True
|
347
|
+
else:
|
348
|
+
exclude = {"job_variables"}
|
347
349
|
kwargs["exclude"] = exclude
|
348
350
|
|
349
351
|
return super().dict(**kwargs)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
Functions within this module check for Pydantic V2 compatibility and provide mechanisms for copying,
|
3
3
|
dumping, and validating models in a way that is agnostic to the underlying Pydantic version.
|
4
4
|
"""
|
5
|
+
|
5
6
|
import typing
|
6
7
|
|
7
8
|
from ._base_model import BaseModel as PydanticBaseModel
|
@@ -4,7 +4,7 @@ Conditional decorator for fields depending on Pydantic version.
|
|
4
4
|
|
5
5
|
import functools
|
6
6
|
from inspect import signature
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal,
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, TypeVar, Union
|
8
8
|
|
9
9
|
from typing_extensions import TypeAlias
|
10
10
|
|
@@ -21,11 +21,8 @@ def field_validator(
|
|
21
21
|
field: str,
|
22
22
|
/,
|
23
23
|
*fields: str,
|
24
|
-
mode: FieldValidatorModes = "after",
|
24
|
+
mode: FieldValidatorModes = "after",
|
25
25
|
check_fields: Union[bool, None] = None,
|
26
|
-
pre: bool = False, # v1 only
|
27
|
-
allow_reuse: Optional[bool] = None,
|
28
|
-
always: bool = False, # v1 only
|
29
26
|
) -> Callable[[Any], Any]:
|
30
27
|
"""Usage docs: https://docs.pydantic.dev/2.7/concepts/validators/#field-validators
|
31
28
|
Returns a decorator that conditionally applies Pydantic's `field_validator` or `validator`,
|
@@ -37,6 +34,26 @@ def field_validator(
|
|
37
34
|
|
38
35
|
Decorate methods on the class indicating that they should be used to validate fields.
|
39
36
|
|
37
|
+
!!! note Replacing Pydantic V1 `pre=True` kwarg:
|
38
|
+
To replace a @validator that uses Pydantic V1's `pre` parameter, e.g. `@validator('a', pre=True)`,
|
39
|
+
you can use `mode='before'`, e.g. @field_validator('a', mode='before').
|
40
|
+
|
41
|
+
If a user has Pydantic V1 installed, `mode` will map to the `pre` parameter of `validator` if the value is `before`.
|
42
|
+
|
43
|
+
!!! note Replacing Pydantic V1 `always=True` kwarg:
|
44
|
+
To replace a @validator that uses Pydantic V1's `always` parameter, e.g. `@validator('a', always=True)`,
|
45
|
+
you can use the @model_validator (not the @field_validator) with the `mode='before'` parameter, (and also add a check that the field is not None, if necessary).
|
46
|
+
|
47
|
+
Read more discussion on that here: https://github.com/pydantic/pydantic/discussions/6337
|
48
|
+
|
49
|
+
!!! note Replacing Pydantic V1 `allow_reuse=True` kwarg:
|
50
|
+
To replace a @validator that uses Pydantic V1's `allow_reuse=True` parameter, e.g. `@validator('a', allow_reuse=True)`,
|
51
|
+
you can simply remove the `allow_reuse` parameter when replacing the decorator, e.g. `@field_validator('a')`. This is because
|
52
|
+
Pydantic V2 by default allows reuse of the decorated function, rendering the kwarg necessary), while Pydantic V1 required explicit
|
53
|
+
declaration of `allow_reuse=True`.
|
54
|
+
|
55
|
+
https://docs.pydantic.dev/2.0/migration/#the-allow_reuse-keyword-argument-is-no-longer-necessary
|
56
|
+
|
40
57
|
Example usage:
|
41
58
|
```py
|
42
59
|
from typing import Any
|
@@ -120,14 +137,12 @@ def field_validator(
|
|
120
137
|
|
121
138
|
return validate_func(cls, v, **filtered_kwargs)
|
122
139
|
|
123
|
-
#
|
124
|
-
|
125
|
-
|
140
|
+
# Map Pydantic V2's `mode` to Pydantic V1's `pre` parameter for use in `@validator`
|
141
|
+
pre: bool = mode == "before"
|
142
|
+
|
126
143
|
validator_kwargs: Dict[str, Any] = {
|
127
144
|
"pre": pre,
|
128
|
-
"always": always,
|
129
145
|
"check_fields": check_fields if check_fields is not None else True,
|
130
|
-
"allow_reuse": allow_reuse if allow_reuse is not None else False,
|
131
146
|
}
|
132
147
|
|
133
148
|
return validator(field, *fields, **validator_kwargs)(wrapper) # type: ignore
|
@@ -13,8 +13,6 @@ def model_validator(
|
|
13
13
|
_func: Optional[Callable] = None,
|
14
14
|
*,
|
15
15
|
mode: Literal["wrap", "before", "after"] = "before", # v2 only
|
16
|
-
pre: bool = False, # v1 only
|
17
|
-
skip_on_failure: bool = False, # v1 only
|
18
16
|
) -> Any:
|
19
17
|
"""Usage docs: https://docs.pydantic.dev/2.6/concepts/validators/#model-validators
|
20
18
|
|
@@ -33,6 +31,15 @@ def model_validator(
|
|
33
31
|
validation logic before, after, or wrapping the original method call, depending on the
|
34
32
|
`mode` parameter.
|
35
33
|
|
34
|
+
!!! note Replacing Pydantic V1 `pre=True` kwarg:
|
35
|
+
To replace a @root_validator that uses Pydantic V1's `pre=True` parameter, e.g. `@root_validator('a', pre=True)`,
|
36
|
+
you can use the @model_validator with the `mode='before'` parameter, (and also add a check that the field is not None, if necessary).
|
37
|
+
This will map to the `pre` parameter of `root_validator` in Pydantic V1, if the value is `True`.
|
38
|
+
|
39
|
+
!!! note Replacing Pydantic V1 `skip_on_failure=True` kwarg:
|
40
|
+
To replace a @root_validator that uses Pydantic V1's `skip_on_failure=True` parameter, e.g. `@root_validator('a', skip_on_failure=True)`,
|
41
|
+
we'll simply remove it. Pydantic V2 does not have an equivalent parameter, and we use it in only 3 places in Prefect, none of which are critical.
|
42
|
+
|
36
43
|
Args:
|
37
44
|
_func: The function to be decorated. If None, the decorator is applied with parameters.
|
38
45
|
mode: Specifies when the validation should occur. 'before' or 'after' are for v1 compatibility,
|
@@ -69,9 +76,10 @@ def model_validator(
|
|
69
76
|
) -> Any:
|
70
77
|
return validate_func(cls, v)
|
71
78
|
|
79
|
+
pre: bool = mode == "before"
|
80
|
+
|
72
81
|
return root_validator(
|
73
82
|
pre=pre,
|
74
|
-
skip_on_failure=skip_on_failure,
|
75
83
|
)(wrapper) # type: ignore
|
76
84
|
|
77
85
|
if _func is None:
|
@@ -372,12 +372,6 @@ def reconcile_paused_deployment(values):
|
|
372
372
|
return values
|
373
373
|
|
374
374
|
|
375
|
-
def interval_schedule_must_be_positive(v: datetime.timedelta) -> datetime.timedelta:
|
376
|
-
if v.total_seconds() <= 0:
|
377
|
-
raise ValueError("The interval must be positive")
|
378
|
-
return v
|
379
|
-
|
380
|
-
|
381
375
|
def default_anchor_date(v: DateTimeTZ) -> DateTimeTZ:
|
382
376
|
if v is None:
|
383
377
|
return pendulum.now("UTC")
|
prefect/_version.py
CHANGED
@@ -4,19 +4,22 @@
|
|
4
4
|
# directories (produced by setup.py build) will contain a much shorter file
|
5
5
|
# that just contains the computed version number.
|
6
6
|
|
7
|
-
# This file is released into the public domain.
|
8
|
-
# versioneer-0.
|
7
|
+
# This file is released into the public domain.
|
8
|
+
# Generated by versioneer-0.29
|
9
|
+
# https://github.com/python-versioneer/python-versioneer
|
9
10
|
|
10
11
|
"""Git implementation of _version.py."""
|
11
12
|
|
12
13
|
import errno
|
14
|
+
import functools
|
13
15
|
import os
|
14
16
|
import re
|
15
17
|
import subprocess
|
16
18
|
import sys
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
17
20
|
|
18
21
|
|
19
|
-
def get_keywords():
|
22
|
+
def get_keywords() -> Dict[str, str]:
|
20
23
|
"""Get the keywords needed to look up the version information."""
|
21
24
|
# these strings will be replaced by git during git-archive.
|
22
25
|
# setup.py/versioneer.py will grep for the variable names, so they must
|
@@ -29,11 +32,18 @@ def get_keywords():
|
|
29
32
|
return keywords
|
30
33
|
|
31
34
|
|
32
|
-
class VersioneerConfig:
|
35
|
+
class VersioneerConfig:
|
33
36
|
"""Container for Versioneer configuration parameters."""
|
34
37
|
|
38
|
+
VCS: str
|
39
|
+
style: str
|
40
|
+
tag_prefix: str
|
41
|
+
parentdir_prefix: str
|
42
|
+
versionfile_source: str
|
43
|
+
verbose: bool
|
35
44
|
|
36
|
-
|
45
|
+
|
46
|
+
def get_config() -> VersioneerConfig:
|
37
47
|
"""Create, populate and return the VersioneerConfig() object."""
|
38
48
|
# these strings are filled in when 'setup.py versioneer' creates
|
39
49
|
# _version.py
|
@@ -51,14 +61,14 @@ class NotThisMethod(Exception):
|
|
51
61
|
"""Exception raised if a method is not valid for the current scenario."""
|
52
62
|
|
53
63
|
|
54
|
-
LONG_VERSION_PY = {}
|
55
|
-
HANDLERS = {}
|
64
|
+
LONG_VERSION_PY: Dict[str, str] = {}
|
65
|
+
HANDLERS: Dict[str, Dict[str, Callable]] = {}
|
56
66
|
|
57
67
|
|
58
|
-
def register_vcs_handler(vcs, method): # decorator
|
68
|
+
def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator
|
59
69
|
"""Create decorator to mark a method as the handler of a VCS."""
|
60
70
|
|
61
|
-
def decorate(f):
|
71
|
+
def decorate(f: Callable) -> Callable:
|
62
72
|
"""Store f in HANDLERS[vcs][method]."""
|
63
73
|
if vcs not in HANDLERS:
|
64
74
|
HANDLERS[vcs] = {}
|
@@ -68,11 +78,25 @@ def register_vcs_handler(vcs, method): # decorator
|
|
68
78
|
return decorate
|
69
79
|
|
70
80
|
|
71
|
-
|
72
|
-
|
81
|
+
def run_command(
|
82
|
+
commands: List[str],
|
83
|
+
args: List[str],
|
84
|
+
cwd: Optional[str] = None,
|
85
|
+
verbose: bool = False,
|
86
|
+
hide_stderr: bool = False,
|
87
|
+
env: Optional[Dict[str, str]] = None,
|
88
|
+
) -> Tuple[Optional[str], Optional[int]]:
|
73
89
|
"""Call the given command(s)."""
|
74
90
|
assert isinstance(commands, list)
|
75
91
|
process = None
|
92
|
+
|
93
|
+
popen_kwargs: Dict[str, Any] = {}
|
94
|
+
if sys.platform == "win32":
|
95
|
+
# This hides the console window if pythonw.exe is used
|
96
|
+
startupinfo = subprocess.STARTUPINFO()
|
97
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
98
|
+
popen_kwargs["startupinfo"] = startupinfo
|
99
|
+
|
76
100
|
for command in commands:
|
77
101
|
try:
|
78
102
|
dispcmd = str([command] + args)
|
@@ -83,10 +107,10 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=
|
|
83
107
|
env=env,
|
84
108
|
stdout=subprocess.PIPE,
|
85
109
|
stderr=(subprocess.PIPE if hide_stderr else None),
|
110
|
+
**popen_kwargs,
|
86
111
|
)
|
87
112
|
break
|
88
|
-
except
|
89
|
-
e = sys.exc_info()[1]
|
113
|
+
except OSError as e:
|
90
114
|
if e.errno == errno.ENOENT:
|
91
115
|
continue
|
92
116
|
if verbose:
|
@@ -106,7 +130,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=
|
|
106
130
|
return stdout, process.returncode
|
107
131
|
|
108
132
|
|
109
|
-
def versions_from_parentdir(
|
133
|
+
def versions_from_parentdir(
|
134
|
+
parentdir_prefix: str,
|
135
|
+
root: str,
|
136
|
+
verbose: bool,
|
137
|
+
) -> Dict[str, Any]:
|
110
138
|
"""Try to determine the version from the parent directory name.
|
111
139
|
|
112
140
|
Source tarballs conventionally unpack into a directory that includes both
|
@@ -137,13 +165,13 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
|
|
137
165
|
|
138
166
|
|
139
167
|
@register_vcs_handler("git", "get_keywords")
|
140
|
-
def git_get_keywords(versionfile_abs):
|
168
|
+
def git_get_keywords(versionfile_abs: str) -> Dict[str, str]:
|
141
169
|
"""Extract version information from the given file."""
|
142
170
|
# the code embedded in _version.py can just fetch the value of these
|
143
171
|
# keywords. When used from setup.py, we don't want to import _version.py,
|
144
172
|
# so we do it with a regexp instead. This function is not used from
|
145
173
|
# _version.py.
|
146
|
-
keywords = {}
|
174
|
+
keywords: Dict[str, str] = {}
|
147
175
|
try:
|
148
176
|
with open(versionfile_abs, "r") as fobj:
|
149
177
|
for line in fobj:
|
@@ -159,13 +187,17 @@ def git_get_keywords(versionfile_abs):
|
|
159
187
|
mo = re.search(r'=\s*"(.*)"', line)
|
160
188
|
if mo:
|
161
189
|
keywords["date"] = mo.group(1)
|
162
|
-
except
|
190
|
+
except OSError:
|
163
191
|
pass
|
164
192
|
return keywords
|
165
193
|
|
166
194
|
|
167
195
|
@register_vcs_handler("git", "keywords")
|
168
|
-
def git_versions_from_keywords(
|
196
|
+
def git_versions_from_keywords(
|
197
|
+
keywords: Dict[str, str],
|
198
|
+
tag_prefix: str,
|
199
|
+
verbose: bool,
|
200
|
+
) -> Dict[str, Any]:
|
169
201
|
"""Get version information from git keywords."""
|
170
202
|
if "refnames" not in keywords:
|
171
203
|
raise NotThisMethod("Short version file found")
|
@@ -236,7 +268,9 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
|
236
268
|
|
237
269
|
|
238
270
|
@register_vcs_handler("git", "pieces_from_vcs")
|
239
|
-
def git_pieces_from_vcs(
|
271
|
+
def git_pieces_from_vcs(
|
272
|
+
tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command
|
273
|
+
) -> Dict[str, Any]:
|
240
274
|
"""Get version from 'git describe' in the root of the source tree.
|
241
275
|
|
242
276
|
This only gets called if the git-archive 'subst' keywords were *not*
|
@@ -247,7 +281,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
|
|
247
281
|
if sys.platform == "win32":
|
248
282
|
GITS = ["git.cmd", "git.exe"]
|
249
283
|
|
250
|
-
|
284
|
+
# GIT_DIR can interfere with correct operation of Versioneer.
|
285
|
+
# It may be intended to be passed to the Versioneer-versioned project,
|
286
|
+
# but that should not change where we get our version from.
|
287
|
+
env = os.environ.copy()
|
288
|
+
env.pop("GIT_DIR", None)
|
289
|
+
runner = functools.partial(runner, env=env)
|
290
|
+
|
291
|
+
_, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose)
|
251
292
|
if rc != 0:
|
252
293
|
if verbose:
|
253
294
|
print("Directory %s not under git control" % root)
|
@@ -264,7 +305,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
|
|
264
305
|
"--always",
|
265
306
|
"--long",
|
266
307
|
"--match",
|
267
|
-
"
|
308
|
+
f"{tag_prefix}[[:digit:]]*",
|
268
309
|
],
|
269
310
|
cwd=root,
|
270
311
|
)
|
@@ -277,7 +318,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
|
|
277
318
|
raise NotThisMethod("'git rev-parse' failed")
|
278
319
|
full_out = full_out.strip()
|
279
320
|
|
280
|
-
pieces = {}
|
321
|
+
pieces: Dict[str, Any] = {}
|
281
322
|
pieces["long"] = full_out
|
282
323
|
pieces["short"] = full_out[:7] # maybe improved later
|
283
324
|
pieces["error"] = None
|
@@ -356,8 +397,8 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
|
|
356
397
|
else:
|
357
398
|
# HEX: no tags
|
358
399
|
pieces["closest-tag"] = None
|
359
|
-
|
360
|
-
pieces["distance"] =
|
400
|
+
out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root)
|
401
|
+
pieces["distance"] = len(out.split()) # total number of commits
|
361
402
|
|
362
403
|
# commit date: see ISO-8601 comment in git_versions_from_keywords()
|
363
404
|
date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
|
@@ -369,14 +410,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
|
|
369
410
|
return pieces
|
370
411
|
|
371
412
|
|
372
|
-
def plus_or_dot(pieces):
|
413
|
+
def plus_or_dot(pieces: Dict[str, Any]) -> str:
|
373
414
|
"""Return a + if we don't already have one, else return a ."""
|
374
415
|
if "+" in pieces.get("closest-tag", ""):
|
375
416
|
return "."
|
376
417
|
return "+"
|
377
418
|
|
378
419
|
|
379
|
-
def render_pep440(pieces):
|
420
|
+
def render_pep440(pieces: Dict[str, Any]) -> str:
|
380
421
|
"""Build up version string, with post-release "local version identifier".
|
381
422
|
|
382
423
|
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
|
@@ -400,7 +441,7 @@ def render_pep440(pieces):
|
|
400
441
|
return rendered
|
401
442
|
|
402
443
|
|
403
|
-
def render_pep440_branch(pieces):
|
444
|
+
def render_pep440_branch(pieces: Dict[str, Any]) -> str:
|
404
445
|
"""TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
|
405
446
|
|
406
447
|
The ".dev0" means not master branch. Note that .dev0 sorts backwards
|
@@ -429,23 +470,41 @@ def render_pep440_branch(pieces):
|
|
429
470
|
return rendered
|
430
471
|
|
431
472
|
|
432
|
-
def
|
433
|
-
"""
|
473
|
+
def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]:
|
474
|
+
"""Split pep440 version string at the post-release segment.
|
475
|
+
|
476
|
+
Returns the release segments before the post-release and the
|
477
|
+
post-release version number (or -1 if no post-release segment is present).
|
478
|
+
"""
|
479
|
+
vc = str.split(ver, ".post")
|
480
|
+
return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
|
481
|
+
|
482
|
+
|
483
|
+
def render_pep440_pre(pieces: Dict[str, Any]) -> str:
|
484
|
+
"""TAG[.postN.devDISTANCE] -- No -dirty.
|
434
485
|
|
435
486
|
Exceptions:
|
436
487
|
1: no tags. 0.post0.devDISTANCE
|
437
488
|
"""
|
438
489
|
if pieces["closest-tag"]:
|
439
|
-
rendered = pieces["closest-tag"]
|
440
490
|
if pieces["distance"]:
|
441
|
-
|
491
|
+
# update the post release segment
|
492
|
+
tag_version, post_version = pep440_split_post(pieces["closest-tag"])
|
493
|
+
rendered = tag_version
|
494
|
+
if post_version is not None:
|
495
|
+
rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"])
|
496
|
+
else:
|
497
|
+
rendered += ".post0.dev%d" % (pieces["distance"])
|
498
|
+
else:
|
499
|
+
# no commits, use the tag as the version
|
500
|
+
rendered = pieces["closest-tag"]
|
442
501
|
else:
|
443
502
|
# exception #1
|
444
503
|
rendered = "0.post0.dev%d" % pieces["distance"]
|
445
504
|
return rendered
|
446
505
|
|
447
506
|
|
448
|
-
def render_pep440_post(pieces):
|
507
|
+
def render_pep440_post(pieces: Dict[str, Any]) -> str:
|
449
508
|
"""TAG[.postDISTANCE[.dev0]+gHEX] .
|
450
509
|
|
451
510
|
The ".dev0" means dirty. Note that .dev0 sorts backwards
|
@@ -472,7 +531,7 @@ def render_pep440_post(pieces):
|
|
472
531
|
return rendered
|
473
532
|
|
474
533
|
|
475
|
-
def render_pep440_post_branch(pieces):
|
534
|
+
def render_pep440_post_branch(pieces: Dict[str, Any]) -> str:
|
476
535
|
"""TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
|
477
536
|
|
478
537
|
The ".dev0" means not master branch.
|
@@ -501,7 +560,7 @@ def render_pep440_post_branch(pieces):
|
|
501
560
|
return rendered
|
502
561
|
|
503
562
|
|
504
|
-
def render_pep440_old(pieces):
|
563
|
+
def render_pep440_old(pieces: Dict[str, Any]) -> str:
|
505
564
|
"""TAG[.postDISTANCE[.dev0]] .
|
506
565
|
|
507
566
|
The ".dev0" means dirty.
|
@@ -523,7 +582,7 @@ def render_pep440_old(pieces):
|
|
523
582
|
return rendered
|
524
583
|
|
525
584
|
|
526
|
-
def render_git_describe(pieces):
|
585
|
+
def render_git_describe(pieces: Dict[str, Any]) -> str:
|
527
586
|
"""TAG[-DISTANCE-gHEX][-dirty].
|
528
587
|
|
529
588
|
Like 'git describe --tags --dirty --always'.
|
@@ -543,7 +602,7 @@ def render_git_describe(pieces):
|
|
543
602
|
return rendered
|
544
603
|
|
545
604
|
|
546
|
-
def render_git_describe_long(pieces):
|
605
|
+
def render_git_describe_long(pieces: Dict[str, Any]) -> str:
|
547
606
|
"""TAG-DISTANCE-gHEX[-dirty].
|
548
607
|
|
549
608
|
Like 'git describe --tags --dirty --always -long'.
|
@@ -563,7 +622,7 @@ def render_git_describe_long(pieces):
|
|
563
622
|
return rendered
|
564
623
|
|
565
624
|
|
566
|
-
def render(pieces, style):
|
625
|
+
def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]:
|
567
626
|
"""Render the given version pieces into the requested style."""
|
568
627
|
if pieces["error"]:
|
569
628
|
return {
|
@@ -605,7 +664,7 @@ def render(pieces, style):
|
|
605
664
|
}
|
606
665
|
|
607
666
|
|
608
|
-
def get_versions():
|
667
|
+
def get_versions() -> Dict[str, Any]:
|
609
668
|
"""Get version information or return default if unable to do so."""
|
610
669
|
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
|
611
670
|
# __file__, we can work backwards from there to the root. Some
|
prefect/blocks/abstract.py
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
|
+
from contextlib import contextmanager
|
2
3
|
from logging import Logger
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import
|
5
|
+
from typing import (
|
6
|
+
Any,
|
7
|
+
BinaryIO,
|
8
|
+
Dict,
|
9
|
+
Generator,
|
10
|
+
Generic,
|
11
|
+
List,
|
12
|
+
Optional,
|
13
|
+
Tuple,
|
14
|
+
TypeVar,
|
15
|
+
Union,
|
16
|
+
)
|
5
17
|
|
6
18
|
from typing_extensions import Self
|
7
19
|
|
@@ -48,6 +60,13 @@ class CredentialsBlock(Block, ABC):
|
|
48
60
|
"""
|
49
61
|
|
50
62
|
|
63
|
+
class NotificationError(Exception):
|
64
|
+
"""Raised if a notification block fails to send a notification."""
|
65
|
+
|
66
|
+
def __init__(self, log: str) -> None:
|
67
|
+
self.log = log
|
68
|
+
|
69
|
+
|
51
70
|
class NotificationBlock(Block, ABC):
|
52
71
|
"""
|
53
72
|
Block that represents a resource in an external system that is able to send notifications.
|
@@ -82,6 +101,20 @@ class NotificationBlock(Block, ABC):
|
|
82
101
|
subject: The subject of the notification.
|
83
102
|
"""
|
84
103
|
|
104
|
+
_raise_on_failure: bool = False
|
105
|
+
|
106
|
+
@contextmanager
|
107
|
+
def raise_on_failure(self) -> Generator[None, None, None]:
|
108
|
+
"""
|
109
|
+
Context manager that, while active, causes the block to raise errors if it
|
110
|
+
encounters a failure sending notifications.
|
111
|
+
"""
|
112
|
+
self._raise_on_failure = True
|
113
|
+
try:
|
114
|
+
yield
|
115
|
+
finally:
|
116
|
+
self._raise_on_failure = False
|
117
|
+
|
85
118
|
|
86
119
|
class JobRun(ABC, Generic[T]): # not a block
|
87
120
|
"""
|
prefect/blocks/notifications.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
import logging
|
1
2
|
from abc import ABC
|
2
3
|
from typing import Dict, List, Optional
|
3
4
|
|
4
5
|
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
6
|
+
from prefect.logging import LogEavesdropper
|
5
7
|
|
6
8
|
if HAS_PYDANTIC_V2:
|
7
9
|
from pydantic.v1 import AnyHttpUrl, Field, SecretStr
|
@@ -10,7 +12,7 @@ else:
|
|
10
12
|
|
11
13
|
from typing_extensions import Literal
|
12
14
|
|
13
|
-
from prefect.blocks.abstract import NotificationBlock
|
15
|
+
from prefect.blocks.abstract import NotificationBlock, NotificationError
|
14
16
|
from prefect.blocks.fields import SecretDict
|
15
17
|
from prefect.events.instrument import instrument_instance_method_call
|
16
18
|
from prefect.utilities.asyncutils import sync_compatible
|
@@ -61,10 +63,17 @@ class AbstractAppriseNotificationBlock(NotificationBlock, ABC):
|
|
61
63
|
|
62
64
|
@sync_compatible
|
63
65
|
@instrument_instance_method_call()
|
64
|
-
async def notify(
|
65
|
-
|
66
|
-
|
67
|
-
|
66
|
+
async def notify(
|
67
|
+
self,
|
68
|
+
body: str,
|
69
|
+
subject: Optional[str] = None,
|
70
|
+
):
|
71
|
+
with LogEavesdropper("apprise", level=logging.DEBUG) as eavesdropper:
|
72
|
+
result = await self._apprise_client.async_notify(
|
73
|
+
body=body, title=subject, notify_type=self.notify_type
|
74
|
+
)
|
75
|
+
if not result and self._raise_on_failure:
|
76
|
+
raise NotificationError(log=eavesdropper.text())
|
68
77
|
|
69
78
|
|
70
79
|
class AppriseNotificationBlock(AbstractAppriseNotificationBlock, ABC):
|
prefect/client/base.py
CHANGED
@@ -193,11 +193,18 @@ class PrefectHttpxClient(httpx.AsyncClient):
|
|
193
193
|
[Configuring Cloudflare Rate Limiting](https://support.cloudflare.com/hc/en-us/articles/115001635128-Configuring-Rate-Limiting-from-UI)
|
194
194
|
"""
|
195
195
|
|
196
|
-
def __init__(
|
196
|
+
def __init__(
|
197
|
+
self,
|
198
|
+
*args,
|
199
|
+
enable_csrf_support: bool = False,
|
200
|
+
raise_on_all_errors: bool = True,
|
201
|
+
**kwargs,
|
202
|
+
):
|
197
203
|
self.enable_csrf_support: bool = enable_csrf_support
|
198
204
|
self.csrf_token: Optional[str] = None
|
199
205
|
self.csrf_token_expiration: Optional[datetime] = None
|
200
206
|
self.csrf_client_id: uuid.UUID = uuid.uuid4()
|
207
|
+
self.raise_on_all_errors: bool = raise_on_all_errors
|
201
208
|
|
202
209
|
super().__init__(*args, **kwargs)
|
203
210
|
|
@@ -345,10 +352,8 @@ class PrefectHttpxClient(httpx.AsyncClient):
|
|
345
352
|
# Convert to a Prefect response to add nicer errors messages
|
346
353
|
response = PrefectResponse.from_httpx_response(response)
|
347
354
|
|
348
|
-
|
349
|
-
|
350
|
-
# `PrefectClient`
|
351
|
-
response.raise_for_status()
|
355
|
+
if self.raise_on_all_errors:
|
356
|
+
response.raise_for_status()
|
352
357
|
|
353
358
|
return response
|
354
359
|
|