prefect-client 3.3.8.dev4__py3-none-any.whl → 3.4.1__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/_build_info.py +3 -3
- prefect/_experimental/bundles/__init__.py +1 -1
- prefect/_internal/schemas/bases.py +11 -1
- prefect/_internal/schemas/validators.py +0 -98
- prefect/_internal/uuid7.py +11 -0
- prefect/_versioning.py +2 -0
- prefect/blocks/core.py +20 -1
- prefect/client/orchestration/__init__.py +16 -8
- prefect/client/schemas/actions.py +13 -35
- prefect/client/schemas/objects.py +30 -22
- prefect/client/subscriptions.py +18 -9
- prefect/deployments/runner.py +54 -4
- prefect/events/clients.py +6 -6
- prefect/events/filters.py +25 -11
- prefect/events/schemas/automations.py +3 -1
- prefect/events/schemas/events.py +3 -2
- prefect/flows.py +94 -28
- prefect/infrastructure/provisioners/cloud_run.py +1 -0
- prefect/runner/_observers.py +60 -0
- prefect/runner/runner.py +72 -214
- prefect/server/api/server.py +18 -1
- prefect/server/api/workers.py +42 -6
- prefect/settings/base.py +7 -7
- prefect/settings/models/experiments.py +2 -0
- prefect/task_runners.py +2 -1
- prefect/tasks.py +3 -2
- prefect/types/__init__.py +24 -36
- prefect/types/names.py +139 -0
- prefect/utilities/dockerutils.py +18 -8
- prefect/utilities/importtools.py +12 -4
- prefect/workers/base.py +66 -21
- {prefect_client-3.3.8.dev4.dist-info → prefect_client-3.4.1.dist-info}/METADATA +4 -3
- {prefect_client-3.3.8.dev4.dist-info → prefect_client-3.4.1.dist-info}/RECORD +35 -32
- {prefect_client-3.3.8.dev4.dist-info → prefect_client-3.4.1.dist-info}/WHEEL +0 -0
- {prefect_client-3.3.8.dev4.dist-info → prefect_client-3.4.1.dist-info}/licenses/LICENSE +0 -0
prefect/types/__init__.py
CHANGED
@@ -1,13 +1,21 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from functools import partial
|
4
|
-
from typing import Annotated, Any,
|
4
|
+
from typing import Annotated, Any, Optional, TypeVar, Union, cast
|
5
|
+
from uuid import UUID
|
5
6
|
from typing_extensions import Literal
|
6
7
|
import orjson
|
7
8
|
import pydantic
|
8
9
|
|
9
|
-
|
10
10
|
from ._datetime import DateTime, Date
|
11
|
+
from .names import (
|
12
|
+
Name,
|
13
|
+
NameOrEmpty,
|
14
|
+
NonEmptyishName,
|
15
|
+
BANNED_CHARACTERS,
|
16
|
+
WITHOUT_BANNED_CHARACTERS,
|
17
|
+
MAX_VARIABLE_NAME_LENGTH,
|
18
|
+
)
|
11
19
|
from pydantic import (
|
12
20
|
BeforeValidator,
|
13
21
|
Field,
|
@@ -21,7 +29,6 @@ from zoneinfo import available_timezones
|
|
21
29
|
|
22
30
|
T = TypeVar("T")
|
23
31
|
|
24
|
-
MAX_VARIABLE_NAME_LENGTH = 255
|
25
32
|
MAX_VARIABLE_VALUE_LENGTH = 5000
|
26
33
|
|
27
34
|
NonNegativeInteger = Annotated[int, Field(ge=0)]
|
@@ -39,37 +46,14 @@ TimeZone = Annotated[
|
|
39
46
|
]
|
40
47
|
|
41
48
|
|
42
|
-
BANNED_CHARACTERS = ["/", "%", "&", ">", "<"]
|
43
|
-
|
44
|
-
WITHOUT_BANNED_CHARACTERS = r"^[^" + "".join(BANNED_CHARACTERS) + "]+$"
|
45
|
-
Name = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS)]
|
46
|
-
|
47
|
-
WITHOUT_BANNED_CHARACTERS_EMPTY_OK = r"^[^" + "".join(BANNED_CHARACTERS) + "]*$"
|
48
|
-
NameOrEmpty = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS_EMPTY_OK)]
|
49
|
-
|
50
|
-
|
51
|
-
def non_emptyish(value: str) -> str:
|
52
|
-
if not value.strip("' \""):
|
53
|
-
raise ValueError("name cannot be an empty string")
|
54
|
-
|
55
|
-
return value
|
56
|
-
|
57
|
-
|
58
|
-
NonEmptyishName = Annotated[
|
59
|
-
str,
|
60
|
-
Field(pattern=WITHOUT_BANNED_CHARACTERS),
|
61
|
-
BeforeValidator(non_emptyish),
|
62
|
-
]
|
63
|
-
|
64
|
-
|
65
49
|
VariableValue = Union[
|
66
50
|
StrictStr,
|
67
51
|
StrictInt,
|
68
52
|
StrictBool,
|
69
53
|
StrictFloat,
|
70
54
|
None,
|
71
|
-
|
72
|
-
|
55
|
+
dict[str, Any],
|
56
|
+
list[Any],
|
73
57
|
]
|
74
58
|
|
75
59
|
|
@@ -100,24 +84,24 @@ def cast_none_to_empty_dict(value: Any) -> dict[str, Any]:
|
|
100
84
|
|
101
85
|
|
102
86
|
KeyValueLabels = Annotated[
|
103
|
-
|
87
|
+
dict[str, Union[StrictBool, StrictInt, StrictFloat, str]],
|
104
88
|
BeforeValidator(cast_none_to_empty_dict),
|
105
89
|
]
|
106
90
|
|
107
91
|
|
108
92
|
ListOfNonEmptyStrings = Annotated[
|
109
|
-
|
93
|
+
list[str],
|
110
94
|
BeforeValidator(lambda x: [str(s) for s in x if str(s).strip()]),
|
111
95
|
]
|
112
96
|
|
113
97
|
|
114
|
-
class SecretDict(pydantic.Secret[
|
98
|
+
class SecretDict(pydantic.Secret[dict[str, Any]]):
|
115
99
|
pass
|
116
100
|
|
117
101
|
|
118
102
|
def validate_set_T_from_delim_string(
|
119
|
-
value: Union[str, T,
|
120
|
-
) ->
|
103
|
+
value: Union[str, T, set[T], None], type_: Any, delim: str | None = None
|
104
|
+
) -> set[T]:
|
121
105
|
"""
|
122
106
|
"no-info" before validator useful in scooping env vars
|
123
107
|
|
@@ -131,20 +115,20 @@ def validate_set_T_from_delim_string(
|
|
131
115
|
delim = delim or ","
|
132
116
|
if isinstance(value, str):
|
133
117
|
return {T_adapter.validate_strings(s.strip()) for s in value.split(delim)}
|
134
|
-
errors = []
|
118
|
+
errors: list[pydantic.ValidationError] = []
|
135
119
|
try:
|
136
120
|
return {T_adapter.validate_python(value)}
|
137
121
|
except pydantic.ValidationError as e:
|
138
122
|
errors.append(e)
|
139
123
|
try:
|
140
|
-
return TypeAdapter(
|
124
|
+
return TypeAdapter(set[type_]).validate_python(value)
|
141
125
|
except pydantic.ValidationError as e:
|
142
126
|
errors.append(e)
|
143
127
|
raise ValueError(f"Invalid set[{type_}]: {errors}")
|
144
128
|
|
145
129
|
|
146
130
|
ClientRetryExtraCodes = Annotated[
|
147
|
-
Union[str, StatusCode,
|
131
|
+
Union[str, StatusCode, set[StatusCode], None],
|
148
132
|
BeforeValidator(partial(validate_set_T_from_delim_string, type_=StatusCode)),
|
149
133
|
]
|
150
134
|
|
@@ -170,11 +154,15 @@ KeyValueLabelsField = Annotated[
|
|
170
154
|
|
171
155
|
|
172
156
|
__all__ = [
|
157
|
+
"BANNED_CHARACTERS",
|
158
|
+
"WITHOUT_BANNED_CHARACTERS",
|
173
159
|
"ClientRetryExtraCodes",
|
174
160
|
"Date",
|
175
161
|
"DateTime",
|
176
162
|
"LogLevel",
|
177
163
|
"KeyValueLabelsField",
|
164
|
+
"MAX_VARIABLE_NAME_LENGTH",
|
165
|
+
"MAX_VARIABLE_VALUE_LENGTH",
|
178
166
|
"NonNegativeInteger",
|
179
167
|
"PositiveInteger",
|
180
168
|
"ListOfNonEmptyStrings",
|
prefect/types/names.py
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from functools import partial
|
5
|
+
from typing import Annotated, overload
|
6
|
+
|
7
|
+
from pydantic import AfterValidator, BeforeValidator, Field
|
8
|
+
|
9
|
+
LOWERCASE_LETTERS_NUMBERS_AND_DASHES_ONLY_REGEX = "^[a-z0-9-]*$"
|
10
|
+
LOWERCASE_LETTERS_NUMBERS_AND_UNDERSCORES_REGEX = "^[a-z0-9_]*$"
|
11
|
+
LOWERCASE_LETTERS_NUMBERS_AND_DASHES_OR_UNDERSCORES_REGEX = "^[a-z0-9-_]*$"
|
12
|
+
|
13
|
+
|
14
|
+
@overload
|
15
|
+
def raise_on_name_alphanumeric_dashes_only(
|
16
|
+
value: str, field_name: str = ...
|
17
|
+
) -> str: ...
|
18
|
+
|
19
|
+
|
20
|
+
@overload
|
21
|
+
def raise_on_name_alphanumeric_dashes_only(
|
22
|
+
value: None, field_name: str = ...
|
23
|
+
) -> None: ...
|
24
|
+
|
25
|
+
|
26
|
+
def raise_on_name_alphanumeric_dashes_only(
|
27
|
+
value: str | None, field_name: str = "value"
|
28
|
+
) -> str | None:
|
29
|
+
if value is not None and not bool(
|
30
|
+
re.match(LOWERCASE_LETTERS_NUMBERS_AND_DASHES_ONLY_REGEX, value)
|
31
|
+
):
|
32
|
+
raise ValueError(
|
33
|
+
f"{field_name} must only contain lowercase letters, numbers, and dashes."
|
34
|
+
)
|
35
|
+
return value
|
36
|
+
|
37
|
+
|
38
|
+
@overload
|
39
|
+
def raise_on_name_alphanumeric_underscores_only(
|
40
|
+
value: str, field_name: str = ...
|
41
|
+
) -> str: ...
|
42
|
+
|
43
|
+
|
44
|
+
@overload
|
45
|
+
def raise_on_name_alphanumeric_underscores_only(
|
46
|
+
value: None, field_name: str = ...
|
47
|
+
) -> None: ...
|
48
|
+
|
49
|
+
|
50
|
+
def raise_on_name_alphanumeric_underscores_only(
|
51
|
+
value: str | None, field_name: str = "value"
|
52
|
+
) -> str | None:
|
53
|
+
if value is not None and not re.match(
|
54
|
+
LOWERCASE_LETTERS_NUMBERS_AND_UNDERSCORES_REGEX, value
|
55
|
+
):
|
56
|
+
raise ValueError(
|
57
|
+
f"{field_name} must only contain lowercase letters, numbers, and"
|
58
|
+
" underscores."
|
59
|
+
)
|
60
|
+
return value
|
61
|
+
|
62
|
+
|
63
|
+
def raise_on_name_alphanumeric_dashes_underscores_only(
|
64
|
+
value: str, field_name: str = "value"
|
65
|
+
) -> str:
|
66
|
+
if not re.match(LOWERCASE_LETTERS_NUMBERS_AND_DASHES_OR_UNDERSCORES_REGEX, value):
|
67
|
+
raise ValueError(
|
68
|
+
f"{field_name} must only contain lowercase letters, numbers, and"
|
69
|
+
" dashes or underscores."
|
70
|
+
)
|
71
|
+
return value
|
72
|
+
|
73
|
+
|
74
|
+
BANNED_CHARACTERS = ["/", "%", "&", ">", "<"]
|
75
|
+
|
76
|
+
WITHOUT_BANNED_CHARACTERS = r"^[^" + "".join(BANNED_CHARACTERS) + "]+$"
|
77
|
+
Name = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS)]
|
78
|
+
|
79
|
+
WITHOUT_BANNED_CHARACTERS_EMPTY_OK = r"^[^" + "".join(BANNED_CHARACTERS) + "]*$"
|
80
|
+
NameOrEmpty = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS_EMPTY_OK)]
|
81
|
+
|
82
|
+
|
83
|
+
def non_emptyish(value: str) -> str:
|
84
|
+
if not value.strip("' \""):
|
85
|
+
raise ValueError("name cannot be an empty string")
|
86
|
+
|
87
|
+
return value
|
88
|
+
|
89
|
+
|
90
|
+
NonEmptyishName = Annotated[
|
91
|
+
str,
|
92
|
+
Field(pattern=WITHOUT_BANNED_CHARACTERS),
|
93
|
+
BeforeValidator(non_emptyish),
|
94
|
+
]
|
95
|
+
|
96
|
+
|
97
|
+
### specific names
|
98
|
+
|
99
|
+
BlockDocumentName = Annotated[
|
100
|
+
Name,
|
101
|
+
AfterValidator(
|
102
|
+
partial(
|
103
|
+
raise_on_name_alphanumeric_dashes_only, field_name="Block document name"
|
104
|
+
)
|
105
|
+
),
|
106
|
+
]
|
107
|
+
|
108
|
+
|
109
|
+
BlockTypeSlug = Annotated[
|
110
|
+
str,
|
111
|
+
AfterValidator(
|
112
|
+
partial(raise_on_name_alphanumeric_dashes_only, field_name="Block type slug")
|
113
|
+
),
|
114
|
+
]
|
115
|
+
|
116
|
+
ArtifactKey = Annotated[
|
117
|
+
str,
|
118
|
+
AfterValidator(
|
119
|
+
partial(raise_on_name_alphanumeric_dashes_only, field_name="Artifact key")
|
120
|
+
),
|
121
|
+
]
|
122
|
+
|
123
|
+
MAX_VARIABLE_NAME_LENGTH = 255
|
124
|
+
|
125
|
+
|
126
|
+
VariableName = Annotated[
|
127
|
+
str,
|
128
|
+
AfterValidator(
|
129
|
+
partial(
|
130
|
+
raise_on_name_alphanumeric_dashes_underscores_only,
|
131
|
+
field_name="Variable name",
|
132
|
+
)
|
133
|
+
),
|
134
|
+
Field(
|
135
|
+
max_length=MAX_VARIABLE_NAME_LENGTH,
|
136
|
+
description="The name of the variable",
|
137
|
+
examples=["my_variable"],
|
138
|
+
),
|
139
|
+
]
|
prefect/utilities/dockerutils.py
CHANGED
@@ -495,10 +495,11 @@ def parse_image_tag(name: str) -> tuple[str, Optional[str]]:
|
|
495
495
|
"""
|
496
496
|
Parse Docker Image String
|
497
497
|
|
498
|
-
- If a tag exists, this function parses and returns the image registry and tag,
|
498
|
+
- If a tag or digest exists, this function parses and returns the image registry and tag/digest,
|
499
499
|
separately as a tuple.
|
500
500
|
- Example 1: 'prefecthq/prefect:latest' -> ('prefecthq/prefect', 'latest')
|
501
501
|
- Example 2: 'hostname.io:5050/folder/subfolder:latest' -> ('hostname.io:5050/folder/subfolder', 'latest')
|
502
|
+
- Example 3: 'prefecthq/prefect@sha256:abc123' -> ('prefecthq/prefect', 'sha256:abc123')
|
502
503
|
- Supports parsing Docker Image strings that follow Docker Image Specification v1.1.0
|
503
504
|
- Image building tools typically enforce this standard
|
504
505
|
|
@@ -506,26 +507,35 @@ def parse_image_tag(name: str) -> tuple[str, Optional[str]]:
|
|
506
507
|
name (str): Name of Docker Image
|
507
508
|
|
508
509
|
Return:
|
509
|
-
tuple: image registry, image tag
|
510
|
+
tuple: image registry, image tag/digest
|
510
511
|
"""
|
511
512
|
tag = None
|
512
513
|
name_parts = name.split("/")
|
513
|
-
|
514
|
-
# -
|
514
|
+
|
515
|
+
# First handles the simplest image names (DockerHub-based, index-free, potentially with a tag or digest)
|
516
|
+
# - Example: simplename:latest or simplename@sha256:abc123
|
515
517
|
if len(name_parts) == 1:
|
516
|
-
if "
|
518
|
+
if "@" in name_parts[0]:
|
519
|
+
image_name, tag = name_parts[0].split("@")
|
520
|
+
elif ":" in name_parts[0]:
|
517
521
|
image_name, tag = name_parts[0].split(":")
|
522
|
+
|
518
523
|
else:
|
519
524
|
image_name = name_parts[0]
|
520
525
|
else:
|
521
526
|
# 1. Separates index (hostname.io or prefecthq) from path:tag (folder/subfolder:latest or prefect:latest)
|
522
|
-
# 2. Separates path and tag (if
|
523
|
-
# 3. Reunites index and path (without tag) as image name
|
527
|
+
# 2. Separates path and tag/digest (if exists)
|
528
|
+
# 3. Reunites index and path (without tag/digest) as image name
|
524
529
|
index_name = name_parts[0]
|
525
530
|
image_path = "/".join(name_parts[1:])
|
526
|
-
|
531
|
+
|
532
|
+
if "@" in image_path:
|
533
|
+
image_path, tag = image_path.split("@")
|
534
|
+
elif ":" in image_path:
|
527
535
|
image_path, tag = image_path.split(":")
|
536
|
+
|
528
537
|
image_name = f"{index_name}/{image_path}"
|
538
|
+
|
529
539
|
return image_name, tag
|
530
540
|
|
531
541
|
|
prefect/utilities/importtools.py
CHANGED
@@ -145,17 +145,19 @@ def import_object(import_path: str) -> Any:
|
|
145
145
|
- module.object
|
146
146
|
- module:object
|
147
147
|
- /path/to/script.py:object
|
148
|
+
- module:object.method
|
149
|
+
- /path/to/script.py:object.method
|
148
150
|
|
149
151
|
This function is not thread safe as it modifies the 'sys' module during execution.
|
150
152
|
"""
|
151
153
|
if ".py:" in import_path:
|
152
|
-
script_path,
|
154
|
+
script_path, object_path = import_path.rsplit(":", 1)
|
153
155
|
module = load_script_as_module(script_path)
|
154
156
|
else:
|
155
157
|
if ":" in import_path:
|
156
|
-
module_name,
|
158
|
+
module_name, object_path = import_path.rsplit(":", 1)
|
157
159
|
elif "." in import_path:
|
158
|
-
module_name,
|
160
|
+
module_name, object_path = import_path.rsplit(".", 1)
|
159
161
|
else:
|
160
162
|
raise ValueError(
|
161
163
|
f"Invalid format for object import. Received {import_path!r}."
|
@@ -163,7 +165,13 @@ def import_object(import_path: str) -> Any:
|
|
163
165
|
|
164
166
|
module = load_module(module_name)
|
165
167
|
|
166
|
-
|
168
|
+
# Handle nested object/method access
|
169
|
+
parts = object_path.split(".")
|
170
|
+
obj = module
|
171
|
+
for part in parts:
|
172
|
+
obj = getattr(obj, part)
|
173
|
+
|
174
|
+
return obj
|
167
175
|
|
168
176
|
|
169
177
|
class DelayedImportErrorModule(ModuleType):
|
prefect/workers/base.py
CHANGED
@@ -55,6 +55,7 @@ from prefect.exceptions import (
|
|
55
55
|
Abort,
|
56
56
|
ObjectNotFound,
|
57
57
|
)
|
58
|
+
from prefect.filesystems import LocalFileSystem
|
58
59
|
from prefect.futures import PrefectFlowRunFuture
|
59
60
|
from prefect.logging.loggers import (
|
60
61
|
PrefectLogAdapter,
|
@@ -696,6 +697,25 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
696
697
|
"Workers must implement a method for running submitted flow runs"
|
697
698
|
)
|
698
699
|
|
700
|
+
async def _initiate_run(
|
701
|
+
self,
|
702
|
+
flow_run: "FlowRun",
|
703
|
+
configuration: C,
|
704
|
+
) -> None:
|
705
|
+
"""
|
706
|
+
This method is called by the worker to initiate a flow run and should return as
|
707
|
+
soon as possible.
|
708
|
+
|
709
|
+
This method is used in `.submit` to allow non-blocking submission of flows. For
|
710
|
+
workers that wait for completion in their `run` method, this method should be
|
711
|
+
implemented to return immediately.
|
712
|
+
|
713
|
+
If this method is not implemented, `.submit` will fall back to the `.run` method.
|
714
|
+
"""
|
715
|
+
raise NotImplementedError(
|
716
|
+
"This worker has not implemented `_initiate_run`. Please use `run` instead."
|
717
|
+
)
|
718
|
+
|
699
719
|
async def submit(
|
700
720
|
self,
|
701
721
|
flow: "Flow[..., FR]",
|
@@ -722,15 +742,6 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
722
742
|
if self._runs_task_group is None:
|
723
743
|
raise RuntimeError("Worker not properly initialized")
|
724
744
|
|
725
|
-
from prefect.results import get_result_store
|
726
|
-
|
727
|
-
current_result_store = get_result_store()
|
728
|
-
if current_result_store.result_storage is None and flow.result_storage is None:
|
729
|
-
self._logger.warning(
|
730
|
-
f"Flow {flow.name!r} has no result storage configured. Please configure "
|
731
|
-
"result storage for the flow if you want to retrieve the result for the flow run."
|
732
|
-
)
|
733
|
-
|
734
745
|
flow_run = await self._runs_task_group.start(
|
735
746
|
partial(
|
736
747
|
self._submit_adhoc_run,
|
@@ -766,6 +777,32 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
766
777
|
"work-pool storage configure`."
|
767
778
|
)
|
768
779
|
|
780
|
+
from prefect.results import aresolve_result_storage, get_result_store
|
781
|
+
|
782
|
+
current_result_store = get_result_store()
|
783
|
+
# Check result storage and use the work pool default if needed
|
784
|
+
if (
|
785
|
+
current_result_store.result_storage is None
|
786
|
+
or isinstance(current_result_store.result_storage, LocalFileSystem)
|
787
|
+
and flow.result_storage is None
|
788
|
+
):
|
789
|
+
if (
|
790
|
+
self.work_pool.storage_configuration.default_result_storage_block_id
|
791
|
+
is None
|
792
|
+
):
|
793
|
+
self._logger.warning(
|
794
|
+
f"Flow {flow.name!r} has no result storage configured. Please configure "
|
795
|
+
"result storage for the flow if you want to retrieve the result for the flow run."
|
796
|
+
)
|
797
|
+
else:
|
798
|
+
# Use the work pool's default result storage block for the flow run to ensure the caller can retrieve the result
|
799
|
+
flow = flow.with_options(
|
800
|
+
result_storage=await aresolve_result_storage(
|
801
|
+
self.work_pool.storage_configuration.default_result_storage_block_id
|
802
|
+
),
|
803
|
+
persist_result=True,
|
804
|
+
)
|
805
|
+
|
769
806
|
bundle_key = str(uuid.uuid4())
|
770
807
|
upload_command = convert_step_to_command(
|
771
808
|
self.work_pool.storage_configuration.bundle_upload_step,
|
@@ -778,8 +815,9 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
778
815
|
|
779
816
|
job_variables = (job_variables or {}) | {"command": " ".join(execute_command)}
|
780
817
|
parameters = parameters or {}
|
781
|
-
parent_task_run = None
|
782
818
|
|
819
|
+
# Create a parent task run if this is a child flow run to ensure it shows up as a child flow in the UI
|
820
|
+
parent_task_run = None
|
783
821
|
if flow_run_ctx := FlowRunContext.get():
|
784
822
|
parent_task = Task[Any, Any](
|
785
823
|
name=flow.name,
|
@@ -821,6 +859,8 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
821
859
|
|
822
860
|
bundle = create_bundle_for_flow_run(flow=flow, flow_run=flow_run)
|
823
861
|
|
862
|
+
# Write the bundle to a temporary directory so it can be uploaded to the bundle storage
|
863
|
+
# via the upload command
|
824
864
|
with tempfile.TemporaryDirectory() as temp_dir:
|
825
865
|
await (
|
826
866
|
anyio.Path(temp_dir)
|
@@ -843,16 +883,21 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
843
883
|
logger.debug("Successfully uploaded execution bundle")
|
844
884
|
|
845
885
|
try:
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
await self.
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
886
|
+
# Call the implementation-specific run method with the constructed configuration. This is where the
|
887
|
+
# rubber meets the road.
|
888
|
+
try:
|
889
|
+
await self._initiate_run(flow_run, configuration)
|
890
|
+
except NotImplementedError:
|
891
|
+
result = await self.run(flow_run, configuration)
|
892
|
+
|
893
|
+
if result.status_code != 0:
|
894
|
+
await self._propose_crashed_state(
|
895
|
+
flow_run,
|
896
|
+
(
|
897
|
+
"Flow run infrastructure exited with non-zero status code"
|
898
|
+
f" {result.status_code}."
|
899
|
+
),
|
900
|
+
)
|
856
901
|
except Exception as exc:
|
857
902
|
# This flow run was being submitted and did not start successfully
|
858
903
|
logger.exception(
|
@@ -1128,7 +1173,7 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
1128
1173
|
if self._limiter:
|
1129
1174
|
self._limiter.acquire_on_behalf_of_nowait(flow_run.id)
|
1130
1175
|
except anyio.WouldBlock:
|
1131
|
-
self._logger.
|
1176
|
+
self._logger.debug(
|
1132
1177
|
f"Flow run limit reached; {self.limiter.borrowed_tokens} flow runs"
|
1133
1178
|
" in progress."
|
1134
1179
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: prefect-client
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.4.1
|
4
4
|
Summary: Workflow orchestration and management.
|
5
5
|
Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
|
6
6
|
Project-URL: Documentation, https://docs.prefect.io
|
@@ -40,7 +40,7 @@ Requires-Dist: jsonpatch<2.0,>=1.32
|
|
40
40
|
Requires-Dist: jsonschema<5.0.0,>=4.0.0
|
41
41
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.27.0
|
42
42
|
Requires-Dist: orjson<4.0,>=3.7
|
43
|
-
Requires-Dist: packaging<
|
43
|
+
Requires-Dist: packaging<25.1,>=21.3
|
44
44
|
Requires-Dist: pathspec>=0.8.0
|
45
45
|
Requires-Dist: pendulum<4,>=3.0.0; python_version < '3.13'
|
46
46
|
Requires-Dist: prometheus-client>=0.20.0
|
@@ -60,9 +60,10 @@ Requires-Dist: sniffio<2.0.0,>=1.3.0
|
|
60
60
|
Requires-Dist: toml>=0.10.0
|
61
61
|
Requires-Dist: typing-extensions<5.0.0,>=4.10.0
|
62
62
|
Requires-Dist: ujson<6.0.0,>=5.8.0
|
63
|
+
Requires-Dist: uuid7>=0.1.0
|
63
64
|
Requires-Dist: uvicorn!=0.29.0,>=0.14.0
|
64
65
|
Requires-Dist: websockets<16.0,>=13.0
|
65
|
-
Requires-Dist: whenever<0.
|
66
|
+
Requires-Dist: whenever<0.9.0,>=0.7.3; python_version >= '3.13'
|
66
67
|
Provides-Extra: notifications
|
67
68
|
Requires-Dist: apprise<2.0.0,>=1.1.0; extra == 'notifications'
|
68
69
|
Description-Content-Type: text/markdown
|