142 lines
4.5 KiB
Python
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()
|