prefect-client 3.7.2.dev2__py3-none-any.whl → 3.7.2.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/__init__.py CHANGED
@@ -113,6 +113,11 @@ def _initialize_plugins() -> None:
113
113
  # Re-raise SystemExit from strict mode
114
114
  raise
115
115
  except Exception as e:
116
+ from pydantic_settings.exceptions import SettingsError
117
+
118
+ if isinstance(e, SettingsError):
119
+ return
120
+
116
121
  # Log but don't crash on plugin errors
117
122
  try:
118
123
  from prefect.logging import get_logger
prefect/_build_info.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.7.2.dev2"
3
- __build_date__ = "2026-05-19 09:17:54.056516+00:00"
4
- __git_commit__ = "68244724cf7a168f14f81bdf032689c912a404de"
2
+ __version__ = "3.7.2.dev4"
3
+ __build_date__ = "2026-05-21 09:15:36.144441+00:00"
4
+ __git_commit__ = "354623c1258b2e75de62230b8e3f0400f2722cf1"
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",
@@ -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: Lowercase hex digest of the zip content.
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 with DEFLATED compression
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
- # Compute SHA256 hash using chunked reading
107
- sha256_hash = self._compute_hash(zip_path)
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:
@@ -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/runner/runner.py CHANGED
@@ -215,7 +215,7 @@ class Runner:
215
215
  def goodbye_flow(name):
216
216
  print(f"goodbye {name}")
217
217
 
218
- if __name__ == "__main__"
218
+ if __name__ == "__main__":
219
219
  runner = Runner(name="my-runner")
220
220
 
221
221
  # Will be runnable via the API
@@ -614,17 +614,17 @@ class Runner:
614
614
  def goodbye_flow(name):
615
615
  print(f"goodbye {name}")
616
616
 
617
- if __name__ == "__main__"
618
- runner = Runner(name="my-runner")
617
+ if __name__ == "__main__":
618
+ runner = Runner(name="my-runner")
619
619
 
620
- # Will be runnable via the API
621
- runner.add_flow(hello_flow)
620
+ # Will be runnable via the API
621
+ runner.add_flow(hello_flow)
622
622
 
623
- # Run on a cron schedule
624
- runner.add_flow(goodbye_flow, schedule={"cron": "0 * * * *"})
623
+ # Run on a cron schedule
624
+ runner.add_flow(goodbye_flow, schedule={"cron": "0 * * * *"})
625
625
 
626
- asyncio.run(runner.start())
627
- ```
626
+ asyncio.run(runner.start())
627
+ ```
628
628
  """
629
629
  from prefect.runner.server import start_webserver
630
630
 
@@ -781,9 +781,9 @@ class Runner:
781
781
  # The process may be a multiprocessing.context.SpawnProcess, in which case it will have an `exitcode` attribute
782
782
  # but no `returncode` attribute
783
783
  if (
784
- getattr(process, "returncode", None)
785
- or getattr(process, "exitcode", None)
786
- ) is None:
784
+ getattr(process, "returncode", None) is None
785
+ and getattr(process, "exitcode", None) is None
786
+ ):
787
787
  await self._add_flow_run_process_map_entry(
788
788
  flow_run.id, ProcessMapEntry(pid=process.pid, flow_run=flow_run)
789
789
  )
@@ -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
 
@@ -78,6 +78,15 @@ async def update_work_queue(
78
78
  Updates an existing work queue.
79
79
  """
80
80
  async with db.session_context(begin_transaction=True) as session:
