prefect-client 3.7.2.dev2__py3-none-any.whl → 3.7.2.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/_build_info.py +3 -3
- prefect/automations.py +2 -0
- prefect/bundles/_zip_builder.py +32 -25
- prefect/events/__init__.py +2 -0
- prefect/events/actions.py +7 -0
- prefect/server/api/clients.py +3 -0
- prefect/server/api/workers.py +412 -14
- prefect/utilities/templating/__init__.py +25 -4
- prefect/workers/_worker_channel/_protocol.py +4 -0
- prefect/workers/_worker_channel/_sync.py +15 -8
- prefect/workers/base.py +1 -3
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev3.dist-info}/METADATA +1 -1
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev3.dist-info}/RECORD +15 -15
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev3.dist-info}/WHEEL +0 -0
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev3.dist-info}/licenses/LICENSE +0 -0
prefect/_build_info.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Generated by versioningit
|
|
2
|
-
__version__ = "3.7.2.
|
|
3
|
-
__build_date__ = "2026-05-
|
|
4
|
-
__git_commit__ = "
|
|
2
|
+
__version__ = "3.7.2.dev3"
|
|
3
|
+
__build_date__ = "2026-05-20 09:16:00.764815+00:00"
|
|
4
|
+
__git_commit__ = "30ca141845030af77328e2ebe495e200db2fc679"
|
|
5
5
|
__dirty__ = False
|
prefect/automations.py
CHANGED
|
@@ -11,6 +11,7 @@ from prefect.events.actions import (
|
|
|
11
11
|
CancelFlowRun,
|
|
12
12
|
ChangeFlowRunState,
|
|
13
13
|
DeclareIncident,
|
|
14
|
+
DeleteFlowRun,
|
|
14
15
|
DoNothing,
|
|
15
16
|
PauseAutomation,
|
|
16
17
|
PauseDeployment,
|
|
@@ -61,6 +62,7 @@ __all__ = [
|
|
|
61
62
|
"PauseDeployment",
|
|
62
63
|
"ResumeDeployment",
|
|
63
64
|
"CancelFlowRun",
|
|
65
|
+
"DeleteFlowRun",
|
|
64
66
|
"ChangeFlowRunState",
|
|
65
67
|
"PauseWorkQueue",
|
|
66
68
|
"ResumeWorkQueue",
|
prefect/bundles/_zip_builder.py
CHANGED
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import logging
|
|
13
13
|
import shutil
|
|
14
|
+
import struct
|
|
14
15
|
import tempfile
|
|
15
16
|
import zipfile
|
|
16
17
|
from dataclasses import dataclass
|
|
@@ -32,7 +33,8 @@ class ZipResult:
|
|
|
32
33
|
|
|
33
34
|
Attributes:
|
|
34
35
|
zip_path: Path to the temporary zip file.
|
|
35
|
-
sha256_hash:
|
|
36
|
+
sha256_hash: SHA256 hex digest of the canonical bundled file contents
|
|
37
|
+
(paths, modes, and bytes), not the raw zip archive bytes.
|
|
36
38
|
storage_key: Content-addressed storage key in format "files/{hash}.zip".
|
|
37
39
|
size_bytes: Size of the zip file in bytes.
|
|
38
40
|
"""
|
|
@@ -95,16 +97,40 @@ class ZipBuilder:
|
|
|
95
97
|
self._temp_dir = tempfile.mkdtemp(prefix="prefect-zip-")
|
|
96
98
|
zip_path = Path(self._temp_dir) / "files.zip"
|
|
97
99
|
|
|
98
|
-
# Build the zip
|
|
100
|
+
# Build the zip and compute the content hash in a single pass so
|
|
101
|
+
# that every file is read exactly once, avoiding race conditions
|
|
102
|
+
# where a source file could change between the zip write and the
|
|
103
|
+
# hash computation.
|
|
104
|
+
hasher = hashlib.sha256()
|
|
105
|
+
hasher.update(b"prefect-bundle-files-v1\0")
|
|
106
|
+
|
|
99
107
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
100
108
|
for file_path in sorted_files:
|
|
101
|
-
# Compute relative path with forward slashes
|
|
102
109
|
rel_path = file_path.relative_to(self.base_dir)
|
|
103
110
|
arcname = str(rel_path).replace("\\", "/")
|
|
104
|
-
zf.write(file_path, arcname)
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
file_stat = file_path.stat()
|
|
113
|
+
|
|
114
|
+
arcname_bytes = arcname.encode("utf-8")
|
|
115
|
+
hasher.update(struct.pack(">I", len(arcname_bytes)))
|
|
116
|
+
hasher.update(arcname_bytes)
|
|
117
|
+
hasher.update(struct.pack(">I", file_stat.st_mode & 0xFFFF))
|
|
118
|
+
hasher.update(struct.pack(">Q", file_stat.st_size))
|
|
119
|
+
|
|
120
|
+
zip_info = zipfile.ZipInfo.from_file(file_path, arcname)
|
|
121
|
+
zip_info.compress_type = zipfile.ZIP_DEFLATED
|
|
122
|
+
with (
|
|
123
|
+
file_path.open("rb") as src,
|
|
124
|
+
zf.open(zip_info, "w") as dest,
|
|
125
|
+
):
|
|
126
|
+
while True:
|
|
127
|
+
chunk = src.read(HASH_CHUNK_SIZE)
|
|
128
|
+
if not chunk:
|
|
129
|
+
break
|
|
130
|
+
dest.write(chunk)
|
|
131
|
+
hasher.update(chunk)
|
|
132
|
+
|
|
133
|
+
sha256_hash = hasher.hexdigest()
|
|
108
134
|
|
|
109
135
|
# Get file size
|
|
110
136
|
size_bytes = zip_path.stat().st_size
|
|
@@ -123,25 +149,6 @@ class ZipBuilder:
|
|
|
123
149
|
size_bytes=size_bytes,
|
|
124
150
|
)
|
|
125
151
|
|
|
126
|
-
def _compute_hash(self, zip_path: Path) -> str:
|
|
127
|
-
"""
|
|
128
|
-
Compute SHA256 hash of a file using chunked reading.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
zip_path: Path to the file to hash.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
Lowercase hex digest of the SHA256 hash.
|
|
135
|
-
"""
|
|
136
|
-
hasher = hashlib.sha256()
|
|
137
|
-
with open(zip_path, "rb") as f:
|
|
138
|
-
while True:
|
|
139
|
-
chunk = f.read(HASH_CHUNK_SIZE)
|
|
140
|
-
if not chunk:
|
|
141
|
-
break
|
|
142
|
-
hasher.update(chunk)
|
|
143
|
-
return hasher.hexdigest()
|
|
144
|
-
|
|
145
152
|
def _emit_size_warning(
|
|
146
153
|
self, zip_path: Path, files: list[Path], size_bytes: int
|
|
147
154
|
) -> None:
|
prefect/events/__init__.py
CHANGED
|
@@ -31,6 +31,7 @@ from .actions import (
|
|
|
31
31
|
ResumeDeployment,
|
|
32
32
|
ChangeFlowRunState,
|
|
33
33
|
CancelFlowRun,
|
|
34
|
+
DeleteFlowRun,
|
|
34
35
|
SuspendFlowRun,
|
|
35
36
|
CallWebhook,
|
|
36
37
|
SendNotification,
|
|
@@ -78,6 +79,7 @@ __all__ = [
|
|
|
78
79
|
"ResumeDeployment",
|
|
79
80
|
"ChangeFlowRunState",
|
|
80
81
|
"CancelFlowRun",
|
|
82
|
+
"DeleteFlowRun",
|
|
81
83
|
"SuspendFlowRun",
|
|
82
84
|
"CallWebhook",
|
|
83
85
|
"SendNotification",
|
prefect/events/actions.py
CHANGED
|
@@ -126,6 +126,12 @@ class CancelFlowRun(Action):
|
|
|
126
126
|
type: Literal["cancel-flow-run"] = "cancel-flow-run"
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
class DeleteFlowRun(Action):
|
|
130
|
+
"""Deletes a flow run associated with the trigger"""
|
|
131
|
+
|
|
132
|
+
type: Literal["delete-flow-run"] = "delete-flow-run"
|
|
133
|
+
|
|
134
|
+
|
|
129
135
|
class ResumeFlowRun(Action):
|
|
130
136
|
"""Resumes a flow run associated with the trigger"""
|
|
131
137
|
|
|
@@ -294,6 +300,7 @@ ActionTypes: TypeAlias = Union[
|
|
|
294
300
|
ResumeDeployment,
|
|
295
301
|
ResumeFlowRun,
|
|
296
302
|
CancelFlowRun,
|
|
303
|
+
DeleteFlowRun,
|
|
297
304
|
ChangeFlowRunState,
|
|
298
305
|
PauseWorkQueue,
|
|
299
306
|
ResumeWorkQueue,
|
prefect/server/api/clients.py
CHANGED
|
@@ -93,6 +93,9 @@ class OrchestrationClient(BaseClient):
|
|
|
93
93
|
async def read_flow_run_raw(self, flow_run_id: UUID) -> Response:
|
|
94
94
|
return await self._http_client.get(f"/flow_runs/{flow_run_id}")
|
|
95
95
|
|
|
96
|
+
async def delete_flow_run(self, flow_run_id: UUID) -> Response:
|
|
97
|
+
return await self._http_client.delete(f"/flow_runs/{flow_run_id}")
|
|
98
|
+
|
|
96
99
|
async def read_task_run_raw(self, task_run_id: UUID) -> Response:
|
|
97
100
|
return await self._http_client.get(f"/task_runs/{task_run_id}")
|
|
98
101
|
|
prefect/server/api/workers.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Routes for interacting with work queue objects.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from logging import Logger
|
|
7
|
+
from typing import TYPE_CHECKING, Any, List, Optional
|
|
6
8
|
from uuid import UUID
|
|
7
9
|
|
|
8
10
|
import sqlalchemy as sa
|
|
@@ -11,15 +13,32 @@ from fastapi import (
|
|
|
11
13
|
Depends,
|
|
12
14
|
HTTPException,
|
|
13
15
|
Path,
|
|
16
|
+
WebSocket,
|
|
14
17
|
status,
|
|
15
18
|
)
|
|
16
19
|
from packaging.version import Version
|
|
20
|
+
from pydantic import ValidationError
|
|
17
21
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
18
22
|
|
|
19
23
|
import prefect.server.api.dependencies as dependencies
|
|
20
24
|
import prefect.server.models as models
|
|
21
25
|
import prefect.server.schemas as schemas
|
|
22
26
|
from prefect._internal.uuid7 import uuid7
|
|
27
|
+
from prefect.client.schemas.worker_channel import (
|
|
28
|
+
WORK_POOL_SNAPSHOT_CAPABILITY,
|
|
29
|
+
WORKER_CHANNEL_CLOSE_POLICIES,
|
|
30
|
+
WORKER_HEARTBEAT_CAPABILITY,
|
|
31
|
+
WorkerChannelCloseReason,
|
|
32
|
+
WorkerChannelProtocolError,
|
|
33
|
+
WorkerHeartbeatFrame,
|
|
34
|
+
WorkerHelloFrame,
|
|
35
|
+
WorkerReadyFrame,
|
|
36
|
+
WorkPoolSnapshot,
|
|
37
|
+
WorkPoolSnapshotPayload,
|
|
38
|
+
select_worker_channel_version,
|
|
39
|
+
validate_worker_channel_frame,
|
|
40
|
+
)
|
|
41
|
+
from prefect.logging import get_logger
|
|
23
42
|
from prefect.server.api.validation import validate_job_variable_defaults_for_work_pool
|
|
24
43
|
from prefect.server.database import PrefectDBInterface, provide_database_interface
|
|
25
44
|
from prefect.server.models.deployments import mark_deployments_ready
|
|
@@ -29,17 +48,25 @@ from prefect.server.models.work_queues import (
|
|
|
29
48
|
)
|
|
30
49
|
from prefect.server.models.workers import emit_work_pool_status_event
|
|
31
50
|
from prefect.server.schemas.statuses import WorkQueueStatus
|
|
51
|
+
from prefect.server.utilities import subscriptions
|
|
32
52
|
from prefect.server.utilities.server import PrefectRouter
|
|
33
53
|
from prefect.types import DateTime
|
|
34
54
|
from prefect.types._datetime import now
|
|
35
55
|
|
|
36
56
|
if TYPE_CHECKING:
|
|
37
|
-
from prefect.server.database.orm_models import
|
|
57
|
+
from prefect.server.database.orm_models import WorkPool as ORMWorkPool
|
|
58
|
+
from prefect.server.database.orm_models import WorkQueue as ORMWorkQueue
|
|
38
59
|
|
|
39
60
|
router: PrefectRouter = PrefectRouter(
|
|
40
61
|
prefix="/work_pools",
|
|
41
62
|
tags=["Work Pools"],
|
|
42
63
|
)
|
|
64
|
+
logger: Logger = get_logger("prefect.server.api.workers")
|
|
65
|
+
|
|
66
|
+
_OSS_WORKER_CHANNEL_ACCEPTED_CAPABILITIES = [
|
|
67
|
+
WORKER_HEARTBEAT_CAPABILITY,
|
|
68
|
+
WORK_POOL_SNAPSHOT_CAPABILITY,
|
|
69
|
+
]
|
|
43
70
|
|
|
44
71
|
|
|
45
72
|
# -----------------------------------------------------
|
|
@@ -145,6 +172,288 @@ class WorkerLookups:
|
|
|
145
172
|
return queue.id
|
|
146
173
|
|
|
147
174
|
|
|
175
|
+
class WorkerChannelSetupError(Exception):
|
|
176
|
+
def __init__(self, close_reason: WorkerChannelCloseReason, detail: str):
|
|
177
|
+
super().__init__(detail)
|
|
178
|
+
self.close_reason = close_reason
|
|
179
|
+
self.detail = detail
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class WorkerChannelWorkPoolUpdateEvent:
|
|
184
|
+
work_pool_id: UUID
|
|
185
|
+
changed_fields: dict[str, dict[str, Any]]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def _close_worker_channel(
|
|
189
|
+
websocket: WebSocket, close_reason: WorkerChannelCloseReason
|
|
190
|
+
) -> None:
|
|
191
|
+
policy = WORKER_CHANNEL_CLOSE_POLICIES[close_reason]
|
|
192
|
+
await websocket.close(code=policy.websocket_code, reason=close_reason.value)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def _receive_worker_hello(websocket: WebSocket) -> WorkerHelloFrame:
|
|
196
|
+
try:
|
|
197
|
+
message = await websocket.receive_json()
|
|
198
|
+
frame = validate_worker_channel_frame(message)
|
|
199
|
+
except ValidationError as exc:
|
|
200
|
+
raise WorkerChannelSetupError(
|
|
201
|
+
WorkerChannelCloseReason.PROTOCOL_ERROR,
|
|
202
|
+
"Worker channel received a malformed hello frame",
|
|
203
|
+
) from exc
|
|
204
|
+
except ValueError as exc:
|
|
205
|
+
raise WorkerChannelSetupError(
|
|
206
|
+
WorkerChannelCloseReason.PROTOCOL_ERROR,
|
|
207
|
+
"Worker channel received invalid JSON during setup",
|
|
208
|
+
) from exc
|
|
209
|
+
|
|
210
|
+
if not isinstance(frame, WorkerHelloFrame):
|
|
211
|
+
raise WorkerChannelSetupError(
|
|
212
|
+
WorkerChannelCloseReason.PROTOCOL_ERROR,
|
|
213
|
+
"Expected worker.hello.v1 during worker channel setup",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return frame
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def _resolve_worker_channel_work_pool(
|
|
220
|
+
session: AsyncSession,
|
|
221
|
+
work_pool_name: str,
|
|
222
|
+
hello: WorkerHelloFrame,
|
|
223
|
+
) -> "ORMWorkPool":
|
|
224
|
+
work_pool = await models.workers.read_work_pool_by_name(
|
|
225
|
+
session=session,
|
|
226
|
+
work_pool_name=work_pool_name,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
default_base_job_template = hello.payload.default_base_job_template
|
|
230
|
+
if work_pool is None:
|
|
231
|
+
if not hello.payload.create_pool_if_not_found:
|
|
232
|
+
raise WorkerChannelSetupError(
|
|
233
|
+
WorkerChannelCloseReason.AUTHORIZATION_FAILED,
|
|
234
|
+
"work_pool_not_found",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if work_pool_name.lower().startswith("prefect"):
|
|
238
|
+
raise WorkerChannelSetupError(
|
|
239
|
+
WorkerChannelCloseReason.AUTHORIZATION_FAILED,
|
|
240
|
+
"work_pool_creation_unauthorized",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
await validate_job_variable_defaults_for_work_pool(
|
|
244
|
+
session, work_pool_name, default_base_job_template
|
|
245
|
+
)
|
|
246
|
+
try:
|
|
247
|
+
async with session.begin_nested():
|
|
248
|
+
work_pool = await models.workers.create_work_pool(
|
|
249
|
+
session=session,
|
|
250
|
+
work_pool=schemas.actions.WorkPoolCreate(
|
|
251
|
+
name=work_pool_name,
|
|
252
|
+
type=hello.payload.worker_type,
|
|
253
|
+
base_job_template=default_base_job_template,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
except sa.exc.IntegrityError:
|
|
257
|
+
work_pool = await models.workers.read_work_pool_by_name(
|
|
258
|
+
session=session,
|
|
259
|
+
work_pool_name=work_pool_name,
|
|
260
|
+
)
|
|
261
|
+
if work_pool is None:
|
|
262
|
+
raise
|
|
263
|
+
return work_pool
|
|
264
|
+
|
|
265
|
+
return work_pool
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def _resolve_worker_channel_work_queues(
|
|
269
|
+
session: AsyncSession,
|
|
270
|
+
work_pool_id: UUID,
|
|
271
|
+
work_pool_name: str,
|
|
272
|
+
work_queue_names: list[str],
|
|
273
|
+
) -> list["ORMWorkQueue"]:
|
|
274
|
+
if not work_queue_names:
|
|
275
|
+
return list(
|
|
276
|
+
await models.workers.read_work_queues(
|
|
277
|
+
session=session, work_pool_id=work_pool_id
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
work_queues = []
|
|
282
|
+
for work_queue_name in dict.fromkeys(work_queue_names):
|
|
283
|
+
work_queue = await models.workers.read_work_queue_by_name(
|
|
284
|
+
session=session,
|
|
285
|
+
work_pool_name=work_pool_name,
|
|
286
|
+
work_queue_name=work_queue_name,
|
|
287
|
+
)
|
|
288
|
+
if work_queue is None:
|
|
289
|
+
raise WorkerChannelSetupError(
|
|
290
|
+
WorkerChannelCloseReason.AUTHORIZATION_FAILED,
|
|
291
|
+
"work_queue_not_found",
|
|
292
|
+
)
|
|
293
|
+
work_queues.append(work_queue)
|
|
294
|
+
|
|
295
|
+
return work_queues
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def _build_worker_channel_work_pool_snapshot(
|
|
299
|
+
session: AsyncSession,
|
|
300
|
+
work_pool: "ORMWorkPool",
|
|
301
|
+
) -> WorkPoolSnapshot:
|
|
302
|
+
work_pool_response = schemas.responses.WorkPoolResponse.model_validate(
|
|
303
|
+
work_pool, from_attributes=True
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if work_pool_response.concurrency_limit is not None:
|
|
307
|
+
work_pool_response.active_slots = (
|
|
308
|
+
await models.workers.count_work_pool_active_slots(
|
|
309
|
+
session=session,
|
|
310
|
+
work_pool_id=work_pool.id,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return WorkPoolSnapshot.model_validate(work_pool_response.model_dump(mode="json"))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
async def _build_worker_ready_frame(
|
|
318
|
+
session: AsyncSession,
|
|
319
|
+
work_pool_name: str,
|
|
320
|
+
hello: WorkerHelloFrame,
|
|
321
|
+
) -> tuple[WorkerReadyFrame, WorkerChannelWorkPoolUpdateEvent | None]:
|
|
322
|
+
try:
|
|
323
|
+
selected_channel_version = select_worker_channel_version(
|
|
324
|
+
hello.payload.supported_channel_versions
|
|
325
|
+
)
|
|
326
|
+
except WorkerChannelProtocolError as exc:
|
|
327
|
+
raise WorkerChannelSetupError(exc.close_reason, str(exc)) from exc
|
|
328
|
+
|
|
329
|
+
work_pool = await _resolve_worker_channel_work_pool(
|
|
330
|
+
session=session,
|
|
331
|
+
work_pool_name=work_pool_name,
|
|
332
|
+
hello=hello,
|
|
333
|
+
)
|
|
334
|
+
work_queues = await _resolve_worker_channel_work_queues(
|
|
335
|
+
session=session,
|
|
336
|
+
work_pool_id=work_pool.id,
|
|
337
|
+
work_pool_name=work_pool_name,
|
|
338
|
+
work_queue_names=hello.payload.work_queue_names,
|
|
339
|
+
)
|
|
340
|
+
default_base_job_template = hello.payload.default_base_job_template
|
|
341
|
+
work_pool_update_event = None
|
|
342
|
+
if not work_pool.base_job_template and default_base_job_template:
|
|
343
|
+
previous_base_job_template = work_pool.base_job_template
|
|
344
|
+
await validate_job_variable_defaults_for_work_pool(
|
|
345
|
+
session, work_pool_name, default_base_job_template
|
|
346
|
+
)
|
|
347
|
+
updated = await models.workers.update_work_pool(
|
|
348
|
+
session=session,
|
|
349
|
+
work_pool_id=work_pool.id,
|
|
350
|
+
work_pool=schemas.actions.WorkPoolUpdate(
|
|
351
|
+
base_job_template=default_base_job_template
|
|
352
|
+
),
|
|
353
|
+
emit_update_event=False,
|
|
354
|
+
emit_status_change=emit_work_pool_status_event,
|
|
355
|
+
)
|
|
356
|
+
if updated:
|
|
357
|
+
work_pool_update_event = WorkerChannelWorkPoolUpdateEvent(
|
|
358
|
+
work_pool_id=work_pool.id,
|
|
359
|
+
changed_fields={
|
|
360
|
+
"base_job_template": {
|
|
361
|
+
"from": previous_base_job_template,
|
|
362
|
+
"to": default_base_job_template,
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
refreshed = await models.workers.read_work_pool(
|
|
367
|
+
session=session, work_pool_id=work_pool.id
|
|
368
|
+
)
|
|
369
|
+
assert refreshed is not None
|
|
370
|
+
work_pool = refreshed
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
worker = await models.workers.record_worker_heartbeat(
|
|
374
|
+
session=session,
|
|
375
|
+
work_pool=work_pool,
|
|
376
|
+
worker_name=hello.payload.worker_name,
|
|
377
|
+
heartbeat_interval_seconds=hello.payload.heartbeat_interval_seconds,
|
|
378
|
+
emit_status_change=emit_work_pool_status_event,
|
|
379
|
+
return_worker=True,
|
|
380
|
+
)
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
raise WorkerChannelSetupError(
|
|
383
|
+
WorkerChannelCloseReason.HEARTBEAT_PERSISTENCE_FAILED,
|
|
384
|
+
"worker_channel_initial_heartbeat_failed",
|
|
385
|
+
) from exc
|
|
386
|
+
assert worker is not None
|
|
387
|
+
|
|
388
|
+
refreshed_work_pool = await models.workers.read_work_pool(
|
|
389
|
+
session=session, work_pool_id=work_pool.id
|
|
390
|
+
)
|
|
391
|
+
assert refreshed_work_pool is not None
|
|
392
|
+
initial_snapshot = WorkPoolSnapshotPayload(
|
|
393
|
+
snapshot_sequence=1,
|
|
394
|
+
reason="initial",
|
|
395
|
+
work_pool=await _build_worker_channel_work_pool_snapshot(
|
|
396
|
+
session=session,
|
|
397
|
+
work_pool=refreshed_work_pool,
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
requested_capabilities = list(dict.fromkeys(hello.payload.requested_capabilities))
|
|
402
|
+
accepted = _OSS_WORKER_CHANNEL_ACCEPTED_CAPABILITIES
|
|
403
|
+
accepted_set = set(accepted)
|
|
404
|
+
rejected = [
|
|
405
|
+
capability
|
|
406
|
+
for capability in requested_capabilities
|
|
407
|
+
if capability not in accepted_set
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
WorkerReadyFrame(
|
|
412
|
+
type="worker.ready.v1",
|
|
413
|
+
id=uuid7(),
|
|
414
|
+
sent_at=now("UTC"),
|
|
415
|
+
payload={
|
|
416
|
+
"consumer_id": hello.payload.consumer_id,
|
|
417
|
+
"worker_id": worker.id,
|
|
418
|
+
"selected_channel_version": selected_channel_version,
|
|
419
|
+
"effective_heartbeat_interval_seconds": (
|
|
420
|
+
hello.payload.heartbeat_interval_seconds
|
|
421
|
+
),
|
|
422
|
+
"accepted_capabilities": accepted,
|
|
423
|
+
"rejected_capabilities": rejected,
|
|
424
|
+
"effective_max_cleanup_concurrency": 0,
|
|
425
|
+
"resolved_work_queues": [
|
|
426
|
+
{"id": work_queue.id, "name": work_queue.name}
|
|
427
|
+
for work_queue in work_queues
|
|
428
|
+
],
|
|
429
|
+
"initial_snapshot": initial_snapshot,
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
work_pool_update_event,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
async def _persist_worker_channel_heartbeat(
|
|
437
|
+
session: AsyncSession,
|
|
438
|
+
work_pool_name: str,
|
|
439
|
+
frame: WorkerHeartbeatFrame,
|
|
440
|
+
) -> None:
|
|
441
|
+
work_pool = await models.workers.read_work_pool_by_name(
|
|
442
|
+
session=session,
|
|
443
|
+
work_pool_name=work_pool_name,
|
|
444
|
+
)
|
|
445
|
+
if work_pool is None:
|
|
446
|
+
raise RuntimeError("Worker channel work pool no longer exists")
|
|
447
|
+
|
|
448
|
+
await models.workers.record_worker_heartbeat(
|
|
449
|
+
session=session,
|
|
450
|
+
work_pool=work_pool,
|
|
451
|
+
worker_name=frame.payload.worker_name,
|
|
452
|
+
heartbeat_interval_seconds=frame.payload.heartbeat_interval_seconds,
|
|
453
|
+
emit_status_change=emit_work_pool_status_event,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
148
457
|
# -----------------------------------------------------
|
|
149
458
|
# --
|
|
150
459
|
# --
|
|
@@ -756,6 +1065,104 @@ async def delete_work_queue(
|
|
|
756
1065
|
# -----------------------------------------------------
|
|
757
1066
|
|
|
758
1067
|
|
|
1068
|
+
@router.websocket("/{work_pool_name}/workers/connect")
|
|
1069
|
+
async def worker_channel_connect(
|
|
1070
|
+
websocket: WebSocket,
|
|
1071
|
+
work_pool_name: str = Path(..., description="The work pool name"),
|
|
1072
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
|
1073
|
+
) -> None:
|
|
1074
|
+
websocket = await subscriptions.accept_prefect_socket(
|
|
1075
|
+
websocket,
|
|
1076
|
+
require_prefect_subprotocol=True,
|
|
1077
|
+
authentication_failed_reason=WorkerChannelCloseReason.AUTHENTICATION_FAILED.value,
|
|
1078
|
+
)
|
|
1079
|
+
if not websocket:
|
|
1080
|
+
return
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
hello = await _receive_worker_hello(websocket)
|
|
1084
|
+
async with db.session_context(begin_transaction=True) as session:
|
|
1085
|
+
ready, work_pool_update_event = await _build_worker_ready_frame(
|
|
1086
|
+
session=session,
|
|
1087
|
+
work_pool_name=work_pool_name,
|
|
1088
|
+
hello=hello,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
if work_pool_update_event is not None:
|
|
1092
|
+
async with db.session_context() as session:
|
|
1093
|
+
work_pool = await models.workers.read_work_pool(
|
|
1094
|
+
session=session,
|
|
1095
|
+
work_pool_id=work_pool_update_event.work_pool_id,
|
|
1096
|
+
)
|
|
1097
|
+
assert work_pool is not None
|
|
1098
|
+
await models.workers.emit_work_pool_updated_event(
|
|
1099
|
+
session=session,
|
|
1100
|
+
work_pool=work_pool,
|
|
1101
|
+
changed_fields=work_pool_update_event.changed_fields,
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
await websocket.send_json(ready.model_dump(mode="json"))
|
|
1105
|
+
|
|
1106
|
+
while True:
|
|
1107
|
+
try:
|
|
1108
|
+
message = await websocket.receive_json()
|
|
1109
|
+
frame = validate_worker_channel_frame(message)
|
|
1110
|
+
except ValidationError:
|
|
1111
|
+
await _close_worker_channel(
|
|
1112
|
+
websocket, WorkerChannelCloseReason.PROTOCOL_ERROR
|
|
1113
|
+
)
|
|
1114
|
+
return
|
|
1115
|
+
except ValueError:
|
|
1116
|
+
await _close_worker_channel(
|
|
1117
|
+
websocket, WorkerChannelCloseReason.PROTOCOL_ERROR
|
|
1118
|
+
)
|
|
1119
|
+
return
|
|
1120
|
+
|
|
1121
|
+
if not isinstance(frame, WorkerHeartbeatFrame):
|
|
1122
|
+
await _close_worker_channel(
|
|
1123
|
+
websocket, WorkerChannelCloseReason.PROTOCOL_ERROR
|
|
1124
|
+
)
|
|
1125
|
+
return
|
|
1126
|
+
|
|
1127
|
+
if (
|
|
1128
|
+
frame.payload.consumer_id != hello.payload.consumer_id
|
|
1129
|
+
or frame.payload.worker_name != hello.payload.worker_name
|
|
1130
|
+
):
|
|
1131
|
+
await _close_worker_channel(
|
|
1132
|
+
websocket, WorkerChannelCloseReason.PROTOCOL_ERROR
|
|
1133
|
+
)
|
|
1134
|
+
return
|
|
1135
|
+
|
|
1136
|
+
try:
|
|
1137
|
+
async with db.session_context(begin_transaction=True) as session:
|
|
1138
|
+
await _persist_worker_channel_heartbeat(
|
|
1139
|
+
session=session,
|
|
1140
|
+
work_pool_name=work_pool_name,
|
|
1141
|
+
frame=frame,
|
|
1142
|
+
)
|
|
1143
|
+
except Exception:
|
|
1144
|
+
logger.exception("Worker channel heartbeat persistence failed")
|
|
1145
|
+
await _close_worker_channel(
|
|
1146
|
+
websocket,
|
|
1147
|
+
WorkerChannelCloseReason.HEARTBEAT_PERSISTENCE_FAILED,
|
|
1148
|
+
)
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
except WorkerChannelSetupError as exc:
|
|
1152
|
+
logger.info("Worker channel setup failed: %s", exc.detail)
|
|
1153
|
+
await _close_worker_channel(websocket, exc.close_reason)
|
|
1154
|
+
except HTTPException as exc:
|
|
1155
|
+
logger.info("Worker channel setup failed HTTP validation: %s", exc.detail)
|
|
1156
|
+
await _close_worker_channel(websocket, WorkerChannelCloseReason.PROTOCOL_ERROR)
|
|
1157
|
+
except subscriptions.NORMAL_DISCONNECT_EXCEPTIONS:
|
|
1158
|
+
return
|
|
1159
|
+
except Exception:
|
|
1160
|
+
logger.exception("Worker channel setup failed due to a transient server error")
|
|
1161
|
+
await _close_worker_channel(
|
|
1162
|
+
websocket, WorkerChannelCloseReason.TRANSIENT_SERVER_ERROR
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
|
|
759
1166
|
@router.post(
|
|
760
1167
|
"/{work_pool_name}/workers/heartbeat",
|
|
761
1168
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
@@ -780,23 +1187,14 @@ async def worker_heartbeat(
|
|
|
780
1187
|
detail=f'Work pool "{work_pool_name}" not found.',
|
|
781
1188
|
)
|
|
782
1189
|
|
|
783
|
-
await models.workers.
|
|
1190
|
+
await models.workers.record_worker_heartbeat(
|
|
784
1191
|
session=session,
|
|
785
|
-
|
|
1192
|
+
work_pool=work_pool,
|
|
786
1193
|
worker_name=name,
|
|
787
1194
|
heartbeat_interval_seconds=heartbeat_interval_seconds,
|
|
1195
|
+
emit_status_change=emit_work_pool_status_event,
|
|
788
1196
|
)
|
|
789
1197
|
|
|
790
|
-
if work_pool.status == schemas.statuses.WorkPoolStatus.NOT_READY:
|
|
791
|
-
await models.workers.update_work_pool(
|
|
792
|
-
session=session,
|
|
793
|
-
work_pool_id=work_pool.id,
|
|
794
|
-
work_pool=schemas.internal.InternalWorkPoolUpdate(
|
|
795
|
-
status=schemas.statuses.WorkPoolStatus.READY
|
|
796
|
-
),
|
|
797
|
-
emit_status_change=emit_work_pool_status_event,
|
|
798
|
-
)
|
|
799
|
-
|
|
800
1198
|
|
|
801
1199
|
@router.post("/{work_pool_name}/workers/filter")
|
|
802
1200
|
async def read_workers(
|
|
@@ -14,6 +14,7 @@ from typing import (
|
|
|
14
14
|
overload,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
import prefect.exceptions
|
|
17
18
|
from prefect.client.utilities import inject_client
|
|
18
19
|
from prefect.logging.loggers import get_logger
|
|
19
20
|
from prefect.utilities.annotations import NotSet
|
|
@@ -348,9 +349,19 @@ async def resolve_block_document_references(
|
|
|
348
349
|
)
|
|
349
350
|
block_type_slug, block_document_name, *value_keypath = parts
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
try:
|
|
353
|
+
block_document = await client.read_block_document_by_name(
|
|
354
|
+
name=block_document_name, block_type_slug=block_type_slug
|
|
355
|
+
)
|
|
356
|
+
except prefect.exceptions.ObjectNotFound as exc:
|
|
357
|
+
raise prefect.exceptions.ObjectNotFound(
|
|
358
|
+
http_exc=exc.http_exc,
|
|
359
|
+
help_message=(
|
|
360
|
+
f"Block not found: '{block_document_name}' of type '{block_type_slug}'. "
|
|
361
|
+
f"This block was referenced in your deployment or work pool configuration but no longer exists. "
|
|
362
|
+
f"It may have been deleted. Please check your configuration or create a new block."
|
|
363
|
+
),
|
|
364
|
+
) from exc
|
|
354
365
|
|
|
355
366
|
data = block_document.data
|
|
356
367
|
value: Any = data
|
|
@@ -377,7 +388,17 @@ async def resolve_block_document_references(
|
|
|
377
388
|
if isinstance(template, dict):
|
|
378
389
|
block_document_id = template.get("$ref", {}).get("block_document_id")
|
|
379
390
|
if block_document_id:
|
|
380
|
-
|
|
391
|
+
try:
|
|
392
|
+
block_document = await client.read_block_document(block_document_id)
|
|
393
|
+
except prefect.exceptions.ObjectNotFound as exc:
|
|
394
|
+
raise prefect.exceptions.ObjectNotFound(
|
|
395
|
+
http_exc=exc.http_exc,
|
|
396
|
+
help_message=(
|
|
397
|
+
f"Block not found: ID '{block_document_id}'. "
|
|
398
|
+
f"This block was referenced in your deployment or work pool configuration but no longer exists. "
|
|
399
|
+
f"It may have been deleted. Please check your configuration or create a new block."
|
|
400
|
+
),
|
|
401
|
+
) from exc
|
|
381
402
|
return block_document.data
|
|
382
403
|
updated_template: dict[str, Any] = {}
|
|
383
404
|
for key, value in template.items():
|
|
@@ -93,6 +93,10 @@ class WorkerChannelProtocolHandler:
|
|
|
93
93
|
def work_pool_snapshots_available(self) -> bool:
|
|
94
94
|
return self._work_pool_snapshots.snapshots_available
|
|
95
95
|
|
|
96
|
+
@property
|
|
97
|
+
def work_pool_snapshot_sequence(self) -> int | None:
|
|
98
|
+
return self._work_pool_snapshots.last_applied_sequence
|
|
99
|
+
|
|
96
100
|
async def handshake(
|
|
97
101
|
self, websocket: websockets.asyncio.client.ClientConnection
|
|
98
102
|
) -> WorkerReadyFrame:
|
|
@@ -103,6 +103,7 @@ class WorkPoolWorkerChannel:
|
|
|
103
103
|
)
|
|
104
104
|
self._websocket_started = False
|
|
105
105
|
self._run_scope: anyio.CancelScope | None = None
|
|
106
|
+
self._stopped = False
|
|
106
107
|
|
|
107
108
|
@property
|
|
108
109
|
def url(self) -> str | None:
|
|
@@ -128,13 +129,11 @@ class WorkPoolWorkerChannel:
|
|
|
128
129
|
self._client = client
|
|
129
130
|
|
|
130
131
|
def stop(self) -> None:
|
|
132
|
+
self._stopped = True
|
|
131
133
|
if self._run_scope is not None:
|
|
132
134
|
self._run_scope.cancel()
|
|
133
135
|
|
|
134
136
|
async def sync(self, task_group: anyio.abc.TaskGroup | None) -> None:
|
|
135
|
-
if self.state.healthy and self.snapshots_available:
|
|
136
|
-
return
|
|
137
|
-
|
|
138
137
|
if task_group is not None:
|
|
139
138
|
channel_started = await self._start_websocket(task_group)
|
|
140
139
|
else:
|
|
@@ -142,10 +141,9 @@ class WorkPoolWorkerChannel:
|
|
|
142
141
|
if self.url is None:
|
|
143
142
|
self.state.mark_terminal("endpoint_unavailable")
|
|
144
143
|
|
|
145
|
-
if channel_started and self.snapshots_available:
|
|
146
|
-
|
|
144
|
+
if not (channel_started and self.snapshots_available):
|
|
145
|
+
await self._sync_rest_work_pool()
|
|
147
146
|
|
|
148
|
-
await self._sync_rest_work_pool()
|
|
149
147
|
await self._send_rest_worker_heartbeat()
|
|
150
148
|
|
|
151
149
|
async def _start_websocket(self, task_group: anyio.abc.TaskGroup) -> bool:
|
|
@@ -239,6 +237,8 @@ class WorkPoolWorkerChannel:
|
|
|
239
237
|
async def _run(self, initial_session: WorkerChannelSession | None = None) -> None:
|
|
240
238
|
with anyio.CancelScope() as scope:
|
|
241
239
|
self._run_scope = scope
|
|
240
|
+
if self._stopped:
|
|
241
|
+
scope.cancel()
|
|
242
242
|
try:
|
|
243
243
|
async with AsyncExitStack() as stack:
|
|
244
244
|
if self._cleanup_executor is not None:
|
|
@@ -296,6 +296,7 @@ class WorkPoolWorkerChannel:
|
|
|
296
296
|
self._run_scope = None
|
|
297
297
|
|
|
298
298
|
async def _sync_rest_work_pool(self) -> None:
|
|
299
|
+
initial_snapshot_sequence = self._protocol.work_pool_snapshot_sequence
|
|
299
300
|
try:
|
|
300
301
|
work_pool = await self._client.read_work_pool(
|
|
301
302
|
work_pool_name=self.work_pool_name
|
|
@@ -334,7 +335,10 @@ class WorkPoolWorkerChannel:
|
|
|
334
335
|
)
|
|
335
336
|
return
|
|
336
337
|
|
|
337
|
-
if
|
|
338
|
+
if (
|
|
339
|
+
self.state.healthy
|
|
340
|
+
and self._protocol.work_pool_snapshot_sequence != initial_snapshot_sequence
|
|
341
|
+
):
|
|
338
342
|
self._logger.debug(
|
|
339
343
|
"Skipping REST work pool sync because the worker channel applied a "
|
|
340
344
|
"snapshot while REST sync was in flight."
|
|
@@ -347,7 +351,10 @@ class WorkPoolWorkerChannel:
|
|
|
347
351
|
)
|
|
348
352
|
work_pool.base_job_template = self.default_base_job_template
|
|
349
353
|
|
|
350
|
-
if
|
|
354
|
+
if (
|
|
355
|
+
self.state.healthy
|
|
356
|
+
and self._protocol.work_pool_snapshot_sequence != initial_snapshot_sequence
|
|
357
|
+
):
|
|
351
358
|
self._logger.debug(
|
|
352
359
|
"Skipping REST work pool snapshot because the worker channel applied "
|
|
353
360
|
"a snapshot while REST sync was in flight."
|
prefect/workers/base.py
CHANGED
|
@@ -11,6 +11,7 @@ import uuid
|
|
|
11
11
|
import warnings
|
|
12
12
|
from contextlib import AsyncExitStack
|
|
13
13
|
from functools import partial
|
|
14
|
+
from importlib.metadata import distributions
|
|
14
15
|
from typing import (
|
|
15
16
|
TYPE_CHECKING,
|
|
16
17
|
Any,
|
|
@@ -26,9 +27,6 @@ from zoneinfo import ZoneInfo
|
|
|
26
27
|
import anyio
|
|
27
28
|
import anyio.abc
|
|
28
29
|
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
|
|
29
|
-
from importlib_metadata import (
|
|
30
|
-
distributions, # type: ignore[reportUnknownVariableType] incomplete typing
|
|
31
|
-
)
|
|
32
30
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
|
33
31
|
from pydantic.json_schema import GenerateJsonSchema
|
|
34
32
|
from typing_extensions import Literal, Self, TypeVar
|
|
@@ -2,11 +2,11 @@ prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
|
|
|
2
2
|
prefect/AGENTS.md,sha256=rOU4L6B0ZdvnVmLr_eL394QlwfJkPh1OZswHppAjUN4,10063
|
|
3
3
|
prefect/__init__.py,sha256=4e69hAJtYPoEeRxc8iNjzp5WIkmXP0i2SNiN3c97_Go,6703
|
|
4
4
|
prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
|
|
5
|
-
prefect/_build_info.py,sha256=
|
|
5
|
+
prefect/_build_info.py,sha256=D7OiFa_wqNlew0FeFafW9ZPD-g41AtRn_SNU2gkqMNQ,185
|
|
6
6
|
prefect/_flow_run_suspension.py,sha256=5zTTB7ZIBHzoS0pVrhNn23-9hK51qZ3CQA6C-azluC0,4144
|
|
7
7
|
prefect/agent.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
8
8
|
prefect/artifacts.py,sha256=ZdMLJeJGK82hibtRzbsVa-g95dMa0D2UP1LiESoXmf4,23951
|
|
9
|
-
prefect/automations.py,sha256=
|
|
9
|
+
prefect/automations.py,sha256=OxfOezLuHHOB_S-ohyRBoTg6yL9MtbrlAyQq8zGkF9A,12607
|
|
10
10
|
prefect/cache_policies.py,sha256=K0cymkMbEQBuwY1fSehefllN4Y4RblpNzYsh-LtjdXM,13156
|
|
11
11
|
prefect/context.py,sha256=arDksENXHs3O1yOP8Iiq3CaerbsT6ixxM4BIrIuE2tM,38026
|
|
12
12
|
prefect/engine.py,sha256=F0YQlbewnvQGJZyB-wDePQxm_9b89D0_9S1PhMvuULI,6002
|
|
@@ -146,7 +146,7 @@ prefect/bundles/__init__.py,sha256=3LiZeX-QG-5NnKq2FaQZrPOI_2Ta-2bOUir4F9-tEV0,3
|
|
|
146
146
|
prefect/bundles/_file_collector.py,sha256=15v9l1vtbSwSfgA9tHF3jJzJxf2vWCCEJRvFIR89OSw,21222
|
|
147
147
|
prefect/bundles/_ignore_filter.py,sha256=4p7Ku8h6LOVmagC0lLlK3SBo-FLrz2JXoJexFto5nKo,11067
|
|
148
148
|
prefect/bundles/_path_resolver.py,sha256=-3VCDZxD7aDFJlucOdTK4PneW4XEfxpFo1MSfLJe5Xk,18366
|
|
149
|
-
prefect/bundles/_zip_builder.py,sha256
|
|
149
|
+
prefect/bundles/_zip_builder.py,sha256=-agAOPVSaW5mEFyGcmAFgVj_K2sdtaWF-bPt0PGQLCo,6360
|
|
150
150
|
prefect/bundles/_zip_extractor.py,sha256=aFU2PWH6Ar-6HMOljsSvdSruSS6QUlHt5rjdCpIWvNU,4637
|
|
151
151
|
prefect/bundles/execute.py,sha256=KubXt8jNr9Xq7hf44oxi5fZEh0QcLHQO7ivtSElrQq0,1736
|
|
152
152
|
prefect/client/AGENTS.md,sha256=16krPZUqEyiEaKk6jsLRK7MJHKlshe1mWnKvtvZiMco,3079
|
|
@@ -230,8 +230,8 @@ prefect/docker/__init__.py,sha256=gX5MJ5yHIK7WwpoOR3LpcR-vydv5SxrkOWdYQcgUL5o,71
|
|
|
230
230
|
prefect/docker/_buildx.py,sha256=sVh0v12RZ3TkqzdawuIzfj1d2GTgpvT2WM5khrGXsqA,519
|
|
231
231
|
prefect/docker/docker_image.py,sha256=afAOYLv8pIGBV-rDuMbzfnnfGlu7qMUb6l3jXGCAe_k,6962
|
|
232
232
|
prefect/events/AGENTS.md,sha256=ZCrjaA67BQGdDzO5R7yJB7HO5Ahb8z7A3sT8yuKQ6V0,3049
|
|
233
|
-
prefect/events/__init__.py,sha256=
|
|
234
|
-
prefect/events/actions.py,sha256=
|
|
233
|
+
prefect/events/__init__.py,sha256=e3aIcHzTfseOmq_A3hM_jSPNYP_OdndJErzjcdrMuAc,2202
|
|
234
|
+
prefect/events/actions.py,sha256=Ii5P4FX10Xu1nzU0e7okA1KC1eVIHhzExubFurrOr_Q,9785
|
|
235
235
|
prefect/events/clients.py,sha256=m0KN7LpulgatjCVX5-3f8kzQKI_7u7Y3Nk6V948tTyc,33339
|
|
236
236
|
prefect/events/filters.py,sha256=_vweQbeRxK8KiyKIPb6fYEtA5crbaUxlYPkWorFmMm4,9201
|
|
237
237
|
prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
|
|
@@ -304,7 +304,7 @@ prefect/server/api/block_capabilities.py,sha256=0x1vtC2CtSRVcCWydgNbylmfv_LnJHIo
|
|
|
304
304
|
prefect/server/api/block_documents.py,sha256=srU5Jmn-Fguhut6PtV2N4XWZLJa2enwZ7ShcbrDRR6w,6034
|
|
305
305
|
prefect/server/api/block_schemas.py,sha256=H9kxnKPlTvz62SMcX1ssHnQ8KACU2728ebCtSp2iLcs,5508
|
|
306
306
|
prefect/server/api/block_types.py,sha256=DdKesZhnpGlTJtIidGoBSLEFFcAwFp-_XEA-SUOlhAM,8343
|
|
307
|
-
prefect/server/api/clients.py,sha256=
|
|
307
|
+
prefect/server/api/clients.py,sha256=6bOdrsgn7jyM_YD89opjzrg9cJn6uUTHbs1P6wFotRI,9198
|
|
308
308
|
prefect/server/api/collections.py,sha256=RI7cjdM8RYFyAk2rgb35vqh06PWGXAamTvwThl83joY,2454
|
|
309
309
|
prefect/server/api/concurrency_limits.py,sha256=nJKOiKZwQvxFfAMBaqyeAWHjm6hrpErfnQIMuhhszoA,24503
|
|
310
310
|
prefect/server/api/concurrency_limits_v2.py,sha256=-Wwk7eWe0stLt738wQslHZQxYUiMIiTodMWU1P6BuLk,17817
|
|
@@ -328,7 +328,7 @@ prefect/server/api/templates.py,sha256=EW5aJOuvSXBeShd5VIygI1f9W0uTUpGb32ADrL9LG
|
|
|
328
328
|
prefect/server/api/validation.py,sha256=DPofXjLFejs_qusUQgzRkG89X2EFoviAy9J08pnNvyM,14543
|
|
329
329
|
prefect/server/api/variables.py,sha256=8Ursuf20R5zXUS015ZexCH72K7R6Yv76ehkr_l3Xt0k,6186
|
|
330
330
|
prefect/server/api/work_queues.py,sha256=BQQgiBWytrMgPhbHbOR_6Vx8T1V7JDIgeoZ4_LhmvCo,11276
|
|
331
|
-
prefect/server/api/workers.py,sha256=
|
|
331
|
+
prefect/server/api/workers.py,sha256=WnlQHlD41DS6ZH4NCr9Rx7dOXNWPTTFNaoH3TkHPNn4,44174
|
|
332
332
|
prefect/server/api/collections_data/views/aggregate-worker-metadata.json,sha256=otKGMKJUh_ySkQyNBFCe84E6VRbuR-Z7_676D21YKsw,81525
|
|
333
333
|
prefect/server/api/static/prefect-logo-mark-gradient.png,sha256=ylRjJkI_JHCw8VbQasNnXQHwZW-sH-IQiUGSD3aWP1E,73430
|
|
334
334
|
prefect/server/api/ui/__init__.py,sha256=TCXO4ZUZCqCbm2QoNvWNTErkzWiX2nSACuO-0Tiomvg,93
|
|
@@ -422,23 +422,23 @@ prefect/utilities/schema_tools/__init__.py,sha256=At3rMHd2g_Em2P3_dFQlFgqR_EpBwr
|
|
|
422
422
|
prefect/utilities/schema_tools/hydration.py,sha256=446SKv-W65VlX_5UGY3YvGYPQ7aYgb6dPy85EueU8FM,9671
|
|
423
423
|
prefect/utilities/schema_tools/validation.py,sha256=UhRLWStdT9__u4yz-mnc3lcdt-VJmRGJBGV_f_9um-Y,10749
|
|
424
424
|
prefect/utilities/templating/AGENTS.md,sha256=vkflyqkm9snY5sR-Wfxr855F55yJQTfSBEt5ThoSUF4,2097
|
|
425
|
-
prefect/utilities/templating/__init__.py,sha256
|
|
425
|
+
prefect/utilities/templating/__init__.py,sha256=-GGrp9CRHVPddmc_9z3BTBkMO7Vwqg-yevkJFaFHI2I,19248
|
|
426
426
|
prefect/workers/AGENTS.md,sha256=rNcd5PutF7TUQJerXezNX3kemXR7VII_5CnuYq0l0vs,4282
|
|
427
427
|
prefect/workers/__init__.py,sha256=EaM1F0RZ-XIJaGeTKLsXDnfOPHzVWk5bk0_c4BVS44M,64
|
|
428
428
|
prefect/workers/_cleanup.py,sha256=U0tc0vExmsVpXaTo9H9CGHWnJOoykZiQVKI-cn5TJao,23675
|
|
429
429
|
prefect/workers/_cleanup_handlers.py,sha256=dVy2qi5qjazjusunWcRZro5Hiy-stp8NqSeMNthjOnk,5169
|
|
430
|
-
prefect/workers/base.py,sha256=
|
|
430
|
+
prefect/workers/base.py,sha256=cDxZY3jCZ5sm7xB_EXpxuxe6XIUdMqQ0OrcSNiwEscY,78325
|
|
431
431
|
prefect/workers/block.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
432
432
|
prefect/workers/cloud.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
433
433
|
prefect/workers/process.py,sha256=QA9p3Z3br77PGVL_McPgYyeF2zABXlGTF1qLAsTHYwY,12820
|
|
434
434
|
prefect/workers/server.py,sha256=bWnYfMfJf5_IO3y3aJOpia7p9lFKC3ZZjiMvHox-UKY,1992
|
|
435
435
|
prefect/workers/utilities.py,sha256=-re_0s1Jr98W5xhJr4Xvvz34OxN3T4ypSupxdBiC8aA,2633
|
|
436
436
|
prefect/workers/_worker_channel/__init__.py,sha256=vEc7NvC1sjxnCn5thQv5lSU1bDKRaT1sV7AA_6VRoCM,709
|
|
437
|
-
prefect/workers/_worker_channel/_protocol.py,sha256=
|
|
437
|
+
prefect/workers/_worker_channel/_protocol.py,sha256=GpCNh1o3qmmqHA_UOOTge1QVC6IRvWP2RdpAEBqXPs0,15834
|
|
438
438
|
prefect/workers/_worker_channel/_state.py,sha256=eQTFZtAVDZH1vVWps3SdeY6aW3qu2wx1UKYQXK3AyuE,5369
|
|
439
|
-
prefect/workers/_worker_channel/_sync.py,sha256=
|
|
439
|
+
prefect/workers/_worker_channel/_sync.py,sha256=G5G8_UaQYbeLebi5Mb1Z_KgGfXfyjXo5uT9la5_zwqY,14984
|
|
440
440
|
prefect/workers/_worker_channel/_transport.py,sha256=cgrtAENawDpIPB8gwILd62y2VduykUCmg1NaO1pL-tg,9021
|
|
441
|
-
prefect_client-3.7.2.
|
|
442
|
-
prefect_client-3.7.2.
|
|
443
|
-
prefect_client-3.7.2.
|
|
444
|
-
prefect_client-3.7.2.
|
|
441
|
+
prefect_client-3.7.2.dev3.dist-info/METADATA,sha256=30GTP0tqIsf673WM91EeD7fFTwH5bpIw8m1fekn7s9M,7502
|
|
442
|
+
prefect_client-3.7.2.dev3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
443
|
+
prefect_client-3.7.2.dev3.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
|
|
444
|
+
prefect_client-3.7.2.dev3.dist-info/RECORD,,
|
|
File without changes
|
{prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|