Working-tree checkpoint of in-progress work carried across prior sessions on the feat/crud-rbac branch, committed so it lands on the remote alongside the CI changes. - Operaciones admin panel: apps/api/src/ops (ingest upload, backup / restore / re-import jobs) wired into app.module + RBAC abilities, and the apps/web/src/app/operaciones page. docker-compose gets INGEST_DIR / BACKUP_DIR volumes; .gitignore excludes migration/ingest + backups. - migration/sync.py plus transform_*.py / run_all / config / dbenv / blob_extract adjustments for the additive sync path. - crud/rbac phase-5 web bits: AppShell, api/labels/types libs, globals. - schema.prisma + PLAN/RESUME doc updates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
"""
|
|
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.<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.<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 <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:
|
|
f = REPO / "deploy" / f".env.{env}"
|
|
if not f.exists():
|
|
raise SystemExit(
|
|
f"missing {f} — deploy the '{env}' DB stack and write its .env first "
|
|
f"(see dbenv.py header)."
|
|
)
|
|
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
|
|
if "DATABASE_URL" not in out:
|
|
raise SystemExit(f"{f} has no DATABASE_URL")
|
|
return out
|
|
|
|
|
|
def database_url(env: str) -> str:
|
|
"""Target DB URL. A DATABASE_URL in the process environment wins over
|
|
deploy/.env.<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 os.environ.get("DATABASE_URL") or load_env(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.<env>")
|
|
if extra_args:
|
|
extra_args(p)
|
|
return p.parse_args().env
|