Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ jobs:
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Ruff
run: ruff check src tests
run: |
ruff check src tests
ruff format --check src tests
- name: Mypy (typed resources)
run: mypy
- name: Pytest
run: pytest -q
run: pytest -q --cov=clicksign --cov-fail-under=88
env:
PYTHONPATH: src
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ htmlcov/
.claude/exec.log
.claude/
.idea/
docs/CONSIDERACOES.md
30 changes: 27 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,32 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

---

## [0.1.0] - 2026-05-21

Primeira release do SDK Python para a API Clicksign v3 (JSON:API). Versão alinhada a `REVISION`.

### Adicionado

- Estrutura inicial do projeto
- `docs/SDK_CONTRACT.md` — especificação comportamental agnóstica de linguagem
- `docs/SDK_TEST_MATRIX.md` — matriz de cobertura de testes obrigatória
- SDK Python para API v3: resources notariais (`Envelope`, `Document`, `Signer`, `Requirement`, `BulkRequirement`, `SignatureWatcher`, eventos aninhados) e administrativos (`Webhook`, `Folder`, `User`, `Template`, `TemplateField`, `Membership`, `Group`, e parciais)
- `configure()` + resources, `ClicksignClient` / `AsyncClicksignClient`, `Services`, webhooks HMAC (`construct_event`, `verify_signature`)
- HTTP stdlib (`UrllibHTTPClient`) e opcional `httpx` / `HttpxAsyncHTTPClient` para sync e async
- Retry com jitter, header `Retry-After`, mapeamento de erros JSON:API, `RequestOptions`, correlation id
- Paginação via `QueryProxy`, `with_includes`, bulk atômico (`BulkRequirement`)
- TypedDicts em `clicksign.types` para resources principais
- Documentação em `docs/` (contrato, workflow, spec, troubleshooting, 11 receitas)
- Suíte de testes com mocks HTTP (483+ testes), CI com ruff, mypy e cobertura mínima 88%

### Alterado

- N/A (release inicial)

### Corrigido

- N/A (release inicial)

### Notas de breaking (desde pré-release interna)

