hobots
SDK Python da Hobots para robôs (RPAs) e integrações. Tudo é uma task: você registra o que o robô faz e a plataforma passa a receber logs, itens processados, progresso, prints, erros e o "estou vivo" do robô.
hobots.register(task, handler)— sob demanda: o robô fica online aguardando solicitações criadas no app (formulários e agendamentos) e executa o handler a cada uma. Módulo Solicitações.hobots.start(task, handler)— sempre ligado: o robô abre a própria execução ao iniciar e roda o handler (o seu loop) dentro dela; a plataforma só recebe logs, itens e progresso. Sem formulário, sem solicitante.
Cliente HTTP puro, sem dependências de runtime (só a stdlib). Requer Python 3.9+. A API é síncrona: handlers são funções normais (def) e o SDK administra as threads de fundo — combina com Selenium, Playwright síncrono, pyautogui e código legado.
Todas as durações são em milissegundos, como no SDK Node:
retry_delay=5000são 5 segundos,hobots.sleep(2000)dorme 2 segundos. Os nomes não levam sufixo_ms.
Instalação
pip install hobots
import hobots # o próprio módulo é o objeto global
Em 30 segundos
export HOBOTS_CLIENT_SECRET=hb_<64 hex> # Configurações → Agentes
export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — default: hostname da máquina
export HOBOTS_ENVIRONMENT=staging # opcional — default: production
import hobots
# sob demanda: uma execução por solicitação do app (o task_slug casa com o formulário)
def processar_nfe(params, ctx):
hobots.log.info(f"lote {params['lote']}")
hobots.progress.total(len(params["notas"]))
for nota in params["notas"]:
baixar(nota)
hobots.items.succeeded({"numero_nota": nota}, id=nota)
hobots.progress.advance()
# sempre ligado: o handler é o loop do robô; cada iteração reporta o que fez
def monitor_pasta(params, ctx):
while True:
hobots.sleep(60_000) # cancelável
for arquivo in novos_arquivos():
hobots.items.succeeded({"arquivo": arquivo})
hobots.log.info("ciclo ok")
hobots.register("processar-nfe", processar_nfe)
hobots.start("monitor-pasta", monitor_pasta)
# as duas chamadas voltam na hora; o processo fica vivo (heartbeat) até hobots.close()
# ou Ctrl-C — encerramento gracioso em "Encerramento", abaixo
O módulo hobots é o objeto global: hobots.log, hobots.items, hobots.progress, hobots.transaction… resolvem a execução atual de qualquer módulo do programa — sem passar contexto de mão em mão, mesmo com várias execuções em paralelo.
Credenciais e identidade
| Env | Opção | O que é |
|---|---|---|
HOBOTS_CLIENT_SECRET |
client_secret |
O Client Secret do agente (hb_<64 hex>), copiado em Configurações → Agentes. Viaja só no header Authorization: Bearer, nunca na URL. Ausente ou inválido → register()/start() levanta ValueError na hora, citando a env (sem ecoar o valor). |
HOBOTS_INSTANCE_ID |
instance_id |
Identidade estável desta instância do robô (vm-cliente-01). Sem ela, o hostname. Heartbeat e tasks convergem nela: um agente só no painel. |
HOBOTS_ENVIRONMENT |
environment |
Ambiente lógico do robô (development | testing | staging | production). Sem ela, production. |
HOBOTS_API_URL |
— | Só em desenvolvimento: aponta o SDK para outro host (http://localhost:3001). Em produção é sempre https://api2.hobots.app. |
A opção passada em register()/start() tem precedência sobre a env — útil para copiar e colar o snippet do app. O secret nunca aparece em repr() do SDK.
Os dois modos
register(task, handler, **opções) |
start(task, handler, **opções) |
|
|---|---|---|
| Quem dispara | Uma solicitação no app (formulário com esse task_slug, ou agendamento) |
O próprio robô, ao iniciar |
params do handler |
O dict validado do formulário | {} |
| Quantas execuções | Uma por solicitação; concurrency em paralelo (1..5) |
Uma só — o handler é o seu loop; quando ele retorna, a execução fecha |
| O processo | Fica vivo fazendo polling | Fica vivo pelo heartbeat, mesmo depois do handler retornar |
| Reexecutar pelo app | Sim | Não — só reiniciando o robô |
As duas chamadas voltam na hora: o polling e o handler do modo start rodam em threads de fundo. Os dois modos podem coexistir no mesmo processo: um monitor sempre ligado + tasks sob demanda, um heartbeat, uma instância. Cada slug só pode ser registrado uma vez.
O handler é sempre handler(params, ctx) — dois argumentos posicionais, também no modo start (onde params é {}). O retorno é ignorado.
Modo start e o limite de logs: o servidor guarda no máximo 1000 linhas de log por execução. Num loop que roda por dias, logue o que importa (ciclos com novidade, avisos, erros) e registre o trabalho em
hobots.items— itens não têm esse teto tão baixo (10.000 por execução).
O módulo hobots
| Membro | O que faz |
|---|---|
hobots.log.debug/info/warn/error/success(msg, *rest) |
Log da execução, enviado incrementalmente ao app (e ecoado no terminal). Aceita vários argumentos como print() — hobots.log.info("nota", numero, "ok") — cada um vira texto, unidos por espaço. Nunca levanta — pode ir em finally. |
hobots.items.succeeded(payload=None, *, id=None) · .failed(msg, payload=None, *, id=None) · .occurrence(msg, …) · .add(status, …) |
A entrega do robô: cada item processado, com status e atributos (viram colunas no app). Bufferizado, em lotes. |
hobots.progress.total(n) · .advance(k=1) · .set(atual, total=None) · .current · .snapshot |
Barra, percentual e tempo restante na solicitação. Sem total() nada é enviado. |
hobots.transaction.start_child(op, descrição) |
Uma step no waterfall de performance da execução (também como with). A transaction da execução já existe e é finalizada pelo SDK. |
hobots.attach(imagem, *, caption=None, level="info") |
Pendura um print (PNG/JPEG/WEBP, até 5 MB, 20 por execução) na linha de log. Nunca levanta nem bloqueia — devolve um Future[AttachResult]. |
hobots.capture_exception(erro, hint=None) |
Abre uma issue (módulo Issues). hint["attachment"] anexa um print à linha de log correspondente. |
hobots.retry(fn, *, retries=3, delay=1000, should_retry=None) |
Retry ciente do cancelamento da execução. fn recebe o número da tentativa; delay aceita callable(tentativa) -> ms. |
hobots.sleep(ms) |
Espera cancelável: levanta RunCanceled se a execução for cancelada. Use no lugar de time.sleep(). |
hobots.signal · hobots.raise_if_canceled() |
Cancelamento cooperativo manual (signal é um threading.Event), para controle fino. |
hobots.run_id · hobots.task · hobots.params |
Identidade da execução atual. |
hobots.register(...) · hobots.start(...) · hobots.close(timeout=30_000) |
Ciclo de vida do processo. |
O handler também recebe o mesmo contexto como 2º argumento (ctx.log, ctx.items, ctx.progress, ctx.transaction, ctx.signal, ctx.params, ctx.sleep, ctx.attach, ctx.retry, ctx.raise_if_canceled), para quem prefere explícito. Fora de qualquer execução, os membros de contexto caem em fallbacks seguros e avisam uma vez — hobots.log continua imprimindo no terminal.
Evite
from hobots import log(ouitems,progress…) no topo de um módulo: o valor capturado no import é o de fora de execução. Importe o módulo e chamehobots.log.info(...)— cada acesso resolve a execução atual.
Cancelamento automático
Uma execução cancelada no app não mata o handler — o SDK dispara hobots.signal e o próximo ponto de parada levanta RunCanceled:
hobots.items.*,hobots.progress.total/advance/set,hobots.transaction.start_child()(estep.start_child()),hobots.sleep()e cada tentativa dehobots.retry().- Nunca
hobots.log.*— um log dentro de umfinallyde limpeza não pode explodir.
Num loop de itens, isso costuma bastar: registrar o item processado já é o ponto de parada. Para controle fino, hobots.raise_if_canceled() e hobots.signal.is_set() continuam disponíveis. Quando o cancelamento veio do app, o RunCanceled é suprimido — a execução já está cancelada no servidor e nada é reportado por cima. Limpeza (fechar navegador, desfazer estado) vai em try/finally.
Atenção: não use
if hobots.signal.is_set(): return. Retornar normalmente reporta a execução comosucceeded. É preciso levantar — e é o que os pontos de parada fazem. Se capturar oRunCanceledpara limpar, re-levante.
Retry
Quando o handler levanta, o SDK tenta de novo na mesma execução: aviso no log (tentativa 1/2 falhou: … — nova tentativa em 5000ms), espera cancelável, re-invocação com os mesmos params. Default: 1 tentativa extra com 5 s. Itens e progresso já enviados permanecem — escreva handlers idempotentes (use o id de negócio nos itens: repetido, o servidor ignora).
hobots.register("processar-nfe", handler, retries=3, retry_delay=lambda tentativa: tentativa * 10_000)
Nunca há nova tentativa em cancelamento, timeout (TaskTimeout é terminal) ou execução já cancelada no app. Para uma chamada instável dentro do handler, use hobots.retry().
Opções
hobots.register(task, handler, *, client_secret=None, ..., concurrency=None, poll_interval=None)
hobots.start(task, handler, *, client_secret=None, ...)
Todas as opções são keyword-only.
| Opção | Default | Descrição |
|---|---|---|
client_secret |
env HOBOTS_CLIENT_SECRET |
Client Secret do agente |
instance_id |
env HOBOTS_INSTANCE_ID → hostname |
Identidade da instância |
environment |
env HOBOTS_ENVIRONMENT → "production" |
development | testing | staging | production — decide quem é alertado |
release |
— | Versão do programa, ex. robo-nfe@1.4.2 |
tags |
— | Tags de todos os eventos |
debug |
False |
Loga a atividade do SDK |
machine_metrics |
True |
CPU/memória/disco no heartbeat e em cada evento |
capture_unhandled |
True |
sys.excepthook/threading.excepthook viram issues |
retries |
1 |
Tentativas extras do handler (0 desliga) |
retry_delay |
5000 |
ms fixo ou callable(tentativa) -> ms |
timeout |
— (infinito) | Timeout por tentativa, em ms; terminal (TaskTimeout). O handler não é morto: o signal dispara e o slot é liberado |
concurrency (register) |
1 |
Execuções simultâneas desta task (1..5) |
poll_interval (register) |
2000 |
Intervalo entre polls sem trabalho (mín. 1000) |
avançadas: install_exit_handlers, log_flush_interval, cancel_check_interval, cancel_grace |
True, 2000, 5000, 10000 |
Ver Referência de API |
As opções de processo (client_secret, instance_id, environment, release, tags, debug, machine_metrics, capture_unhandled, install_exit_handlers, e poll_interval entre os register()) valem as da primeira chamada; chamadas seguintes com valor diferente avisam e são ignoradas.
Encerramento
O heartbeat mantém o processo vivo por design — é a única thread não daemon do SDK, o que faz o script continuar rodando depois da última linha. hobots.close(timeout=30_000) para o polling, aguarda as execuções em voo, para o heartbeat (libera o processo) e faz o flush final; devolve False se algo não terminou a tempo.
Para um encerramento gracioso no Ctrl-C/SIGTERM (esperar a execução em andamento terminar), instale o seu handler de sinal e segure a main thread até ele disparar:
import signal
import threading
import hobots
parar = threading.Event()
signal.signal(signal.SIGINT, lambda signum, frame: parar.set())
signal.signal(signal.SIGTERM, lambda signum, frame: parar.set())
hobots.register("processar-nfe", processar_nfe)
while not parar.wait(1): # o timeout mantém o Ctrl-C responsivo também no Windows
pass
hobots.close(15_000) # espera a execução em voo, para o heartbeat e faz o flush
Com handler próprio de sinal, o SDK não interfere. Sem ele, um SIGINT/SIGTERM com execuções ativas reporta cada uma como failed/AgentTerminated, faz o flush da telemetria e o processo sai com 130/143 — o app mostra a falha na hora. Uma exceção não tratada na main thread também encerra o processo (exit 1), mesmo com o heartbeat vivo.
Não chame
sys.exit()de dentro do handler de sinal quando o script principal já terminou: oSystemExitcai dentro dothreading._shutdowndo interpretador ("Exception ignored in…"). O padrão acima — sinal só seta umEvent, a main thread chamaclose()— evita isso.
Dados sensíveis: logs, itens, mensagens de erro e prints são enviados ao servidor e ficam visíveis no app. O SDK redige padrões óbvios de segredo (
hb_…, Bearer,sk-…, credenciais em URL), mas não borra prints nem filtra dados pessoais. Capture só o necessário.
Migrando da 2.x
A 3.0 remove o init() e a telemetria "solta": tudo passa a acontecer dentro de uma task.
| 2.x | 3.0 |
|---|---|
hobots.init(client_secret=…, instance_id=…, tasks=…, heartbeat=…) |
Nada — env HOBOTS_CLIENT_SECRET/HOBOTS_INSTANCE_ID (ou as opções de register()/start()); heartbeat sempre ligado |
@hobots.task("slug") + hobots.run_forever() |
hobots.register("slug", handler) (volta na hora; o processo fica vivo sozinho) |
Robô que roda sozinho em loop + start_transaction |
hobots.start("slug", handler) — uma execução própria, com logs/itens/progresso |
hobots.stop() / hobots.flush() |
hobots.close(timeout) |
ctx.tx |
hobots.transaction / ctx.transaction |
ctx.cancel_event · ctx.is_cancelled() · ctx.raise_if_cancelled() |
hobots.signal · hobots.signal.is_set() · hobots.raise_if_canceled() |
ctx.attach(...) (bloqueia) · ctx.attach_nowait(...) |
hobots.attach(...) — não bloqueia; .result() no Future para esperar |
tasks={"task_timeout": …, "concurrency": …, "poll_interval": …} |
register(..., timeout=…, concurrency=…, poll_interval=…) — concurrency agora é por task |
capture_message, add_breadcrumb, set_tag(s)/set_user/set_context/set_extra, with_scope |
Removidos — narração vai em hobots.log, dados de negócio em hobots.items; tags fixas em tags= |
get_client, Client, Scope, InitOptions, TasksOptions, HeartbeatOptions, sample_rate, traces_sample_rate, before_send, max_breadcrumbs |
Removidos |
| Handler sem retry | retries=1 por padrão — o handler é re-invocado na mesma execução |
Antes (2.x):
import hobots
hobots.init(client_secret="hb_…", instance_id="vm-01", heartbeat=True, tasks={"concurrency": 2})
@hobots.task("processar-nfe")
def processar(params, ctx):
for nota in params["notas"]:
ctx.raise_if_cancelled()
with ctx.tx.start_child("http", f"GET /notas/{nota}"):
baixar(nota)
ctx.items.succeeded({"numero_nota": nota}, id=nota)
hobots.run_forever()
Depois (3.0):
import hobots # HOBOTS_CLIENT_SECRET e HOBOTS_INSTANCE_ID no ambiente
def processar(params, ctx):
for nota in params["notas"]:
with hobots.transaction.start_child("http", f"GET /notas/{nota}"): # ponto de parada
baixar(nota)
hobots.items.succeeded({"numero_nota": nota}, id=nota) # ponto de parada
hobots.register("processar-nfe", processar, concurrency=2)
Diferenças em relação ao SDK Node
Mesmo protocolo, mesmos defaults, nomes em snake_case. O que muda por causa da linguagem:
- Boot síncrono: o primeiro
register()/start()configura o processo na hora (no Node, num microtask). Tasks registradas depois entram no próximo poll, antecipado na hora. - Sem
globalThis:import hobotsjá é o objeto global — os membros de contexto são resolvidos por__getattr__do módulo, comcontextvarsno lugar doAsyncLocalStorage. attachdevolveconcurrent.futures.Futureem vez dePromise;hobots.signalé umthreading.Eventem vez deAbortSignal.- Threads próprias do handler não herdam o contexto da execução. Com uma execução só ativa,
hobots.*ainda a encontra; comconcurrency > 1, passe octxou rode a thread comcontextvars.copy_context().run(...). - Requisições em voo não são interrompíveis (
urllibnão temAbortSignal): umclose()durante um poll espera a resposta e devolve à plataforma, comoAgentTerminated, qualquer run que ele ainda entregue.
Changelog resumido
- 3.1.0 —
hobots.log.*aceita vários argumentos (comoprint); eco de transaction/steps no terminal só comdebug=True;Nonedentro deextra/payload/params/metadataviaja comonull; heartbeat com jitter de ±10% einterval_msclampado à faixa do servidor; fila da telemetria nunca descarta heartbeat; prazos do reporter (flush, progresso, heartbeat de cancelamento) valem mesmo com o handler num loop apertado; logs emitidos depois do fim da execução seguem como log tardio;close()nunca deixa uma run reivindicada sem dono; PATCH terminal nunca passa de 100 logs;progress.set(inf)/advanceconcorrente corrigidos; tiposEnvironment/EnvironmentInpute todos os tipos do protocolo exportados.
Documentação
- Getting started — instalação, envs, primeira task nos dois modos, encerramento.
- Tasks — handler,
hobots.*, itens, progresso, anexos, cancelamento, retry, concorrência, idempotência. - Conceitos — o vocabulário: task, execução, evento, transaction, step, heartbeat, instância, claim…
- Referência de API — assinaturas, tabelas de opções e tempos.
- Protocolo & transporte — endpoints, envelope, retries, claim.
- Exemplos — scripts executáveis, um por funcionalidade.
Desenvolvimento
pip install -e ".[dev]"
pytest # sem rede (servidor HTTP local nas fixtures)
ruff check . && mypy src
coverage run -m pytest && coverage report # reprova abaixo de 97% (linhas + branches)
Publicação
- Rode a suíte — não publique com ela vermelha:
pytest && ruff check . && mypy src && coverage run -m pytest && coverage report
- Ajuste a versão em
src/hobots/__about__.py(semver). - Construa o pacote:
python3 -m pip install build twine python3 -m build
- Publique no PyPI (requer credenciais em
~/.pypirc):python3 -m twine upload dist/*
- Confirme a publicação em pypi.org/project/hobots.
- Crie um commit com a nova versão.
Os artifacts em dist/ podem ser descartados após a publicação.
Licença
MIT © Hobots
Metadata
Release files for hobots 3.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hobots-3.1.0.tar.gz | 188.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hobots-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 289.8 kB
Release files / hobots-3.1.0.tar.gz
| Download URL | hobots-3.1.0.tar.gz |
|---|---|
| Size | 188.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e6154591bda27c8aa451585be80ff1f1eee0deac450fed008259ec70c2d4c132
|
|
BLAKE2b-256 checksum How to use checksums |
30c193a73e9cf8340bf33c2eb1988731f476c93f2cbd37fa49c482ae75994fd8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / hobots-3.1.0-py3-none-any.whl
| Download URL | hobots-3.1.0-py3-none-any.whl |
|---|---|
| Size | 101.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a3130c3734cf7cfc55fed17b75f3ab7e27b8b5d17e836a49dba873e0b1f6601e
|
|
BLAKE2b-256 checksum How to use checksums |
ed22e2f9a85e0f5cd953a72625d67017611f95ea9111ba14bb524f6045ce95f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|