vitrin 0.2.0__tar.gz → 0.2.2__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.
- vitrin-0.2.2/CHANGELOG.md +44 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/PKG-INFO +16 -1
- {vitrin-0.2.0 → vitrin-0.2.2}/README.md +15 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/pyproject.toml +1 -1
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/_client.py +23 -2
- {vitrin-0.2.0 → vitrin-0.2.2}/tests/test_client.py +61 -0
- vitrin-0.2.0/CHANGELOG.md +0 -18
- {vitrin-0.2.0 → vitrin-0.2.2}/.gitignore +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/__init__.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/errors.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/__init__.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/balance.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/charges.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/customers.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/plans.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/products.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/resources/subscriptions.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/src/vitrin/webhooks.py +0 -0
- {vitrin-0.2.0 → vitrin-0.2.2}/tests/test_webhooks.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog — vitrin
|
|
2
|
+
|
|
3
|
+
## 0.2.2 — 2026-08-26
|
|
4
|
+
|
|
5
|
+
### Corrigido
|
|
6
|
+
- **Retry em timeout deixa de duplicar cobrança.** O client retentava em
|
|
7
|
+
timeout e erro de rede em qualquer método, apesar de a documentação
|
|
8
|
+
prometer "429 e 5xx". Timeout é justamente o caso em que o servidor
|
|
9
|
+
recebeu e processou o pedido — só a resposta se perdeu —, então cada
|
|
10
|
+
retry de um `POST /charges/` criava uma cobrança nova. Sem
|
|
11
|
+
`idempotency_key`, um clique podia virar até 4 débitos.
|
|
12
|
+
|
|
13
|
+
Agora só retentamos quando é seguro: métodos idempotentes por HTTP
|
|
14
|
+
(GET/HEAD/PUT/DELETE) sempre, e POST/PATCH apenas com `idempotency_key`
|
|
15
|
+
— aí o servidor reconhece a repetição e devolve a mesma resposta.
|
|
16
|
+
|
|
17
|
+
**Passe `idempotency_key` nas chamadas que criam cobrança.** Sem ela,
|
|
18
|
+
uma falha de rede agora vira erro em vez de cobrança duplicada, que é o
|
|
19
|
+
comportamento correto mas significa que o retry passa a ser seu.
|
|
20
|
+
|
|
21
|
+
## 0.2.1 — 2026-06-29
|
|
22
|
+
|
|
23
|
+
### Documentação
|
|
24
|
+
- README: seção "Organizações LLC (US)" — USD, cartão + PIX cross-border,
|
|
25
|
+
liquidação `auto_payout`, onboarding/KYC. O SDK é passthrough (retorna
|
|
26
|
+
`dict`), então os campos LLC (`currency`, `client_secret`,
|
|
27
|
+
`presentment_*`) já vêm nas respostas sem mudança de código.
|
|
28
|
+
|
|
29
|
+
## 0.2.0 — 2026-05-16
|
|
30
|
+
|
|
31
|
+
### Adicionado
|
|
32
|
+
- `customers.save_card()` / `saved_card()` / `delete_saved_card()` —
|
|
33
|
+
tokenização + persistência de cartão preferido (one-click buy).
|
|
34
|
+
Requer `consent` com `accepted_at` + `ip` (LGPD).
|
|
35
|
+
- `customers.saved_card_management_link()` — gera URL com JWT 24h pro
|
|
36
|
+
cliente final ver/remover o próprio cartão.
|
|
37
|
+
- `charges.create_with_saved_card()` — cobrança one-click server-side
|
|
38
|
+
(sem CVV) usando o token salvo.
|
|
39
|
+
- `customers.one_click_link()` — gera link assinado de recompra (JWT
|
|
40
|
+
15min, uso único) pra o cliente final confirmar a compra com 1 clique
|
|
41
|
+
numa página hospedada pela Vitrin. Aceita `coupon_code` e `bump_ids`
|
|
42
|
+
(só produto).
|
|
43
|
+
|
|
44
|
+
## 0.1.0 — Release inicial
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vitrin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: SDK oficial Python para a API da Vitrin Digital
|
|
5
5
|
Project-URL: Homepage, https://vitrin.digital
|
|
6
6
|
Project-URL: Documentation, https://vitrin.digital/docs/integracao
|
|
@@ -53,6 +53,17 @@ vitrin = Vitrin(
|
|
|
53
53
|
|
|
54
54
|
Use `vd_test_*` em desenvolvimento, `vd_live_*` em produção.
|
|
55
55
|
|
|
56
|
+
## Organizações LLC (US)
|
|
57
|
+
|
|
58
|
+
Se a org é uma **US LLC** (`entity_type == "llc"`), os valores são em **USD**
|
|
59
|
+
(campo `currency` nas transações); os métodos default são **cartão** e **Pix
|
|
60
|
+
cross-border** (o cliente paga em BRL, a LLC recebe em USD). A liquidação é
|
|
61
|
+
**automática**: os endpoints de saldo/recebíveis/transferências retornam
|
|
62
|
+
`{ "auto_payout": true }` em vez de saldo retido (não há saque/antecipação
|
|
63
|
+
manual; o cronograma "PIX D+1 / Boleto D+2 / Cartão Nx D+30" abaixo é só para
|
|
64
|
+
CNPJ). O onboarding inclui verificação de identidade (KYC). Veja
|
|
65
|
+
`docs/api/onboarding-llc.md` e `docs/api/differences-cnpj-llc.md`.
|
|
66
|
+
|
|
56
67
|
## Recursos
|
|
57
68
|
|
|
58
69
|
### Clientes
|
|
@@ -164,6 +175,10 @@ Retry automático em **429** e **5xx** com backoff exponencial (default: 3 tenta
|
|
|
164
175
|
|
|
165
176
|
Inclua `idempotency_key` em POSTs sensíveis. Se o request chegar duas vezes (retry de rede, deploy etc), a Vitrin reconhece pela chave e devolve a mesma resposta — sem cobrar duas vezes.
|
|
166
177
|
|
|
178
|
+
Vale em `charges.create()` e `charges.create_with_saved_card()`. A chave é escopada por organização (a sua nunca colide com a de outro lojista) e aceita até 128 caracteres.
|
|
179
|
+
|
|
180
|
+
Use um identificador **seu** e estável — número do pedido, id do carrinho. A proteção vem de a chave se repetir no retry; um UUID novo a cada tentativa não protege de nada.
|
|
181
|
+
|
|
167
182
|
```python
|
|
168
183
|
vitrin.charges.create(
|
|
169
184
|
customer_id="cus_1",
|
|
@@ -25,6 +25,17 @@ vitrin = Vitrin(
|
|
|
25
25
|
|
|
26
26
|
Use `vd_test_*` em desenvolvimento, `vd_live_*` em produção.
|
|
27
27
|
|
|
28
|
+
## Organizações LLC (US)
|
|
29
|
+
|
|
30
|
+
Se a org é uma **US LLC** (`entity_type == "llc"`), os valores são em **USD**
|
|
31
|
+
(campo `currency` nas transações); os métodos default são **cartão** e **Pix
|
|
32
|
+
cross-border** (o cliente paga em BRL, a LLC recebe em USD). A liquidação é
|
|
33
|
+
**automática**: os endpoints de saldo/recebíveis/transferências retornam
|
|
34
|
+
`{ "auto_payout": true }` em vez de saldo retido (não há saque/antecipação
|
|
35
|
+
manual; o cronograma "PIX D+1 / Boleto D+2 / Cartão Nx D+30" abaixo é só para
|
|
36
|
+
CNPJ). O onboarding inclui verificação de identidade (KYC). Veja
|
|
37
|
+
`docs/api/onboarding-llc.md` e `docs/api/differences-cnpj-llc.md`.
|
|
38
|
+
|
|
28
39
|
## Recursos
|
|
29
40
|
|
|
30
41
|
### Clientes
|
|
@@ -136,6 +147,10 @@ Retry automático em **429** e **5xx** com backoff exponencial (default: 3 tenta
|
|
|
136
147
|
|
|
137
148
|
Inclua `idempotency_key` em POSTs sensíveis. Se o request chegar duas vezes (retry de rede, deploy etc), a Vitrin reconhece pela chave e devolve a mesma resposta — sem cobrar duas vezes.
|
|
138
149
|
|
|
150
|
+
Vale em `charges.create()` e `charges.create_with_saved_card()`. A chave é escopada por organização (a sua nunca colide com a de outro lojista) e aceita até 128 caracteres.
|
|
151
|
+
|
|
152
|
+
Use um identificador **seu** e estável — número do pedido, id do carrinho. A proteção vem de a chave se repetir no retry; um UUID novo a cada tentativa não protege de nada.
|
|
153
|
+
|
|
139
154
|
```python
|
|
140
155
|
vitrin.charges.create(
|
|
141
156
|
customer_id="cus_1",
|
|
@@ -4,6 +4,10 @@ Wrapper sobre `requests.Session` com:
|
|
|
4
4
|
- Headers padrão (Authorization, User-Agent, Content-Type)
|
|
5
5
|
- Mapeamento de status HTTP → :class:`vitrin.errors.VitrinError`
|
|
6
6
|
- Retry automático em 429/5xx com backoff exponencial e jitter
|
|
7
|
+
- Retry em timeout/erro de rede SÓ quando é seguro: método idempotente
|
|
8
|
+
(GET/HEAD/PUT/DELETE) ou POST/PATCH com ``idempotency_key``. Sem isso o
|
|
9
|
+
retry de um POST cria uma segunda cobrança — o timeout não diz que o
|
|
10
|
+
servidor deixou de processar, só que a resposta se perdeu.
|
|
7
11
|
- Idempotency-Key opcional pra POSTs
|
|
8
12
|
"""
|
|
9
13
|
|
|
@@ -77,13 +81,13 @@ class Client:
|
|
|
77
81
|
)
|
|
78
82
|
except requests.exceptions.Timeout as e:
|
|
79
83
|
last_exc = VitrinNetworkError(f'Timeout após {self.timeout}s')
|
|
80
|
-
if attempt < self.max_retries:
|
|
84
|
+
if attempt < self.max_retries and _pode_retentar(method, idempotency_key):
|
|
81
85
|
time.sleep(_backoff(attempt))
|
|
82
86
|
continue
|
|
83
87
|
raise last_exc from e
|
|
84
88
|
except requests.exceptions.RequestException as e:
|
|
85
89
|
last_exc = VitrinNetworkError(str(e))
|
|
86
|
-
if attempt < self.max_retries:
|
|
90
|
+
if attempt < self.max_retries and _pode_retentar(method, idempotency_key):
|
|
87
91
|
time.sleep(_backoff(attempt))
|
|
88
92
|
continue
|
|
89
93
|
raise last_exc from e
|
|
@@ -186,6 +190,23 @@ def _parse_retry_after(value: str | None) -> float | None:
|
|
|
186
190
|
return None
|
|
187
191
|
|
|
188
192
|
|
|
193
|
+
|
|
194
|
+
_METODOS_IDEMPOTENTES = frozenset({'GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS'})
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _pode_retentar(method: str, idempotency_key: str | None) -> bool:
|
|
198
|
+
"""Se é seguro repetir a chamada depois de timeout ou falha de rede.
|
|
199
|
+
|
|
200
|
+
Diferente de um 5xx, que garante que o servidor NÃO concluiu, um timeout
|
|
201
|
+
não diz nada: o pedido pode ter sido processado e só a resposta ter se
|
|
202
|
+
perdido. Repetir um POST nesse estado duplica o efeito — em cobrança,
|
|
203
|
+
duplica o débito. Com `idempotency_key` o servidor reconhece a repetição e
|
|
204
|
+
devolve a mesma resposta, então aí é seguro.
|
|
205
|
+
"""
|
|
206
|
+
if method.upper() in _METODOS_IDEMPOTENTES:
|
|
207
|
+
return True
|
|
208
|
+
return bool(idempotency_key)
|
|
209
|
+
|
|
189
210
|
def _backoff(attempt: int) -> float:
|
|
190
211
|
base = 0.25 * (2 ** attempt)
|
|
191
212
|
return base + random.uniform(0, base * 0.5)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Testes do client HTTP (mocked com responses)."""
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
import requests
|
|
4
5
|
import responses
|
|
5
6
|
|
|
6
7
|
from vitrin import (
|
|
7
8
|
Vitrin,
|
|
8
9
|
VitrinAuthError,
|
|
10
|
+
VitrinNetworkError,
|
|
9
11
|
VitrinNotFoundError,
|
|
10
12
|
VitrinRateLimitError,
|
|
11
13
|
VitrinServerError,
|
|
@@ -134,3 +136,62 @@ def test_customers_namespaces_exposed():
|
|
|
134
136
|
json={'id': 'cus_x', 'name': 'Maria'}, status=200)
|
|
135
137
|
v = Vitrin(api_key='k')
|
|
136
138
|
assert v.customers.retrieve('cus_x')['name'] == 'Maria'
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── Retry em timeout / erro de rede ───────────────────────────────────
|
|
142
|
+
#
|
|
143
|
+
# Timeout é o caso em que o servidor MUITO PROVAVELMENTE recebeu e processou o
|
|
144
|
+
# pedido — o que não chegou foi a resposta. Retentar um POST assim cria uma
|
|
145
|
+
# segunda cobrança. Medido na produção da Vitrin em 26/08/2026: o POST
|
|
146
|
+
# /charges/ tem p50 de 2,3s e cauda até 16,9s, e integrador com timeout curto
|
|
147
|
+
# gerava cobrança duplicada exatamente por esse caminho.
|
|
148
|
+
#
|
|
149
|
+
# Regra: GET/HEAD/PUT/DELETE são idempotentes por HTTP e podem ser retentados.
|
|
150
|
+
# POST e PATCH só com `idempotency_key`, que faz o servidor devolver a mesma
|
|
151
|
+
# resposta em vez de agir duas vezes.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@responses.activate
|
|
155
|
+
def test_post_sem_chave_nao_retenta_em_timeout():
|
|
156
|
+
responses.add(
|
|
157
|
+
responses.POST, f'{BASE}/charges/',
|
|
158
|
+
body=requests.exceptions.Timeout('estourou'),
|
|
159
|
+
)
|
|
160
|
+
with pytest.raises(VitrinNetworkError):
|
|
161
|
+
Vitrin(api_key='k', max_retries=3).request('/charges/', method='POST', body={})
|
|
162
|
+
assert len(responses.calls) == 1
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@responses.activate
|
|
166
|
+
def test_post_com_chave_retenta_em_timeout():
|
|
167
|
+
responses.add(
|
|
168
|
+
responses.POST, f'{BASE}/charges/',
|
|
169
|
+
body=requests.exceptions.Timeout('estourou'),
|
|
170
|
+
)
|
|
171
|
+
with pytest.raises(VitrinNetworkError):
|
|
172
|
+
Vitrin(api_key='k', max_retries=2).request(
|
|
173
|
+
'/charges/', method='POST', body={}, idempotency_key='pedido-1',
|
|
174
|
+
)
|
|
175
|
+
assert len(responses.calls) == 3 # 1 inicial + 2 retries
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@responses.activate
|
|
179
|
+
def test_get_retenta_em_timeout():
|
|
180
|
+
responses.add(
|
|
181
|
+
responses.GET, f'{BASE}/transactions/',
|
|
182
|
+
body=requests.exceptions.Timeout('estourou'),
|
|
183
|
+
)
|
|
184
|
+
with pytest.raises(VitrinNetworkError):
|
|
185
|
+
Vitrin(api_key='k', max_retries=2).request('/transactions/')
|
|
186
|
+
assert len(responses.calls) == 3
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@responses.activate
|
|
190
|
+
def test_post_sem_chave_nao_retenta_em_erro_de_rede():
|
|
191
|
+
responses.add(
|
|
192
|
+
responses.POST, f'{BASE}/charges/',
|
|
193
|
+
body=requests.exceptions.ConnectionError('conexão caiu'),
|
|
194
|
+
)
|
|
195
|
+
with pytest.raises(VitrinNetworkError):
|
|
196
|
+
Vitrin(api_key='k', max_retries=3).request('/charges/', method='POST', body={})
|
|
197
|
+
assert len(responses.calls) == 1
|
vitrin-0.2.0/CHANGELOG.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Changelog — vitrin
|
|
2
|
-
|
|
3
|
-
## 0.2.0 — 2026-05-16
|
|
4
|
-
|
|
5
|
-
### Adicionado
|
|
6
|
-
- `customers.save_card()` / `saved_card()` / `delete_saved_card()` —
|
|
7
|
-
tokenização + persistência de cartão preferido (one-click buy).
|
|
8
|
-
Requer `consent` com `accepted_at` + `ip` (LGPD).
|
|
9
|
-
- `customers.saved_card_management_link()` — gera URL com JWT 24h pro
|
|
10
|
-
cliente final ver/remover o próprio cartão.
|
|
11
|
-
- `charges.create_with_saved_card()` — cobrança one-click server-side
|
|
12
|
-
(sem CVV) usando o token salvo.
|
|
13
|
-
- `customers.one_click_link()` — gera link assinado de recompra (JWT
|
|
14
|
-
15min, uso único) pra o cliente final confirmar a compra com 1 clique
|
|
15
|
-
numa página hospedada pela Vitrin. Aceita `coupon_code` e `bump_ids`
|
|
16
|
-
(só produto).
|
|
17
|
-
|
|
18
|
-
## 0.1.0 — Release inicial
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|