| |
| """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" |
|
|
| |
| |
| |
| 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", |
| ".ruff_cache/*", ".pytest_cache/*", ".venv/*", |
| ] |
|
|
| |
| SECRET_KEYS = ["F2M_EMAIL", "F2M_PASSWORD", "OPENROUTER_KEY", "NVIDIA_API_KEY"] |
|
|
| |
| |
| 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) |
| |
| 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)", |
| ) |
|
|
| |
| |
| 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()) |
|
|