Metadata-Version: 2.5
Name: protoncloud-sdk
Version: 5.5.0
Summary: Proton Cloud SDK para Python: execução de automações, ambientes, logs, parâmetros e recursos.
Project-URL: Homepage, https://protoncloud.com.br
Author: Atomic Solutions
License: Proprietary
License-File: LICENSE
Keywords: automation,proton,qa,rpa,testing
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: requests>=2.32.4
Description-Content-Type: text/markdown

# Proton Cloud SDK (Python)

SDK Python da plataforma [Proton](https://protoncloud.com.br). A automação usa
esta biblioteca para reportar ao Proton o progresso, o status, os logs, os
parâmetros e os recursos de cada execução.

Distribuição: `protoncloud-sdk`. Import: `proton`.

```python
from proton.proton_automation import start_component, end_component, update_run_status
from proton.run_status import RunStatus
```

## Instalação

```bash
pip install protoncloud-sdk
```

ou, com [uv](https://docs.astral.sh/uv/):

```bash
uv add protoncloud-sdk
```

## Configuração

Host e token são resolvidos nesta ordem, a mesma convenção da SDK Java do Proton:

1. **Variável de ambiente**: `PROTON_HOST`, `PROTON_TOKEN`
2. **`proton.ini`** no diretório de execução

Quando a automação roda pelo Proton Runner, ele injeta no processo o
`PROTON_HOST`, apontando para o servidor de onde a execução nasceu, e o id da
execução (`idDatasetRun`). É isso que permite o mesmo projeto rodar em qualquer
servidor sem editar arquivo.

### Token da execução

O runner atualizado pede ao Proton, para cada execução, um token que vale só
para ela e só enquanto ela roda, e o entrega em `PROTON_TOKEN` (o valor começa
com `proton_run_`). A biblioteca usa esse token mesmo que o `proton.ini` tenha
outro, e mesmo com `config_precedence = file`, que segue valendo para o
endereço.

Com o token da execução, o projeto não precisa guardar token nenhum: tire-o do
`proton.ini`. Enquanto ele estiver lá, a biblioteca avisa uma vez no log da
execução, sem imprimir valor nenhum.

Com runner anterior, que não entrega o token da execução, o token vem da
variável `PROTON_TOKEN` da máquina do runner ou do `proton.ini`, como antes.

O arquivo continua válido e é a única fonte quando se roda a automação fora do
runner:

```ini
[server]
hostname = https://app.protoncloud.com.br/api
token = <token>

[reports]
video_record = false
video_upload = true
```

O token pode vir com ou sem o prefixo `Bearer `; a biblioteca normaliza e nunca
o duplica.

Não versione o `proton.ini` com o token preenchido: deixe-o fora do controle de
versão. O token no arquivo serve para rodar fora do runner, na máquina de quem
desenvolve.

### Fixando o servidor do projeto

Para um teste apontado a outro servidor, ou um projeto que precise ignorar o que
o runner injeta:

```ini
[server]
config_precedence = file
```

Sem isso, quando o valor injetado difere do arquivo, a biblioteca imprime uma
linha dizendo qual venceu. O valor do token nunca é impresso, porque a saída da
automação sobe como log da execução no Proton.

## Contexto da execução

| variável | origem | uso |
|---|---|---|
| `idDatasetRun` | runner | identifica a execução; sem ela (ou vazia) a biblioteca fica inerte (`is_proton_execution()` é `False`) |
| `PROTON_HOST` | runner ou `proton.ini` | base da API |
| `PROTON_TOKEN` | runner (token da execução), variável da máquina ou `proton.ini` | autenticação |
| `idRunProgress` | definida pela própria biblioteca em `start_component()` | componente em andamento |
| ambiente da execução | Proton, lido uma vez por execução em `start_component()` | variáveis do ambiente (`get_environment_value`); só em memória, nunca vira variável do processo |

Fora de uma execução do Proton, `is_proton_execution()` é `False` e as chamadas
viram no-op: o mesmo teste roda localmente sem falar com o servidor.

## Dados da execução

| função (`proton.proton_automation`) | o que devolve |
|---|---|
| `get_automation_name()` | nome da automação |
| `get_cycle_name()` | nome do ciclo |
| `get_current_component_name()` e `get_current_component_system()` | componente da vez e o sistema dele; vazio quando não há mais passos |
| `get_dataset_run_status(id=None)` | status da execução (a corrente, sem id) |
| `get_dataset_run_info()` | dados da execução; numa automação mobile, `device` (no BrowserStack, o nome do aparelho), `deviceFarm` (`local` ou `browserstack`), `deviceOsVersion` (a versão do sistema escolhida no disparo do BrowserStack) e `app`; os dois do meio só num Proton que já os devolve |
| `get_dataset_by_dataset_run()` | id do dataset da execução |
| `get_dataset_info(id=None)` | dados do dataset (o da execução, sem id) |
| `get_automation_group_info(id)` | dados do grupo de automação |

Com o token da execução, as leituras de dataset, grupo e status respondem só
para a própria execução.

## Ambiente da execução

No Proton 5, cada execução roda num **ambiente** (Homologação, Produção...): o
escolhido no disparo ou, sem escolha, o ambiente padrão do dataset. O ambiente
tem variáveis (endereço, usuário, senha), cadastradas no Proton, e o script as lê
pelo nome:

```python
from proton.proton_automation import start_component, end_component
from proton.proton_environment import get_environment_value, get_environment_name, is_production
from proton.proton_logs import set_log

start_component()  # já lê o ambiente da execução, uma vez

url = get_environment_value("SAP_URL")
usuario = get_environment_value("SAP_USUARIO")
senha = get_environment_value("SAP_SENHA")

set_log(f"Ambiente: {get_environment_name()}")
if is_production():
    set_log("Execução em produção: sem gravar documento de teste")

end_component()
```

| função (`proton.proton_environment`) | o que faz |
|---|---|
| `get_environment_value(nome, allow_empty=False)` | valor da variável; erro se ela não existir ou estiver vazia (`allow_empty=True` aceita `""`) |
| `get_environment_variables()` | cópia de todas as variáveis; `{}` sem ambiente ou fora do Proton |
| `get_environment_name()` | nome do ambiente; `None` sem ambiente ou fora do Proton |
| `is_production()` | `True` quando o ambiente tem a marca de produção; `False` sem ambiente ou fora do Proton |
| `mask_environment_values(texto)` | o texto com os valores do ambiente trocados por `••••••` |
| `load_environment(force=False)` | lê o ambiente; o `start_component()` já chama, e `force=True` lê de novo |

O nome da variável é comparado exatamente, com maiúsculas e minúsculas. Nos
ambientes, use os **mesmos nomes** de variáveis (`SISTEMA_URL` em Homologação e em
Produção): o script lê sempre o mesmo nome, e o ambiente decide o valor. Só o que
muda de comportamento em produção usa `is_production()`.

### Uma leitura por execução

Os valores são lidos **uma vez por execução**: no `start_component()` ou, se o
script não o chamar, no primeiro acesso ao ambiente. As leituras seguintes usam o
que já está em memória, e a execução inteira usa os mesmos valores, mesmo que
alguém altere o ambiente no meio. Se o id da execução mudar no mesmo processo
(`set_id_dataset_run`), a biblioteca lê de novo.

Os valores nunca são gravados em disco, em log ou em variáveis do processo
(`os.environ` passa para processos filhos e aparece em dumps). A biblioteca
imprime uma linha só com o nome do ambiente e a quantidade de variáveis:

```
[proton] Ambiente da execução: Produção (produção), 3 variáveis
```

### Erros

As leituras levantam `ProtonEnvironmentError` (subclasse de `RuntimeError`), com
`reason`, `variable` e `environment`. A mensagem nunca traz o valor de uma
variável nem o corpo da resposta do servidor.

| `reason` | quando |
|---|---|
| `notProtonExecution` | leitura por nome fora de uma execução do Proton |
| `runWithoutEnvironment` | a execução não tem ambiente: defina o ambiente padrão do dataset ou escolha um ambiente no disparo |
| `environmentVariableNotFound` | o ambiente não tem a variável pedida |
| `emptyVariable` | a variável existe, mas está vazia (`allow_empty=True` aceita) |
| `runAlreadyFinished` | a execução já terminou: o ambiente só é entregue enquanto ela roda |
| `datasetRunNotFound` | a execução não existe na organização do token |
| `httpError` | sem acesso (HTTP 401 ou 403: confira o token e a permissão `automation.datasetRun.read`), outro status ou sem resposta |

Uma falha na leitura feita pelo `start_component()` não derruba o componente: a
automação que não usa ambiente segue normalmente, e o erro volta no primeiro
acesso ao ambiente, inclusive em `get_environment_name()` e `is_production()`,
para o script não concluir "não é produção" por causa de uma falha de rede.

### Máscara nos logs

O Proton não diz quais variáveis são segredo, então a biblioteca mascara **todos**
os valores do ambiente, trocando-os por `••••••` em tudo o que ela imprime e envia
ao log da execução (`set_log`, `set_error_log`, `set_log_from_exception`):

```python
set_log(f"url={url} senha={senha}")  # no log: url=•••••• senha=••••••
```

- Valores com **menos de 4 caracteres não são mascarados**, senão o log fica
  ilegível (um `1` ou um `sim` sumiria do texto todo). Não guarde segredo com
  menos de 4 caracteres.
- A troca vai do valor mais longo para o mais curto.
- O parâmetro de saída (`set_proton_value`) é gravado como veio: ele é escrito de
  propósito.
- `print(get_environment_variables())` mostra só os nomes, com os valores
  mascarados.
- Para os logs próprios do script, use `mask_environment_values(texto)`.

## Log da execução

Cada linha do log tem um nível: Informação (`info`, o padrão), Alerta (`warning`),
Erro (`error`) ou Crítico (`critical`). A tela da execução filtra por nível e
destaca alertas e erros, e a busca do Proton acha a linha entre execuções.

```python
from proton.proton_logs import log_error, log_info, log_warning, set_log

log_info("Abrindo a transação BP")
log_warning("Cadastro já existe: seguindo para o próximo", context={"cnpj": cnpj})
log_error("Timeout no LoginSAP", context={"tentativa": 3})
set_log("Mensagem com o nível escolhido", level="critical")
```

- `context` é um `dict` opcional que vai junto com a linha (o item processado, a
  tentativa, o código de retorno). Os valores do ambiente saem mascarados nele
  também.
- O `logging` do projeto, encaminhado pelo executor de componentes, chega com o
  nível dele: `warning`, `error` e `critical` viram Alerta, Erro e Crítico.
- `set_log_from_exception(e)` manda o stack trace inteiro como uma linha de Erro.
- Numa execução do Proton, cada linha também sai na saída do processo, com a hora
  e o nível. O runner sobe essa saída como o arquivo de log da execução: acima do
  limite do log no Proton (10 mil linhas), é nele que o log completo fica.

## Correlation ID

Cada execução tem um Correlation ID, o mesmo que a tela do Proton mostra. O runner
o entrega em `PROTON_CORRELATION_ID` (runner 5.0.8 em diante), e a biblioteca o
envia em toda chamada ao Proton (`X-Correlation-Id` e `traceparent`, no formato
W3C Trace Context).

Para levá-lo às chamadas que a automação faz (SAP, APIs, portais), use
`correlation_headers()`. Com ele, o log do sistema chamado se acha pelo id da
execução:

```python
import requests
from proton.proton_env import correlation_headers, get_correlation_id

requests.post(url, json=pedido, headers={**correlation_headers(), "Authorization": token})
log_info(f"Pedido enviado ({get_correlation_id()})")
```

Fora do Proton, ou com um runner que ainda não entrega o id, `correlation_headers()`
devolve um dicionário vazio.

### Categoria do dataset (Proton 4)

No Proton 5 o dataset não tem categoria. `get_dataset_category()` e
`get_dataset_category_name()` estão obsoletas: emitem `DeprecationWarning` e, na
primeira chamada, uma linha `[proton]` no log; `get_dataset_category_name()`
devolve vazio. Em vez de decidir endereço, usuário e senha pela categoria, leia os
valores do ambiente:

```python
# Antes (Proton 4)
if get_dataset_category_name() == "PRD":
    url = "https://sistema.empresa.com.br"
else:
    url = "https://sistema-hml.empresa.com.br"

# Depois (Proton 5)
url = get_environment_value("SISTEMA_URL")
```

## Status da execução

```python
from proton.proton_automation import update_run_status
from proton.run_status import RunStatus

update_run_status(RunStatus.FAILED)
```

O Proton 5 aceita `RUNNING`, `PASSED` e `FAILED`. Junto com o `FAILED`, o motivo
diz por que a execução falhou, sem mudar o status:

```python
from proton.run_status import FailureReason

update_run_status(RunStatus.FAILED, reason=FailureReason.BUSINESS)
```

| Motivo | Quando |
|---|---|
| `FailureReason.BUSINESS` | A regra de negócio não deixou seguir: cadastro que já existe, nota já recusada. Repetir dá o mesmo resultado. |
| `FailureReason.TECHNICAL` | Qualquer outro erro: elemento que não apareceu, sistema fora do ar, exceção do código. É o padrão. |
| `FailureReason.ENVIRONMENT` | O ambiente não deixou rodar: dependência que falta, configuração errada. |

Com o executor de componentes, basta lançar `ProtonBusinessError` no passo: ele grava
a falha com o motivo de negócio. `ProtonEnvironmentFailure` é a falha de ambiente (a
sessão que não abre, o aparelho fora do ar). Qualquer outra exceção conta como técnica.

```python
from proton.run_status import ProtonBusinessError, ProtonEnvironmentFailure

if cadastro_existe:
    raise ProtonBusinessError(f"O CNPJ {cnpj} já está cadastrado")

try:
    navegador = abrir_navegador()
except WebDriverException as erro:
    raise ProtonEnvironmentFailure("O grid não abriu o navegador") from erro
```

O motivo vem do erro e das causas dele (`raise ... from causa`), pela função
`failure_reason` (`proton.run_status`): o primeiro que diz o motivo vale. A exceção diz
o motivo pelo atributo `failure_reason`, então a exceção de uma biblioteca que já
estende outra classe também pode dizer:

```python
class SessaoNaoAbriu(RuntimeError):
    failure_reason = FailureReason.ENVIRONMENT
```

O `ProtonEnvironmentError` do ambiente da execução (a variável que falta) também conta
como falha de ambiente. Na SDK Java, a falha de ambiente se chama `ProtonEnvironmentError`;
aqui esse nome já era o erro do ambiente da execução, por isso `ProtonEnvironmentFailure`.

Os status do Proton 4 (`FAILED_DATA`, `FAILED_ENVIRONMENT` e `IN_PROCESS`) não
existem mais: o servidor os recusa, e a biblioteca envia `FAILED` no lugar dos dois
primeiros, com o motivo de negócio e de ambiente, e `RUNNING` no lugar do último,
com um `DeprecationWarning` e uma linha `[proton]` no log na primeira vez.

## Executor de componentes

Desde a 5.3.0 a SDK traz o laço que roda os passos do dataset, que antes cada
projeto copiava no `tests/test_proton_script.py`. O projeto só diz, pelo nome do
componente cadastrado no Proton, qual função executar:

```python
# tests/test_proton_script.py: o único arquivo do projeto que importa a SDK
from proton.component_runner import run_components
from automacao.passos import localizar


def test_run_proton_execution():
    run_components(localizar)
```

`localizar` recebe o nome do componente e devolve o passo: uma função que recebe
os parâmetros (`dict`) e devolve as saídas (`dict` ou `None`). Também serve um
`dict` de nome para função. O código de negócio não importa nada do Proton e roda
igual fora dele.

A cada passo, o executor:

- marca o início e o fim, inclusive quando o passo falha;
- passa só os parâmetros daquele passo, já resolvidos pelo Proton (variável,
  referência a saída, valor digitado no disparo, criptografado);
- grava as saídas devolvidas. Uma saída que não está cadastrada como parâmetro do
  passo vira aviso no log;
- sobe o que apareceu na pasta de evidências desde o último envio: imagens no passo,
  o resto na execução (`evidence_dir` muda a pasta; `None` desliga);
- manda para a execução o que o projeto registra pelo `logging`, do nível INFO
  para cima (`forward_logs=False` desliga).

No fim, grava `PASSED` quando não sobra passo. Quando um passo falha, grava o
stack trace no painel de erro (sem as linhas do pytest e sem as cores do terminal),
grava `FAILED` com o motivo (`failure_reason`, acima) e relança a exceção. Execução
parada não recebe status por cima.

### Pasta de evidências

O runner pode rodar várias execuções ao mesmo tempo na mesma pasta do projeto. Cada
execução grava na pasta dela, `run_evidence_dir()` (`evidencias/<id da execução>`, ou
`evidencias` fora do Proton). Na pasta padrão, o executor olha `evidencias` inteira
menos as subpastas das outras execuções: o que a execução grava na pasta dela sobe só
para ela, e o que o projeto grava direto em `evidencias` continua subindo, como antes.

### Fechar no fim: o vídeo de uma execução que passou

O vídeo e o trace só ficam prontos quando a sessão fecha, depois do último passo. Com
`close_at_end`, o executor fecha os recursos (objetos com `close()` ou funções sem
argumento, como `driver.quit`), na ordem dada e uma vez só, e sobe o que apareceu na
pasta de evidências antes de gravar o status:

```python
from proton.component_runner import run_components

navegador = abrir_navegador()
run_components(localizar, close_at_end=[navegador])
```

Fecha também quando um passo falha (antes do `FAILED`) e quando o próximo passo é de
outro projeto. Um erro ao fechar não muda o resultado: com tudo passando, vira um
alerta no log; com um passo que falhou, vira uma nota no erro dele.

### Falha antes do primeiro passo

A falha que acontece antes do executor (a sessão que não abre) vai ao Proton por
`record_failure`: o erro no painel e `FAILED` com o motivo do erro, ou com o motivo
dado. Fora do Proton, ou com a execução já encerrada, não grava nada.

```python
from proton.component_runner import record_failure
from proton.run_status import FailureReason

try:
    navegador = abrir_navegador()
except Exception as erro:
    record_failure(erro, FailureReason.ENVIRONMENT)
    raise
```

Um projeto pode cuidar de várias plataformas (SAP, web, API, desktop), cada uma
cadastrada como um sistema no Proton e todas ligadas ao mesmo repositório: o executor
segue no mesmo processo enquanto o projeto tiver o componente do próximo passo, e as
sessões abertas continuam valendo. Quando o projeto não tem o componente e o passo é de
outro sistema, o executor devolve o controle ao runner, que chama o projeto daquele
sistema.

Fora de uma execução do Proton, `run_components` lança `ProtonRunError` em vez de
chamar o servidor.

## Migrando de uma cópia local da pasta `proton/`

Projetos que carregam esta biblioteca como uma pasta `proton/` copiada migram em
dois passos:

1. adicione `protoncloud-sdk` às dependências;
2. **apague a pasta `proton/` do projeto.**

O segundo passo não é opcional: a pasta local tem precedência sobre o pacote
instalado, e enquanto ela existir o projeto continua executando a cópia antiga.
