prefect 3.7.3.dev7__py3-none-any.whl → 3.7.4.dev2__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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.7.3.dev7"
3
- __build_date__ = "2026-06-01 09:59:22.915411+00:00"
4
- __git_commit__ = "2c89d0976e5c18361c9091ed7819336608284830"
2
+ __version__ = "3.7.4.dev2"
3
+ __build_date__ = "2026-06-03 09:46:04.741454+00:00"
4
+ __git_commit__ = "24ebdd47271b0f77b3704903bfadb3374e4a487f"
5
5
  __dirty__ = False
@@ -64,7 +64,7 @@ async def init(
64
64
  recipe_paths = prefect.__module_path__ / "deployments" / "recipes"
65
65
 
66
66
  for field in fields:
67
- key, value = field.split("=")
67
+ key, value = field.split("=", 1)
68
68
  inputs[key] = value
69
69
 
70
70
  from prefect.cli._prompts import prompt_select_from_table
prefect/cli/work_pool.py CHANGED
@@ -1379,7 +1379,9 @@ async def storage_configure_s3(
1379
1379
  " --aws-credentials-block-name to use default credentials."
1380
1380
  )
1381
1381
 
1382
- result_storage_block_document_name = f"default-{work_pool_name}-result-storage"
1382
+ result_storage_block_document_name = (
1383
+ f"default-{work_pool_name.lower()}-result-storage"
1384
+ )
1383
1385
  # Always set `credentials` explicitly (a $ref for a named block, or
1384
1386
  # an empty dict for ambient auth). Setting it on every run clears any
1385
1387
  # stale credential reference from a prior --aws-credentials-block-name
@@ -1518,7 +1520,9 @@ async def storage_configure_gcs(
1518
1520
  " --gcp-credentials-block-name to use default credentials."
1519
1521
  )
1520
1522
 
1521
- result_storage_block_document_name = f"default-{work_pool_name}-result-storage"
1523
+ result_storage_block_document_name = (
1524
+ f"default-{work_pool_name.lower()}-result-storage"
1525
+ )
1522
1526
  # Always set `gcp_credentials` explicitly (a $ref for a named block,
1523
1527
  # or an empty dict for ambient auth). Setting it on every run clears
1524
1528
  # any stale credential reference from a prior
@@ -1652,7 +1656,9 @@ async def storage_configure_azure_blob_storage(
1652
1656
  " `prefect block create azure-blob-storage-credentials`."
1653
1657
  )
1654
1658
 
1655
- result_storage_block_document_name = f"default-{work_pool_name}-result-storage"
1659
+ result_storage_block_document_name = (
1660
+ f"default-{work_pool_name.lower()}-result-storage"
1661
+ )
1656
1662
  block_data = {
1657
1663
  "container_name": container,
1658
1664
  "credentials": {
@@ -10,6 +10,8 @@ from prefect.exceptions import ObjectNotFound
10
10
  if TYPE_CHECKING:
11
11
  from uuid import UUID
12
12
 
13
+ from prefect.client.schemas.sorting import AutomationSort
14
+ from prefect.events.filters import AutomationFilter
13
15
  from prefect.events.schemas.automations import Automation, AutomationCore
14
16
 
15
17
 
@@ -37,8 +39,24 @@ class AutomationClient(BaseClient):
37
39
  )
38
40
  response.raise_for_status()
39
41
 
40
- def read_automations(self) -> list["Automation"]:
41
- response = self.request("POST", "/automations/filter")
42
+ def read_automations(
43
+ self,
44
+ *,
45
+ automations: "AutomationFilter | None" = None,
46
+ sort: "AutomationSort | None" = None,
47
+ limit: int | None = None,
48
+ offset: int = 0,
49
+ ) -> list["Automation"]:
50
+ body: dict[str, object] = {
51
+ "automations": (
52
+ automations.model_dump(mode="json") if automations else None
53
+ ),
54
+ "sort": sort,
55
+ "limit": limit,
56
+ "offset": offset,
57
+ }
58
+
59
+ response = self.request("POST", "/automations/filter", json=body)
42
60
  response.raise_for_status()
43
61
  from prefect.events.schemas.automations import Automation
44
62
 
@@ -196,8 +214,24 @@ class AutomationAsyncClient(BaseAsyncClient):
196
214
  )
197
215
  response.raise_for_status()
198
216
 
199
- async def read_automations(self) -> list["Automation"]:
200
- response = await self.request("POST", "/automations/filter")
217
+ async def read_automations(
218
+ self,
219
+ *,
220
+ automations: "AutomationFilter | None" = None,
221
+ sort: "AutomationSort | None" = None,
222
+ limit: int | None = None,
223
+ offset: int = 0,
224
+ ) -> list["Automation"]:
225
+ body: dict[str, object] = {
226
+ "automations": (
227
+ automations.model_dump(mode="json") if automations else None
228
+ ),
229
+ "sort": sort,
230
+ "limit": limit,
231
+ "offset": offset,
232
+ }
233
+
234
+ response = await self.request("POST", "/automations/filter", json=body)
201
235
  response.raise_for_status()
202
236
  from prefect.events.schemas.automations import Automation
203
237
 
prefect/events/filters.py CHANGED
@@ -32,6 +32,25 @@ class AutomationFilterName(PrefectBaseModel):
32
32
  )
33
33
 
34
34
 
35
+ class AutomationFilterTags(PrefectBaseModel):
36
+ """Filter by `Automation.tags`."""
37
+
38
+ all_: Optional[list[str]] = Field(
39
+ default=None,
40
+ examples=[["tag-1", "tag-2"]],
41
+ description="A list of tags. Automations will be returned only if their tags are a superset of the list",
42
+ )
43
+ any_: Optional[list[str]] = Field(
44
+ default=None,
45
+ examples=[["tag-1", "tag-2"]],
46
+ description="A list of tags. Automations will be returned if their tags contain any of the tags in the list",
47
+ )
48
+ is_null_: Optional[bool] = Field(
49
+ default=None,
50
+ description="If true, only include automations without tags",
51
+ )
52
+
53
+
35
54
  class AutomationFilter(PrefectBaseModel):
