feat(datastore): add relative --since duration flag and overnight sleep test
This commit is contained in:
@@ -44,6 +44,41 @@ def generate_ulid() -> str:
|
||||
BERLIN_TZ = ZoneInfo("Europe/Berlin")
|
||||
|
||||
|
||||
def parse_duration(val: str) -> datetime.timedelta:
|
||||
"""Parses duration strings like '15m', '15 min', '1h', '1h30m', '1.5h' into timedelta."""
|
||||
s = val.strip().lower()
|
||||
if not s:
|
||||
raise ValueError("Empty duration string")
|
||||
|
||||
# Decimal hours like "1.5h" or "1.5 stunden"
|
||||
m_dec_h = re.fullmatch(r"^(\d+(?:\.\d+)?)\s*(?:h|stunden?)$", s)
|
||||
if m_dec_h:
|
||||
return datetime.timedelta(hours=float(m_dec_h.group(1)))
|
||||
|
||||
# Decimal minutes like "2.5m" or "15 min"
|
||||
m_dec_m = re.fullmatch(r"^(\d+(?:\.\d+)?)\s*(?:m|min|minuten?)$", s)
|
||||
if m_dec_m:
|
||||
return datetime.timedelta(minutes=float(m_dec_m.group(1)))
|
||||
|
||||
# Composite like "1h 30m" or "1h30min" or "45s"
|
||||
m_comp = re.fullmatch(
|
||||
r"^(?:(\d+)\s*(?:h|stunden?))?\s*(?:(\d+)\s*(?:m|min|minuten?))?\s*(?:(\d+)\s*(?:s|sek|sekunden?))?$",
|
||||
s,
|
||||
)
|
||||
if m_comp and any(m_comp.groups()):
|
||||
h = int(m_comp.group(1) or 0)
|
||||
m = int(m_comp.group(2) or 0)
|
||||
sec = int(m_comp.group(3) or 0)
|
||||
return datetime.timedelta(hours=h, minutes=m, seconds=sec)
|
||||
|
||||
# Pure number without unit: default to minutes
|
||||
if s.isdigit():
|
||||
return datetime.timedelta(minutes=int(s))
|
||||
|
||||
raise ValueError(f"Unrecognized duration format: '{val}'. Expected formats: '15m', '15 min', '1h', '1h30m'.")
|
||||
|
||||
|
||||
|
||||
def find_data_dir() -> Path:
|
||||
"""Discovers the knowledge-wiki data directory."""
|
||||
if os.environ.get("DATASTORE_DIR"):
|
||||
@@ -279,6 +314,7 @@ class DatastoreEngine:
|
||||
payload: Dict[str, Any],
|
||||
start: Optional[str] = None,
|
||||
end: Optional[str] = None,
|
||||
since: Optional[str] = None,
|
||||
estimated: bool = False,
|
||||
meal: Optional[str] = None,
|
||||
note: Optional[str] = None,
|
||||
@@ -289,8 +325,15 @@ class DatastoreEngine:
|
||||
now_dt = datetime.datetime.now(datetime.timezone.utc)
|
||||
now_iso = now_dt.isoformat()
|
||||
|
||||
if since and start:
|
||||
raise ValueError("Cannot specify both 'since' and 'start'. Use 'since' for relative times or 'start' for absolute times.")
|
||||
|
||||
# Parse or default start time
|
||||
if not start:
|
||||
if since:
|
||||
delta = parse_duration(since)
|
||||
start_dt = (now_dt - delta).astimezone(BERLIN_TZ)
|
||||
start_iso = start_dt.isoformat()
|
||||
elif not start:
|
||||
start_dt = now_dt.astimezone(BERLIN_TZ)
|
||||
start_iso = start_dt.isoformat()
|
||||
else:
|
||||
@@ -713,6 +756,7 @@ def run_mcp_server():
|
||||
"payload": {"type": "object", "description": "Detailed payload matching the kind schema"},
|
||||
"start": {"type": "string", "description": "ISO 8601 timestamp (optional, defaults to now)"},
|
||||
"end": {"type": "string", "description": "ISO 8601 timestamp (optional for intervals)"},
|
||||
"since": {"type": "string", "description": "Relative start offset backwards from now (e.g. '5m', '15m', '1h'). Use when logging events that started X minutes ago without computing absolute timestamps."},
|
||||
"estimated": {"type": "boolean", "description": "True if portions or macros are estimated"},
|
||||
"meal": {"type": "string", "description": "Optional meal grouping (e.g. 'Frühstück', 'Mittagessen')"},
|
||||
"note": {"type": "string", "description": "Free text notes or context"},
|
||||
@@ -832,6 +876,7 @@ def run_mcp_server():
|
||||
payload=args["payload"],
|
||||
start=args.get("start"),
|
||||
end=args.get("end"),
|
||||
since=args.get("since"),
|
||||
estimated=args.get("estimated", False),
|
||||
meal=args.get("meal"),
|
||||
note=args.get("note"),
|
||||
@@ -906,6 +951,7 @@ def run_cli():
|
||||
p_add.add_argument("--payload", required=True, help="JSON payload string")
|
||||
p_add.add_argument("--start", help="ISO start time")
|
||||
p_add.add_argument("--end", help="ISO end time")
|
||||
p_add.add_argument("--since", help="Relative start offset backwards from now (e.g. '5m', '15m', '1h')")
|
||||
p_add.add_argument("--meal", help="Meal grouping")
|
||||
p_add.add_argument("--note", help="Free-form note")
|
||||
p_add.add_argument("--estimated", action="store_true", help="Flag as estimated")
|
||||
@@ -944,6 +990,7 @@ def run_cli():
|
||||
payload=payload,
|
||||
start=args.start,
|
||||
end=args.end,
|
||||
since=args.since,
|
||||
meal=args.meal,
|
||||
note=args.note,
|
||||
estimated=args.estimated,
|
||||
|
||||
Reference in New Issue
Block a user