prefect-client 3.4.7.dev3__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 CHANGED
@@ -1,38 +1,28 @@
1
- # Core Prefect Library
1
+ # Core Prefect SDK
2
2
 
3
- This is the core Prefect library containing the fundamental building blocks for workflow orchestration.
3
+ The foundation for building and executing workflows with Python.
4
4
 
5
- ## Core Concepts
5
+ ## Key Components
6
6
 
7
- - **Flows**: Top-level workflow containers defined with `@flow` decorator
8
- - **Tasks**: Individual units of work defined with `@task` decorator
9
- - **States**: Represent the status of flow/task runs (Pending, Running, Completed, Failed, etc.)
10
- - **Context**: Runtime information available during flow/task execution
11
- - **Results**: Persistence layer for task outputs and artifacts
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
12
13
 
13
- ## Key Modules
14
+ ## Main Modules
14
15
 
15
- - `flows.py` - Flow definition, execution, and lifecycle management
16
- - `tasks.py` - Task definition, execution, and dependency resolution
17
- - `states.py` - State management and transitions
18
- - `context.py` - Runtime context and dependency injection
16
+ - `flows.py` - Flow lifecycle and execution
17
+ - `tasks.py` - Task definition and dependency resolution
19
18
  - `engine.py` - Core execution engine
20
- - `client/` - Client interfaces for communicating with Prefect server/cloud
21
- - `runner/` - Process management for running flows
22
- - `deployments/` - Deployment creation and management
23
- - `blocks/` - Reusable configuration and infrastructure components
19
+ - `client/` - Server/Cloud API communication
20
+ - `deployments/` - Deployment management
21
+ - `blocks/` - Infrastructure and storage blocks
24
22
 
25
- ## Execution Architecture
23
+ ## SDK-Specific Notes
26
24
 
27
- Prefect uses an asynchronous execution model:
28
- - Flows and tasks are async by default but support sync execution
29
- - Task dependencies are resolved through return value tracking
30
- - State management handles retries, caching, and failure policies
31
- - Results are persisted and retrievable across runs
32
-
33
- ## Development Patterns
34
-
35
- - Prefer composition over inheritance for extensibility
36
- - Use dependency injection through context for runtime services
37
- - Maintain backward compatibility in public APIs
38
- - Task and flow definitions are immutable after creation
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.dev3"
3
- __build_date__ = "2025-06-17 08:09:28.549090+00:00"
4
- __git_commit__ = "a40dd41d3b8dc001c893d5a5d778016da797aa82"
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/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,
@@ -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
@@ -2,16 +2,20 @@
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
7
  from fastapi import Body, Depends, WebSocket, status
8
+ from pydantic import TypeAdapter
8
9
  from starlette.status import WS_1002_PROTOCOL_ERROR
9
10
 
10
11
  import prefect.server.api.dependencies as dependencies
11
12
  import prefect.server.models as models
12
- import prefect.server.schemas as schemas
13
13
  from prefect.server.database import PrefectDBInterface, provide_database_interface
14
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
15
19
  from prefect.server.utilities import subscriptions
16
20
  from prefect.server.utilities.server import PrefectRouter
17
21
 
@@ -20,7 +24,7 @@ router: PrefectRouter = PrefectRouter(prefix="/logs", tags=["Logs"])
20
24
 
21
25
  @router.post("/", status_code=status.HTTP_201_CREATED)
22
26
  async def create_logs(
23
- logs: List[schemas.actions.LogCreate],
27
+ logs: Sequence[LogCreate],
24
28
  db: PrefectDBInterface = Depends(provide_database_interface),
25
29
  ) -> None:
26
30
  """
@@ -33,31 +37,32 @@ async def create_logs(
33
37
  await models.logs.create_logs(session=session, logs=batch)
34
38
 
35
39
 
40
+ logs_adapter: TypeAdapter[Sequence[Log]] = TypeAdapter(Sequence[Log])
41
+
42
+
36
43
  @router.post("/filter")
37
44
  async def read_logs(
38
45
  limit: int = dependencies.LimitBody(),
39
46
  offset: int = Body(0, ge=0),
40
- logs: schemas.filters.LogFilter = None,
41
- sort: schemas.sorting.LogSort = Body(schemas.sorting.LogSort.TIMESTAMP_ASC),
47
+ logs: Optional[LogFilter] = None,
48
+ sort: LogSort = Body(LogSort.TIMESTAMP_ASC),
42
49
  db: PrefectDBInterface = Depends(provide_database_interface),
43
- ) -> List[schemas.core.Log]:
50
+ ) -> Sequence[Log]:
44
51
  """
