Skip to content

Commit 19e1f21

Browse files
committed
feat: implementação inicial do bot-runner
0 parents  commit 19e1f21

12 files changed

Lines changed: 822 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main", "develop"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
testes:
11+
name: Testes Python ${{ matrix.python-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.10", "3.11", "3.12"]
16+
17+
steps:
18+
- name: Checkout do código
19+
uses: actions/checkout@v4
20+
21+
- name: Configurar Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Instalar dependências
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install -e "../yaml-workflow-engine" || pip install pyyaml
30+
pip install -e ".[dev]"
31+
32+
- name: Lint com Ruff
33+
run: ruff check bot_runner/ tests/
34+
35+
- name: Executar testes
36+
run: pytest tests/ -v --cov=bot_runner --cov-report=term-missing

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
dist/
6+
build/
7+
.venv/
8+
venv/
9+
.env
10+
.pytest_cache/
11+
.coverage
12+
htmlcov/
13+
.ruff_cache/
14+
.mypy_cache/
15+
.vscode/
16+
.idea/
17+
.DS_Store
18+
Thumbs.db

README.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# bot-runner
2+
3+
> Executor integrado de automações — conecta **yaml-workflow-engine** e **bot-report-api** em um único runner com CLI.
4+
5+
[![CI](https://github.com/EricJoness/bot-runner/actions/workflows/ci.yml/badge.svg)](https://github.com/EricJoness/bot-runner/actions/workflows/ci.yml)
6+
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
7+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8+
9+
---
10+
11+
## O problema
12+
13+
Com múltiplas libs de automação, falta um componente que:
14+
1. **Execute** o workflow definido em YAML
15+
2. **Registre** início e fim da execução automaticamente
16+
3. **Reporte** o resultado para uma API central de observabilidade
17+
18+
**bot-runner** resolve isso, fechando o ciclo ponta a ponta.
19+
20+
---
21+
22+
## Arquitetura do ecossistema
23+
24+
```
25+
┌──────────────────────────────────────────────────────┐
26+
│ bot-runner │
27+
│ │
28+
│ ┌──────────────────┐ ┌────────────────────┐ │
29+
│ │ yaml-workflow- │ │ bot-report-api │ │
30+
│ │ engine │───▶│ (registro e │ │
31+
│ │ (executa YAML) │ │ métricas) │ │
32+
│ └──────────────────┘ └────────────────────┘ │
33+
└──────────────────────────────────────────────────────┘
34+
```
35+
36+
---
37+
38+
## Instalação
39+
40+
```bash
41+
git clone https://github.com/EricJoness/bot-runner.git
42+
cd bot-runner
43+
pip install -e "."
44+
```
45+
46+
---
47+
48+
## Uso via CLI
49+
50+
```bash
51+
# Executar um workflow (bot-report-api em localhost:8000)
52+
bot-runner examples/pipeline_dados.yaml
53+
54+
# Apontar para API em outro servidor
55+
bot-runner workflow.yaml --api http://meu-server:8000
56+
57+
# Executar sem reportar para a API
58+
bot-runner workflow.yaml --sem-api
59+
```
60+
61+
**Saída:**
62+
```
63+
🤖 Iniciando: pipeline_dados.yaml
64+
65+
[LOG:INFO] 🤖 Iniciando pipeline de dados...
66+
[LOG:INFO] 📥 Buscando dados da origem...
67+
[LOG:INFO] ✅ Dados processados com sucesso!
68+
[LOG:INFO] 📧 Relatório enviado para o time!
69+
70+
✅ Pipeline de Dados — 4/4 steps em 0.01s
71+
```
72+
73+
---
74+
75+
## Uso como biblioteca
76+
77+
```python
78+
from bot_runner import BotRunner, Config
79+
80+
config = Config(
81+
api_url="http://localhost:8000", # URL do bot-report-api
82+
parar_na_falha=True,
83+
log_level="INFO",
84+
)
85+
86+
runner = BotRunner(config=config)
87+
resultado = runner.executar(
88+
"workflow.yaml",
89+
nome_bot="Pipeline de Dados",
90+
dados_iniciais={"ambiente": "producao"},
91+
)
92+
93+
print(resultado)
94+
# ✅ Pipeline de Dados — 3/3 steps em 0.45s
95+
96+
print(resultado.exec_id) # ID no bot-report-api
97+
print(resultado.sucesso) # True / False
98+
print(resultado.duracao_segundos)
99+
```
100+
101+
---
102+
103+
## Workflow YAML de exemplo
104+
105+
```yaml
106+
nome: Pipeline de Dados
107+
108+
steps:
109+
- type: log
110+
mensagem: "🤖 Iniciando pipeline..."
111+
112+
- type: http_get
113+
url: "https://api.exemplo.com/dados"
114+
salvar_como: resposta
115+
116+
- type: log
117+
mensagem: "✅ Concluído!"
118+
```
119+
120+
---
121+
122+
## Comportamento sem API
123+
124+
Se o `bot-report-api` não estiver disponível, o bot-runner **ainda executa o workflow normalmente** — a integração com a API é opcional e não bloqueia a automação.
125+
126+
---
127+
128+
## Configuração via variáveis de ambiente
129+
130+
| Variável | Padrão | Descrição |
131+
|---|---|---|
132+
| `BOT_REPORT_API_URL` | `http://localhost:8000` | URL do bot-report-api |
133+
| `BOT_REPORT_API_TIMEOUT` | `10` | Timeout HTTP em segundos |
134+
| `BOT_RUNNER_LOG_LEVEL` | `INFO` | Nível de log |
135+
| `BOT_RUNNER_PARAR_NA_FALHA` | `true` | Interrompe ao falhar step |
136+
137+
---
138+
139+
## Executar os testes
140+
141+
```bash
142+
pip install -e ".[dev]"
143+
pytest tests/ -v --cov=bot_runner
144+
```
145+
146+
---
147+
148+
## Estrutura do projeto
149+
150+
```
151+
bot-runner/
152+
├── bot_runner/
153+
│ ├── runner.py # BotRunner (orquestrador principal)
154+
│ ├── api_client.py # Cliente HTTP para o bot-report-api
155+
│ ├── config.py # Configuração via env vars
156+
│ └── cli.py # Interface de linha de comando
157+
├── tests/
158+
│ └── test_runner.py
159+
└── examples/
160+
├── pipeline_dados.yaml
161+
└── exemplo_uso.py
162+
```
163+
164+
---
165+
166+
## Relacionado
167+
168+
| Repo | Papel |
169+
|---|---|
170+
| [yaml-workflow-engine](https://github.com/EricJoness/yaml-workflow-engine) | Engine que executa os workflows YAML |
171+
| [bot-report-api](https://github.com/EricJoness/bot-report-api) | API REST que recebe os relatórios |
172+
| [botflow](https://github.com/EricJoness/botflow) | Orquestrador alternativo orientado a steps Python |
173+
174+
---
175+
176+
## Licença
177+
178+
MIT © Eric Jones Silva

bot_runner/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
bot-runner — Executor integrado de automações com BotCity.
3+
4+
Orquestra workflows YAML via yaml-workflow-engine,
5+
mantém estado via botflow, e envia os resultados para o bot-report-api.
6+
"""
7+
8+
from bot_runner.runner import BotRunner
9+
from bot_runner.config import Config
10+
11+
__version__ = "0.1.0"
12+
__author__ = "Eric Jones Silva"
13+
14+
__all__ = ["BotRunner", "Config"]

bot_runner/api_client.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Cliente HTTP para comunicar com o bot-report-api.
3+
"""
4+
from __future__ import annotations
5+
6+
import json
7+
import urllib.request
8+
import urllib.error
9+
from typing import Any, Optional
10+
11+
12+
class ApiError(Exception):
13+
"""Erro de comunicação com o bot-report-api."""
14+
def __init__(self, status: int, mensagem: str) -> None:
15+
self.status = status
16+
super().__init__(f"API retornou {status}: {mensagem}")
17+
18+
19+
class BotReportClient:
20+
"""
21+
Cliente leve para o bot-report-api (sem dependência de requests).
22+
23+
Usa apenas a stdlib do Python para comunicação HTTP.
24+
25+
Exemplo::
26+
27+
client = BotReportClient("http://localhost:8000")
28+
bot = client.obter_ou_criar_bot("Meu Bot", tipo="web")
29+
exec_id = client.iniciar_execucao(bot["id"])
30+
client.finalizar_execucao(exec_id, status="sucesso", duracao=12.5)
31+
"""
32+
33+
def __init__(self, base_url: str, timeout: int = 10) -> None:
34+
self.base_url = base_url.rstrip("/")
35+
self.timeout = timeout
36+
37+
# ------------------------------------------------------------------
38+
# Bots
39+
# ------------------------------------------------------------------
40+
41+
def listar_bots(self) -> list[dict]:
42+
return self._get("/bots/")
43+
44+
def criar_bot(self, nome: str, tipo: str = "web", descricao: str = "") -> dict:
45+
return self._post("/bots/", {"nome": nome, "tipo": tipo, "descricao": descricao})
46+
47+
def obter_ou_criar_bot(self, nome: str, tipo: str = "web") -> dict:
48+
"""Retorna o bot existente com o nome dado, ou cria um novo."""
49+
bots = self.listar_bots()
50+
for bot in bots:
51+
if bot["nome"] == nome:
52+
return bot
53+
return self.criar_bot(nome, tipo=tipo)
54+
55+
# ------------------------------------------------------------------
56+
# Execuções
57+
# ------------------------------------------------------------------
58+
59+
def iniciar_execucao(self, bot_id: int) -> int:
60+
"""Registra início de execução e retorna o ID da execução."""
61+
exec_ = self._post("/execucoes/", {"bot_id": bot_id, "status": "em_andamento"})
62+
return exec_["id"]
63+
64+
def finalizar_execucao(
65+
self,
66+
exec_id: int,
67+
status: str,
68+
duracao_segundos: Optional[float] = None,
69+
mensagem: Optional[str] = None,
70+
) -> dict:
71+
"""Atualiza o status de uma execução (sucesso/falha/cancelado)."""
72+
payload: dict[str, Any] = {"status": status}
73+
if duracao_segundos is not None:
74+
payload["duracao_segundos"] = duracao_segundos
75+
if mensagem:
76+
payload["mensagem"] = mensagem
77+
return self._patch(f"/execucoes/{exec_id}", payload)
78+
79+
def health(self) -> bool:
80+
"""Retorna True se a API estiver respondendo."""
81+
try:
82+
resp = self._get("/health")
83+
return resp.get("status") == "ok"
84+
except Exception:
85+
return False
86+
87+
# ------------------------------------------------------------------
88+
# HTTP interno
89+
# ------------------------------------------------------------------
90+
91+
def _get(self, path: str) -> Any:
92+
url = f"{self.base_url}{path}"
93+
req = urllib.request.Request(url, headers={"Accept": "application/json"})
94+
try:
95+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
96+
return json.loads(resp.read())
97+
except urllib.error.HTTPError as e:
98+
raise ApiError(e.code, e.read().decode())
99+
100+
def _post(self, path: str, dados: dict) -> Any:
101+
return self._request("POST", path, dados)
102+
103+
def _patch(self, path: str, dados: dict) -> Any:
104+
return self._request("PATCH", path, dados)
105+
106+
def _request(self, metodo: str, path: str, dados: dict) -> Any:
107+
url = f"{self.base_url}{path}"
108+
corpo = json.dumps(dados).encode()
109+
req = urllib.request.Request(
110+
url, data=corpo, method=metodo,
111+
headers={"Content-Type": "application/json", "Accept": "application/json"},
112+
)
113+
try:
114+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
115+
return json.loads(resp.read())
116+
except urllib.error.HTTPError as e:
117+
raise ApiError(e.code, e.read().decode())

0 commit comments

Comments
 (0)