- `Signer.notify(envelope_id, signer_id, message, ...)` — classmethod com rota aninhada; não há `POST /signers/:id/notifications`
- Eventos apenas em rotas aninhadas (`Envelope.list_events`, `Document.list_events`, `Event.create_for_document`)
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Cliente Python para a [Clicksign API v3](https://developers.clicksign.com/) (JSO
- [Multi-conta](#multi-conta)
- [Timeouts, retry e instrumentação](#timeouts-retry-e-instrumentação)
- [Início rápido](#início-rápido)
- [Fluxo de assinatura (notarial)](#fluxo-de-assinatura-notarial)
- [Fluxo de assinatura (notarial)](#fluxo-de-assinatura-notarial) (criar → ativar → notificar → eventos)
- [Filtros, ordenação e paginação](#filtros-ordenação-e-paginação)
- [Outros recursos](#outros-recursos)
- [Tratamento de erros](#tratamento-de-erros)
Expand Down Expand Up @@ -274,9 +274,24 @@ client.notarial.bulk_requirements.create(

```python
envelope.update(status="running")
# ou: Envelope.activate(envelope.id) — POST /envelopes/:id/activate
```

### 6. Monitorar eventos
### 6. Notificar signatários

```python
from clicksign.resources.notarial.signer import Signer

# Um signatário (envelope_id e signer_id obrigatórios)
Signer.notify(envelope.id, signer.id, message="Seu contrato está disponível para assinatura.")

# Todos os pendentes do envelope
envelope.notify(message="Seu contrato está disponível para assinatura.")
```

Detalhes: [`docs/WORKFLOW.md`](docs/WORKFLOW.md) (seção 6).

### 7. Monitorar eventos

```python
from clicksign.resources.notarial.envelope import Envelope
Expand Down Expand Up @@ -428,6 +443,8 @@ 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).

```python
import asyncio
from clicksign import AsyncClicksignClient
Expand All @@ -439,11 +456,13 @@ async def main():
print(env.id)
envelope = await client.envelopes.retrieve("uuid")
await envelope.update_async(status="running")
# Ativar: await client.notarial.envelopes.activate(envelope.id)

asyncio.run(main())
```

Não use `Services.use()` dentro do asyncio — passe `AsyncClicksignClient` explícito por coroutine.
- Não use `Services.use()` no asyncio — use `AsyncClicksignClient` explícito.
- Paginação: `.page(n)` só vale com `.first()`; `.to_list()` auto-pagina desde a página 1 — [`docs/PAGINATION.md`](docs/PAGINATION.md).

---

Expand Down
35 changes: 34 additions & 1 deletion docs/PAGINATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ Envelope.filter(status="draft").per(50).to_list()
# .per(100) levanta ValueError
```

## `page(n)` — página explícita vs auto-paginação

| Terminal | `page[number]` na chain |
|----------|-------------------------|
| `.first()` | **Respeitado** — uma única requisição com os params da chain |
| `.to_list()`, `for ... in proxy`, `.count()`, `.last()` | **Ignorado** na auto-paginação — o SDK começa em página **1** e avança até não haver `links.next` |

```python
# Uma página fixa (ex.: página 3, 25 itens) — use terminal que não auto-pagina tudo
proxy = Envelope.filter(status="draft").page(3).per(25)
first_item = proxy.first() # GET com page[number]=3
# proxy.to_list() # evite: percorre TODAS as páginas desde a 1

# Todas as páginas da collection filtrada
for envelope in Envelope.filter(status="draft").per(25):
...
```

Async: mesmo contrato em `AsyncQueryProxy` (`await ...first()`, `async for ...`).

## Auto-paginação

`filter(...).to_list()`, `for item in filter(...):`, `.count()` e `.last()` percorrem todas as páginas automaticamente.
Expand Down Expand Up @@ -53,9 +73,22 @@ Envelope.filter(status="draft").on_page(log_page).to_list()

Útil para progresso, métricas ou integração com `instrumentation.on_request` sem duplicar lógica de paginação.

## Listagens aninhadas (não são `filter` na raiz)

Rotas como `GET /envelopes/:id/signers` **não** usam `QueryProxy` da collection raiz. O SDK expõe métodos de classe equivalentes:

| Conteúdo | Método A | Método B (mesma rota HTTP) |
|----------|----------|----------------------------|
| Signatários do envelope | `Signer.list_for_envelope(envelope_id)` | `Envelope.list_signers(envelope_id)` |
| Documentos do envelope | `Document.list_for_envelope(envelope_id)` | `Envelope.list_documents(envelope_id)` |
| Requisitos do envelope | — | `Envelope.list_requirements(envelope_id, **filters)` |

Filtros em `Envelope.list_requirements` usam `QueryBuilder` (`document_id=...`, etc.).
`Requirement.list_for_document` / `list_for_signer` listam via rotas de relacionamento (`/documents/:id/relationships/requirements`).

## Async

`AsyncClicksignClient` expõe o mesmo comportamento em `AsyncQueryProxy` (`async for`, `on_page`, `page_responses`).
`AsyncClicksignClient` expõe o mesmo comportamento em `AsyncQueryProxy` (`async for`, `on_page`, `page_responses`). Receita: [`examples/13-async-fastapi.md`](examples/13-async-fastapi.md).

## Referências

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
| [`examples/10-observability-opentelemetry.md`](examples/10-observability-opentelemetry.md) | OpenTelemetry manual |
| [`examples/11-observability-metrics.md`](examples/11-observability-metrics.md) | Prometheus / StatsD |
| [`examples/12-http-connection-pool.md`](examples/12-http-connection-pool.md) | `HttpxHTTPClient` singleton |
| [`examples/13-async-fastapi.md`](examples/13-async-fastapi.md) | FastAPI, lifespan, fluxo notarial async |

Índice do examples: [`examples/README.md`](examples/README.md).

Expand Down
2 changes: 2 additions & 0 deletions docs/SDK_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ fetch_auto_pages(params):
**`links.next` tem prioridade.** A heurística de contagem é o fallback para APIs que omitem `links`.
Quando `links.next` é null, NÃO faça outra requisição mesmo que `len(items) == per`.

**Página explícita:** em `to_list()` / `for ... in proxy` / `count()` / `last()`, o `page[number]` da chain **não** é usado — a auto-paginação reinicia em `page = 1`. Para buscar uma página fixa, use `.first()` (uma requisição com os params da chain) ou não use auto-paginação.

### Query chain

Builder encadeável que acumula parâmetros antes de executar:
Expand Down
24 changes: 23 additions & 1 deletion docs/SDK_TEST_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,12 @@ Cada item deve ter teste em `tests/clicksign/`. Marque `[x]` quando coberto.

---

## Async — `test_async_client.py`, `test_async_clicksign_client.py`
## Async — `test_async_client.py`, `test_async_clicksign_client.py`, `test_async_http_executor.py`, `test_httpx_async_transport.py`

- [x] HTTP do AsyncClient; resources e paginação do AsyncClicksignClient
- [x] Retry 429/422/timeout no AsyncClient (`test_async_client.py`)
- [x] `execute_async_http_request` — sucesso, retry 429/timeout, sem retry 422 (`test_async_http_executor.py`)
- [x] `HttpxAsyncHTTPClient` — import guard, delegação, status error, `aclose` (`test_httpx_async_transport.py`)

---

Expand All @@ -138,6 +141,25 @@ Cobertura por resource (CRUD, filter, erros 404/422 onde aplicável):
- [x] Admin: user, template, template_field, membership, group, folder, webhook
- [x] Parcial: acceptance_term, auto_signature, access_control_list, envelope_bulk_creation (conforme métodos expostos)

### Notarial — métodos aninhados e notify

- [x] `Envelope.list_documents` / `list_signers` / `list_requirements` / `list_signature_watchers` (`test_envelope.py`)
- [x] `Envelope.activate` (classe) (`test_envelope.py`)
- [x] `Envelope.notify` (`test_envelope.py`)
- [x] `Signer.notify(envelope_id, signer_id, ...)` — classmethod com ids explícitos (`test_signer.py`)
- [x] Facade `documents.create` / `signers.create` com envelope id posicional (`test_clicksign_client.py`)
- [x] Facade `signers.notify` / `envelopes.notify` (`test_clicksign_client.py`)
- [x] `Requirement.list_for_document` / `list_for_signer` com filtros (`test_requirement.py`)
- [x] `Envelope.list_requirements` com `QueryBuilder` (`test_envelope.py`)

### Infraestrutura — `test_json_api_operations.py`, `test_bound_resource.py`, `test_async_bound_resource.py`, `test_httpx_transport.py`, `test_httpx_async_transport.py`, `test_types_attrs.py`

- [x] `BulkRequirementOperations` (agree, evidence, rubricate, remove, payload)
- [x] `BoundQueryProxy` sync: includes, fields, count, last, iter, on_page, page/per/order
- [x] `AsyncBoundQueryProxy`: filter chain, includes, count, page/per
- [x] `HttpxHTTPClient` / `HttpxAsyncHTTPClient`: owned timeout, status 4xx/5xx, exceções httpx
- [x] `types._attrs` helpers

Adicionar testes ao criar novos métodos (ver [`SPEC.md`](SPEC.md) e esta matriz).

---
Expand Down
10 changes: 10 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ Import direto: `from clicksign import Envelope, Document, Signer` (notarial expo
- `Envelope.list_events(envelope_id, **filters)`
- `Envelope.list_documents` / `list_signers` / `list_requirements` / `list_signature_watchers`
- `envelope.notify(message=..., subject=...)`
- `Signer.notify(envelope_id, signer_id, message=..., subject=...)` — classmethod; ids obrigatórios

**Listagens equivalentes** (mesma rota HTTP; escolha por estilo):

| Recurso | Via envelope | Via resource |
|---------|--------------|--------------|
| Signatários | `Envelope.list_signers(envelope_id)` | `Signer.list_for_envelope(envelope_id)` |
| Documentos | `Envelope.list_documents(envelope_id)` | `Document.list_for_envelope(envelope_id)` |

Detalhes e paginação: [`PAGINATION.md`](PAGINATION.md).

---

Expand Down
30 changes: 30 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ Receita completa: [`examples/03-webhooks.md`](examples/03-webhooks.md).

---

## Signer.notify — envelope_id e signer_id obrigatórios

`Signer.notify` é um **classmethod**: sempre informe `envelope_id` e `signer_id` (não há rota na raiz `/signers/{id}/notifications`).

```python
Signer.notify(envelope.id, signer.id, message="Lembrete")

# após retrieve — ainda precisa do envelope_id
signer = Signer.retrieve(signer_id) # rota raiz; envelope_id vem de envelope.id ou relationships
Signer.notify(envelope.id, signer.id, message="Lembrete")
```

Na facade: `client.notarial.signers.notify(envelope_id, signer_id, message="...")`.

---

## Document / Signer `create` na facade

`Document.create` e `Signer.create` recebem o **id do envelope como primeiro argumento posicional**, não como `envelope_id=`:
Expand All @@ -121,6 +137,20 @@ Document.create(envelope_id=envelope.id, filename="a.pdf")

---

## Paginação — `page(n)` não fixa a página em `to_list()`

`Envelope.filter(...).page(3).per(25).to_list()` **não** retorna só a página 3: a auto-paginação começa em `page[number]=1` e segue até o fim.

Para uma página específica:

```python
Envelope.filter(status="draft").page(3).per(25).first() # uma requisição, página 3
```

Detalhes e listagens aninhadas: [`PAGINATION.md`](PAGINATION.md).

---

## Eventos de envelope/documento

Não existe `GET /events` na raiz da conta. Use rotas aninhadas:
Expand Down
19 changes: 17 additions & 2 deletions docs/TYPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Resources com **TypedDict** de params e **properties** explícitas nos atributos

| Resource | Create / update / filter |
|----------|-------------------------|
| **Notarial** | `Envelope`, `Document`, `Signer`, `Requirement`, `SignatureWatcher`, `NotarialEvent` |
| **Notarial** | `Envelope`, `Document`, `Signer`, `Requirement` (`auth` em create, ex. `"email"`), `SignatureWatcher`, `NotarialEvent` |
| **Conta** | `Webhook`, `Folder`, `User`, `Template`, `TemplateField`, `Membership`, `Group` |

Import:
Expand Down Expand Up @@ -75,7 +75,22 @@ pytest -q

`pyproject.toml` usa `strict = true` nos módulos listados em `[tool.mypy] files`; resources tipados devem passar no mypy sem `# type: ignore` desnecessários.

## Próximos passos (fora do §4)
## Imports fora de `clicksign.__all__`

Alguns resources existem no pacote mas **não** são reexportados na raiz (`import clicksign`). Use submódulo:

```python
from clicksign.resources.notarial.event import Event
from clicksign.resources.user import User
from clicksign.resources.template import Template
from clicksign.resources.membership import Membership
from clicksign.resources.group import Group
from clicksign.resources.access_control_list import AccessControlList
```

Na raiz hoje: `Envelope`, `Document`, `Signer`, `Requirement`, `BulkRequirement`, `SignatureWatcher`, `Webhook`, `Folder` — ver `clicksign.__all__` em `src/clicksign/__init__.py`.

## Próximos passos (tipagem)

- Resources admin restantes (`AccessControlList`, `EnvelopeBulkCreation`, …) conforme [`SPEC.md`](SPEC.md)
- `Required[...]` em create params quando a API publicar obrigatoriedade
Expand Down
8 changes: 5 additions & 3 deletions docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ envelope.notify(message="Seu contrato está disponível para assinatura.")
### Um signatário

```python
signer.notify(message="Lembrete: seu documento aguarda assinatura.")
signer.notify(
Signer.notify(envelope.id, signer.id, message="Lembrete: seu documento aguarda assinatura.")
Signer.notify(
envelope.id,
signer.id,
message="Por favor, assine até sexta-feira.",
subject="Ação necessária: assinatura pendente",
)
Expand Down Expand Up @@ -203,7 +205,7 @@ Requirement.create(envelope.id, signer_id=signer.id, document_id=document.id, ac
envelope.update(status="running")

# 6. Notificar
signer.notify(message="Seu contrato ACME está disponível para assinatura.")
Signer.notify(envelope.id, signer.id, message="Seu contrato ACME está disponível para assinatura.")

print(f"Envelope {envelope.id} ativo e signatário notificado.")
```
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/04-multi-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ async def get_client(tenant_id: str) -> AsyncClicksignClient:
return AsyncClicksignClient(api_key=tenant.api_key, environment=tenant.environment)
```

Ver [08-production-limitations.md](08-production-limitations.md).
Receita completa: [13-async-fastapi.md](13-async-fastapi.md) · limitações: [08-production-limitations.md](08-production-limitations.md).

---

Expand Down
15 changes: 14 additions & 1 deletion docs/examples/07-list-and-filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,20 @@ Com `ClicksignClient`: `client.envelopes.filter(...).to_list()`.

## Listagens aninhadas

Métodos como `Envelope.list_documents(envelope_id)` são rotas aninhadas — não passam por `filter` da collection raiz. Ver [`SPEC.md`](../SPEC.md).
Não use `Envelope.filter(...)` para listar signatários **dentro** de um envelope. Use métodos aninhados:

```python
# Equivalentes (mesma API)
signers_a = Signer.list_for_envelope(envelope.id)
signers_b = Envelope.list_signers(envelope.id)

docs = Document.list_for_envelope(envelope.id)
# ou Envelope.list_documents(envelope.id)

reqs = Envelope.list_requirements(envelope.id, document_id=doc.id)
```

Tabela completa: [`../PAGINATION.md`](../PAGINATION.md) · rotas: [`../SPEC.md`](../SPEC.md).

---

Expand Down
Loading
Loading