CNN-BiGRU / cnn_bigru /scripts /push_to_hf.py
PowerMachine's picture
v3.0: fix push_to_hf.py to use path_in_repo=cnn_bigru
16200a3 verified
Raw History Blame Contribute Delete
11.1 kB
"""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())