Source code for rath.adapters.context
"""Uniform request context propagated to every external adapter."""
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import datetime, timezone
from uuid import UUID
from rath.context import TraceContext
from rath.security import PolicyConstraints
__all__ = [
"AdapterRequestContext",
"effective_timeout_seconds",
"merge_policy_constraints",
"with_policy_constraints",
]
[docs]
@dataclass(frozen=True, slots=True)
class AdapterRequestContext:
run_id: UUID
node_id: str
tenant_id: str
deadline: datetime | None
trace_context: TraceContext
idempotency_key: str | None
policy_constraints: PolicyConstraints
checkpoint_sequence: int | None = None
def __post_init__(self) -> None:
if not self.node_id.strip():
raise ValueError("adapter node_id must not be empty")
if not self.tenant_id.strip():
raise ValueError("adapter tenant_id must not be empty")
if self.deadline is not None and self.deadline.tzinfo is None:
raise ValueError("adapter deadline must be timezone-aware")
if self.checkpoint_sequence is not None and self.checkpoint_sequence < 1:
raise ValueError("checkpoint_sequence must be positive")
def effective_timeout_seconds(
requested: float,
*,
adapter_context: AdapterRequestContext,
run_remaining_seconds: float | None,
) -> float:
"""Resolve the strictest positive timeout from spec, policy and deadlines."""
candidates = [requested]
policy_timeout = adapter_context.policy_constraints.timeout_seconds
if policy_timeout is not None:
candidates.append(policy_timeout)
if run_remaining_seconds is not None:
candidates.append(run_remaining_seconds)
if adapter_context.deadline is not None:
candidates.append(
max(
0.0,
(adapter_context.deadline - datetime.now(timezone.utc)).total_seconds(),
)
)
return min(candidates)
[docs]
def merge_policy_constraints(
declared: PolicyConstraints,
decided: PolicyConstraints,
) -> PolicyConstraints:
"""Combine caller and policy limits without weakening either side."""
timeouts = [
value
for value in (declared.timeout_seconds, decided.timeout_seconds)
if value is not None
]
output_limits = [
value
for value in (declared.max_output_bytes, decided.max_output_bytes)
if value is not None
]
if declared.allowed_network_hosts and decided.allowed_network_hosts:
allowed_hosts = declared.allowed_network_hosts & decided.allowed_network_hosts
else:
allowed_hosts = declared.allowed_network_hosts or decided.allowed_network_hosts
return PolicyConstraints(
timeout_seconds=min(timeouts) if timeouts else None,
max_output_bytes=min(output_limits) if output_limits else None,
allowed_network_hosts=allowed_hosts,
filesystem_root=decided.filesystem_root or declared.filesystem_root,
read_only=declared.read_only or decided.read_only,
redactions=declared.redactions | decided.redactions,
)
[docs]
def with_policy_constraints(
context: AdapterRequestContext,
constraints: PolicyConstraints,
) -> AdapterRequestContext:
"""Return the adapter context carrying the evaluated policy constraints."""
return replace(
context,
policy_constraints=merge_policy_constraints(
context.policy_constraints,
constraints,
),
)