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 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.dev2"
3
- __build_date__ = "2025-06-13 08:09:11.132645+00:00"
4
- __git_commit__ = "5486631396a72284b1035fd98e50dc6c837d2cd0"
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 = pydantic.TypeAdapter(list[Workspace]).validate_python(
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 pydantic.TypeAdapter(list[FlowRun]).validate_python(response.json())
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 pydantic.TypeAdapter(list[TaskRun]).validate_python(response.json())
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 pydantic.TypeAdapter(list[prefect.states.State]).validate_python(
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 pydantic.TypeAdapter(list[WorkQueue]).validate_python(response.json())
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 pydantic.TypeAdapter(list[TaskRun]).validate_python(response.json())
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 pydantic.TypeAdapter(list[prefect.states.State]).validate_python(
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, connect
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: list[str] = Field(
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
- return TypeAdapter(from_qualified_name(result["__class__"])).validate_python(
72
- result["data"]
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:
@@ -2,14 +2,21 @@
2
2
  Routes for interacting with log objects.
3
3
  """
4
4
 
5
- from typing import List
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: List[schemas.actions.LogCreate],
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: schemas.filters.LogFilter = None,
38
- sort: schemas.sorting.LogSort = Body(schemas.sorting.LogSort.TIMESTAMP_ASC),
47
+ logs: Optional[LogFilter] = None,
48
+ sort: LogSort = Body(LogSort.TIMESTAMP_ASC),
39
49
  db: PrefectDBInterface = Depends(provide_database_interface),
40
- ) -> List[schemas.core.Log]:
50
+ ) -> Sequence[Log]:
41
51
  """
42
52
  Query for logs.
43
53
  """
44
54
  async with db.session_context() as session:
45
- return await models.logs.read_logs(
46
- session=session, log_filter=logs, offset=offset, limit=limit, sort=sort
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, str]
20
+ DecoratedFnMetadata: TypeAlias = dict[str, Any]
21
21
 
22
22
 
23
23
  async def find_prefect_decorated_functions_in_file(
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.4.7.dev2
3
+ Version: 3.4.7.dev4
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://docs.prefect.io
@@ -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=p-1WZT8sHGg-wrREDK4_ct9XaOJZFHNK6WNakLCaTSw,185
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=lU9A1rGEfAfhr8nTl3rf-K7ED78QNShXOrmRBhgNk3Y,9566
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=jnFgg0osMVDGbLjdWkDX3rQg_0pI_zvfSlU480XCWGQ,6523
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=DPbazZvQDgoSZipuNk4z_AILgJbM6zBld-1OsVH55ME,55831
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=6rR9ccLJ4f1Hw0J8ywzgX2L3FRw8--XCQX9blBrE7R8,57984
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=r_C3ZevVYUzIW53CpmFbEtR2DwhYeYB4budtB3GaYl0,27625
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=U3wNJtaxz42qi-X4n3SX2AAyqaNk9jX3m01E5cn4OyQ,14775
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=0z78tM2B5sRgJWYRWJn5lHhRoLtZB_OU3C-uALV8tOs,1571
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/root.py,sha256=Dk_Zx4eGUy1h2cAetDKphnd6TWhDrK6DHOLJxdP7e1Y,5215
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=sgEPUWElih-3cp4PoAy1IOyPtu8E27lL0Dldf3ijnYY,4905
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=HcXA3_Stb8CBtp074SuFKuMy-ge2KW89X5towbzGjaY,25925
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.dev2.dist-info/METADATA,sha256=yhvRf0gZS7dNvoTtnT_QQ52HsH68jUAWDdCaGSHaHnM,7517
329
- prefect_client-3.4.7.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
330
- prefect_client-3.4.7.dev2.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
331
- prefect_client-3.4.7.dev2.dist-info/RECORD,,
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,,