feat(int-autentique): add Autentique e-signature integration skill#97
feat(int-autentique): add Autentique e-signature integration skill#97mt-alarcon wants to merge 1 commit into
Conversation
Autentique (BR e-signature, GraphQL v2) integration with:
- whoami, documents list/get/create/download/delete
- multipart/form-data upload (GraphQL multipart spec)
- sandbox mode (no credit consumption), reminder cadence, ICP-Brasil qualified signatures
- standardized smoke command (exit 0 + JSON {overall, steps[]})
- 31 unit tests (network-blocked, mock-based)
Token via AUTENTIQUE_API_TOKEN env. stdlib-only (urllib).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer's GuideAdds a new stdlib-only Python CLI skill Sequence diagram for Autentique document creation commandsequenceDiagram
actor User
participant CLI as AutentiqueCLI
participant Client as cmd_documents_create
participant Transport as gql_upload
participant Autentique as AutentiqueAPI
User->>CLI: run documents create
CLI->>Client: main parses args
Client->>Client: json.loads signers
Client->>Client: build document_input and variables
Client->>Client: read AUTENTIQUE_ORGANIZATION_ID
Client->>Transport: gql_upload(query, variables, file)
Transport->>Transport: _token reads AUTENTIQUE_API_TOKEN
Transport->>Autentique: POST multipart/form-data
Autentique-->>Transport: GraphQL response
Transport-->>Client: data
Client-->>CLI: output(data)
CLI-->>User: JSON result
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The
_load_dotenvhelper assumes a fixed directory depth (parents[4]) to find the project.env, which is brittle if the repo layout changes; consider either resolving from a known anchor (e.g..clauderoot) or allowing an explicitENV_PATHoverride instead of hardcoding the parent count. cmd_smokeonly wrapsgqlcalls in a genericexcept Exception, so failures that triggersys.exitinsidegql(e.g. HTTP 5xx / GraphQL errors) will terminate with a non-zero status and bypass the intended 'always exit 0 with JSON' contract; you may want to catchSystemExitaroundgqlcalls and convert them intoFAILsteps instead.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `_load_dotenv` helper assumes a fixed directory depth (`parents[4]`) to find the project `.env`, which is brittle if the repo layout changes; consider either resolving from a known anchor (e.g. `.claude` root) or allowing an explicit `ENV_PATH` override instead of hardcoding the parent count.
- `cmd_smoke` only wraps `gql` calls in a generic `except Exception`, so failures that trigger `sys.exit` inside `gql` (e.g. HTTP 5xx / GraphQL errors) will terminate with a non-zero status and bypass the intended 'always exit 0 with JSON' contract; you may want to catch `SystemExit` around `gql` calls and convert them into `FAIL` steps instead.
## Individual Comments
### Comment 1
<location path=".claude/skills/int-autentique/scripts/autentique_client.py" line_range="346-355" />
<code_context>
+def cmd_smoke(args):
</code_context>
<issue_to_address>
**issue (bug_risk):** The `smoke` command is structured as if `gql` could raise, but `gql` currently exits the process instead of propagating errors.
Because `gql` prints JSON and calls `sys.exit(2)` on errors, the process ends before your `try/except` and `steps` reporting logic can run, so smoke JSON is never produced on failures. To make this work, `gql` needs to raise or return structured errors instead of exiting, so `cmd_smoke` can capture them, emit the smoke report, and still control the final exit code (e.g. remain 0).
</issue_to_address>
### Comment 2
<location path=".claude/skills/int-autentique/tests/test_autentique_client.py" line_range="322-331" />
<code_context>
+class TestCmdDocumentsDownload:
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for default output filename and download error handling in `cmd_documents_download`
Current tests cover only the missing-URL case and a successful download with an explicit `--output` path. Please also add (1) a test that omits `output` and asserts the created filename matches `<name>.<version>.pdf` (or `<id>.<version>.pdf`), and (2) a test where `urllib.request.urlopen` raises, asserting the JSON error payload (`{"error": "Falha ao baixar", ...}`) and exit code 2. This will exercise the remaining download paths.
Suggested implementation:
```python
class TestCmdDocumentsDownload:
=======
# cmd_documents_download
# ===========================================================================
class TestCmdDocumentsDownload:
def test_nome_arquivo_padrao_usa_nome_e_versao(self, monkeypatch, tmp_path, aut):
"""Quando --output é omitido → usa `<name>.<version>.pdf` (ou `<id>.<version>.pdf`) como nome do arquivo."""
import io
import json
from pathlib import Path
from types import SimpleNamespace
import urllib.error
# payload semelhante ao usado em outros testes de download, mas com URL presente
payload = {
"document": {
"id": "doc1",
"name": "Distrato",
"files": {
"original": "https://exemplo.com/documento.pdf",
"signed": "https://exemplo.com/documento-assinado.pdf",
},
}
}
# preparar stdin com o payload
monkeypatch.setattr(
"sys.stdin",
io.StringIO(json.dumps(payload)),
)
monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")
# trabalhar em diretório temporário
monkeypatch.chdir(tmp_path)
# simular resposta do urlopen
class _FakeResponse:
def __init__(self, data: bytes):
self._data = data
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self) -> bytes:
return self._data
def fake_urlopen(url, *args, **kwargs):
# deve ser chamada com a URL do arquivo
assert url == "https://exemplo.com/documento-assinado.pdf"
return _FakeResponse(b"%PDF-1.4\n...")
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
# sem --output → deve usar `<name>.<version>.pdf` (ou `<id>.<version>.pdf`)
args = SimpleNamespace(
id="doc1",
version="1",
output=None,
sandbox=True,
)
aut.cmd_documents_download(args)
# nome preferencialmente com `name`, senão com `id`
expected_by_name = tmp_path / "Distrato.1.pdf"
expected_by_id = tmp_path / "doc1.1.pdf"
assert expected_by_name.is_file() or expected_by_id.is_file()
def test_erro_ao_baixar_emite_json_e_exit_code_2(self, monkeypatch, capsys, tmp_path, aut):
"""Quando urlopen levanta exceção → imprime JSON de erro e sai com código 2."""
import io
import json
from types import SimpleNamespace
import urllib.error
payload = {
"document": {
"id": "doc1",
"name": "Distrato",
"files": {
"original": "https://exemplo.com/documento.pdf",
"signed": "https://exemplo.com/documento-assinado.pdf",
},
}
}
monkeypatch.setattr(
"sys.stdin",
io.StringIO(json.dumps(payload)),
)
monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")
monkeypatch.chdir(tmp_path)
def failing_urlopen(url, *args, **kwargs):
raise urllib.error.URLError("boom")
monkeypatch.setattr("urllib.request.urlopen", failing_urlopen)
args = SimpleNamespace(
id="doc1",
version="1",
output=None,
sandbox=True,
)
with pytest.raises(SystemExit) as excinfo:
aut.cmd_documents_download(args)
assert excinfo.value.code == 2
captured = capsys.readouterr()
# saída deve ser um JSON com a chave "error" e mensagem "Falha ao baixar"
data = json.loads(captured.out)
assert data.get("error") == "Falha ao baixar"
```
1. Estes testes assumem que:
- Existe um fixture `aut` com o método `cmd_documents_download(args)`, similar ao usado em `cmd_documents_create`.
- `cmd_documents_download` lê um JSON de `sys.stdin` no formato usado em `test_url_ausente_exits2`.
- Quando `urllib.request.urlopen` lança uma exceção, `cmd_documents_download` captura o erro, escreve um JSON com `{"error": "Falha ao baixar", ...}` em `stdout` e encerra com `SystemExit(2)`.
2. Caso a função `cmd_documents_download` use outros nomes de argumentos (por exemplo, `document_id` em vez de `id`, ou `version_number` em vez de `version`), ajuste o `SimpleNamespace` nos testes para refletir a assinatura real.
3. Se o comando não estiver a ser chamado via o fixture `aut`, mas sim por uma função global (por ex. `cmd_documents_download(args)` importada diretamente), adapte as chamadas nos testes para usar o ponto de entrada correto.
4. Os testes usam `pytest` explicitamente (`pytest.raises`); se ainda não estiver importado neste arquivo, adicione `import pytest` ao topo do módulo.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def cmd_smoke(args): | ||
| import time as _time | ||
| overall = "PASS" | ||
| steps = [] | ||
| t_total = _time.monotonic() | ||
| try: | ||
| # step 1: auth — verifica token e retorna dados do usuário (reutiliza whoami) | ||
| t0 = _time.monotonic() | ||
| try: | ||
| token_val = os.environ.get("AUTENTIQUE_API_TOKEN", "") |
There was a problem hiding this comment.
issue (bug_risk): The smoke command is structured as if gql could raise, but gql currently exits the process instead of propagating errors.
Because gql prints JSON and calls sys.exit(2) on errors, the process ends before your try/except and steps reporting logic can run, so smoke JSON is never produced on failures. To make this work, gql needs to raise or return structured errors instead of exiting, so cmd_smoke can capture them, emit the smoke report, and still control the final exit code (e.g. remain 0).
| class TestCmdDocumentsDownload: | ||
| def test_url_ausente_exits2(self, monkeypatch, capsys, tmp_path): | ||
| """Se URL da versão solicitada não estiver disponível → error, exit 2.""" | ||
| payload = { | ||
| "document": { | ||
| "id": "doc1", | ||
| "name": "Distrato", | ||
| "files": {"original": "http://x.com/orig.pdf", "signed": None}, | ||
| } | ||
| } |
There was a problem hiding this comment.
suggestion (testing): Add tests for default output filename and download error handling in cmd_documents_download
Current tests cover only the missing-URL case and a successful download with an explicit --output path. Please also add (1) a test that omits output and asserts the created filename matches <name>.<version>.pdf (or <id>.<version>.pdf), and (2) a test where urllib.request.urlopen raises, asserting the JSON error payload ({"error": "Falha ao baixar", ...}) and exit code 2. This will exercise the remaining download paths.
Suggested implementation:
class TestCmdDocumentsDownload:
=======
# cmd_documents_download
# ===========================================================================
class TestCmdDocumentsDownload:
def test_nome_arquivo_padrao_usa_nome_e_versao(self, monkeypatch, tmp_path, aut):
"""Quando --output é omitido → usa `<name>.<version>.pdf` (ou `<id>.<version>.pdf`) como nome do arquivo."""
import io
import json
from pathlib import Path
from types import SimpleNamespace
import urllib.error
# payload semelhante ao usado em outros testes de download, mas com URL presente
payload = {
"document": {
"id": "doc1",
"name": "Distrato",
"files": {
"original": "https://exemplo.com/documento.pdf",
"signed": "https://exemplo.com/documento-assinado.pdf",
},
}
}
# preparar stdin com o payload
monkeypatch.setattr(
"sys.stdin",
io.StringIO(json.dumps(payload)),
)
monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")
# trabalhar em diretório temporário
monkeypatch.chdir(tmp_path)
# simular resposta do urlopen
class _FakeResponse:
def __init__(self, data: bytes):
self._data = data
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self) -> bytes:
return self._data
def fake_urlopen(url, *args, **kwargs):
# deve ser chamada com a URL do arquivo
assert url == "https://exemplo.com/documento-assinado.pdf"
return _FakeResponse(b"%PDF-1.4\n...")
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
# sem --output → deve usar `<name>.<version>.pdf` (ou `<id>.<version>.pdf`)
args = SimpleNamespace(
id="doc1",
version="1",
output=None,
sandbox=True,
)
aut.cmd_documents_download(args)
# nome preferencialmente com `name`, senão com `id`
expected_by_name = tmp_path / "Distrato.1.pdf"
expected_by_id = tmp_path / "doc1.1.pdf"
assert expected_by_name.is_file() or expected_by_id.is_file()
def test_erro_ao_baixar_emite_json_e_exit_code_2(self, monkeypatch, capsys, tmp_path, aut):
"""Quando urlopen levanta exceção → imprime JSON de erro e sai com código 2."""
import io
import json
from types import SimpleNamespace
import urllib.error
payload = {
"document": {
"id": "doc1",
"name": "Distrato",
"files": {
"original": "https://exemplo.com/documento.pdf",
"signed": "https://exemplo.com/documento-assinado.pdf",
},
}
}
monkeypatch.setattr(
"sys.stdin",
io.StringIO(json.dumps(payload)),
)
monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")
monkeypatch.chdir(tmp_path)
def failing_urlopen(url, *args, **kwargs):
raise urllib.error.URLError("boom")
monkeypatch.setattr("urllib.request.urlopen", failing_urlopen)
args = SimpleNamespace(
id="doc1",
version="1",
output=None,
sandbox=True,
)
with pytest.raises(SystemExit) as excinfo:
aut.cmd_documents_download(args)
assert excinfo.value.code == 2
captured = capsys.readouterr()
# saída deve ser um JSON com a chave "error" e mensagem "Falha ao baixar"
data = json.loads(captured.out)
assert data.get("error") == "Falha ao baixar"-
Estes testes assumem que:
- Existe um fixture
autcom o métodocmd_documents_download(args), similar ao usado emcmd_documents_create. cmd_documents_downloadlê um JSON desys.stdinno formato usado emtest_url_ausente_exits2.- Quando
urllib.request.urlopenlança uma exceção,cmd_documents_downloadcaptura o erro, escreve um JSON com{"error": "Falha ao baixar", ...}emstdoute encerra comSystemExit(2).
- Existe um fixture
-
Caso a função
cmd_documents_downloaduse outros nomes de argumentos (por exemplo,document_idem vez deid, ouversion_numberem vez deversion), ajuste oSimpleNamespacenos testes para refletir a assinatura real. -
Se o comando não estiver a ser chamado via o fixture
aut, mas sim por uma função global (por ex.cmd_documents_download(args)importada diretamente), adapte as chamadas nos testes para usar o ponto de entrada correto. -
Os testes usam
pytestexplicitamente (pytest.raises); se ainda não estiver importado neste arquivo, adicioneimport pytestao topo do módulo.
What
Adds a new integration skill int-autentique — wrapper for Autentique (Brazilian e-signature platform, GraphQL v2 API).
Why
Autentique is a widely-used BR e-signature provider (legally backed by MP 2.200-2/2001 + Lei 14.063/2020). This gives EvoNexus a first-class way to send contracts, NDAs, proposals, and termination agreements for formal signature instead of relying on scanned PDFs.
Commands
whoami— authenticated accountdocuments list / get / create / download / deletecreatesupports multipart/form-data upload (GraphQL multipart spec), sandbox mode (no credit consumption), reminder cadence, and ICP-Brasil qualified signaturessmoke— standardized health-check (always exit 0 + JSON{overall, steps[], duration_ms})Implementation notes
urllib), no third-party depsAUTENTIQUE_API_TOKENenv var (no hardcoded secrets){error, details}+ exit codes (0 ok / 1 usage / 2 API) on failureTests
31 unit tests, network-blocked (mock-based) —
pytest tests/→ 31 passed.🤖 Generated with Claude Code
Summary by Sourcery
Add a new Autentique e-signature integration skill with a Python CLI client, documentation, and comprehensive tests.
New Features:
Enhancements:
Documentation:
Tests: