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,1516 @@
1
+ """Admin/observer domain models with OpenAPI bounds and tolerant parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import uuid
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass, field
9
+ from datetime import UTC, datetime
10
+ from typing import Any, Final
11
+
12
+ from _workhold_client_core.models import Task
13
+
14
+ PAGE_LIMIT_MIN: Final[int] = 1
15
+ PAGE_LIMIT_MAX: Final[int] = 100
16
+ PAGE_LIMIT_DEFAULT: Final[int] = 50
17
+ CURSOR_MAX_LENGTH: Final[int] = 512
18
+ QUEUE_NAME_MAX_LENGTH: Final[int] = 128
19
+ IDEMPOTENCY_KEY_MIN_LENGTH: Final[int] = 1
20
+ IDEMPOTENCY_KEY_MAX_LENGTH: Final[int] = 256
21
+ REASON_MAX_LENGTH: Final[int] = 512
22
+ CONFIRMATION_TOKEN_MAX_LENGTH: Final[int] = 24576
23
+ BULK_CANDIDATE_MAX: Final[int] = 100
24
+ BULK_BATCH_MAX: Final[int] = 25
25
+ BULK_SAMPLE_MAX: Final[int] = 5
26
+ POLICY_VERSION_MIN: Final[int] = 1
27
+ CONFIG_VERSION_MIN: Final[int] = 1
28
+ RETRY_DELAY_SECONDS_MAX: Final[int] = 86400
29
+ MAX_ATTEMPTS_MIN: Final[int] = 1
30
+ BREAK_GLASS_REASON_MAX_LENGTH: Final[int] = 512
31
+ INCIDENT_REFERENCE_MAX_LENGTH: Final[int] = 128
32
+ PARTITION_NAME_MAX_LENGTH: Final[int] = 128
33
+ REPLAY_FACTOR_MIN: Final[float] = 1.0
34
+ REPLAY_FACTOR_MAX: Final[float] = 10.0
35
+ REPLAY_TTL_SECONDS_MIN: Final[int] = 1
36
+ REPLAY_TTL_SECONDS_MAX: Final[int] = 3600
37
+ REGISTRY_ENTRY_ID_MIN: Final[int] = 1
38
+ EXTEND_SECONDS_MIN: Final[int] = 1
39
+ EXTEND_SECONDS_MAX: Final[int] = 2592000
40
+ FAILURE_CODE_MAX_LENGTH: Final[int] = 128
41
+
42
+ _QUEUE_NAME_RE: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
43
+ _PARTITION_NAME_RE: Final[re.Pattern[str]] = re.compile(r"^[a-z][a-z0-9_]*_[0-9]{8}$")
44
+
45
+ KNOWN_QUEUE_STATES: Final[frozenset[str]] = frozenset({"active", "paused", "draining"})
46
+ KNOWN_BACKOFF_STRATEGIES: Final[frozenset[str]] = frozenset({"fixed"})
47
+ KNOWN_ATTEMPT_OUTCOMES: Final[frozenset[str]] = frozenset(
48
+ {
49
+ "active",
50
+ "succeeded",
51
+ "retry_scheduled",
52
+ "dead_lettered",
53
+ "expired",
54
+ "cancelled",
55
+ }
56
+ )
57
+ KNOWN_MAINTENANCE_OUTCOMES: Final[frozenset[str]] = frozenset(
58
+ {"succeeded", "failed", "skipped_lock"}
59
+ )
60
+ KNOWN_STATS_FRESHNESS: Final[frozenset[str]] = frozenset(
61
+ {"fresh", "stale", "unavailable"}
62
+ )
63
+ KNOWN_STATS_AVAILABILITY: Final[frozenset[str]] = frozenset({"available", "unavailable"})
64
+ KNOWN_STATS_TELEMETRY_SOURCE: Final[frozenset[str]] = frozenset({"process_telemetry"})
65
+
66
+ KNOWN_AUDIT_OPERATIONS: Final[frozenset[str]] = frozenset(
67
+ {
68
+ "create_queue",
69
+ "create_policy",
70
+ "activate_policy",
71
+ "set_queue_state",
72
+ "run_maintenance",
73
+ "replay_dead_letter",
74
+ "bulk_replay",
75
+ "bulk_cancel",
76
+ }
77
+ )
78
+ KNOWN_BULK_OPERATIONS: Final[frozenset[str]] = frozenset({"bulk_replay", "bulk_cancel"})
79
+ _FORBIDDEN_BULK_FILTER_KEYS: Final[frozenset[str]] = frozenset(
80
+ {
81
+ "q",
82
+ "query",
83
+ "search",
84
+ "text",
85
+ "payload",
86
+ "payload_field",
87
+ "business_key",
88
+ "worker_id",
89
+ "sql",
90
+ "offset",
91
+ }
92
+ )
93
+ _ALLOWED_BULK_FILTER_KEYS: Final[frozenset[str]] = frozenset(
94
+ {
95
+ "from",
96
+ "to",
97
+ "failure_code",
98
+ "state",
99
+ "states",
100
+ }
101
+ )
102
+ _FORBIDDEN_BREAK_GLASS_SECRET_KEY_RE: Final[re.Pattern[str]] = re.compile(
103
+ r"claim.*token|lease_token",
104
+ re.IGNORECASE,
105
+ )
106
+ KNOWN_BULK_ITEM_OUTCOMES: Final[frozenset[str]] = frozenset(
107
+ {
108
+ "succeeded",
109
+ "replayed",
110
+ "skipped",
111
+ "failed",
112
+ "cancelled",
113
+ "cancel_requested",
114
+ }
115
+ )
116
+
117
+
118
+ @dataclass(frozen=True, slots=True)
119
+ class QueueState:
120
+ value: str
121
+
122
+ @property
123
+ def is_unknown(self) -> bool:
124
+ return self.value not in KNOWN_QUEUE_STATES
125
+
126
+ @classmethod
127
+ def parse(cls, raw: object) -> QueueState:
128
+ if not isinstance(raw, str) or not raw:
129
+ raise ValueError("queue.state must be a non-empty string")
130
+ return cls(raw)
131
+
132
+
133
+ @dataclass(frozen=True, slots=True)
134
+ class BackoffStrategy:
135
+ value: str
136
+
137
+ @property
138
+ def is_unknown(self) -> bool:
139
+ return self.value not in KNOWN_BACKOFF_STRATEGIES
140
+
141
+ @classmethod
142
+ def parse(cls, raw: object) -> BackoffStrategy:
143
+ if not isinstance(raw, str) or not raw:
144
+ raise ValueError("backoff_strategy must be a non-empty string")
145
+ return cls(raw)
146
+
147
+
148
+ @dataclass(frozen=True, slots=True)
149
+ class AttemptOutcome:
150
+ value: str
151
+
152
+ @property
153
+ def is_unknown(self) -> bool:
154
+ return self.value not in KNOWN_ATTEMPT_OUTCOMES
155
+
156
+ @classmethod
157
+ def parse(cls, raw: object) -> AttemptOutcome:
158
+ if not isinstance(raw, str) or not raw:
159
+ raise ValueError("attempt.outcome must be a non-empty string")
160
+ return cls(raw)
161
+
162
+
163
+ @dataclass(frozen=True, slots=True)
164
+ class AuditOperation:
165
+ value: str
166
+
167
+ @property
168
+ def is_unknown(self) -> bool:
169
+ return self.value not in KNOWN_AUDIT_OPERATIONS
170
+
171
+ @classmethod
172
+ def parse(cls, raw: object) -> AuditOperation:
173
+ if not isinstance(raw, str) or not raw:
174
+ raise ValueError("audit.operation must be a non-empty string")
175
+ return cls(raw)
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class MaintenanceOutcome:
180
+ value: str
181
+
182
+ @property
183
+ def is_unknown(self) -> bool:
184
+ return self.value not in KNOWN_MAINTENANCE_OUTCOMES
185
+
186
+ @classmethod
187
+ def parse(cls, raw: object) -> MaintenanceOutcome:
188
+ if not isinstance(raw, str) or not raw:
189
+ raise ValueError("maintenance.outcome must be a non-empty string")
190
+ return cls(raw)
191
+
192
+
193
+ @dataclass(frozen=True, slots=True)
194
+ class StatsFreshness:
195
+ value: str
196
+
197
+ @property
198
+ def is_unknown(self) -> bool:
199
+ return self.value not in KNOWN_STATS_FRESHNESS
200
+
201
+ @classmethod
202
+ def parse(cls, raw: object) -> StatsFreshness:
203
+ if not isinstance(raw, str) or not raw:
204
+ raise ValueError("freshness must be a non-empty string")
205
+ return cls(raw)
206
+
207
+
208
+ @dataclass(frozen=True, slots=True)
209
+ class StatsAvailability:
210
+ value: str
211
+
212
+ @property
213
+ def is_unknown(self) -> bool:
214
+ return self.value not in KNOWN_STATS_AVAILABILITY
215
+
216
+ @classmethod
217
+ def parse(cls, raw: object) -> StatsAvailability:
218
+ if not isinstance(raw, str) or not raw:
219
+ raise ValueError("availability must be a non-empty string")
220
+ return cls(raw)
221
+
222
+
223
+ @dataclass(frozen=True, slots=True)
224
+ class StatsTelemetrySource:
225
+ value: str
226
+
227
+ @property
228
+ def is_unknown(self) -> bool:
229
+ return self.value not in KNOWN_STATS_TELEMETRY_SOURCE
230
+
231
+ @classmethod
232
+ def parse(cls, raw: object) -> StatsTelemetrySource:
233
+ if not isinstance(raw, str) or not raw:
234
+ raise ValueError("telemetry.source must be a non-empty string")
235
+ return cls(raw)
236
+
237
+
238
+ @dataclass(frozen=True, slots=True)
239
+ class ConfigVersion:
240
+ """Optimistic concurrency pin for admin mutations (OpenAPI ``expected_config_version``)."""
241
+
242
+ value: int
243
+
244
+ def to_wire(self) -> int:
245
+ return self.value
246
+
247
+ @classmethod
248
+ def parse(cls, raw: object) -> ConfigVersion:
249
+ return cls(value=_require_config_version(raw, "expected_config_version"))
250
+
251
+
252
+ @dataclass(frozen=True, slots=True)
253
+ class PolicyVersion:
254
+ """Immutable retry-policy version number (OpenAPI path ``policy_version``)."""
255
+
256
+ value: int
257
+
258
+ def to_wire(self) -> int:
259
+ return self.value
260
+
261
+ @classmethod
262
+ def parse(cls, raw: object) -> PolicyVersion:
263
+ return cls(value=_require_policy_version(raw, "policy_version"))
264
+
265
+
266
+ @dataclass(frozen=True, slots=True)
267
+ class RetryPolicyDraft:
268
+ """Retry-policy content for create queue / create policy (OpenAPI draft schemas)."""
269
+
270
+ enabled: bool
271
+ max_attempts: int
272
+ backoff_strategy: BackoffStrategy
273
+ retry_delay_seconds: int
274
+
275
+ def to_wire(self) -> dict[str, Any]:
276
+ if self.backoff_strategy.value != "fixed":
277
+ raise ValueError("backoff_strategy must be fixed for draft mutations")
278
+ return {
279
+ "enabled": self.enabled,
280
+ "max_attempts": _require_max_attempts(self.max_attempts),
281
+ "backoff_strategy": self.backoff_strategy.value,
282
+ "retry_delay_seconds": _require_retry_delay_seconds(self.retry_delay_seconds),
283
+ }
284
+
285
+ @classmethod
286
+ def parse(cls, raw: object) -> RetryPolicyDraft:
287
+ if not isinstance(raw, Mapping):
288
+ raise ValueError("retry policy draft must be an object")
289
+ required = (
290
+ "enabled",
291
+ "max_attempts",
292
+ "backoff_strategy",
293
+ "retry_delay_seconds",
294
+ )
295
+ missing = [key for key in required if key not in raw]
296
+ if missing:
297
+ raise ValueError(f"retry policy draft missing fields: {missing}")
298
+ strategy = BackoffStrategy.parse(raw["backoff_strategy"])
299
+ if strategy.value != "fixed":
300
+ raise ValueError("backoff_strategy must be fixed for draft mutations")
301
+ return cls(
302
+ enabled=_require_bool(raw["enabled"], "enabled"),
303
+ max_attempts=_require_max_attempts(raw["max_attempts"]),
304
+ backoff_strategy=strategy,
305
+ retry_delay_seconds=_require_retry_delay_seconds(raw["retry_delay_seconds"]),
306
+ )
307
+
308
+
309
+ @dataclass(frozen=True, slots=True)
310
+ class RetryPolicy:
311
+ version: int
312
+ enabled: bool
313
+ max_attempts: int
314
+ backoff_strategy: BackoffStrategy
315
+ retry_delay_seconds: int
316
+ created_at: str
317
+ extra: Mapping[str, Any] = field(default_factory=dict)
318
+
319
+ @classmethod
320
+ def parse(cls, raw: object) -> RetryPolicy:
321
+ if not isinstance(raw, Mapping):
322
+ raise ValueError("active_policy must be an object")
323
+ required = (
324
+ "version",
325
+ "enabled",
326
+ "max_attempts",
327
+ "backoff_strategy",
328
+ "retry_delay_seconds",
329
+ "created_at",
330
+ )
331
+ missing = [key for key in required if key not in raw]
332
+ if missing:
333
+ raise ValueError(f"active_policy missing fields: {missing}")
334
+ known = set(required)
335
+ extra = {k: v for k, v in raw.items() if k not in known}
336
+ return cls(
337
+ version=_require_int(raw["version"], "active_policy.version"),
338
+ enabled=_require_bool(raw["enabled"], "active_policy.enabled"),
339
+ max_attempts=_require_int(raw["max_attempts"], "active_policy.max_attempts"),
340
+ backoff_strategy=BackoffStrategy.parse(raw["backoff_strategy"]),
341
+ retry_delay_seconds=_require_int(
342
+ raw["retry_delay_seconds"], "active_policy.retry_delay_seconds"
343
+ ),
344
+ created_at=_require_str(raw["created_at"], "active_policy.created_at"),
345
+ extra=dict(extra),
346
+ )
347
+
348
+
349
+ @dataclass(frozen=True, slots=True)
350
+ class Queue:
351
+ queue_id: str
352
+ name: str
353
+ state: QueueState
354
+ config_version: int
355
+ active_policy: RetryPolicy
356
+ created_at: str
357
+ updated_at: str
358
+ active_depth: int | None = None
359
+ ready_count: int | None = None
360
+ delayed_count: int | None = None
361
+ leased_count: int | None = None
362
+ drain_complete: bool | None = None
363
+ extra: Mapping[str, Any] = field(default_factory=dict)
364
+
365
+ @classmethod
366
+ def parse(cls, raw: object) -> Queue:
367
+ if not isinstance(raw, Mapping):
368
+ raise ValueError("queue must be an object")
369
+ required = (
370
+ "queue_id",
371
+ "name",
372
+ "state",
373
+ "config_version",
374
+ "active_policy",
375
+ "created_at",
376
+ "updated_at",
377
+ )
378
+ missing = [key for key in required if key not in raw]
379
+ if missing:
380
+ raise ValueError(f"queue missing required fields: {missing}")
381
+ known = set(required) | {
382
+ "active_depth",
383
+ "ready_count",
384
+ "delayed_count",
385
+ "leased_count",
386
+ "drain_complete",
387
+ }
388
+ extra = {k: v for k, v in raw.items() if k not in known}
389
+ return cls(
390
+ queue_id=_require_str(raw["queue_id"], "queue_id"),
391
+ name=_require_str(raw["name"], "name"),
392
+ state=QueueState.parse(raw["state"]),
393
+ config_version=_require_int(raw["config_version"], "config_version"),
394
+ active_policy=RetryPolicy.parse(raw["active_policy"]),
395
+ created_at=_require_str(raw["created_at"], "created_at"),
396
+ updated_at=_require_str(raw["updated_at"], "updated_at"),
397
+ active_depth=_optional_int(raw.get("active_depth"), "active_depth"),
398
+ ready_count=_optional_int(raw.get("ready_count"), "ready_count"),
399
+ delayed_count=_optional_int(raw.get("delayed_count"), "delayed_count"),
400
+ leased_count=_optional_int(raw.get("leased_count"), "leased_count"),
401
+ drain_complete=_optional_bool(raw.get("drain_complete"), "drain_complete"),
402
+ extra=dict(extra),
403
+ )
404
+
405
+
406
+ @dataclass(frozen=True, slots=True)
407
+ class Attempt:
408
+ attempt_id: int
409
+ task_id: str
410
+ claim_id: str
411
+ generation: int
412
+ claimed_at: str
413
+ worker_id: str
414
+ lease_expires_at: str
415
+ outcome: AttemptOutcome
416
+ ended_at: str | None = None
417
+ failure_code: str | None = None
418
+ failure_detail: str | None = None
419
+ extra: Mapping[str, Any] = field(default_factory=dict)
420
+
421
+ @classmethod
422
+ def parse(cls, raw: object) -> Attempt:
423
+ if not isinstance(raw, Mapping):
424
+ raise ValueError("attempt must be an object")
425
+ required = (
426
+ "attempt_id",
427
+ "task_id",
428
+ "claim_id",
429
+ "generation",
430
+ "claimed_at",
431
+ "worker_id",
432
+ "lease_expires_at",
433
+ "outcome",
434
+ )
435
+ missing = [key for key in required if key not in raw]
436
+ if missing:
437
+ raise ValueError(f"attempt missing required fields: {missing}")
438
+ known = set(required) | {"ended_at", "failure_code", "failure_detail"}
439
+ extra = {k: v for k, v in raw.items() if k not in known}
440
+ return cls(
441
+ attempt_id=_require_int(raw["attempt_id"], "attempt_id"),
442
+ task_id=_require_str(raw["task_id"], "task_id"),
443
+ claim_id=_require_str(raw["claim_id"], "claim_id"),
444
+ generation=_require_int(raw["generation"], "generation"),
445
+ claimed_at=_require_str(raw["claimed_at"], "claimed_at"),
446
+ worker_id=_require_str(raw["worker_id"], "worker_id"),
447
+ lease_expires_at=_require_str(raw["lease_expires_at"], "lease_expires_at"),
448
+ outcome=AttemptOutcome.parse(raw["outcome"]),
449
+ ended_at=_optional_str(raw.get("ended_at"), "ended_at"),
450
+ failure_code=_optional_str(raw.get("failure_code"), "failure_code"),
451
+ failure_detail=_optional_str(raw.get("failure_detail"), "failure_detail"),
452
+ extra=dict(extra),
453
+ )
454
+
455
+
456
+ @dataclass(frozen=True, slots=True)
457
+ class QueuePage:
458
+ items: tuple[Queue, ...]
459
+ next_cursor: str | None = None
460
+ extra: Mapping[str, Any] = field(default_factory=dict)
461
+
462
+ @classmethod
463
+ def parse(cls, raw: object) -> QueuePage:
464
+ if not isinstance(raw, Mapping):
465
+ raise ValueError("queue page must be an object")
466
+ if "items" not in raw:
467
+ raise ValueError("queue page missing items")
468
+ items_raw = raw["items"]
469
+ if not isinstance(items_raw, list):
470
+ raise ValueError("queue page items must be an array")
471
+ known = {"items", "next_cursor"}
472
+ extra = {k: v for k, v in raw.items() if k not in known}
473
+ return cls(
474
+ items=tuple(Queue.parse(item) for item in items_raw),
475
+ next_cursor=_optional_str(raw.get("next_cursor"), "next_cursor"),
476
+ extra=dict(extra),
477
+ )
478
+
479
+
480
+ @dataclass(frozen=True, slots=True)
481
+ class AdminMutationResult:
482
+ queue: Queue
483
+ replayed: bool
484
+ admin_replay_expires_at: str
485
+ extra: Mapping[str, Any] = field(default_factory=dict)
486
+
487
+ @classmethod
488
+ def parse(cls, raw: object) -> AdminMutationResult:
489
+ if not isinstance(raw, Mapping):
490
+ raise ValueError("admin mutation result must be an object")
491
+ required = ("queue", "replayed", "admin_replay_expires_at")
492
+ missing = [key for key in required if key not in raw]
493
+ if missing:
494
+ raise ValueError(f"admin mutation result missing fields: {missing}")
495
+ known = set(required)
496
+ extra = {k: v for k, v in raw.items() if k not in known}
497
+ return cls(
498
+ queue=Queue.parse(raw["queue"]),
499
+ replayed=_require_bool(raw["replayed"], "replayed"),
500
+ admin_replay_expires_at=_require_str(
501
+ raw["admin_replay_expires_at"], "admin_replay_expires_at"
502
+ ),
503
+ extra=dict(extra),
504
+ )
505
+
506
+
507
+ @dataclass(frozen=True, slots=True)
508
+ class AttemptPage:
509
+ items: tuple[Attempt, ...]
510
+ next_cursor: str | None = None
511
+ extra: Mapping[str, Any] = field(default_factory=dict)
512
+
513
+ @classmethod
514
+ def parse(cls, raw: object) -> AttemptPage:
515
+ if not isinstance(raw, Mapping):
516
+ raise ValueError("attempt page must be an object")
517
+ if "items" not in raw:
518
+ raise ValueError("attempt page missing items")
519
+ items_raw = raw["items"]
520
+ if not isinstance(items_raw, list):
521
+ raise ValueError("attempt page items must be an array")
522
+ known = {"items", "next_cursor"}
523
+ extra = {k: v for k, v in raw.items() if k not in known}
524
+ return cls(
525
+ items=tuple(Attempt.parse(item) for item in items_raw),
526
+ next_cursor=_optional_str(raw.get("next_cursor"), "next_cursor"),
527
+ extra=dict(extra),
528
+ )
529
+
530
+
531
+ @dataclass(frozen=True, slots=True)
532
+ class TaskPage:
533
+ items: tuple[Task, ...]
534
+ next_cursor: str | None = None
535
+ extra: Mapping[str, Any] = field(default_factory=dict)
536
+
537
+ @classmethod
538
+ def parse(cls, raw: object) -> TaskPage:
539
+ if not isinstance(raw, Mapping):
540
+ raise ValueError("task page must be an object")
541
+ if "items" not in raw:
542
+ raise ValueError("task page missing items")
543
+ items_raw = raw["items"]
544
+ if not isinstance(items_raw, list):
545
+ raise ValueError("task page items must be an array")
546
+ known = {"items", "next_cursor"}
547
+ extra = {k: v for k, v in raw.items() if k not in known}
548
+ return cls(
549
+ items=tuple(Task.parse(item) for item in items_raw),
550
+ next_cursor=_optional_str(raw.get("next_cursor"), "next_cursor"),
551
+ extra=dict(extra),
552
+ )
553
+
554
+
555
+ @dataclass(frozen=True, slots=True)
556
+ class DeadLetterPage:
557
+ items: tuple[Task, ...]
558
+ next_cursor: str | None = None
559
+ extra: Mapping[str, Any] = field(default_factory=dict)
560
+
561
+ @classmethod
562
+ def parse(cls, raw: object) -> DeadLetterPage:
563
+ if not isinstance(raw, Mapping):
564
+ raise ValueError("dead letter page must be an object")
565
+ if "items" not in raw:
566
+ raise ValueError("dead letter page missing items")
567
+ items_raw = raw["items"]
568
+ if not isinstance(items_raw, list):
569
+ raise ValueError("dead letter page items must be an array")
570
+ known = {"items", "next_cursor"}
571
+ extra = {k: v for k, v in raw.items() if k not in known}
572
+ return cls(
573
+ items=tuple(Task.parse(item) for item in items_raw),
574
+ next_cursor=_optional_str(raw.get("next_cursor"), "next_cursor"),
575
+ extra=dict(extra),
576
+ )
577
+
578
+
579
+ @dataclass(frozen=True, slots=True)
580
+ class AuditRecord:
581
+ audit_id: int
582
+ audit_at: str
583
+ actor_id: str
584
+ operation: AuditOperation
585
+ request_id: str
586
+ queue_id: str | None = None
587
+ previous_config_version: int | None = None
588
+ new_config_version: int | None = None
589
+ extra: Mapping[str, Any] = field(default_factory=dict)
590
+
591
+ @classmethod
592
+ def parse(cls, raw: object) -> AuditRecord:
593
+ if not isinstance(raw, Mapping):
594
+ raise ValueError("audit record must be an object")
595
+ required = ("audit_id", "audit_at", "actor_id", "operation", "request_id")
596
+ missing = [key for key in required if key not in raw]
597
+ if missing:
598
+ raise ValueError(f"audit record missing required fields: {missing}")
599
+ known = set(required) | {
600
+ "queue_id",
601
+ "previous_config_version",
602
+ "new_config_version",
603
+ }
604
+ extra = {k: v for k, v in raw.items() if k not in known}
605
+ return cls(
606
+ audit_id=_require_int(raw["audit_id"], "audit_id"),
607
+ audit_at=_require_str(raw["audit_at"], "audit_at"),
608
+ actor_id=_require_str(raw["actor_id"], "actor_id"),
609
+ operation=AuditOperation.parse(raw["operation"]),
610
+ request_id=_require_str(raw["request_id"], "request_id"),
611
+ queue_id=_optional_str(raw.get("queue_id"), "queue_id"),
612
+ previous_config_version=_optional_int(
613
+ raw.get("previous_config_version"), "previous_config_version"
614
+ ),
615
+ new_config_version=_optional_int(
616
+ raw.get("new_config_version"), "new_config_version"
617
+ ),
618
+ extra=dict(extra),
619
+ )
620
+
621
+
622
+ @dataclass(frozen=True, slots=True)
623
+ class AuditPage:
624
+ items: tuple[AuditRecord, ...]
625
+ next_cursor: str | None = None
626
+ extra: Mapping[str, Any] = field(default_factory=dict)
627
+
628
+ @classmethod
629
+ def parse(cls, raw: object) -> AuditPage:
630
+ if not isinstance(raw, Mapping):
631
+ raise ValueError("audit page must be an object")
632
+ if "items" not in raw:
633
+ raise ValueError("audit page missing items")
634
+ items_raw = raw["items"]
635
+ if not isinstance(items_raw, list):
636
+ raise ValueError("audit page items must be an array")
637
+ known = {"items", "next_cursor"}
638
+ extra = {k: v for k, v in raw.items() if k not in known}
639
+ return cls(
640
+ items=tuple(AuditRecord.parse(item) for item in items_raw),
641
+ next_cursor=_optional_str(raw.get("next_cursor"), "next_cursor"),
642
+ extra=dict(extra),
643
+ )
644
+
645
+
646
+ @dataclass(frozen=True, slots=True)
647
+ class MaintenanceStatus:
648
+ updated_at: str
649
+ last_started_at: str | None = None
650
+ last_succeeded_at: str | None = None
651
+ premade_through: str | None = None
652
+ retained_from: str | None = None
653
+ last_error_code: str | None = None
654
+ outcome: MaintenanceOutcome | None = None
655
+ maintenance_run_id: str | None = None
656
+ extra: Mapping[str, Any] = field(default_factory=dict)
657
+
658
+ @classmethod
659
+ def parse(cls, raw: object) -> MaintenanceStatus:
660
+ if not isinstance(raw, Mapping):
661
+ raise ValueError("maintenance status must be an object")
662
+ if "updated_at" not in raw:
663
+ raise ValueError("maintenance status missing updated_at")
664
+ known = {
665
+ "updated_at",
666
+ "last_started_at",
667
+ "last_succeeded_at",
668
+ "premade_through",
669
+ "retained_from",
670
+ "last_error_code",
671
+ "outcome",
672
+ "maintenance_run_id",
673
+ }
674
+ extra = {k: v for k, v in raw.items() if k not in known}
675
+ outcome_raw = raw.get("outcome")
676
+ return cls(
677
+ updated_at=_require_str(raw["updated_at"], "updated_at"),
678
+ last_started_at=_optional_str(raw.get("last_started_at"), "last_started_at"),
679
+ last_succeeded_at=_optional_str(
680
+ raw.get("last_succeeded_at"), "last_succeeded_at"
681
+ ),
682
+ premade_through=_optional_str(raw.get("premade_through"), "premade_through"),
683
+ retained_from=_optional_str(raw.get("retained_from"), "retained_from"),
684
+ last_error_code=_optional_str(raw.get("last_error_code"), "last_error_code"),
685
+ outcome=(
686
+ None if outcome_raw is None else MaintenanceOutcome.parse(outcome_raw)
687
+ ),
688
+ maintenance_run_id=_optional_str(
689
+ raw.get("maintenance_run_id"), "maintenance_run_id"
690
+ ),
691
+ extra=dict(extra),
692
+ )
693
+
694
+
695
+ @dataclass(frozen=True, slots=True)
696
+ class MaintenanceRunResult:
697
+ status: MaintenanceStatus
698
+ replayed: bool
699
+ admin_replay_expires_at: str
700
+ extra: Mapping[str, Any] = field(default_factory=dict)
701
+
702
+ @classmethod
703
+ def parse(cls, raw: object) -> MaintenanceRunResult:
704
+ if not isinstance(raw, Mapping):
705
+ raise ValueError("maintenance run result must be an object")
706
+ required = ("status", "replayed", "admin_replay_expires_at")
707
+ missing = [key for key in required if key not in raw]
708
+ if missing:
709
+ raise ValueError(f"maintenance run result missing fields: {missing}")
710
+ known = set(required)
711
+ extra = {k: v for k, v in raw.items() if k not in known}
712
+ return cls(
713
+ status=MaintenanceStatus.parse(raw["status"]),
714
+ replayed=_require_bool(raw["replayed"], "replayed"),
715
+ admin_replay_expires_at=_require_str(
716
+ raw["admin_replay_expires_at"], "admin_replay_expires_at"
717
+ ),
718
+ extra=dict(extra),
719
+ )
720
+
721
+
722
+ @dataclass(frozen=True, slots=True)
723
+ class StatsQueueAggregate:
724
+ name: str
725
+ ready_depth: int
726
+ delayed_depth: int
727
+ leased_depth: int
728
+ as_of: str
729
+ freshness: StatsFreshness
730
+ oldest_ready_age_seconds: float | None = None
731
+ extra: Mapping[str, Any] = field(default_factory=dict)
732
+
733
+ @classmethod
734
+ def parse(cls, raw: object) -> StatsQueueAggregate:
735
+ if not isinstance(raw, Mapping):
736
+ raise ValueError("stats queue aggregate must be an object")
737
+ required = (
738
+ "name",
739
+ "ready_depth",
740
+ "delayed_depth",
741
+ "leased_depth",
742
+ "as_of",
743
+ "freshness",
744
+ )
745
+ missing = [key for key in required if key not in raw]
746
+ if missing:
747
+ raise ValueError(f"stats queue aggregate missing fields: {missing}")
748
+ known = set(required) | {"oldest_ready_age_seconds"}
749
+ extra = {k: v for k, v in raw.items() if k not in known}
750
+ age = raw.get("oldest_ready_age_seconds")
751
+ if age is not None and not isinstance(age, (int, float)):
752
+ raise ValueError("oldest_ready_age_seconds must be a number or null")
753
+ return cls(
754
+ name=_require_str(raw["name"], "name"),
755
+ ready_depth=_require_int(raw["ready_depth"], "ready_depth"),
756
+ delayed_depth=_require_int(raw["delayed_depth"], "delayed_depth"),
757
+ leased_depth=_require_int(raw["leased_depth"], "leased_depth"),
758
+ as_of=_require_str(raw["as_of"], "as_of"),
759
+ freshness=StatsFreshness.parse(raw["freshness"]),
760
+ oldest_ready_age_seconds=float(age) if age is not None else None,
761
+ extra=dict(extra),
762
+ )
763
+
764
+
765
+ @dataclass(frozen=True, slots=True)
766
+ class StatsTelemetrySummary:
767
+ availability: StatsAvailability
768
+ source: StatsTelemetrySource
769
+ total: int | None = None
770
+ extra: Mapping[str, Any] = field(default_factory=dict)
771
+
772
+ @classmethod
773
+ def parse(cls, raw: object) -> StatsTelemetrySummary:
774
+ if not isinstance(raw, Mapping):
775
+ raise ValueError("stats telemetry summary must be an object")
776
+ if "availability" not in raw or "source" not in raw:
777
+ raise ValueError("stats telemetry summary requires availability and source")
778
+ known = {"availability", "source", "total"}
779
+ extra = {k: v for k, v in raw.items() if k not in known}
780
+ total = raw.get("total")
781
+ if total is not None:
782
+ total = _require_int(total, "total")
783
+ return cls(
784
+ availability=StatsAvailability.parse(raw["availability"]),
785
+ source=StatsTelemetrySource.parse(raw["source"]),
786
+ total=total,
787
+ extra=dict(extra),
788
+ )
789
+
790
+
791
+ @dataclass(frozen=True, slots=True)
792
+ class StatsMaintenanceSummary:
793
+ availability: StatsAvailability
794
+ last_started_at: str | None = None
795
+ last_succeeded_at: str | None = None
796
+ premade_through: str | None = None
797
+ retained_from: str | None = None
798
+ last_error_code: str | None = None
799
+ updated_at: str | None = None
800
+ extra: Mapping[str, Any] = field(default_factory=dict)
801
+
802
+ @classmethod
803
+ def parse(cls, raw: object) -> StatsMaintenanceSummary:
804
+ if not isinstance(raw, Mapping):
805
+ raise ValueError("stats maintenance summary must be an object")
806
+ if "availability" not in raw:
807
+ raise ValueError("stats maintenance summary missing availability")
808
+ known = {
809
+ "availability",
810
+ "last_started_at",
811
+ "last_succeeded_at",
812
+ "premade_through",
813
+ "retained_from",
814
+ "last_error_code",
815
+ "updated_at",
816
+ }
817
+ extra = {k: v for k, v in raw.items() if k not in known}
818
+ return cls(
819
+ availability=StatsAvailability.parse(raw["availability"]),
820
+ last_started_at=_optional_str(raw.get("last_started_at"), "last_started_at"),
821
+ last_succeeded_at=_optional_str(
822
+ raw.get("last_succeeded_at"), "last_succeeded_at"
823
+ ),
824
+ premade_through=_optional_str(raw.get("premade_through"), "premade_through"),
825
+ retained_from=_optional_str(raw.get("retained_from"), "retained_from"),
826
+ last_error_code=_optional_str(raw.get("last_error_code"), "last_error_code"),
827
+ updated_at=_optional_str(raw.get("updated_at"), "updated_at"),
828
+ extra=dict(extra),
829
+ )
830
+
831
+
832
+ @dataclass(frozen=True, slots=True)
833
+ class StatsSnapshot:
834
+ as_of: str
835
+ generated_at: str
836
+ age_seconds: float
837
+ freshness: StatsFreshness
838
+ queues: tuple[StatsQueueAggregate, ...]
839
+ retry: StatsTelemetrySummary
840
+ dead_letter: StatsTelemetrySummary
841
+ maintenance: StatsMaintenanceSummary
842
+ extra: Mapping[str, Any] = field(default_factory=dict)
843
+
844
+ @classmethod
845
+ def parse(cls, raw: object) -> StatsSnapshot:
846
+ if not isinstance(raw, Mapping):
847
+ raise ValueError("stats snapshot must be an object")
848
+ required = (
849
+ "as_of",
850
+ "generated_at",
851
+ "age_seconds",
852
+ "freshness",
853
+ "queues",
854
+ "retry",
855
+ "dead_letter",
856
+ "maintenance",
857
+ )
858
+ missing = [key for key in required if key not in raw]
859
+ if missing:
860
+ raise ValueError(f"stats snapshot missing fields: {missing}")
861
+ queues_raw = raw["queues"]
862
+ if not isinstance(queues_raw, list):
863
+ raise ValueError("stats snapshot queues must be an array")
864
+ age = raw["age_seconds"]
865
+ if not isinstance(age, (int, float)):
866
+ raise ValueError("age_seconds must be a number")
867
+ known = set(required)
868
+ extra = {k: v for k, v in raw.items() if k not in known}
869
+ return cls(
870
+ as_of=_require_str(raw["as_of"], "as_of"),
871
+ generated_at=_require_str(raw["generated_at"], "generated_at"),
872
+ age_seconds=float(age),
873
+ freshness=StatsFreshness.parse(raw["freshness"]),
874
+ queues=tuple(StatsQueueAggregate.parse(item) for item in queues_raw),
875
+ retry=StatsTelemetrySummary.parse(raw["retry"]),
876
+ dead_letter=StatsTelemetrySummary.parse(raw["dead_letter"]),
877
+ maintenance=StatsMaintenanceSummary.parse(raw["maintenance"]),
878
+ extra=dict(extra),
879
+ )
880
+
881
+
882
+ @dataclass(frozen=True, slots=True)
883
+ class BulkOperation:
884
+ value: str
885
+
886
+ @property
887
+ def is_unknown(self) -> bool:
888
+ return self.value not in KNOWN_BULK_OPERATIONS
889
+
890
+ @classmethod
891
+ def parse(cls, raw: object) -> BulkOperation:
892
+ if not isinstance(raw, str) or not raw:
893
+ raise ValueError("operation must be a non-empty string")
894
+ return cls(raw)
895
+
896
+
897
+ @dataclass(frozen=True, slots=True)
898
+ class BulkItemOutcomeKind:
899
+ value: str
900
+
901
+ @property
902
+ def is_unknown(self) -> bool:
903
+ return self.value not in KNOWN_BULK_ITEM_OUTCOMES
904
+
905
+ @classmethod
906
+ def parse(cls, raw: object) -> BulkItemOutcomeKind:
907
+ if not isinstance(raw, str) or not raw:
908
+ raise ValueError("outcome must be a non-empty string")
909
+ return cls(raw)
910
+
911
+
912
+ @dataclass(frozen=True, slots=True)
913
+ class DeadLetterReplayResult:
914
+ """Single dead-letter replay outcome.
915
+
916
+ Replay is at-least-once: ``replayed`` may be true on idempotent retry and
917
+ external side effects may repeat. ``source_task_id`` preserves immutable
918
+ terminal lineage; the new ``task_id`` is a distinct ready task.
919
+ """
920
+
921
+ task_id: str
922
+ source_task_id: str
923
+ queue: str
924
+ policy_version: int
925
+ replayed: bool
926
+ warning: str
927
+ admin_replay_expires_at: str | None = None
928
+ extra: Mapping[str, Any] = field(default_factory=dict)
929
+
930
+ @classmethod
931
+ def parse(cls, raw: object) -> DeadLetterReplayResult:
932
+ if not isinstance(raw, Mapping):
933
+ raise ValueError("dead letter replay result must be an object")
934
+ required = (
935
+ "task_id",
936
+ "source_task_id",
937
+ "queue",
938
+ "policy_version",
939
+ "replayed",
940
+ "warning",
941
+ )
942
+ missing = [key for key in required if key not in raw]
943
+ if missing:
944
+ raise ValueError(f"dead letter replay result missing fields: {missing}")
945
+ known = set(required) | {"admin_replay_expires_at"}
946
+ extra = {k: v for k, v in raw.items() if k not in known}
947
+ return cls(
948
+ task_id=_require_str(raw["task_id"], "task_id"),
949
+ source_task_id=_require_str(raw["source_task_id"], "source_task_id"),
950
+ queue=_require_str(raw["queue"], "queue"),
951
+ policy_version=_require_policy_version(raw["policy_version"], "policy_version"),
952
+ replayed=_require_bool(raw["replayed"], "replayed"),
953
+ warning=_require_str(raw["warning"], "warning"),
954
+ admin_replay_expires_at=_optional_str(
955
+ raw.get("admin_replay_expires_at"), "admin_replay_expires_at"
956
+ ),
957
+ extra=dict(extra),
958
+ )
959
+
960
+
961
+ @dataclass(frozen=True, slots=True)
962
+ class BulkPreviewResult:
963
+ """Dry-run bulk command preview with principal-bound confirmation token."""
964
+
965
+ operation: BulkOperation
966
+ queue: str
967
+ candidate_count: int
968
+ truncated: bool
969
+ sample_task_ids: tuple[str, ...]
970
+ confirmation_token: str
971
+ confirmation_expires_at: str
972
+ max_batch: int
973
+ extra: Mapping[str, Any] = field(default_factory=dict)
974
+
975
+ @classmethod
976
+ def parse(cls, raw: object) -> BulkPreviewResult:
977
+ if not isinstance(raw, Mapping):
978
+ raise ValueError("bulk preview result must be an object")
979
+ required = (
980
+ "operation",
981
+ "queue",
982
+ "candidate_count",
983
+ "truncated",
984
+ "sample_task_ids",
985
+ "confirmation_token",
986
+ "confirmation_expires_at",
987
+ "max_batch",
988
+ )
989
+ missing = [key for key in required if key not in raw]
990
+ if missing:
991
+ raise ValueError(f"bulk preview result missing fields: {missing}")
992
+ sample_raw = raw["sample_task_ids"]
993
+ if not isinstance(sample_raw, list):
994
+ raise ValueError("sample_task_ids must be an array")
995
+ if len(sample_raw) > BULK_SAMPLE_MAX:
996
+ raise ValueError("sample_task_ids exceeds 5 items")
997
+ candidate_count = _require_int(raw["candidate_count"], "candidate_count")
998
+ if candidate_count < 0 or candidate_count > BULK_CANDIDATE_MAX:
999
+ raise ValueError("candidate_count must be between 0 and 100 inclusive")
1000
+ max_batch = _require_int(raw["max_batch"], "max_batch")
1001
+ if max_batch < 1 or max_batch > BULK_BATCH_MAX:
1002
+ raise ValueError("max_batch must be between 1 and 25 inclusive")
1003
+ known = set(required)
1004
+ extra = {k: v for k, v in raw.items() if k not in known}
1005
+ return cls(
1006
+ operation=BulkOperation.parse(raw["operation"]),
1007
+ queue=_require_str(raw["queue"], "queue"),
1008
+ candidate_count=candidate_count,
1009
+ truncated=_require_bool(raw["truncated"], "truncated"),
1010
+ sample_task_ids=tuple(_require_str(item, "sample_task_ids[]") for item in sample_raw),
1011
+ confirmation_token=validate_confirmation_token(
1012
+ _require_str(raw["confirmation_token"], "confirmation_token")
1013
+ ),
1014
+ confirmation_expires_at=_require_str(
1015
+ raw["confirmation_expires_at"], "confirmation_expires_at"
1016
+ ),
1017
+ max_batch=max_batch,
1018
+ extra=dict(extra),
1019
+ )
1020
+
1021
+ def __repr__(self) -> str:
1022
+ return (
1023
+ f"BulkPreviewResult(operation={self.operation!r}, queue={self.queue!r}, "
1024
+ f"candidate_count={self.candidate_count}, truncated={self.truncated}, "
1025
+ f"sample_task_ids={self.sample_task_ids!r}, "
1026
+ f"confirmation_token=<redacted>, "
1027
+ f"confirmation_expires_at={self.confirmation_expires_at!r}, "
1028
+ f"max_batch={self.max_batch})"
1029
+ )
1030
+
1031
+ def __str__(self) -> str:
1032
+ return self.__repr__()
1033
+
1034
+
1035
+ @dataclass(frozen=True, slots=True)
1036
+ class BulkItemOutcome:
1037
+ task_id: str
1038
+ outcome: BulkItemOutcomeKind
1039
+ code: str | None = None
1040
+ extra: Mapping[str, Any] = field(default_factory=dict)
1041
+
1042
+ @classmethod
1043
+ def parse(cls, raw: object) -> BulkItemOutcome:
1044
+ if not isinstance(raw, Mapping):
1045
+ raise ValueError("bulk item outcome must be an object")
1046
+ if "task_id" not in raw or "outcome" not in raw:
1047
+ raise ValueError("bulk item outcome requires task_id and outcome")
1048
+ known = {"task_id", "outcome", "code"}
1049
+ extra = {k: v for k, v in raw.items() if k not in known}
1050
+ return cls(
1051
+ task_id=_require_str(raw["task_id"], "task_id"),
1052
+ outcome=BulkItemOutcomeKind.parse(raw["outcome"]),
1053
+ code=_optional_str(raw.get("code"), "code"),
1054
+ extra=dict(extra),
1055
+ )
1056
+
1057
+
1058
+ @dataclass(frozen=True, slots=True)
1059
+ class BulkExecuteResult:
1060
+ """Bounded bulk execute summary with explicit partial/retry semantics."""
1061
+
1062
+ operation: BulkOperation
1063
+ queue: str
1064
+ candidate_count: int
1065
+ start_index: int
1066
+ processed: int
1067
+ succeeded: int
1068
+ skipped: int
1069
+ failed: int
1070
+ partial: bool
1071
+ outcomes: tuple[BulkItemOutcome, ...]
1072
+ next_start_index: int | None = None
1073
+ extra: Mapping[str, Any] = field(default_factory=dict)
1074
+
1075
+ @classmethod
1076
+ def parse(cls, raw: object) -> BulkExecuteResult:
1077
+ if not isinstance(raw, Mapping):
1078
+ raise ValueError("bulk execute result must be an object")
1079
+ required = (
1080
+ "operation",
1081
+ "queue",
1082
+ "candidate_count",
1083
+ "start_index",
1084
+ "processed",
1085
+ "succeeded",
1086
+ "skipped",
1087
+ "failed",
1088
+ "partial",
1089
+ "outcomes",
1090
+ )
1091
+ missing = [key for key in required if key not in raw]
1092
+ if missing:
1093
+ raise ValueError(f"bulk execute result missing fields: {missing}")
1094
+ outcomes_raw = raw["outcomes"]
1095
+ if not isinstance(outcomes_raw, list):
1096
+ raise ValueError("outcomes must be an array")
1097
+ if len(outcomes_raw) > BULK_BATCH_MAX:
1098
+ raise ValueError("outcomes exceeds 25 items")
1099
+ candidate_count = _require_int(raw["candidate_count"], "candidate_count")
1100
+ if candidate_count < 0 or candidate_count > BULK_CANDIDATE_MAX:
1101
+ raise ValueError("candidate_count must be between 0 and 100 inclusive")
1102
+ processed = _require_int(raw["processed"], "processed")
1103
+ if processed < 0 or processed > BULK_BATCH_MAX:
1104
+ raise ValueError("processed must be between 0 and 25 inclusive")
1105
+ start_index = _require_int(raw["start_index"], "start_index")
1106
+ if start_index < 0:
1107
+ raise ValueError("start_index must be non-negative")
1108
+ known = set(required) | {"next_start_index"}
1109
+ extra = {k: v for k, v in raw.items() if k not in known}
1110
+ return cls(
1111
+ operation=BulkOperation.parse(raw["operation"]),
1112
+ queue=_require_str(raw["queue"], "queue"),
1113
+ candidate_count=candidate_count,
1114
+ start_index=start_index,
1115
+ processed=processed,
1116
+ succeeded=_require_int(raw["succeeded"], "succeeded"),
1117
+ skipped=_require_int(raw["skipped"], "skipped"),
1118
+ failed=_require_int(raw["failed"], "failed"),
1119
+ partial=_require_bool(raw["partial"], "partial"),
1120
+ outcomes=tuple(BulkItemOutcome.parse(item) for item in outcomes_raw),
1121
+ next_start_index=_optional_int(raw.get("next_start_index"), "next_start_index"),
1122
+ extra=dict(extra),
1123
+ )
1124
+
1125
+
1126
+ @dataclass(frozen=True, slots=True)
1127
+ class BreakGlassMutationResult:
1128
+ operation: str
1129
+ target_id: str
1130
+ outcome: str
1131
+ queue: str | None = None
1132
+ generation: int | None = None
1133
+ extra: Mapping[str, Any] = field(default_factory=dict)
1134
+
1135
+ @classmethod
1136
+ def parse(cls, raw: object) -> BreakGlassMutationResult:
1137
+ if not isinstance(raw, Mapping):
1138
+ raise ValueError("break-glass mutation result must be an object")
1139
+ _reject_break_glass_secret_keys(raw)
1140
+ required = ("operation", "target_id", "outcome")
1141
+ missing = [key for key in required if key not in raw]
1142
+ if missing:
1143
+ raise ValueError(f"break-glass mutation result missing fields: {missing}")
1144
+ known = set(required) | {"queue", "generation"}
1145
+ extra = {k: v for k, v in raw.items() if k not in known}
1146
+ generation = raw.get("generation")
1147
+ if generation is not None:
1148
+ generation = _require_int(generation, "generation")
1149
+ if generation < 0:
1150
+ raise ValueError("generation must be at least 0")
1151
+ return cls(
1152
+ operation=_require_str(raw["operation"], "operation"),
1153
+ target_id=_require_str(raw["target_id"], "target_id"),
1154
+ outcome=_require_str(raw["outcome"], "outcome"),
1155
+ queue=_optional_str(raw.get("queue"), "queue"),
1156
+ generation=generation,
1157
+ extra=dict(extra),
1158
+ )
1159
+
1160
+
1161
+ @dataclass(frozen=True, slots=True)
1162
+ class BreakGlassCounterResult:
1163
+ operation: str
1164
+ queue: str
1165
+ target_id: str
1166
+ outcome: str
1167
+ delayed_count: int
1168
+ ready_count: int
1169
+ leased_count: int
1170
+ extra: Mapping[str, Any] = field(default_factory=dict)
1171
+
1172
+ @classmethod
1173
+ def parse(cls, raw: object) -> BreakGlassCounterResult:
1174
+ if not isinstance(raw, Mapping):
1175
+ raise ValueError("break-glass counter result must be an object")
1176
+ _reject_break_glass_secret_keys(raw)
1177
+ required = (
1178
+ "operation",
1179
+ "queue",
1180
+ "target_id",
1181
+ "outcome",
1182
+ "delayed_count",
1183
+ "ready_count",
1184
+ "leased_count",
1185
+ )
1186
+ missing = [key for key in required if key not in raw]
1187
+ if missing:
1188
+ raise ValueError(f"break-glass counter result missing fields: {missing}")
1189
+ known = set(required)
1190
+ extra = {k: v for k, v in raw.items() if k not in known}
1191
+ return cls(
1192
+ operation=_require_str(raw["operation"], "operation"),
1193
+ queue=_require_str(raw["queue"], "queue"),
1194
+ target_id=_require_str(raw["target_id"], "target_id"),
1195
+ outcome=_require_str(raw["outcome"], "outcome"),
1196
+ delayed_count=_require_int(raw["delayed_count"], "delayed_count"),
1197
+ ready_count=_require_int(raw["ready_count"], "ready_count"),
1198
+ leased_count=_require_int(raw["leased_count"], "leased_count"),
1199
+ extra=dict(extra),
1200
+ )
1201
+
1202
+
1203
+ @dataclass(frozen=True, slots=True)
1204
+ class BreakGlassReplayLimitResult:
1205
+ operation: str
1206
+ queue: str
1207
+ target_id: str
1208
+ outcome: str
1209
+ effective_rps: float
1210
+ extra: Mapping[str, Any] = field(default_factory=dict)
1211
+
1212
+ @classmethod
1213
+ def parse(cls, raw: object) -> BreakGlassReplayLimitResult:
1214
+ if not isinstance(raw, Mapping):
1215
+ raise ValueError("break-glass replay limit result must be an object")
1216
+ _reject_break_glass_secret_keys(raw)
1217
+ required = ("operation", "queue", "target_id", "outcome", "effective_rps")
1218
+ missing = [key for key in required if key not in raw]
1219
+ if missing:
1220
+ raise ValueError(f"break-glass replay limit result missing fields: {missing}")
1221
+ effective = raw["effective_rps"]
1222
+ if not isinstance(effective, (int, float)):
1223
+ raise ValueError("effective_rps must be a number")
1224
+ if effective < 0:
1225
+ raise ValueError("effective_rps must be at least 0")
1226
+ known = set(required)
1227
+ extra = {k: v for k, v in raw.items() if k not in known}
1228
+ return cls(
1229
+ operation=_require_str(raw["operation"], "operation"),
1230
+ queue=_require_str(raw["queue"], "queue"),
1231
+ target_id=_require_str(raw["target_id"], "target_id"),
1232
+ outcome=_require_str(raw["outcome"], "outcome"),
1233
+ effective_rps=float(effective),
1234
+ extra=dict(extra),
1235
+ )
1236
+
1237
+
1238
+ def validate_break_glass_reason(reason: str) -> str:
1239
+ if not isinstance(reason, str) or not reason:
1240
+ raise ValueError("reason must be a non-empty string")
1241
+ if len(reason) > BREAK_GLASS_REASON_MAX_LENGTH:
1242
+ raise ValueError("reason exceeds 512 characters")
1243
+ return reason
1244
+
1245
+
1246
+ def validate_incident_reference(incident_reference: str) -> str:
1247
+ if not isinstance(incident_reference, str) or not incident_reference:
1248
+ raise ValueError("incident_reference must be a non-empty string")
1249
+ if len(incident_reference) > INCIDENT_REFERENCE_MAX_LENGTH:
1250
+ raise ValueError("incident_reference exceeds 128 characters")
1251
+ return incident_reference
1252
+
1253
+
1254
+ def validate_risk_acknowledged(value: bool) -> bool:
1255
+ if value is not True:
1256
+ raise ValueError("risk_acknowledged must be true")
1257
+ return True
1258
+
1259
+
1260
+ def validate_acknowledge_duplicate_window(value: bool) -> bool:
1261
+ if value is not True:
1262
+ raise ValueError("acknowledge_duplicate_window must be true")
1263
+ return True
1264
+
1265
+
1266
+ def validate_event_id(event_id: str) -> str:
1267
+ return validate_task_id(event_id)
1268
+
1269
+
1270
+ def validate_partition_name(partition_name: str) -> str:
1271
+ if not isinstance(partition_name, str) or not partition_name:
1272
+ raise ValueError("partition_name must be a non-empty string")
1273
+ if len(partition_name) > PARTITION_NAME_MAX_LENGTH:
1274
+ raise ValueError("partition_name exceeds 128 characters")
1275
+ if not _PARTITION_NAME_RE.match(partition_name):
1276
+ raise ValueError("partition_name must match OpenAPI pattern")
1277
+ return partition_name
1278
+
1279
+
1280
+ def validate_replay_factor(factor: float) -> float:
1281
+ if isinstance(factor, bool) or not isinstance(factor, (int, float)):
1282
+ raise ValueError("factor must be a number")
1283
+ numeric = float(factor)
1284
+ if numeric < REPLAY_FACTOR_MIN or numeric > REPLAY_FACTOR_MAX:
1285
+ raise ValueError("factor must be between 1.0 and 10.0 inclusive")
1286
+ return numeric
1287
+
1288
+
1289
+ def validate_replay_ttl_seconds(ttl_seconds: int) -> int:
1290
+ if isinstance(ttl_seconds, bool) or not isinstance(ttl_seconds, int):
1291
+ raise ValueError("ttl_seconds must be an integer")
1292
+ if ttl_seconds < REPLAY_TTL_SECONDS_MIN or ttl_seconds > REPLAY_TTL_SECONDS_MAX:
1293
+ raise ValueError("ttl_seconds must be between 1 and 3600 inclusive")
1294
+ return ttl_seconds
1295
+
1296
+
1297
+ def validate_registry_entry_id(entry_id: int) -> int:
1298
+ if isinstance(entry_id, bool) or not isinstance(entry_id, int):
1299
+ raise ValueError("entry_id must be an integer")
1300
+ if entry_id < REGISTRY_ENTRY_ID_MIN:
1301
+ raise ValueError("entry_id must be at least 1")
1302
+ return entry_id
1303
+
1304
+
1305
+ def validate_extend_seconds(extend_seconds: int) -> int:
1306
+ if isinstance(extend_seconds, bool) or not isinstance(extend_seconds, int):
1307
+ raise ValueError("extend_seconds must be an integer")
1308
+ if extend_seconds < EXTEND_SECONDS_MIN or extend_seconds > EXTEND_SECONDS_MAX:
1309
+ raise ValueError("extend_seconds must be between 1 and 2592000 inclusive")
1310
+ return extend_seconds
1311
+
1312
+
1313
+ def validate_failure_code(failure_code: str) -> str:
1314
+ if not isinstance(failure_code, str) or not failure_code:
1315
+ raise ValueError("failure_code must be a non-empty string")
1316
+ if len(failure_code) > FAILURE_CODE_MAX_LENGTH:
1317
+ raise ValueError("failure_code exceeds 128 characters")
1318
+ return failure_code
1319
+
1320
+
1321
+ def validate_idempotency_key(key: str) -> str:
1322
+ if not isinstance(key, str) or not key:
1323
+ raise ValueError("idempotency_key must be a non-empty string")
1324
+ if len(key) < IDEMPOTENCY_KEY_MIN_LENGTH or len(key) > IDEMPOTENCY_KEY_MAX_LENGTH:
1325
+ raise ValueError("idempotency_key must be between 1 and 256 characters inclusive")
1326
+ return key
1327
+
1328
+
1329
+ def validate_reason(reason: str) -> str:
1330
+ if not isinstance(reason, str) or not reason:
1331
+ raise ValueError("reason must be a non-empty string")
1332
+ if len(reason) > REASON_MAX_LENGTH:
1333
+ raise ValueError("reason exceeds 512 characters")
1334
+ return reason
1335
+
1336
+
1337
+ def validate_confirmation_token(token: str) -> str:
1338
+ if not isinstance(token, str) or not token:
1339
+ raise ValueError("confirmation_token must be a non-empty string")
1340
+ if len(token) > CONFIRMATION_TOKEN_MAX_LENGTH:
1341
+ raise ValueError("confirmation_token exceeds 24576 characters")
1342
+ return token
1343
+
1344
+
1345
+ def _reject_break_glass_secret_keys(raw: Mapping[str, Any]) -> None:
1346
+ for key in raw:
1347
+ key_str = str(key)
1348
+ if _FORBIDDEN_BREAK_GLASS_SECRET_KEY_RE.search(key_str):
1349
+ raise ValueError(f"break-glass result must not contain {key_str!r}")
1350
+
1351
+
1352
+ def validate_bulk_filters(filters: Mapping[str, str]) -> dict[str, str]:
1353
+ if not isinstance(filters, Mapping):
1354
+ raise ValueError("filters must be a mapping")
1355
+ normalized: dict[str, str] = {}
1356
+ for raw_key, raw_val in filters.items():
1357
+ if not isinstance(raw_key, str) or not raw_key.strip():
1358
+ raise ValueError("filter keys must be non-empty strings")
1359
+ key = raw_key.strip().lower()
1360
+ if key in _FORBIDDEN_BULK_FILTER_KEYS:
1361
+ raise ValueError(f"filter {key!r} is not allowed")
1362
+ if key not in _ALLOWED_BULK_FILTER_KEYS:
1363
+ raise ValueError(f"unknown filter {key!r}")
1364
+ if not isinstance(raw_val, str) or not raw_val.strip():
1365
+ raise ValueError("filter values must be non-empty strings")
1366
+ stripped = raw_val.strip()
1367
+ if len(stripped) > 128:
1368
+ raise ValueError("filter values must be at most 128 characters")
1369
+ normalized[key] = stripped
1370
+ return dict(sorted(normalized.items()))
1371
+
1372
+
1373
+ def validate_batch_limit(batch_limit: int | None) -> int | None:
1374
+ if batch_limit is None:
1375
+ return None
1376
+ if isinstance(batch_limit, bool) or not isinstance(batch_limit, int):
1377
+ raise ValueError("batch_limit must be an integer or None")
1378
+ if batch_limit < 1 or batch_limit > BULK_BATCH_MAX:
1379
+ raise ValueError("batch_limit must be between 1 and 25 inclusive")
1380
+ return batch_limit
1381
+
1382
+
1383
+ def validate_start_index(start_index: int) -> int:
1384
+ if isinstance(start_index, bool) or not isinstance(start_index, int):
1385
+ raise ValueError("start_index must be an integer")
1386
+ if start_index < 0:
1387
+ raise ValueError("start_index must be non-negative")
1388
+ return start_index
1389
+
1390
+
1391
+ def validate_known_queue_state(state: QueueState) -> QueueState:
1392
+ if state.is_unknown:
1393
+ raise ValueError("state must be active, paused, or draining")
1394
+ return state
1395
+
1396
+
1397
+ def validate_queue_name(name: str) -> str:
1398
+ if not isinstance(name, str) or not name:
1399
+ raise ValueError("queue_name must be a non-empty string")
1400
+ if len(name) > QUEUE_NAME_MAX_LENGTH:
1401
+ raise ValueError("queue_name exceeds 128 characters")
1402
+ if not _QUEUE_NAME_RE.match(name):
1403
+ raise ValueError("queue_name must match OpenAPI pattern")
1404
+ return name
1405
+
1406
+
1407
+ def validate_task_id(task_id: str) -> str:
1408
+ if not isinstance(task_id, str) or not task_id:
1409
+ raise ValueError("task_id must be a non-empty string")
1410
+ try:
1411
+ uuid.UUID(task_id)
1412
+ except ValueError as exc:
1413
+ raise ValueError("task_id must be a UUID") from exc
1414
+ return task_id
1415
+
1416
+
1417
+ def validate_page_limit(limit: int) -> int:
1418
+ if isinstance(limit, bool) or not isinstance(limit, int):
1419
+ raise ValueError("limit must be an integer")
1420
+ if limit < PAGE_LIMIT_MIN or limit > PAGE_LIMIT_MAX:
1421
+ raise ValueError("limit must be between 1 and 100 inclusive")
1422
+ return limit
1423
+
1424
+
1425
+ def validate_cursor(cursor: str | None) -> str | None:
1426
+ if cursor is None:
1427
+ return None
1428
+ if not isinstance(cursor, str):
1429
+ raise ValueError("cursor must be a string or None")
1430
+ if len(cursor) > CURSOR_MAX_LENGTH:
1431
+ raise ValueError("cursor exceeds 512 characters")
1432
+ return cursor
1433
+
1434
+
1435
+ def validate_time_range(time_from: datetime, time_to: datetime) -> tuple[datetime, datetime]:
1436
+ if time_from.tzinfo is None or time_from.utcoffset() is None:
1437
+ raise ValueError("from must be timezone-aware")
1438
+ if time_to.tzinfo is None or time_to.utcoffset() is None:
1439
+ raise ValueError("to must be timezone-aware")
1440
+ if time_from > time_to:
1441
+ raise ValueError("from must be no later than to")
1442
+ return time_from, time_to
1443
+
1444
+
1445
+ def format_datetime(value: datetime) -> str:
1446
+ if value.tzinfo is None or value.utcoffset() is None:
1447
+ raise ValueError("datetime must be timezone-aware")
1448
+ utc = value.astimezone(UTC)
1449
+ text = utc.isoformat()
1450
+ if text.endswith("+00:00"):
1451
+ return text[:-6] + "Z"
1452
+ return text
1453
+
1454
+
1455
+ def _require_str(value: object, name: str) -> str:
1456
+ if not isinstance(value, str):
1457
+ raise ValueError(f"{name} must be a string")
1458
+ return value
1459
+
1460
+
1461
+ def _optional_str(value: object, name: str) -> str | None:
1462
+ if value is None:
1463
+ return None
1464
+ return _require_str(value, name)
1465
+
1466
+
1467
+ def _require_int(value: object, name: str) -> int:
1468
+ if isinstance(value, bool) or not isinstance(value, int):
1469
+ raise ValueError(f"{name} must be an integer")
1470
+ return value
1471
+
1472
+
1473
+ def _optional_int(value: object, name: str) -> int | None:
1474
+ if value is None:
1475
+ return None
1476
+ return _require_int(value, name)
1477
+
1478
+
1479
+ def _require_bool(value: object, name: str) -> bool:
1480
+ if not isinstance(value, bool):
1481
+ raise ValueError(f"{name} must be a boolean")
1482
+ return value
1483
+
1484
+
1485
+ def _optional_bool(value: object, name: str) -> bool | None:
1486
+ if value is None:
1487
+ return None
1488
+ return _require_bool(value, name)
1489
+
1490
+
1491
+ def _require_config_version(value: object, name: str) -> int:
1492
+ parsed = _require_int(value, name)
1493
+ if parsed < CONFIG_VERSION_MIN:
1494
+ raise ValueError(f"{name} must be at least 1")
1495
+ return parsed
1496
+
1497
+
1498
+ def _require_policy_version(value: object, name: str) -> int:
1499
+ parsed = _require_int(value, name)
1500
+ if parsed < POLICY_VERSION_MIN:
1501
+ raise ValueError(f"{name} must be at least 1")
1502
+ return parsed
1503
+
1504
+
1505
+ def _require_max_attempts(value: object) -> int:
1506
+ parsed = _require_int(value, "max_attempts")
1507
+ if parsed < MAX_ATTEMPTS_MIN:
1508
+ raise ValueError("max_attempts must be at least 1")
1509
+ return parsed
1510
+
1511
+
1512
+ def _require_retry_delay_seconds(value: object) -> int:
1513
+ parsed = _require_int(value, "retry_delay_seconds")
1514
+ if parsed < 0 or parsed > RETRY_DELAY_SECONDS_MAX:
1515
+ raise ValueError("retry_delay_seconds must be between 0 and 86400 inclusive")
1516
+ return parsed