first commit
This commit is contained in:
141
agente/agente.py
Normal file
141
agente/agente.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user