roberty-code 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ node_modules/
2
+ dist/
3
+ *.tgz
4
+ .yarn/*
5
+ **/.yarn/*
6
+ !.yarn/releases
7
+ !.yarn/plugins
8
+
9
+ __pycache__/
10
+ *.py[cod]
11
+ .venv/
12
+ build/
13
+ *.egg-info/
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+
17
+ .DS_Store
18
+ .env*
19
+
20
+ .yarn/install-state.gz
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberty Automation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: roberty-code
3
+ Version: 0.1.0
4
+ Summary: SDK oficial para Code Projects da Roberty: leia argumentos, escreva o resultado e descubra o gatilho da execução.
5
+ Project-URL: Homepage, https://github.com/robertyautomation/code-sdk/tree/main/packages/python#readme
6
+ Project-URL: Repository, https://github.com/robertyautomation/code-sdk
7
+ Project-URL: Issues, https://github.com/robertyautomation/code-sdk/issues
8
+ Project-URL: Documentation, https://github.com/robertyautomation/code/blob/main/roberty-code-project-guide.md
9
+ Author: Roberty Automation
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: automation,code-project,roberty,rpa,sdk
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+
28
+ # roberty-code
29
+
30
+ SDK oficial para **Code Projects** da [Roberty](https://roberty.app) em Python. Leia argumentos, escreva o resultado e descubra o gatilho da execução sem lidar com variáveis de ambiente.
31
+
32
+ ```bash
33
+ pip install roberty-code
34
+ ```
35
+
36
+ Python ≥ 3.9. Sem dependências. Tipado.
37
+
38
+ ## Uso
39
+
40
+ ```python
41
+ from datetime import datetime, timezone
42
+ import roberty_code as roberty
43
+
44
+ print(f"Iniciando ({roberty.environment}, gatilho: {roberty.trigger})")
45
+
46
+ cpf = roberty.input("cpf") # um argumento
47
+ args = roberty.inputs() # todos os argumentos (ou None)
48
+
49
+ # ... lógica do robô ...
50
+
51
+ roberty.output({"status": "ok", "processedAt": datetime.now(timezone.utc).isoformat()})
52
+ ```
53
+
54
+ Fora da Roberty o mesmo código roda: `inputs()` devolve `None`, `output()` avisa uma vez e devolve `False`.
55
+
56
+ ## API
57
+
58
+ | Membro | Retorno | Descrição |
59
+ | --- | --- | --- |
60
+ | `roberty.trigger` | `str \| None` | `manual`, `schedule`, `webhook` ou `evaluation` |
61
+ | `roberty.environment` | `str \| None` | `production` ou `preview` |
62
+ | `roberty.is_roberty` | `bool` | `True` quando roda na Roberty |
63
+ | `roberty.inputs()` | `Any \| None` | Todos os argumentos (`webhook`/`evaluation`) |
64
+ | `roberty.input(name, default=None)` | `Any` | Um argumento específico |
65
+ | `roberty.output(value)` | `bool` | Grava o resultado (JSON) lido pela plataforma |
66
+ | `roberty.exception()` | `ExceptionDetails \| None` | `{"message", "stack", "exitCode"}` dentro do `exceptionHandler` |
67
+ | `roberty.run(handler, exit_on_complete=True)` | `Any` | **Opcional**: executa `handler(roberty)`, grava o retorno e encerra com exit code `0`/`1`. Aceita `async def` |
68
+ | `roberty.ENV_VARS` | `dict` | Nomes das variáveis `roberty-*` |
69
+
70
+ Erros: `RobertyArgsError` (arquivo de argumentos com JSON inválido) e `RobertyOutputError` (valor não serializável; converta `datetime` com `.isoformat()`).
71
+
72
+ ### `exceptionHandler`
73
+
74
+ ```python
75
+ # on_error.py — declarado em roberty.json como "exceptionHandler": "on_error.py"
76
+ import roberty_code as roberty
77
+
78
+ error = roberty.exception()
79
+ print(f"Robô falhou (exit code {error['exitCode']}): {error['message']}")
80
+ ```
81
+
82
+ ### Atalho `run`
83
+
84
+ ```python
85
+ roberty.run(lambda r: {"status": "ok", "cpf": r.input("cpf")})
86
+ ```
87
+
88
+ Encerra o processo ao terminar; passe `exit_on_complete=False` para embutir em testes.
89
+
90
+ ## Documentação
91
+
92
+ [Guia de Code Projects](https://github.com/robertyautomation/code/blob/main/roberty-code-project-guide.md): contrato de execução, `roberty.json`, gatilhos e webhook.
93
+
94
+ ## Licença
95
+
96
+ MIT. Copyright © Roberty Automation. Todos os direitos reservados.
@@ -0,0 +1,69 @@
1
+ # roberty-code
2
+
3
+ SDK oficial para **Code Projects** da [Roberty](https://roberty.app) em Python. Leia argumentos, escreva o resultado e descubra o gatilho da execução sem lidar com variáveis de ambiente.
4
+
5
+ ```bash
6
+ pip install roberty-code
7
+ ```
8
+
9
+ Python ≥ 3.9. Sem dependências. Tipado.
10
+
11
+ ## Uso
12
+
13
+ ```python
14
+ from datetime import datetime, timezone
15
+ import roberty_code as roberty
16
+
17
+ print(f"Iniciando ({roberty.environment}, gatilho: {roberty.trigger})")
18
+
19
+ cpf = roberty.input("cpf") # um argumento
20
+ args = roberty.inputs() # todos os argumentos (ou None)
21
+
22
+ # ... lógica do robô ...
23
+
24
+ roberty.output({"status": "ok", "processedAt": datetime.now(timezone.utc).isoformat()})
25
+ ```
26
+
27
+ Fora da Roberty o mesmo código roda: `inputs()` devolve `None`, `output()` avisa uma vez e devolve `False`.
28
+
29
+ ## API
30
+
31
+ | Membro | Retorno | Descrição |
32
+ | --- | --- | --- |
33
+ | `roberty.trigger` | `str \| None` | `manual`, `schedule`, `webhook` ou `evaluation` |
34
+ | `roberty.environment` | `str \| None` | `production` ou `preview` |
35
+ | `roberty.is_roberty` | `bool` | `True` quando roda na Roberty |
36
+ | `roberty.inputs()` | `Any \| None` | Todos os argumentos (`webhook`/`evaluation`) |
37
+ | `roberty.input(name, default=None)` | `Any` | Um argumento específico |
38
+ | `roberty.output(value)` | `bool` | Grava o resultado (JSON) lido pela plataforma |
39
+ | `roberty.exception()` | `ExceptionDetails \| None` | `{"message", "stack", "exitCode"}` dentro do `exceptionHandler` |
40
+ | `roberty.run(handler, exit_on_complete=True)` | `Any` | **Opcional**: executa `handler(roberty)`, grava o retorno e encerra com exit code `0`/`1`. Aceita `async def` |
41
+ | `roberty.ENV_VARS` | `dict` | Nomes das variáveis `roberty-*` |
42
+
43
+ Erros: `RobertyArgsError` (arquivo de argumentos com JSON inválido) e `RobertyOutputError` (valor não serializável; converta `datetime` com `.isoformat()`).
44
+
45
+ ### `exceptionHandler`
46
+
47
+ ```python
48
+ # on_error.py — declarado em roberty.json como "exceptionHandler": "on_error.py"
49
+ import roberty_code as roberty
50
+
51
+ error = roberty.exception()
52
+ print(f"Robô falhou (exit code {error['exitCode']}): {error['message']}")
53
+ ```
54
+
55
+ ### Atalho `run`
56
+
57
+ ```python
58
+ roberty.run(lambda r: {"status": "ok", "cpf": r.input("cpf")})
59
+ ```
60
+
61
+ Encerra o processo ao terminar; passe `exit_on_complete=False` para embutir em testes.
62
+
63
+ ## Documentação
64
+
65
+ [Guia de Code Projects](https://github.com/robertyautomation/code/blob/main/roberty-code-project-guide.md): contrato de execução, `roberty.json`, gatilhos e webhook.
66
+
67
+ ## Licença
68
+
69
+ MIT. Copyright © Roberty Automation. Todos os direitos reservados.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "roberty-code"
7
+ version = "0.1.0"
8
+ description = "SDK oficial para Code Projects da Roberty: leia argumentos, escreva o resultado e descubra o gatilho da execução."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Roberty Automation" }]
13
+ requires-python = ">=3.9"
14
+ dependencies = []
15
+ keywords = ["roberty", "rpa", "automation", "code-project", "sdk"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Libraries",
28
+ "Typing :: Typed",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/robertyautomation/code-sdk/tree/main/packages/python#readme"
33
+ Repository = "https://github.com/robertyautomation/code-sdk"
34
+ Issues = "https://github.com/robertyautomation/code-sdk/issues"
35
+ Documentation = "https://github.com/robertyautomation/code/blob/main/roberty-code-project-guide.md"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/roberty_code"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
42
+
43
+ [tool.ruff]
44
+ line-length = 100
45
+ target-version = "py39"
46
+
47
+ [tool.ruff.lint]
48
+ select = ["E", "F", "W", "I", "UP", "B"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
@@ -0,0 +1,52 @@
1
+ """
2
+ SDK oficial para Code Projects da Roberty.
3
+
4
+ import roberty_code as roberty
5
+
6
+ cpf = roberty.input("cpf")
7
+ roberty.output({"status": "ok"})
8
+ """
9
+
10
+ from typing import TYPE_CHECKING, Any, Optional
11
+
12
+ from . import context as _context
13
+ from ._env import ENV_VARS
14
+ from .errors import RobertyArgsError, RobertyOutputError
15
+ from .exception import ExceptionDetails, exception
16
+ from .inputs import input, inputs # noqa: A004
17
+ from .output import output
18
+ from .run import run
19
+
20
+ __all__ = [
21
+ "ENV_VARS",
22
+ "ExceptionDetails",
23
+ "RobertyArgsError",
24
+ "RobertyOutputError",
25
+ "environment",
26
+ "exception",
27
+ "input",
28
+ "inputs",
29
+ "is_roberty",
30
+ "output",
31
+ "run",
32
+ "trigger",
33
+ ]
34
+
35
+ if TYPE_CHECKING:
36
+ trigger: Optional[str]
37
+ environment: Optional[str]
38
+ is_roberty: bool
39
+
40
+ _DYNAMIC = {
41
+ "trigger": _context.trigger,
42
+ "environment": _context.environment,
43
+ "is_roberty": _context.is_roberty,
44
+ }
45
+
46
+
47
+ def __getattr__(name: str) -> Any:
48
+ """`trigger`, `environment` e `is_roberty` são lidos da env a cada acesso."""
49
+ getter = _DYNAMIC.get(name)
50
+ if getter is None:
51
+ raise AttributeError(f"module 'roberty_code' has no attribute '{name}'")
52
+ return getter()
@@ -0,0 +1,24 @@
1
+ import json
2
+ import os
3
+ from typing import Any
4
+
5
+ from ._env import ENV_VARS
6
+ from .errors import RobertyArgsError
7
+
8
+ _cache: dict[str, Any] = {}
9
+
10
+
11
+ def read_args_file() -> Any:
12
+ """Lê (com cache) o JSON apontado por `roberty-args-file-path`; `None` sem a variável."""
13
+ file_path = os.environ.get(ENV_VARS["args_file_path"])
14
+ if not file_path:
15
+ return None
16
+ if file_path in _cache:
17
+ return _cache[file_path]
18
+ try:
19
+ with open(file_path, encoding="utf-8") as file:
20
+ parsed = json.load(file)
21
+ except (OSError, ValueError) as cause:
22
+ raise RobertyArgsError(file_path, cause) from cause
23
+ _cache[file_path] = parsed
24
+ return parsed
@@ -0,0 +1,8 @@
1
+ """Nomes das variáveis de ambiente injetadas pela Roberty."""
2
+
3
+ ENV_VARS = {
4
+ "environment": "roberty-environment",
5
+ "trigger": "roberty-trigger",
6
+ "args_file_path": "roberty-args-file-path",
7
+ "output_file_path": "roberty-output-file-path",
8
+ }
@@ -0,0 +1,19 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ from ._env import ENV_VARS
5
+
6
+
7
+ def trigger() -> Optional[str]:
8
+ """`manual`, `schedule`, `webhook` ou `evaluation`. `None` fora da Roberty."""
9
+ return os.environ.get(ENV_VARS["trigger"])
10
+
11
+
12
+ def environment() -> Optional[str]:
13
+ """`production` ou `preview`. `None` fora da Roberty."""
14
+ return os.environ.get(ENV_VARS["environment"])
15
+
16
+
17
+ def is_roberty() -> bool:
18
+ """`True` quando o processo roda na Roberty."""
19
+ return bool(os.environ.get(ENV_VARS["trigger"]))
@@ -0,0 +1,15 @@
1
+ class RobertyArgsError(Exception):
2
+ """O arquivo de argumentos existe mas não pôde ser lido como JSON."""
3
+
4
+ def __init__(self, file_path: str, cause: BaseException) -> None:
5
+ super().__init__(
6
+ f'Roberty: não foi possível ler os argumentos em "{file_path}": {cause}'
7
+ )
8
+ self.file_path = file_path
9
+
10
+
11
+ class RobertyOutputError(Exception):
12
+ """O resultado não pôde ser serializado como JSON."""
13
+
14
+ def __init__(self, cause: BaseException) -> None:
15
+ super().__init__(f"Roberty: o resultado não pôde ser serializado como JSON: {cause}")
@@ -0,0 +1,22 @@
1
+ from typing import Optional, TypedDict
2
+
3
+ from ._args_file import read_args_file
4
+
5
+
6
+ class ExceptionDetails(TypedDict):
7
+ message: str
8
+ stack: Optional[str]
9
+ exitCode: int
10
+
11
+
12
+ def exception() -> Optional[ExceptionDetails]:
13
+ """Detalhes do erro dentro do `exceptionHandler`. `None` fora dele."""
14
+ details = read_args_file()
15
+ if not isinstance(details, dict):
16
+ return None
17
+ stack = details.get("stack")
18
+ return ExceptionDetails(
19
+ message=str(details.get("message", "")),
20
+ stack=stack if isinstance(stack, str) else None,
21
+ exitCode=int(details.get("exitCode", 1)),
22
+ )
@@ -0,0 +1,22 @@
1
+ from typing import Any, Optional, TypeVar, Union, overload
2
+
3
+ from ._args_file import read_args_file
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ def inputs() -> Optional[Any]:
9
+ """Todos os argumentos da execução (`webhook`/`evaluation`). `None` em `manual`/`schedule`."""
10
+ return read_args_file()
11
+
12
+
13
+ @overload
14
+ def input(name: str) -> Optional[Any]: ... # noqa: A001
15
+ @overload
16
+ def input(name: str, default: T) -> Union[Any, T]: ... # noqa: A001
17
+ def input(name: str, default: Any = None) -> Any: # noqa: A001
18
+ """Um argumento específico. `default` quando não existe ou não há argumentos."""
19
+ args = read_args_file()
20
+ if not isinstance(args, dict):
21
+ return default
22
+ return args.get(name, default)
@@ -0,0 +1,47 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ from typing import Any
5
+
6
+ from ._env import ENV_VARS
7
+ from .errors import RobertyOutputError
8
+
9
+ OUTPUT_LIMIT_BYTES = 256 * 1024
10
+
11
+ _state = {"warned_missing_path": False}
12
+
13
+
14
+ def output(value: Any) -> bool:
15
+ """Grava `value` como JSON no arquivo de resultado; `False` fora da Roberty (aviso único)."""
16
+ serialized = _serialize(value)
17
+ file_path = os.environ.get(ENV_VARS["output_file_path"])
18
+ if not file_path:
19
+ _warn_missing_path_once()
20
+ return False
21
+ if len(serialized.encode("utf-8")) > OUTPUT_LIMIT_BYTES:
22
+ print(
23
+ "Roberty: resultado acima de 256 KB; "
24
+ "em execuções webhook a plataforma corta o excedente.",
25
+ file=sys.stderr,
26
+ )
27
+ with open(file_path, "w", encoding="utf-8") as file:
28
+ file.write(serialized)
29
+ return True
30
+
31
+
32
+ def _serialize(value: Any) -> str:
33
+ try:
34
+ return json.dumps(value, ensure_ascii=False)
35
+ except (TypeError, ValueError) as cause:
36
+ raise RobertyOutputError(cause) from cause
37
+
38
+
39
+ def _warn_missing_path_once() -> None:
40
+ if _state["warned_missing_path"]:
41
+ return
42
+ _state["warned_missing_path"] = True
43
+ print(
44
+ f'Roberty: "{ENV_VARS["output_file_path"]}" não definida; '
45
+ "o resultado não foi gravado (execução fora da Roberty).",
46
+ file=sys.stderr,
47
+ )
File without changes
@@ -0,0 +1,36 @@
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ import sys
5
+ import traceback
6
+ from typing import Any, Callable
7
+
8
+ from .context import is_roberty
9
+ from .output import output
10
+
11
+
12
+ def run(handler: Callable[[Any], Any], exit_on_complete: bool = True) -> Any:
13
+ """
14
+ Atalho opcional: executa `handler(roberty)`, grava o retorno como resultado e encerra
15
+ com exit code 0/1. Fora da Roberty imprime o resultado no stdout. Aceita `async def`.
16
+ """
17
+ import roberty_code
18
+
19
+ try:
20
+ result = handler(roberty_code)
21
+ if inspect.isawaitable(result):
22
+ result = asyncio.run(result)
23
+ if result is not None:
24
+ output(result)
25
+ if not is_roberty():
26
+ print(json.dumps(result, indent=2, ensure_ascii=False))
27
+ if exit_on_complete:
28
+ sys.exit(0)
29
+ return result
30
+ except SystemExit:
31
+ raise
32
+ except BaseException:
33
+ traceback.print_exc()
34
+ if exit_on_complete:
35
+ sys.exit(1)
36
+ raise
@@ -0,0 +1,154 @@
1
+ import importlib
2
+ import json
3
+ import subprocess
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ import roberty_code as roberty
9
+
10
+ output_module = importlib.import_module("roberty_code.output")
11
+
12
+ ENV = [
13
+ "roberty-environment",
14
+ "roberty-trigger",
15
+ "roberty-args-file-path",
16
+ "roberty-output-file-path",
17
+ ]
18
+
19
+
20
+ @pytest.fixture(autouse=True)
21
+ def clean_env(monkeypatch):
22
+ for key in ENV:
23
+ monkeypatch.delenv(key, raising=False)
24
+ output_module._state["warned_missing_path"] = False
25
+
26
+
27
+ @pytest.fixture
28
+ def write_args(tmp_path, monkeypatch):
29
+ def _write(value):
30
+ file = tmp_path / "args.json"
31
+ file.write_text(value if isinstance(value, str) else json.dumps(value), encoding="utf-8")
32
+ monkeypatch.setenv("roberty-args-file-path", str(file))
33
+ return str(file)
34
+
35
+ return _write
36
+
37
+
38
+ @pytest.fixture
39
+ def output_file(tmp_path, monkeypatch):
40
+ file = tmp_path / "out.json"
41
+ monkeypatch.setenv("roberty-output-file-path", str(file))
42
+ return file
43
+
44
+
45
+ class TestContext:
46
+ def test_reads_env(self, monkeypatch):
47
+ monkeypatch.setenv("roberty-trigger", "webhook")
48
+ monkeypatch.setenv("roberty-environment", "preview")
49
+ assert roberty.trigger == "webhook"
50
+ assert roberty.environment == "preview"
51
+ assert roberty.is_roberty is True
52
+
53
+ def test_none_outside_roberty(self):
54
+ assert roberty.trigger is None
55
+ assert roberty.environment is None
56
+ assert roberty.is_roberty is False
57
+
58
+ def test_unknown_attribute(self):
59
+ with pytest.raises(AttributeError):
60
+ roberty.nope # noqa: B018
61
+
62
+
63
+ class TestInputs:
64
+ def test_all_and_single(self, write_args):
65
+ write_args({"cpf": "123", "tries": 2})
66
+ assert roberty.inputs() == {"cpf": "123", "tries": 2}
67
+ assert roberty.input("cpf") == "123"
68
+ assert roberty.input("missing") is None
69
+ assert roberty.input("missing", "x") == "x"
70
+
71
+ def test_without_args(self):
72
+ assert roberty.inputs() is None
73
+ assert roberty.input("cpf") is None
74
+ assert roberty.input("cpf", 1) == 1
75
+
76
+ def test_invalid_json(self, write_args):
77
+ file = write_args("{ not json")
78
+ with pytest.raises(roberty.RobertyArgsError) as error:
79
+ roberty.inputs()
80
+ assert file in str(error.value)
81
+
82
+ def test_list_returned_as_is(self, write_args):
83
+ write_args([1, 2])
84
+ assert roberty.inputs() == [1, 2]
85
+ assert roberty.input("0") is None
86
+
87
+
88
+ class TestException:
89
+ def test_details(self, write_args):
90
+ write_args({"message": "boom", "stack": None, "exitCode": 3})
91
+ assert roberty.exception() == {"message": "boom", "stack": None, "exitCode": 3}
92
+
93
+ def test_none_outside_handler(self):
94
+ assert roberty.exception() is None
95
+
96
+
97
+ class TestOutput:
98
+ def test_writes_json(self, output_file):
99
+ assert roberty.output({"ok": True, "nome": "João"}) is True
100
+ assert json.loads(output_file.read_text(encoding="utf-8")) == {"ok": True, "nome": "João"}
101
+
102
+ def test_none_and_overwrite(self, output_file):
103
+ roberty.output({"a": 1})
104
+ roberty.output(None)
105
+ assert output_file.read_text(encoding="utf-8") == "null"
106
+
107
+ def test_false_and_single_warning(self, capsys):
108
+ assert roberty.output({}) is False
109
+ assert roberty.output({}) is False
110
+ assert capsys.readouterr().err.count("roberty-output-file-path") == 1
111
+
112
+ def test_warns_above_limit_but_writes(self, output_file, capsys):
113
+ assert roberty.output({"big": "x" * (300 * 1024)}) is True
114
+ assert "256 KB" in capsys.readouterr().err
115
+
116
+ def test_unserializable(self, output_file):
117
+ with pytest.raises(roberty.RobertyOutputError):
118
+ roberty.output({"when": object()})
119
+
120
+
121
+ class TestRun:
122
+ def test_writes_and_returns(self, output_file, monkeypatch):
123
+ monkeypatch.setenv("roberty-trigger", "manual")
124
+ result = roberty.run(lambda r: {"trigger": r.trigger}, exit_on_complete=False)
125
+ assert result == {"trigger": "manual"}
126
+ assert json.loads(output_file.read_text(encoding="utf-8")) == {"trigger": "manual"}
127
+
128
+ def test_async_handler(self, output_file, monkeypatch):
129
+ monkeypatch.setenv("roberty-trigger", "manual")
130
+
131
+ async def handler(r):
132
+ return {"ok": 1}
133
+
134
+ assert roberty.run(handler, exit_on_complete=False) == {"ok": 1}
135
+
136
+ def test_prints_outside_roberty(self, capsys):
137
+ roberty.run(lambda r: {"ok": 1}, exit_on_complete=False)
138
+ assert json.dumps({"ok": 1}, indent=2) in capsys.readouterr().out
139
+
140
+ def test_logs_and_reraises(self, capsys):
141
+ def handler(r):
142
+ raise ValueError("boom")
143
+
144
+ with pytest.raises(ValueError):
145
+ roberty.run(handler, exit_on_complete=False)
146
+ assert "boom" in capsys.readouterr().err
147
+
148
+ def test_exit_codes_in_subprocess(self, tmp_path):
149
+ ok = tmp_path / "ok.py"
150
+ ok.write_text("import roberty_code as r\nr.run(lambda _: {'ok': 1})\n", encoding="utf-8")
151
+ fail = tmp_path / "fail.py"
152
+ fail.write_text("import roberty_code as r\nr.run(lambda _: 1 / 0)\n", encoding="utf-8")
153
+ assert subprocess.run([sys.executable, str(ok)], capture_output=True).returncode == 0
154
+ assert subprocess.run([sys.executable, str(fail)], capture_output=True).returncode == 1