Source code for rath.context
"""Request, trace, and durable run context contracts."""
from __future__ import annotations
import secrets
from dataclasses import dataclass, field
from datetime import datetime, timezone
from uuid import UUID, uuid4
from rath.errors import ErrorCode, RathError
from rath.security.context import SecurityContext
__all__ = [
"DeadlineExceededError",
"RunContext",
"TraceContext",
]
def _validate_hex(value: str, *, length: int, field_name: str) -> str:
normalized = value.lower()
if len(normalized) != length:
raise ValueError(f"{field_name} must contain {length} hexadecimal characters")
try:
int(normalized, 16)
except ValueError as exc:
raise ValueError(f"{field_name} must be hexadecimal") from exc
return normalized
[docs]
@dataclass(frozen=True, slots=True)
class TraceContext:
"""Minimal W3C-compatible trace correlation identifiers."""
trace_id: str
span_id: str
sampled: bool = True
def __post_init__(self) -> None:
object.__setattr__(
self,
"trace_id",
_validate_hex(self.trace_id, length=32, field_name="trace_id"),
)
object.__setattr__(
self,
"span_id",
_validate_hex(self.span_id, length=16, field_name="span_id"),
)
[docs]
@classmethod
def new(cls, *, sampled: bool = True) -> "TraceContext":
return cls(
trace_id=secrets.token_hex(16),
span_id=secrets.token_hex(8),
sampled=sampled,
)
[docs]
class DeadlineExceededError(RathError):
def __init__(self) -> None:
super().__init__(
ErrorCode.DEADLINE_EXCEEDED,
"run deadline has been exceeded",
retryable=False,
)
[docs]
@dataclass(frozen=True, slots=True)
class RunContext:
"""Explicit context propagated through runtime and adapter calls."""
security: SecurityContext
revision_id: UUID
request_id: UUID = field(default_factory=uuid4)
trace_context: TraceContext = field(default_factory=TraceContext.new)
deadline: datetime | None = None
def __post_init__(self) -> None:
if self.deadline is not None and self.deadline.tzinfo is None:
raise ValueError("deadline must be timezone-aware")
[docs]
@classmethod
def local(
cls,
*,
revision_id: UUID,
deadline: datetime | None = None,
) -> "RunContext":
return cls(
security=SecurityContext.local(),
revision_id=revision_id,
deadline=deadline,
)
[docs]
def remaining_seconds(self, *, now: datetime | None = None) -> float | None:
if self.deadline is None:
return None
current = now or datetime.now(timezone.utc)
if current.tzinfo is None:
raise ValueError("now must be timezone-aware")
return max(0.0, (self.deadline - current).total_seconds())
[docs]
def ensure_active(self, *, now: datetime | None = None) -> None:
remaining = self.remaining_seconds(now=now)
if remaining is not None and remaining <= 0:
raise DeadlineExceededError()