mempalace.date_window
Source: mempalace/date_window.py
Shared date-window parsing for read-path filters.
list_drawers (#1128) and search (#463) accept the same optional since/before bounds and compare them against drawer filed_at metadata. The parsing and window semantics live here — a side-effect-free module — so searcher can use them without importing mcp_server (whose import installs MCP stdio protection) and mcp_server keeps its existing behavior by aliasing these functions.
Semantics (issue spec, #1128):
sinceis inclusive,beforeis exclusive:[since, before).- Comparison is wall-clock and timezone-naive.
filed_atvalues are written as naive local ISO strings (datetime.now().isoformat()) almost everywhere; the one aware writer (diary_ingest, UTC) is compared on its wall-clock fields after the offset is dropped. - A drawer whose
filed_atis missing or unparseable is EXCLUDED whenever a bound is active — a date-filtered result must never silently include rows of unknown age.
Functions
parse_date_bound
def parse_date_bound(value: Optional[str] = None, field_name: str = 'date') -> Optional[datetime]Parse an optional ISO-8601 date/datetime filter bound.
Accepts a date ("2026-04-01"), a naive timestamp ("2026-04-01T09:30:00"), or one carrying a Z/+HH:MM offset. Returns a naive datetime for wall-clock comparison against drawer filed_at values, which are stored as naive local ISO strings (datetime.now().isoformat()). Any timezone offset on the input is dropped so an aware bound never raises a TypeError against a naive filed_at. Comparison is therefore wall-clock, which is what the local-first single-machine model wants; an offset bound is matched on its wall-clock fields, not its absolute instant, so a bound whose offset differs from the zone filed_at was recorded in is matched by clock time. The accepted grammar is a date, an ISO timestamp (optionally fractional), and an optional Z/±HH:MM offset; other ISO 8601 forms (basic format, week dates) are outside the contract and are rejected on the Python 3.9 floor even where a newer fromisoformat would accept them. Blank / whitespace-only means "no filter" (None). Raises ValueError on an unparseable value so the caller can surface a clear error, mirroring the wing/room sanitizers.
parse_window
def parse_window(since: Optional[str] = None, before: Optional[str] = None)Parse a [since, before) pair, rejecting an inverted window.
Returns (since_dt, before_dt) — either side None when absent. Raises ValueError (naming the offending field or the inversion) so callers surface the same message everywhere a window is accepted.
filed_at_in_window
def filed_at_in_window(filed_at, since_dt: Optional[datetime], before_dt: Optional[datetime]) -> boolTrue if a drawer's filed_at falls in [since, before).
since is inclusive and before is exclusive, matching the issue spec. Parsing (Z/offset normalization, tz drop) is delegated to parse_date_bound so a bound and a filed_at are compared identically. A drawer whose filed_at is missing or unparseable cannot be confirmed in-window, so it is EXCLUDED whenever a bound is active — a date-filtered listing must never silently include rows of unknown age.
