|
25 | 25 | from importlib.metadata import PackageNotFoundError |
26 | 26 | from importlib.metadata import version as _pkg_version |
27 | 27 | from typing import Any |
| 28 | +from urllib.parse import quote |
28 | 29 |
|
29 | 30 | import httpx |
30 | 31 |
|
@@ -102,6 +103,127 @@ class WorkflowList: |
102 | 103 | next_page_token: str | None = None |
103 | 104 |
|
104 | 105 |
|
| 106 | +@dataclass |
| 107 | +class TaskQueueTaskAdmission: |
| 108 | + """Workflow/activity admission state for one task queue.""" |
| 109 | + |
| 110 | + status: str | None = None |
| 111 | + budget_source: str | None = None |
| 112 | + server_budget_source: str | None = None |
| 113 | + active_worker_count: int | None = None |
| 114 | + configured_slot_count: int | None = None |
| 115 | + leased_count: int | None = None |
| 116 | + ready_count: int | None = None |
| 117 | + available_slot_count: int | None = None |
| 118 | + server_max_active_leases_per_queue: int | None = None |
| 119 | + server_active_lease_count: int | None = None |
| 120 | + server_remaining_active_lease_capacity: int | None = None |
| 121 | + server_lock_required: bool | None = None |
| 122 | + server_lock_supported: bool | None = None |
| 123 | + |
| 124 | + @classmethod |
| 125 | + def from_dict(cls, data: dict[str, Any] | None) -> TaskQueueTaskAdmission | None: |
| 126 | + if data is None: |
| 127 | + return None |
| 128 | + return cls( |
| 129 | + status=data.get("status"), |
| 130 | + budget_source=data.get("budget_source"), |
| 131 | + server_budget_source=data.get("server_budget_source"), |
| 132 | + active_worker_count=data.get("active_worker_count"), |
| 133 | + configured_slot_count=data.get("configured_slot_count"), |
| 134 | + leased_count=data.get("leased_count"), |
| 135 | + ready_count=data.get("ready_count"), |
| 136 | + available_slot_count=data.get("available_slot_count"), |
| 137 | + server_max_active_leases_per_queue=data.get("server_max_active_leases_per_queue"), |
| 138 | + server_active_lease_count=data.get("server_active_lease_count"), |
| 139 | + server_remaining_active_lease_capacity=data.get("server_remaining_active_lease_capacity"), |
| 140 | + server_lock_required=data.get("server_lock_required"), |
| 141 | + server_lock_supported=data.get("server_lock_supported"), |
| 142 | + ) |
| 143 | + |
| 144 | + |
| 145 | +@dataclass |
| 146 | +class TaskQueueQueryAdmission: |
| 147 | + """Worker-routed query-task admission state for one task queue.""" |
| 148 | + |
| 149 | + status: str | None = None |
| 150 | + budget_source: str | None = None |
| 151 | + max_pending_per_queue: int | None = None |
| 152 | + approximate_pending_count: int | None = None |
| 153 | + remaining_pending_capacity: int | None = None |
| 154 | + lock_required: bool | None = None |
| 155 | + lock_supported: bool | None = None |
| 156 | + |
| 157 | + @classmethod |
| 158 | + def from_dict(cls, data: dict[str, Any] | None) -> TaskQueueQueryAdmission | None: |
| 159 | + if data is None: |
| 160 | + return None |
| 161 | + return cls( |
| 162 | + status=data.get("status"), |
| 163 | + budget_source=data.get("budget_source"), |
| 164 | + max_pending_per_queue=data.get("max_pending_per_queue"), |
| 165 | + approximate_pending_count=data.get("approximate_pending_count"), |
| 166 | + remaining_pending_capacity=data.get("remaining_pending_capacity"), |
| 167 | + lock_required=data.get("lock_required"), |
| 168 | + lock_supported=data.get("lock_supported"), |
| 169 | + ) |
| 170 | + |
| 171 | + |
| 172 | +@dataclass |
| 173 | +class TaskQueueAdmission: |
| 174 | + """Server-side admission budgets for workflow, activity, and query tasks.""" |
| 175 | + |
| 176 | + workflow_tasks: TaskQueueTaskAdmission | None = None |
| 177 | + activity_tasks: TaskQueueTaskAdmission | None = None |
| 178 | + query_tasks: TaskQueueQueryAdmission | None = None |
| 179 | + raw: dict[str, Any] | None = None |
| 180 | + |
| 181 | + @classmethod |
| 182 | + def from_dict(cls, data: dict[str, Any] | None) -> TaskQueueAdmission: |
| 183 | + payload = data or {} |
| 184 | + return cls( |
| 185 | + workflow_tasks=TaskQueueTaskAdmission.from_dict(payload.get("workflow_tasks")), |
| 186 | + activity_tasks=TaskQueueTaskAdmission.from_dict(payload.get("activity_tasks")), |
| 187 | + query_tasks=TaskQueueQueryAdmission.from_dict(payload.get("query_tasks")), |
| 188 | + raw=payload, |
| 189 | + ) |
| 190 | + |
| 191 | + |
| 192 | +@dataclass |
| 193 | +class TaskQueueDescription: |
| 194 | + """Current server visibility and admission state for one task queue.""" |
| 195 | + |
| 196 | + name: str |
| 197 | + namespace: str | None = None |
| 198 | + stats: dict[str, Any] | None = None |
| 199 | + admission: TaskQueueAdmission | None = None |
| 200 | + pollers: list[dict[str, Any]] | None = None |
| 201 | + current_leases: list[dict[str, Any]] | None = None |
| 202 | + raw: dict[str, Any] | None = None |
| 203 | + |
| 204 | + @classmethod |
| 205 | + def from_dict(cls, data: dict[str, Any]) -> TaskQueueDescription: |
| 206 | + pollers = data.get("pollers") |
| 207 | + current_leases = data.get("current_leases") |
| 208 | + return cls( |
| 209 | + name=data.get("name", ""), |
| 210 | + namespace=data.get("namespace"), |
| 211 | + stats=data.get("stats"), |
| 212 | + admission=TaskQueueAdmission.from_dict(data.get("admission")), |
| 213 | + pollers=pollers if isinstance(pollers, list) else None, |
| 214 | + current_leases=current_leases if isinstance(current_leases, list) else None, |
| 215 | + raw=data, |
| 216 | + ) |
| 217 | + |
| 218 | + |
| 219 | +@dataclass |
| 220 | +class TaskQueueList: |
| 221 | + """One task-queue visibility page returned by the server.""" |
| 222 | + |
| 223 | + namespace: str | None |
| 224 | + task_queues: list[TaskQueueDescription] |
| 225 | + |
| 226 | + |
105 | 227 | @dataclass |
106 | 228 | class ScheduleSpec: |
107 | 229 | """Calendar or interval rules for a scheduled workflow.""" |
@@ -571,6 +693,38 @@ async def health(self) -> dict[str, Any]: |
571 | 693 | ) |
572 | 694 | return result |
573 | 695 |
|
| 696 | + # ── Task queues ──────────────────────────────────────────────────── |
| 697 | + async def list_task_queues(self) -> TaskQueueList: |
| 698 | + """List task queues with server-side admission status. |
| 699 | +
|
| 700 | + Admission data describes server budgets and observed backlog. Worker |
| 701 | + constructor limits remain local semaphores that are advertised during |
| 702 | + registration. |
| 703 | + """ |
| 704 | + data = await self._request("GET", "/task-queues") |
| 705 | + items = data.get("task_queues", []) if isinstance(data, dict) else [] |
| 706 | + return TaskQueueList( |
| 707 | + namespace=data.get("namespace") if isinstance(data, dict) else None, |
| 708 | + task_queues=[ |
| 709 | + TaskQueueDescription.from_dict(item) |
| 710 | + for item in items |
| 711 | + if isinstance(item, dict) |
| 712 | + ], |
| 713 | + ) |
| 714 | + |
| 715 | + async def describe_task_queue(self, name: str) -> TaskQueueDescription: |
| 716 | + """Return backlog, poller, lease, and admission detail for ``name``.""" |
| 717 | + data = await self._request("GET", f"/task-queues/{quote(name, safe='')}", context=name) |
| 718 | + if not isinstance(data, dict): |
| 719 | + raise ServerError( |
| 720 | + 200, |
| 721 | + { |
| 722 | + "reason": "invalid_task_queue_response", |
| 723 | + "message": f"expected JSON object, got {type(data).__name__}", |
| 724 | + }, |
| 725 | + ) |
| 726 | + return TaskQueueDescription.from_dict(data) |
| 727 | + |
574 | 728 | # ── Workflows ────────────────────────────────────────────────────── |
575 | 729 | async def start_workflow( |
576 | 730 | self, |
|
0 commit comments