workhold-admin 1.0.0__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.
@@ -0,0 +1,291 @@
1
+ """Emergency break-glass SDK adapter (OpenAPI BREAK_GLASS operations only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Final
6
+
7
+ from _workhold_client_core.errors import MalformedResponseError
8
+ from _workhold_client_core.transport import HttpJsonTransport, encode_path_segment
9
+
10
+ from workhold_admin.models import (
11
+ BreakGlassCounterResult,
12
+ BreakGlassMutationResult,
13
+ BreakGlassReplayLimitResult,
14
+ validate_acknowledge_duplicate_window,
15
+ validate_break_glass_reason,
16
+ validate_event_id,
17
+ validate_extend_seconds,
18
+ validate_failure_code,
19
+ validate_incident_reference,
20
+ validate_partition_name,
21
+ validate_registry_entry_id,
22
+ validate_replay_factor,
23
+ validate_replay_ttl_seconds,
24
+ validate_risk_acknowledged,
25
+ validate_queue_name,
26
+ validate_task_id,
27
+ )
28
+
29
+ _DEFAULT_FAILURE_CODE: Final[str] = "break_glass_force_dead_letter"
30
+ _DEFAULT_EXTEND_SECONDS: Final[int] = 86400
31
+ _DEFAULT_REGISTRY: Final[str] = "enqueue_dedup"
32
+ _DEFAULT_REPLAY_FACTOR: Final[float] = 2.0
33
+ _DEFAULT_REPLAY_TTL_SECONDS: Final[int] = 60
34
+
35
+
36
+ class BreakGlassClient:
37
+ """Break-glass emergency mutations on the admin control plane.
38
+
39
+ Requires deployment-issued short-lived JIT credentials with role
40
+ ``BREAK_GLASS`` (Bearer token). The SDK does not parse, validate expiry,
41
+ or mint JIT credentials; the server remains authoritative for credential
42
+ lifetime and ``allowed_operations`` audience.
43
+
44
+ Every mutation requires a non-empty ``reason``, ``incident_reference`` and
45
+ literal ``risk_acknowledged=True``. Operations never return worker claim
46
+ tokens. The SDK does not silently retry requests.
47
+ """
48
+
49
+ def __init_subclass__(cls, **kwargs: Any) -> None:
50
+ raise TypeError(f"{cls.__name__} cannot be subclassed")
51
+
52
+ def __init__(self, transport: HttpJsonTransport, *, bearer_token: str) -> None:
53
+ if not bearer_token or not bearer_token.strip():
54
+ raise ValueError("bearer_token is required")
55
+ self._transport = transport
56
+ self._bearer_token = bearer_token
57
+
58
+ def __repr__(self) -> str:
59
+ return "BreakGlassClient(transport=..., bearer_token=<redacted>)"
60
+
61
+ def _auth_headers(self, extra: dict[str, str] | None = None) -> dict[str, str]:
62
+ headers = {"Authorization": f"Bearer {self._bearer_token}"}
63
+ if extra:
64
+ headers.update(extra)
65
+ return headers
66
+
67
+ def force_lease_expiry(
68
+ self,
69
+ queue_name: str,
70
+ task_id: str,
71
+ *,
72
+ reason: str,
73
+ incident_reference: str,
74
+ risk_acknowledged: bool,
75
+ ) -> BreakGlassMutationResult:
76
+ """POST ``.../tasks/{task_id}:force-lease-expiry`` (``forceLeaseExpiry``)."""
77
+
78
+ wire_name = validate_queue_name(queue_name)
79
+ wire_task_id = validate_task_id(task_id)
80
+ body = _ack_body(
81
+ reason=reason,
82
+ incident_reference=incident_reference,
83
+ risk_acknowledged=risk_acknowledged,
84
+ extra={"task_id": wire_task_id},
85
+ )
86
+ path = (
87
+ f"/admin/v1/queues/{encode_path_segment(wire_name)}/tasks/"
88
+ f"{encode_path_segment(wire_task_id)}:force-lease-expiry"
89
+ )
90
+ return self._post(path, body, BreakGlassMutationResult.parse)
91
+
92
+ def force_delivery_reclaim(
93
+ self,
94
+ queue_name: str,
95
+ event_id: str,
96
+ *,
97
+ reason: str,
98
+ incident_reference: str,
99
+ risk_acknowledged: bool,
100
+ ) -> BreakGlassMutationResult:
101
+ """POST ``.../delivery-events/{event_id}:force-reclaim``."""
102
+
103
+ wire_name = validate_queue_name(queue_name)
104
+ wire_event_id = validate_event_id(event_id)
105
+ body = _ack_body(
106
+ reason=reason,
107
+ incident_reference=incident_reference,
108
+ risk_acknowledged=risk_acknowledged,
109
+ )
110
+ path = (
111
+ f"/admin/v1/queues/{encode_path_segment(wire_name)}/delivery-events/"
112
+ f"{encode_path_segment(wire_event_id)}:force-reclaim"
113
+ )
114
+ return self._post(path, body, BreakGlassMutationResult.parse)
115
+
116
+ def force_delivery_dead_letter(
117
+ self,
118
+ queue_name: str,
119
+ event_id: str,
120
+ *,
121
+ reason: str,
122
+ incident_reference: str,
123
+ risk_acknowledged: bool,
124
+ failure_code: str = _DEFAULT_FAILURE_CODE,
125
+ ) -> BreakGlassMutationResult:
126
+ """POST ``.../delivery-events/{event_id}:force-dead-letter``."""
127
+
128
+ wire_name = validate_queue_name(queue_name)
129
+ wire_event_id = validate_event_id(event_id)
130
+ wire_failure_code = validate_failure_code(failure_code)
131
+ body = _ack_body(
132
+ reason=reason,
133
+ incident_reference=incident_reference,
134
+ risk_acknowledged=risk_acknowledged,
135
+ extra={"failure_code": wire_failure_code},
136
+ )
137
+ path = (
138
+ f"/admin/v1/queues/{encode_path_segment(wire_name)}/delivery-events/"
139
+ f"{encode_path_segment(wire_event_id)}:force-dead-letter"
140
+ )
141
+ return self._post(path, body, BreakGlassMutationResult.parse)
142
+
143
+ def reconcile_counters(
144
+ self,
145
+ queue_name: str,
146
+ *,
147
+ reason: str,
148
+ incident_reference: str,
149
+ risk_acknowledged: bool,
150
+ ) -> BreakGlassCounterResult:
151
+ """POST ``/admin/v1/queues/{queue_name}:reconcile-counters``."""
152
+
153
+ wire_name = validate_queue_name(queue_name)
154
+ body = _ack_body(
155
+ reason=reason,
156
+ incident_reference=incident_reference,
157
+ risk_acknowledged=risk_acknowledged,
158
+ )
159
+ path = f"/admin/v1/queues/{encode_path_segment(wire_name)}:reconcile-counters"
160
+ return self._post(path, body, BreakGlassCounterResult.parse)
161
+
162
+ def raise_replay_limit(
163
+ self,
164
+ queue_name: str,
165
+ *,
166
+ reason: str,
167
+ incident_reference: str,
168
+ risk_acknowledged: bool,
169
+ factor: float = _DEFAULT_REPLAY_FACTOR,
170
+ ttl_seconds: int = _DEFAULT_REPLAY_TTL_SECONDS,
171
+ ) -> BreakGlassReplayLimitResult:
172
+ """POST ``/admin/v1/queues/{queue_name}:raise-replay-limit``."""
173
+
174
+ wire_name = validate_queue_name(queue_name)
175
+ wire_factor = validate_replay_factor(factor)
176
+ wire_ttl = validate_replay_ttl_seconds(ttl_seconds)
177
+ body = _ack_body(
178
+ reason=reason,
179
+ incident_reference=incident_reference,
180
+ risk_acknowledged=risk_acknowledged,
181
+ extra={"factor": wire_factor, "ttl_seconds": wire_ttl},
182
+ )
183
+ path = f"/admin/v1/queues/{encode_path_segment(wire_name)}:raise-replay-limit"
184
+ return self._post(path, body, BreakGlassReplayLimitResult.parse)
185
+
186
+ def drop_expired_partition(
187
+ self,
188
+ partition_name: str,
189
+ *,
190
+ reason: str,
191
+ incident_reference: str,
192
+ risk_acknowledged: bool,
193
+ ) -> BreakGlassMutationResult:
194
+ """POST ``/admin/v1/partitions/{partition_name}:force-drop``."""
195
+
196
+ wire_partition = validate_partition_name(partition_name)
197
+ body = _ack_body(
198
+ reason=reason,
199
+ incident_reference=incident_reference,
200
+ risk_acknowledged=risk_acknowledged,
201
+ )
202
+ path = (
203
+ f"/admin/v1/partitions/{encode_path_segment(wire_partition)}:force-drop"
204
+ )
205
+ return self._post(path, body, BreakGlassMutationResult.parse)
206
+
207
+ def repair_registry_entry(
208
+ self,
209
+ queue_name: str,
210
+ *,
211
+ entry_id: int,
212
+ reason: str,
213
+ incident_reference: str,
214
+ risk_acknowledged: bool,
215
+ acknowledge_duplicate_window: bool,
216
+ extend_seconds: int = _DEFAULT_EXTEND_SECONDS,
217
+ registry: str = _DEFAULT_REGISTRY,
218
+ ) -> BreakGlassMutationResult:
219
+ """POST ``/admin/v1/queues/{queue_name}/registry:repair``."""
220
+
221
+ wire_name = validate_queue_name(queue_name)
222
+ wire_entry_id = validate_registry_entry_id(entry_id)
223
+ wire_extend = validate_extend_seconds(extend_seconds)
224
+ wire_registry = _wire_registry(registry)
225
+ body = _ack_body(
226
+ reason=reason,
227
+ incident_reference=incident_reference,
228
+ risk_acknowledged=risk_acknowledged,
229
+ extra={
230
+ "entry_id": wire_entry_id,
231
+ "acknowledge_duplicate_window": validate_acknowledge_duplicate_window(
232
+ acknowledge_duplicate_window
233
+ ),
234
+ "extend_seconds": wire_extend,
235
+ "registry": wire_registry,
236
+ },
237
+ )
238
+ path = f"/admin/v1/queues/{encode_path_segment(wire_name)}/registry:repair"
239
+ return self._post(path, body, BreakGlassMutationResult.parse)
240
+
241
+ def _post(
242
+ self,
243
+ path: str,
244
+ body: dict[str, Any],
245
+ parser: Any,
246
+ ) -> Any:
247
+ response = self._transport.request(
248
+ "POST",
249
+ path,
250
+ headers=self._auth_headers(),
251
+ json_body=body,
252
+ )
253
+ return self._parse(parser, response.status_code, response.body)
254
+
255
+ @staticmethod
256
+ def _parse(parser: Any, status_code: int, body: object | None) -> Any:
257
+ if body is None:
258
+ raise MalformedResponseError(
259
+ status_code=status_code,
260
+ reason="empty success response body",
261
+ )
262
+ try:
263
+ return parser(body)
264
+ except ValueError as exc:
265
+ raise MalformedResponseError(
266
+ status_code=status_code,
267
+ reason=str(exc),
268
+ ) from exc
269
+
270
+
271
+ def _ack_body(
272
+ *,
273
+ reason: str,
274
+ incident_reference: str,
275
+ risk_acknowledged: bool,
276
+ extra: dict[str, Any] | None = None,
277
+ ) -> dict[str, Any]:
278
+ body: dict[str, Any] = {
279
+ "reason": validate_break_glass_reason(reason),
280
+ "incident_reference": validate_incident_reference(incident_reference),
281
+ "risk_acknowledged": validate_risk_acknowledged(risk_acknowledged),
282
+ }
283
+ if extra:
284
+ body.update(extra)
285
+ return body
286
+
287
+
288
+ def _wire_registry(registry: str) -> str:
289
+ if registry != _DEFAULT_REGISTRY:
290
+ raise ValueError("registry must be enqueue_dedup")
291
+ return registry