Cco / scripts /deploy_hf.py
Gabriel Sapucaia
Deploy CCO Apoena (réplica do cco)
4ce2557 verified
Raw
History Blame Contribute Delete
5.71 kB
#!/usr/bin/env python3
"""Deploy do dashboard CCO Apoena como HF Space (Docker, público).
Cria/atualiza o Space, sobe o código + DB embutido + artefatos, e configura
os secrets de runtime (credenciais F2M e LLM) a partir do .env.local.
Público por default (paridade com o gêmeo cco-mina-almas; Safari não abre
Space privado por bloqueio de cookie cross-site). Use --private p/ restringir.
Uso: ./scripts/deploy_hf.py [--private]
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SPACE_NAME = "cco-apn"
# Arquivos/dirs a NÃO subir para o Space.
# .gitignore é EXCLUÍDO de propósito: se for pro Space, tanto huggingface_hub
# quanto git passam a honrá-lo e os *.duckdb embutidos somem do build.
IGNORE = [
".git/*", ".git",
".gitignore",
".env.local", ".session_secret",
"**/__pycache__/*", "**/__pycache__", "*.pyc",
".claude/*", ".claude",
"*.bak", "*.bak*",
"cco_state.duckdb", "f2m_2026.duckdb",
"docs/PORTABILIDADE_CCO.json", # mapa interno, desnecessário em runtime
".ruff_cache/*", ".pytest_cache/*", ".venv/*",
]
# Secrets de runtime (lidos do .env.local). F2M_REPORT_BASE já está no Dockerfile.
SECRET_KEYS = ["F2M_EMAIL", "F2M_PASSWORD", "OPENROUTER_KEY", "NVIDIA_API_KEY"]
# Variáveis públicas do Space (não-sensíveis). Poll a cada 120s = mesma cadência
# do gêmeo cco-mina-almas (que sobrescreve o default 300 do Dockerfile via variável).
SPACE_VARIABLES = {"F2M_POLL_INTERVAL": "120"}
def load_env_local(path: Path) -> None:
if not path.exists():
return
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
def _push_dbs_via_git(token: str, user: str, repo_id: str) -> None:
"""Sobe os *.duckdb embutidos via git-lfs nativo (robusto p/ arquivos >10MB)."""
import shutil
import subprocess
import tempfile
dbs = [fn for fn in ("f2m_local.duckdb", "viagens_baseline.duckdb") if (ROOT / fn).exists()]
if not dbs:
print(" nenhum *.duckdb local — pule (rode bootstrap_extract.py / build_viagens_baseline.py)")
return
url = f"https://{user}:{token}@huggingface.co/spaces/{repo_id}"
with tempfile.TemporaryDirectory() as tmp:
clone = Path(tmp) / "space"
subprocess.run(["git", "lfs", "install", "--skip-repo"], check=True, capture_output=True)
subprocess.run(["git", "clone", "--depth", "1", url, str(clone)], check=True, capture_output=True)
# .gitattributes: garante LFS p/ duckdb; remove .gitignore (não pode ir pro Space)
ga = clone / ".gitattributes"
txt = ga.read_text() if ga.exists() else ""
if "duckdb" not in txt:
ga.write_text(txt.rstrip() + "\n*.duckdb filter=lfs diff=lfs merge=lfs -text\n")
(clone / ".gitignore").unlink(missing_ok=True)
for fn in dbs:
shutil.copy2(ROOT / fn, clone / fn)
g = ["git", "-C", str(clone)]
subprocess.run([*g, "rm", "-q", "--cached", "--ignore-unmatch", ".gitignore"], capture_output=True)
subprocess.run([*g, "add", "-f", ".gitattributes", *dbs], check=True)
subprocess.run([*g, "-c", "user.email=deploy@local", "-c", "user.name=deploy",
"commit", "-q", "-m", "DBs embutidos (git-lfs)"], capture_output=True)
subprocess.run([*g, "push", "origin", "HEAD:main"], check=True, capture_output=True)
for fn in dbs:
print(f" {fn}: {(ROOT / fn).stat().st_size/1e6:.1f} MB (LFS)")
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--private", action="store_true", help="cria Space privado (default: público)")
args = p.parse_args()
load_env_local(ROOT / ".env.local")
token = os.environ.get("HF_TOKEN", "")
if not token:
print("ERRO: HF_TOKEN ausente no .env.local")
return 1
from huggingface_hub import HfApi
api = HfApi(token=token)
user = api.whoami()["name"]
repo_id = f"{user}/{SPACE_NAME}"
private = args.private
print(f"== Space: {repo_id} (private={private}, sdk=docker) ==")
api.create_repo(
repo_id=repo_id, repo_type="space", space_sdk="docker",
private=private, exist_ok=True,
)
print("== Configurando secrets de runtime ==")
for k in SECRET_KEYS:
v = os.environ.get(k, "")
if v:
api.add_space_secret(repo_id=repo_id, key=k, value=v)
print(f" secret {k}: set ({len(v)} chars)")
else:
print(f" secret {k}: AUSENTE no .env.local — pulado")
print("== Configurando variáveis públicas do Space ==")
for k, v in SPACE_VARIABLES.items():
api.add_space_variable(repo_id=repo_id, key=k, value=v)
print(f" variable {k}={v}")
print("== Upload código + artefatos (upload_folder honra .gitignore)... ==")
api.upload_folder(
folder_path=str(ROOT),
repo_id=repo_id, repo_type="space",
ignore_patterns=IGNORE,
commit_message="Deploy CCO Apoena (réplica do cco)",
)
# Os DuckDB embutidos (jan-jun) sobem via git + git-lfs nativo: o
# huggingface_hub 1.9.0 cria o commit mas não persiste o blob LFS (>10MB).
print("== Upload DBs embutidos (git-lfs)... ==")
_push_dbs_via_git(token, user, repo_id)
print(f"\nOK — https://huggingface.co/spaces/{repo_id}")
print(f"App: https://{user}-{SPACE_NAME}.hf.space")
return 0
if __name__ == "__main__":
raise SystemExit(main())