36
55
  name: Optional[AutomationFilterName] = Field(
37
56
  default=None, description="Filter criteria for `Automation.name`"
@@ -39,6 +58,9 @@ class AutomationFilter(PrefectBaseModel):
39
58
  created: Optional[AutomationFilterCreated] = Field(
40
59
  default=None, description="Filter criteria for `Automation.created`"
41
60
  )
61
+ tags: Optional[AutomationFilterTags] = Field(
62
+ default=None, description="Filter criteria for `Automation.tags`"
63
+ )
42
64
 
43
65
 
44
66
  class EventDataFilter(PrefectBaseModel, extra="forbid"): # type: ignore[call-arg]
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import os
4
5
  import shutil
6
+ import site
5
7
  import subprocess
6
8
  import sys
9
+ import sysconfig
7
10
  from contextlib import contextmanager
8
11
  from pathlib import Path
9
12
  from typing import TYPE_CHECKING, Any, Iterable, Iterator
@@ -109,14 +112,83 @@ def _workspace_sys_path(workspace: PreparedWorkspace) -> list[str]:
109
112
  return entries
110
113
 
111
114
 
115
+ @functools.lru_cache(maxsize=1)
116
+ def _stdlib_prefixes() -> tuple[str, ...]:
117
+ """Resolved stdlib directory prefixes whose children should not land on PYTHONPATH."""
118
+ roots: set[str] = set()
119
+ paths = sysconfig.get_paths()
120
+ for key in ("stdlib", "platstdlib"):
121
+ val = paths.get(key)
122
+ if val:
123
+ roots.add(str(Path(val).resolve()))
124
+ return tuple(sorted(roots))
125
+
126
+
127
+ @functools.lru_cache(maxsize=1)
128
+ def _site_packages_dirs() -> tuple[str, ...]:
129
+ """Resolved site-packages directories that should always be kept."""
130
+ dirs: set[str] = set()
131
+ paths = sysconfig.get_paths()
132
+ for key in ("purelib", "platlib"):
133
+ val = paths.get(key)
134
+ if val:
135
+ dirs.add(str(Path(val).resolve()))
136
+ try:
137
+ for sp in site.getsitepackages():
138
+ dirs.add(str(Path(sp).resolve()))
139
+ except AttributeError:
140
+ pass
141
+ try:
142
+ usp = site.getusersitepackages()
143
+ if isinstance(usp, str):
144
+ dirs.add(str(Path(usp).resolve()))
145
+ except AttributeError:
146
+ pass
147
+ return tuple(sorted(dirs))
148
+
149
+
150
+ def _is_stdlib_path(entry: str) -> bool:
151
+ """True when *entry* is a stdlib, lib-dynload, or stdlib zip path.
152
+
153
+ Site-packages directories that live under the stdlib tree are kept.
154
+ Only interpreter stdlib zip archives next to stdlib directories are filtered;
155
+ user archives like `/app/python_deps.zip` are preserved.
156
+ """
157
+ if not entry:
158
+ return False
159
+
160
+ resolved_path = Path(entry).resolve()
161
+ resolved = str(resolved_path)
162
+
163
+ for sp in _site_packages_dirs():
164
+ if resolved == sp or resolved.startswith(sp + os.sep):
165
+ return False
166
+
167
+ for root in _stdlib_prefixes():
168
+ if resolved == root or resolved.startswith(root + os.sep):
169
+ return True
170
+
171
+ interpreter_zip_name = f"python{sys.version_info.major}{sys.version_info.minor}.zip"
172
+ if resolved_path.name == interpreter_zip_name:
173
+ resolved_parent = str(resolved_path.parent)
174
+ stdlib_parents = {str(Path(r).parent) for r in _stdlib_prefixes()}
175
+ if resolved_parent in stdlib_parents:
176
+ return True
177
+
178
+ return False
179
+
180
+
112
181
  def workspace_environment(workspace: PreparedWorkspace) -> dict[str, str]:
113
182
  environment = dict(workspace.environment)
114
- pythonpath_entries = _workspace_sys_path(workspace)
183
+ pythonpath_entries: list[str] = []
184
+ candidate_entries = _workspace_sys_path(workspace)
115
185
  existing_pythonpath = environment.get("PYTHONPATH")
116
186
  if existing_pythonpath:
117
- for entry in existing_pythonpath.split(os.pathsep):
118
- if entry and entry not in pythonpath_entries:
119
- pythonpath_entries.append(entry)
187
+ candidate_entries.extend(existing_pythonpath.split(os.pathsep))
188
+
189
+ for entry in candidate_entries:
190
+ if entry and entry not in pythonpath_entries and not _is_stdlib_path(entry):
191
+ pythonpath_entries.append(entry)
120
192
 
121
193
  environment["PYTHONPATH"] = os.pathsep.join(pythonpath_entries)
122
194
  return environment
prefect/runner/runner.py CHANGED
@@ -1701,7 +1701,7 @@ class Runner:
1701
1701
  ):
1702
1702
  return self._deployment_registry.get_flow(flow_run.deployment_id)
1703
1703
  return await load_flow_from_flow_run(
1704
- self._client, flow_run, storage_base_path=str(self._tmp_dir)
1704
+ flow_run, storage_base_path=str(self._tmp_dir)
1705
1705
  )
1706
1706
 
1707
1707
  self._hook_runner = HookRunner(resolve_flow=_resolve_flow_for_hooks)
@@ -118,3 +118,5 @@ from prefect.settings.context import get_current_settings
118
118
  settings = get_current_settings()
119
119
  value = settings.server.services.my_service.my_setting
