Metadata-Version: 2.5
Name: onerom-desktop
Version: 1.5.2
Summary: Runtime de automacao desktop da plataforma ONEROM (UIA, OCR, imagem, coordenadas e SAP GUI)
Project-URL: Homepage, https://onerom.dev.br
Author: ONEROM Team
License: MIT
License-File: LICENSE
Keywords: automation,desktop,ocr,onerom,rpa,sap,uia
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Requires-Dist: mss>=9.0.0
Requires-Dist: pillow>=10.0.0
Requires-Dist: psutil>=5.9.0
Requires-Dist: pyautogui>=0.9.54
Requires-Dist: pyperclip>=1.9.0
Provides-Extra: all
Requires-Dist: comtypes>=1.4.16; (sys_platform == 'win32') and extra == 'all'
Requires-Dist: numpy>=1.24; extra == 'all'
Requires-Dist: opencv-python-headless<5,>=4.9.0; extra == 'all'
Requires-Dist: pytesseract>=0.3.10; extra == 'all'
Requires-Dist: pywin32>=306; (sys_platform == 'win32') and extra == 'all'
Requires-Dist: pywinauto>=0.6.9; (sys_platform == 'win32') and extra == 'all'
Provides-Extra: backend
Requires-Dist: comtypes>=1.4.16; (sys_platform == 'win32') and extra == 'backend'
Requires-Dist: pywin32>=306; (sys_platform == 'win32') and extra == 'backend'
Requires-Dist: pywinauto>=0.6.9; (sys_platform == 'win32') and extra == 'backend'
Provides-Extra: image
Requires-Dist: numpy>=1.24; extra == 'image'
Requires-Dist: opencv-python-headless<5,>=4.9.0; extra == 'image'
Provides-Extra: ocr
Requires-Dist: numpy>=1.24; extra == 'ocr'
Requires-Dist: opencv-python-headless<5,>=4.9.0; extra == 'ocr'
Requires-Dist: pytesseract>=0.3.10; extra == 'ocr'
Provides-Extra: sap
Requires-Dist: pywin32>=306; (sys_platform == 'win32') and extra == 'sap'
Description-Content-Type: text/markdown

# onerom-desktop

Runtime de automação desktop da plataforma ONEROM.

É a biblioteca que os bots gerados pelo **ONEROM Inspector** importam para clicar,
preencher e ler telas — de aplicações Windows nativas a SAP GUI, passando por
janelas que não expõem nenhuma árvore de automação.

```bash
pip install "onerom-desktop[all]"
```

## Uma classe, cinco estratégias

```python
from onerom_desktop import App

app = App(strategy="backend", automation_id="btnLogin", window_title="Login")
app.wait_visible()
app.click()
```

O que você passa no construtor vira padrão para todas as ações; o que passa na
ação vale só para aquela chamada. É isso que deixa o script curto sem esconder o
que ele está mirando.

| `strategy=` | Como localiza | Quando usar |
|---|---|---|
| `backend` | UIAutomation via pywinauto | **Padrão.** Sobrevive a janela movida, resolução diferente e troca de tema |
| `ocr` | Texto lido da tela (Tesseract) | Citrix, RDP, canvas Java/Delphi — quando não há árvore de automação |
| `image` | Template matching (OpenCV + ORB) | Ícones e controles desenhados, sem identidade nem rótulo |
| `coordinate` | Ponto fixo na tela | Último recurso: não verifica nada |
| `sap` | SAP GUI Scripting (COM) | Sempre, quando o alvo é SAP |
| `auto` | `backend` → `ocr` → `image` → `coordinate` | Degrada sozinho quando a aplicação muda |

```python
app.fill(strategy="ocr", text="Usuário", value="admin")
app.click(strategy="image", image="botao.png", confidence=0.9)
app.press(strategy="sap", sap_id="wnd[0]/tbar[0]/btn[0]")
app.click(strategy="auto")
```

## Âncora visual e ações relativas

Muito campo não tem identidade nenhuma — sem `automation_id`, sem texto próprio,
sem pixels distintivos — mas fica sempre ao lado de algo que tem: um rótulo, um
ícone, um cabeçalho de coluna. Ancore no que dá para ver e aja por deslocamento:

```python
app.find(strategy="image", image="rotulo_usuario.png")   # acha uma vez
app.fill_relative(140, 0, value="admin")                 # campo à direita
app.fill_relative(140, 34, value="segredo")              # o de baixo
app.click_relative(140, 70)                              # o botão
```

O deslocamento parte do **centro** da âncora, então sobrevive à janela mudar de
lugar — ao contrário de uma coordenada absoluta. E a busca por imagem acontece
**uma vez** para todos os campos ao redor, em vez de varrer a tela a cada um.

