prefect-client 3.4.7.dev2__py3-none-any.whl → 3.4.7.dev4__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/AGENTS.md +28 -0
- prefect/_build_info.py +3 -3
- prefect/_internal/websockets.py +106 -0
- prefect/client/cloud.py +11 -1
- prefect/client/orchestration/__init__.py +16 -6
- prefect/client/schemas/objects.py +0 -4
- prefect/events/clients.py +2 -76
- prefect/events/schemas/automations.py +1 -1
- prefect/logging/clients.py +347 -0
- prefect/serializers.py +8 -3
- prefect/server/api/logs.py +64 -9
- prefect/settings/models/server/logs.py +28 -0
- prefect/settings/models/server/root.py +5 -0
- prefect/utilities/_ast.py +2 -2
- prefect/utilities/callables.py +1 -1
- {prefect_client-3.4.7.dev2.dist-info → prefect_client-3.4.7.dev4.dist-info}/METADATA +1 -1
- {prefect_client-3.4.7.dev2.dist-info → prefect_client-3.4.7.dev4.dist-info}/RECORD +19 -15
- {prefect_client-3.4.7.dev2.dist-info → prefect_client-3.4.7.dev4.dist-info}/WHEEL +0 -0
- {prefect_client-3.4.7.dev2.dist-info → prefect_client-3.4.7.dev4.dist-info}/licenses/LICENSE +0 -0
prefect/AGENTS.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Core Prefect SDK
|
2
|
+
|
3
|
+
The foundation for building and executing workflows with Python.
|
4
|
+
|
5
|
+
## Key Components
|
6
|
+
|
7
|
+
- **Flows & Tasks**: Workflow definition with `@flow` and `@task` decorators
|
8
|
+
- **States**: Execution status tracking (Pending, Running, Completed, Failed)
|
9
|
+
- **Context**: Runtime information and dependency injection
|
10
|
+
- **Results**: Task output persistence and retrieval
|
11
|
+
- **Deployments**: Packaging flows for scheduled/triggered execution
|
12
|
+
- **Blocks**: Reusable configuration for external systems
|
13
|
+
|
14
|
+
## Main Modules
|
15
|
+
|
16
|
+
- `flows.py` - Flow lifecycle and execution
|
17
|
+
- `tasks.py` - Task definition and dependency resolution
|
18
|
+
- `engine.py` - Core execution engine
|
19
|
+
- `client/` - Server/Cloud API communication
|
20
|
+
- `deployments/` - Deployment management
|
21
|
+
- `blocks/` - Infrastructure and storage blocks
|
22
|
+
|
23
|
+
## SDK-Specific Notes
|
24
|
+
|
25
|
+
- Async-first execution model with sync support
|
26
|
+
- Immutable flow/task definitions after creation
|
27
|
+
- State transitions handle retries and caching
|
28
|
+
- Backward compatibility required for public APIs
|
prefect/_build_info.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Generated by versioningit
|
2
|
-
__version__ = "3.4.7.
|
3
|
-
__build_date__ = "2025-06-
|
4
|
-
__git_commit__ = "
|
2
|
+
__version__ = "3.4.7.dev4"
|
3
|
+
__build_date__ = "2025-06-18 08:09:28.573646+00:00"
|
4
|
+
__git_commit__ = "f2a136e5b5fb96adfde1d9aa44fdedb5b11c97a0"
|
5
5
|
__dirty__ = False
|
@@ -0,0 +1,106 @@
|
|
1
|
+
"""
|
2
|
+
Internal WebSocket proxy utilities for Prefect client connections.
|
3
|
+
|
4
|
+
This module provides shared WebSocket proxy connection logic and SSL configuration
|
5
|
+
to avoid duplication between events and logs clients.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import ssl
|
10
|
+
from typing import Any, Generator, Optional
|
11
|
+
from urllib.parse import urlparse
|
12
|
+
from urllib.request import proxy_bypass
|
13
|
+
|
14
|
+
import certifi
|
15
|
+
from python_socks.async_.asyncio import Proxy
|
16
|
+
from typing_extensions import Self
|
17
|
+
from websockets.asyncio.client import ClientConnection, connect
|
18
|
+
|
19
|
+
from prefect.settings import get_current_settings
|
20
|
+
|
21
|
+
|
22
|
+
def create_ssl_context_for_websocket(uri: str) -> Optional[ssl.SSLContext]:
|
23
|
+
"""Create SSL context for WebSocket connections based on URI scheme."""
|
24
|
+
u = urlparse(uri)
|
25
|
+
|
26
|
+
if u.scheme != "wss":
|
27
|
+
return None
|
28
|
+
|
29
|
+
if get_current_settings().api.tls_insecure_skip_verify:
|
30
|
+
# Create an unverified context for insecure connections
|
31
|
+
ctx = ssl.create_default_context()
|
32
|
+
ctx.check_hostname = False
|
33
|
+
ctx.verify_mode = ssl.CERT_NONE
|
34
|
+
return ctx
|
35
|
+
else:
|
36
|
+
# Create a verified context with the certificate file
|
37
|
+
cert_file = get_current_settings().api.ssl_cert_file
|
38
|
+
if not cert_file:
|
39
|
+
cert_file = certifi.where()
|
40
|
+
return ssl.create_default_context(cafile=cert_file)
|
41
|
+
|
42
|
+
|
43
|
+
class WebsocketProxyConnect(connect):
|
44
|
+
"""
|
45
|
+
WebSocket connection class with proxy and SSL support.
|
46
|
+
|
47
|
+
Extends the websockets.asyncio.client.connect class to add HTTP/HTTPS
|
48
|
+
proxy support via environment variables, proxy bypass logic, and SSL
|
49
|
+
certificate verification.
|
50
|
+
"""
|
51
|
+
|
52
|
+
def __init__(self: Self, uri: str, **kwargs: Any):
|
53
|
+
# super() is intentionally deferred to the _proxy_connect method
|
54
|
+
# to allow for the socket to be established first
|
55
|
+
|
56
|
+
self.uri = uri
|
57
|
+
self._kwargs = kwargs
|
58
|
+
|
59
|
+
u = urlparse(uri)
|
60
|
+
host = u.hostname
|
61
|
+
|
62
|
+
if not host:
|
63
|
+
raise ValueError(f"Invalid URI {uri}, no hostname found")
|
64
|
+
|
65
|
+
if u.scheme == "ws":
|
66
|
+
port = u.port or 80
|
67
|
+
proxy_url = os.environ.get("HTTP_PROXY")
|
68
|
+
elif u.scheme == "wss":
|
69
|
+
port = u.port or 443
|
70
|
+
proxy_url = os.environ.get("HTTPS_PROXY")
|
71
|
+
kwargs["server_hostname"] = host
|
72
|
+
else:
|
73
|
+
raise ValueError(
|
74
|
+
"Unsupported scheme %s. Expected 'ws' or 'wss'. " % u.scheme
|
75
|
+
)
|
76
|
+
|
77
|
+
self._proxy = (
|
78
|
+
Proxy.from_url(proxy_url) if proxy_url and not proxy_bypass(host) else None
|
79
|
+
)
|
80
|
+
self._host = host
|
81
|
+
self._port = port
|
82
|
+
|
83
|
+
# Configure SSL context for HTTPS connections
|
84
|
+
ssl_context = create_ssl_context_for_websocket(uri)
|
85
|
+
if ssl_context:
|
86
|
+
self._kwargs.setdefault("ssl", ssl_context)
|
87
|
+
|
88
|
+
async def _proxy_connect(self: Self) -> ClientConnection:
|
89
|
+
if self._proxy:
|
90
|
+
sock = await self._proxy.connect(
|
91
|
+
dest_host=self._host,
|
92
|
+
dest_port=self._port,
|
93
|
+
)
|
94
|
+
self._kwargs["sock"] = sock
|
95
|
+
|
96
|
+
super().__init__(self.uri, **self._kwargs)
|
97
|
+
proto = await self.__await_impl__()
|
98
|
+
return proto
|
99
|
+
|
100
|
+
def __await__(self: Self) -> Generator[Any, None, ClientConnection]:
|
101
|
+
return self._proxy_connect().__await__()
|
102
|
+
|
103
|
+
|
104
|
+
def websocket_connect(uri: str, **kwargs: Any) -> WebsocketProxyConnect:
|
105
|
+
"""Create a WebSocket connection with proxy and SSL support."""
|
106
|
+
return WebsocketProxyConnect(uri, **kwargs)
|
prefect/client/cloud.py
CHANGED
@@ -24,6 +24,16 @@ from prefect.settings import (
|
|
24
24
|
|
25
25
|
PARSE_API_URL_REGEX = re.compile(r"accounts/(.{36})/workspaces/(.{36})")
|
26
26
|
|
27
|
+
# Cache for TypeAdapter instances to avoid repeated instantiation
|
28
|
+
_TYPE_ADAPTER_CACHE: dict[type, pydantic.TypeAdapter[Any]] = {}
|
29
|
+
|
30
|
+
|
31
|
+
def _get_type_adapter(type_: type) -> pydantic.TypeAdapter[Any]:
|
32
|
+
"""Get or create a cached TypeAdapter for the given type."""
|
33
|
+
if type_ not in _TYPE_ADAPTER_CACHE:
|
34
|
+
_TYPE_ADAPTER_CACHE[type_] = pydantic.TypeAdapter(type_)
|
35
|
+
return _TYPE_ADAPTER_CACHE[type_]
|
36
|
+
|
27
37
|
|
28
38
|
def get_cloud_client(
|
29
39
|
host: Optional[str] = None,
|
@@ -112,7 +122,7 @@ class CloudClient:
|
|
112
122
|
await self.read_workspaces()
|
113
123
|
|
114
124
|
async def read_workspaces(self) -> list[Workspace]:
|
115
|
-
workspaces =
|
125
|
+
workspaces = _get_type_adapter(list[Workspace]).validate_python(
|
116
126
|
await self.get("/me/workspaces")
|
117
127
|
)
|
118
128
|
return workspaces
|
@@ -144,6 +144,16 @@ P = ParamSpec("P")
|
|
144
144
|
R = TypeVar("R", infer_variance=True)
|
145
145
|
T = TypeVar("T")
|
146
146
|
|
147
|
+
# Cache for TypeAdapter instances to avoid repeated instantiation
|
148
|
+
_TYPE_ADAPTER_CACHE: dict[type, pydantic.TypeAdapter[Any]] = {}
|
149
|
+
|
150
|
+
|
151
|
+
def _get_type_adapter(type_: type) -> pydantic.TypeAdapter[Any]:
|
152
|
+
"""Get or create a cached TypeAdapter for the given type."""
|
153
|
+
if type_ not in _TYPE_ADAPTER_CACHE:
|
154
|
+
_TYPE_ADAPTER_CACHE[type_] = pydantic.TypeAdapter(type_)
|
155
|
+
return _TYPE_ADAPTER_CACHE[type_]
|
156
|
+
|
147
157
|
|
148
158
|
@overload
|
149
159
|
def get_client(
|
@@ -635,7 +645,7 @@ class PrefectClient(
|
|
635
645
|
raise prefect.exceptions.ObjectNotFound(http_exc=e) from e
|
636
646
|
else:
|
637
647
|
raise
|
638
|
-
return
|
648
|
+
return _get_type_adapter(list[FlowRun]).validate_python(response.json())
|
639
649
|
|
640
650
|
async def read_work_queue(
|
641
651
|
self,
|
@@ -894,7 +904,7 @@ class PrefectClient(
|
|
894
904
|
"offset": offset,
|
895
905
|
}
|
896
906
|
response = await self._client.post("/task_runs/filter", json=body)
|
897
|
-
return
|
907
|
+
return _get_type_adapter(list[TaskRun]).validate_python(response.json())
|
898
908
|
|
899
909
|
async def delete_task_run(self, task_run_id: UUID) -> None:
|
900
910
|
"""
|
@@ -958,7 +968,7 @@ class PrefectClient(
|
|
958
968
|
response = await self._client.get(
|
959
969
|
"/task_run_states/", params=dict(task_run_id=str(task_run_id))
|
960
970
|
)
|
961
|
-
return
|
971
|
+
return _get_type_adapter(list[prefect.states.State]).validate_python(
|
962
972
|
response.json()
|
963
973
|
)
|
964
974
|
|
@@ -1005,7 +1015,7 @@ class PrefectClient(
|
|
1005
1015
|
else:
|
1006
1016
|
response = await self._client.post("/work_queues/filter", json=json)
|
1007
1017
|
|
1008
|
-
return
|
1018
|
+
return _get_type_adapter(list[WorkQueue]).validate_python(response.json())
|
1009
1019
|
|
1010
1020
|
async def read_worker_metadata(self) -> dict[str, Any]:
|
1011
1021
|
"""Reads worker metadata stored in Prefect collection registry."""
|
@@ -1554,7 +1564,7 @@ class SyncPrefectClient(
|
|
1554
1564
|
"offset": offset,
|
1555
1565
|
}
|
1556
1566
|
response = self._client.post("/task_runs/filter", json=body)
|
1557
|
-
return
|
1567
|
+
return _get_type_adapter(list[TaskRun]).validate_python(response.json())
|
1558
1568
|
|
1559
1569
|
def set_task_run_state(
|
1560
1570
|
self,
|
@@ -1598,6 +1608,6 @@ class SyncPrefectClient(
|
|
1598
1608
|
response = self._client.get(
|
1599
1609
|
"/task_run_states/", params=dict(task_run_id=str(task_run_id))
|
1600
1610
|
)
|
1601
|
-
return
|
1611
|
+
return _get_type_adapter(list[prefect.states.State]).validate_python(
|
1602
1612
|
response.json()
|
1603
1613
|
)
|
@@ -279,10 +279,6 @@ class State(TimeSeriesBaseModel, ObjectBaseModel, Generic[R]):
|
|
279
279
|
if the state is of type `FAILED` and the underlying data is an exception. When flow
|
280
280
|
was run in a different memory space (using `run_deployment`), this will only raise
|
281
281
|
if `fetch` is `True`.
|
282
|
-
fetch: a boolean specifying whether to resolve references to persisted
|
283
|
-
results into data. For synchronous users, this defaults to `True`.
|
284
|
-
For asynchronous users, this defaults to `False` for backwards
|
285
|
-
compatibility.
|
286
282
|
retry_result_failure: a boolean specifying whether to retry on failures to
|
287
283
|
load the result from result storage
|
288
284
|
|
prefect/events/clients.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
import abc
|
2
2
|
import asyncio
|
3
|
-
import os
|
4
|
-
import ssl
|
5
3
|
from datetime import timedelta
|
6
4
|
from types import TracebackType
|
7
5
|
from typing import (
|
@@ -9,7 +7,6 @@ from typing import (
|
|
9
7
|
Any,
|
10
8
|
ClassVar,
|
11
9
|
Dict,
|
12
|
-
Generator,
|
13
10
|
List,
|
14
11
|
MutableMapping,
|
15
12
|
Optional,
|
@@ -17,18 +14,14 @@ from typing import (
|
|
17
14
|
Type,
|
18
15
|
cast,
|
19
16
|
)
|
20
|
-
from urllib.parse import urlparse
|
21
|
-
from urllib.request import proxy_bypass
|
22
17
|
from uuid import UUID
|
23
18
|
|
24
|
-
import certifi
|
25
19
|
import orjson
|
26
20
|
from cachetools import TTLCache
|
27
21
|
from prometheus_client import Counter
|
28
|
-
from python_socks.async_.asyncio import Proxy
|
29
22
|
from typing_extensions import Self
|
30
23
|
from websockets import Subprotocol
|
31
|
-
from websockets.asyncio.client import ClientConnection
|
24
|
+
from websockets.asyncio.client import ClientConnection
|
32
25
|
from websockets.exceptions import (
|
33
26
|
ConnectionClosed,
|
34
27
|
ConnectionClosedError,
|
@@ -36,13 +29,12 @@ from websockets.exceptions import (
|
|
36
29
|
)
|
37
30
|
|
38
31
|
import prefect.types._datetime
|
32
|
+
from prefect._internal.websockets import websocket_connect
|
39
33
|
from prefect.events import Event
|
40
34
|
from prefect.logging import get_logger
|
41
35
|
from prefect.settings import (
|
42
36
|
PREFECT_API_AUTH_STRING,
|
43
37
|
PREFECT_API_KEY,
|
44
|
-
PREFECT_API_SSL_CERT_FILE,
|
45
|
-
PREFECT_API_TLS_INSECURE_SKIP_VERIFY,
|
46
38
|
PREFECT_API_URL,
|
47
39
|
PREFECT_CLOUD_API_URL,
|
48
40
|
PREFECT_DEBUG_MODE,
|
@@ -94,72 +86,6 @@ def events_out_socket_from_api_url(url: str) -> str:
|
|
94
86
|
return http_to_ws(url) + "/events/out"
|
95
87
|
|
96
88
|
|
97
|
-
class WebsocketProxyConnect(connect):
|
98
|
-
def __init__(self: Self, uri: str, **kwargs: Any):
|
99
|
-
# super() is intentionally deferred to the _proxy_connect method
|
100
|
-
# to allow for the socket to be established first
|
101
|
-
|
102
|
-
self.uri = uri
|
103
|
-
self._kwargs = kwargs
|
104
|
-
|
105
|
-
u = urlparse(uri)
|
106
|
-
host = u.hostname
|
107
|
-
|
108
|
-
if not host:
|
109
|
-
raise ValueError(f"Invalid URI {uri}, no hostname found")
|
110
|
-
|
111
|
-
if u.scheme == "ws":
|
112
|
-
port = u.port or 80
|
113
|
-
proxy_url = os.environ.get("HTTP_PROXY")
|
114
|
-
elif u.scheme == "wss":
|
115
|
-
port = u.port or 443
|
116
|
-
proxy_url = os.environ.get("HTTPS_PROXY")
|
117
|
-
kwargs["server_hostname"] = host
|
118
|
-
else:
|
119
|
-
raise ValueError(
|
120
|
-
"Unsupported scheme %s. Expected 'ws' or 'wss'. " % u.scheme
|
121
|
-
)
|
122
|
-
|
123
|
-
self._proxy = (
|
124
|
-
Proxy.from_url(proxy_url) if proxy_url and not proxy_bypass(host) else None
|
125
|
-
)
|
126
|
-
self._host = host
|
127
|
-
self._port = port
|
128
|
-
|
129
|
-
if PREFECT_API_TLS_INSECURE_SKIP_VERIFY and u.scheme == "wss":
|
130
|
-
# Create an unverified context for insecure connections
|
131
|
-
ctx = ssl.create_default_context()
|
132
|
-
ctx.check_hostname = False
|
133
|
-
ctx.verify_mode = ssl.CERT_NONE
|
134
|
-
self._kwargs.setdefault("ssl", ctx)
|
135
|
-
elif u.scheme == "wss":
|
136
|
-
cert_file = PREFECT_API_SSL_CERT_FILE.value()
|
137
|
-
if not cert_file:
|
138
|
-
cert_file = certifi.where()
|
139
|
-
# Create a verified context with the certificate file
|
140
|
-
ctx = ssl.create_default_context(cafile=cert_file)
|
141
|
-
self._kwargs.setdefault("ssl", ctx)
|
142
|
-
|
143
|
-
async def _proxy_connect(self: Self) -> ClientConnection:
|
144
|
-
if self._proxy:
|
145
|
-
sock = await self._proxy.connect(
|
146
|
-
dest_host=self._host,
|
147
|
-
dest_port=self._port,
|
148
|
-
)
|
149
|
-
self._kwargs["sock"] = sock
|
150
|
-
|
151
|
-
super().__init__(self.uri, **self._kwargs)
|
152
|
-
proto = await self.__await_impl__()
|
153
|
-
return proto
|
154
|
-
|
155
|
-
def __await__(self: Self) -> Generator[Any, None, ClientConnection]:
|
156
|
-
return self._proxy_connect().__await__()
|
157
|
-
|
158
|
-
|
159
|
-
def websocket_connect(uri: str, **kwargs: Any) -> WebsocketProxyConnect:
|
160
|
-
return WebsocketProxyConnect(uri, **kwargs)
|
161
|
-
|
162
|
-
|
163
89
|
def get_events_client(
|
164
90
|
reconnection_attempts: int = 10,
|
165
91
|
checkpoint_every: int = 700,
|
@@ -416,7 +416,7 @@ class AutomationCore(PrefectBaseModel, extra="ignore"): # type: ignore[call-arg
|
|
416
416
|
enabled: bool = Field(
|
417
417
|
default=True, description="Whether this automation will be evaluated"
|
418
418
|
)
|
419
|
-
tags:
|
419
|
+
tags: List[str] = Field(
|
420
420
|
default_factory=list,
|
421
421
|
description="A list of tags associated with this automation",
|
422
422
|
)
|
@@ -0,0 +1,347 @@
|
|
1
|
+
import asyncio
|
2
|
+
from datetime import timedelta
|
3
|
+
from types import TracebackType
|
4
|
+
from typing import (
|
5
|
+
TYPE_CHECKING,
|
6
|
+
Any,
|
7
|
+
MutableMapping,
|
8
|
+
Optional,
|
9
|
+
Tuple,
|
10
|
+
Type,
|
11
|
+
cast,
|
12
|
+
)
|
13
|
+
from uuid import UUID
|
14
|
+
|
15
|
+
import orjson
|
16
|
+
from cachetools import TTLCache
|
17
|
+
from prometheus_client import Counter
|
18
|
+
from typing_extensions import Self
|
19
|
+
from websockets import Subprotocol
|
20
|
+
from websockets.asyncio.client import ClientConnection
|
21
|
+
from websockets.exceptions import (
|
22
|
+
ConnectionClosed,
|
23
|
+
ConnectionClosedError,
|
24
|
+
ConnectionClosedOK,
|
25
|
+
)
|
26
|
+
|
27
|
+
from prefect._internal.websockets import (
|
28
|
+
create_ssl_context_for_websocket,
|
29
|
+
websocket_connect,
|
30
|
+
)
|
31
|
+
from prefect.client.schemas.objects import Log
|
32
|
+
from prefect.logging import get_logger
|
33
|
+
from prefect.settings import (
|
34
|
+
PREFECT_API_AUTH_STRING,
|
35
|
+
PREFECT_API_KEY,
|
36
|
+
PREFECT_API_URL,
|
37
|
+
PREFECT_CLOUD_API_URL,
|
38
|
+
PREFECT_SERVER_ALLOW_EPHEMERAL_MODE,
|
39
|
+
)
|
40
|
+
from prefect.types._datetime import now
|
41
|
+
|
42
|
+
if TYPE_CHECKING:
|
43
|
+
import logging
|
44
|
+
|
45
|
+
from prefect.client.schemas.filters import LogFilter
|
46
|
+
|
47
|
+
logger: "logging.Logger" = get_logger(__name__)
|
48
|
+
|
49
|
+
LOGS_OBSERVED = Counter(
|
50
|
+
"prefect_logs_observed",
|
51
|
+
"The number of logs observed by Prefect log subscribers",
|
52
|
+
labelnames=["client"],
|
53
|
+
)
|
54
|
+
LOG_WEBSOCKET_CONNECTIONS = Counter(
|
55
|
+
"prefect_log_websocket_connections",
|
56
|
+
(
|
57
|
+
"The number of times Prefect log clients have connected to a log stream, "
|
58
|
+
"broken down by direction (in/out) and connection (initial/reconnect)"
|
59
|
+
),
|
60
|
+
labelnames=["client", "direction", "connection"],
|
61
|
+
)
|
62
|
+
|
63
|
+
SEEN_LOGS_SIZE = 500_000
|
64
|
+
SEEN_LOGS_TTL = 120
|
65
|
+
|
66
|
+
|
67
|
+
def http_to_ws(url: str) -> str:
|
68
|
+
return url.replace("https://", "wss://").replace("http://", "ws://").rstrip("/")
|
69
|
+
|
70
|
+
|
71
|
+
def logs_out_socket_from_api_url(url: str) -> str:
|
72
|
+
return http_to_ws(url) + "/logs/out"
|
73
|
+
|
74
|
+
|
75
|
+
def _get_api_url_and_key(
|
76
|
+
api_url: Optional[str], api_key: Optional[str]
|
77
|
+
) -> Tuple[str, str]:
|
78
|
+
api_url = api_url or PREFECT_API_URL.value()
|
79
|
+
api_key = api_key or PREFECT_API_KEY.value()
|
80
|
+
|
81
|
+
if not api_url or not api_key:
|
82
|
+
raise ValueError(
|
83
|
+
"api_url and api_key must be provided or set in the Prefect configuration"
|
84
|
+
)
|
85
|
+
|
86
|
+
return api_url, api_key
|
87
|
+
|
88
|
+
|
89
|
+
def get_logs_subscriber(
|
90
|
+
filter: Optional["LogFilter"] = None,
|
91
|
+
reconnection_attempts: int = 10,
|
92
|
+
) -> "PrefectLogsSubscriber":
|
93
|
+
"""
|
94
|
+
Get a logs subscriber based on the current Prefect configuration.
|
95
|
+
|
96
|
+
Similar to get_events_subscriber, this automatically detects whether
|
97
|
+
you're using Prefect Cloud or OSS and returns the appropriate subscriber.
|
98
|
+
"""
|
99
|
+
api_url = PREFECT_API_URL.value()
|
100
|
+
|
101
|
+
if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
|
102
|
+
return PrefectCloudLogsSubscriber(
|
103
|
+
filter=filter, reconnection_attempts=reconnection_attempts
|
104
|
+
)
|
105
|
+
elif api_url:
|
106
|
+
return PrefectLogsSubscriber(
|
107
|
+
api_url=api_url,
|
108
|
+
filter=filter,
|
109
|
+
reconnection_attempts=reconnection_attempts,
|
110
|
+
)
|
111
|
+
elif PREFECT_SERVER_ALLOW_EPHEMERAL_MODE:
|
112
|
+
from prefect.server.api.server import SubprocessASGIServer
|
113
|
+
|
114
|
+
server = SubprocessASGIServer()
|
115
|
+
server.start()
|
116
|
+
return PrefectLogsSubscriber(
|
117
|
+
api_url=server.api_url,
|
118
|
+
filter=filter,
|
119
|
+
reconnection_attempts=reconnection_attempts,
|
120
|
+
)
|
121
|
+
else:
|
122
|
+
raise ValueError(
|
123
|
+
"No Prefect API URL provided. Please set PREFECT_API_URL to the address of a running Prefect server."
|
124
|
+
)
|
125
|
+
|
126
|
+
|
127
|
+
class PrefectLogsSubscriber:
|
128
|
+
"""
|
129
|
+
Subscribes to a Prefect logs stream, yielding logs as they occur.
|
130
|
+
|
131
|
+
Example:
|
132
|
+
|
133
|
+
from prefect.logging.clients import PrefectLogsSubscriber
|
134
|
+
from prefect.client.schemas.filters import LogFilter, LogFilterLevel
|
135
|
+
import logging
|
136
|
+
|
137
|
+
filter = LogFilter(level=LogFilterLevel(ge_=logging.INFO))
|
138
|
+
|
139
|
+
async with PrefectLogsSubscriber(filter=filter) as subscriber:
|
140
|
+
async for log in subscriber:
|
141
|
+
print(log.timestamp, log.level, log.message)
|
142
|
+
|
143
|
+
"""
|
144
|
+
|
145
|
+
_websocket: Optional[ClientConnection]
|
146
|
+
_filter: "LogFilter"
|
147
|
+
_seen_logs: MutableMapping[UUID, bool]
|
148
|
+
|
149
|
+
_api_key: Optional[str]
|
150
|
+
_auth_token: Optional[str]
|
151
|
+
|
152
|
+
def __init__(
|
153
|
+
self,
|
154
|
+
api_url: Optional[str] = None,
|
155
|
+
filter: Optional["LogFilter"] = None,
|
156
|
+
reconnection_attempts: int = 10,
|
157
|
+
):
|
158
|
+
"""
|
159
|
+
Args:
|
160
|
+
api_url: The base URL for a Prefect workspace
|
161
|
+
filter: Log filter to apply
|
162
|
+
reconnection_attempts: When the client is disconnected, how many times
|
163
|
+
the client should attempt to reconnect
|
164
|
+
"""
|
165
|
+
self._api_key = None
|
166
|
+
self._auth_token = PREFECT_API_AUTH_STRING.value()
|
167
|
+
|
168
|
+
if not api_url:
|
169
|
+
api_url = cast(str, PREFECT_API_URL.value())
|
170
|
+
|
171
|
+
from prefect.client.schemas.filters import LogFilter
|
172
|
+
|
173
|
+
self._filter = filter or LogFilter() # type: ignore[call-arg]
|
174
|
+
self._seen_logs = TTLCache(maxsize=SEEN_LOGS_SIZE, ttl=SEEN_LOGS_TTL)
|
175
|
+
|
176
|
+
socket_url = logs_out_socket_from_api_url(api_url)
|
177
|
+
|
178
|
+
logger.debug("Connecting to %s", socket_url)
|
179
|
+
|
180
|
+
# Configure SSL context for the connection
|
181
|
+
ssl_context = create_ssl_context_for_websocket(socket_url)
|
182
|
+
connect_kwargs: dict[str, Any] = {"subprotocols": [Subprotocol("prefect")]}
|
183
|
+
if ssl_context:
|
184
|
+
connect_kwargs["ssl"] = ssl_context
|
185
|
+
|
186
|
+
self._connect = websocket_connect(socket_url, **connect_kwargs)
|
187
|
+
self._websocket = None
|
188
|
+
self._reconnection_attempts = reconnection_attempts
|
189
|
+
if self._reconnection_attempts < 0:
|
190
|
+
raise ValueError("reconnection_attempts must be a non-negative integer")
|
191
|
+
|
192
|
+
@property
|
193
|
+
def client_name(self) -> str:
|
194
|
+
return self.__class__.__name__
|
195
|
+
|
196
|
+
async def __aenter__(self) -> Self:
|
197
|
+
# Don't handle any errors in the initial connection, because these are most
|
198
|
+
# likely a permission or configuration issue that should propagate
|
199
|
+
try:
|
200
|
+
await self._reconnect()
|
201
|
+
finally:
|
202
|
+
LOG_WEBSOCKET_CONNECTIONS.labels(self.client_name, "out", "initial").inc()
|
203
|
+
return self
|
204
|
+
|
205
|
+
async def _reconnect(self) -> None:
|
206
|
+
logger.debug("Reconnecting...")
|
207
|
+
if self._websocket:
|
208
|
+
self._websocket = None
|
209
|
+
await self._connect.__aexit__(None, None, None)
|
210
|
+
|
211
|
+
self._websocket = await self._connect.__aenter__()
|
212
|
+
|
213
|
+
# make sure we have actually connected
|
214
|
+
logger.debug(" pinging...")
|
215
|
+
pong = await self._websocket.ping()
|
216
|
+
await pong
|
217
|
+
|
218
|
+
# Send authentication message - logs WebSocket requires auth handshake
|
219
|
+
auth_token = self._api_key or self._auth_token
|
220
|
+
auth_message = {"type": "auth", "token": auth_token}
|
221
|
+
logger.debug(" authenticating...")
|
222
|
+
await self._websocket.send(orjson.dumps(auth_message).decode())
|
223
|
+
|
224
|
+
# Wait for auth response
|
225
|
+
try:
|
226
|
+
message = orjson.loads(await self._websocket.recv())
|
227
|
+
logger.debug(" auth result %s", message)
|
228
|
+
assert message["type"] == "auth_success", message.get("reason", "")
|
229
|
+
except AssertionError as e:
|
230
|
+
raise Exception(
|
231
|
+
"Unable to authenticate to the log stream. Please ensure the "
|
232
|
+
"provided api_key or auth_token you are using is valid for this environment. "
|
233
|
+
f"Reason: {e.args[0]}"
|
234
|
+
)
|
235
|
+
except ConnectionClosedError as e:
|
236
|
+
reason = getattr(e.rcvd, "reason", None)
|
237
|
+
msg = "Unable to authenticate to the log stream. Please ensure the "
|
238
|
+
msg += "provided api_key or auth_token you are using is valid for this environment. "
|
239
|
+
msg += f"Reason: {reason}" if reason else ""
|
240
|
+
raise Exception(msg) from e
|
241
|
+
|
242
|
+
from prefect.client.schemas.filters import LogFilterTimestamp
|
243
|
+
|
244
|
+
current_time = now("UTC")
|
245
|
+
self._filter.timestamp = LogFilterTimestamp(
|
246
|
+
after_=current_time - timedelta(minutes=1), # type: ignore[arg-type]
|
247
|
+
before_=current_time + timedelta(days=365), # type: ignore[arg-type]
|
248
|
+
)
|
249
|
+
|
250
|
+
logger.debug(" filtering logs since %s...", self._filter.timestamp.after_)
|
251
|
+
filter_message = {
|
252
|
+
"type": "filter",
|
253
|
+
"filter": self._filter.model_dump(mode="json"),
|
254
|
+
}
|
255
|
+
await self._websocket.send(orjson.dumps(filter_message).decode())
|
256
|
+
|
257
|
+
async def __aexit__(
|
258
|
+
self,
|
259
|
+
exc_type: Optional[Type[BaseException]],
|
260
|
+
exc_val: Optional[BaseException],
|
261
|
+
exc_tb: Optional[TracebackType],
|
262
|
+
) -> None:
|
263
|
+
self._websocket = None
|
264
|
+
await self._connect.__aexit__(exc_type, exc_val, exc_tb)
|
265
|
+
|
266
|
+
def __aiter__(self) -> Self:
|
267
|
+
return self
|
268
|
+
|
269
|
+
async def __anext__(self) -> Log:
|
270
|
+
assert self._reconnection_attempts >= 0
|
271
|
+
for i in range(self._reconnection_attempts + 1): # pragma: no branch
|
272
|
+
try:
|
273
|
+
# If we're here and the websocket is None, then we've had a failure in a
|
274
|
+
# previous reconnection attempt.
|
275
|
+
#
|
276
|
+
# Otherwise, after the first time through this loop, we're recovering
|
277
|
+
# from a ConnectionClosed, so reconnect now.
|
278
|
+
if not self._websocket or i > 0:
|
279
|
+
try:
|
280
|
+
await self._reconnect()
|
281
|
+
finally:
|
282
|
+
LOG_WEBSOCKET_CONNECTIONS.labels(
|
283
|
+
self.client_name, "out", "reconnect"
|
284
|
+
).inc()
|
285
|
+
assert self._websocket
|
286
|
+
|
287
|
+
while True:
|
288
|
+
message = orjson.loads(await self._websocket.recv())
|
289
|
+
log: Log = Log.model_validate(message["log"])
|
290
|
+
|
291
|
+
if log.id in self._seen_logs:
|
292
|
+
continue
|
293
|
+
self._seen_logs[log.id] = True
|
294
|
+
|
295
|
+
try:
|
296
|
+
return log
|
297
|
+
finally:
|
298
|
+
LOGS_OBSERVED.labels(self.client_name).inc()
|
299
|
+
|
300
|
+
except ConnectionClosedOK:
|
301
|
+
logger.debug('Connection closed with "OK" status')
|
302
|
+
raise StopAsyncIteration
|
303
|
+
except ConnectionClosed:
|
304
|
+
logger.debug(
|
305
|
+
"Connection closed with %s/%s attempts",
|
306
|
+
i + 1,
|
307
|
+
self._reconnection_attempts,
|
308
|
+
)
|
309
|
+
if i == self._reconnection_attempts:
|
310
|
+
# this was our final chance, raise the most recent error
|
311
|
+
raise
|
312
|
+
|
313
|
+
if i > 2:
|
314
|
+
# let the first two attempts happen quickly in case this is just
|
315
|
+
# a standard load balancer timeout, but after that, just take a
|
316
|
+
# beat to let things come back around.
|
317
|
+
await asyncio.sleep(1)
|
318
|
+
raise StopAsyncIteration
|
319
|
+
|
320
|
+
|
321
|
+
class PrefectCloudLogsSubscriber(PrefectLogsSubscriber):
|
322
|
+
"""Logs subscriber for Prefect Cloud"""
|
323
|
+
|
324
|
+
def __init__(
|
325
|
+
self,
|
326
|
+
api_url: Optional[str] = None,
|
327
|
+
api_key: Optional[str] = None,
|
328
|
+
filter: Optional["LogFilter"] = None,
|
329
|
+
reconnection_attempts: int = 10,
|
330
|
+
):
|
331
|
+
"""
|
332
|
+
Args:
|
333
|
+
api_url: The base URL for a Prefect Cloud workspace
|
334
|
+
api_key: The API key of an actor with the see_flows scope
|
335
|
+
filter: Log filter to apply
|
336
|
+
reconnection_attempts: When the client is disconnected, how many times
|
337
|
+
the client should attempt to reconnect
|
338
|
+
"""
|
339
|
+
api_url, api_key = _get_api_url_and_key(api_url, api_key)
|
340
|
+
|
341
|
+
super().__init__(
|
342
|
+
api_url=api_url,
|
343
|
+
filter=filter,
|
344
|
+
reconnection_attempts=reconnection_attempts,
|
345
|
+
)
|
346
|
+
|
347
|
+
self._api_key = api_key
|
prefect/serializers.py
CHANGED
@@ -38,6 +38,8 @@ from prefect.utilities.pydantic import custom_pydantic_encoder
|
|
38
38
|
|
39
39
|
D = TypeVar("D", default=Any)
|
40
40
|
|
41
|
+
_TYPE_ADAPTER_CACHE: dict[str, TypeAdapter[Any]] = {}
|
42
|
+
|
41
43
|
|
42
44
|
def prefect_json_object_encoder(obj: Any) -> Any:
|
43
45
|
"""
|
@@ -68,9 +70,12 @@ def prefect_json_object_decoder(result: dict[str, Any]) -> Any:
|
|
68
70
|
with `prefect_json_object_encoder`
|
69
71
|
"""
|
70
72
|
if "__class__" in result:
|
71
|
-
|
72
|
-
|
73
|
-
|
73
|
+
class_name = result["__class__"]
|
74
|
+
if class_name not in _TYPE_ADAPTER_CACHE:
|
75
|
+
_TYPE_ADAPTER_CACHE[class_name] = TypeAdapter(
|
76
|
+
from_qualified_name(class_name)
|
77
|
+
)
|
78
|
+
return _TYPE_ADAPTER_CACHE[class_name].validate_python(result["data"])
|
74
79
|
elif "__exc_type__" in result:
|
75
80
|
return from_qualified_name(result["__exc_type__"])(result["message"])
|
76
81
|
else:
|
prefect/server/api/logs.py
CHANGED
@@ -2,14 +2,21 @@
|
|
2
2
|
Routes for interacting with log objects.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from typing import
|
5
|
+
from typing import Optional, Sequence
|
6
6
|
|
7
|
-
from fastapi import Body, Depends, status
|
7
|
+
from fastapi import Body, Depends, WebSocket, status
|
8
|
+
from pydantic import TypeAdapter
|
9
|
+
from starlette.status import WS_1002_PROTOCOL_ERROR
|
8
10
|
|
9
11
|
import prefect.server.api.dependencies as dependencies
|
10
12
|
import prefect.server.models as models
|
11
|
-
import prefect.server.schemas as schemas
|
12
13
|
from prefect.server.database import PrefectDBInterface, provide_database_interface
|
14
|
+
from prefect.server.logs import stream
|
15
|
+
from prefect.server.schemas.actions import LogCreate
|
16
|
+
from prefect.server.schemas.core import Log
|
17
|
+
from prefect.server.schemas.filters import LogFilter
|
18
|
+
from prefect.server.schemas.sorting import LogSort
|
19
|
+
from prefect.server.utilities import subscriptions
|
13
20
|
from prefect.server.utilities.server import PrefectRouter
|
14
21
|
|
15
22
|
router: PrefectRouter = PrefectRouter(prefix="/logs", tags=["Logs"])
|
@@ -17,7 +24,7 @@ router: PrefectRouter = PrefectRouter(prefix="/logs", tags=["Logs"])
|
|
17
24
|
|
18
25
|
@router.post("/", status_code=status.HTTP_201_CREATED)
|
19
26
|
async def create_logs(
|
20
|
-
logs:
|
27
|
+
logs: Sequence[LogCreate],
|
21
28
|
db: PrefectDBInterface = Depends(provide_database_interface),
|
22
29
|
) -> None:
|
23
30
|
"""
|
@@ -30,18 +37,66 @@ async def create_logs(
|
|
30
37
|
await models.logs.create_logs(session=session, logs=batch)
|
31
38
|
|
32
39
|
|
40
|
+
logs_adapter: TypeAdapter[Sequence[Log]] = TypeAdapter(Sequence[Log])
|
41
|
+
|
42
|
+
|
33
43
|
@router.post("/filter")
|
34
44
|
async def read_logs(
|
35
45
|
limit: int = dependencies.LimitBody(),
|
36
46
|
offset: int = Body(0, ge=0),
|
37
|
-
logs:
|
38
|
-
sort:
|
47
|
+
logs: Optional[LogFilter] = None,
|
48
|
+
sort: LogSort = Body(LogSort.TIMESTAMP_ASC),
|
39
49
|
db: PrefectDBInterface = Depends(provide_database_interface),
|
40
|
-
) ->
|
50
|
+
) -> Sequence[Log]:
|
41
51
|
"""
|
42
52
|
Query for logs.
|
43
53
|
"""
|
44
54
|
async with db.session_context() as session:
|
45
|
-
return
|
46
|
-
|
55
|
+
return logs_adapter.validate_python(
|
56
|
+
await models.logs.read_logs(
|
57
|
+
session=session, log_filter=logs, offset=offset, limit=limit, sort=sort
|
58
|
+
)
|
47
59
|
)
|
60
|
+
|
61
|
+
|
62
|
+
@router.websocket("/out")
|
63
|
+
async def stream_logs_out(websocket: WebSocket) -> None:
|
64
|
+
"""Serve a WebSocket to stream live logs"""
|
65
|
+
websocket = await subscriptions.accept_prefect_socket(websocket)
|
66
|
+
if not websocket:
|
67
|
+
return
|
68
|
+
|
69
|
+
try:
|
70
|
+
# After authentication, the next message is expected to be a filter message, any
|
71
|
+
# other type of message will close the connection.
|
72
|
+
message = await websocket.receive_json()
|
73
|
+
|
74
|
+
if message["type"] != "filter":
|
75
|
+
return await websocket.close(
|
76
|
+
WS_1002_PROTOCOL_ERROR, reason="Expected 'filter' message"
|
77
|
+
)
|
78
|
+
|
79
|
+
try:
|
80
|
+
filter = LogFilter.model_validate(message["filter"])
|
81
|
+
except Exception as e:
|
82
|
+
return await websocket.close(
|
83
|
+
WS_1002_PROTOCOL_ERROR, reason=f"Invalid filter: {e}"
|
84
|
+
)
|
85
|
+
|
86
|
+
# No backfill support for logs - only live streaming
|
87
|
+
# Subscribe to the ongoing log stream
|
88
|
+
async with stream.logs(filter) as log_stream:
|
89
|
+
async for log in log_stream:
|
90
|
+
if not log:
|
91
|
+
if await subscriptions.still_connected(websocket):
|
92
|
+
continue
|
93
|
+
break
|
94
|
+
|
95
|
+
await websocket.send_json(
|
96
|
+
{"type": "log", "log": log.model_dump(mode="json")}
|
97
|
+
)
|
98
|
+
|
99
|
+
except subscriptions.NORMAL_DISCONNECT_EXCEPTIONS: # pragma: no cover
|
100
|
+
pass # it's fine if a client disconnects either normally or abnormally
|
101
|
+
|
102
|
+
return None
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import ClassVar
|
4
|
+
|
5
|
+
from pydantic import Field
|
6
|
+
from pydantic_settings import SettingsConfigDict
|
7
|
+
|
8
|
+
from prefect.settings.base import PrefectBaseSettings, build_settings_config
|
9
|
+
|
10
|
+
|
11
|
+
class ServerLogsSettings(PrefectBaseSettings):
|
12
|
+
"""
|
13
|
+
Settings for controlling behavior of the logs subsystem
|
14
|
+
"""
|
15
|
+
|
16
|
+
model_config: ClassVar[SettingsConfigDict] = build_settings_config(
|
17
|
+
("server", "logs")
|
18
|
+
)
|
19
|
+
|
20
|
+
stream_out_enabled: bool = Field(
|
21
|
+
default=False,
|
22
|
+
description="Whether or not to stream logs out to the API via websockets.",
|
23
|
+
)
|
24
|
+
|
25
|
+
stream_publishing_enabled: bool = Field(
|
26
|
+
default=False,
|
27
|
+
description="Whether or not to publish logs to the streaming system.",
|
28
|
+
)
|
@@ -13,6 +13,7 @@ from .deployments import ServerDeploymentsSettings
|
|
13
13
|
from .ephemeral import ServerEphemeralSettings
|
14
14
|
from .events import ServerEventsSettings
|
15
15
|
from .flow_run_graph import ServerFlowRunGraphSettings
|
16
|
+
from .logs import ServerLogsSettings
|
16
17
|
from .services import ServerServicesSettings
|
17
18
|
from .tasks import ServerTasksSettings
|
18
19
|
from .ui import ServerUISettings
|
@@ -127,6 +128,10 @@ class ServerSettings(PrefectBaseSettings):
|
|
127
128
|
default_factory=ServerFlowRunGraphSettings,
|
128
129
|
description="Settings for controlling flow run graph behavior",
|
129
130
|
)
|
131
|
+
logs: ServerLogsSettings = Field(
|
132
|
+
default_factory=ServerLogsSettings,
|
133
|
+
description="Settings for controlling server logs behavior",
|
134
|
+
)
|
130
135
|
services: ServerServicesSettings = Field(
|
131
136
|
default_factory=ServerServicesSettings,
|
132
137
|
description="Settings for controlling server services behavior",
|
prefect/utilities/_ast.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import ast
|
2
2
|
import math
|
3
|
-
from typing import TYPE_CHECKING, Literal
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal
|
4
4
|
|
5
5
|
import anyio
|
6
6
|
from typing_extensions import TypeAlias
|
@@ -17,7 +17,7 @@ OPEN_FILE_SEMAPHORE = LazySemaphore(lambda: math.floor(get_open_file_limit() * 0
|
|
17
17
|
# this potentially could be a TypedDict, but you
|
18
18
|
# need some way to convince the type checker that
|
19
19
|
# Literal["flow_name", "task_name"] are being provided
|
20
|
-
DecoratedFnMetadata: TypeAlias = dict[str,
|
20
|
+
DecoratedFnMetadata: TypeAlias = dict[str, Any]
|
21
21
|
|
22
22
|
|
23
23
|
async def find_prefect_decorated_functions_in_file(
|
prefect/utilities/callables.py
CHANGED
@@ -654,7 +654,7 @@ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str
|
|
654
654
|
and isinstance(func_def.body[0], ast.Expr)
|
655
655
|
and isinstance(func_def.body[0].value, ast.Constant)
|
656
656
|
):
|
657
|
-
return func_def.body[0].value.value
|
657
|
+
return str(func_def.body[0].value.value)
|
658
658
|
return None
|
659
659
|
|
660
660
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
|
2
|
+
prefect/AGENTS.md,sha256=qmCZAuKIF9jQyp5TrW_T8bsM_97-QaiCoQp71A_b2Lg,1008
|
2
3
|
prefect/__init__.py,sha256=iCdcC5ZmeewikCdnPEP6YBAjPNV5dvfxpYCTpw30Hkw,3685
|
3
4
|
prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
|
4
|
-
prefect/_build_info.py,sha256=
|
5
|
+
prefect/_build_info.py,sha256=tffaShFeVAkOpp2FeHJl2rknEfdLB7UQINPTvgTU6a8,185
|
5
6
|
prefect/_result_records.py,sha256=S6QmsODkehGVSzbMm6ig022PYbI6gNKz671p_8kBYx4,7789
|
6
7
|
prefect/_versioning.py,sha256=YqR5cxXrY4P6LM1Pmhd8iMo7v_G2KJpGNdsf4EvDFQ0,14132
|
7
8
|
prefect/_waiters.py,sha256=Ia2ITaXdHzevtyWIgJoOg95lrEXQqNEOquHvw3T33UQ,9026
|
@@ -22,7 +23,7 @@ prefect/plugins.py,sha256=FPRLR2mWVBMuOnlzeiTD9krlHONZH2rtYLD753JQDNQ,2516
|
|
22
23
|
prefect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
24
|
prefect/results.py,sha256=Amm3TQu8U_oakSn__tCogIJ5DsTj0w_kLzuENWsxK6A,36824
|
24
25
|
prefect/schedules.py,sha256=dhq4OhImRvcmtxF7UH1m8RbwYdHT5RQsp_FrxVXfODE,7289
|
25
|
-
prefect/serializers.py,sha256=
|
26
|
+
prefect/serializers.py,sha256=MICSdT_1iL2SSq9cYatJ8T7wqPS97uyw9ew5Fh86-NM,9789
|
26
27
|
prefect/states.py,sha256=rh7l1bnIYpTXdlXt5nnpz66y9KLjBWAJrN9Eo5RwgQs,26023
|
27
28
|
prefect/task_engine.py,sha256=j7i_UiLvijV4Vut1Bw5-72kSlOqAPxqeS7-3cMVEBPA,65509
|
28
29
|
prefect/task_runners.py,sha256=ptgE5wuXg_IVHM0j7d6l7ELAVg3SXSy4vggnoHRF8dA,17040
|
@@ -44,6 +45,7 @@ prefect/_internal/integrations.py,sha256=U4cZMDbnilzZSKaMxvzZcSL27a1tzRMjDoTfr2u
|
|
44
45
|
prefect/_internal/pytz.py,sha256=Sy_cD-Hkmo_Yrhx2Jucy7DgTRhvO8ZD0whW1ywbSg_U,13765
|
45
46
|
prefect/_internal/retries.py,sha256=pMHofrTQPDSxbVWclDwXbfhFKaDC6sxe1DkUOWugV6k,3040
|
46
47
|
prefect/_internal/uuid7.py,sha256=yvndhibNDrqnYrG-qUncas4XQp8bKVbmM8XfF7JrjJI,4203
|
48
|
+
prefect/_internal/websockets.py,sha256=CloIdusf2Bbefdit46pT91cVDudeYtztPI-MmqSnuLI,3466
|
47
49
|
prefect/_internal/compatibility/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
50
|
prefect/_internal/compatibility/async_dispatch.py,sha256=cUXOqSeseMUaje9oYUzasVPtNttyiHvrqfJl0zK66XI,2949
|
49
51
|
prefect/_internal/compatibility/blocks.py,sha256=SSZXoWVuCMYu1EzjqmTa4lKjDCyxvOFK47XMj6s4hsk,984
|
@@ -84,12 +86,12 @@ prefect/blocks/system.py,sha256=4KiUIy5zizMqfGJrxvi9GLRLcMj4BjAXARxCUEmgbKI,5041
|
|
84
86
|
prefect/blocks/webhook.py,sha256=xylFigbDOsn-YzxahkTzNqYwrIA7wwS6204P0goLY3A,2907
|
85
87
|
prefect/client/__init__.py,sha256=bDeOC_I8_la5dwCAfxKzYSTSAr2tlq5HpxJgVoCCdAs,675
|
86
88
|
prefect/client/base.py,sha256=7VAMyoy8KtmtI-H8KYsI16_uw9TlrXSrcxChFuMp65Q,26269
|
87
|
-
prefect/client/cloud.py,sha256=
|
89
|
+
prefect/client/cloud.py,sha256=v1UO5YUF3kP6u5I1SKHe5DfpcVXB1_xc1rxr6P9-5DY,6927
|
88
90
|
prefect/client/collections.py,sha256=t9XkVU_onQMZ871L21F1oZnAiPSQeeVfd_MuDEBS3iM,1050
|
89
91
|
prefect/client/constants.py,sha256=Z_GG8KF70vbbXxpJuqW5pLnwzujTVeHbcYYRikNmGH0,29
|
90
92
|
prefect/client/subscriptions.py,sha256=PTYi1Pp7rX-aGdcxZkxRBZkZnpzBt1P17APsm05EDR8,4376
|
91
93
|
prefect/client/utilities.py,sha256=UEJD6nwYg2mD8-GSmru-E2ofXaBlmSFZ2-8T_5rIK6c,3472
|
92
|
-
prefect/client/orchestration/__init__.py,sha256=
|
94
|
+
prefect/client/orchestration/__init__.py,sha256=lG3IW4XfbBkgZyPiSrxeyBhTqt3YfFcBHnEXDVrNmLs,56220
|
93
95
|
prefect/client/orchestration/base.py,sha256=HM6ryHBZSzuHoCFQM9u5qR5k1dN9Bbr_ah6z1UPNbZQ,1542
|
94
96
|
prefect/client/orchestration/routes.py,sha256=_-HC-EmgMhsYdmGwZTxIXlINaVzYuX7RZAvzjHbVp-4,4266
|
95
97
|
prefect/client/orchestration/_artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -119,7 +121,7 @@ prefect/client/orchestration/_work_pools/client.py,sha256=s1DfUQQBgB2sLiVVPhLNTl
|
|
119
121
|
prefect/client/schemas/__init__.py,sha256=InZcDzdeWA2oaV0TlyvoMcyLcbi_aaqU1U9D6Gx-eoU,2747
|
120
122
|
prefect/client/schemas/actions.py,sha256=E46Mdq7vAq8hhYmMj6zqUF20uAPXZricViZcIYmgEf0,32443
|
121
123
|
prefect/client/schemas/filters.py,sha256=qa--NNZduuSOcL1xw-YMd4FVIKMrDnBwPPY4m5Di0GA,35963
|
122
|
-
prefect/client/schemas/objects.py,sha256=
|
124
|
+
prefect/client/schemas/objects.py,sha256=JYcHShcR4JUBjc1VrsMaJ0QYd3H9peXbXtT9U4Lhkc8,57708
|
123
125
|
prefect/client/schemas/responses.py,sha256=Zdcx7jlIaluEa2uYIOE5mK1HsJvWPErRAcaWM20oY_I,17336
|
124
126
|
prefect/client/schemas/schedules.py,sha256=sxLFk0SmFY7X1Y9R9HyGDqOS3U5NINBWTciUU7vTTic,14836
|
125
127
|
prefect/client/schemas/sorting.py,sha256=L-2Mx-igZPtsUoRUguTcG3nIEstMEMPD97NwPM2Ox5s,2579
|
@@ -153,7 +155,7 @@ prefect/docker/__init__.py,sha256=z6wdc6UFfiBG2jb9Jk64uCWVM04JKVWeVyDWwuuon8M,52
|
|
153
155
|
prefect/docker/docker_image.py,sha256=bR_pEq5-FDxlwTj8CP_7nwZ_MiGK6KxIi8v7DRjy1Kg,3138
|
154
156
|
prefect/events/__init__.py,sha256=GtKl2bE--pJduTxelH2xy7SadlLJmmis8WR1EYixhuA,2094
|
155
157
|
prefect/events/actions.py,sha256=A7jS8bo4zWGnrt3QfSoQs0uYC1xfKXio3IfU0XtTb5s,9129
|
156
|
-
prefect/events/clients.py,sha256=
|
158
|
+
prefect/events/clients.py,sha256=pvCbvPcehDhaFEJfeu1DzUP6RhBhacKU7L5Z4XPSvIE,25132
|
157
159
|
prefect/events/filters.py,sha256=tnAbA4Z0Npem8Jbin-qqe38K_4a-4YdpU-Oc4u8Y95Q,8697
|
158
160
|
prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
|
159
161
|
prefect/events/utilities.py,sha256=ww34bTMENCNwcp6RhhgzG0KgXOvKGe0MKmBdSJ8NpZY,3043
|
@@ -161,7 +163,7 @@ prefect/events/worker.py,sha256=HjbibR0_J1W1nnNMZDFTXAbB0cl_cFGaFI87DvNGcnI,4557
|
|
161
163
|
prefect/events/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
162
164
|
prefect/events/cli/automations.py,sha256=uCX3NnypoI25TmyAoyL6qYhanWjZbJ2watwv1nfQMxs,11513
|
163
165
|
prefect/events/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
164
|
-
prefect/events/schemas/automations.py,sha256=
|
166
|
+
prefect/events/schemas/automations.py,sha256=GVAfgyNoTxr8NpEw_Ao-1Prfd_MSsrhrLsXv6SLKUdY,14775
|
165
167
|
prefect/events/schemas/deployment_triggers.py,sha256=OX9g9eHe0nqJ3PtVEzqs9Ub2LaOHMA4afLZSvSukKGU,3191
|
166
168
|
prefect/events/schemas/events.py,sha256=r8sSx2Q1A0KIofnZR_Bri7YT1wzXKV3YS-LnxpeIXHE,9270
|
167
169
|
prefect/events/schemas/labelling.py,sha256=bU-XYaHXhI2MEBIHngth96R9D02m8HHb85KNcHZ_1Gc,3073
|
@@ -181,6 +183,7 @@ prefect/locking/filesystem.py,sha256=PxC9ndDbo59-gBEx9jtKad4T-Jav0srJSM9vYGzvQwE
|
|
181
183
|
prefect/locking/memory.py,sha256=EFQnhAO94jEy4TyS880DbsJ42CHT5WNuNc6Wj8dYrKc,7842
|
182
184
|
prefect/locking/protocol.py,sha256=RsfvlaHTTEJ0YvYWSqFGoZuT2w4FPPxyQlHqjoyNGuE,4240
|
183
185
|
prefect/logging/__init__.py,sha256=DpRZzZeWeiDHFlMDEQdknRzbxpL0ObFh5IqqS9iaZwQ,170
|
186
|
+
prefect/logging/clients.py,sha256=nKEv-Xfzy5QwtFmZvNirBI8G5YraZ9YzuIkKHb4XVXM,11825
|
184
187
|
prefect/logging/configuration.py,sha256=go9lA4W5HMpK6azDz_ez2YqgQ2b3aCFXxJH-AopoHy8,3404
|
185
188
|
prefect/logging/filters.py,sha256=NnRYubh9dMmWcCAjuW32cIVQ37rLxdn8ci26wTtQMyU,1136
|
186
189
|
prefect/logging/formatters.py,sha256=Sum42BmYZ7mns64jSOy4OA_K8KudEZjeG2h7SZcY9mA,4167
|
@@ -218,7 +221,7 @@ prefect/server/api/events.py,sha256=mUTv5ZNxiRsEOpzq8fpfCkLpPasjt-ROUAowA5eFbDE,
|
|
218
221
|
prefect/server/api/flow_run_states.py,sha256=lIdxVE9CqLgtDCuH9bTaKkzHNL81FPrr11liPzvONrw,1661
|
219
222
|
prefect/server/api/flow_runs.py,sha256=Lmb165fLbN4DioxjxgDYaAJ5Qxj771iRYaqn-hYq9KM,33744
|
220
223
|
prefect/server/api/flows.py,sha256=Bz0ISh-9oY0W1X3mqA631_8678pQ6tuRGMpSgWAfxOc,7018
|
221
|
-
prefect/server/api/logs.py,sha256=
|
224
|
+
prefect/server/api/logs.py,sha256=O0W9jomHQuWF7XPMPOhW2p5Uidss6ssMqmKKwMGiv7Y,3526
|
222
225
|
prefect/server/api/middleware.py,sha256=WkyuyeJIfo9Q0GAIVU5gO6yIGNVwoHwuBah5AB5oUyw,2733
|
223
226
|
prefect/server/api/root.py,sha256=CeumFYIM_BDvPicJH9ry5PO_02PZTLeMqbLMGGTh90o,942
|
224
227
|
prefect/server/api/run_history.py,sha256=EW-GTPxZAQ5zXiAqHzmS-iAN_Bn6ZSgVQksDT-ZTsyc,5995
|
@@ -270,7 +273,8 @@ prefect/settings/models/server/deployments.py,sha256=LjWQr2U1mjItYhuuLqMT_QQ7P4K
|
|
270
273
|
prefect/settings/models/server/ephemeral.py,sha256=rh8Py5Nxh-gq9KgfB7CDnIgT_nuOuv59OrLGuhMIGmk,1043
|
271
274
|
prefect/settings/models/server/events.py,sha256=9rdlbLz9SIg_easm1UcFTfX1seS935Xtv5d9y3r39Eo,5578
|
272
275
|
prefect/settings/models/server/flow_run_graph.py,sha256=PuAZqqdu6fzvrbUgXZzyntUH_Ii_bP7qezgcgvW7ULk,1146
|
273
|
-
prefect/settings/models/server/
|
276
|
+
prefect/settings/models/server/logs.py,sha256=tk6tzZS2pAHcAA55Ko-WaIbYz88sUGSGESvZHjIzv9Q,756
|
277
|
+
prefect/settings/models/server/root.py,sha256=9z58934Yqudf8N3-aRCIL73GPsfs3dKuTt-E9ECaxB4,5409
|
274
278
|
prefect/settings/models/server/services.py,sha256=Mb71MG5I1hPlCaJ54vNmHgU7Rxde2x8QeDQl9a8cGU4,18998
|
275
279
|
prefect/settings/models/server/tasks.py,sha256=_CaOUfh3WDXvUhmHXmR-MkTRaQqocZck4efmX74iOg8,2976
|
276
280
|
prefect/settings/models/server/ui.py,sha256=hShsi4rPBtdJA2WnT1Er0tWqu-e5wUum8NkNgucShkk,1867
|
@@ -286,13 +290,13 @@ prefect/types/_datetime.py,sha256=_N3eAMhYlwSEubMQlfeTGxLJHn2jRFPrNPxkod21B_s,75
|
|
286
290
|
prefect/types/entrypoint.py,sha256=2FF03-wLPgtnqR_bKJDB2BsXXINPdu8ptY9ZYEZnXg8,328
|
287
291
|
prefect/types/names.py,sha256=dGXNrP9nibQTm4hOBOpaQebKm3Avf3OGM5MH4M5BUKc,4013
|
288
292
|
prefect/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
289
|
-
prefect/utilities/_ast.py,sha256=
|
293
|
+
prefect/utilities/_ast.py,sha256=IE_XGZAfkd_C7Rl6MvvNC4kGSbbqIFAbYa5S2PPks-U,4910
|
290
294
|
prefect/utilities/_deprecated.py,sha256=b3pqRSoFANdVJAc8TJkygBcP-VjZtLJUxVIWC7kwspI,1303
|
291
295
|
prefect/utilities/_engine.py,sha256=9GW4X1lyAbmPwCuXXIubVJ7Z0DMT3dykkEUtp9tm5hI,3356
|
292
296
|
prefect/utilities/_git.py,sha256=bPYWQdr9xvH0BqxR1ll1RkaSb3x0vhwylhYD5EilkKU,863
|
293
297
|
prefect/utilities/annotations.py,sha256=0Elqgq6LR7pQqezNqT5wb6U_0e2pDO_zx6VseVL6kL8,4396
|
294
298
|
prefect/utilities/asyncutils.py,sha256=xcfeNym2j3WH4gKXznON2hI1PpUTcwr_BGc16IQS3C4,19789
|
295
|
-
prefect/utilities/callables.py,sha256=
|
299
|
+
prefect/utilities/callables.py,sha256=57adLaN2QGJEE0YCdv1jS1L5R3vi4IuzPiNVZ7cCcEk,25930
|
296
300
|
prefect/utilities/collections.py,sha256=c3nPLPWqIZQQdNuHs_nrbQJwuhQSX4ivUl-h9LtzXto,23243
|
297
301
|
prefect/utilities/compat.py,sha256=nnPA3lf2f4Y-l645tYFFNmj5NDPaYvjqa9pbGKZ3WKE,582
|
298
302
|
prefect/utilities/context.py,sha256=23SDMgdt07SjmB1qShiykHfGgiv55NBzdbMXM3fE9CI,1447
|
@@ -325,7 +329,7 @@ prefect/workers/cloud.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
325
329
|
prefect/workers/process.py,sha256=Yi5D0U5AQ51wHT86GdwtImXSefe0gJf3LGq4r4z9zwM,11090
|
326
330
|
prefect/workers/server.py,sha256=2pmVeJZiVbEK02SO6BEZaBIvHMsn6G8LzjW8BXyiTtk,1952
|
327
331
|
prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
|
328
|
-
prefect_client-3.4.7.
|
329
|
-
prefect_client-3.4.7.
|
330
|
-
prefect_client-3.4.7.
|
331
|
-
prefect_client-3.4.7.
|
332
|
+
prefect_client-3.4.7.dev4.dist-info/METADATA,sha256=OkzXvCsiyz1Qs2jMNsixg3GzEMIuQLwmn0ZYSQaPraY,7517
|
333
|
+
prefect_client-3.4.7.dev4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
334
|
+
prefect_client-3.4.7.dev4.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
|
335
|
+
prefect_client-3.4.7.dev4.dist-info/RECORD,,
|
File without changes
|
{prefect_client-3.4.7.dev2.dist-info → prefect_client-3.4.7.dev4.dist-info}/licenses/LICENSE
RENAMED
File without changes
|