agnes-the-ai-analyst/server/bin/notify-scripts
Petr c56905d34f Initial commit: OSS data distribution platform
Open-source AI data analyst platform extracted from internal repo.
Includes data sync engine, Keboola adapter, Flask web portal,
server deployment scripts, and configuration templates.
2026-03-08 23:31:28 +01:00

169 lines
5 KiB
Python
Executable file

#!/usr/bin/env python3
"""List or run notification scripts for the calling user.
This helper is designed to be invoked via sudo -u <user>, so it runs
with the target user's permissions and can access their home directory.
Callers (www-data, deploy) never need to traverse the user's home.
Usage:
notify-scripts list - JSON array of scripts with last_run info
notify-scripts run <script.py> - execute script, print its JSON output
notify-scripts sync-status - JSON with ~/server/ mtime (last sync)
"""
import grp
import json
import os
import pwd
import subprocess
import sys
import time
from pathlib import Path
home = Path.home()
SCRIPTS_DIR = home / "user" / "notifications"
STATE_DIR = home / ".notifications" / "state"
VENV_PYTHON = home / ".venv" / "bin" / "python"
SCRIPT_TIMEOUT_SECONDS = 60
def validate_username(username):
"""Ensure username is a member of dataread group (authorized analyst)."""
try:
# Get dataread group members
dataread_group = grp.getgrnam('dataread')
dataread_members = dataread_group.gr_mem
# Also check users whose primary group is dataread
user_info = pwd.getpwnam(username)
if user_info.pw_gid == dataread_group.gr_gid:
return True
# Check if user is in dataread supplementary groups
return username in dataread_members
except KeyError:
return False
def cmd_list():
"""Print JSON array of notification scripts with last_run metadata."""
if not SCRIPTS_DIR.is_dir():
print("[]")
return
result = []
for script in sorted(SCRIPTS_DIR.glob("*.py")):
state_file = STATE_DIR / f"{script.stem}.json"
last_run = _read_last_run(state_file)
result.append({
"name": script.name,
"stem": script.stem,
"last_run": last_run,
})
print(json.dumps(result))
def cmd_run(script_name: str):
"""Execute a notification script and relay its stdout."""
# Validate that current user is an authorized analyst
current_user = pwd.getpwuid(os.getuid()).pw_name
if not validate_username(current_user):
print(json.dumps({"error": f"User '{current_user}' is not authorized"}))
sys.exit(1)
script_path = SCRIPTS_DIR / script_name
if not script_name.endswith(".py") or not script_path.is_file():
print(json.dumps({"error": "Script not found"}))
sys.exit(1)
if not VENV_PYTHON.is_file():
print(json.dumps({"error": "No .venv found"}))
sys.exit(1)
env = os.environ.copy()
env["MPLCONFIGDIR"] = f"/tmp/mpl_{pwd.getpwuid(os.getuid()).pw_name}"
try:
proc = subprocess.run(
[str(VENV_PYTHON), str(script_path)],
capture_output=True,
text=True,
timeout=SCRIPT_TIMEOUT_SECONDS,
cwd=str(home),
env=env,
)
except subprocess.TimeoutExpired:
print(json.dumps({"error": f"Script timed out after {SCRIPT_TIMEOUT_SECONDS}s"}))
sys.exit(1)
if proc.returncode != 0:
print(json.dumps({"error": proc.stderr[:500]}))
sys.exit(1)
sys.stdout.write(proc.stdout)
def cmd_sync_status():
"""Print JSON with last sync time based on ~/server/ directory mtime."""
server_dir = home / "server"
if not server_dir.is_dir():
print(json.dumps({"synced": False, "elapsed_seconds": None}))
return
try:
mtime = server_dir.stat().st_mtime
elapsed = time.time() - mtime
print(json.dumps({
"synced": True,
"elapsed_seconds": round(elapsed),
"elapsed_display": _format_elapsed(elapsed) + " ago",
}))
except OSError:
print(json.dumps({"synced": False, "elapsed_seconds": None}))
def _read_last_run(state_file: Path) -> str | None:
"""Read state file and return human-readable elapsed time, or None."""
if not state_file.exists():
return None
try:
state = json.loads(state_file.read_text())
ts = state.get("last_sent", 0)
if ts <= 0:
return None
elapsed = time.time() - ts
return _format_elapsed(elapsed) + " ago"
except Exception:
return None
def _format_elapsed(seconds: float) -> str:
"""Format elapsed seconds into a compact string."""
if seconds < 60:
return f"{int(seconds)}s"
if seconds < 3600:
return f"{int(seconds / 60)}m"
if seconds < 86400:
return f"{int(seconds / 3600)}h"
return f"{int(seconds / 86400)}d"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: notify-scripts <list|run|sync-status> [script_name]", file=sys.stderr)
sys.exit(1)
command = sys.argv[1]
if command == "list":
cmd_list()
elif command == "run" and len(sys.argv) >= 3:
cmd_run(sys.argv[2])
elif command == "sync-status":
cmd_sync_status()
else:
print(f"Unknown command: {command}", file=sys.stderr)
sys.exit(1)