prefect-client 2.16.4__py3-none-any.whl → 2.16.5__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/concurrency/calls.py +3 -2
- prefect/_internal/pydantic/__init__.py +2 -0
- prefect/_internal/pydantic/_compat.py +180 -0
- prefect/artifacts.py +185 -0
- prefect/client/base.py +80 -10
- prefect/client/cloud.py +1 -1
- prefect/client/orchestration.py +17 -1
- prefect/client/schemas/objects.py +13 -0
- prefect/client/subscriptions.py +3 -3
- prefect/engine.py +10 -6
- prefect/events/related.py +1 -1
- prefect/events/schemas.py +45 -1
- prefect/flows.py +12 -4
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +14 -2
- prefect/settings.py +62 -8
- prefect/task_server.py +9 -3
- prefect/tasks.py +13 -8
- prefect/utilities/callables.py +3 -1
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.5.dist-info}/METADATA +1 -1
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.5.dist-info}/RECORD +23 -21
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.5.dist-info}/LICENSE +0 -0
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.5.dist-info}/WHEEL +0 -0
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.5.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@
|
|
2
2
|
Implementation of the `Call` data structure for transport of deferred function calls
|
3
3
|
and low-level management of call execution.
|
4
4
|
"""
|
5
|
+
|
5
6
|
import abc
|
6
7
|
import asyncio
|
7
8
|
import concurrent.futures
|
@@ -40,8 +41,8 @@ P = ParamSpec("P")
|
|
40
41
|
# we already have strong references to the `Call` objects in other places
|
41
42
|
# and b) this is used for performance optimizations where we have fallback
|
42
43
|
# behavior if this weakref is garbage collected. A fix for issue #10952.
|
43
|
-
current_call: contextvars.ContextVar["weakref.ref[Call]"] =
|
44
|
-
"current_call"
|
44
|
+
current_call: contextvars.ContextVar["weakref.ref[Call]"] = ( # novm
|
45
|
+
contextvars.ContextVar("current_call")
|
45
46
|
)
|
46
47
|
|
47
48
|
# Create a strong reference to tasks to prevent destruction during execution errors
|
@@ -0,0 +1,180 @@
|
|
1
|
+
from typing import Any, Dict, Literal, Optional, Set, Type, Union
|
2
|
+
|
3
|
+
import typing_extensions
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
7
|
+
from prefect.logging.loggers import get_logger
|
8
|
+
from prefect.settings import PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS
|
9
|
+
|
10
|
+
IncEx: typing_extensions.TypeAlias = (
|
11
|
+
"Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None]"
|
12
|
+
)
|
13
|
+
|
14
|
+
logger = get_logger("prefect._internal.pydantic")
|
15
|
+
|
16
|
+
if HAS_PYDANTIC_V2:
|
17
|
+
from pydantic.json_schema import GenerateJsonSchema
|
18
|
+
|
19
|
+
|
20
|
+
def is_pydantic_v2_compatible(
|
21
|
+
model_instance: Optional[BaseModel] = None, fn_name: Optional[str] = None
|
22
|
+
) -> bool:
|
23
|
+
"""
|
24
|
+
Determines if the current environment is compatible with Pydantic V2 features,
|
25
|
+
based on the presence of Pydantic V2 and a global setting that enables V2 functionalities.
|
26
|
+
|
27
|
+
This function primarily serves to facilitate conditional logic in code that needs to
|
28
|
+
operate differently depending on the availability of Pydantic V2 features. It checks
|
29
|
+
two conditions: whether Pydantic V2 is installed, and whether the use of V2 features
|
30
|
+
is explicitly enabled through a global setting (`PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS`).
|
31
|
+
|
32
|
+
Parameters:
|
33
|
+
-----------
|
34
|
+
model_instance : Optional[BaseModel], optional
|
35
|
+
An instance of a Pydantic model. This parameter is used to perform a type check
|
36
|
+
to ensure the passed object is a Pydantic model instance. If not provided or if
|
37
|
+
the object is not a Pydantic model, a TypeError is raised. Defaults to None.
|
38
|
+
|
39
|
+
fn_name : Optional[str], optional
|
40
|
+
The name of the function or feature for which V2 compatibility is being checked.
|
41
|
+
This is used for logging purposes to provide more context in debug messages.
|
42
|
+
Defaults to None.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
--------
|
46
|
+
bool
|
47
|
+
True if the current environment supports Pydantic V2 features and if the global
|
48
|
+
setting for enabling V2 features is set to True. False otherwise.
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
-------
|
52
|
+
TypeError
|
53
|
+
If `model_instance` is provided but is not an instance of a Pydantic BaseModel.
|
54
|
+
"""
|
55
|
+
if model_instance and not isinstance(model_instance, BaseModel):
|
56
|
+
raise TypeError(
|
57
|
+
f"Expected a Pydantic model, but got {type(model_instance).__name__}"
|
58
|
+
)
|
59
|
+
|
60
|
+
should_dump_as_v2_model = (
|
61
|
+
HAS_PYDANTIC_V2 and PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS
|
62
|
+
)
|
63
|
+
|
64
|
+
if should_dump_as_v2_model:
|
65
|
+
logger.debug(
|
66
|
+
f"Using Pydantic v2 compatibility layer for `{fn_name}`. This will be removed in a future release."
|
67
|
+
)
|
68
|
+
|
69
|
+
return True
|
70
|
+
|
71
|
+
elif HAS_PYDANTIC_V2:
|
72
|
+
logger.debug(
|
73
|
+
"Pydantic v2 compatibility layer is disabled. To enable, set `PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS` to `True`."
|
74
|
+
)
|
75
|
+
|
76
|
+
else:
|
77
|
+
logger.debug("Pydantic v2 is not installed.")
|
78
|
+
|
79
|
+
return False
|
80
|
+
|
81
|
+
|
82
|
+
def model_dump(
|
83
|
+
model_instance: BaseModel,
|
84
|
+
*,
|
85
|
+
mode: Union[Literal["json", "python"], str] = "python",
|
86
|
+
include: IncEx = None,
|
87
|
+
exclude: IncEx = None,
|
88
|
+
by_alias: bool = False,
|
89
|
+
exclude_unset: bool = False,
|
90
|
+
exclude_defaults: bool = False,
|
91
|
+
exclude_none: bool = False,
|
92
|
+
round_trip: bool = False,
|
93
|
+
warnings: bool = True,
|
94
|
+
) -> Dict[str, Any]:
|
95
|
+
"""
|
96
|
+
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
mode: The mode in which `to_python` should run.
|
100
|
+
If mode is 'json', the output will only contain JSON serializable types.
|
101
|
+
If mode is 'python', the output may contain non-JSON-serializable Python objects.
|
102
|
+
include: A list of fields to include in the output.
|
103
|
+
exclude: A list of fields to exclude from the output.
|
104
|
+
by_alias: Whether to use the field's alias in the dictionary key if defined.
|
105
|
+
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
106
|
+
exclude_defaults: Whether to exclude fields that are set to their default value.
|
107
|
+
exclude_none: Whether to exclude fields that have a value of `None`.
|
108
|
+
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
|
109
|
+
warnings: Whether to log warnings when invalid fields are encountered.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
A dictionary representation of the model.
|
113
|
+
"""
|
114
|
+
if is_pydantic_v2_compatible(model_instance=model_instance, fn_name="model_dump"):
|
115
|
+
return model_instance.model_dump(
|
116
|
+
mode=mode,
|
117
|
+
include=include,
|
118
|
+
exclude=exclude,
|
119
|
+
by_alias=by_alias,
|
120
|
+
exclude_unset=exclude_unset,
|
121
|
+
exclude_defaults=exclude_defaults,
|
122
|
+
exclude_none=exclude_none,
|
123
|
+
round_trip=round_trip,
|
124
|
+
warnings=warnings,
|
125
|
+
)
|
126
|
+
|
127
|
+
return model_instance.dict(
|
128
|
+
include=include,
|
129
|
+
exclude=exclude,
|
130
|
+
by_alias=by_alias,
|
131
|
+
exclude_unset=exclude_unset,
|
132
|
+
exclude_defaults=exclude_defaults,
|
133
|
+
exclude_none=exclude_none,
|
134
|
+
)
|
135
|
+
|
136
|
+
|
137
|
+
DEFAULT_REF_TEMPLATE = "#/$defs/{model}"
|
138
|
+
JsonSchemaMode = Literal["validation", "serialization"]
|
139
|
+
|
140
|
+
|
141
|
+
def model_json_schema(
|
142
|
+
model: Type[BaseModel],
|
143
|
+
*,
|
144
|
+
by_alias: bool = True,
|
145
|
+
ref_template: str = DEFAULT_REF_TEMPLATE,
|
146
|
+
schema_generator=None,
|
147
|
+
mode: JsonSchemaMode = "validation",
|
148
|
+
) -> Dict[str, Any]:
|
149
|
+
"""
|
150
|
+
Generates a JSON schema for a model class.
|
151
|
+
|
152
|
+
Parameters
|
153
|
+
----------
|
154
|
+
by_alias : bool, optional
|
155
|
+
Whether to use attribute aliases or not, by default True
|
156
|
+
ref_template : str, optional
|
157
|
+
The reference template, by default DEFAULT_REF_TEMPLATE
|
158
|
+
schema_generator : type[GenerateEmptySchemaForUserClasses], optional
|
159
|
+
To override the logic used to generate the JSON schema, as a subclass of GenerateEmptySchemaForUserClasses with your desired modifications, by default GenerateEmptySchemaForUserClasses
|
160
|
+
mode : JsonSchemaMode, optional
|
161
|
+
The mode in which to generate the schema, by default 'validation'
|
162
|
+
|
163
|
+
Returns
|
164
|
+
-------
|
165
|
+
dict[str, Any]
|
166
|
+
The JSON schema for the given model class.
|
167
|
+
"""
|
168
|
+
if is_pydantic_v2_compatible(fn_name="model_json_schema"):
|
169
|
+
schema_generator = GenerateJsonSchema
|
170
|
+
return model.model_json_schema(
|
171
|
+
by_alias=by_alias,
|
172
|
+
ref_template=ref_template,
|
173
|
+
schema_generator=schema_generator,
|
174
|
+
mode=mode,
|
175
|
+
)
|
176
|
+
|
177
|
+
return model.schema(
|
178
|
+
by_alias=by_alias,
|
179
|
+
ref_template=ref_template,
|
180
|
+
)
|
prefect/artifacts.py
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
"""
|
2
|
+
Interface for creating and reading artifacts.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import math
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
8
|
+
from uuid import UUID
|
9
|
+
|
10
|
+
from prefect.client.orchestration import PrefectClient
|
11
|
+
from prefect.client.schemas.actions import ArtifactCreate
|
12
|
+
from prefect.client.utilities import inject_client
|
13
|
+
from prefect.context import FlowRunContext, TaskRunContext
|
14
|
+
from prefect.utilities.asyncutils import sync_compatible
|
15
|
+
|
16
|
+
INVALID_TABLE_TYPE_ERROR = (
|
17
|
+
"`create_table_artifact` requires a `table` argument of type `dict[list]` or"
|
18
|
+
" `list[dict]`."
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
@inject_client
|
23
|
+
async def _create_artifact(
|
24
|
+
type: str,
|
25
|
+
key: Optional[str] = None,
|
26
|
+
description: Optional[str] = None,
|
27
|
+
data: Optional[Union[Dict[str, Any], Any]] = None,
|
28
|
+
client: Optional[PrefectClient] = None,
|
29
|
+
) -> UUID:
|
30
|
+
"""
|
31
|
+
Helper function to create an artifact.
|
32
|
+
|
33
|
+
Arguments:
|
34
|
+
type: A string identifying the type of artifact.
|
35
|
+
key: A user-provided string identifier.
|
36
|
+
The key must only contain lowercase letters, numbers, and dashes.
|
37
|
+
description: A user-specified description of the artifact.
|
38
|
+
data: A JSON payload that allows for a result to be retrieved.
|
39
|
+
client: The PrefectClient
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
- The table artifact ID.
|
43
|
+
"""
|
44
|
+
artifact_args = {}
|
45
|
+
task_run_ctx = TaskRunContext.get()
|
46
|
+
flow_run_ctx = FlowRunContext.get()
|
47
|
+
|
48
|
+
if task_run_ctx:
|
49
|
+
artifact_args["task_run_id"] = task_run_ctx.task_run.id
|
50
|
+
artifact_args["flow_run_id"] = task_run_ctx.task_run.flow_run_id
|
51
|
+
elif flow_run_ctx:
|
52
|
+
artifact_args["flow_run_id"] = flow_run_ctx.flow_run.id
|
53
|
+
|
54
|
+
if key is not None:
|
55
|
+
artifact_args["key"] = key
|
56
|
+
if type is not None:
|
57
|
+
artifact_args["type"] = type
|
58
|
+
if description is not None:
|
59
|
+
artifact_args["description"] = description
|
60
|
+
if data is not None:
|
61
|
+
artifact_args["data"] = data
|
62
|
+
|
63
|
+
artifact = ArtifactCreate(**artifact_args)
|
64
|
+
|
65
|
+
return await client.create_artifact(artifact=artifact)
|
66
|
+
|
67
|
+
|
68
|
+
@sync_compatible
|
69
|
+
async def create_link_artifact(
|
70
|
+
link: str,
|
71
|
+
link_text: Optional[str] = None,
|
72
|
+
key: Optional[str] = None,
|
73
|
+
description: Optional[str] = None,
|
74
|
+
) -> UUID:
|
75
|
+
"""
|
76
|
+
Create a link artifact.
|
77
|
+
|
78
|
+
Arguments:
|
79
|
+
link: The link to create.
|
80
|
+
link_text: The link text.
|
81
|
+
key: A user-provided string identifier.
|
82
|
+
Required for the artifact to show in the Artifacts page in the UI.
|
83
|
+
The key must only contain lowercase letters, numbers, and dashes.
|
84
|
+
description: A user-specified description of the artifact.
|
85
|
+
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
The table artifact ID.
|
89
|
+
"""
|
90
|
+
formatted_link = f"[{link_text}]({link})" if link_text else f"[{link}]({link})"
|
91
|
+
artifact = await _create_artifact(
|
92
|
+
key=key,
|
93
|
+
type="markdown",
|
94
|
+
description=description,
|
95
|
+
data=formatted_link,
|
96
|
+
)
|
97
|
+
|
98
|
+
return artifact.id
|
99
|
+
|
100
|
+
|
101
|
+
@sync_compatible
|
102
|
+
async def create_markdown_artifact(
|
103
|
+
markdown: str,
|
104
|
+
key: Optional[str] = None,
|
105
|
+
description: Optional[str] = None,
|
106
|
+
) -> UUID:
|
107
|
+
"""
|
108
|
+
Create a markdown artifact.
|
109
|
+
|
110
|
+
Arguments:
|
111
|
+
markdown: The markdown to create.
|
112
|
+
key: A user-provided string identifier.
|
113
|
+
Required for the artifact to show in the Artifacts page in the UI.
|
114
|
+
The key must only contain lowercase letters, numbers, and dashes.
|
115
|
+
description: A user-specified description of the artifact.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
The table artifact ID.
|
119
|
+
"""
|
120
|
+
artifact = await _create_artifact(
|
121
|
+
key=key,
|
122
|
+
type="markdown",
|
123
|
+
description=description,
|
124
|
+
data=markdown,
|
125
|
+
)
|
126
|
+
|
127
|
+
return artifact.id
|
128
|
+
|
129
|
+
|
130
|
+
@sync_compatible
|
131
|
+
async def create_table_artifact(
|
132
|
+
table: Union[Dict[str, List[Any]], List[Dict[str, Any]], List[List[Any]]],
|
133
|
+
key: Optional[str] = None,
|
134
|
+
description: Optional[str] = None,
|
135
|
+
) -> UUID:
|
136
|
+
"""
|
137
|
+
Create a table artifact.
|
138
|
+
|
139
|
+
Arguments:
|
140
|
+
table: The table to create.
|
141
|
+
key: A user-provided string identifier.
|
142
|
+
Required for the artifact to show in the Artifacts page in the UI.
|
143
|
+
The key must only contain lowercase letters, numbers, and dashes.
|
144
|
+
description: A user-specified description of the artifact.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
The table artifact ID.
|
148
|
+
"""
|
149
|
+
|
150
|
+
def _sanitize_nan_values(item):
|
151
|
+
"""
|
152
|
+
Sanitize NaN values in a given item. The item can be a dict, list or float.
|
153
|
+
"""
|
154
|
+
|
155
|
+
if isinstance(item, list):
|
156
|
+
return [_sanitize_nan_values(sub_item) for sub_item in item]
|
157
|
+
|
158
|
+
elif isinstance(item, dict):
|
159
|
+
return {k: _sanitize_nan_values(v) for k, v in item.items()}
|
160
|
+
|
161
|
+
elif isinstance(item, float) and math.isnan(item):
|
162
|
+
return None
|
163
|
+
|
164
|
+
else:
|
165
|
+
return item
|
166
|
+
|
167
|
+
sanitized_table = _sanitize_nan_values(table)
|
168
|
+
|
169
|
+
if isinstance(table, dict) and all(isinstance(v, list) for v in table.values()):
|
170
|
+
pass
|
171
|
+
elif isinstance(table, list) and all(isinstance(v, (list, dict)) for v in table):
|
172
|
+
pass
|
173
|
+
else:
|
174
|
+
raise TypeError(INVALID_TABLE_TYPE_ERROR)
|
175
|
+
|
176
|
+
formatted_table = json.dumps(sanitized_table)
|
177
|
+
|
178
|
+
artifact = await _create_artifact(
|
179
|
+
key=key,
|
180
|
+
type="table",
|
181
|
+
description=description,
|
182
|
+
data=formatted_table,
|
183
|
+
)
|
184
|
+
|
185
|
+
return artifact.id
|
prefect/client/base.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
import copy
|
2
2
|
import sys
|
3
3
|
import threading
|
4
|
+
import uuid
|
4
5
|
from collections import defaultdict
|
5
6
|
from contextlib import asynccontextmanager
|
6
|
-
from
|
7
|
+
from datetime import datetime, timezone
|
7
8
|
from typing import (
|
8
9
|
Any,
|
9
10
|
AsyncGenerator,
|
@@ -11,6 +12,7 @@ from typing import (
|
|
11
12
|
Callable,
|
12
13
|
Dict,
|
13
14
|
MutableMapping,
|
15
|
+
Optional,
|
14
16
|
Protocol,
|
15
17
|
Set,
|
16
18
|
Tuple,
|
@@ -21,10 +23,11 @@ from typing import (
|
|
21
23
|
import anyio
|
22
24
|
import httpx
|
23
25
|
from asgi_lifespan import LifespanManager
|
24
|
-
from httpx import HTTPStatusError, Response
|
26
|
+
from httpx import HTTPStatusError, Request, Response
|
25
27
|
from prefect._vendor.starlette import status
|
26
28
|
from typing_extensions import Self
|
27
29
|
|
30
|
+
from prefect.client.schemas.objects import CsrfToken
|
28
31
|
from prefect.exceptions import PrefectHTTPStatusError
|
29
32
|
from prefect.logging import get_logger
|
30
33
|
from prefect.settings import (
|
@@ -188,9 +191,20 @@ class PrefectHttpxClient(httpx.AsyncClient):
|
|
188
191
|
[Configuring Cloudflare Rate Limiting](https://support.cloudflare.com/hc/en-us/articles/115001635128-Configuring-Rate-Limiting-from-UI)
|
189
192
|
"""
|
190
193
|
|
194
|
+
def __init__(self, *args, enable_csrf_support: bool = False, **kwargs):
|
195
|
+
self.enable_csrf_support: bool = enable_csrf_support
|
196
|
+
self.csrf_token: Optional[str] = None
|
197
|
+
self.csrf_token_expiration: Optional[datetime] = None
|
198
|
+
self.csrf_client_id: uuid.UUID = uuid.uuid4()
|
199
|
+
|
200
|
+
super().__init__(*args, **kwargs)
|
201
|
+
|
191
202
|
async def _send_with_retry(
|
192
203
|
self,
|
193
|
-
request:
|
204
|
+
request: Request,
|
205
|
+
send: Callable[[Request], Awaitable[Response]],
|
206
|
+
send_args: Tuple,
|
207
|
+
send_kwargs: Dict,
|
194
208
|
retry_codes: Set[int] = set(),
|
195
209
|
retry_exceptions: Tuple[Exception, ...] = tuple(),
|
196
210
|
):
|
@@ -207,21 +221,34 @@ class PrefectHttpxClient(httpx.AsyncClient):
|
|
207
221
|
try_count = 0
|
208
222
|
response = None
|
209
223
|
|
224
|
+
is_change_request = request.method.lower() in {"post", "put", "patch", "delete"}
|
225
|
+
|
226
|
+
if self.enable_csrf_support and is_change_request:
|
227
|
+
await self._add_csrf_headers(request=request)
|
228
|
+
|
210
229
|
while try_count <= PREFECT_CLIENT_MAX_RETRIES.value():
|
211
230
|
try_count += 1
|
212
231
|
retry_seconds = None
|
213
232
|
exc_info = None
|
214
233
|
|
215
234
|
try:
|
216
|
-
response = await request
|
235
|
+
response = await send(request, *send_args, **send_kwargs)
|
217
236
|
except retry_exceptions: # type: ignore
|
218
237
|
if try_count > PREFECT_CLIENT_MAX_RETRIES.value():
|
219
238
|
raise
|
220
239
|
# Otherwise, we will ignore this error but capture the info for logging
|
221
240
|
exc_info = sys.exc_info()
|
222
241
|
else:
|
223
|
-
# We got a response;
|
224
|
-
if
|
242
|
+
# We got a response; check if it's a CSRF error, otherwise
|
243
|
+
# return immediately if it is not retryable
|
244
|
+
if (
|
245
|
+
response.status_code == status.HTTP_403_FORBIDDEN
|
246
|
+
and "Invalid CSRF token" in response.text
|
247
|
+
):
|
248
|
+
# We got a CSRF error, clear the token and try again
|
249
|
+
self.csrf_token = None
|
250
|
+
await self._add_csrf_headers(request)
|
251
|
+
elif response.status_code not in retry_codes:
|
225
252
|
return response
|
226
253
|
|
227
254
|
if "Retry-After" in response.headers:
|
@@ -268,19 +295,24 @@ class PrefectHttpxClient(httpx.AsyncClient):
|
|
268
295
|
# We ran out of retries, return the failed response
|
269
296
|
return response
|
270
297
|
|
271
|
-
async def send(self, *args, **kwargs) -> Response:
|
298
|
+
async def send(self, request: Request, *args, **kwargs) -> Response:
|
272
299
|
"""
|
273
300
|
Send a request with automatic retry behavior for the following status codes:
|
274
301
|
|
302
|
+
- 403 Forbidden, if the request failed due to CSRF protection
|
303
|
+
- 408 Request Timeout
|
275
304
|
- 429 CloudFlare-style rate limiting
|
276
305
|
- 502 Bad Gateway
|
277
306
|
- 503 Service unavailable
|
307
|
+
- Any additional status codes provided in `PREFECT_CLIENT_RETRY_EXTRA_CODES`
|
278
308
|
"""
|
279
309
|
|
280
|
-
|
281
|
-
|
310
|
+
super_send = super().send
|
282
311
|
response = await self._send_with_retry(
|
283
|
-
request=
|
312
|
+
request=request,
|
313
|
+
send=super_send,
|
314
|
+
send_args=args,
|
315
|
+
send_kwargs=kwargs,
|
284
316
|
retry_codes={
|
285
317
|
status.HTTP_429_TOO_MANY_REQUESTS,
|
286
318
|
status.HTTP_503_SERVICE_UNAVAILABLE,
|
@@ -312,3 +344,41 @@ class PrefectHttpxClient(httpx.AsyncClient):
|
|
312
344
|
response.raise_for_status()
|
313
345
|
|
314
346
|
return response
|
347
|
+
|
348
|
+
async def _add_csrf_headers(self, request: Request):
|
349
|
+
now = datetime.now(timezone.utc)
|
350
|
+
|
351
|
+
if not self.enable_csrf_support:
|
352
|
+
return
|
353
|
+
|
354
|
+
if not self.csrf_token or (
|
355
|
+
self.csrf_token_expiration and now > self.csrf_token_expiration
|
356
|
+
):
|
357
|
+
token_request = self.build_request(
|
358
|
+
"GET", f"/csrf-token?client={self.csrf_client_id}"
|
359
|
+
)
|
360
|
+
|
361
|
+
try:
|
362
|
+
token_response = await self.send(token_request)
|
363
|
+
except PrefectHTTPStatusError as exc:
|
364
|
+
old_server = exc.response.status_code == status.HTTP_404_NOT_FOUND
|
365
|
+
unconfigured_server = (
|
366
|
+
exc.response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
367
|
+
and "CSRF protection is disabled." in exc.response.text
|
368
|
+
)
|
369
|
+
|
370
|
+
if old_server or unconfigured_server:
|
371
|
+
# The token endpoint is either unavailable, suggesting an
|
372
|
+
# older server, or CSRF protection is disabled. In either
|
373
|
+
# case we should disable CSRF support.
|
374
|
+
self.enable_csrf_support = False
|
375
|
+
return
|
376
|
+
|
377
|
+
raise
|
378
|
+
|
379
|
+
token: CsrfToken = CsrfToken.parse_obj(token_response.json())
|
380
|
+
self.csrf_token = token.token
|
381
|
+
self.csrf_token_expiration = token.expiration
|
382
|
+
|
383
|
+
request.headers["Prefect-Csrf-Token"] = self.csrf_token
|
384
|
+
request.headers["Prefect-Csrf-Client"] = str(self.csrf_client_id)
|
prefect/client/cloud.py
CHANGED
@@ -72,7 +72,7 @@ class CloudClient:
|
|
72
72
|
httpx_settings.setdefault("base_url", host)
|
73
73
|
if not PREFECT_UNIT_TEST_MODE.value():
|
74
74
|
httpx_settings.setdefault("follow_redirects", True)
|
75
|
-
self._client = PrefectHttpxClient(**httpx_settings)
|
75
|
+
self._client = PrefectHttpxClient(**httpx_settings, enable_csrf_support=False)
|
76
76
|
|
77
77
|
async def api_healthcheck(self):
|
78
78
|
"""
|
prefect/client/orchestration.py
CHANGED
@@ -15,6 +15,7 @@ from typing import (
|
|
15
15
|
)
|
16
16
|
from uuid import UUID, uuid4
|
17
17
|
|
18
|
+
import certifi
|
18
19
|
import httpcore
|
19
20
|
import httpx
|
20
21
|
import pendulum
|
@@ -134,8 +135,10 @@ from prefect.settings import (
|
|
134
135
|
PREFECT_API_ENABLE_HTTP2,
|
135
136
|
PREFECT_API_KEY,
|
136
137
|
PREFECT_API_REQUEST_TIMEOUT,
|
138
|
+
PREFECT_API_SSL_CERT_FILE,
|
137
139
|
PREFECT_API_TLS_INSECURE_SKIP_VERIFY,
|
138
140
|
PREFECT_API_URL,
|
141
|
+
PREFECT_CLIENT_CSRF_SUPPORT_ENABLED,
|
139
142
|
PREFECT_CLOUD_API_URL,
|
140
143
|
PREFECT_UNIT_TEST_MODE,
|
141
144
|
)
|
@@ -220,6 +223,11 @@ class PrefectClient:
|
|
220
223
|
|
221
224
|
if PREFECT_API_TLS_INSECURE_SKIP_VERIFY:
|
222
225
|
httpx_settings.setdefault("verify", False)
|
226
|
+
else:
|
227
|
+
cert_file = PREFECT_API_SSL_CERT_FILE.value()
|
228
|
+
if not cert_file:
|
229
|
+
cert_file = certifi.where()
|
230
|
+
httpx_settings.setdefault("verify", cert_file)
|
223
231
|
|
224
232
|
if api_version is None:
|
225
233
|
api_version = SERVER_API_VERSION
|
@@ -316,7 +324,15 @@ class PrefectClient:
|
|
316
324
|
|
317
325
|
if not PREFECT_UNIT_TEST_MODE:
|
318
326
|
httpx_settings.setdefault("follow_redirects", True)
|
319
|
-
|
327
|
+
|
328
|
+
enable_csrf_support = (
|
329
|
+
self.server_type != ServerType.CLOUD
|
330
|
+
and PREFECT_CLIENT_CSRF_SUPPORT_ENABLED.value()
|
331
|
+
)
|
332
|
+
|
333
|
+
self._client = PrefectHttpxClient(
|
334
|
+
**httpx_settings, enable_csrf_support=enable_csrf_support
|
335
|
+
)
|
320
336
|
self._loop = None
|
321
337
|
|
322
338
|
# See https://www.python-httpx.org/advanced/#custom-transports
|
@@ -1632,3 +1632,16 @@ class GlobalConcurrencyLimit(ObjectBaseModel):
|
|
1632
1632
|
" is used as a rate limit."
|
1633
1633
|
),
|
1634
1634
|
)
|
1635
|
+
|
1636
|
+
|
1637
|
+
class CsrfToken(ObjectBaseModel):
|
1638
|
+
token: str = Field(
|
1639
|
+
default=...,
|
1640
|
+
description="The CSRF token",
|
1641
|
+
)
|
1642
|
+
client: str = Field(
|
1643
|
+
default=..., description="The client id associated with the CSRF token"
|
1644
|
+
)
|
1645
|
+
expiration: DateTimeTZ = Field(
|
1646
|
+
default=..., description="The expiration time of the CSRF token"
|
1647
|
+
)
|
prefect/client/subscriptions.py
CHANGED
@@ -74,9 +74,9 @@ class Subscription(Generic[S]):
|
|
74
74
|
auth: Dict[str, Any] = orjson.loads(await websocket.recv())
|
75
75
|
assert auth["type"] == "auth_success", auth.get("message")
|
76
76
|
|
77
|
-
message = {"type": "subscribe", "keys": self.keys}
|
78
|
-
|
79
|
-
|
77
|
+
message = {"type": "subscribe", "keys": self.keys}
|
78
|
+
if self.client_id:
|
79
|
+
message.update({"client_id": self.client_id})
|
80
80
|
|
81
81
|
await websocket.send(orjson.dumps(message).decode())
|
82
82
|
except (
|
prefect/engine.py
CHANGED
@@ -2002,10 +2002,14 @@ async def orchestrate_task_run(
|
|
2002
2002
|
)
|
2003
2003
|
|
2004
2004
|
# Emit an event to capture that the task run was in the `PENDING` state.
|
2005
|
-
last_event =
|
2005
|
+
last_event = emit_task_run_state_change_event(
|
2006
2006
|
task_run=task_run, initial_state=None, validated_state=task_run.state
|
2007
2007
|
)
|
2008
|
-
last_state =
|
2008
|
+
last_state = (
|
2009
|
+
Pending()
|
2010
|
+
if flow_run_context and flow_run_context.autonomous_task_run
|
2011
|
+
else task_run.state
|
2012
|
+
)
|
2009
2013
|
|
2010
2014
|
# Completed states with persisted results should have result data. If it's missing,
|
2011
2015
|
# this could be a manual state transition, so we should use the Unknown result type
|
@@ -2094,7 +2098,7 @@ async def orchestrate_task_run(
|
|
2094
2098
|
break
|
2095
2099
|
|
2096
2100
|
# Emit an event to capture the result of proposing a `RUNNING` state.
|
2097
|
-
last_event =
|
2101
|
+
last_event = emit_task_run_state_change_event(
|
2098
2102
|
task_run=task_run,
|
2099
2103
|
initial_state=last_state,
|
2100
2104
|
validated_state=state,
|
@@ -2187,7 +2191,7 @@ async def orchestrate_task_run(
|
|
2187
2191
|
await _check_task_failure_retriable(task, task_run, terminal_state)
|
2188
2192
|
)
|
2189
2193
|
state = await propose_state(client, terminal_state, task_run_id=task_run.id)
|
2190
|
-
last_event =
|
2194
|
+
last_event = emit_task_run_state_change_event(
|
2191
2195
|
task_run=task_run,
|
2192
2196
|
initial_state=last_state,
|
2193
2197
|
validated_state=state,
|
@@ -2220,7 +2224,7 @@ async def orchestrate_task_run(
|
|
2220
2224
|
)
|
2221
2225
|
# Attempt to enter a running state again
|
2222
2226
|
state = await propose_state(client, Running(), task_run_id=task_run.id)
|
2223
|
-
last_event =
|
2227
|
+
last_event = emit_task_run_state_change_event(
|
2224
2228
|
task_run=task_run,
|
2225
2229
|
initial_state=last_state,
|
2226
2230
|
validated_state=state,
|
@@ -2896,7 +2900,7 @@ async def check_api_reachable(client: PrefectClient, fail_message: str):
|
|
2896
2900
|
API_HEALTHCHECKS[api_url] = get_deadline(60 * 10)
|
2897
2901
|
|
2898
2902
|
|
2899
|
-
def
|
2903
|
+
def emit_task_run_state_change_event(
|
2900
2904
|
task_run: TaskRun,
|
2901
2905
|
initial_state: Optional[State],
|
2902
2906
|
validated_state: State,
|
prefect/events/related.py
CHANGED
prefect/events/schemas.py
CHANGED
@@ -372,9 +372,53 @@ class MetricTrigger(ResourceTrigger):
|
|
372
372
|
)
|
373
373
|
|
374
374
|
|
375
|
-
|
375
|
+
class CompositeTrigger(Trigger, abc.ABC):
|
376
|
+
"""
|
377
|
+
Requires some number of triggers to have fired within the given time period.
|
378
|
+
"""
|
379
|
+
|
380
|
+
type: Literal["compound", "sequence"]
|
381
|
+
triggers: List["TriggerTypes"]
|
382
|
+
within: Optional[timedelta]
|
383
|
+
|
384
|
+
|
385
|
+
class CompoundTrigger(CompositeTrigger):
|
386
|
+
"""A composite trigger that requires some number of triggers to have
|
387
|
+
fired within the given time period"""
|
388
|
+
|
389
|
+
type: Literal["compound"] = "compound"
|
390
|
+
require: Union[int, Literal["any", "all"]]
|
391
|
+
|
392
|
+
@root_validator
|
393
|
+
def validate_require(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
394
|
+
require = values.get("require")
|
395
|
+
|
396
|
+
if isinstance(require, int):
|
397
|
+
if require < 1:
|
398
|
+
raise ValueError("required must be at least 1")
|
399
|
+
if require > len(values["triggers"]):
|
400
|
+
raise ValueError(
|
401
|
+
"required must be less than or equal to the number of triggers"
|
402
|
+
)
|
403
|
+
|
404
|
+
return values
|
405
|
+
|
406
|
+
|
407
|
+
class SequenceTrigger(CompositeTrigger):
|
408
|
+
"""A composite trigger that requires some number of triggers to have fired
|
409
|
+
within the given time period in a specific order"""
|
410
|
+
|
411
|
+
type: Literal["sequence"] = "sequence"
|
412
|
+
|
413
|
+
|
414
|
+
TriggerTypes: TypeAlias = Union[
|
415
|
+
EventTrigger, MetricTrigger, CompoundTrigger, SequenceTrigger
|
416
|
+
]
|
376
417
|
"""The union of all concrete trigger types that a user may actually create"""
|
377
418
|
|
419
|
+
CompoundTrigger.update_forward_refs()
|
420
|
+
SequenceTrigger.update_forward_refs()
|
421
|
+
|
378
422
|
|
379
423
|
class Automation(PrefectBaseModel):
|
380
424
|
"""Defines an action a user wants to take when a certain number of events
|
prefect/flows.py
CHANGED
@@ -1345,8 +1345,12 @@ def flow(
|
|
1345
1345
|
result_serializer: Optional[ResultSerializer] = None,
|
1346
1346
|
cache_result_in_memory: bool = True,
|
1347
1347
|
log_prints: Optional[bool] = None,
|
1348
|
-
on_completion: Optional[
|
1349
|
-
|
1348
|
+
on_completion: Optional[
|
1349
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1350
|
+
] = None,
|
1351
|
+
on_failure: Optional[
|
1352
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1353
|
+
] = None,
|
1350
1354
|
on_cancellation: Optional[
|
1351
1355
|
List[Callable[[FlowSchema, FlowRun, State], None]]
|
1352
1356
|
] = None,
|
@@ -1373,8 +1377,12 @@ def flow(
|
|
1373
1377
|
result_serializer: Optional[ResultSerializer] = None,
|
1374
1378
|
cache_result_in_memory: bool = True,
|
1375
1379
|
log_prints: Optional[bool] = None,
|
1376
|
-
on_completion: Optional[
|
1377
|
-
|
1380
|
+
on_completion: Optional[
|
1381
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1382
|
+
] = None,
|
1383
|
+
on_failure: Optional[
|
1384
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1385
|
+
] = None,
|
1378
1386
|
on_cancellation: Optional[
|
1379
1387
|
List[Callable[[FlowSchema, FlowRun, State], None]]
|
1380
1388
|
] = None,
|
@@ -912,8 +912,7 @@
|
|
912
912
|
"metadata": {
|
913
913
|
"name": "{{ name }}",
|
914
914
|
"annotations": {
|
915
|
-
"run.googleapis.com/launch-stage": "BETA"
|
916
|
-
"run.googleapis.com/vpc-access-connector": "{{ vpc_connector_name }}"
|
915
|
+
"run.googleapis.com/launch-stage": "BETA"
|
917
916
|
}
|
918
917
|
},
|
919
918
|
"spec": {
|
@@ -941,6 +940,11 @@
|
|
941
940
|
"serviceAccountName": "{{ service_account_name }}"
|
942
941
|
}
|
943
942
|
}
|
943
|
+
},
|
944
|
+
"metadata": {
|
945
|
+
"annotations": {
|
946
|
+
"run.googleapis.com/vpc-access-connector": "{{ vpc_connector_name }}"
|
947
|
+
}
|
944
948
|
}
|
945
949
|
}
|
946
950
|
}
|
@@ -1093,8 +1097,10 @@
|
|
1093
1097
|
"launchStage": "{{ launch_stage }}",
|
1094
1098
|
"template": {
|
1095
1099
|
"template": {
|
1100
|
+
"serviceAccount": "{{ service_account_name }}",
|
1096
1101
|
"maxRetries": "{{ max_retries }}",
|
1097
1102
|
"timeout": "{{ timeout }}",
|
1103
|
+
"vpcAccess": "{{ vpc_connector_name }}",
|
1098
1104
|
"containers": [
|
1099
1105
|
{
|
1100
1106
|
"env": [],
|
@@ -1229,6 +1235,12 @@
|
|
1229
1235
|
"title": "VPC Connector Name",
|
1230
1236
|
"description": "The name of the VPC connector to use for the Cloud Run job.",
|
1231
1237
|
"type": "string"
|
1238
|
+
},
|
1239
|
+
"service_account_name": {
|
1240
|
+
"title": "Service Account Name",
|
1241
|
+
"description": "The name of the service account to use for the task execution of Cloud Run Job. By default Cloud Run jobs run as the default Compute Engine Service Account.",
|
1242
|
+
"example": "service-account@example.iam.gserviceaccount.com",
|
1243
|
+
"type": "string"
|
1232
1244
|
}
|
1233
1245
|
},
|
1234
1246
|
"definitions": {
|
prefect/settings.py
CHANGED
@@ -102,7 +102,10 @@ T = TypeVar("T")
|
|
102
102
|
|
103
103
|
DEFAULT_PROFILES_PATH = Path(__file__).parent.joinpath("profiles.toml")
|
104
104
|
|
105
|
-
REMOVED_EXPERIMENTAL_FLAGS = {
|
105
|
+
REMOVED_EXPERIMENTAL_FLAGS = {
|
106
|
+
"PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_SCHEDULING_UI",
|
107
|
+
"PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS",
|
108
|
+
}
|
106
109
|
|
107
110
|
|
108
111
|
class Setting(Generic[T]):
|
@@ -592,6 +595,16 @@ PREFECT_API_TLS_INSECURE_SKIP_VERIFY = Setting(
|
|
592
595
|
This is recommended only during development, e.g. when using self-signed certificates.
|
593
596
|
"""
|
594
597
|
|
598
|
+
PREFECT_API_SSL_CERT_FILE = Setting(
|
599
|
+
str,
|
600
|
+
default=os.environ.get("SSL_CERT_FILE"),
|
601
|
+
)
|
602
|
+
"""
|
603
|
+
This configuration settings option specifies the path to an SSL certificate file.
|
604
|
+
When set, it allows the application to use the specified certificate for secure communication.
|
605
|
+
If left unset, the setting will default to the value provided by the `SSL_CERT_FILE` environment variable.
|
606
|
+
"""
|
607
|
+
|
595
608
|
PREFECT_API_URL = Setting(
|
596
609
|
str,
|
597
610
|
default=None,
|
@@ -657,6 +670,21 @@ A comma-separated list of extra HTTP status codes to retry on. Defaults to an em
|
|
657
670
|
may result in unexpected behavior.
|
658
671
|
"""
|
659
672
|
|
673
|
+
PREFECT_CLIENT_CSRF_SUPPORT_ENABLED = Setting(bool, default=True)
|
674
|
+
"""
|
675
|
+
Determines if CSRF token handling is active in the Prefect client for API
|
676
|
+
requests.
|
677
|
+
|
678
|
+
When enabled (`True`), the client automatically manages CSRF tokens by
|
679
|
+
retrieving, storing, and including them in applicable state-changing requests
|
680
|
+
(POST, PUT, PATCH, DELETE) to the API.
|
681
|
+
|
682
|
+
Disabling this setting (`False`) means the client will not handle CSRF tokens,
|
683
|
+
which might be suitable for environments where CSRF protection is disabled.
|
684
|
+
|
685
|
+
Defaults to `True`, ensuring CSRF protection is enabled by default.
|
686
|
+
"""
|
687
|
+
|
660
688
|
PREFECT_CLOUD_API_URL = Setting(
|
661
689
|
str,
|
662
690
|
default="https://api.prefect.cloud/api",
|
@@ -1207,6 +1235,33 @@ Note this setting only applies when calling `prefect server start`; if hosting t
|
|
1207
1235
|
API with another tool you will need to configure this there instead.
|
1208
1236
|
"""
|
1209
1237
|
|
1238
|
+
PREFECT_SERVER_CSRF_PROTECTION_ENABLED = Setting(bool, default=False)
|
1239
|
+
"""
|
1240
|
+
Controls the activation of CSRF protection for the Prefect server API.
|
1241
|
+
|
1242
|
+
When enabled (`True`), the server enforces CSRF validation checks on incoming
|
1243
|
+
state-changing requests (POST, PUT, PATCH, DELETE), requiring a valid CSRF
|
1244
|
+
token to be included in the request headers or body. This adds a layer of
|
1245
|
+
security by preventing unauthorized or malicious sites from making requests on
|
1246
|
+
behalf of authenticated users.
|
1247
|
+
|
1248
|
+
It is recommended to enable this setting in production environments where the
|
1249
|
+
API is exposed to web clients to safeguard against CSRF attacks.
|
1250
|
+
|
1251
|
+
Note: Enabling this setting requires corresponding support in the client for
|
1252
|
+
CSRF token management. See PREFECT_CLIENT_CSRF_SUPPORT_ENABLED for more.
|
1253
|
+
"""
|
1254
|
+
|
1255
|
+
PREFECT_SERVER_CSRF_TOKEN_EXPIRATION = Setting(timedelta, default=timedelta(hours=1))
|
1256
|
+
"""
|
1257
|
+
Specifies the duration for which a CSRF token remains valid after being issued
|
1258
|
+
by the server.
|
1259
|
+
|
1260
|
+
The default expiration time is set to 1 hour, which offers a reasonable
|
1261
|
+
compromise. Adjust this setting based on your specific security requirements
|
1262
|
+
and usage patterns.
|
1263
|
+
"""
|
1264
|
+
|
1210
1265
|
PREFECT_UI_ENABLED = Setting(
|
1211
1266
|
bool,
|
1212
1267
|
default=True,
|
@@ -1292,12 +1347,12 @@ PREFECT_API_MAX_FLOW_RUN_GRAPH_ARTIFACTS = Setting(int, default=10000)
|
|
1292
1347
|
The maximum number of artifacts to show on a flow run graph on the v2 API
|
1293
1348
|
"""
|
1294
1349
|
|
1295
|
-
PREFECT_EXPERIMENTAL_ENABLE_ARTIFACTS_ON_FLOW_RUN_GRAPH = Setting(bool, default=
|
1350
|
+
PREFECT_EXPERIMENTAL_ENABLE_ARTIFACTS_ON_FLOW_RUN_GRAPH = Setting(bool, default=True)
|
1296
1351
|
"""
|
1297
1352
|
Whether or not to enable artifacts on the flow run graph.
|
1298
1353
|
"""
|
1299
1354
|
|
1300
|
-
PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=
|
1355
|
+
PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=True)
|
1301
1356
|
"""
|
1302
1357
|
Whether or not to enable flow run states on the flow run graph.
|
1303
1358
|
"""
|
@@ -1342,11 +1397,6 @@ PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_CANCELLATION = Setting(bool, default=True)
|
|
1342
1397
|
Whether or not to enable experimental enhanced flow run cancellation.
|
1343
1398
|
"""
|
1344
1399
|
|
1345
|
-
PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS = Setting(bool, default=True)
|
1346
|
-
"""
|
1347
|
-
Whether or not to enable enhanced deployment parameters.
|
1348
|
-
"""
|
1349
|
-
|
1350
1400
|
PREFECT_EXPERIMENTAL_WARN_ENHANCED_CANCELLATION = Setting(bool, default=False)
|
1351
1401
|
"""
|
1352
1402
|
Whether or not to warn when experimental enhanced flow run cancellation is used.
|
@@ -1525,6 +1575,10 @@ PREFECT_EXPERIMENTAL_ENABLE_WORK_QUEUE_STATUS = Setting(bool, default=True)
|
|
1525
1575
|
Whether or not to enable experimental work queue status in-place of work queue health.
|
1526
1576
|
"""
|
1527
1577
|
|
1578
|
+
PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS = Setting(bool, default=False)
|
1579
|
+
"""
|
1580
|
+
Whether or not to enable internal experimental Pydantic v2 behavior.
|
1581
|
+
"""
|
1528
1582
|
|
1529
1583
|
# Defaults -----------------------------------------------------------------------------
|
1530
1584
|
|
prefect/task_server.py
CHANGED
@@ -6,7 +6,7 @@ import socket
|
|
6
6
|
import sys
|
7
7
|
from contextlib import AsyncExitStack
|
8
8
|
from functools import partial
|
9
|
-
from typing import Optional, Type
|
9
|
+
from typing import List, Optional, Type
|
10
10
|
|
11
11
|
import anyio
|
12
12
|
from websockets.exceptions import InvalidStatusCode
|
@@ -15,7 +15,7 @@ from prefect import Task, get_client
|
|
15
15
|
from prefect._internal.concurrency.api import create_call, from_sync
|
16
16
|
from prefect.client.schemas.objects import TaskRun
|
17
17
|
from prefect.client.subscriptions import Subscription
|
18
|
-
from prefect.engine import propose_state
|
18
|
+
from prefect.engine import emit_task_run_state_change_event, propose_state
|
19
19
|
from prefect.logging.loggers import get_logger
|
20
20
|
from prefect.results import ResultFactory
|
21
21
|
from prefect.settings import (
|
@@ -72,7 +72,7 @@ class TaskServer:
|
|
72
72
|
*tasks: Task,
|
73
73
|
task_runner: Optional[Type[BaseTaskRunner]] = None,
|
74
74
|
):
|
75
|
-
self.tasks:
|
75
|
+
self.tasks: List[Task] = tasks
|
76
76
|
|
77
77
|
self.task_runner: BaseTaskRunner = task_runner or ConcurrentTaskRunner()
|
78
78
|
self.started: bool = False
|
@@ -205,6 +205,12 @@ class TaskServer:
|
|
205
205
|
" Task run may have already begun execution."
|
206
206
|
)
|
207
207
|
|
208
|
+
emit_task_run_state_change_event(
|
209
|
+
task_run=task_run,
|
210
|
+
initial_state=task_run.state,
|
211
|
+
validated_state=state,
|
212
|
+
)
|
213
|
+
|
208
214
|
self._runs_task_group.start_soon(
|
209
215
|
partial(
|
210
216
|
submit_autonomous_task_run_to_engine,
|
prefect/tasks.py
CHANGED
@@ -283,13 +283,14 @@ class Task(Generic[P, R]):
|
|
283
283
|
if not hasattr(self.fn, "__qualname__"):
|
284
284
|
self.task_key = to_qualified_name(type(self.fn))
|
285
285
|
else:
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
self.name, os.path.abspath(task_definition_path)
|
286
|
+
try:
|
287
|
+
task_origin_hash = hash_objects(
|
288
|
+
self.name, os.path.abspath(inspect.getsourcefile(self.fn))
|
290
289
|
)
|
291
|
-
|
292
|
-
|
290
|
+
except TypeError:
|
291
|
+
task_origin_hash = "unknown-source-file"
|
292
|
+
|
293
|
+
self.task_key = f"{self.fn.__qualname__}-{task_origin_hash}"
|
293
294
|
|
294
295
|
self.cache_key_fn = cache_key_fn
|
295
296
|
self.cache_expiration = cache_expiration
|
@@ -390,8 +391,12 @@ class Task(Generic[P, R]):
|
|
390
391
|
timeout_seconds: Union[int, float] = None,
|
391
392
|
log_prints: Optional[bool] = NotSet,
|
392
393
|
refresh_cache: Optional[bool] = NotSet,
|
393
|
-
on_completion: Optional[
|
394
|
-
|
394
|
+
on_completion: Optional[
|
395
|
+
List[Callable[["Task", TaskRun, State], Union[Awaitable[None], None]]]
|
396
|
+
] = None,
|
397
|
+
on_failure: Optional[
|
398
|
+
List[Callable[["Task", TaskRun, State], Union[Awaitable[None], None]]]
|
399
|
+
] = None,
|
395
400
|
retry_condition_fn: Optional[Callable[["Task", TaskRun, State], bool]] = None,
|
396
401
|
viz_return_value: Optional[Any] = None,
|
397
402
|
):
|
prefect/utilities/callables.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Utilities for working with Python callables.
|
3
3
|
"""
|
4
|
+
|
4
5
|
import inspect
|
5
6
|
from functools import partial
|
6
7
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
@@ -312,8 +313,9 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
|
|
312
313
|
ParameterSchema: the argument schema
|
313
314
|
"""
|
314
315
|
try:
|
315
|
-
signature = inspect.signature(fn, eval_str=True)
|
316
|
+
signature = inspect.signature(fn, eval_str=True) # novm
|
316
317
|
except (NameError, TypeError):
|
318
|
+
# `eval_str` is not available in Python < 3.10
|
317
319
|
signature = inspect.signature(fn)
|
318
320
|
|
319
321
|
model_fields = {}
|
@@ -2,12 +2,13 @@ prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
|
|
2
2
|
prefect/__init__.py,sha256=FqZ2FacBZq5wr-RnLQphwqXN0atLdu9bDC2YtmZi-AU,5456
|
3
3
|
prefect/_version.py,sha256=fQguBh1dzT7Baahj504O5RrsLlSyg3Zrx42OpgdPnFc,22378
|
4
4
|
prefect/agent.py,sha256=ZHRiQo13SC8F_flWaoWskVopM1DZKgZVwx9kkg_z0A0,27791
|
5
|
+
prefect/artifacts.py,sha256=QLnFkVaBpMQp9fLWkHlayZOUCp2OI6lPmAkUbT-NMLo,5274
|
5
6
|
prefect/context.py,sha256=QK_U3ym-h2i1Y_EOSr4BQeeMN0AIOpG81LQS7k1RiRA,18103
|
6
|
-
prefect/engine.py,sha256=
|
7
|
+
prefect/engine.py,sha256=kWU4jJQm3ApiJiuq3rZysyZZC2zBQc3DdJuYfpydmIE,110127
|
7
8
|
prefect/exceptions.py,sha256=84rpsDLp0cn_v2gE1TnK_NZXh27NJtzgZQtARVKyVEE,10953
|
8
9
|
prefect/filesystems.py,sha256=HkBczs0r69yJQRWsPUVJiU2JKK6NPrkPvSSVIUrvMpQ,36444
|
9
10
|
prefect/flow_runs.py,sha256=mFHLavZk1yZ62H3UazuNDBZWAF7AqKttA4rMcHgsVSw,3119
|
10
|
-
prefect/flows.py,sha256=
|
11
|
+
prefect/flows.py,sha256=CvQ_sGsJNA7zs4i9l4jaZpReon_RznSeK8vZ4TbpSY0,70275
|
11
12
|
prefect/futures.py,sha256=RaWfYIXtH7RsWxQ5QWTTlAzwtVV8XWpXaZT_hLq35vQ,12590
|
12
13
|
prefect/manifests.py,sha256=xfwEEozSEqPK2Lro4dfgdTnjVbQx-aCECNBnf7vO7ZQ,808
|
13
14
|
prefect/plugins.py,sha256=0C-D3-dKi06JZ44XEGmLjCiAkefbE_lKX-g3urzdbQ4,4163
|
@@ -15,12 +16,12 @@ prefect/profiles.toml,sha256=1Tz7nKBDTDXL_6KPJSeB7ok0Vx_aQJ_p0AUmbnzDLzw,39
|
|
15
16
|
prefect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
17
|
prefect/results.py,sha256=FgudRagwoNKVKR5590I4AN0mxgYoyXG_7Q1HVoMXdaU,24731
|
17
18
|
prefect/serializers.py,sha256=sSbe40Ipj-d6VuzBae5k2ao9lkMUZpIXcLtD7f2a7cE,10852
|
18
|
-
prefect/settings.py,sha256=
|
19
|
+
prefect/settings.py,sha256=3xU5DQQglwa7MFnYQ6GqhWHbdIis81Ehnj1fMa7d30M,71067
|
19
20
|
prefect/states.py,sha256=-Ud4AUom3Qu-HQ4hOLvfVZuuF-b_ibaqtzmL7V949Ac,20839
|
20
21
|
prefect/task_engine.py,sha256=_2I7XLwoT_nNhpzTMa_52aQKjsDoaW6WpzwIHYEWZS0,2598
|
21
22
|
prefect/task_runners.py,sha256=HXUg5UqhZRN2QNBqMdGE1lKhwFhT8TaRN75ScgLbnw8,11012
|
22
|
-
prefect/task_server.py,sha256=
|
23
|
-
prefect/tasks.py,sha256=
|
23
|
+
prefect/task_server.py,sha256=6sJAQ6re5ahAHF9IjYkr05mUgefUB6Ga0BeKicgH84A,10532
|
24
|
+
prefect/tasks.py,sha256=AFDCyb0p0r8mamiFMu220-DfGMLSjq-uRi4vL6oxQOE,50477
|
24
25
|
prefect/variables.py,sha256=sk3pfwfPY5lKLt4Qi7OQJPeYipzYip3gidgA9gydcpI,978
|
25
26
|
prefect/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
27
|
prefect/_internal/_logging.py,sha256=HvNHY-8P469o5u4LYEDBTem69XZEt1QUeUaLToijpak,810
|
@@ -30,7 +31,7 @@ prefect/_internal/compatibility/deprecated.py,sha256=5vd4iIzpeGftFdtVaP6PCKNQRiN
|
|
30
31
|
prefect/_internal/compatibility/experimental.py,sha256=bQ2ia6MjgIu1SAWpkGVza87wSz5aTo58X_z3JawqleQ,7442
|
31
32
|
prefect/_internal/concurrency/__init__.py,sha256=ncKwi1NhE3umSFGSKRk9wEVKzN1z1ZD-fmY4EDZHH_U,2142
|
32
33
|
prefect/_internal/concurrency/api.py,sha256=I6OHx53rP7f8GI_O-VHLook1wJfM5Wbe6i2OlAcEcjs,8765
|
33
|
-
prefect/_internal/concurrency/calls.py,sha256=
|
34
|
+
prefect/_internal/concurrency/calls.py,sha256=SVMR1yPTQJtBX095WfRk6cMTq4YKf_L6G77qtaTyN3I,15564
|
34
35
|
prefect/_internal/concurrency/cancellation.py,sha256=eiVsdG5BE_2HCvkQGzIcZ6Gw4ANmchcPRzHsI9wtP3Y,18201
|
35
36
|
prefect/_internal/concurrency/event_loop.py,sha256=rOxUa7e95xP4ionH3o0gRpUzzG6aZMQUituLpMTvTFo,2596
|
36
37
|
prefect/_internal/concurrency/inspection.py,sha256=GWFoSzgs8bZZGNN-Im9sQ-0t0Dqdn8EbwPR1UY3Mhro,3452
|
@@ -38,7 +39,8 @@ prefect/_internal/concurrency/primitives.py,sha256=kxCPD9yLtCeqt-JIHjevL4Zt5FvrF
|
|
38
39
|
prefect/_internal/concurrency/services.py,sha256=aggJd4IUSB6ufppRYdRT-36daEg1JSpJCvK635R8meg,11951
|
39
40
|
prefect/_internal/concurrency/threads.py,sha256=-tReWZL9_XMkRS35SydAfeePH2vqCqb1CGM8lgrKT1I,7846
|
40
41
|
prefect/_internal/concurrency/waiters.py,sha256=DXTD_bbVEUhcTplYQFX8mGmL6nsqJGEDfvS0TmHmIQk,9475
|
41
|
-
prefect/_internal/pydantic/__init__.py,sha256=
|
42
|
+
prefect/_internal/pydantic/__init__.py,sha256=zhbVYT051zywa0rF7Q62jaVFH2D2no3CTCJ1ZXktmR8,482
|
43
|
+
prefect/_internal/pydantic/_compat.py,sha256=YTRAmOTTYybXKJtwsPjee40shpWCtAYlI7RZbPADVO0,6532
|
42
44
|
prefect/_internal/pydantic/schemas.py,sha256=tsRKq5yEIgiRbWMl3BPnbfNaKyDN6pq8WSs0M8SQMm4,452
|
43
45
|
prefect/_internal/pydantic/v2_schema.py,sha256=fySqjMCFoJpRs7wN6c5qoVKePbDbWcXYUoYOs5eFzL0,3485
|
44
46
|
prefect/_internal/pydantic/v2_validated_func.py,sha256=44I4o8jjiS7TYep-E6UYMwjpYH5F1WwJFajW81A3wts,3823
|
@@ -137,17 +139,17 @@ prefect/blocks/notifications.py,sha256=gtr2irqxlvQ5aJTUioG1VfsdSL1xu5e8pWxAYzf49
|
|
137
139
|
prefect/blocks/system.py,sha256=Nlp-3315Hye3FJ5uhDovSPGBIEKi5UbCkAcy3hDxhKk,3057
|
138
140
|
prefect/blocks/webhook.py,sha256=hhyWck7mAPfD_12bl40dJedNC9HIaqs7z13iYcZZ14o,2005
|
139
141
|
prefect/client/__init__.py,sha256=yJ5FRF9RxNUio2V_HmyKCKw5G6CZO0h8cv6xA_Hkpcc,477
|
140
|
-
prefect/client/base.py,sha256=
|
141
|
-
prefect/client/cloud.py,sha256=
|
142
|
+
prefect/client/base.py,sha256=VsJWgaSEyIbHo2MfIkBuErahYwXnU68P3R-n83jx2LI,15211
|
143
|
+
prefect/client/cloud.py,sha256=rrxwmYE9yH4HIewu-xG0HY4P7rwP9gFNitBMYQybcvE,3998
|
142
144
|
prefect/client/collections.py,sha256=I9EgbTg4Fn57gn8vwP_WdDmgnATbx9gfkm2jjhCORjw,1037
|
143
145
|
prefect/client/constants.py,sha256=Z_GG8KF70vbbXxpJuqW5pLnwzujTVeHbcYYRikNmGH0,29
|
144
|
-
prefect/client/orchestration.py,sha256=
|
145
|
-
prefect/client/subscriptions.py,sha256=
|
146
|
+
prefect/client/orchestration.py,sha256=HnBg-i5vrbzhn0KaXgJDbGST5KDsmEv5oJFyWDemHZ0,111199
|
147
|
+
prefect/client/subscriptions.py,sha256=3kqPH3F-CwyrR5wygCpJMjRjM_gcQjd54Qjih6FcLlA,3372
|
146
148
|
prefect/client/utilities.py,sha256=oGU8dJIq7ExEF4WFt-0aSPNX0JP7uH6NmfRlNhfJu00,2660
|
147
149
|
prefect/client/schemas/__init__.py,sha256=KlyqFV-hMulMkNstBn_0ijoHoIwJZaBj6B1r07UmgvE,607
|
148
150
|
prefect/client/schemas/actions.py,sha256=hoJ6q10z6lS-GA2osURjBp-rD0lOzJA2qGu6Opnonjo,25937
|
149
151
|
prefect/client/schemas/filters.py,sha256=r6gnxZREnmE8Glt2SF6vPxHr0SIeiFBjTrrN32cw-Mo,35514
|
150
|
-
prefect/client/schemas/objects.py,sha256=
|
152
|
+
prefect/client/schemas/objects.py,sha256=U3rNUzQspoenwxpb_1b5PcVsX5s2KSNPW9wl1EbGqAE,55562
|
151
153
|
prefect/client/schemas/responses.py,sha256=hErSClfLjt3Ys18YZyZS6dyjVf3eLkiAF6mjEgF4LGg,9344
|
152
154
|
prefect/client/schemas/schedules.py,sha256=ncGWmmBzZvf5G4AL27E0kWGiJxGX-haR2_-GUNvFlv4,14829
|
153
155
|
prefect/client/schemas/sorting.py,sha256=Y-ea8k_vTUKAPKIxqGebwLSXM7x1s5mJ_4-sDd1Ivi8,2276
|
@@ -179,8 +181,8 @@ prefect/events/actions.py,sha256=wYc52xin_CLrNZaou05FdGdLZ5VEhT2lKM_k-MQEJ34,139
|
|
179
181
|
prefect/events/clients.py,sha256=_4_QV6TWnG-dOIXWaudMprxmdjUaqMc8BgZHYdnGuHU,13975
|
180
182
|
prefect/events/filters.py,sha256=vSWHGDCCsi_znQs3gZomCxh-Q498ukn_QHJ7H8q16do,6922
|
181
183
|
prefect/events/instrument.py,sha256=uNiD7AnkfuiwTsCMgNyJURmY9H2tXNfLCb3EC5FL0Qw,3805
|
182
|
-
prefect/events/related.py,sha256=
|
183
|
-
prefect/events/schemas.py,sha256=
|
184
|
+
prefect/events/related.py,sha256=jMsCL6VKgMmMcVF4TXdJxQQRT5sxCuAu6piAxSOJxxs,6746
|
185
|
+
prefect/events/schemas.py,sha256=x1Wy7btsI6RZ5FSBmdMVmU4WD8ZPANGoJ8uDJgW9VJc,19475
|
184
186
|
prefect/events/utilities.py,sha256=gUEJA_kVuYASCqDpGX0HwDW0yczMX0AdgmxXbxhzWbM,2452
|
185
187
|
prefect/events/worker.py,sha256=Z6MZmcCyXZtWi4vEtnFyvnzIEBW7HD14lEH1Crye3gY,2716
|
186
188
|
prefect/infrastructure/__init__.py,sha256=Fm1Rhc4I7ZfJePpUAl1F4iNEtcDugoT650WXXt6xoCM,770
|
@@ -214,7 +216,7 @@ prefect/runtime/__init__.py,sha256=iYmfK1HmXiXXCQK77wDloOqZmY7SFF5iyr37jRzuf-c,4
|
|
214
216
|
prefect/runtime/deployment.py,sha256=UWNXH-3-NNVxLCl5XnDKiofo4a5j8w_42ns1OSQMixg,4751
|
215
217
|
prefect/runtime/flow_run.py,sha256=aFM3e9xqpeZQ4WkvZQXD0lmXu2fNVVVA1etSN3ZI9aE,8444
|
216
218
|
prefect/runtime/task_run.py,sha256=_np3pjBHWkvEtSe-QElEAGwUct629vVx_sahPr-H8gM,3402
|
217
|
-
prefect/server/api/collections_data/views/aggregate-worker-metadata.json,sha256=
|
219
|
+
prefect/server/api/collections_data/views/aggregate-worker-metadata.json,sha256=Gwv5t_RXgqI77MRk5EV0cWVXRrxZ3_wMWxypV1F2ypI,78423
|
218
220
|
prefect/server/api/static/prefect-logo-mark-gradient.png,sha256=ylRjJkI_JHCw8VbQasNnXQHwZW-sH-IQiUGSD3aWP1E,73430
|
219
221
|
prefect/software/__init__.py,sha256=cn7Hesmkv3unA3NynEiyB0Cj2jAzV17yfwjVsS5Ecso,106
|
220
222
|
prefect/software/base.py,sha256=GV6a5RrLx3JaOg1RI44jZTsI_qbqNWbWF3uVO5csnHM,1464
|
@@ -224,7 +226,7 @@ prefect/software/python.py,sha256=reuEJFZPJ5PrDMfK3BuPpYieHNkOXJAyCAaopQcjDqE,17
|
|
224
226
|
prefect/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
225
227
|
prefect/utilities/annotations.py,sha256=p33yhh1Zx8BZUlTtl8gKRbpwWU9FVnZ8cfYrcd5KxDI,3103
|
226
228
|
prefect/utilities/asyncutils.py,sha256=dNVKZKLVdNOhQObPf-224l3uWyKnt1jSFpReMpWFwT4,15674
|
227
|
-
prefect/utilities/callables.py,sha256=
|
229
|
+
prefect/utilities/callables.py,sha256=5G2K_ZAnNoWoY7DqESKpbf4ltF5fkGRlJUvDwBGD7t0,11603
|
228
230
|
prefect/utilities/collections.py,sha256=D_DT489rTCwyzZb021i0xp8osBkkQgSW9XLOoLBzgkg,15436
|
229
231
|
prefect/utilities/compat.py,sha256=mNQZDnzyKaOqy-OV-DnmH_dc7CNF5nQgW_EsA4xMr7g,906
|
230
232
|
prefect/utilities/context.py,sha256=nb_Kui1q9cYK5fLy84baoBzko5-mOToQkd1AnZhwyq8,418
|
@@ -253,8 +255,8 @@ prefect/workers/block.py,sha256=lvKlaWdA-DCCXDX23HHK9M5urEq4x2wmpKtU9ft3a7k,7767
|
|
253
255
|
prefect/workers/process.py,sha256=Kxj_eZYh6R8t8253LYIIafiG7dodCF8RZABwd3Ng_R0,10253
|
254
256
|
prefect/workers/server.py,sha256=WVZJxR8nTMzK0ov0BD0xw5OyQpT26AxlXbsGQ1OrxeQ,1551
|
255
257
|
prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
|
256
|
-
prefect_client-2.16.
|
257
|
-
prefect_client-2.16.
|
258
|
-
prefect_client-2.16.
|
259
|
-
prefect_client-2.16.
|
260
|
-
prefect_client-2.16.
|
258
|
+
prefect_client-2.16.5.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
|
259
|
+
prefect_client-2.16.5.dist-info/METADATA,sha256=A58u55jaNnt77urHhD1faDHXMlL8cClM78JMOFrLwhw,7349
|
260
|
+
prefect_client-2.16.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
261
|
+
prefect_client-2.16.5.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
|
262
|
+
prefect_client-2.16.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|