120
120
  ```
121
+
122
+ `temporary_settings()` accepts both legacy `Setting` objects and dotted-path string keys (e.g., `"server.api.host"`) or uppercase env var names (e.g., `"PREFECT_SERVER_API_HOST"`). Prefer string keys in new tests to avoid importing legacy `Setting` objects.
@@ -20,8 +20,8 @@ class ServerWorkerChannelSettings(PrefectBaseSettings):
20
20
  description=(
21
21
  "The module to use for storing worker cleanup delivery messages. "
22
22
  "The default in-memory backend stores messages, leases, wakeups, "
23
- "and dead-letter entries only in the current server process; use a "
24
- "Redis-backed storage module for high availability or restart-safe "
23
+ "and dead-letter entries only in the current server process; use "
24
+ "an external storage module for high availability or restart-safe "
25
25
  "cleanup delivery."
26
26
  ),
27
27
  )
@@ -47,8 +47,8 @@ class ServerWorkerChannelSettings(PrefectBaseSettings):
47
47
  description=(
48
48
  "How long completed cleanup idempotency records are retained after "
49
49
  "acknowledgement. None keeps them for the lifetime of the current "
50
- "server process. The in-memory backend does not survive restart; "
51
- "use Redis for high availability or restart-safe cleanup "
50
+ "server process. The in-memory backend does not survive restart; use "
51
+ "external storage for high availability or restart-safe cleanup "
52
52
  "idempotency."
53
53
  ),
54
54
  )
@@ -1 +1,2 @@
1
1
  from .blocks import BlockStandardTestSuite
2
+ from .worker_cleanup_queue import WorkerCleanupQueueStandardTestSuite
@@ -0,0 +1,622 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Callable, Iterator
5
+ from contextlib import AbstractContextManager, contextmanager
6
+ from datetime import datetime, timedelta
7
+ from typing import Any, Literal, Protocol
8
+ from uuid import UUID, uuid4
9
+
10
+ import pytest
11
+
12
+ from prefect.client.schemas.worker_channel import (
13
+ CANCELLING_TIMEOUT_TEARDOWN,
14
+ PENDING_CLAIM_TEARDOWN,
15
+ )
16
+ from prefect.server.worker_communication.cleanup_queue import WorkerCleanupQueue
17
+ from prefect.settings import (
18
+ PREFECT_SERVER_WORKER_CHANNEL_CLEANUP_COMPLETED_IDEMPOTENCY_RETENTION_SECONDS,
19
+ PREFECT_SERVER_WORKER_CHANNEL_CLEANUP_LEASE_SECONDS,
20
+ PREFECT_SERVER_WORKER_CHANNEL_CLEANUP_MAX_DELIVERY_ATTEMPTS,
21
+ temporary_settings,
22
+ )
23
+
24
+ CleanupPolicySettings = Callable[..., AbstractContextManager[None]]
25
+
26
+
27
+ class QueueTestClock(Protocol):
28
+ current: datetime
29
+
30
+ def advance(self, duration: timedelta) -> None: ...
31
+
32
+
33
+ def _target() -> dict[str, str]:
34
+ return {"flow_run_id": str(uuid4())}
35
+
36
+
37
+ async def _enqueue_message(
38
+ queue: WorkerCleanupQueue,
39
+ *,
40
+ work_pool_id: UUID | None = None,
41
+ message_id: UUID | None = None,
42
+ idempotency_key: str = "cleanup-key",
43
+ work_queue_id: UUID | None = None,
44
+ ) -> UUID:
45
+ message_id = message_id or uuid4()
46
+ await queue.enqueue(
47
+ message_id=message_id,
48
+ idempotency_key=idempotency_key,
49
+ work_pool_id=work_pool_id or uuid4(),
50
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
51
+ target=_target(),
52
+ work_queue_id=work_queue_id,
53
+ )
54
+ return message_id
55
+
56
+
57
+ class WorkerCleanupQueueStandardTestSuite:
58
+ @pytest.fixture
59
+ def cleanup_policy_settings(self) -> CleanupPolicySettings:
60
+ @contextmanager
61
+ def settings(
62
+ *,
63
+ lease_seconds: float | None = None,
64
+ max_delivery_attempts: int | None = None,
65
+ completed_idempotency_retention_seconds: float | None = None,
66
+ ) -> Iterator[None]:
67
+ values: dict[Any, Any] = {}
68
+ if lease_seconds is not None:
69
+ values[PREFECT_SERVER_WORKER_CHANNEL_CLEANUP_LEASE_SECONDS] = (
70
+ lease_seconds
71
+ )
72
+ if max_delivery_attempts is not None:
73
+ values[PREFECT_SERVER_WORKER_CHANNEL_CLEANUP_MAX_DELIVERY_ATTEMPTS] = (
74
+ max_delivery_attempts
75
+ )
76
+ if completed_idempotency_retention_seconds is not None:
77
+ values[
78
+ PREFECT_SERVER_WORKER_CHANNEL_CLEANUP_COMPLETED_IDEMPOTENCY_RETENTION_SECONDS
79
+ ] = completed_idempotency_retention_seconds
80
+
81
+ with temporary_settings(values):
82
+ yield
83
+
84
+ return settings
85
+
86
+ async def test_enqueue_is_idempotent_for_stable_cleanup_keys(
87
+ self,
88
+ queue: WorkerCleanupQueue,
89
+ ) -> None:
90
+ work_pool_id = uuid4()
91
+ first_message_id = uuid4()
92
+ second_message_id = uuid4()
93
+
94
+ first = await queue.enqueue(
95
+ message_id=first_message_id,
96
+ idempotency_key="flow-run-cleanup",
97
+ work_pool_id=work_pool_id,
98
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
99
+ target=_target(),
100
+ )
101
+ second = await queue.enqueue(
102
+ message_id=second_message_id,
103
+ idempotency_key="flow-run-cleanup",
104
+ work_pool_id=work_pool_id,
105
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
106
+ target=_target(),
107
+ )
108
+
109
+ assert second.message_id == first.message_id
110
+ assert second.message_id != second_message_id
111
+
112
+ async def test_enqueue_deep_copies_payload_state(
113
+ self,
114
+ queue: WorkerCleanupQueue,
115
+ ) -> None:
116
+ work_pool_id = uuid4()
117
+ message_id = uuid4()
118
+ target = {"flow_run": {"id": str(uuid4())}}
119
+ data = {"resources": [{"id": str(uuid4())}]}
120
+ original_target = {"flow_run": dict(target["flow_run"])}
121
+ original_data = {"resources": [dict(data["resources"][0])]}
122
+
123
+ await queue.enqueue(
124
+ message_id=message_id,
125
+ idempotency_key="flow-run-cleanup",
126
+ work_pool_id=work_pool_id,
127
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
128
+ target=target,
129
+ data=data,
130
+ )
131
+
132
+ target["flow_run"]["id"] = str(uuid4())
133
+ data["resources"][0]["id"] = str(uuid4())
134
+
135
+ message = await queue.read_message(
136
+ work_pool_id=work_pool_id, message_id=message_id
137
+ )
138
+
139
+ assert message is not None
140
+ assert message.target == original_target
141
+ assert message.data == original_data
142
+
143
+ async def test_reserve_commits_delivery_count_and_single_active_reservation(
144
+ self,
145
+ queue: WorkerCleanupQueue,
146
+ clock: QueueTestClock,
147
+ ) -> None:
148
+ work_pool_id = uuid4()
149
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
150
+
151
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
152
+
153
+ assert reservation is not None
154
+ assert reservation.message_id == message_id
155
+ assert reservation.delivery_count == 1
156
+ assert reservation.lease_expires_at == clock.current + timedelta(seconds=30)
157
+ assert len(reservation.reservation_token) > 32
158
+
159
+ assert await queue.reserve(work_pool_id=work_pool_id) is None
160
+
161
+ async def test_reservation_operations_require_current_token(
162
+ self,
163
+ queue: WorkerCleanupQueue,
164
+ ) -> None:
165
+ work_pool_id = uuid4()
166
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
167
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
168
+ assert reservation is not None
169
+
170
+ rejected = await queue.ack(
171
+ work_pool_id=work_pool_id,
172
+ message_id=message_id,
173
+ reservation_token="not-the-current-token",
174
+ )
175
+
176
+ assert rejected.status == "invalid_token"
177
+ assert (
178
+ await queue.read_message(work_pool_id=work_pool_id, message_id=message_id)
179
+ is not None
180
+ )
181
+
182
+ accepted = await queue.ack(
183
+ work_pool_id=work_pool_id,
184
+ message_id=message_id,
185
+ reservation_token=reservation.reservation_token,
186
+ )
187
+
188
+ assert accepted.status == "accepted"
189
+ assert (
190
+ await queue.read_message(work_pool_id=work_pool_id, message_id=message_id)
191
+ is None
192
+ )
193
+
194
+ @pytest.mark.parametrize("operation", ["ack", "release", "renew"])
195
+ async def test_reservation_operations_require_matching_work_pool_scope(
196
+ self,
197
+ queue: WorkerCleanupQueue,
198
+ operation: Literal["ack", "release", "renew"],
199
+ ) -> None:
200
+ work_pool_id = uuid4()
201
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
202
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
203
+ assert reservation is not None
204
+
205
+ other_work_pool_id = uuid4()
206
+ if operation == "ack":
207
+ rejected = await queue.ack(
208
+ work_pool_id=other_work_pool_id,
209
+ message_id=message_id,
210
+ reservation_token=reservation.reservation_token,
211
+ )
212
+ elif operation == "release":
213
+ rejected = await queue.release(
214
+ work_pool_id=other_work_pool_id,
215
+ message_id=message_id,
216
+ reservation_token=reservation.reservation_token,
217
+ reason="wrong_pool",
218
+ )
219
+ else:
220
+ rejected = await queue.renew(
221
+ work_pool_id=other_work_pool_id,
222
+ message_id=message_id,
223
+ reservation_token=reservation.reservation_token,
224
+ )
225
+
226
+ assert rejected.status in {"unauthorized", "not_found"}
227
+ assert rejected.reason in {"work_pool_mismatch", "message_not_found"}
228
+ assert (
229
+ await queue.read_message(work_pool_id=work_pool_id, message_id=message_id)
230
+ is not None
231
+ )
232
+
233
+ accepted = await queue.ack(
234
+ work_pool_id=work_pool_id,
235
+ message_id=message_id,
236
+ reservation_token=reservation.reservation_token,
237
+ )
238
+ assert accepted.status == "accepted"
239
+
240
+ async def test_read_helpers_require_matching_work_pool_scope(
241
+ self,
242
+ queue: WorkerCleanupQueue,
243
+ cleanup_policy_settings: CleanupPolicySettings,
244
+ ) -> None:
245
+ work_pool_id = uuid4()
246
+ other_work_pool_id = uuid4()
247
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
248
+
249
+ message = await queue.read_message(
250
+ work_pool_id=work_pool_id, message_id=message_id
251
+ )
252
+ wrong_pool_message = await queue.read_message(
253
+ work_pool_id=other_work_pool_id, message_id=message_id
254
+ )
255
+
256
+ assert message is not None
257
+ assert wrong_pool_message is None
258
+
259
+ with cleanup_policy_settings(max_delivery_attempts=1):
260
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
261
+ assert reservation is not None
262
+ result = await queue.release(
263
+ work_pool_id=work_pool_id,
264
+ message_id=message_id,
265
+ reservation_token=reservation.reservation_token,
266
+ reason="unsupported_cleanup_kind",
267
+ )
268
+
269
+ dead_letter = await queue.read_dead_letter(
270
+ work_pool_id=work_pool_id, message_id=message_id
271
+ )
272
+ wrong_pool_dead_letter = await queue.read_dead_letter(
273
+ work_pool_id=other_work_pool_id, message_id=message_id
274
+ )
275
+
276
+ assert result.status == "dead_lettered"
277
+ assert dead_letter is not None
278
+ assert wrong_pool_dead_letter is None
279
+
280
+ async def test_reserve_prefers_matching_work_queue_but_falls_back_to_pool(
281
+ self,
282
+ queue: WorkerCleanupQueue,
283
+ ) -> None:
284
+ work_pool_id = uuid4()
285
+ preferred_work_queue_id = uuid4()
286
+ fallback_work_queue_id = uuid4()
287
+ fallback_message_id = await _enqueue_message(
288
+ queue,
289
+ work_pool_id=work_pool_id,
290
+ idempotency_key="fallback-cleanup",
291
+ work_queue_id=fallback_work_queue_id,
292
+ )
293
+ preferred_message_id = await _enqueue_message(
294
+ queue,
295
+ work_pool_id=work_pool_id,
296
+ idempotency_key="preferred-cleanup",
297
+ work_queue_id=preferred_work_queue_id,
298
+ )
299
+
300
+ preferred = await queue.reserve(
301
+ work_pool_id=work_pool_id,
302
+ preferred_work_queue_ids=[preferred_work_queue_id],
303
+ )
304
+ assert preferred is not None
305
+ assert preferred.message_id == preferred_message_id
306
+
307
+ accepted = await queue.ack(
308
+ work_pool_id=work_pool_id,
309
+ message_id=preferred_message_id,
310
+ reservation_token=preferred.reservation_token,
311
+ )
312
+ assert accepted.status == "accepted"
313
+
314
+ fallback = await queue.reserve(
315
+ work_pool_id=work_pool_id,
316
+ preferred_work_queue_ids=[preferred_work_queue_id],
317
+ )
318
+
319
+ assert fallback is not None
320
+ assert fallback.message_id == fallback_message_id
321
+
322
+ async def test_enqueue_after_ack_keeps_idempotency_key_completed(
323
+ self,
324
+ queue: WorkerCleanupQueue,
325
+ ) -> None:
326
+ work_pool_id = uuid4()
327
+ message_id = uuid4()
328
+ idempotency_key = "flow-run-cleanup"
329
+ first = await queue.enqueue(
330
+ message_id=message_id,
331
+ idempotency_key=idempotency_key,
332
+ work_pool_id=work_pool_id,
333
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
334
+ target=_target(),
335
+ )
336
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
337
+ assert reservation is not None
338
+ accepted = await queue.ack(
339
+ work_pool_id=work_pool_id,
340
+ message_id=message_id,
341
+ reservation_token=reservation.reservation_token,
342
+ )
343
+ wakeup_sequence = await queue.read_wakeup_sequence(work_pool_id)
344
+
345
+ duplicate = await queue.enqueue(
346
+ message_id=uuid4(),
347
+ idempotency_key=idempotency_key,
348
+ work_pool_id=work_pool_id,
349
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
350
+ target=_target(),
351
+ )
352
+
353
+ assert accepted.status == "accepted"
354
+ assert duplicate.message_id == first.message_id
355
+ assert (
356
+ await queue.read_message(work_pool_id=work_pool_id, message_id=message_id)
357
+ is None
358
+ )
359
+ assert await queue.reserve(work_pool_id=work_pool_id) is None
360
+ assert await queue.read_wakeup_sequence(work_pool_id) == wakeup_sequence
361
+
362
+ async def test_completed_idempotency_retention_can_expire_tombstones(
363
+ self,
364
+ queue: WorkerCleanupQueue,
365
+ cleanup_policy_settings: CleanupPolicySettings,
366
+ ) -> None:
367
+ work_pool_id = uuid4()
368
+ message_id = uuid4()
369
+ idempotency_key = "flow-run-cleanup"
370
+ with cleanup_policy_settings(completed_idempotency_retention_seconds=0.0):
371
+ await queue.enqueue(
372
+ message_id=message_id,
373
+ idempotency_key=idempotency_key,
374
+ work_pool_id=work_pool_id,
375
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
376
+ target=_target(),
377
+ )
378
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
379
+ assert reservation is not None
380
+ accepted = await queue.ack(
381
+ work_pool_id=work_pool_id,
382
+ message_id=message_id,
383
+ reservation_token=reservation.reservation_token,
384
+ )
385
+ assert accepted.status == "accepted"
386
+
387
+ new_message_id = uuid4()
388
+ duplicate_after_retention = await queue.enqueue(
389
+ message_id=new_message_id,
390
+ idempotency_key=idempotency_key,
391
+ work_pool_id=work_pool_id,
392
+ kind=CANCELLING_TIMEOUT_TEARDOWN,
393
+ target=_target(),
394
+ )
395
+
396
+ assert duplicate_after_retention.message_id == new_message_id
397
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
398
+ assert reservation is not None
399
+ assert reservation.message_id == new_message_id
400
+
401
+ async def test_release_makes_message_eligible_for_redelivery(
402
+ self,
403
+ queue: WorkerCleanupQueue,
404
+ ) -> None:
405
+ work_pool_id = uuid4()
406
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
407
+ first = await queue.reserve(work_pool_id=work_pool_id)
408
+ assert first is not None
409
+
410
+ released = await queue.release(
411
+ work_pool_id=work_pool_id,
412
+ message_id=message_id,
413
+ reservation_token=first.reservation_token,
414
+ reason="cannot_act",
415
+ )
416
+ second = await queue.reserve(work_pool_id=work_pool_id)
417
+
418
+ assert released.status == "accepted"
419
+ assert second is not None
420
+ assert second.message_id == message_id
421
+ assert second.delivery_count == 2
422
+ assert second.reservation_token != first.reservation_token
423
+
424
+ async def test_release_moves_message_to_dlq_after_retry_limit(
425
+ self,
426
+ queue: WorkerCleanupQueue,
427
+ cleanup_policy_settings: CleanupPolicySettings,
428
+ ) -> None:
429
+ work_pool_id = uuid4()
430
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
431
+ with cleanup_policy_settings(max_delivery_attempts=1):
432
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
433
+ assert reservation is not None
434
+
435
+ result = await queue.release(
436
+ work_pool_id=work_pool_id,
437
+ message_id=message_id,
438
+ reservation_token=reservation.reservation_token,
439
+ reason="unsupported_cleanup_kind",
440
+ )
441
+ dead_letter = await queue.read_dead_letter(
442
+ work_pool_id=work_pool_id, message_id=message_id
443
+ )
444
+
445
+ assert result.status == "dead_lettered"
446
+ assert result.reason == "max_delivery_attempts_reached"
447
+ assert dead_letter is not None
448
+ assert dead_letter.final_delivery_count == 1
449
+ assert dead_letter.release_reason == "unsupported_cleanup_kind"
450
+ assert (
451
+ await queue.read_message(work_pool_id=work_pool_id, message_id=message_id)
452
+ is None
453
+ )
454
+
455
+ async def test_expired_leases_redeliver_then_dlq_at_retry_limit(
456
+ self,
457
+ queue: WorkerCleanupQueue,
458
+ clock: QueueTestClock,
459
+ cleanup_policy_settings: CleanupPolicySettings,
460
+ ) -> None:
461
+ work_pool_id = uuid4()
462
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
463
+ with cleanup_policy_settings(lease_seconds=10.0, max_delivery_attempts=2):
464
+ first = await queue.reserve(work_pool_id=work_pool_id)
465
+ assert first is not None
466
+
467
+ clock.advance(timedelta(seconds=11))
468
+ first_expiry = await queue.expire_leases()
469
+ second = await queue.reserve(work_pool_id=work_pool_id)
470
+ assert second is not None
471
+
472
+ clock.advance(timedelta(seconds=11))
473
+ second_expiry = await queue.expire_leases()
474
+ dead_letter = await queue.read_dead_letter(
475
+ work_pool_id=work_pool_id, message_id=message_id
476
+ )
477
+
478
+ assert [message.message_id for message in first_expiry.redelivered] == [
479
+ message_id
480
+ ]
481
+ assert first_expiry.dead_lettered == []
482
+ assert second.delivery_count == 2
483
+ assert second_expiry.redelivered == []
484
+ assert len(second_expiry.dead_lettered) == 1
485
+ assert dead_letter is not None
486
+ assert dead_letter.final_delivery_count == 2
487
+ assert await queue.reserve(work_pool_id=work_pool_id) is None
488
+
489
+ async def test_reserve_wakes_dispatchers_after_expiring_leases(
490
+ self,
491
+ queue: WorkerCleanupQueue,
492
+ clock: QueueTestClock,
493
+ cleanup_policy_settings: CleanupPolicySettings,
494
+ ) -> None:
495
+ work_pool_id = uuid4()
496
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
497
+
498
+ with cleanup_policy_settings(lease_seconds=10.0):
499
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
500
+ assert reservation is not None
501
+ sequence = await queue.read_wakeup_sequence(work_pool_id)
502
+
503
+ clock.advance(timedelta(seconds=11))
504
+ unmatched_reservation = await queue.reserve(
505
+ work_pool_id=work_pool_id,
506
+ cleanup_kinds=[PENDING_CLAIM_TEARDOWN],
507
+ )
508
+ wakeup = await queue.wait_for_wakeup(
509
+ work_pool_id, after=sequence, timeout=1
510
+ )
511
+
512
+ redelivered = await queue.reserve(work_pool_id=work_pool_id)
513
+
514
+ assert unmatched_reservation is None
515
+ assert wakeup is not None
516
+ assert wakeup.sequence == sequence + 1
517
+ assert redelivered is not None
518
+ assert redelivered.message_id == message_id
519
+
520
+ async def test_expire_leases_respects_limit_and_work_pool_scope(
521
+ self,
522
+ queue: WorkerCleanupQueue,
523
+ clock: QueueTestClock,
524
+ cleanup_policy_settings: CleanupPolicySettings,
525
+ ) -> None:
526
+ work_pool_id = uuid4()
527
+ other_work_pool_id = uuid4()
528
+ first_message_id = await _enqueue_message(
529
+ queue, work_pool_id=work_pool_id, idempotency_key="first-cleanup"
530
+ )
531
+ second_message_id = await _enqueue_message(
532
+ queue, work_pool_id=work_pool_id, idempotency_key="second-cleanup"
533
+ )
534
+ other_message_id = await _enqueue_message(
535
+ queue, work_pool_id=other_work_pool_id, idempotency_key="other-cleanup"
536
+ )
537
+
538
+ with cleanup_policy_settings(lease_seconds=10.0):
539
+ assert await queue.reserve(work_pool_id=work_pool_id) is not None
540
+ assert await queue.reserve(work_pool_id=work_pool_id) is not None
541
+ assert await queue.reserve(work_pool_id=other_work_pool_id) is not None
542
+
543
+ clock.advance(timedelta(seconds=11))
544
+ first_expiry = await queue.expire_leases(work_pool_id=work_pool_id, limit=1)
545
+ second_expiry = await queue.expire_leases(
546
+ work_pool_id=work_pool_id, limit=10
547
+ )
548
+ other_expiry = await queue.expire_leases(
549
+ work_pool_id=other_work_pool_id, limit=10
550
+ )
551
+
552
+ assert len(first_expiry.redelivered) == 1
553
+ assert len(second_expiry.redelivered) == 1
554
+ assert {
555
+ first_expiry.redelivered[0].message_id,
556
+ second_expiry.redelivered[0].message_id,
557
+ } == {first_message_id, second_message_id}
558
+ assert [message.message_id for message in other_expiry.redelivered] == [
559
+ other_message_id
560
+ ]
561
+
562
+ async def test_renew_extends_current_reservation(
563
+ self,
564
+ queue: WorkerCleanupQueue,
565
+ clock: QueueTestClock,
566
+ ) -> None:
567
+ work_pool_id = uuid4()
568
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
569
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
570
+ assert reservation is not None
571
+
572
+ clock.advance(timedelta(seconds=5))
573
+ result = await queue.renew(
574
+ work_pool_id=work_pool_id,
575
+ message_id=message_id,
576
+ reservation_token=reservation.reservation_token,
577
+ )
578
+
579
+ assert result.status == "accepted"
580
+ assert result.lease_expires_at == clock.current + timedelta(seconds=30)
581
+
582
+ async def test_operation_on_expired_lease_wakes_dispatchers(
583
+ self,
584
+ queue: WorkerCleanupQueue,
585
+ clock: QueueTestClock,
586
+ cleanup_policy_settings: CleanupPolicySettings,
587
+ ) -> None:
588
+ work_pool_id = uuid4()
589
+ message_id = await _enqueue_message(queue, work_pool_id=work_pool_id)
590
+ with cleanup_policy_settings(lease_seconds=10.0):
591
+ reservation = await queue.reserve(work_pool_id=work_pool_id)
592
+ assert reservation is not None
593
+ sequence = await queue.read_wakeup_sequence(work_pool_id)
594
+
595
+ clock.advance(timedelta(seconds=11))
596
+ result = await queue.renew(
597
+ work_pool_id=work_pool_id,
598
+ message_id=message_id,
599
+ reservation_token=reservation.reservation_token,
600
+ )
601
+ wakeup = await queue.wait_for_wakeup(work_pool_id, after=sequence, timeout=1)
602
+
603
+ assert result.status == "expired"
604
+ assert wakeup is not None
605
+ assert wakeup.sequence == sequence + 1
606
+
607
+ async def test_enqueue_wakes_local_dispatchers(
608
+ self,
609
+ queue: WorkerCleanupQueue,
610
+ ) -> None:
611
+ work_pool_id = uuid4()
612
+ sequence = await queue.read_wakeup_sequence(work_pool_id)
613
+ waiter = asyncio.create_task(
614
+ queue.wait_for_wakeup(work_pool_id, after=sequence, timeout=1)
615
+ )
616
+
617
+ await _enqueue_message(queue, work_pool_id=work_pool_id)
618
+ wakeup = await waiter
619
+
620
+ assert wakeup is not None
621
+ assert wakeup.work_pool_id == work_pool_id
622
+ assert wakeup.sequence == sequence + 1
@@ -333,7 +333,8 @@ def parse_obj_as(
333
333
  origin: Optional[Any] = get_origin(type_)
334
334
  if origin is list and isinstance(data, dict):
335
335
  values_dict: dict[Any, Any] = data
336
- data = next(iter(values_dict.values()))
336
+ if values_dict:
337
+ data = next(iter(values_dict.values()))
337
338
 
338
339
  parser: Callable[[Any], T] = getattr(adapter, f"validate_{mode}")
339
340
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect
3
- Version: 3.7.3.dev7
3
+ Version: 3.7.4.dev2
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
@@ -2,7 +2,7 @@ prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/AGENTS.md,sha256=SNF9LRWtYJ2pIHUVswgtMaWlloKszhNTL6dhPreh5kc,10484
3
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=MkksLScbP0JPUAsPt3pRsTduor62DKNNmUQh0yIC2BQ,185
5
+ prefect/_build_info.py,sha256=Iq3OJkGs6udHR0PUIZry5OcTNeFp4y0M1AD1jcaT2K8,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
@@ -184,7 +184,7 @@ prefect/cli/task.py,sha256=Oqz7o4sAVV8ZuLy3HF0osE_cAral2Zo3KBVmSwrLRyE,3669
184
184
  prefect/cli/task_run.py,sha256=q3kFAdka1kaoPB-6-797Qe1XyXuZDl3Gh5fVvQmXzNg,11363
185
185
  prefect/cli/variable.py,sha256=mosmUsu2jbj57tspicIjLD3dXjglP-C97bB5TsFyQgc,6478
186
186
  prefect/cli/version.py,sha256=SQKKtxYqqYoNMviSVz7U0r80GD0noEeVn4v2duYfCyk,4135
187
- prefect/cli/work_pool.py,sha256=s6mLR-VHO5UJOsgfCn9piVkt2rp2ojuY2kdAtd-i58E,60144
187
+ prefect/cli/work_pool.py,sha256=1cdpbzVsfr0eqrT-cB8lnIc43h7o17Kgjx50FbzpZ7A,60240
188
188
  prefect/cli/work_queue.py,sha256=k_gsedmldy23EMCT7xRDXbuhntPzcxqww5zyw2tNoNA,28200
189
189
  prefect/cli/worker.py,sha256=hfj2gCSBahcfz1wj0VHd6w2HkKSqg9MLvTT0oUs0aHs,7697
190
190
  prefect/cli/cloud/__init__.py,sha256=RuE_t07oi1axjyuKr8ev_B83jBePW5fHB001RP_mtsA,14978
@@ -193,7 +193,7 @@ prefect/cli/cloud/ip_allowlist.py,sha256=xNWvDXXLF159q3MgLF7KqjAe2XjyDqEXfGjC72I
193
193
  prefect/cli/cloud/webhook.py,sha256=j0nzupRem5gr1cLutrO5ALhDaiTeNWTBusq1c-UBArI,8718
194
194
  prefect/cli/deploy/__init__.py,sha256=plVbJQ0ZDPsTFCRL7SYZXUhXaUi0KNU86dlydBeKGjs,121
195
195
  prefect/cli/deploy/_actions.py,sha256=JLseIc8MdS0kZtAs5doSxVfCFhUIwnD87twtLYA4Wsw,10321
196
- prefect/cli/deploy/_commands.py,sha256=J8vnIRLPJ-yIuQ-KGA4vJ91MnP6yTm2MNvDAIievM40,16277
196
+ prefect/cli/deploy/_commands.py,sha256=ofKDJ0KVTdEZy3BraJoaltKjxhZbDgTrq8xg4dk4IGg,16280
197
197
  prefect/cli/deploy/_config.py,sha256=d1LEuz1tWmYNqYTp3Gwdly0Pg-Wg-RsEmN5AiiF8jG0,19245
198
198
  prefect/cli/deploy/_core.py,sha256=0LD5UUynF07cFfbzx4j1IVQgaz4mHEmS7u0pKnsgLkQ,20417
199
199
  prefect/cli/deploy/_models.py,sha256=9yuv9RwOxw5u8ldbYpa23Z-60wBtxRA887Jy6dzVmII,5119
@@ -230,7 +230,7 @@ prefect/client/orchestration/routes.py,sha256=YTA183Auz6LN4fqq6uLWxGLupOv6PWf0Ct
230
230
  prefect/client/orchestration/_artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
231
231
  prefect/client/orchestration/_artifacts/client.py,sha256=BFSWJkJ4P3SNrWfjIkuEXl5WDVhFnQRDCxquQGu_aJU,11056
232
232
  prefect/client/orchestration/_automations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
233
- prefect/client/orchestration/_automations/client.py,sha256=z4WC7Dov6c75SSmX_awXi4bFYcSxwPwimEbWGEabdkk,10931
233
+ prefect/client/orchestration/_automations/client.py,sha256=cGBIbicgshufID3UZrybavmyI9B4sovPY6i3hSy4Qbc,11947
234
234
  prefect/client/orchestration/_blocks_documents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
235
235
  prefect/client/orchestration/_blocks_documents/client.py,sha256=HTGUIsOkHbe-Vh4hod6oN4VnKNSaOyVuhvToDDGOZ3M,11474
236
236
  prefect/client/orchestration/_blocks_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -310,7 +310,7 @@ prefect/events/AGENTS.md,sha256=ZCrjaA67BQGdDzO5R7yJB7HO5Ahb8z7A3sT8yuKQ6V0,3049
310
310
  prefect/events/__init__.py,sha256=e3aIcHzTfseOmq_A3hM_jSPNYP_OdndJErzjcdrMuAc,2202
311
311
  prefect/events/actions.py,sha256=Ii5P4FX10Xu1nzU0e7okA1KC1eVIHhzExubFurrOr_Q,9785
312
312
  prefect/events/clients.py,sha256=gL8EClV_ryULkXvnR8DU0nrOH1kTd8FrMPYKy3c8SdQ,33354
313
- prefect/events/filters.py,sha256=_vweQbeRxK8KiyKIPb6fYEtA5crbaUxlYPkWorFmMm4,9201
313
+ prefect/events/filters.py,sha256=YiM5A9E3KWeVL7HoMHYbATXUK1_pnChg1-g746GaMds,9999
314
314
  prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
315
315
  prefect/events/subscribers.py,sha256=OvT6vh6YVS5y_ktqCRM_ayNWQOFzZA_dvakKcSUl-4U,6714
316
316
  prefect/events/utilities.py,sha256=UPx_uRC0X7ahADvyP641-pPzgQeT4CcECe3iGScy4J4,3445
@@ -364,8 +364,8 @@ prefect/runner/_starter_direct.py,sha256=8cneJRIBud3HXUaf93ut92rQJfWEqg7_y0r_EVG
364
364
  prefect/runner/_starter_engine.py,sha256=FjcHQe_iryk8BFpeiGw8MCWcLSRKhYwmb2iA4eou0zg,6507
365
365
  prefect/runner/_state_proposer.py,sha256=iW-SlB5EOWHErWbyRJwRTirfqwo0BJ1fAlbScVi6vcg,7225
366
366
  prefect/runner/_workspace_resolver.py,sha256=2wQ7PXTanHY5yVGqhZuGh122DvNpTNZXEY_rMfexEtE,14020
367
- prefect/runner/_workspace_starter.py,sha256=DYaQG4_5RXAlyrBetLX0iJq3T4I3oym85KG1gp6lLQw,9797
368
- prefect/runner/runner.py,sha256=O4krExcrpdS3T7pe6RLSfQbpkup8-SltDYS0E7p3obA,75799
367
+ prefect/runner/_workspace_starter.py,sha256=P9tX_iv0zROc6k6YB68L5eNiyuc_HoP2clOSqE--rsg,12096
368
+ prefect/runner/runner.py,sha256=ZNgmuD19yNldoCDCXynNp1NoB8idIbZQb24vdKCnMvM,75785
369
369
  prefect/runner/server.py,sha256=YqvQjlxZZHyhSsqyaLvOy2NwTDg1hLSZB2PK3t8FJUg,3636
370
370
  prefect/runner/storage.py,sha256=yt_v2cVxYbazkZgfEdDQv0n-BS8rUvlFE8M_X3iN6NM,40525
371
371
  prefect/runtime/__init__.py,sha256=JswiTlYRup2zXOYu8AqJ7czKtgcw9Kxo0tTbS6aWCqY,407
@@ -1263,7 +1263,7 @@ prefect/server/utilities/schemas/serializers.py,sha256=HqawNki57u9BZz-czAxccscdz
1263
1263
  prefect/server/worker_communication/__init__.py,sha256=QAyUV9J4JM8C-0Yge7fsbOrQepqIxEL3FUi1m8bLrew,53
1264
1264
  prefect/server/worker_communication/cleanup_queue/__init__.py,sha256=fNED8IFb3mmznsoxoK2RdL-i7eNnWoSvED8ks1m3ch8,9245
1265
1265
  prefect/server/worker_communication/cleanup_queue/memory.py,sha256=DE4H5hb3ngxI1UO4dGXi8ARtZeHNbhJeN4nu71bBZkI,26449
1266
- prefect/settings/AGENTS.md,sha256=RIa012qDv1oa-BOkM3FlidVW_jl8kn5l5KYzV-FllAQ,4366
1266
+ prefect/settings/AGENTS.md,sha256=WciIrqatma3vrWgr9bdJZ5P9tKu7G2GnMfgTsXUT8oo,4625
1267
1267
  prefect/settings/__init__.py,sha256=3IbV7BM6RZ7bVCNJUxEayYUL-G0ydIIK0oB_2Vooe7U,2227
1268
1268
  prefect/settings/_types.py,sha256=Ud3cph5Rdee3EXfh0MyCxW2Kwurp7DkaUn9TSpG8wzQ,8884
1269
1269
  prefect/settings/base.py,sha256=UamKBsN3GBbHsU1CkbrPgT8qn3wlWSNkzq2keYHi9sc,11088
@@ -1307,7 +1307,7 @@ prefect/settings/models/server/root.py,sha256=j7uAJm82IZjnafFLmQDEk4jdKjMEkcKwUk
1307
1307
  prefect/settings/models/server/services.py,sha256=DKKzwcQLbG89wfuv-JbqDP2j58jWYO7R1Ajx8xmz344,24989
1308
1308
  prefect/settings/models/server/tasks.py,sha256=ckxRbx33HuHldRkGtiyRKerieubBK8U-sLRKxbrdGGI,2976
1309
1309
  prefect/settings/models/server/ui.py,sha256=q5Vy2w77GzU0mat3bRih-Plha0JXgUhUQeOpNXRBfsY,2284
1310
- prefect/settings/models/server/worker_channel.py,sha256=rDNwwRycwAT6Zic75znhL55ftmHxaNae93x20Q21MQQ,1877
1310
+ prefect/settings/models/server/worker_channel.py,sha256=FroUoIs2sTL2McQTlKgODOLIg25KXe3vZMWvLnkM7gI,1885
1311
1311
  prefect/telemetry/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
1312
1312
  prefect/telemetry/run_telemetry.py,sha256=2VTRIoLOrC5Prb6i2JCSMUR5bWM-pVMRH9b9mOIupNY,8595
1313
1313
  prefect/testing/AGENTS.md,sha256=Q7ys6LcpZYprXnUlIx6gsX73unD36lsnqS0pOMWbF8Y,2156
@@ -1316,8 +1316,9 @@ prefect/testing/cli.py,sha256=ieHwsS3vlM08KY9jP5QbGwUXXkJiUEiluoTAvGVHn58,13129
1316
1316
  prefect/testing/docker.py,sha256=N46yDmWaGseej_LEVZy91BJcbDaEhFyMKk30-xDVzPQ,635
1317
1317
  prefect/testing/fixtures.py,sha256=tkTsJyh4JkHkdeVRX0E_6QnpwRKAMVJJJITMBU6oRC8,17207
1318
1318
  prefect/testing/utilities.py,sha256=bVcXMDdgkcOr2V_Ldl1LM0Vz7MbMj_R4OPKN3JNMtDM,11368
1319
- prefect/testing/standard_test_suites/__init__.py,sha256=xvBZSNPjbUB_6yidr0UHktyXFTboJbaQFUuKngA5FLY,43
1319
+ prefect/testing/standard_test_suites/__init__.py,sha256=MpVyC1aPbUEZalvzT4gpH-8XvUzxBKXRBobtRCnbETc,113
1320
1320
  prefect/testing/standard_test_suites/blocks.py,sha256=mLA4YCgkLlibmnWvgJD8kWs4w9cwXfuCBl2AqoTPqjY,2835
1321
+ prefect/testing/standard_test_suites/worker_cleanup_queue.py,sha256=WRCulmfuPtSDOYnmdZ1xQ0vvDZ7REoiKrbQEpPzdYwA,22954
1321
1322
  prefect/types/__init__.py,sha256=-w7-dPhK3E1EczOR0wDQ5dZWoDc2fLe1e6T2G6QtpWc,8273
1322
1323
  prefect/types/_concurrency.py,sha256=ENLOFJlQlt3y2H0gJyZuZmrNyaBDsfFAZq3vaNER33s,354
1323
1324
  prefect/types/_datetime.py,sha256=Xd9PqknkNTyUHpnszb3LdVenFpFqwvS14KAAA8mO4Vk,11625
@@ -1336,7 +1337,7 @@ prefect/utilities/hashing.py,sha256=7jRy26s46IJAFRmVnCnoK9ek9N4p_UfXxQQvu2tW6dM,
1336
1337
  prefect/utilities/importtools.py,sha256=Ft9pU21xvooJwzMubJqp5Gj9a2y8TpesSYJssAViBiQ,18330
1337
1338
  prefect/utilities/math.py,sha256=UPIdJMP13lCU3o0Yz98o4VDw3LTkkrsOAsvAdA3Xifc,2954
1338
1339
  prefect/utilities/names.py,sha256=PcNp3IbSoJY6P3UiJDYDjpYQw6BYWtn6OarFDCq1dUE,1744
1339
- prefect/utilities/pydantic.py,sha256=X3O_ewH1uGVa2KVDRPfzkzbV4rhWUBEuxBctTFtyox0,13454
1340
+ prefect/utilities/pydantic.py,sha256=5N4Adv2fEU51fAkPDKxuDhrRDyRjacnxXPZy0Mwq6w4,13482
1340
1341
  prefect/utilities/render_swagger.py,sha256=y0GcR38qW083lUPrfHIbDVKPm_fyyodtBM8MTLNF8oI,4155
1341
1342
  prefect/utilities/services.py,sha256=YV_Cv_ELkzqSM9V9fGDBdiCfHWjc4mgMqwuhApg-b0s,7713
1342
1343
  prefect/utilities/slugify.py,sha256=57Vb14t13F3zm1P65KAu8nVeAz0iJCd1Qc5eMG-R5y8,169
@@ -1375,8 +1376,8 @@ prefect/workers/_worker_channel/_protocol.py,sha256=GpCNh1o3qmmqHA_UOOTge1QVC6IR
1375
1376
  prefect/workers/_worker_channel/_state.py,sha256=eQTFZtAVDZH1vVWps3SdeY6aW3qu2wx1UKYQXK3AyuE,5369
1376
1377
  prefect/workers/_worker_channel/_sync.py,sha256=G5G8_UaQYbeLebi5Mb1Z_KgGfXfyjXo5uT9la5_zwqY,14984
1377
1378
  prefect/workers/_worker_channel/_transport.py,sha256=_8aWX16tdbZcpqBg4HCX_vCzl2FX47jsYPKMPLAANRA,9181
1378
- prefect-3.7.3.dev7.dist-info/METADATA,sha256=aptgmQYHtMDcqIqZSRFC2W8Dh_irr8pEk1CG9b64nvE,14030
1379
- prefect-3.7.3.dev7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
1380
- prefect-3.7.3.dev7.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
1381
- prefect-3.7.3.dev7.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
1382
- prefect-3.7.3.dev7.dist-info/RECORD,,
1379
+ prefect-3.7.4.dev2.dist-info/METADATA,sha256=DDLxV4QfjlNJQ27nE3KXI8MZ1hNwWn6CF-w6OgUQ5f8,14030
1380
+ prefect-3.7.4.dev2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
1381
+ prefect-3.7.4.dev2.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
1382
+ prefect-3.7.4.dev2.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
1383
+ prefect-3.7.4.dev2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any