File size: 4,819 Bytes
5e21013 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | """One-shot operator tool: UPDATE a row in training_config.
Reads POSTGRES_URL_NON_POOLING (or POSTGRES_URL) from .env, runs an
upsert against the public.training_config (key, value) table, prints
the new value back. No HTTP layer, no admin surface β direct SQL by
the operator with intent.
Why exists:
- The workspace has GET surfaces for training_config (via
apps/workspace/src/lib/training.ts:getConfig) but no PUT/PATCH.
- We don't want a public admin endpoint for it (low-priority surface
area, write-only by ops).
- One-off configuration changes (e.g. flip enabled_tiers when a tier
becomes trainable, set monthly_budget_usd, update github_topics)
need a fast, auditable path.
Usage:
python scripts/ops/set_training_config.py \\
--key enabled_tiers \\
--value '{"tiers":["cell"]}'
--dry-run prints the SQL + parameters without executing.
--get <key> reads and prints the current value (read-only, no mutation).
The value MUST be valid JSON. The DB column is `value jsonb`.
Auth: postgres connection string from .env (POSTGRES_URL_NON_POOLING
preferred β direct connection avoids the pgbouncer transaction-mode
limitation; falls back to POSTGRES_URL).
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
def load_env() -> dict[str, str]:
"""Read .env into a dict. Doesn't touch os.environ."""
env_path = REPO_ROOT / ".env"
if not env_path.exists():
sys.exit(f"missing .env at {env_path}")
out: dict[str, str] = {}
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
out[key.strip()] = val.strip().strip('"').strip("'")
return out
def resolve_dsn(env: dict[str, str]) -> str:
"""Direct (non-pooling) preferred; pooling fallback. Both must work
with psycopg's parser. Supabase's pooling URL uses port 6543 with
pgbouncer transaction mode, which is fine for one-off UPDATEs."""
dsn = env.get("POSTGRES_URL_NON_POOLING") or env.get("POSTGRES_URL")
if not dsn:
sys.exit("POSTGRES_URL_NON_POOLING or POSTGRES_URL must be set in .env")
return dsn
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--key", help="training_config.key value to set")
p.add_argument("--value", help="JSON string to write into training_config.value")
p.add_argument("--get", metavar="KEY", help="read and print current value (no mutation)")
p.add_argument("--dry-run", action="store_true",
help="print SQL + params without executing")
args = p.parse_args()
if not args.key and not args.get:
sys.exit("either --key + --value (write) or --get KEY (read) required")
if args.key and not args.value:
sys.exit("--key requires --value")
env = load_env()
dsn = resolve_dsn(env)
# Lazy import β psycopg only needed when actually running.
try:
import psycopg
except ImportError:
sys.exit(
"psycopg not installed. Run:\n"
" /Users/christopherfrost/Desktop/Bee/.venv/bin/pip install 'psycopg[binary]'"
)
if args.get:
sql = "SELECT value FROM public.training_config WHERE key = %s"
if args.dry_run:
print(f"[dry-run] SQL: {sql}\n params: ({args.get!r},)")
return
with psycopg.connect(dsn) as conn:
with conn.cursor() as cur:
cur.execute(sql, (args.get,))
row = cur.fetchone()
if row is None:
print(f"key not found: {args.get}")
sys.exit(2)
# row[0] is jsonb returned as Python dict/list/str by psycopg
print(json.dumps(row[0], indent=2))
return
# Write path
try:
parsed = json.loads(args.value)
except json.JSONDecodeError as e:
sys.exit(f"--value must be valid JSON: {e}")
sql = """
INSERT INTO public.training_config (key, value)
VALUES (%s, %s::jsonb)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
RETURNING value
"""
params = (args.key, json.dumps(parsed))
if args.dry_run:
print(f"[dry-run] SQL:{sql}\n params: ({args.key!r}, {json.dumps(parsed)})")
return
print(f"upsert: key={args.key!r} value={json.dumps(parsed)}")
with psycopg.connect(dsn) as conn:
with conn.cursor() as cur:
cur.execute(sql, params)
new_val = cur.fetchone()[0]
conn.commit()
print(f" new value: {json.dumps(new_val, indent=2)}")
if __name__ == "__main__":
main()
|