45
52
  Query for logs.
46
53
  """
47
54
  async with db.session_context() as session:
48
- return await models.logs.read_logs(
49
- 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
+ )
50
59
  )
51
60
 
52
61
 
53
62
  @router.websocket("/out")
54
- async def stream_logs_out(
55
- websocket: WebSocket,
56
- ) -> None:
63
+ async def stream_logs_out(websocket: WebSocket) -> None:
57
64
  """Serve a WebSocket to stream live logs"""
58
- websocket = await subscriptions.accept_prefect_socket(
59
- websocket,
60
- )
65
+ websocket = await subscriptions.accept_prefect_socket(websocket)
61
66
  if not websocket:
62
67
  return
63
68
 
@@ -72,7 +77,7 @@ async def stream_logs_out(
72
77
  )
73
78
 
74
79
  try:
75
- filter = schemas.filters.LogFilter.model_validate(message["filter"])
80
+ filter = LogFilter.model_validate(message["filter"])
76
81
  except Exception as e:
77
82
  return await websocket.close(
78
83
  WS_1002_PROTOCOL_ERROR, reason=f"Invalid filter: {e}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.4.7.dev3
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,8 +1,8 @@
1
1
  prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
- prefect/AGENTS.md,sha256=y9wvcT-AJ7zaP58Vj6IPoGEL82Fhw9X15UlnvGGoD0k,1645
2
+ prefect/AGENTS.md,sha256=qmCZAuKIF9jQyp5TrW_T8bsM_97-QaiCoQp71A_b2Lg,1008
3
3
  prefect/__init__.py,sha256=iCdcC5ZmeewikCdnPEP6YBAjPNV5dvfxpYCTpw30Hkw,3685
4
4
  prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
5
- prefect/_build_info.py,sha256=8xVqpc5-nJMEr7cLce8MxOqYg18OrvFqwHYf_UbNSpY,185
5
+ prefect/_build_info.py,sha256=tffaShFeVAkOpp2FeHJl2rknEfdLB7UQINPTvgTU6a8,185
6
6
  prefect/_result_records.py,sha256=S6QmsODkehGVSzbMm6ig022PYbI6gNKz671p_8kBYx4,7789
7
7
  prefect/_versioning.py,sha256=YqR5cxXrY4P6LM1Pmhd8iMo7v_G2KJpGNdsf4EvDFQ0,14132
8
8
  prefect/_waiters.py,sha256=Ia2ITaXdHzevtyWIgJoOg95lrEXQqNEOquHvw3T33UQ,9026
@@ -45,6 +45,7 @@ prefect/_internal/integrations.py,sha256=U4cZMDbnilzZSKaMxvzZcSL27a1tzRMjDoTfr2u
45
45
  prefect/_internal/pytz.py,sha256=Sy_cD-Hkmo_Yrhx2Jucy7DgTRhvO8ZD0whW1ywbSg_U,13765
46
46
  prefect/_internal/retries.py,sha256=pMHofrTQPDSxbVWclDwXbfhFKaDC6sxe1DkUOWugV6k,3040
47
47
  prefect/_internal/uuid7.py,sha256=yvndhibNDrqnYrG-qUncas4XQp8bKVbmM8XfF7JrjJI,4203
48
+ prefect/_internal/websockets.py,sha256=CloIdusf2Bbefdit46pT91cVDudeYtztPI-MmqSnuLI,3466
48
49
  prefect/_internal/compatibility/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
50
  prefect/_internal/compatibility/async_dispatch.py,sha256=cUXOqSeseMUaje9oYUzasVPtNttyiHvrqfJl0zK66XI,2949
50
51
  prefect/_internal/compatibility/blocks.py,sha256=SSZXoWVuCMYu1EzjqmTa4lKjDCyxvOFK47XMj6s4hsk,984
@@ -154,7 +155,7 @@ prefect/docker/__init__.py,sha256=z6wdc6UFfiBG2jb9Jk64uCWVM04JKVWeVyDWwuuon8M,52
154
155
  prefect/docker/docker_image.py,sha256=bR_pEq5-FDxlwTj8CP_7nwZ_MiGK6KxIi8v7DRjy1Kg,3138
155
156
  prefect/events/__init__.py,sha256=GtKl2bE--pJduTxelH2xy7SadlLJmmis8WR1EYixhuA,2094
156
157
  prefect/events/actions.py,sha256=A7jS8bo4zWGnrt3QfSoQs0uYC1xfKXio3IfU0XtTb5s,9129
157
- prefect/events/clients.py,sha256=r_C3ZevVYUzIW53CpmFbEtR2DwhYeYB4budtB3GaYl0,27625
158
+ prefect/events/clients.py,sha256=pvCbvPcehDhaFEJfeu1DzUP6RhBhacKU7L5Z4XPSvIE,25132
158
159
  prefect/events/filters.py,sha256=tnAbA4Z0Npem8Jbin-qqe38K_4a-4YdpU-Oc4u8Y95Q,8697
159
160
  prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
160
161
  prefect/events/utilities.py,sha256=ww34bTMENCNwcp6RhhgzG0KgXOvKGe0MKmBdSJ8NpZY,3043
@@ -182,6 +183,7 @@ prefect/locking/filesystem.py,sha256=PxC9ndDbo59-gBEx9jtKad4T-Jav0srJSM9vYGzvQwE
182
183
  prefect/locking/memory.py,sha256=EFQnhAO94jEy4TyS880DbsJ42CHT5WNuNc6Wj8dYrKc,7842
183
184
  prefect/locking/protocol.py,sha256=RsfvlaHTTEJ0YvYWSqFGoZuT2w4FPPxyQlHqjoyNGuE,4240
184
185
  prefect/logging/__init__.py,sha256=DpRZzZeWeiDHFlMDEQdknRzbxpL0ObFh5IqqS9iaZwQ,170
186
+ prefect/logging/clients.py,sha256=nKEv-Xfzy5QwtFmZvNirBI8G5YraZ9YzuIkKHb4XVXM,11825
185
187
  prefect/logging/configuration.py,sha256=go9lA4W5HMpK6azDz_ez2YqgQ2b3aCFXxJH-AopoHy8,3404
186
188
  prefect/logging/filters.py,sha256=NnRYubh9dMmWcCAjuW32cIVQ37rLxdn8ci26wTtQMyU,1136
187
189
  prefect/logging/formatters.py,sha256=Sum42BmYZ7mns64jSOy4OA_K8KudEZjeG2h7SZcY9mA,4167
@@ -219,7 +221,7 @@ prefect/server/api/events.py,sha256=mUTv5ZNxiRsEOpzq8fpfCkLpPasjt-ROUAowA5eFbDE,
219
221
  prefect/server/api/flow_run_states.py,sha256=lIdxVE9CqLgtDCuH9bTaKkzHNL81FPrr11liPzvONrw,1661
220
222
  prefect/server/api/flow_runs.py,sha256=Lmb165fLbN4DioxjxgDYaAJ5Qxj771iRYaqn-hYq9KM,33744
221
223
  prefect/server/api/flows.py,sha256=Bz0ISh-9oY0W1X3mqA631_8678pQ6tuRGMpSgWAfxOc,7018
222
- prefect/server/api/logs.py,sha256=10Xsjg_cj1qY9spRe0I6lFskSEFX1RyPU0-650_gMKY,3284
224
+ prefect/server/api/logs.py,sha256=O0W9jomHQuWF7XPMPOhW2p5Uidss6ssMqmKKwMGiv7Y,3526
223
225
  prefect/server/api/middleware.py,sha256=WkyuyeJIfo9Q0GAIVU5gO6yIGNVwoHwuBah5AB5oUyw,2733
224
226
  prefect/server/api/root.py,sha256=CeumFYIM_BDvPicJH9ry5PO_02PZTLeMqbLMGGTh90o,942
225
227
  prefect/server/api/run_history.py,sha256=EW-GTPxZAQ5zXiAqHzmS-iAN_Bn6ZSgVQksDT-ZTsyc,5995
@@ -327,7 +329,7 @@ prefect/workers/cloud.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
327
329
  prefect/workers/process.py,sha256=Yi5D0U5AQ51wHT86GdwtImXSefe0gJf3LGq4r4z9zwM,11090
328
330
  prefect/workers/server.py,sha256=2pmVeJZiVbEK02SO6BEZaBIvHMsn6G8LzjW8BXyiTtk,1952
329
331
  prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
330
- prefect_client-3.4.7.dev3.dist-info/METADATA,sha256=I7zZWLn2BwTOehFSIbkXwLGRffJwBHXUV1_Ticj2-5g,7517
331
- prefect_client-3.4.7.dev3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
332
- prefect_client-3.4.7.dev3.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
333
- prefect_client-3.4.7.dev3.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,,