Source code for httk.store.store_timestamp
"""Backend-neutral store-managed timestamp helpers."""
import datetime
import time
from collections.abc import Callable
__all__ = [
"FUTURE_TIMESTAMP_SLACK_NS",
"_CLOCK_REGRESSION_GRACE_NS",
"StoreClockRegressionError",
"advance_store_timestamp_mark",
"capture_store_timestamp",
"encode_store_timestamp_state",
"ns_operand_to_store_units",
"parse_store_timestamp_state",
]
_CLOCK_REGRESSION_GRACE_NS = 1_000_000
[docs]
FUTURE_TIMESTAMP_SLACK_NS = 2_000_000_000
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)
[docs]
class StoreClockRegressionError(RuntimeError):
"""A writable store clock is behind its process-local timestamp mark."""
def __init__(self, mark_ns: int, clock_ns: int) -> None:
super().__init__(
f"store clock regressed: high-water mark is {mark_ns} ns but clock is {clock_ns} ns; "
"wait for the clock to catch up or enable allow_clock_regression=True"
)
[docs]
def capture_store_timestamp(
clock: Callable[[], int],
resolution: int,
mark: int | None,
*,
allow_clock_regression: bool = False,
clock_regression_grace: bool = True,
) -> int:
"""Capture one guarded store-unit timestamp from a nanosecond clock."""
clock_ns = clock()
if allow_clock_regression:
return clock_ns // resolution
captured = clock_ns // resolution
if mark is not None and captured < mark:
deficit_ns = mark * resolution - clock_ns
if clock_regression_grace and deficit_ns < _CLOCK_REGRESSION_GRACE_NS:
time.sleep(deficit_ns / 1_000_000_000 + 0.000001)
clock_ns = clock()
captured = clock_ns // resolution
if captured < mark:
raise StoreClockRegressionError(mark * resolution, clock_ns)
return captured
[docs]
def advance_store_timestamp_mark(mark: int | None, captured: int | None, *, allow_clock_regression: bool) -> int | None:
"""Advance a process-local timestamp high-water mark after a successful save."""
if captured is None or allow_clock_regression:
return mark
return captured if mark is None else max(mark, captured)
[docs]
def encode_store_timestamp_state(enabled: bool, resolution: int) -> str:
"""Encode the persisted timestamp configuration marker."""
return f"v1:{resolution}" if enabled else "off"
[docs]
def parse_store_timestamp_state(value: object) -> tuple[bool, int | None] | None:
"""Parse a persisted timestamp marker, returning ``None`` for unknown values."""
if value == "off":
return False, None
if isinstance(value, str) and value.startswith("v1:"):
encoded_resolution = value[3:]
try:
resolution = int(encoded_resolution)
except ValueError:
return None
if resolution <= 0 or str(resolution) != encoded_resolution:
return None
return True, resolution
return None
[docs]
def ns_operand_to_store_units(value: object, resolution: int) -> int:
"""Convert a nanosecond, aware datetime, or RFC3339 string to store units."""
if isinstance(value, int) and not isinstance(value, bool):
return value // resolution
if isinstance(value, datetime.datetime):
parsed = value
elif isinstance(value, str):
normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value
try:
parsed = datetime.datetime.fromisoformat(normalized)
except ValueError as error:
raise ValueError("store_timestamp requires an RFC3339/ISO-8601 timestamp") from error
else:
raise ValueError("store_timestamp requires an integer nanosecond, aware datetime, or ISO-8601 string")
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ValueError("store_timestamp requires a timezone-aware datetime")
utc = parsed.astimezone(datetime.UTC)
delta = utc - EPOCH
nanoseconds = delta.days * 86_400 * 1_000_000_000 + delta.seconds * 1_000_000_000 + delta.microseconds * 1_000
return nanoseconds // resolution