""" Environment selection for the migration scripts. Every transform is environment-agnostic: it reads the staged Parquet (same for all environments) and writes to whichever database `--env` selects. The target is defined entirely by `deploy/.env.` (the same file Portainer is fed at deploy time), whose `DATABASE_URL` is the single source of truth for host / port / credentials / database. Reproduce the whole migration in a new environment (e.g. prod) by: 1. deploy the DB stack for that env (deploy/jorgecuadros-db.stack.yml) 2. write deploy/.env. with its DATABASE_URL 3. push the schema: DATABASE_URL=... npx prisma@5 db push --schema=packages/database/prisma/schema.prisma 4. run: ./.venv/bin/python run_all.py --env Usage in a script: from dbenv import connect, env_arg env = env_arg() # --env dev|prod (default dev) conn = connect(env) """ from __future__ import annotations import argparse import os from pathlib import Path from urllib.parse import unquote, urlparse import pymysql REPO = Path(__file__).resolve().parents[1] def load_env(env: str) -> dict: """deploy/.env. parsed to a dict, or {} when the file is absent. Absent is normal, not an error: the API container runs these scripts with DATABASE_URL / S3_* injected as real environment variables and ships no deploy/ directory. Use `setting()` / `require()` rather than this — they layer the process environment on top, which is what actually resolves.""" f = REPO / "deploy" / f".env.{env}" if not f.exists(): return {} out = {} for line in f.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) out[k] = v return out def setting(env: str, *keys: str): """First non-empty value for `keys`, process environment first, then deploy/.env.. Several keys = fallback aliases (S3_ACCESS_KEY then MINIO_ROOT_USER, as apps/api/src/storage/storage.service.ts does).""" fromfile = load_env(env) for k in keys: v = os.environ.get(k) or fromfile.get(k) if v: return v return None def require(env: str, *keys: str) -> str: v = setting(env, *keys) if not v: raise SystemExit( f"missing {' / '.join(keys)} — set it in the environment, or deploy the " f"'{env}' stack and write {REPO / 'deploy' / f'.env.{env}'} " f"(see dbenv.py header)." ) return v def database_url(env: str) -> str: """Target DB URL. A DATABASE_URL in the process environment wins over deploy/.env. — this is how the API container (which has its own DATABASE_URL and no deploy/.env files) drives a re-import against its own database.""" return require(env, "DATABASE_URL") def connect(env: str): url = database_url(env) u = urlparse(url) # mysql://user:pass@host:port/db return pymysql.connect( host=u.hostname, port=u.port or 3306, user=unquote(u.username or ""), password=unquote(u.password or ""), database=(u.path or "/").lstrip("/"), autocommit=False, charset="utf8mb4", ) def env_arg(extra_args=None) -> str: """Parse --env (default 'dev') and return it. Scripts that need more args can pass an argparse parser via extra_args(parser).""" p = argparse.ArgumentParser() p.add_argument("--env", default="dev", help="target environment (dev|prod|...): reads deploy/.env.") if extra_args: extra_args(p) return p.parse_args().env