Files
dtf-system/agente/agente.py
Cauê Faleiros 98c951d374
Some checks failed
Validate, publish and deploy / validate (push) Successful in 2m2s
Validate, publish and deploy / publish-and-deploy (push) Failing after 8s
first commit
2026-09-15 16:42:34 -03:00

142 lines
4.5 KiB
Python

"""
Agente da fábrica — traz a arte aprovada do portal para o servidor local.
Roda como serviço:
Windows: nssm install DtfAgente "C:\\Python312\\python.exe" "C:\\dtf\\agente.py"
Linux: systemd (ver dtf-agente.service no repositório)
Duas fontes de trabalho:
- webhook do portal (chega na hora)
- polling a cada 60 s (rede de segurança se o webhook falhar)
Se a internet cair, o agente para e o portal continua recebendo.
Quando voltar, ele baixa o acumulado. Nada se perde.
"""
from __future__ import annotations
import hashlib
import logging
import os
import sqlite3
import time
from datetime import datetime
from pathlib import Path
import httpx
PORTAL = os.environ["BASE_PORTAL"]
TOKEN = os.environ["AGENTE_TOKEN"]
RAIZ = Path(os.environ.get("PASTA_DTF", r"\\servidor\DTF"))
BANCO = Path(os.environ.get("KANBAN_DB", r"C:\dtf\kanban.db"))
INTERVALO = 60
HEARTBEAT = 300
PASTA_ENTRADA = RAIZ / "00_ARTE_RECEBIDA"
logging.basicConfig(
filename=RAIZ / "_log" / "agente.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger("agente")
def sha256(caminho: Path) -> str:
h = hashlib.sha256()
with open(caminho, "rb") as f:
for bloco in iter(lambda: f.read(1 << 20), b""):
h.update(bloco)
return h.hexdigest()
def registrar_no_kanban(arte: dict, arquivos: list[Path]) -> None:
"""Insere o card. Idempotente: a chave é o arte_id, não o nome do arquivo."""
con = sqlite3.connect(BANCO)
try:
con.execute("""
CREATE TABLE IF NOT EXISTS card(
arte_id INTEGER PRIMARY KEY,
pedido TEXT, cliente TEXT, metros REAL,
coluna TEXT DEFAULT 'rec', desde TEXT,
maquina TEXT, partes INTEGER
)""")
con.execute("""
INSERT OR IGNORE INTO card(arte_id,pedido,cliente,metros,desde,partes)
VALUES (?,?,?,?,?,?)""",
(arte["arte_id"], arte["pedido"], arte["cliente"],
arte["metros"], datetime.now().isoformat(timespec="seconds"),
len(arquivos)))
con.commit()
finally:
con.close()
def baixar(cliente: httpx.Client, arte: dict) -> bool:
PASTA_ENTRADA.mkdir(parents=True, exist_ok=True)
salvos: list[Path] = []
for parte in arte["partes"]:
destino = PASTA_ENTRADA / f"{arte['pedido']}_{parte['nome']}"
if destino.exists():
salvos.append(destino)
continue
tmp = destino.with_suffix(".parcial")
with cliente.stream("GET", parte["url"]) as r:
r.raise_for_status()
with open(tmp, "wb") as f:
for bloco in r.iter_bytes(1 << 20):
f.write(bloco)
tmp.rename(destino) # só aparece na pasta quando está completo
salvos.append(destino)
log.info("baixado %s (%s) sha=%s", destino.name,
f"{parte['metros']:.2f} m", sha256(destino)[:12])
registrar_no_kanban(arte, salvos)
cliente.post(f"{PORTAL}/api/artes/{arte['arte_id']}/baixada",
headers={"x-token": TOKEN}, timeout=15)
return True
def ciclo(cliente: httpx.Client) -> int:
r = cliente.get(f"{PORTAL}/api/artes", headers={"x-token": TOKEN}, timeout=30)
r.raise_for_status()
artes = r.json()
for arte in artes:
try:
baixar(cliente, arte)
except Exception as e:
log.error("falha na arte %s: %s", arte["arte_id"], e)
return len(artes)
def main() -> None:
log.info("agente iniciado · portal=%s · pasta=%s", PORTAL, RAIZ)
ultimo_hb = 0.0
with httpx.Client(follow_redirects=True) as cliente:
while True:
try:
n = ciclo(cliente)
if n:
log.info("%d arte(s) processada(s)", n)
except httpx.HTTPError as e:
# internet caiu: não é erro fatal, o portal segue recebendo
log.warning("sem conexão com o portal: %s", e)
except Exception as e:
log.exception("erro inesperado: %s", e)
agora = time.time()
if agora - ultimo_hb > HEARTBEAT:
try:
cliente.post(f"{PORTAL}/api/agente/heartbeat",
headers={"x-token": TOKEN}, timeout=10)
ultimo_hb = agora
except Exception:
pass # sem sinal por 15 min, o portal alerta o TI
time.sleep(INTERVALO)
if __name__ == "__main__":
main()