← Files Portable ResumeARCHIVED FILE
skills/.portable-resume/runtime/portable_resume/time_range.py
6.67 KB · Oct 4, 2026 · 12:33 UTC
"""Human-friendly time ranges and list continuation cursors (#157)."""
from __future__ import annotations
import base64
import json
import re
from datetime import datetime, timedelta, timezone
from typing import Any
from .diagnostics import DiagnosticError
from .model import SessionSummary
from .select import _timestamp_micros, summary_sort_key
_REL = re.compile(r"^(?P<n>\d+)(?P<u>[mhdw])$")
_UNIT_SECONDS = {"m": 60, "h": 3600, "d": 86400, "w": 604800}
# Product policy: at most 2 years of relative history for a single query.
_MAX_RELATIVE_SECONDS = 2 * 365 * 24 * 3600
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def parse_time_token(raw: str, *, now: datetime | None = None) -> datetime:
"""Parse ``30m`` / ``2h`` / ``7d`` / ``2w`` or timezone-aware ISO-8601."""
if not isinstance(raw, str) or not raw.strip():
raise DiagnosticError.invalid()
text = raw.strip()
rel = _REL.fullmatch(text)
if rel is not None:
n = int(rel.group("n"))
unit = rel.group("u")
seconds = n * _UNIT_SECONDS[unit]
if n < 1 or seconds > _MAX_RELATIVE_SECONDS:
raise DiagnosticError.invalid()
base = now if now is not None else utc_now()
if base.tzinfo is None:
base = base.replace(tzinfo=timezone.utc)
return base.astimezone(timezone.utc) - timedelta(seconds=seconds)
# ISO-8601 with explicit timezone (reject naive).
try:
if text.endswith("Z"):
dt = datetime.fromisoformat(text[:-1] + "+00:00")
else:
dt = datetime.fromisoformat(text)
except ValueError as error:
raise DiagnosticError.invalid() from error
if dt.tzinfo is None:
raise DiagnosticError.invalid()
return dt.astimezone(timezone.utc)
def resolve_window(
*,
since: str | None,
until: str | None,
within_min: int | None,
now: datetime | None = None,
) -> tuple[datetime | None, datetime | None, dict[str, Any]]:
"""Return (since_dt, until_dt, echo) for listing filters.
``--within-min`` conflicts with ``--since``. ``within_min==0`` means no age
filter (existing product semantics).
"""
base = now if now is not None else utc_now()
if base.tzinfo is None:
base = base.replace(tzinfo=timezone.utc)
base = base.astimezone(timezone.utc)
if since is not None and within_min is not None:
raise DiagnosticError.invalid()
since_dt: datetime | None = None
until_dt: datetime | None = None
echo: dict[str, Any] = {
"since": None,
"until": None,
"within_min": within_min,
}
if within_min is not None:
if within_min < 0 or within_min > 10 * 365 * 24 * 60:
raise DiagnosticError.invalid()
if within_min > 0:
since_dt = base - timedelta(minutes=within_min)
echo["since"] = since_dt.isoformat().replace("+00:00", "Z")
if since is not None:
since_dt = parse_time_token(since, now=base)
echo["since"] = since_dt.isoformat().replace("+00:00", "Z")
if until is not None:
until_dt = parse_time_token(until, now=base)
echo["until"] = until_dt.isoformat().replace("+00:00", "Z")
if since_dt is not None and until_dt is not None and since_dt > until_dt:
raise DiagnosticError.invalid()
return since_dt, until_dt, echo
def summary_in_window(
summary: SessionSummary,
*,
since_dt: datetime | None,
until_dt: datetime | None,
) -> bool:
if since_dt is None and until_dt is None:
return True
micros = _timestamp_micros(summary.updated_at)
if micros == 0 and summary.updated_at is None:
# Unknown time: exclude when a time bound is active (fail closed).
return False
ts = datetime.fromtimestamp(micros / 1_000_000, tz=timezone.utc)
if since_dt is not None and ts < since_dt:
return False
if until_dt is not None and ts > until_dt:
return False
return True
def encode_cursor(summary: SessionSummary) -> str:
payload = {
"u": summary.updated_at,
"s": summary.source,
"i": summary.session_id,
}
raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def decode_cursor(token: str) -> dict[str, str | None]:
if not isinstance(token, str) or not token.strip():
raise DiagnosticError.invalid()
text = token.strip()
pad = "=" * (-len(text) % 4)
try:
raw = base64.urlsafe_b64decode(text + pad)
data = json.loads(raw.decode("utf-8"))
except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as error:
raise DiagnosticError.invalid() from error
if not isinstance(data, dict):
raise DiagnosticError.invalid()
u = data.get("u")
s = data.get("s")
i = data.get("i")
if s is not None and not isinstance(s, str):
raise DiagnosticError.invalid()
if i is not None and not isinstance(i, str):
raise DiagnosticError.invalid()
if u is not None and not isinstance(u, str):
raise DiagnosticError.invalid()
return {"u": u, "s": s, "i": i}
def apply_cursor(
summaries: list[SessionSummary],
cursor: str | None,
) -> list[SessionSummary]:
"""Keyset continuation: return items strictly after the cursor in sort order."""
ordered = sorted(summaries, key=summary_sort_key)
if cursor is None:
return ordered
mark = decode_cursor(cursor)
# Find first item strictly after cursor using the same sort key shape.
marker = SessionSummary(
source=str(mark["s"] or ""),
session_id=str(mark["i"] or ""),
updated_at=mark["u"],
)
marker_key = summary_sort_key(marker)
return [row for row in ordered if summary_sort_key(row) > marker_key]
def page_summaries(
summaries: list[SessionSummary],
*,
limit: int,
cursor: str | None,
since_dt: datetime | None,
until_dt: datetime | None,
) -> tuple[list[SessionSummary], str | None, dict[str, Any]]:
"""Filter by time, apply cursor, take limit; return page + next_cursor + meta."""
from .bounds import DEFAULT_BOUNDS
if type(limit) is not int or limit < 1 or limit > DEFAULT_BOUNDS.listed_sessions:
raise DiagnosticError.invalid()
filtered = [
row
for row in summaries
if summary_in_window(row, since_dt=since_dt, until_dt=until_dt)
]
after = apply_cursor(filtered, cursor)
page = after[:limit]
next_cursor = encode_cursor(page[-1]) if len(after) > limit and page else None
meta = {
"limit": limit,
"returned": len(page),
"next_cursor": next_cursor,
"has_more": next_cursor is not None,
}
return page, next_cursor, meta
SHA-256: 50f8ba8589ab3898eb0b46de639be1995302ccf47a41deefe6e0c39c8feb7520