Sem âncora, dá para deslocar direto na ação:

```python
app.click(strategy="image", image="rotulo.png", offset_x=140, offset_y=0)
```

Disponíveis: `click_relative`, `double_click_relative`, `right_click_relative`,
`hover_relative`, `fill_relative`, `type_relative`. E `app.anchor` devolve a
região atual, ou `None` se ainda não houve `find()`.

## Ações

`click` · `double_click` · `right_click` · `hover` · `fill` · `click_and_fill` ·
`type_text` · `get_text` · `wait_visible` · `wait_not_visible` · `exists` ·
`find` · `find_all` · `scroll` · `drag_to` · `screenshot`

Teclado: `press_key` · `hotkey` · `copy_text`.

Janela e processo: `start` · `connect` · `activate` · `maximize` · `minimize` ·
`restore` · `move_window` · `window_region` · `window_exists` · `wait_window` ·
`close_window` · `kill`.

Diálogos e hierarquia: `wait_dialog` · `dialog` · `dialogs` · `owner_window` ·
`parent_window` · `child_windows` · `window_title` · `window_class` · `use_window`.

Para SAP, também: `set_text` · `press` · `submit` · `grid_read` · `grid_rows` ·
`grid_columns` · `grid_cell` · `grid_set_cell`.

```python
if app.exists(strategy="backend", automation_id="dlgErro", window_title="Erro"):
    app.click(automation_id="btnFechar")

total = app.get_text(strategy="ocr", x=800, y=440, width=160, height=28)
app.screenshot("evidencia.png")
```

## Janela e processo

Antes de clicar em qualquer coisa é preciso que a janela certa esteja aberta e na
frente. Um bot que não traz a janela para frente clica no que estiver por cima.

```python
app = App(process="sistema.exe", window_title="Login")

app.start(r"C:\Program Files\Sistema\sistema.exe")   # abre e espera a janela
app.activate()                                        # traz para frente e foca
app.maximize()
...
app.close_window()                                    # pede para fechar
```

| Método | O que faz |
|---|---|
| `start(command, args=, cwd=, wait=)` | Abre o aplicativo e espera a janela. Devolve o pid |
| `connect(timeout=)` | Anexa a um aplicativo já aberto. Devolve o handle |
| `activate()` | Traz para frente e dá foco de teclado |
| `maximize()` · `minimize()` · `restore()` | Estado da janela |
| `move_window(x, y, width=, height=)` | Move e redimensiona. Tamanho 0 mantém o atual |
| `window_region()` | O retângulo da janela, como `Region` |
| `window_exists()` | Se há uma janela correspondente aberta agora |
| `wait_window(timeout=)` | Bloqueia até a janela aparecer |
| `close_window(timeout=)` | Pede para fechar; **devolve se realmente fechou** |
| `kill(timeout=)` | Derruba o processo. Nada é salvo |
| `app.hwnd` · `app.pid` | A última janela alcançada e o processo iniciado |

Tudo isso é `ctypes` sobre o `user32`, **sem pywinauto** — logo funciona na
instalação base, inclusive para bots de `image` e `ocr`, que são justamente os que
mais precisam da janela na frente por clicarem em pixels reais.

### Três detalhes que não são óbvios

**`close_window()` devolve um booleano e você tem de olhar.** Um aplicativo que
responde com "deseja salvar?" não fechou — e isso é um desfecho normal, não uma
falha:

```python
if not app.close_window(timeout=5):
    app.click(strategy="image", image="nao_salvar.png")
```

**`activate()` levanta em vez de seguir em frente.** O Windows recusa foco a quem
não o tem, e o quanto ele recusa é configuração de máquina (`ForegroundLockTimeout`
pode estar em "nunca"). A lib escala sozinha — chamada direta, depois
`AttachThreadInput`, depois um ciclo minimizar/restaurar — mas se nada funcionou ela
levanta `WindowActivationError`, porque continuar mandaria o próximo clique para a
janela errada.

**O handle é cacheado, e é de propósito.** Resolver pelo título a cada chamada
quebra no fluxo mais comum de software corporativo: o título muda depois do login
("Login" vira "Sistema - Jane Doe"). O handle não muda. `app.hwnd` é a última janela
alcançada; passar `process=`/`window_title=` numa chamada sempre resolve de novo.

## Teclado

Nem tudo se resolve clicando. Enter envia formulário, Tab anda entre campos, F5
recarrega uma lista — e não existe equivalente de colagem para nenhum deles.

