From 69d23106c0278b9c842314f9abc56a5b8cc6b0ec Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 13:39:39 -0300 Subject: [PATCH 1/2] chore: adicionar scripts/sandbox e .env.example para integradores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/sandbox/ — 4 scripts de integração manual contra sandbox: 01_list_collections, 02_create_notarial_draft, 03_consult_envelope, 04_activate_optional + _config.py com loader de .env e PDF mínimo - scripts/sandbox/.env.example — template de variáveis (API key, environment) - .env.example na raiz — ponto de entrada para integradores novos - .gitignore: exclui scripts/sandbox/.env e .last_run.json (dados locais) Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 5 ++ .gitignore | 2 + scripts/sandbox/.env.example | 2 + scripts/sandbox/01_list_collections.py | 59 ++++++++++++++ scripts/sandbox/02_create_notarial_draft.py | 89 +++++++++++++++++++++ scripts/sandbox/03_consult_envelope.py | 70 ++++++++++++++++ scripts/sandbox/04_activate_optional.py | 79 ++++++++++++++++++ scripts/sandbox/README.md | 35 ++++++++ scripts/sandbox/_config.py | 70 ++++++++++++++++ 9 files changed, 411 insertions(+) create mode 100644 .env.example create mode 100644 scripts/sandbox/.env.example create mode 100644 scripts/sandbox/01_list_collections.py create mode 100644 scripts/sandbox/02_create_notarial_draft.py create mode 100644 scripts/sandbox/03_consult_envelope.py create mode 100644 scripts/sandbox/04_activate_optional.py create mode 100644 scripts/sandbox/README.md create mode 100644 scripts/sandbox/_config.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..69095c0 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Copie para .env e preencha com seus valores +# Obtenha o token em: https://app.clicksign.com/configuracoes/integracoes + +CLICKSIGN_API_KEY=seu-access-token-aqui +CLICKSIGN_ENVIRONMENT=sandbox # sandbox | production diff --git a/.gitignore b/.gitignore index 43e5f70..fc1e837 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ htmlcov/ .claude/ .idea/ docs/CONSIDERACOES.md +scripts/sandbox/.env +scripts/sandbox/.last_run.json diff --git a/scripts/sandbox/.env.example b/scripts/sandbox/.env.example new file mode 100644 index 0000000..85797b7 --- /dev/null +++ b/scripts/sandbox/.env.example @@ -0,0 +1,2 @@ +CLICKSIGN_API_KEY=seu-access-token-aqui +CLICKSIGN_ENVIRONMENT=sandbox diff --git a/scripts/sandbox/01_list_collections.py b/scripts/sandbox/01_list_collections.py new file mode 100644 index 0000000..174d852 --- /dev/null +++ b/scripts/sandbox/01_list_collections.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Lista collections principais no sandbox (somente leitura).""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) + +from _config import get_client # noqa: E402 + + +def _print_block(title: str, items: list, limit: int = 5) -> None: + print(f"\n=== {title} ({len(items)} total, mostrando até {limit}) ===") + for item in items[:limit]: + label = getattr(item, "name", None) or getattr(item, "email", None) or item.id + extra = getattr(item, "status", None) + suffix = f" [{extra}]" if extra else "" + print(f" - {item.id} {label}{suffix}") + if len(items) > limit: + print(f" ... +{len(items) - limit} itens") + + +def main() -> None: + import os + + from clicksign.configuration import _ENVIRONMENTS + + client = get_client() + env = os.environ.get("CLICKSIGN_ENVIRONMENT", "sandbox") + print(f"Ambiente: {env} → {_ENVIRONMENTS.get(env, env)}") + + envelopes = client.notarial.envelopes.list() + _print_block("Envelopes", envelopes) + + try: + folders = client.folders.list() + _print_block("Folders", folders) + except Exception as exc: + print(f"\n=== Folders (erro: {exc}) ===") + + try: + webhooks = client.webhooks.list() + _print_block("Webhooks", webhooks) + except Exception as exc: + print(f"\n=== Webhooks (erro: {exc}) ===") + + try: + users = client.users.list() + _print_block("Users", users) + except Exception as exc: + print(f"\n=== Users (erro: {exc}) ===") + + print("\nOK — consultas concluídas.") + + +if __name__ == "__main__": + main() diff --git a/scripts/sandbox/02_create_notarial_draft.py b/scripts/sandbox/02_create_notarial_draft.py new file mode 100644 index 0000000..66aa5e2 --- /dev/null +++ b/scripts/sandbox/02_create_notarial_draft.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Cria envelope + documento + signatário + requisitos no sandbox (permanece em draft). + +Não ativa nem notifica — use 03_consult_envelope.py depois. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import UTC, datetime +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) + +from _config import get_client, pdf_content_base64, save_last_run # noqa: E402 + + +def main() -> None: + client = get_client() + stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") + suffix = uuid.uuid4().hex[:8] + + print("1/4 Criando envelope...") + envelope = client.notarial.envelopes.create( + name=f"SDK sandbox {stamp}", + locale="pt-BR", + auto_close=True, + ) + print(f" envelope.id = {envelope.id} status={envelope.status}") + + print("2/4 Criando documento...") + doc = client.notarial.documents.create( + envelope.id, + filename=f"sandbox-{stamp}.pdf", + content_base64=pdf_content_base64(), + ) + print(f" document.id = {doc.id} filename={doc.filename}") + + print("3/4 Criando signatário...") + email = f"sandbox+{suffix}@mailinator.com" + signer = client.notarial.signers.create( + envelope.id, + name="Maria Sandbox Silva", + email=email, + has_documentation=False, + ) + print(f" signer.id = {signer.id} email={email}") + + print("4/4 Criando requisitos (bulk)...") + bulk = client.notarial.bulk_requirements.create( + envelope.id, + block=lambda ops: ( + ops.add_agree(signer_id=signer.id, document_id=doc.id, role="sign"), + ops.add_provide_evidence( + signer_id=signer.id, + document_id=doc.id, + auth="email", + ), + ), + ) + if bulk.success(): + print(" bulk: sucesso") + else: + print(" bulk: falhas parciais") + for f in bulk.failures: + print(f" - slot {f.index} op={f.op} errors={f.errors}") + sys.exit(1) + + # Reconsulta via API + envelope2 = client.notarial.envelopes.retrieve(envelope.id) + print(f"\nEnvelope após bulk: status={envelope2.status}") + + save_last_run( + { + "envelope_id": envelope.id, + "document_id": doc.id, + "signer_id": signer.id, + } + ) + + print("\nPróximo passo:") + print(f" python scripts/sandbox/03_consult_envelope.py {envelope.id}") + print(" # ou sem argumento para usar .last_run.json") + + +if __name__ == "__main__": + main() diff --git a/scripts/sandbox/03_consult_envelope.py b/scripts/sandbox/03_consult_envelope.py new file mode 100644 index 0000000..d8b073a --- /dev/null +++ b/scripts/sandbox/03_consult_envelope.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Consulta envelope e listagens aninhadas no sandbox.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) + +from _config import get_client, load_last_run # noqa: E402 + + +def main() -> None: + envelope_id = (sys.argv[1] if len(sys.argv) > 1 else "").strip() + if not envelope_id: + envelope_id = load_last_run().get("envelope_id", "") + if not envelope_id: + print("Uso: python 03_consult_envelope.py ", file=sys.stderr) + sys.exit(1) + + client = get_client() + last = load_last_run() + + with client.use(): + print(f"=== Envelope {envelope_id} ===") + envelope = client.notarial.envelopes.retrieve(envelope_id) + print( + f" name={envelope.name!r} status={envelope.status!r} locale={envelope.locale!r}" + ) + + print("\n--- Documentos (envelope.list_documents) ---") + for doc in client.notarial.envelopes.list_documents(envelope_id): + print(f" {doc.id} filename={doc.filename!r} status={doc.status!r}") + + print("\n--- Signatários (envelope.list_signers) ---") + for signer in client.notarial.envelopes.list_signers(envelope_id): + print(f" {signer.id} name={signer.name!r} email={signer.email!r}") + + print("\n--- Requisitos (envelope.list_requirements) ---") + for req in client.notarial.envelopes.list_requirements(envelope_id): + print( + f" {req.id} action={req.action!r} role={getattr(req, 'role', None)!r}" + ) + + print("\n--- Document.list_for_envelope (atalho) ---") + for doc in client.notarial.documents.list_for_envelope(envelope_id): + print(f" Document.list_for_envelope → {doc.id}") + + print("\n--- Eventos do envelope ---") + events = client.notarial.envelopes.list_events(envelope_id) + print(f" {len(events)} evento(s)") + for ev in events[:5]: + print(f" {ev.id} name={getattr(ev, 'name', None)!r}") + + doc_id = last.get("document_id") + if not doc_id: + docs = client.notarial.envelopes.list_documents(envelope_id) + doc_id = docs[0].id if docs else "" + if doc_id: + doc_events = client.notarial.documents.list_events( + doc_id, envelope_id=envelope_id + ) + print(f" eventos do documento {doc_id}: {len(doc_events)}") + + print("\nOK.") + + +if __name__ == "__main__": + main() diff --git a/scripts/sandbox/04_activate_optional.py b/scripts/sandbox/04_activate_optional.py new file mode 100644 index 0000000..87e2d41 --- /dev/null +++ b/scripts/sandbox/04_activate_optional.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +Opcional: ativa envelope (status running) e envia notificação. + +Uso: python 04_activate_optional.py [envelope_id] +Requer IDs em .last_run.json (rode 02_create_notarial_draft.py antes). + +Nota: alguns tokens sandbox retornam 403 em POST /activate; neste caso +usamos PATCH status=running (documentado em docs/WORKFLOW.md). +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) + +from _config import get_client, load_last_run # noqa: E402 + + +def _activate_envelope(client, envelope_id: str): + """Ativa o envelope; tenta POST /activate e faz fallback para PATCH.""" + from clicksign.errors import AuthenticationError + + envelope = client.notarial.envelopes.retrieve(envelope_id) + if envelope.status == "running": + print(" envelope já está em running") + return envelope + + try: + return client.notarial.envelopes.activate(envelope_id) + except AuthenticationError as exc: + if exc.status_code != 403: + raise + print(" POST /activate retornou 403 (token sem permissão) — usando update(status='running')") + envelope.update(status="running") + return envelope + + +def main() -> None: + last = load_last_run() + envelope_id = (sys.argv[1] if len(sys.argv) > 1 else last.get("envelope_id", "")).strip() + signer_id = last.get("signer_id", "") + if not envelope_id: + print("Precisa de envelope_id (.last_run.json ou argumento)", file=sys.stderr) + sys.exit(1) + + client = get_client() + + with client.use(): + print(f"Ativando envelope {envelope_id}...") + envelope = _activate_envelope(client, envelope_id) + print(f" status={envelope.status}") + + print("Notificando signatários (envelope.notify)...") + envelope.notify( + message="Teste SDK Python — documento disponível para assinatura (sandbox).", + ) + print(" envelope.notify: OK") + + if signer_id: + print(f"Notificando signatário {signer_id} (Signer.notify, sem subject)...") + try: + client.notarial.signers.notify( + envelope_id, + signer_id, + message="Lembrete: seu documento aguarda assinatura (sandbox).", + ) + print(" Signer.notify: OK") + except Exception as exc: + # 429 ou restrições de conta — envelope.notify já disparou + print(f" Signer.notify ignorado: {type(exc).__name__}: {exc}") + + print("\nOK — envelope ativo e notificação enviada.") + + +if __name__ == "__main__": + main() diff --git a/scripts/sandbox/README.md b/scripts/sandbox/README.md new file mode 100644 index 0000000..671597d --- /dev/null +++ b/scripts/sandbox/README.md @@ -0,0 +1,35 @@ +# Scripts sandbox (local) + +Testes manuais contra **sandbox** com o SDK do repositório. **Não commitar** `.env` nem `.last_run.json`. + +## Setup + +```bash +cd /caminho/clicksign-python-sdk +cp scripts/sandbox/.env.example scripts/sandbox/.env +# edite .env se necessário (já existe .env local com token) +``` + +## Rodar + +```bash +# da raiz do repo +export PYTHONPATH=src + +python scripts/sandbox/01_list_collections.py +python scripts/sandbox/02_create_notarial_draft.py +python scripts/sandbox/03_consult_envelope.py +# opcional — ativa e notifica (cuidado: e-mail real do signatário no passo 2) +python scripts/sandbox/04_activate_optional.py +``` + +## O que cada script faz + +| Script | Ação | +|--------|------| +| `01_list_collections.py` | Lista envelopes, folders, webhooks, users | +| `02_create_notarial_draft.py` | Cria envelope + PDF mínimo + signatário + bulk (draft) | +| `03_consult_envelope.py` | Retrieve + listagens aninhadas + eventos | +| `04_activate_optional.py` | Ativa (`update` se `/activate` der 403) + `envelope.notify` | + +IDs do último `02_*` ficam em `scripts/sandbox/.last_run.json`. diff --git a/scripts/sandbox/_config.py b/scripts/sandbox/_config.py new file mode 100644 index 0000000..25dcb17 --- /dev/null +++ b/scripts/sandbox/_config.py @@ -0,0 +1,70 @@ +"""Config local para scripts sandbox (não importar no pacote).""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +SANDBOX_DIR = Path(__file__).resolve().parent +LAST_RUN_PATH = SANDBOX_DIR / ".last_run.json" + + +def _load_dotenv() -> None: + env_file = SANDBOX_DIR / ".env" + if not env_file.is_file(): + return + for line in env_file.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + os.environ.setdefault(key.strip(), value.strip()) + + +def get_client(): + _load_dotenv() + api_key = os.environ.get("CLICKSIGN_API_KEY", "").strip() + if not api_key: + print( + "Defina CLICKSIGN_API_KEY em scripts/sandbox/.env " + "(copie de .env.example)", + file=sys.stderr, + ) + sys.exit(1) + + from clicksign import ClicksignClient + + environment = os.environ.get("CLICKSIGN_ENVIRONMENT", "sandbox").strip() + return ClicksignClient(api_key=api_key, environment=environment) + + +def save_last_run(data: dict[str, str]) -> None: + LAST_RUN_PATH.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + print(f"\nIDs salvos em {LAST_RUN_PATH}") + + +def load_last_run() -> dict[str, str]: + if not LAST_RUN_PATH.is_file(): + return {} + return json.loads(LAST_RUN_PATH.read_text(encoding="utf-8")) + + +# PDF mínimo válido (1 página em branco) +MINIMAL_PDF_BYTES = ( + b"%PDF-1.4\n1 0 obj<<>>endobj\n2 0 obj<>stream\n" + b"BT /F1 12 Tf 100 700 Td (SDK sandbox test) Tj ET\n" + b"endstream\nendobj\n3 0 obj<>>>>>endobj\n" + b"4 0 obj<>endobj\n" + b"5 0 obj<>endobj\n" + b"trailer<>\n%%EOF\n" +) + + +def pdf_content_base64() -> str: + import base64 + + raw = base64.b64encode(MINIMAL_PDF_BYTES).decode("ascii") + return f"data:application/pdf;base64,{raw}" From 750d40a812cff8f309709ae9a040bc76c2f0710f Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 13:45:26 -0300 Subject: [PATCH 2/2] test: atualizar testes --- tests/clicksign/test_async_bound_resource.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/clicksign/test_async_bound_resource.py b/tests/clicksign/test_async_bound_resource.py index 4ec8dfd..f8c2605 100644 --- a/tests/clicksign/test_async_bound_resource.py +++ b/tests/clicksign/test_async_bound_resource.py @@ -24,11 +24,7 @@ async def test_async_bound_filter_chain_first( ): fake._queue = [http_response(200, collection("envelopes"))] item = await ( - client.notarial.envelopes.filter(status="draft") - .page(2) - .per(15) - .order("name") - .first() + client.notarial.envelopes.filter(status="draft").page(2).per(15).order("name").first() ) assert item is not None url = fake.calls[0]["url"]