Proton Cloud SDK for Python
The protoncloud-sdk package on PyPI, for Python 3.10 or later.
The import name is proton.
Installation
pip install protoncloud-sdk
Or, with uv:
uv add protoncloud-sdk
Configuration
The address and the token are resolved in this order:
- Environment variable:
PROTON_HOSTandPROTON_TOKEN. proton.iniin the working directory.
Through the runner, PROTON_HOST and the run id (idDatasetRun) arrive ready to use. The updated runner
also delivers the run token in PROTON_TOKEN, and SDK 5.2 uses it even if
proton.ini has a different token. As long as the file still has a token, the SDK warns once in the log.
With an older runner, the token comes from the PROTON_TOKEN variable on the runner machine or from
proton.ini, which also lets you run outside the runner, on the developer's machine:
[server]
hostname = https://app.protoncloud.com.br/api
token = <token>
The token can come with or without the Bearer prefix. Keep the proton.ini that holds the token out of
version control.
To point the project to another server and ignore what the runner injects, use config_precedence = file
in the [server] section. It applies to the address and to the personal token, but not to the run token.
Without it, when the injected value differs from the file, the SDK prints a line saying which one won (the
token is never printed).
The lifecycle of a run
The runner calls uv run pytest tests/test_proton_script.py::test_run_proton_execution --id_dataset_run=<id>.
Since 5.3, this test only hands the component executor the function
that finds the component by name:
# tests/test_proton_script.py: the only file in the project that imports the SDK
import os
from proton.component_runner import run_components
from automacao.apoio.passos import localizar # component name -> executar(parametros) function
def test_run_proton_execution(request):
os.environ["idDatasetRun"] = request.config.getoption("id_dataset_run")
run_components(localizar)
localizar receives the component name and returns the function that runs it, or None when the
component doesn't belong to the project. A dict from name to function also works. The --id_dataset_run
option is registered in conftest.py (parser.addoption).
The component receives the step parameters, already resolved by Proton (variable, output reference, value typed when the run was triggered, encrypted), and returns the outputs:
# automacao/componentes/criar_pedido.py
def executar(parametros: dict) -> dict:
numero = TelaDoPedido().criar(parametros["in_cliente"])
return {"out_numero_pedido": numero}
To fail, the component raises an exception. The executor records the stack trace in the error panel and marks the run as Failed, with the failure reason. Since 5.5.0, the panel comes without the pytest lines and without the terminal colors.
Whatever the component records through logging (INFO level and above) goes live to the run log, with its
level: warning, error and critical become Warning, Error and Critical.
The run_components options are:
| Option | What it does |
|---|---|
evidence_dir | Evidence folder; the default is evidencias, and None turns uploading off |
forward_logs | With False, the project's logging doesn't go to the run log |
max_steps | Step limit |
close_at_end | Resources the executor closes after the last step (5.5.0) |
Outside a Proton run, run_components raises ProtonRunError instead of calling the server.
is_proton_execution() (from proton.proton_env) tells whether the process is running in a Proton run;
since 5.5.0, it requires a non-empty idDatasetRun.
Evidence folder
At each step, the executor uploads whatever appeared in the evidence folder since the last upload: images go to the step, everything else to the run. Whatever the project saves between two steps is uploaded with the next step.
The runner can handle several runs at the same time, in the same project folder. Since 5.5.0, each run
saves to its own folder, run_evidence_dir(): evidencias/<run id>, or evidencias outside Proton. The
function returns the path and doesn't create the folder:
from proton.component_runner import run_evidence_dir
pasta = run_evidence_dir()
pasta.mkdir(parents=True, exist_ok=True)
navegador.save_screenshot(str(pasta / "pedido.png"))
In the default folder, the executor looks at all of evidencias except the subfolders of other runs: what
the run saves in its own folder is uploaded only for that run, and what the project saves directly in
evidencias is still uploaded, as before.
Close at the end
The video and the trace of a session are only ready when the session closes, after the last step. With
close_at_end (5.5.0), the executor closes the resources, in the given order and only once, and uploads
whatever appeared in the evidence folder before recording the status. You can pass objects with close()
or functions with no arguments, such as driver.quit. This way, the video of a run that passed also
reaches Proton:
from proton.component_runner import run_components
navegador = abrir_navegador()
run_components(localizar, close_at_end=[navegador])
The executor also closes them when a step fails (before Failed) and when the next step belongs to another project. An error while closing doesn't change the result: if everything passed, it becomes a warning in the log; if a step failed, it becomes a note in that step's error.
Failure before the first step
A failure that happens before the executor starts, such as a session that doesn't open or a device that is
offline, reaches Proton through record_failure (5.5.0): the error goes to the error panel and Failed is
recorded with the error's reason, or with the reason you give.
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
Outside Proton, or when the run has already finished, record_failure records nothing.
Without the executor
Projects older than 5.3 go through the steps with the SDK functions. A simplified example:
from proton.proton_automation import (
end_component,
get_current_component_name,
start_component,
update_run_status,
)
from proton.proton_logs import set_error_log, set_log_from_exception
from proton.run_status import RunStatus
from components import COMPONENTES # component name -> function with its code
def test_run_proton_execution(id_dataset_run):
update_run_status(RunStatus.RUNNING)
try:
while nome := get_current_component_name():
start_component() # opens the step and reads the run environment
COMPONENTES[nome]()
end_component() # closes the step as Passed
update_run_status(RunStatus.PASSED)
except Exception as erro:
set_log_from_exception(erro)
set_error_log(str(erro))
update_run_status(RunStatus.FAILED)
raise
The id_dataset_run fixture reads the --id_dataset_run option and calls set_id_dataset_run() (from
proton.proton_env). Without the option, the SDK stays inactive and the test runs locally.
Inside a component, without the executor
from proton.proton_environment import get_environment_value
from proton.proton_files_and_resources import upload_image
from proton.proton_logs import set_log
from proton.proton_parameter import get_proton_value, set_proton_value
def criar_pedido():
cliente = get_proton_value("CLIENTE") # dataset parameter
url = get_environment_value("SISTEMA_URL") # run environment variable
set_log(f"Creating order for {cliente}")
# ... automation ...
upload_image("evidencias/pedido.png") # step evidence
set_proton_value("NUMERO_PEDIDO", "4500012345") # output parameter
Run log
Since 5.4.0, each log line has a level: Info (info, the default), Warning (warning), Error (error) or
Critical (critical). The run screen filters by level and highlights warnings and errors, and log search
finds the line across runs.
from proton.proton_logs import log_error, log_info, log_warning, set_log
log_info("Opening transaction BP")
log_warning("Record already exists: moving on to the next one", context={"cnpj": cnpj})
log_error("Timeout in LoginSAP", context={"tentativa": 3})
set_log("Message with the chosen level", level="critical")
contextis an optionaldictthat goes along with the line (the item being processed, the attempt, the return code). Environment values are masked in it too.set_log_from_exception(erro)sends the entire stack trace as an Error line.- In a Proton run, each line also goes to the process output, with the time and the level. The runner uploads this output as the run's log file: above the Proton log limit (10,000 lines), that file is where the full log is kept.
Correlation ID
Each run has a Correlation ID, the same one the Proton screen shows. The runner delivers it in
PROTON_CORRELATION_ID (runner 5.0.8 onward), and the SDK sends it in every call to Proton
(X-Correlation-Id and traceparent, in the W3C Trace Context format). To carry it into the calls the
automation makes (SAP, APIs, portals), use correlation_headers(). With it, the log of the called system
can be found by the run id:
import requests
from proton.proton_env import correlation_headers, get_correlation_id
from proton.proton_logs import log_info
requests.post(url, json=pedido, headers={**correlation_headers(), "Authorization": token})
log_info(f"Order sent ({get_correlation_id()})")
Outside Proton, or with a runner that doesn't deliver the id yet, correlation_headers() returns an empty
dictionary.
Run status
Proton 5 accepts RunStatus.RUNNING, PASSED and FAILED. Since 5.4.0, FAILED can carry the reason,
which says why the run failed without changing the status:
from proton.proton_automation import update_run_status
from proton.run_status import FailureReason, RunStatus
update_run_status(RunStatus.FAILED, reason=FailureReason.BUSINESS)
| Reason | When |
|---|---|
FailureReason.BUSINESS | A business rule didn't allow the run to continue: a record that already exists, an invoice already rejected. Running again gives the same result. |
FailureReason.TECHNICAL | Any other error: an element that didn't show up, a system that is down, a code exception. This is the default. |
FailureReason.ENVIRONMENT | The environment didn't allow the run: a missing dependency, a wrong setting. |
With the component executor, just raise the reason's exception in the step: ProtonBusinessError for a
business failure and, since 5.5.0, ProtonEnvironmentFailure for an environment failure (a session that
doesn't open, a device that is offline). Any other exception counts as technical.
from proton.run_status import ProtonBusinessError, ProtonEnvironmentFailure
if cadastro_existe:
raise ProtonBusinessError(f"CNPJ {cnpj} is already registered")
try:
navegador = abrir_navegador()
except WebDriverException as erro:
raise ProtonEnvironmentFailure("The grid didn't open the browser") from erro
Since 5.5.0, the executor looks for the reason in the error and in its causes (raise ... from causa),
through the failure_reason function (from proton.run_status), and the first one that states a reason
wins. An exception states the reason through the failure_reason attribute, so an exception from a library
that already extends another class can state it too:
from proton.run_status import FailureReason
class SessaoNaoAbriu(RuntimeError):
failure_reason = FailureReason.ENVIRONMENT
The ProtonEnvironmentError from the run environment (the missing variable) also
counts as an environment failure. In the Java SDK, the environment failure exception is called
ProtonEnvironmentError; in Python that name was already taken by the run environment error, hence
ProtonEnvironmentFailure.
The Proton 4 statuses (FAILED_DATA, FAILED_ENVIRONMENT and IN_PROCESS) are deprecated: the server
rejects them, and the SDK sends FAILED in place of the first two, with the business and environment
reasons, and RUNNING in place of the last one, with a DeprecationWarning and a warning in the log the
first time.
Reference
| Module | Function | What it does |
|---|---|---|
proton.component_runner | run_components(localizar, ...) | Runs the dataset steps with the component executor |
run_evidence_dir() | The run's evidence folder (5.5.0) | |
record_failure(erro, reason=None) | Records a failure that happened before the executor (5.5.0) | |
proton.proton_automation | start_component() | Opens the current step and reads the run environment |
end_component() | Closes the current step | |
get_current_component_name() | Name of the current component; empty when there are no more steps | |
get_current_component_system() | System of the current component | |
update_run_status(status, reason=None) | Marks the run with a RunStatus and, for FAILED, the reason | |
get_automation_name() and get_cycle_name() | Name of the automation and of the cycle | |
get_dataset_run_status(id=None) | Status of the run (the current one, without an id) | |
get_dataset_run_info() | Run data; in a mobile automation, also the device (device), where it runs (deviceFarm) and the OS version on BrowserStack (deviceOsVersion), the last two on a Proton that already returns them | |
get_dataset_info(id=None) | Dataset data (the run's dataset, without an id) | |
get_automation_group_info(id) | Automation group data | |
proton.proton_parameter | get_proton_value(nome) | Value of a dataset parameter in the open step |
get_proton_all_component_parameters() | All parameters of the open step | |
set_proton_value(nome, valor) | Records an output parameter | |
get_proton_output_parameter_list(nome) | Outputs recorded in the run with that name | |
proton.proton_logs | set_log(texto, disable_date_time=False, level="info", context=None) | Writes to the run log, with the level; with disable_date_time=True, accepts Markdown without date and time |
log_info, log_warning, log_error and log_critical | Writes with the level in the name and an optional context | |
set_log_from_exception(erro) | Writes the stack trace to the log, as Error | |
set_error_log(texto) | Records the run error | |
proton.proton_files_and_resources | upload_image(caminho) | Uploads an image as evidence |
upload_file_resource(caminho) | Uploads a file as evidence | |
proton.proton_environment | get_environment_value(nome) and others | Run environment: see Run environment |
proton.proton_env | is_proton_execution() | Whether the automation is running through Proton |
get_id_dataset_run() and set_id_dataset_run(id) | Run id | |
get_correlation_id() and correlation_headers() | The run's Correlation ID and the headers to pass it along | |
proton.run_status | RunStatus and FailureReason | Status and failure reason |
ProtonBusinessError and ProtonEnvironmentFailure | Exceptions that mark a business failure and an environment failure | |
failure_reason(erro) | Reason of an error, also looking at its causes (5.5.0) |
Communication errors
A server error doesn't stop the automation: reads come back empty (get_proton_value also returns ""
when the parameter doesn't exist) and the control calls print a [proton] line with the route and the HTTP
status, which the runner carries into the run log. When a value comes back empty for no apparent reason,
start with the log.
The exception is the run environment: reading a variable that doesn't exist stops the script with
ProtonEnvironmentError, because continuing with an empty value would hide the problem. Since 5.5.0, in the
component executor, it counts as an environment failure.