81
+ existing_work_queue = await models.work_queues.read_work_queue(
82
+ session=session, work_queue_id=work_queue_id
83
+ )
84
+ if existing_work_queue is None:
85
+ raise HTTPException(
86
+ status_code=status.HTTP_404_NOT_FOUND,
87
+ detail=f"Work Queue {work_queue_id} not found",
88
+ )
89
+
81
90
  result = await models.work_queues.update_work_queue(
82
91
  session=session,
83
92
  work_queue_id=work_queue_id,
@@ -86,7 +95,8 @@ async def update_work_queue(
86
95
  )
87
96
  if not result:
88
97
  raise HTTPException(
89
- status_code=status.HTTP_404_NOT_FOUND, detail=f"Work Queue {id} not found"
98
+ status_code=status.HTTP_404_NOT_FOUND,
99
+ detail=f"Work Queue {work_queue_id} not found",
90
100
  )
91
101
 
92
102
 
@@ -239,6 +249,13 @@ async def delete_work_queue(
239
249
  Delete a work queue by id.
240
250
  """
241
251
  async with db.session_context(begin_transaction=True) as session:
252
+ existing_work_queue = await models.work_queues.read_work_queue(
253
+ session=session, work_queue_id=work_queue_id
254
+ )
255
+ if existing_work_queue is None:
256
+ raise HTTPException(
257
+ status_code=status.HTTP_404_NOT_FOUND, detail="work queue not found"
258
+ )
242
259
  result = await models.work_queues.delete_work_queue(
243
260
  session=session, work_queue_id=work_queue_id
244
261
  )
@@ -2,7 +2,9 @@
2
2
  Routes for interacting with work queue objects.
3
3
  """
4
4
 
5
- from typing import TYPE_CHECKING, List, Optional
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,29 @@ 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_HEARTBEAT_CAPABILITY,
30
+ WorkerChannelCloseReason,
31
+ WorkerChannelProtocolError,
32
+ WorkerHelloFrame,
33
+ WorkerReadyFrame,
34
+ WorkPoolSnapshotPayload,
35
+ select_worker_channel_version,
36
+ validate_worker_channel_frame,
37
+ )
38
+ from prefect.logging import get_logger
23
39
  from prefect.server.api.validation import validate_job_variable_defaults_for_work_pool
24
40
  from prefect.server.database import PrefectDBInterface, provide_database_interface
25
41
  from prefect.server.models.deployments import mark_deployments_ready
@@ -29,17 +45,26 @@ from prefect.server.models.work_queues import (
29
45
  )
30
46
  from prefect.server.models.workers import emit_work_pool_status_event
31
47
  from prefect.server.schemas.statuses import WorkQueueStatus
48
+ from prefect.server.utilities import subscriptions
49
+ from prefect.server.utilities import worker_channel as worker_channel_utils
32
50
  from prefect.server.utilities.server import PrefectRouter
33
51
  from prefect.types import DateTime
34
52
  from prefect.types._datetime import now
35
53
 
36
54
  if TYPE_CHECKING:
37
- from prefect.server.database.orm_models import ORMWorkQueue
55
+ from prefect.server.database.orm_models import WorkPool as ORMWorkPool
56
+ from prefect.server.database.orm_models import WorkQueue as ORMWorkQueue
38
57
 
39
58
  router: PrefectRouter = PrefectRouter(
40
59
  prefix="/work_pools",
41
60
  tags=["Work Pools"],
42
61
  )
62
+ logger: Logger = get_logger("prefect.server.api.workers")
63
+
64
+ _OSS_WORKER_CHANNEL_ACCEPTED_CAPABILITIES = [
65
+ WORKER_HEARTBEAT_CAPABILITY,
66
+ WORK_POOL_SNAPSHOT_CAPABILITY,
67
+ ]
43
68
 
44
69
 
45
70
  # -----------------------------------------------------
@@ -145,6 +170,241 @@ class WorkerLookups:
145
170
  return queue.id
146
171
 
147
172
 
173
+ class WorkerChannelSetupError(Exception):
174
+ def __init__(self, close_reason: WorkerChannelCloseReason, detail: str):
175
+ super().__init__(detail)
176
+ self.close_reason = close_reason
177
+ self.detail = detail
178
+
179
+
180
+ @dataclass(frozen=True)
181
+ class WorkerChannelWorkPoolUpdateEvent:
182
+ work_pool_id: UUID
183
+ changed_fields: dict[str, dict[str, Any]]
184
+
185
+
186
+ async def _receive_worker_hello(websocket: WebSocket) -> WorkerHelloFrame:
187
+ try:
188
+ message = await websocket.receive_json()
189
+ frame = validate_worker_channel_frame(message)
190
+ except ValidationError as exc:
191
+ raise WorkerChannelSetupError(
192
+ WorkerChannelCloseReason.PROTOCOL_ERROR,
193
+ "Worker channel received a malformed hello frame",
194
+ ) from exc
195
+ except ValueError as exc:
196
+ raise WorkerChannelSetupError(
197
+ WorkerChannelCloseReason.PROTOCOL_ERROR,
198
+ "Worker channel received invalid JSON during setup",
199
+ ) from exc
200
+
201
+ if not isinstance(frame, WorkerHelloFrame):
202
+ raise WorkerChannelSetupError(
203
+ WorkerChannelCloseReason.PROTOCOL_ERROR,
204
+ "Expected worker.hello.v1 during worker channel setup",
205
+ )
206
+
207
+ return frame
208
+
209
+
210
+ async def _resolve_worker_channel_work_pool(
211
+ session: AsyncSession,
212
+ work_pool_name: str,
213
+ hello: WorkerHelloFrame,
214
+ ) -> "ORMWorkPool":
215
+ work_pool = await models.workers.read_work_pool_by_name(
216
+ session=session,
217
+ work_pool_name=work_pool_name,
218
+ )
219
+
220
+ default_base_job_template = hello.payload.default_base_job_template
221
+ if work_pool is None:
222
+ if not hello.payload.create_pool_if_not_found:
223
+ raise WorkerChannelSetupError(
224
+ WorkerChannelCloseReason.AUTHORIZATION_FAILED,
225
+ "work_pool_not_found",
226
+ )
227
+
228
+ if work_pool_name.lower().startswith("prefect"):
229
+ raise WorkerChannelSetupError(
230
+ WorkerChannelCloseReason.AUTHORIZATION_FAILED,
231
+ "work_pool_creation_unauthorized",
232
+ )
233
+
234
+ await validate_job_variable_defaults_for_work_pool(
235
+ session, work_pool_name, default_base_job_template
236
+ )
237
+ try:
238
+ async with session.begin_nested():
239
+ work_pool = await models.workers.create_work_pool(
240
+ session=session,
241
+ work_pool=schemas.actions.WorkPoolCreate(
242
+ name=work_pool_name,
243
+ type=hello.payload.worker_type,
244
+ base_job_template=default_base_job_template,
245
+ ),
246
+ )
247
+ except sa.exc.IntegrityError:
248
+ work_pool = await models.workers.read_work_pool_by_name(
249
+ session=session,
250
+ work_pool_name=work_pool_name,
251
+ )
252
+ if work_pool is None:
253
+ raise
254
+ return work_pool
255
+
256
+ return work_pool
257
+
258
+
259
+ async def _resolve_worker_channel_work_queues(
260
+ session: AsyncSession,
261
+ work_pool_id: UUID,
262
+ work_pool_name: str,
263
+ work_queue_names: list[str],
264
+ ) -> list["ORMWorkQueue"]:
265
+ if not work_queue_names:
266
+ return list(
267
+ await models.workers.read_work_queues(
268
+ session=session, work_pool_id=work_pool_id
269
+ )
270
+ )
271
+
272
+ work_queues = []
273
+ for work_queue_name in dict.fromkeys(work_queue_names):
274
+ work_queue = await models.workers.read_work_queue_by_name(
275
+ session=session,
276
+ work_pool_name=work_pool_name,
277
+ work_queue_name=work_queue_name,
278
+ )
279
+ if work_queue is None:
280
+ raise WorkerChannelSetupError(
281
+ WorkerChannelCloseReason.AUTHORIZATION_FAILED,
282
+ "work_queue_not_found",
283
+ )
284
+ work_queues.append(work_queue)
285
+
286
+ return work_queues
287
+
288
+
289
+ async def _build_worker_ready_frame(
290
+ session: AsyncSession,
291
+ work_pool_name: str,
292
+ hello: WorkerHelloFrame,
293
+ ) -> tuple[WorkerReadyFrame, WorkerChannelWorkPoolUpdateEvent | None]:
294
+ try:
295
+ selected_channel_version = select_worker_channel_version(
296
+ hello.payload.supported_channel_versions
297
+ )
298
+ except WorkerChannelProtocolError as exc:
299
+ raise WorkerChannelSetupError(exc.close_reason, str(exc)) from exc
300
+
301
+ work_pool = await _resolve_worker_channel_work_pool(
302
+ session=session,
303
+ work_pool_name=work_pool_name,
304
+ hello=hello,
305
+ )
306
+ work_queues = await _resolve_worker_channel_work_queues(
307
+ session=session,
308
+ work_pool_id=work_pool.id,
309
+ work_pool_name=work_pool_name,
310
+ work_queue_names=hello.payload.work_queue_names,
311
+ )
312
+ default_base_job_template = hello.payload.default_base_job_template
313
+ work_pool_update_event = None
314
+ if not work_pool.base_job_template and default_base_job_template:
315
+ previous_base_job_template = work_pool.base_job_template
316
+ await validate_job_variable_defaults_for_work_pool(
317
+ session, work_pool_name, default_base_job_template
318
+ )
319
+ updated = await models.workers.update_work_pool(
320
+ session=session,
321
+ work_pool_id=work_pool.id,
322
+ work_pool=schemas.actions.WorkPoolUpdate(
323
+ base_job_template=default_base_job_template
324
+ ),
325
+ emit_update_event=False,
326
+ emit_status_change=emit_work_pool_status_event,
327
+ )
328
+ if updated:
329
+ work_pool_update_event = WorkerChannelWorkPoolUpdateEvent(
330
+ work_pool_id=work_pool.id,
331
+ changed_fields={
332
+ "base_job_template": {
333
+ "from": previous_base_job_template,
334
+ "to": default_base_job_template,
335
+ }
336
+ },
337
+ )
338
+ refreshed = await models.workers.read_work_pool(
339
+ session=session, work_pool_id=work_pool.id
340
+ )
341
+ assert refreshed is not None
342
+ work_pool = refreshed
343
+
344
+ try:
345
+ worker = await models.workers.record_worker_heartbeat(
346
+ session=session,
347
+ work_pool=work_pool,
348
+ worker_name=hello.payload.worker_name,
349
+ heartbeat_interval_seconds=hello.payload.heartbeat_interval_seconds,
350
+ emit_status_change=emit_work_pool_status_event,
351
+ return_worker=True,
352
+ )
353
+ except Exception as exc:
354
+ raise WorkerChannelSetupError(
355
+ WorkerChannelCloseReason.HEARTBEAT_PERSISTENCE_FAILED,
356
+ "worker_channel_initial_heartbeat_failed",
357
+ ) from exc
358
+ assert worker is not None
359
+
360
+ refreshed_work_pool = await models.workers.read_work_pool(
361
+ session=session, work_pool_id=work_pool.id
362
+ )
363
+ assert refreshed_work_pool is not None
364
+ initial_snapshot = WorkPoolSnapshotPayload(
365
+ snapshot_sequence=1,
366
+ reason="initial",
367
+ work_pool=await worker_channel_utils.build_worker_channel_work_pool_snapshot(
368
+ session=session,
369
+ work_pool=refreshed_work_pool,
370
+ ),
371
+ )
372
+
373
+ requested_capabilities = list(dict.fromkeys(hello.payload.requested_capabilities))
374
+ accepted = _OSS_WORKER_CHANNEL_ACCEPTED_CAPABILITIES
375
+ accepted_set = set(accepted)
376
+ rejected = [
377
+ capability
378
+ for capability in requested_capabilities
379
+ if capability not in accepted_set
380
+ ]
381
+
382
+ return (
383
+ WorkerReadyFrame(
384
+ type="worker.ready.v1",
385
+ id=uuid7(),
386
+ sent_at=now("UTC"),
387
+ payload={
388
+ "consumer_id": hello.payload.consumer_id,
389
+ "worker_id": worker.id,
390
+ "selected_channel_version": selected_channel_version,
391
+ "effective_heartbeat_interval_seconds": (
392
+ hello.payload.heartbeat_interval_seconds
393
+ ),
394
+ "accepted_capabilities": accepted,
395
+ "rejected_capabilities": rejected,
396
+ "effective_max_cleanup_concurrency": 0,
397
+ "resolved_work_queues": [
398
+ {"id": work_queue.id, "name": work_queue.name}
399
+ for work_queue in work_queues
400
+ ],
401
+ "initial_snapshot": initial_snapshot,
402
+ },
403
+ ),
404
+ work_pool_update_event,
405
+ )
406
+
407
+
148
408
  # -----------------------------------------------------
149
409
  # --
150
410
  # --
@@ -333,13 +593,23 @@ async def update_work_pool(
333
593
  work_pool_id = await worker_lookups._get_work_pool_id_from_name(
334
594
  session=session, work_pool_name=work_pool_name
335
595
  )
336
- await models.workers.update_work_pool(
596
+ updated = await models.workers.update_work_pool(
337
597
  session=session,
338
598
  work_pool_id=work_pool_id,
339
599
  work_pool=work_pool,
340
600
  emit_status_change=emit_work_pool_status_event,
341
601
  )
342
602
 
603
+ if updated and worker_channel_utils.work_pool_update_triggers_snapshot(
604
+ update_values
605
+ ):
606
+ await worker_channel_utils.publish_snapshot_invalidation(
607
+ worker_channel_utils.WorkerChannelSnapshotInvalidation(
608
+ work_pool_id=work_pool_id,
609
+ reason="work_pool_updated",
610
+ )
611
+ )
612
+
343
613
 
344
614
  @router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
345
615
  async def delete_work_pool(
@@ -365,10 +635,19 @@ async def delete_work_pool(
365
635
  session=session, work_pool_name=work_pool_name
366
636
  )
367
637
 
368
- await models.workers.delete_work_pool(
638
+ deleted = await models.workers.delete_work_pool(
369
639
  session=session, work_pool_id=work_pool_id
370
640
  )
371
641
 
642
+ if deleted:
643
+ await worker_channel_utils.publish_snapshot_invalidation(
644
+ worker_channel_utils.WorkerChannelSnapshotInvalidation(
645
+ work_pool_id=work_pool_id,
646
+ reason="work_pool_deleted",
647
+ work_pool_deleted=True,
648
+ )
649
+ )
650
+
372
651
 
373
652
  @router.post("/{name}/concurrency_status")
374
653
  async def read_work_pool_concurrency_status(
@@ -704,8 +983,11 @@ async def update_work_queue(
704
983
  """
705
984
  Update a work pool queue
706
985
  """
707
-
708
986
  async with db.session_context(begin_transaction=True) as session:
987
+ await worker_lookups._get_work_pool_id_from_name(
988
+ session=session,
989
+ work_pool_name=work_pool_name,
990
+ )
709
991
  work_queue_id = await worker_lookups._get_work_queue_id_from_name(
710
992
  work_pool_name=work_pool_name,
711
993
  work_queue_name=work_queue_name,
@@ -736,6 +1018,10 @@ async def delete_work_queue(
736
1018
  """
737
1019
 
738
1020
  async with db.session_context(begin_transaction=True) as session:
1021
+ await worker_lookups._get_work_pool_id_from_name(
1022
+ session=session,
1023
+ work_pool_name=work_pool_name,
1024
+ )
739
1025
  work_queue_id = await worker_lookups._get_work_queue_id_from_name(
740
1026
  session=session,
741
1027
  work_pool_name=work_pool_name,
@@ -756,6 +1042,78 @@ async def delete_work_queue(
756
1042
  # -----------------------------------------------------
757
1043
 
758
1044
 
1045
+ @router.websocket("/{work_pool_name}/workers/connect")
1046
+ async def worker_channel_connect(
1047
+ websocket: WebSocket,
1048
+ work_pool_name: str = Path(..., description="The work pool name"),
1049
+ db: PrefectDBInterface = Depends(provide_database_interface),
1050
+ ) -> None:
1051
+ websocket = await subscriptions.accept_prefect_socket(
1052
+ websocket,
1053
+ require_prefect_subprotocol=True,
1054
+ authentication_failed_reason=WorkerChannelCloseReason.AUTHENTICATION_FAILED.value,
1055
+ )
1056
+ if not websocket:
1057
+ return
1058
+
1059
+ try:
1060
+ hello = await _receive_worker_hello(websocket)
1061
+ async with worker_channel_utils.messaging.ephemeral_subscription(
1062
+ worker_channel_utils.WORKER_CHANNEL_SNAPSHOT_TOPIC,
1063
+ ) as consumer_kwargs:
1064
+ async with db.session_context(begin_transaction=True) as session:
1065
+ ready, work_pool_update_event = await _build_worker_ready_frame(
1066
+ session=session,
1067
+ work_pool_name=work_pool_name,
1068
+ hello=hello,
1069
+ )
1070
+
1071
+ if work_pool_update_event is not None:
1072
+ async with db.session_context() as session:
1073
+ work_pool = await models.workers.read_work_pool(
1074
+ session=session,
1075
+ work_pool_id=work_pool_update_event.work_pool_id,
1076
+ )
1077
+ assert work_pool is not None
1078
+ await models.workers.emit_work_pool_updated_event(
1079
+ session=session,
1080
+ work_pool=work_pool,
1081
+ changed_fields=work_pool_update_event.changed_fields,
1082
+ )
1083
+ await worker_channel_utils.publish_snapshot_invalidation(
1084
+ worker_channel_utils.WorkerChannelSnapshotInvalidation(
1085
+ work_pool_id=work_pool_update_event.work_pool_id,
1086
+ reason="work_pool_updated",
1087
+ )
1088
+ )
1089
+
1090
+ connection = worker_channel_utils.WorkerChannelConnection(
1091
+ websocket=websocket,
1092
+ db=db,
1093
+ work_pool_name=work_pool_name,
1094
+ work_pool_id=ready.payload.initial_snapshot.work_pool.id,
1095
+ consumer_id=hello.payload.consumer_id,
1096
+ worker_name=hello.payload.worker_name,
1097
+ )
1098
+ await connection.run(ready, consumer_kwargs)
1099
+
1100
+ except WorkerChannelSetupError as exc:
1101
+ logger.info("Worker channel setup failed: %s", exc.detail)
1102
+ await worker_channel_utils.close_worker_channel(websocket, exc.close_reason)
1103
+ except HTTPException as exc:
1104
+ logger.info("Worker channel setup failed HTTP validation: %s", exc.detail)
1105
+ await worker_channel_utils.close_worker_channel(
1106
+ websocket, WorkerChannelCloseReason.PROTOCOL_ERROR
1107
+ )
1108
+ except subscriptions.NORMAL_DISCONNECT_EXCEPTIONS:
1109
+ return
1110
+ except Exception:
1111
+ logger.exception("Worker channel setup failed due to a transient server error")
1112
+ await worker_channel_utils.close_worker_channel(
1113
+ websocket, WorkerChannelCloseReason.TRANSIENT_SERVER_ERROR
1114
+ )
1115
+
1116
+
759
1117
  @router.post(
760
1118
  "/{work_pool_name}/workers/heartbeat",
761
1119
  status_code=status.HTTP_204_NO_CONTENT,
@@ -780,23 +1138,14 @@ async def worker_heartbeat(
780
1138
  detail=f'Work pool "{work_pool_name}" not found.',
781
1139
  )
782
1140
 
783
- await models.workers.worker_heartbeat(
1141
+ await models.workers.record_worker_heartbeat(
784
1142
  session=session,
785
- work_pool_id=work_pool.id,
1143
+ work_pool=work_pool,
786
1144
  worker_name=name,
787
1145
  heartbeat_interval_seconds=heartbeat_interval_seconds,
1146
+ emit_status_change=emit_work_pool_status_event,
788
1147
  )
789
1148
 
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
1149
 
801
1150
  @router.post("/{work_pool_name}/workers/filter")
802
1151
  async def read_workers(
@@ -14,6 +14,7 @@ from pydantic_settings import (
14
14
  EnvSettingsSource,
15
15
  PydanticBaseSettingsSource,
16
16
  )
17
+ from pydantic_settings.exceptions import SettingsError
17
18
  from pydantic_settings.sources import (
18
19
  ENV_FILE_SENTINEL,
19
20
  ConfigFileSourceMixin,
@@ -230,7 +231,12 @@ class TomlConfigSettingsSourceBase(PydanticBaseSettingsSource, ConfigFileSourceM
230
231
  self.toml_data: dict[str, Any] = {}
231
232
 
232
233
  def _read_file(self, path: Path) -> dict[str, Any]:
233
- return _read_toml_file(path)
234
+ try:
235
+ return _read_toml_file(path)
236
+ except tomllib.TOMLDecodeError as e:
237
+ raise SettingsError(
238
+ f"Failed to load Prefect settings from {path}: invalid TOML ({e})"
239
+ ) from e
234
240
 
235
241
  @staticmethod
236
242
  def _field_is_dict_type(field: FieldInfo) -> bool:
@@ -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
- block_document = await client.read_block_document_by_name(
352
- name=block_document_name, block_type_slug=block_type_slug
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
- block_document = await client.read_block_document(block_document_id)
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
- return
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 self.state.healthy and self.snapshots_available:
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 self.state.healthy and self.snapshots_available:
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.7.2.dev2
3
+ Version: 3.7.2.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,12 +1,12 @@
1
1
  prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/AGENTS.md,sha256=rOU4L6B0ZdvnVmLr_eL394QlwfJkPh1OZswHppAjUN4,10063
3
- prefect/__init__.py,sha256=4e69hAJtYPoEeRxc8iNjzp5WIkmXP0i2SNiN3c97_Go,6703
3
+ prefect/__init__.py,sha256=Z8rwfLbEOLh-5WcznTZP3FG2-9UgGZxY-prj8sL0-Qk,6828
4
4
  prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
5
- prefect/_build_info.py,sha256=AHkrkaCbyqiZK5RthlP9QpdWA0STowHOiJKlYYABweI,185
5
+ prefect/_build_info.py,sha256=83Y1c4RyZdxkImmappMemLaqeSriPlx8EzdSL4cDLm8,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=ZzPxn2tINdlXTQo805V4rIlbXuNWxd7cdb3gTJxZIeY,12567
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=KfPiuuTXUV9d7B0zLp53NMuWiZAUNBsPk34jcSeYfBU,5799
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=kBeikKnDbUpVGuBJAGGCI8Jj3qaZDleJ2eZddLRuakc,2162
234
- prefect/events/actions.py,sha256=7SXrnc_I72IrUjQRg7W3WTH6olS0r26SAPwusRTtffE,9620
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
@@ -288,7 +288,7 @@ prefect/runner/_starter_engine.py,sha256=FjcHQe_iryk8BFpeiGw8MCWcLSRKhYwmb2iA4eo
288
288
  prefect/runner/_state_proposer.py,sha256=iW-SlB5EOWHErWbyRJwRTirfqwo0BJ1fAlbScVi6vcg,7225
289
289
  prefect/runner/_workspace_resolver.py,sha256=CbCtOULtkXb-XROyq16HOwG7q-8Jk0Tc1_nAum4WgyM,11263
290
290
  prefect/runner/_workspace_starter.py,sha256=M8R9JGjLJgS3qm3ZpzpyZ2_3aGGVaHXu55M25k7nxd8,9673
291
- prefect/runner/runner.py,sha256=9Z-0YHZ0V5QC5QYYdpiuZ0EqCDU0yeEEUCLNLyPwIRg,75756
291
+ prefect/runner/runner.py,sha256=O4krExcrpdS3T7pe6RLSfQbpkup8-SltDYS0E7p3obA,75799
292
292
  prefect/runner/server.py,sha256=YqvQjlxZZHyhSsqyaLvOy2NwTDg1hLSZB2PK3t8FJUg,3636
293
293
  prefect/runner/storage.py,sha256=yt_v2cVxYbazkZgfEdDQv0n-BS8rUvlFE8M_X3iN6NM,40525
294
294
  prefect/runtime/__init__.py,sha256=JswiTlYRup2zXOYu8AqJ7czKtgcw9Kxo0tTbS6aWCqY,407
@@ -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=jP7N62_NUxOp2aUQhl6FFTNYPP7X33XcT00vNQLOEM0,9054
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
@@ -327,8 +327,8 @@ prefect/server/api/task_workers.py,sha256=xFj0jRCLutccoMU6q4sMeY6pLFtrjytXL6Y7Sx
327
327
  prefect/server/api/templates.py,sha256=EW5aJOuvSXBeShd5VIygI1f9W0uTUpGb32ADrL9LG3k,1208
328
328
  prefect/server/api/validation.py,sha256=DPofXjLFejs_qusUQgzRkG89X2EFoviAy9J08pnNvyM,14543
329
329
  prefect/server/api/variables.py,sha256=8Ursuf20R5zXUS015ZexCH72K7R6Yv76ehkr_l3Xt0k,6186
330
- prefect/server/api/work_queues.py,sha256=BQQgiBWytrMgPhbHbOR_6Vx8T1V7JDIgeoZ4_LhmvCo,11276
331
- prefect/server/api/workers.py,sha256=SukmrFKvZro4OHHhiJE2z7iUvQSqlOPHgLXQCnJSNfM,30075
330
+ prefect/server/api/work_queues.py,sha256=wEb_Hx1MlTrg5juOguk5nvGKwLmTGSIHEUyt_-2GJiE,11957
331
+ prefect/server/api/workers.py,sha256=7ZsWWLsDqf5ZhcfeH7YEEPKRmQlert_Qec8ULINoafc,42914
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
@@ -344,7 +344,7 @@ prefect/settings/context.py,sha256=VtMJsBtjwq_P3La9_SRYedBkyjmMNqV_4X_U4h_-6wM,2
344
344
  prefect/settings/legacy.py,sha256=KG00GwaURl1zbwfCKAjwNRdJjB2UdTyo80gYF7U60jk,5693
345
345
  prefect/settings/profiles.py,sha256=ENL59QPCMH5YTD5q6rG8QBc_5Yw4ZmOmqsnxrFpRNvg,12860
346
346
  prefect/settings/profiles.toml,sha256=kTvqDNMzjH3fsm5OEI-NKY4dMmipor5EvQXRB6rPEjY,522
347
- prefect/settings/sources.py,sha256=N4Ye3czVzB12wwj6B1XIOfMV_D9BPjEe88zkdXKaNTc,14797
347
+ prefect/settings/sources.py,sha256=0EUJoa78Qr3CVmIdLGnqDCu8z_XoXq1evn8dHlD1yIo,15051
348
348
  prefect/settings/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
349
349
  prefect/settings/models/_defaults.py,sha256=Mj_ctL3UhMRp-wK_qW2lt4Akf55SAX0qFg_doPOFgHw,4856
350
350
  prefect/settings/models/api.py,sha256=XOxZDtNKeXgOMtvlr6QBJ1_UsvsXad3m0ECgZ0vAfaA,1796
@@ -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=8JR-OpK1Pblxp0AGpbLp48R5KDSBmUhWSIZs8nfkYbs,18091
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=zJWxpfwWxL21y3HpVdSdp-ephTdGCbt3qGyP_Op-ZlA,78395
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=Wo5QuQ9h7gJlq1FLzrHHGbqTRgPS7mJA31svSBkfHBY,15699
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=4VPYQbnhHeB2W4mJr-4qb-XR-YnaigUKjdAvJQqV0iw,14708
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.dev2.dist-info/METADATA,sha256=Wy-hi6qLcnTmx0g139Ic9N6ArEO0VYVs5eXdOAkz3ec,7502
442
- prefect_client-3.7.2.dev2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
443
- prefect_client-3.7.2.dev2.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
444
- prefect_client-3.7.2.dev2.dist-info/RECORD,,
441
+ prefect_client-3.7.2.dev4.dist-info/METADATA,sha256=IxISqFhu8Fotf2r0-YAEjZDEXIpM2WCNwYLAS-gBGMw,7502
442
+ prefect_client-3.7.2.dev4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
443
+ prefect_client-3.7.2.dev4.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
444
+ prefect_client-3.7.2.dev4.dist-info/RECORD,,