"""push_to_hf.py — Envia scripts em lote ao HF repositório 'CNN-BiGRU' (v3.0). Executa: 1. Cria repositório 'PowerMachine/CNN-BiGRU' (se não existir) 2. Envia todos os scripts .py, README.md, requirements.txt, docs/ usando `upload_folder` (upload em lote eficiente) 3. SOBRESCREVE módulos desatualizados (default do upload_folder) 4. Remove HF_TOKEN do ambiente após o uso (requisito do usuário) 5. Deleta estado salvo do modelo (não envia para lugar algum) 6. Filtra arquivos sensíveis (.env, tokens, checkpoints, etc.) V3.0 melhorias: - Filtra arquivos de estado do modelo (.pt, .pth, .bin, .safetensors) - Filtra relatórios de monitor local (monitor_reports/) - Mantém apenas scripts e documentação - Log mais detalhado - Retry automático em caso de falha transitória Usage: HF_TOKEN=hf_xxx python push_to_hf.py """ from __future__ import annotations import os import sys import logging import shutil import time from pathlib import Path from typing import List, Set logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s") logger = logging.getLogger("push_hf") REPO_ID = "PowerMachine/CNN-BiGRU" # Path: scripts/push_to_hf.py -> parent=scripts -> parent=cnn_bigru PROJECT_DIR = Path(__file__).resolve().parent.parent # PROJECT_DIR.parent = raiz do projeto REPO_ROOT = PROJECT_DIR.parent # Extensões e nomes de arquivo permitidos para upload ALLOWED_EXTENSIONS: Set[str] = { ".py", ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".cfg", ".ini", ".sh", } # Padrões a ignorar (em qualquer parte do caminho) IGNORE_PATTERNS: Set[str] = { "__pycache__", ".pyc", ".pyo", ".pyd", ".git", ".gitignore", ".gitattributes", "node_modules", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".DS_Store", "Thumbs.db", "*.log", "*.tmp", "*.swp", "*.bak", ".env", ".venv", "venv", "env", # Estado salvo do modelo — NÃO ENVIAR (requisito do usuário) "*.pt", "*.pth", "*.bin", "*.safetensors", "*.ckpt", # Relatórios de monitor local (contém paths e dados locais) "monitor_reports", "download", "tool-results", "upload", } # Arquivos específicos que NUNCA devem ser upados (podem conter tokens/sensíveis) SENSITIVE_FILENAMES: Set[str] = { ".env", ".env.local", ".env.production", ".env.development", "secrets.json", "credentials.json", "config.local.json", "hf_token.txt", "token.txt", } def should_ignore(path: Path) -> bool: """Verifica se um arquivo deve ser ignorado no upload.""" parts = path.parts name = path.name.lower() # Verificar padrões por partes do caminho for part in parts: part_lower = part.lower() if part_lower in IGNORE_PATTERNS: return True # Verificar extensões de cache if part_lower.endswith((".pyc", ".pyo", ".pyd")): return True # Verificar extensões de estado de modelo if part_lower.endswith((".pt", ".pth", ".bin", ".safetensors", ".ckpt")): return True # Verificar nome do arquivo if name in SENSITIVE_FILENAMES: return True # Verificar extensões permitidas if path.suffix.lower() not in ALLOWED_EXTENSIONS: # Permitir arquivos sem extensão apenas se forem específicos (README, LICENSE, etc.) if name not in {"readme", "license", "license-mit", "authors", "contributors"}: return True # Verificar padrões com wildcards for pattern in IGNORE_PATTERNS: if pattern.startswith("*"): suffix = pattern[1:] if name.endswith(suffix): return True return False def collect_files(project_dir: Path) -> List[Path]: """Coleta todos os arquivos elegíveis para upload.""" files = [] for path in project_dir.rglob("*"): if path.is_file() and not should_ignore(path): files.append(path) return files def delete_saved_model_state(project_dir: Path) -> None: """Deleta todo o estado salvo do modelo (checkpoints, .pt, .pth, etc.). Requisito do usuário: "apagar o estado salvo do modelo (não enviar para lugar algum até ordem em contrário)". """ deleted = [] for path in project_dir.rglob("*"): if not path.is_file(): continue name = path.name.lower() if name.endswith((".pt", ".pth", ".bin", ".safetensors", ".ckpt")): try: path.unlink() deleted.append(path) logger.info(f" Deletado: {path.relative_to(project_dir.parent)}") except Exception as e: logger.warning(f" Falha ao deletar {path}: {e}") # Deletar diretórios de cache e checkpoints for d in project_dir.rglob("*"): if d.is_dir() and d.name.lower() in {"checkpoints", "checkpoint", "model_state", "weights"}: try: shutil.rmtree(d) deleted.append(d) logger.info(f" Deletado dir: {d.relative_to(project_dir.parent)}") except Exception as e: logger.warning(f" Falha ao deletar dir {d}: {e}") if deleted: logger.info(f"Estado salvo do modelo deletado: {len(deleted)} itens") else: logger.info("Nenhum estado salvo do modelo encontrado para deletar") def upload_with_retry(api, folder: Path, repo_id: str, token: str, max_retries: int = 3): """Faz upload_folder com retry em caso de falha transitória. IMPORTANTE: usa path_in_repo='cnn_bigru' para preservar a árvore de pastas (todos os arquivos ficam sob cnn_bigru/ no repo HF, não na raiz). """ last_error = None for attempt in range(1, max_retries + 1): try: logger.info(f"Tentativa {attempt}/{max_retries}: upload_folder (path_in_repo='cnn_bigru')...") commit_info = api.upload_folder( folder_path=str(folder), repo_id=repo_id, repo_type="model", token=token, commit_message=f"v3.0: 9 novos módulos (CyclicReasoning, Medusa, NLG, NLP, MMA, VQVAE2, W8A8, LongContext 1M, Monitor) + bug fixes (attempt {attempt})", # Sobrescreve arquivos desatualizados (default) path_in_repo="cnn_bigru", # PRESERVA árvore de pastas ) logger.info(f"Upload bem-sucedido: {commit_info}") return True, None except Exception as e: last_error = e logger.warning(f"Tentativa {attempt} falhou: {e}") if attempt < max_retries: wait_time = 2 ** attempt logger.info(f"Aguardando {wait_time}s antes de tentar novamente...") time.sleep(wait_time) return False, last_error def main(): # Pega HF_TOKEN da variável de ambiente hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") if not hf_token: logger.error("HF_TOKEN não definido no ambiente. Abortando.") return 1 try: from huggingface_hub import HfApi, create_repo except ImportError: logger.error("huggingface_hub não instalado. Execute: pip install huggingface_hub") return 1 api = HfApi(token=hf_token) # 1. Cria repositório (se não existir) try: repo_url = create_repo( repo_id=REPO_ID, token=hf_token, repo_type="model", exist_ok=True, private=False, ) logger.info(f"Repositório criado/acessado: {repo_url}") except Exception as e: logger.error(f"Falha ao criar repositório: {e}") # Limpa o token mesmo em caso de falha _cleanup_token() return 1 # 2. Coleta todos os arquivos para upload files_to_upload = collect_files(PROJECT_DIR) logger.info(f"Total de arquivos para upload: {len(files_to_upload)} (de {PROJECT_DIR})") if not files_to_upload: logger.warning("Nenhum arquivo elegível para upload.") _cleanup_token() return 1 # Log dos arquivos que serão upados (até 20) for f in files_to_upload[:20]: rel = f.relative_to(REPO_ROOT) logger.info(f" - {rel}") if len(files_to_upload) > 20: logger.info(f" ... e mais {len(files_to_upload) - 20} arquivos") # 3. Upload em lote via upload_folder (muito mais eficiente que upload_file em loop) success, error = upload_with_retry( api, folder=PROJECT_DIR, repo_id=REPO_ID, token=hf_token, max_retries=3 ) if success: logger.info("=" * 60) logger.info(f"Upload concluído com sucesso!") logger.info(f"Repo: https://huggingface.co/{REPO_ID}") logger.info(f"Total de arquivos enviados: {len(files_to_upload)}") logger.info("=" * 60) # 4. Deletar estado salvo do modelo (após upload bem-sucedido) logger.info("Deletando estado salvo do modelo...") delete_saved_model_state(PROJECT_DIR) else: logger.error("=" * 60) logger.error(f"Falha no upload após retries: {error}") logger.error("=" * 60) # 5. Remove HF_TOKEN do ambiente (requisito do usuário) _cleanup_token() # 6. Verifica e remove tokens hardcoded em scripts (requisito do usuário) _scan_scripts_for_tokens(PROJECT_DIR) return 0 if success else 1 def _cleanup_token(): """Remove todas as variáveis de token HF do ambiente.""" removed = [] for var in ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HF_HUB_TOKEN"): if var in os.environ: del os.environ[var] removed.append(var) if removed: logger.info(f"Tokens removidos do ambiente: {', '.join(removed)}") else: logger.info("Nenhum token HF encontrado no ambiente para remover.") def _scan_scripts_for_tokens(project_dir: Path) -> None: """Verifica se há tokens HF hardcoded em scripts e os remove.""" # Padrões de token HF (hf_ seguido de 32+ caracteres alfanuméricos) import re token_pattern = re.compile(r'hf_[A-Za-z0-9]{20,}') scanned = 0 cleaned = 0 for path in project_dir.rglob("*.py"): if "push_to_hf.py" in str(path): continue # skip self try: content = path.read_text(encoding="utf-8") scanned += 1 if token_pattern.search(content): # Substituir por placeholder new_content = token_pattern.sub("HF_TOKEN_FROM_ENV", content) path.write_text(new_content, encoding="utf-8") cleaned += 1 logger.warning(f" Token HF removido de: {path.relative_to(project_dir.parent)}") except Exception as e: logger.debug(f" Falha ao escanear {path}: {e}") if cleaned > 0: logger.info(f"Scripts escaneados: {scanned}, scripts com token removido: {cleaned}") else: logger.info(f"Scripts escaneados: {scanned}, nenhum token hardcoded encontrado.") if __name__ == "__main__": sys.exit(main())