From 4a479b05121e1f042b30736697bc9da533cd111c Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 13:54:29 -0300 Subject: [PATCH 1/5] chore: adicionar skill run-clicksign-python-sdk com smoke script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke script exercita 15 casos da API pública sem rede real: configure(), ClicksignClient, Envelope CRUD, hierarquia de erros (404/401/500/429), instrumentação, RequestOptions e webhook HMAC. Gotchas descobertos em execução real documentados no SKILL.md: - src/ deve preceder tests/ no sys.path (tests/clicksign/ shadowa src/) - ServerError/RateLimitError são retryable → max_retries=0 em testes - rate_limit_remaining é str não int (vem direto do header) - e.api_errors[0].detail, não e.errors[0].detail Ajusta .gitignore: de .claude/ (bloqueia tudo) para exclusões específicas de arquivos runtime (.exec.log, settings.local.json, __pycache__), permitindo que .claude/skills/ seja versionado. Co-Authored-By: Claude Sonnet 4.6 --- .../skills/run-clicksign-python-sdk/SKILL.md | 97 ++++++ .../skills/run-clicksign-python-sdk/smoke.py | 276 ++++++++++++++++++ .gitignore | 3 +- 3 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/run-clicksign-python-sdk/SKILL.md create mode 100644 .claude/skills/run-clicksign-python-sdk/smoke.py diff --git a/.claude/skills/run-clicksign-python-sdk/SKILL.md b/.claude/skills/run-clicksign-python-sdk/SKILL.md new file mode 100644 index 0000000..51790f0 --- /dev/null +++ b/.claude/skills/run-clicksign-python-sdk/SKILL.md @@ -0,0 +1,97 @@ +--- +name: run-clicksign-python-sdk +description: Run, smoke-test, and drive the clicksign-python-sdk library. Use when asked to run, start, smoke-test, verify, or screenshot the SDK. Covers: configure(), ClicksignClient, Envelope CRUD, error hierarchy, instrumentation, webhook, RequestOptions. +--- + +SDK Python puro (sem servidor, sem GUI). Driver é `smoke.py` — importa o pacote diretamente via `FakeHTTPClient`, sem rede real. + +## Pré-requisitos + +Python 3.10+ disponível como `python3`. Sem dependências extras além das do próprio repo. + +```bash +# Verificar +python3 --version # Python 3.10+ +``` + +## Run (caminho do agente) + +Execute sempre a partir da raiz do repo: + +```bash +python3 .claude/skills/run-clicksign-python-sdk/smoke.py +``` + +Sem `PYTHONPATH` — o script injeta `src/` e `tests/` no `sys.path` automaticamente. + +Saída esperada: + +``` +=== clicksign-python-sdk smoke === + + OK versão importada corretamente + OK configure() define estado global + ... + OK verify_signature valida HMAC SHA-256 corretamente + +======================================== +Resultado: 15 OK, 0 FAIL +``` + +Exit code 0 = tudo verde. Exit code 1 = algum caso falhou (traceback impresso). + +## O que o smoke cobre + +| Caso | O que valida | +|------|--------------| +| Versão | `clicksign.__version__` não vazio | +| `configure()` | estado global definido corretamente | +| `ClicksignClient` | instância explícita sem erro | +| `Envelope.list()` | emite `GET /envelopes` | +| `Envelope.create()` | retorna `Envelope` com `id` e `status` | +| `Envelope.retrieve()` | retorna instância com id correto | +| `NotFoundError` | levantado em 404 | +| `AuthenticationError` | levantado em 401 | +| `ServerError` | levantado em 500 (com `max_retries=0`) | +| `RateLimitError` | levantado em 429; `rate_limit_remaining` é string do header | +| Instrumentação | `on_request` callback recebe `method` e `duration_ms` | +| `RequestOptions` | `api_key` por chamada sobrescreve header `Authorization` | +| `ClicksignClient` namespace | `client.notarial.envelopes.list()` | +| `api_errors` | `e.api_errors[0].detail` acessível em erro 422 | +| Webhook | `verify_signature` valida HMAC-SHA256 | + +## Invocar código interno diretamente + +Para testar uma função isolada sem passar pelo smoke inteiro: + +```bash +PYTHONPATH=src python3 - <<'EOF' +from clicksign import ClicksignClient +from clicksign._http.transport import HTTPResponse + +class Stub: + name = "stub" + def request(self, method, url, **kw): + return HTTPResponse(200, '{"data":[],"meta":{},"links":{}}', {}) + +client = ClicksignClient(api_key="test", environment="sandbox", http_client=Stub()) +result = client.notarial.envelopes.list() +print("OK:", result) +EOF +``` + +## Gotchas + +- **`src/` deve vir antes de `tests/` no `sys.path`** — `tests/clicksign/` é um pacote que shadowa `src/clicksign/` se `tests/` vier primeiro. +- **`ServerError` e `RateLimitError` são `retryable=True`** — com `max_retries=3` (padrão), o `FakeHTTPClient` esgota a fila após a 1ª resposta. Use `configure(..., max_retries=0)` em testes de erro retryable. +- **`rate_limit_remaining` é `str`**, não `int` — vem direto do header HTTP sem conversão. +- **`e.errors` é `list[dict]`** — para acessar `ApiError.detail`, use `e.api_errors[0].detail` (propriedade que converte). +- **`clicksign.instrumentation.clear()`** — único jeito de limpar callbacks entre testes; `clicksign._config` não tem atributo `_instrumentation`. + +## Troubleshooting + +| Sintoma | Causa | Fix | +|---------|-------|-----| +| `ModuleNotFoundError: No module named 'clicksign._http'` | `tests/` inserido antes de `src/` | `sys.path.insert(0, "src")` deve ser o último insert (fica em [0]) | +| `FakeHTTPClient: no more responses queued` | erro retryable com múltiplas tentativas | Adicionar `max_retries=0` no `configure()` desse teste | +| `AttributeError: 'dict' object has no attribute 'detail'` | usando `e.errors[0]` em vez de `e.api_errors[0]` | Trocar para `e.api_errors[0].detail` | diff --git a/.claude/skills/run-clicksign-python-sdk/smoke.py b/.claude/skills/run-clicksign-python-sdk/smoke.py new file mode 100644 index 0000000..f89ee6a --- /dev/null +++ b/.claude/skills/run-clicksign-python-sdk/smoke.py @@ -0,0 +1,276 @@ +""" +Smoke script para o clicksign-python-sdk. + +Exercita a API pública sem rede real — usa FakeHTTPClient injetado. +Execute a partir da raiz do repo: + + PYTHONPATH=src python3 .claude/skills/run-clicksign-python-sdk/smoke.py +""" + +from __future__ import annotations + +import json +import sys +import traceback + +# --------------------------------------------------------------------------- +# Bootstrap: adiciona tests/support ao path para FakeHTTPClient +# --------------------------------------------------------------------------- +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +# src must come before tests — tests/clicksign/ would shadow src/clicksign/ otherwise +sys.path.insert(0, str(ROOT / "tests")) +sys.path.insert(0, str(ROOT / "src")) + +from support.fake_http_client import FakeHTTPClient, connection_error, http_error, http_response + +import clicksign +from clicksign import ( + ApiError, + AuthenticationError, + ClicksignClient, + NotFoundError, + RateLimitError, + RequestOptions, + ServerError, +) +from clicksign.resources.notarial.envelope import Envelope +from clicksign.resources.notarial.signer import Signer + +PASS = "\033[32mOK\033[0m" +FAIL = "\033[31mFAIL\033[0m" + +results: list[tuple[str, bool, str]] = [] + + +def check(name: str, fn): + try: + fn() + results.append((name, True, "")) + print(f" {PASS} {name}") + except Exception as exc: + results.append((name, False, traceback.format_exc())) + print(f" {FAIL} {name}: {exc}") + + +# --------------------------------------------------------------------------- +# Helper: JSON:API envelope response +# --------------------------------------------------------------------------- +def envelope_body(env_id: str = "aaa-111", status: str = "draft") -> dict: + return { + "data": { + "id": env_id, + "type": "envelopes", + "attributes": {"status": status, "name": "Smoke Envelope"}, + "relationships": {}, + } + } + + +def signer_body(sig_id: str = "bbb-222") -> dict: + return { + "data": { + "id": sig_id, + "type": "signers", + "attributes": {"name": "Smoke Signer", "email": "smoke@example.com"}, + "relationships": {}, + } + } + + +# --------------------------------------------------------------------------- +# Testes +# --------------------------------------------------------------------------- +print("\n=== clicksign-python-sdk smoke ===\n") + +# 1. Importação e versão +check("versão importada corretamente", lambda: ( + None if clicksign.__version__ else (_ for _ in ()).throw(AssertionError("sem versão")) +)) + +# 2. configure() — estado global +def _global_configure(): + clicksign.configure(api_key="smoke-key", environment="sandbox") + assert clicksign._config.api_key == "smoke-key" + assert clicksign._config.environment == "sandbox" + +check("configure() define estado global", _global_configure) + +# 3. ClicksignClient — cliente explícito +def _explicit_client(): + client = ClicksignClient(api_key="explicit-key", environment="sandbox") + assert client is not None + +check("ClicksignClient instanciado sem erro", _explicit_client) + +# 4. Envelope.list() via FakeHTTPClient +def _envelope_list(): + fake = FakeHTTPClient( + http_response(200, {"data": [], "meta": {}, "links": {}}) + ) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + envelopes = Envelope.list() + assert envelopes == [] + assert fake.calls[0]["method"] == "GET" + assert "/envelopes" in fake.calls[0]["url"] + +check("Envelope.list() emite GET /envelopes", _envelope_list) + +# 5. Envelope.create() via FakeHTTPClient +def _envelope_create(): + fake = FakeHTTPClient(http_response(201, envelope_body())) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + env = Envelope.create(name="Smoke Envelope") + assert env.id == "aaa-111" + assert env.status == "draft" + assert fake.calls[0]["method"] == "POST" + +check("Envelope.create() retorna Envelope com id e status", _envelope_create) + +# 6. Envelope.retrieve() +def _envelope_retrieve(): + fake = FakeHTTPClient(http_response(200, envelope_body("ccc-333", "running"))) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + env = Envelope.retrieve("ccc-333") + assert env.id == "ccc-333" + assert env.status == "running" + +check("Envelope.retrieve() retorna Envelope correto", _envelope_retrieve) + +# 7. NotFoundError em retrieve() +def _not_found(): + fake = FakeHTTPClient( + http_error(404, {"errors": [{"detail": "not found"}]}) + ) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + try: + Envelope.retrieve("nonexistent") + raise AssertionError("deveria ter levantado NotFoundError") + except NotFoundError as e: + assert e.status_code == 404 + +check("NotFoundError levantado em 404", _not_found) + +# 8. AuthenticationError em 401 +def _auth_error(): + fake = FakeHTTPClient( + http_error(401, {"errors": [{"detail": "unauthorized"}]}) + ) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + try: + Envelope.list() + raise AssertionError("deveria ter levantado AuthenticationError") + except AuthenticationError as e: + assert e.status_code == 401 + +check("AuthenticationError levantado em 401", _auth_error) + +# 9. ServerError em 500 +# max_retries=0: ServerError é retryable, sem isso o FakeHTTPClient esgota a queue +def _server_error(): + fake = FakeHTTPClient( + http_error(500, {"errors": [{"detail": "internal error"}]}) + ) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake, max_retries=0) + try: + Envelope.list() + raise AssertionError("deveria ter levantado ServerError") + except ServerError as e: + assert e.status_code == 500 + +check("ServerError levantado em 500", _server_error) + +# 10. RateLimitError em 429 +# max_retries=0: RateLimitError também é retryable +def _rate_limit(): + fake = FakeHTTPClient( + http_error(429, {"errors": [{"detail": "rate limit"}]}, {"X-RateLimit-Remaining": "0"}) + ) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake, max_retries=0) + try: + Envelope.list() + raise AssertionError("deveria ter levantado RateLimitError") + except RateLimitError as e: + assert e.status_code == 429 + assert e.rate_limit_remaining == "0" # header é string + +check("RateLimitError levantado em 429 com rate_limit_remaining", _rate_limit) + +# 11. Instrumentação — on_request callback +def _instrumentation(): + events: list[dict] = [] + clicksign.on_request(lambda e: events.append(e)) + fake = FakeHTTPClient(http_response(200, {"data": [], "meta": {}, "links": {}})) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + Envelope.list() + assert len(events) >= 1 + assert "method" in events[0] + assert "duration_ms" in events[0] + # limpa callback + clicksign.instrumentation.clear() + +check("on_request callback disparado com method e duration_ms", _instrumentation) + +# 12. RequestOptions por chamada +def _request_options(): + fake = FakeHTTPClient(http_response(200, {"data": [], "meta": {}, "links": {}})) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + Envelope.list(options=RequestOptions(api_key="override-key")) + auth = fake.calls[0]["headers"].get("Authorization", "") + assert auth == "override-key", f"esperado 'override-key', obtido '{auth}'" + +check("RequestOptions por chamada sobrescreve api_key no header", _request_options) + +# 13. ClicksignClient — namespace notarial +def _client_namespace(): + fake = FakeHTTPClient(http_response(200, {"data": [], "meta": {}, "links": {}})) + client = ClicksignClient(api_key="client-key", environment="sandbox", http_client=fake) + envelopes = client.notarial.envelopes.list() + assert envelopes == [] + assert "/envelopes" in fake.calls[0]["url"] + +check("ClicksignClient.notarial.envelopes.list() emite GET correto", _client_namespace) + +# 14. ApiError.detail acessível em erros estruturados +def _api_error_detail(): + fake = FakeHTTPClient( + http_error(422, {"errors": [{"detail": "campo obrigatório", "title": "Validation"}]}) + ) + clicksign.configure(api_key="smoke-key", environment="sandbox", http_client=fake) + try: + Envelope.create(name="x") + raise AssertionError("deveria ter levantado erro") + except clicksign.ClicksignError as e: + assert len(e.errors) >= 1 + assert e.api_errors[0].detail == "campo obrigatório" + +check("ApiError.detail acessível em erros 422 estruturados", _api_error_detail) + +# 15. Webhook — compute_signature e verify_signature +def _webhook_sig(): + import hmac + import hashlib + secret = "webhook-secret" + payload = b'{"event":"envelope.signed"}' + sig = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + # deve não levantar + clicksign.verify_signature(payload, sig, secret) + +check("verify_signature valida HMAC SHA-256 corretamente", _webhook_sig) + +# --------------------------------------------------------------------------- +# Resultado final +# --------------------------------------------------------------------------- +passed = sum(1 for _, ok, _ in results if ok) +failed = sum(1 for _, ok, _ in results if not ok) + +print(f"\n{'='*40}") +print(f"Resultado: {passed} OK, {failed} FAIL\n") + +if failed: + for name, ok, tb in results: + if not ok: + print(f"--- {name} ---") + print(tb) + sys.exit(1) diff --git a/.gitignore b/.gitignore index fc1e837..1c5be75 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,8 @@ htmlcov/ *.log .env .claude/exec.log -.claude/ +.claude/__pycache__/ +.claude/settings.local.json .idea/ docs/CONSIDERACOES.md scripts/sandbox/.env From 0733402950b8530da6b7755fa50ebfb72a910178 Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 14:13:14 -0300 Subject: [PATCH 2/5] fix(packaging): renomear pacote PyPI para clicksign-python-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pyproject.toml: name = "clicksign-python-sdk"; adiciona readme, URLs do projeto (Homepage, Docs, Repo, Changelog) e configuração explícita de build targets para wheel (src/clicksign) e sdist - version.py: usa importlib.metadata.version() como fonte primária após pip install — antes dependia só de REVISION, que não existe no wheel publicado; REVISION continua como fallback para desenvolvimento local - transport.py, client.py, clicksign_client.py: atualiza mensagens de erro e docstrings para "pip install clicksign-python-sdk[...]" Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 21 ++++++++++++++++++++- src/clicksign/_async/clicksign_client.py | 2 +- src/clicksign/_async/client.py | 2 +- src/clicksign/_http/transport.py | 4 ++-- src/clicksign/version.py | 22 +++++++++++++++++++++- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 787162a..a97a2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,13 +3,20 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "clicksign" +name = "clicksign-python-sdk" dynamic = ["version"] description = "Official Python SDK for the Clicksign e-signature API (v3)" +readme = "README.md" license = "MIT" requires-python = ">=3.10" dependencies = [] +[project.urls] +Homepage = "https://github.com/djosino/clicksign-python-sdk" +Documentation = "https://github.com/djosino/clicksign-python-sdk/tree/main/docs" +Repository = "https://github.com/djosino/clicksign-python-sdk" +Changelog = "https://github.com/djosino/clicksign-python-sdk/blob/main/CHANGELOG.md" + [project.optional-dependencies] dev = [ "pytest>=8.0", @@ -27,6 +34,18 @@ async = ["httpx>=0.27"] path = "REVISION" pattern = "^(?P.+)$" +[tool.hatch.build.targets.wheel] +packages = ["src/clicksign"] + +[tool.hatch.build.targets.sdist] +only-include = [ + "src/clicksign", + "REVISION", + "LICENSE", + "README.md", + "pyproject.toml", +] + [tool.pytest.ini_options] testpaths = ["tests"] addopts = "--tb=short" diff --git a/src/clicksign/_async/clicksign_client.py b/src/clicksign/_async/clicksign_client.py index 797b38c..0367277 100644 --- a/src/clicksign/_async/clicksign_client.py +++ b/src/clicksign/_async/clicksign_client.py @@ -47,7 +47,7 @@ def __init__(self, client: AsyncClicksignClient) -> None: class AsyncClicksignClient: """Async entry point for the Clicksign API. - Requires ``httpx`` (``pip install clicksign[async]``). The API itself is unchanged; + Requires ``httpx`` (``pip install clicksign-python-sdk[async]``). The API itself is unchanged; this client runs concurrent HTTP I/O on the event loop. ``Services.use()`` is thread-local and is **not** compatible with asyncio — pass an diff --git a/src/clicksign/_async/client.py b/src/clicksign/_async/client.py index e7b68b6..e27434c 100644 --- a/src/clicksign/_async/client.py +++ b/src/clicksign/_async/client.py @@ -26,7 +26,7 @@ class AsyncClient(RequestInstrumentation): - """Async HTTP client for the Clicksign API (requires ``httpx``; see ``clicksign[async]``).""" + """Async HTTP client for the Clicksign API (requires ``httpx``; see ``clicksign-python-sdk[async]``).""" def __init__( self, diff --git a/src/clicksign/_http/transport.py b/src/clicksign/_http/transport.py index 8de40e3..1314763 100644 --- a/src/clicksign/_http/transport.py +++ b/src/clicksign/_http/transport.py @@ -193,7 +193,7 @@ def __init__( import httpx except ImportError as exc: raise ImportError( - "httpx is required for HttpxHTTPClient. Install with: pip install clicksign[httpx]" + "httpx is required for HttpxHTTPClient. Install with: pip install clicksign-python-sdk[httpx]" ) from exc self._client = httpx.Client( proxy=proxy, @@ -308,7 +308,7 @@ def __init__( import httpx except ImportError as exc: raise ImportError( - "httpx is required for async HTTP. Install with: pip install clicksign[async]" + "httpx is required for async HTTP. Install with: pip install clicksign-python-sdk[async]" ) from exc self._client = httpx.AsyncClient( proxy=proxy, diff --git a/src/clicksign/version.py b/src/clicksign/version.py index 07d6c74..1ca93ec 100644 --- a/src/clicksign/version.py +++ b/src/clicksign/version.py @@ -1,3 +1,23 @@ +from __future__ import annotations + import pathlib -__version__ = (pathlib.Path(__file__).parent.parent.parent / "REVISION").read_text().strip() +_DISTRIBUTION = "clicksign-python-sdk" + + +def _resolve_version() -> str: + try: + from importlib.metadata import version + + return version(_DISTRIBUTION) + except Exception: + pass + + revision = pathlib.Path(__file__).resolve().parent.parent.parent / "REVISION" + if revision.is_file(): + return revision.read_text(encoding="utf-8").strip() + + return "0.0.0+unknown" + + +__version__ = _resolve_version() From e088df14073144f27075fe0e5bf5ac826e4cdf37 Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 14:13:25 -0300 Subject: [PATCH 3/5] test: cobertura de version.py (importlib.metadata + fallback REVISION) Co-Authored-By: Claude Sonnet 4.6 --- tests/clicksign/test_version.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/clicksign/test_version.py diff --git a/tests/clicksign/test_version.py b/tests/clicksign/test_version.py new file mode 100644 index 0000000..7020f5d --- /dev/null +++ b/tests/clicksign/test_version.py @@ -0,0 +1,13 @@ +import clicksign +from clicksign.version import __version__, _resolve_version + + +def test_clicksign_exports_version(): + assert clicksign.__version__ + assert clicksign.__version__ == __version__ + + +def test_version_matches_revision_or_metadata(): + v = _resolve_version() + assert v + assert v != "0.0.0+unknown" From cfe7c18e3606b683a0d68b6eb18eea2cfc686153 Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 14:13:35 -0300 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20atualizar=20refer=C3=AAncias=20pip?= =?UTF-8?q?=20install=20para=20clicksign-python-sdk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui todas as ocorrências de "pip install clicksign[...]" por "pip install clicksign-python-sdk[...]" em README, CHANGELOG, SDK_CONTRACT, exemplos e comandos do Claude Code. Esclarece que o import permanece `import clicksign`. Co-Authored-By: Claude Sonnet 4.6 --- .claude/commands/gen-resource.md | 2 +- CHANGELOG.md | 8 ++++++++ README.md | 23 +++++++++++++--------- docs/SDK_CONTRACT.md | 4 ++-- docs/examples/08-production-limitations.md | 4 ++-- docs/examples/12-http-connection-pool.md | 2 +- docs/examples/13-async-fastapi.md | 2 +- 7 files changed, 29 insertions(+), 16 deletions(-) diff --git a/.claude/commands/gen-resource.md b/.claude/commands/gen-resource.md index ad4d651..91183bf 100644 --- a/.claude/commands/gen-resource.md +++ b/.claude/commands/gen-resource.md @@ -9,7 +9,7 @@ e `docs/SDK_TEST_MATRIX.md`. ### Infraestrutura base **`pyproject.toml`** -- `name = "clicksign"` +- `name = "clicksign-python-sdk"` (import: `clicksign`) - `requires-python = ">=3.10"` - Sem dependências de runtime (stdlib apenas, ou `httpx` se adotado) - Dev: `pytest`, `pytest-cov`, `responses` (ou `pytest-httpx`), `ruff`, `mypy` diff --git a/CHANGELOG.md b/CHANGELOG.md index 3744b29..a39868f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Alterado + +- Nome do pacote no PyPI/TestPyPI: `clicksign-python-sdk` (`pip install clicksign-python-sdk`; import permanece `clicksign`) + +### Corrigido + +- `clicksign.__version__` após `pip install`: lê versão dos metadados do pacote (antes dependia de `REVISION`, ausente no wheel) + --- ## [0.1.0] - 2026-05-21 diff --git a/README.md b/README.md index b68f12c..f01f1c5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # Clicksign Python SDK -[![PyPI](https://img.shields.io/pypi/v/clicksign)](https://pypi.org/project/clicksign/) -[![CI](https://github.com/djosino/clicksign-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/djosino/clicksign-python-sdk/actions) -[![Python](https://img.shields.io/pypi/pyversions/clicksign)](https://pypi.org/project/clicksign/) -[![License](https://img.shields.io/pypi/l/clicksign)](LICENSE) +[![versão](https://img.shields.io/badge/versão-0.1.0-0066cc?style=flat-square)](CHANGELOG.md#010---2026-05-21) +[![CI](https://github.com/djosino/clicksign-python-sdk/actions/workflows/ci.yml/badge.svg?branch=main&style=flat-square)](https://github.com/djosino/clicksign-python-sdk/actions/workflows/ci.yml) +[![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/djosino/clicksign-python-sdk/actions/workflows/ci.yml) +[![PyPI](https://img.shields.io/badge/PyPI-em%20breve-9cf?style=flat-square)](https://pypi.org/project/clicksign-python-sdk/) +[![licença](https://img.shields.io/github/license/djosino/clicksign-python-sdk?style=flat-square&label=licença)](LICENSE) +[![documentação](https://img.shields.io/badge/documentação-pt--BR-2563eb?style=flat-square)](docs/) +[![API v3](https://img.shields.io/badge/Clicksign-API%20v3-00a86b?style=flat-square)](https://developers.clicksign.com/) Cliente Python para a [Clicksign API v3](https://developers.clicksign.com/) (JSON:API). Requer Python >= 3.10, sem dependências de runtime (apenas stdlib). Documentação detalhada em [`docs/`](docs/). @@ -41,14 +44,16 @@ Cliente Python para a [Clicksign API v3](https://developers.clicksign.com/) (JSO ## Instalação ```bash -pip install clicksign +pip install clicksign-python-sdk ``` +O módulo importado continua sendo `clicksign` (`import clicksign`). + Extras opcionais: ```bash -pip install clicksign[httpx] # connection pooling (alto QPS) -pip install clicksign[async] # AsyncClicksignClient (requer httpx) +pip install clicksign-python-sdk[httpx] # connection pooling (alto QPS) +pip install clicksign-python-sdk[async] # AsyncClicksignClient (requer httpx) ``` --- @@ -443,7 +448,7 @@ client = ClicksignClient(api_key="...", environment="production") ## Async (FastAPI, asyncio) -Requer `pip install clicksign[async]`. Receita completa: [`docs/examples/13-async-fastapi.md`](docs/examples/13-async-fastapi.md). +Requer `pip install clicksign-python-sdk[async]`. Receita completa: [`docs/examples/13-async-fastapi.md`](docs/examples/13-async-fastapi.md). ```python import asyncio @@ -470,7 +475,7 @@ asyncio.run(main()) **Padrão:** `UrllibHTTPClient` (stdlib, sem connection pool — um handshake TCP/TLS por requisição). Adequado para scripts e baixo QPS. -**Alto QPS:** instale `clicksign[httpx]` e injete um cliente compartilhado: +**Alto QPS:** instale `clicksign-python-sdk[httpx]` e injete um cliente compartilhado: ```python from clicksign import ClicksignClient, HttpxHTTPClient diff --git a/docs/SDK_CONTRACT.md b/docs/SDK_CONTRACT.md index 21208ea..4cdad5c 100644 --- a/docs/SDK_CONTRACT.md +++ b/docs/SDK_CONTRACT.md @@ -2,7 +2,7 @@ **Versão:** 1.0 **Fonte:** Clicksign API v3 (JSON:API 1.1) -**Implementação:** este repositório (`clicksign` no PyPI) +**Implementação:** este repositório (`clicksign-python-sdk` no PyPI; import `clicksign`) Este documento define o **contrato comportamental** do SDK Python. Implementações em outras linguagens devem preservar o mesmo comportamento observável (HTTP, retry, erros, bulk, paginação). Mapa de classes e rotas: [`SPEC.md`](SPEC.md). @@ -284,7 +284,7 @@ Clicksign.instrumentation.clear() # for tests - Configuração global única — não é segura para mutação concorrente - Cliente thread-local: context manager `Services.use(api_key, base_url)` define o cliente apenas para a thread atual -- Async: use `AsyncClicksignClient` / `AsyncClient` (`pip install clicksign[async]`). Não dependa de `Services.use()` sob asyncio; passe um cliente async explícito por escopo de app/coroutine +- Async: use `AsyncClicksignClient` / `AsyncClient` (`pip install clicksign-python-sdk[async]`). Não dependa de `Services.use()` sob asyncio; passe um cliente async explícito por escopo de app/coroutine - Atualizações de instância em fluxos async: `update_async`, `delete_async`, `reload_async` nos resources retornados pelo cliente async ## 12. Validação de webhook diff --git a/docs/examples/08-production-limitations.md b/docs/examples/08-production-limitations.md index 41c9004..e21e549 100644 --- a/docs/examples/08-production-limitations.md +++ b/docs/examples/08-production-limitations.md @@ -23,7 +23,7 @@ Ordem de grandeza típica em bursts (mesmo host, TLS já “quente” no OS): ** ### Mitigação recomendada: `HttpxHTTPClient` ```bash -pip install clicksign[httpx] +pip install clicksign-python-sdk[httpx] ``` ```python @@ -86,7 +86,7 @@ Async: [`README` — Async](../../README.md#async-fastapi-asyncio) · multi-cont ## Checklist rápido -- [ ] Alta carga HTTP → `pip install clicksign[httpx]` + `HttpxHTTPClient` compartilhado por worker +- [ ] Alta carga HTTP → `pip install clicksign-python-sdk[httpx]` + `HttpxHTTPClient` compartilhado por worker - [ ] Multi-tenant sync → `Services.use()` por request/job - [ ] FastAPI/async → `AsyncClicksignClient`, não `Services.use()` - [ ] Observabilidade → `on_request` / `CLICKSIGN_LOG` para volume e lentidão diff --git a/docs/examples/12-http-connection-pool.md b/docs/examples/12-http-connection-pool.md index 126b3fa..a0dbf47 100644 --- a/docs/examples/12-http-connection-pool.md +++ b/docs/examples/12-http-connection-pool.md @@ -3,7 +3,7 @@ O transporte padrão (`UrllibHTTPClient`) abre e fecha a conexão a cada request. Para **muitas chamadas por processo**, use `HttpxHTTPClient` com **uma instância compartilhada** por worker. ```bash -pip install clicksign[httpx] +pip install clicksign-python-sdk[httpx] ``` ## Singleton por processo (recomendado) diff --git a/docs/examples/13-async-fastapi.md b/docs/examples/13-async-fastapi.md index 3fe5a8a..9556f43 100644 --- a/docs/examples/13-async-fastapi.md +++ b/docs/examples/13-async-fastapi.md @@ -2,7 +2,7 @@ Receita para apps async: lifespan, dependência por request e fluxo notarial mínimo. -**Requisito:** `pip install clicksign[async]` (httpx). +**Requisito:** `pip install clicksign-python-sdk[async]` (httpx). --- From f6e1b4d24d90bd6b0dfc5edf38ae1ce7a450af80 Mon Sep 17 00:00:00 2001 From: Danilo Josino Date: Thu, 21 May 2026 14:18:44 -0300 Subject: [PATCH 5/5] fix(lint): quebrar linhas longas para passar ruff no CI Mensagens de ImportError e docstring do AsyncClient excediam 100 colunas. Co-authored-by: Cursor --- src/clicksign/_async/client.py | 5 ++++- src/clicksign/_http/transport.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/clicksign/_async/client.py b/src/clicksign/_async/client.py index e27434c..97fe4b2 100644 --- a/src/clicksign/_async/client.py +++ b/src/clicksign/_async/client.py @@ -26,7 +26,10 @@ class AsyncClient(RequestInstrumentation): - """Async HTTP client for the Clicksign API (requires ``httpx``; see ``clicksign-python-sdk[async]``).""" + """Async HTTP client for the Clicksign API. + + Requires ``httpx`` (``pip install clicksign-python-sdk[async]``). + """ def __init__( self, diff --git a/src/clicksign/_http/transport.py b/src/clicksign/_http/transport.py index 1314763..582b73b 100644 --- a/src/clicksign/_http/transport.py +++ b/src/clicksign/_http/transport.py @@ -193,7 +193,8 @@ def __init__( import httpx except ImportError as exc: raise ImportError( - "httpx is required for HttpxHTTPClient. Install with: pip install clicksign-python-sdk[httpx]" + "httpx is required for HttpxHTTPClient. " + "Install with: pip install clicksign-python-sdk[httpx]" ) from exc self._client = httpx.Client( proxy=proxy, @@ -308,7 +309,8 @@ def __init__( import httpx except ImportError as exc: raise ImportError( - "httpx is required for async HTTP. Install with: pip install clicksign-python-sdk[async]" + "httpx is required for async HTTP. " + "Install with: pip install clicksign-python-sdk[async]" ) from exc self._client = httpx.AsyncClient( proxy=proxy,