148 lines
5.0 KiB
Python
Executable File
148 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""odysseus-contacts — Unix-style CLI for the contacts feature.
|
|
|
|
Talks to the same CardDAV server the web UI does, using credentials
|
|
from `data/settings.json` (set via the Settings → Connections panel).
|
|
Output is JSON on stdout, errors on stderr, non-zero exit on failure.
|
|
|
|
odysseus-contacts list | jq -r '.[] | .name + "\\t" + .email'
|
|
odysseus-contacts search alice
|
|
odysseus-contacts add --name "Alice Doe" --email alice@example.com
|
|
|
|
Subcommands:
|
|
list List all contacts
|
|
search Filter contacts by case-insensitive substring match
|
|
add Add a new contact (name + email)
|
|
config Show the current CardDAV config (URL/user; password masked)
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
import sys
|
|
import os, sys
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
|
|
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
|
|
quiet_logs()
|
|
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
def quiet_logs() -> None:
|
|
level_name = os.environ.get("LOG_LEVEL", "WARNING").upper()
|
|
level = getattr(logging, level_name, logging.WARNING)
|
|
root = logging.getLogger()
|
|
root.setLevel(level)
|
|
for handler in root.handlers:
|
|
handler.setLevel(level)
|
|
|
|
|
|
quiet_logs()
|
|
|
|
try:
|
|
from routes.contacts_routes import (
|
|
_get_carddav_config,
|
|
_fetch_contacts,
|
|
_create_contact,
|
|
)
|
|
quiet_logs()
|
|
except ModuleNotFoundError as e:
|
|
sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n")
|
|
sys.exit(2)
|
|
|
|
|
|
def fail(msg: str, code: int = 1) -> None:
|
|
sys.stderr.write(f"error: {msg}\n")
|
|
sys.exit(code)
|
|
|
|
|
|
def _contact_rows(contacts):
|
|
return [c for c in contacts or [] if isinstance(c, dict)]
|
|
|
|
|
|
# ─── list ────────────────────────────────────────────────────────────
|
|
|
|
def cmd_list(args) -> None:
|
|
cfg = _get_carddav_config()
|
|
if not cfg["url"]:
|
|
fail("CardDAV not configured. Set carddav_url/username/password in the web UI.")
|
|
contacts = _contact_rows(_fetch_contacts(force=args.refresh))
|
|
emit(contacts, args)
|
|
|
|
|
|
# ─── search ──────────────────────────────────────────────────────────
|
|
|
|
def cmd_search(args) -> None:
|
|
cfg = _get_carddav_config()
|
|
if not cfg["url"]:
|
|
fail("CardDAV not configured.")
|
|
q = args.query.lower()
|
|
contacts = _contact_rows(_fetch_contacts())
|
|
matches = [
|
|
c for c in contacts
|
|
if q in (c.get("name") or "").lower() or q in (c.get("email") or "").lower()
|
|
]
|
|
emit(matches, args)
|
|
|
|
|
|
# ─── add ─────────────────────────────────────────────────────────────
|
|
|
|
def cmd_add(args) -> None:
|
|
cfg = _get_carddav_config()
|
|
if not cfg["url"]:
|
|
fail("CardDAV not configured.")
|
|
ok = _create_contact(args.name, args.email)
|
|
if not ok:
|
|
fail("CardDAV PUT failed (see LOG_LEVEL=DEBUG for detail)")
|
|
emit({"ok": True, "name": args.name, "email": args.email}, args)
|
|
|
|
|
|
# ─── config ──────────────────────────────────────────────────────────
|
|
|
|
def cmd_config(args) -> None:
|
|
cfg = _get_carddav_config()
|
|
safe = dict(cfg)
|
|
if safe.get("password"):
|
|
safe["password"] = "***"
|
|
emit(safe, args)
|
|
|
|
|
|
# ─── argparse ────────────────────────────────────────────────────────
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
common = argparse.ArgumentParser(add_help=False)
|
|
common.add_argument("--pretty", action="store_true", help="Pretty-print JSON")
|
|
|
|
p = argparse.ArgumentParser(
|
|
prog="odysseus-contacts",
|
|
description="Shell-friendly wrapper around the Odysseus contacts (CardDAV) feature.",
|
|
parents=[common],
|
|
)
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
|
|
pl = sub.add_parser("list", help="list all contacts", parents=[common])
|
|
pl.add_argument("--refresh", action="store_true", help="bypass cache and re-fetch")
|
|
pl.set_defaults(func=cmd_list)
|
|
|
|
ps = sub.add_parser("search", help="filter by name/email substring", parents=[common])
|
|
ps.add_argument("query")
|
|
ps.set_defaults(func=cmd_search)
|
|
|
|
pa = sub.add_parser("add", help="add a new contact", parents=[common])
|
|
pa.add_argument("--name", required=True)
|
|
pa.add_argument("--email", required=True)
|
|
pa.set_defaults(func=cmd_add)
|
|
|
|
pc = sub.add_parser("config", help="show CardDAV config (password masked)", parents=[common])
|
|
pc.set_defaults(func=cmd_config)
|
|
|
|
return p
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(run(_build_parser()))
|