"""ParentCalendar. Usage: python main.py start the server (settings: see .env.example) python main.py backup copy the database (SQLite online backup) and the photos """ import logging import os import shutil import sqlite3 import sys from datetime import datetime from pathlib import Path from parentcalendar import config, db def backup(destination): dest = Path(destination) dest.mkdir(parents=True, exist_ok=True) target = dest / f"parentcalendar-{datetime.now():%Y%m%d-%H%M%S}.db" src, out = db.connect(), sqlite3.connect(target) try: src.backup(out) # consistent copy even while the server is writing finally: out.close() src.close() if config.IMAGES_DIR.exists(): shutil.copytree(config.IMAGES_DIR, dest / "images", dirs_exist_ok=True) print(f"Backed up to {target}") def hide_feed_tokens(record): """Feed URLs are bearer secrets, so the access log shows /feed/*** instead of the token.""" if isinstance(record.args, tuple) and len(record.args) >= 3 and str(record.args[2]).startswith("/feed/"): record.args = (*record.args[:2], "/feed/***", *record.args[3:]) return True def serve(): if not config.PUBLIC_URL: sys.exit("Set PUBLIC_URL (for example https://calendar.example.com) in the environment or in .env.") if not (config.STATIC_DIR / "index.html").exists(): sys.exit("The front end isn't built yet. Run: cd frontend && npm ci && npm run build") import uvicorn from parentcalendar import extraction env = os.environ.get print(f"ParentCalendar {config.APP_VERSION} ({config.APP_COMMIT}, {config.APP_COMMIT_DATE})") settings = { # what the app actually uses, defaults included "PUBLIC_URL": config.PUBLIC_URL, "DATA_DIR": config.DATA_DIR, "HOST": config.HOST, "PORT": config.PORT, "OLLAMA_URL": extraction.OLLAMA_URL, "EXTRACT_MODEL": extraction.MODEL, "EXTRACT_THINK": int(extraction.THINK), "EXTRACT_THINK_LEVEL": extraction.THINK_LEVEL or "", "EXTRACT_TEMPERATURE": extraction.TEMPERATURE, "EXTRACT_TOP_P": "" if extraction.TOP_P is None else extraction.TOP_P, "EXTRACT_TOP_K": "" if extraction.TOP_K is None else extraction.TOP_K, "EXTRACT_NUM_CTX": extraction.NUM_CTX, "EXTRACT_KEEP_ALIVE": extraction.KEEP_ALIVE, "EXTRACT_DOUBLE_READ": int(extraction.DOUBLE_READ), "EXTRACT_IGNORE": ",".join(sorted(extraction.IGNORE)), "IMAGE_RETENTION_DAYS": config.IMAGE_RETENTION_DAYS, "SHOW_SCAN_STATS": int(config.SHOW_SCAN_STATS), "ANTHROPIC_API_KEY": "set" if env("ANTHROPIC_API_KEY") else "unset", # never print the key "CHECKIDAY_API_KEY": "set" if config.CHECKIDAY_API_KEY else "unset", } print("Settings: " + " ".join(f"{k}={v}" for k, v in settings.items())) # The app's own log lines (scan timings, failures), formatted like uvicorn's. Other libraries stay at warnings. logging.basicConfig(format="%(levelname)s: %(message)s") logging.getLogger("parentcalendar").setLevel(logging.INFO) logging.getLogger("uvicorn.access").addFilter(hide_feed_tokens) uvicorn.run("parentcalendar.app:app", host=config.HOST, port=config.PORT, proxy_headers=True) if __name__ == "__main__": args = sys.argv[1:] if args[:1] == ["backup"] and len(args) == 2: backup(args[1]) elif not args: serve() else: sys.exit(__doc__)