```python
app.press_key("enter")
app.press_key("tab", times=3)
app.hotkey("ctrl", "s")        # ou app.press_key("ctrl+s")
```

Nomes em português são de primeira classe: `"baixo"`, `"cima"`, `"espaco"`,
`"fim"`, `"deletar"`, `"controle"`. Uma tecla desconhecida **levanta** e lista as
válidas — mandar nada pareceria que a aplicação ignorou.

Isso age em quem está com o foco, de propósito. "Manda Enter naquele botão" são
duas ideias — focar e apertar — e juntá-las esconde qual das duas falhou.

`app.copy_text()` dá Ctrl+C e devolve o que caiu no clipboard: a saída para um
controle que mostra texto mas não expõe valor nenhum ao UIAutomation.

## Rolagem e arrasto

```python
app.scroll(5, "down", automation_id="lstPedidos")
app.scroll(3, "up")                                  # onde o ponteiro já está
app.drag_to(900, 400, strategy="image", image="card.png")
app.find(strategy="image", image="item.png")
app.drag_relative(0, 240)
```

O destino do arrasto é `to_x`/`to_y` na assinatura, não `x`/`y` — estes já
significam *onde está a origem* para `strategy="coordinate"`, e um arrasto
precisa das duas pontas.

## Esperar aparecer, e esperar sumir

```python
app.wait_visible(automation_id="btnSalvar", timeout=20)
app.wait_not_visible(strategy="image", image="spinner.png", timeout=60)
```

`wait_not_visible` é a metade que faz um bot progredir: o spinner, o overlay de
"aguarde", o modal que precisa fechar antes do próximo passo. Sem ela todo bot
escreve o mesmo laço na mão e esquece o timeout.

## `find_all` — percorrer uma lista

```python
for linha in app.find_all(strategy="image", image="checkbox_vazio.png"):
    app.click(strategy="coordinate", x=linha.center_x, y=linha.center_y)
```

Funciona nas quatro estratégias: o `backend` enumera os descendentes que casam,
o `image` devolve todas as ocorrências do template, o `ocr` todas as linhas com
o texto. Lista vazia é resposta, não falha.

Uma diferença deliberada em relação ao `find()`: a enumeração **não** afrouxa os
critérios passo a passo. Afrouxar existe para resgatar uma captura que
envelheceu; numa lista, isso misturaria as linhas de um grid com controles sem
relação que apenas compartilham um pedaço do nome — e quem recebe as regiões não
tem como perceber.

## Resiliência e evidência

```python
app = App(
    retries=2,                       # tentativas extras quando não acha
    retry_delay=0.5,
    screenshot_on_error="evidencias" # PNG da tela quando um passo falha de vez
)
```

`retries` é **0 por padrão**: ligar sozinho mudaria a duração de todo passo de
todo bot que já existe. Só falhas do tipo "não estava lá desta vez" são
repetidas — dependência faltando e localizador inválido nunca, porque repetir
esses só enterra a mensagem real sob N cópias iguais. As esperas também nunca
são repetidas: elas já têm timeout próprio, e repetir multiplicaria o que você
pediu.

O screenshot é o que salva a investigação: quando alguém lê o log, o modal
inesperado que derrubou o bot já sumiu da tela. O caminho fica em
`app.last_error_screenshot`.

## Grids do SAP

A coisa que um bot de SAP faz e que **nenhuma** biblioteca genérica de
UIAutomation consegue. O SAP desenha os próprios grids, então nem pywinauto nem
FlaUI enxergam uma linha sequer — a API de scripting é a única porta.

```python
for linha in app.grid_read("wnd[0]/usr/cntlGRID1/shellcont/shell"):
    print(linha["Documento"], linha["Valor"])

app.grid_cell("wnd[0]/usr/cntlGRID1/shellcont/shell", row=0, column="DOCNUM")
app.grid_set_cell(..., row=3, column="QTD", value="10")
```

Atende as duas famílias sem você precisar saber qual a transação usa: o
`GuiGridView` (ALV, colunas por nome) e o `GuiTableControl` (colunas por índice).

**O detalhe que importa:** um `GuiTableControl` só materializa as linhas que
estão **roladas para a tela**. Quando isso acontece, um aviso vai para o log
dizendo quantas de quantas foram lidas. Devolver as 12 primeiras de 300 em
silêncio é como um bot reporta um número errado e ninguém percebe.

## Diálogos e hierarquia de janelas

O Windows tem **duas** relações verticais, e confundi-las é a armadilha clássica
do Win32:

- **pai** — contenção. Um botão *dentro* de um formulário. Não é algo que o
  usuário foca sozinho.
- **dono** — associação. Um diálogo que *pertence* a uma janela mas é top-level
  por conta própria. É o que um "Tem certeza?" modal realmente é.

`GetParent` mistura as duas: para uma janela top-level com dono, ele responde o
**dono**. A lib pergunta sempre a coisa sem ambiguidade.

```python
app.click(automation_id="btnExcluir")          # dispara o modal
modal = app.wait_dialog("Confirmar", timeout=10)
app.activate(hwnd=modal)
app.click(window_title="Confirmar", name="Sim")
```

| Método | Responde |
|---|---|
| `wait_dialog(titulo, timeout=)` | Bloqueia até a janela abrir um diálogo; devolve o handle |
| `dialog(titulo)` | O diálogo aberto agora, ou 0 — a forma de pergunta, para ramificar |
| `dialogs()` | Todos os diálogos que a janela possui |
| `owner_window()` | A aplicação por trás de um diálogo, ou 0 |
| `parent_window()` | A janela que contém esta, ou 0 se for top-level |
| `child_windows()` | Os HWND filhos, como `(hwnd, título, classe)` |
| `window_title()` · `window_class()` | Legenda e classe |
| `use_window(hwnd)` | Aponta a `App` para um handle específico |

`wait_dialog` **espera** de propósito: o modal não existe no instante em que o
clique acontece — a aplicação precisa construí-lo. Sem a espera, o bot responde
o diálogo *às vezes*, e no resto das vezes clica direto no que está atrás.

Ele devolve o handle em vez de repontar a `App` sozinho. Passe de volta como
`hwnd=` nos métodos de janela, e fica óbvio de qual janela cada linha fala.

`window_class()` costuma ser a única alça estável num diálogo: `#32770` é a
classe padrão de diálogo do Windows e não muda com o idioma da interface, ao
contrário da legenda.

## Erros dizem o que fazer

Tudo herda de `OneromDesktopError`, e as classes existem para separar os três
casos que pedem tratamento diferente:

```python
from onerom_desktop import (
    OneromDesktopError,          # base de tudo
    DesktopLocatorError,         # bug no bot — repetir não adianta
    DesktopBackendUnavailableError,  # ambiente quebrado — avisar o operador
    ElementNotFoundError,        # não achou agora — repetir pode funcionar
    WaitTimeoutError,
    MissingDependencyError,      # traz o comando de instalação na mensagem
)
```

`exists()` responde `False` para ausência de verdade, mas **levanta** exceção
quando a verificação em si não pôde ser feita — dependência faltando, backend
indisponível, imagem de template ausente, SAP fechado. Responder `False` nesses
casos é como um bot acaba entrando no branch errado.

## Dependências opcionais

O `import onerom_desktop` é barato: OpenCV, Tesseract, pywinauto e pywin32 só
são carregados quando a estratégia correspondente é usada.

```bash
pip install onerom-desktop              # coordenadas, teclado, screenshot
pip install "onerom-desktop[backend]"   # + UIAutomation (Windows)
pip install "onerom-desktop[image]"     # + OpenCV
pip install "onerom-desktop[ocr]"       # + Tesseract (o binário também é necessário)
pip install "onerom-desktop[sap]"       # + SAP GUI Scripting (Windows)
pip install "onerom-desktop[all]"       # tudo
```

Faltando uma delas, a mensagem já traz o `pip install` certo em vez de um
`ModuleNotFoundError` cru no log do bot.

## Coordenadas são pixels FÍSICOS

Toda coordenada nesta biblioteca é pixel físico da mesa virtual (a união de
todos os monitores) — o mesmo espaço em que o Inspector captura, o `mss`
fotografa e o `pyautogui` clica.

O processo é marcado como *per-monitor DPI aware* no import. Sem isso, o Windows
reescala silenciosamente as coordenadas de um processo DPI-unaware e todo clique
erra o alvo em tela a 125%/150% — o clássico "funciona na minha tela".

## Arquitetura

```
onerom_desktop/
├── app.py          App: defaults, dispatch e a cadeia auto
├── locators.py     kwargs soltos -> locators validados
├── strategies/     interface uniforme de ações (base + 5 estratégias)
├── engines/        uia · ocr · template · sap · windows
├── pointer.py      mouse           screen.py    captura de tela
├── keyboard.py     texto           geometry.py  Region
├── errors.py       hierarquia      optional.py  deps sob demanda
└── dpi.py          invariante de pixel físico
```

`strategies/base.py` concentra o caminho comum: as três estratégias que acabam
clicando um ponto (`ocr`, `image`, `coordinate`) só implementam `locate()` e
herdam todas as ações.

## Desenvolvimento

```bash
uv sync --group dev
./check.sh
```

MIT.
