cobrala 1.0.0__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.
- cobrala-1.0.0/.gitignore +16 -0
- cobrala-1.0.0/.gitlab-ci.yml +87 -0
- cobrala-1.0.0/CHANGELOG.md +26 -0
- cobrala-1.0.0/LICENSE +21 -0
- cobrala-1.0.0/PKG-INFO +114 -0
- cobrala-1.0.0/README.md +87 -0
- cobrala-1.0.0/pyproject.toml +56 -0
- cobrala-1.0.0/src/cobrala/__init__.py +18 -0
- cobrala-1.0.0/src/cobrala/client.py +321 -0
- cobrala-1.0.0/src/cobrala/py.typed +0 -0
- cobrala-1.0.0/src/cobrala/webhooks.py +37 -0
- cobrala-1.0.0/tests/calls.py +126 -0
- cobrala-1.0.0/tests/conftest.py +99 -0
- cobrala-1.0.0/tests/fixtures/openapi.json +3973 -0
- cobrala-1.0.0/tests/test_contract.py +74 -0
- cobrala-1.0.0/tests/test_errors.py +69 -0
- cobrala-1.0.0/tests/test_pages.py +43 -0
- cobrala-1.0.0/tests/test_requests.py +54 -0
- cobrala-1.0.0/tests/test_webhooks.py +47 -0
cobrala-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# CI del paquete. La raíz del repositorio es el paquete.
|
|
2
|
+
#
|
|
3
|
+
# Publicar es empujar un tag:
|
|
4
|
+
#
|
|
5
|
+
# git tag -a v1.0.0 -m "1.0.0" && git push --tags
|
|
6
|
+
#
|
|
7
|
+
# El tag corre lint, tipos y tests; recién si pasan se construye y se sube a
|
|
8
|
+
# PyPI. La subida usa trusted publishing (OIDC), así que no hay ningún token
|
|
9
|
+
# guardado en el repositorio.
|
|
10
|
+
|
|
11
|
+
stages:
|
|
12
|
+
- check
|
|
13
|
+
- build
|
|
14
|
+
- publish
|
|
15
|
+
|
|
16
|
+
default:
|
|
17
|
+
image: python:3.12
|
|
18
|
+
cache:
|
|
19
|
+
key:
|
|
20
|
+
files:
|
|
21
|
+
- pyproject.toml
|
|
22
|
+
paths:
|
|
23
|
+
- .cache/pip
|
|
24
|
+
before_script:
|
|
25
|
+
- pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
variables:
|
|
28
|
+
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
|
29
|
+
|
|
30
|
+
# Verificación
|
|
31
|
+
|
|
32
|
+
lint:
|
|
33
|
+
stage: check
|
|
34
|
+
script:
|
|
35
|
+
- ruff check src tests
|
|
36
|
+
- mypy
|
|
37
|
+
|
|
38
|
+
# Los tests no usan la red ni a Cobrala: un servidor local contesta lo que pide
|
|
39
|
+
# cada test, y las rutas se comparan con el OpenAPI de tests/fixtures/.
|
|
40
|
+
test:
|
|
41
|
+
stage: check
|
|
42
|
+
parallel:
|
|
43
|
+
matrix:
|
|
44
|
+
- PYTHON: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
45
|
+
image: python:$PYTHON
|
|
46
|
+
script:
|
|
47
|
+
- pytest -q
|
|
48
|
+
|
|
49
|
+
# Empaquetado y publicación, solo con tag
|
|
50
|
+
|
|
51
|
+
build:
|
|
52
|
+
stage: build
|
|
53
|
+
rules:
|
|
54
|
+
- if: $CI_COMMIT_TAG
|
|
55
|
+
before_script:
|
|
56
|
+
- pip install build twine
|
|
57
|
+
script:
|
|
58
|
+
# El tag y la versión del paquete tienen que decir lo mismo: si no, se
|
|
59
|
+
# publicaría una versión distinta de la que quedó marcada en el historial.
|
|
60
|
+
- |
|
|
61
|
+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
62
|
+
if [ "$CI_COMMIT_TAG" != "v$VERSION" ]; then
|
|
63
|
+
echo "El tag es $CI_COMMIT_TAG y pyproject.toml dice $VERSION."
|
|
64
|
+
echo "Esperaba el tag v$VERSION."
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
- python -m build
|
|
68
|
+
- twine check dist/*
|
|
69
|
+
artifacts:
|
|
70
|
+
paths:
|
|
71
|
+
- dist/
|
|
72
|
+
expire_in: 1 month
|
|
73
|
+
|
|
74
|
+
publish:
|
|
75
|
+
stage: publish
|
|
76
|
+
rules:
|
|
77
|
+
- if: $CI_COMMIT_TAG
|
|
78
|
+
dependencies:
|
|
79
|
+
- build
|
|
80
|
+
id_tokens:
|
|
81
|
+
PYPI_ID_TOKEN:
|
|
82
|
+
aud: pypi
|
|
83
|
+
before_script:
|
|
84
|
+
- pip install -U twine
|
|
85
|
+
# Sin usuario ni contraseña: twine detecta el token OIDC y lo canjea solo.
|
|
86
|
+
script:
|
|
87
|
+
- twine upload dist/*
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Los cambios notables de cada versión. El formato sigue
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y el versionado es
|
|
5
|
+
[semántico](https://semver.org/lang/es/).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-11-14
|
|
8
|
+
|
|
9
|
+
Primera versión publicada.
|
|
10
|
+
|
|
11
|
+
### Agregado
|
|
12
|
+
|
|
13
|
+
- Una función por cada ruta de la Access API (`/v1`): el catálogo (planes,
|
|
14
|
+
productos y features), los clientes (crearlos, editarlos y cambiarles el
|
|
15
|
+
código, y el link del portal), los permisos (verificar, consumir sin contar dos
|
|
16
|
+
veces, fijar el consumo y sincronizar), las suscripciones (bajas, cambios de
|
|
17
|
+
plan y su vista previa), los cobros (el link de pago y el comprobante), los
|
|
18
|
+
documentos y el checkout de suscripciones y de productos.
|
|
19
|
+
- `every`, para recorrer una lista página por página.
|
|
20
|
+
- `verify_webhook`, que verifica la firma de un webhook sobre el cuerpo crudo, y
|
|
21
|
+
`entitlement_rows`, que les pone nombre a las filas de los permisos.
|
|
22
|
+
- `CobralaError` con el código estable de cada rechazo; `unreachable` cuando
|
|
23
|
+
Cobrala no contesta.
|
|
24
|
+
- Los `429` se reintentan dos veces, esperando lo que dice `Retry-After`.
|
|
25
|
+
- `AsyncCobrala`, con los mismos métodos para `await`.
|
|
26
|
+
- Sin dependencias: solo la biblioteca estándar, Python 3.9 o más.
|
cobrala-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Araitek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cobrala-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cobrala
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the Access API of Cobrala: plans, entitlements, usage and checkout
|
|
5
|
+
Project-URL: Homepage, https://cobrala.com.py/
|
|
6
|
+
Project-URL: Documentación de la API, https://access-api.cobrala.com.py/docs
|
|
7
|
+
Author: Araitek
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: checkout,cobrala,cobros,entitlements,paraguay,suscripciones
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: Spanish
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# cobrala (Python)
|
|
29
|
+
|
|
30
|
+
SDK de la Access API de Cobrala, para Python 3.9 o más y sin dependencias. Envuelve cada ruta de la API, que además publica su OpenAPI en `/docs`, y se autentica con `X-API-Key: sk_...`. La versión para TypeScript y JavaScript tiene las mismas funciones: `npm install cobrala`.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install cobrala
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from cobrala import Cobrala, CobralaError, verify_webhook
|
|
38
|
+
|
|
39
|
+
cobrala = Cobrala(api_key="sk_...", base_url="https://access-api.cobrala.com.py")
|
|
40
|
+
|
|
41
|
+
# Mostrar los planes en tu sitio
|
|
42
|
+
for plan in cobrala.every(cobrala.plans):
|
|
43
|
+
print(plan["name"], plan["intervals"][0]["prices"][0]["price"])
|
|
44
|
+
|
|
45
|
+
# Vender: abrí la url que vuelve
|
|
46
|
+
sale = cobrala.checkout_subscription(
|
|
47
|
+
price_id=17, client_code="USR-42", legal_name="Ana", email="ana@example.com",
|
|
48
|
+
return_url="https://example.com/gracias",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Verificar y consumir
|
|
52
|
+
if cobrala.verify("USR-42", "facturas")["allowed"]:
|
|
53
|
+
cobrala.consume("USR-42", "facturas", idempotency_key="factura-9921")
|
|
54
|
+
|
|
55
|
+
# Un rechazo tiene su código
|
|
56
|
+
try:
|
|
57
|
+
cobrala.change_plan(sale["subscription_id"], price_id=18, actor="customer")
|
|
58
|
+
except CobralaError as error:
|
|
59
|
+
if error.code == "grace_pay_first":
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
# Un webhook, sobre el cuerpo crudo
|
|
63
|
+
valid = verify_webhook(secret, raw_body, headers["X-Webhook-Timestamp"], headers["X-Webhook-Signature"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`AsyncCobrala` tiene los mismos métodos, para `await`. El SDK no guarda respuestas: lo que dice la API tiene que estar bien siempre.
|
|
67
|
+
|
|
68
|
+
## Lo que resolvés en tu sistema
|
|
69
|
+
|
|
70
|
+
**La clave va solo en tu servidor.** Nunca en el navegador ni en una app móvil: quien la tenga hace todo lo que sus permisos dejan. Cada clave es de un servicio.
|
|
71
|
+
|
|
72
|
+
**Qué campo es el `client_code`.** Elegí un dato que tu sistema ya tenga, único y que no cambie: el id de tu usuario, por ejemplo `USR-42`. Es lo que une a tu usuario con su cliente en Cobrala.
|
|
73
|
+
- Si tu usuario ya existe, mandalo en el checkout. Si el cliente no existe en Cobrala, se crea en la misma llamada con el nombre y el correo que mandes.
|
|
74
|
+
- Si todavía no tiene cuenta (la página de precios abierta), no mandes código: Cobrala crea un cliente con un código generado (`CLI-8F3A21BC`) y lo devuelve junto con la dirección. Guardalo al abrirle la cuenta, o cambialo por el tuyo con `PATCH /v1/clients/{code}`.
|
|
75
|
+
- Cobrala nunca une clientes por correo: el mismo correo con otro código es otro cliente, porque dos personas pueden compartirlo. Para unir a uno que ya estaba cargado, buscalo con `GET /v1/clients?email=` y cambiale el código.
|
|
76
|
+
|
|
77
|
+
**Verificar en vivo o con una copia.** Las dos cosas son válidas:
|
|
78
|
+
- En vivo, `POST /v1/entitlements/verify` en cada acción sensible: siempre dice lo correcto, y cuenta contra el límite de 50 pedidos por segundo de tu clave.
|
|
79
|
+
- Con copia, `GET /v1/entitlements/sync` para armar menús y pantallas sin un pedido por feature. Cada fila trae `resets_at` y el vencimiento más cercano: un reinicio o un vencimiento no es un cambio y no te llega como tal, así que tu copia tiene que mirar esas fechas.
|
|
80
|
+
|
|
81
|
+
**Cada cuánto sincronizar.** Lo normal: el webhook `entitlements.changed` en el momento y, por las dudas, una sincronización cada 15 minutos con `since` igual al `as_of` de la anterior. Sin webhooks, sincronizá más seguido y al entrar cada usuario.
|
|
82
|
+
|
|
83
|
+
**Qué hacer si Cobrala no contesta.** Lo decidís vos según tu negocio. Con copia local, seguí con lo último que sabés. Sin copia, elegí entre dejar pasar (mejor para el usuario) o cortar (mejor para una feature cara), y dejalo escrito en tu código. El SDK reintenta solo los `429`, dos veces, esperando lo que dice `Retry-After`; si Cobrala no contesta, da un error con el código `unreachable`.
|
|
84
|
+
|
|
85
|
+
**Registrar consumo sin contar dos veces.** Mandá `idempotency_key` en cada consumo, con un valor propio del evento (`factura-9921`). Si reintentás con la misma clave, la API contesta lo que contestó y no suma.
|
|
86
|
+
|
|
87
|
+
**Vender.** Tu servidor pide el checkout con el `price_id` y abre la `url` que vuelve: es la página de Cobrala, donde el cliente paga. El resultado nunca lo dice la vuelta del navegador: confirmalo por webhook o verificando. El `return_url` es solo el botón "Volver a" de esa página.
|
|
88
|
+
|
|
89
|
+
**Cambiar de plan.** Con `operator` rige en el acto o se programa, y la diferencia sigue el flujo normal. Con `customer`, subir primero se paga por la `url` que devuelve y bajar se programa. `POST /v1/subscriptions/{id}/change-plan/preview` dice qué va a pasar antes de confirmar.
|
|
90
|
+
|
|
91
|
+
**Webhooks.** Verificá la firma sobre el cuerpo crudo, antes de parsearlo; el SDK trae la función. Las filas de `entitlements.changed` y de la sincronización tienen la misma forma, `[incluida, límite, resets_at, vencimiento]`, y el SDK trae otra función que les pone nombre.
|
|
92
|
+
|
|
93
|
+
## Lo que necesita cada función
|
|
94
|
+
|
|
95
|
+
| Qué hace | Ruta | Permiso |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| Planes, productos, features | `GET /v1/plans`, `/v1/products`, `/v1/features` | `plans`, `products`, `features` |
|
|
98
|
+
| Clientes | `GET /v1/clients`, `/v1/clients/{code}` | `clients` |
|
|
99
|
+
| Crear y editar clientes | `POST /v1/clients`, `PATCH /v1/clients/{code}` | `clients_write` |
|
|
100
|
+
| Verificar y sincronizar | `POST /v1/entitlements/verify`, `GET /v1/entitlements/sync` | `entitlements` |
|
|
101
|
+
| Consumir o fijar el consumo | `POST /v1/entitlements/consume`, `/set` | `entitlements_write` |
|
|
102
|
+
| Ver suscripciones y preview del cambio | `GET /v1/subscriptions`, `POST .../change-plan/preview` | `subscriptions` |
|
|
103
|
+
| Bajas y cambios de plan | `POST .../cancel`, `.../cancel/revert`, `.../change-plan`, `.../change-plan/cancel` | `subscriptions_write` |
|
|
104
|
+
| Ver cobros y pedir su link | `GET /v1/payments`, `POST /v1/payments/{id}/pay-link` | `payments` |
|
|
105
|
+
| Subir un comprobante | `POST /v1/payments/{id}/receipt` | `payments_receipt` |
|
|
106
|
+
| Documentos y su PDF | `GET /v1/documents` | `documents` |
|
|
107
|
+
| Vender | `POST /v1/checkout/subscription`, `/product` | `checkout` |
|
|
108
|
+
| Abrir el portal ya autenticado | `POST /v1/clients/{code}/portal-link` | `portal` |
|
|
109
|
+
|
|
110
|
+
Los rechazos son siempre `{"code": ..., "message": ..., "data": {...}}`, y el `code` es estable. Con la cuenta congelada, la clave lee pero no escribe (`account_frozen`); sin la API en el plan, contesta `plan_without_api`.
|
|
111
|
+
|
|
112
|
+
## Licencia
|
|
113
|
+
|
|
114
|
+
MIT. Ver [`LICENSE`](LICENSE).
|
cobrala-1.0.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# cobrala (Python)
|
|
2
|
+
|
|
3
|
+
SDK de la Access API de Cobrala, para Python 3.9 o más y sin dependencias. Envuelve cada ruta de la API, que además publica su OpenAPI en `/docs`, y se autentica con `X-API-Key: sk_...`. La versión para TypeScript y JavaScript tiene las mismas funciones: `npm install cobrala`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install cobrala
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from cobrala import Cobrala, CobralaError, verify_webhook
|
|
11
|
+
|
|
12
|
+
cobrala = Cobrala(api_key="sk_...", base_url="https://access-api.cobrala.com.py")
|
|
13
|
+
|
|
14
|
+
# Mostrar los planes en tu sitio
|
|
15
|
+
for plan in cobrala.every(cobrala.plans):
|
|
16
|
+
print(plan["name"], plan["intervals"][0]["prices"][0]["price"])
|
|
17
|
+
|
|
18
|
+
# Vender: abrí la url que vuelve
|
|
19
|
+
sale = cobrala.checkout_subscription(
|
|
20
|
+
price_id=17, client_code="USR-42", legal_name="Ana", email="ana@example.com",
|
|
21
|
+
return_url="https://example.com/gracias",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Verificar y consumir
|
|
25
|
+
if cobrala.verify("USR-42", "facturas")["allowed"]:
|
|
26
|
+
cobrala.consume("USR-42", "facturas", idempotency_key="factura-9921")
|
|
27
|
+
|
|
28
|
+
# Un rechazo tiene su código
|
|
29
|
+
try:
|
|
30
|
+
cobrala.change_plan(sale["subscription_id"], price_id=18, actor="customer")
|
|
31
|
+
except CobralaError as error:
|
|
32
|
+
if error.code == "grace_pay_first":
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
# Un webhook, sobre el cuerpo crudo
|
|
36
|
+
valid = verify_webhook(secret, raw_body, headers["X-Webhook-Timestamp"], headers["X-Webhook-Signature"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`AsyncCobrala` tiene los mismos métodos, para `await`. El SDK no guarda respuestas: lo que dice la API tiene que estar bien siempre.
|
|
40
|
+
|
|
41
|
+
## Lo que resolvés en tu sistema
|
|
42
|
+
|
|
43
|
+
**La clave va solo en tu servidor.** Nunca en el navegador ni en una app móvil: quien la tenga hace todo lo que sus permisos dejan. Cada clave es de un servicio.
|
|
44
|
+
|
|
45
|
+
**Qué campo es el `client_code`.** Elegí un dato que tu sistema ya tenga, único y que no cambie: el id de tu usuario, por ejemplo `USR-42`. Es lo que une a tu usuario con su cliente en Cobrala.
|
|
46
|
+
- Si tu usuario ya existe, mandalo en el checkout. Si el cliente no existe en Cobrala, se crea en la misma llamada con el nombre y el correo que mandes.
|
|
47
|
+
- Si todavía no tiene cuenta (la página de precios abierta), no mandes código: Cobrala crea un cliente con un código generado (`CLI-8F3A21BC`) y lo devuelve junto con la dirección. Guardalo al abrirle la cuenta, o cambialo por el tuyo con `PATCH /v1/clients/{code}`.
|
|
48
|
+
- Cobrala nunca une clientes por correo: el mismo correo con otro código es otro cliente, porque dos personas pueden compartirlo. Para unir a uno que ya estaba cargado, buscalo con `GET /v1/clients?email=` y cambiale el código.
|
|
49
|
+
|
|
50
|
+
**Verificar en vivo o con una copia.** Las dos cosas son válidas:
|
|
51
|
+
- En vivo, `POST /v1/entitlements/verify` en cada acción sensible: siempre dice lo correcto, y cuenta contra el límite de 50 pedidos por segundo de tu clave.
|
|
52
|
+
- Con copia, `GET /v1/entitlements/sync` para armar menús y pantallas sin un pedido por feature. Cada fila trae `resets_at` y el vencimiento más cercano: un reinicio o un vencimiento no es un cambio y no te llega como tal, así que tu copia tiene que mirar esas fechas.
|
|
53
|
+
|
|
54
|
+
**Cada cuánto sincronizar.** Lo normal: el webhook `entitlements.changed` en el momento y, por las dudas, una sincronización cada 15 minutos con `since` igual al `as_of` de la anterior. Sin webhooks, sincronizá más seguido y al entrar cada usuario.
|
|
55
|
+
|
|
56
|
+
**Qué hacer si Cobrala no contesta.** Lo decidís vos según tu negocio. Con copia local, seguí con lo último que sabés. Sin copia, elegí entre dejar pasar (mejor para el usuario) o cortar (mejor para una feature cara), y dejalo escrito en tu código. El SDK reintenta solo los `429`, dos veces, esperando lo que dice `Retry-After`; si Cobrala no contesta, da un error con el código `unreachable`.
|
|
57
|
+
|
|
58
|
+
**Registrar consumo sin contar dos veces.** Mandá `idempotency_key` en cada consumo, con un valor propio del evento (`factura-9921`). Si reintentás con la misma clave, la API contesta lo que contestó y no suma.
|
|
59
|
+
|
|
60
|
+
**Vender.** Tu servidor pide el checkout con el `price_id` y abre la `url` que vuelve: es la página de Cobrala, donde el cliente paga. El resultado nunca lo dice la vuelta del navegador: confirmalo por webhook o verificando. El `return_url` es solo el botón "Volver a" de esa página.
|
|
61
|
+
|
|
62
|
+
**Cambiar de plan.** Con `operator` rige en el acto o se programa, y la diferencia sigue el flujo normal. Con `customer`, subir primero se paga por la `url` que devuelve y bajar se programa. `POST /v1/subscriptions/{id}/change-plan/preview` dice qué va a pasar antes de confirmar.
|
|
63
|
+
|
|
64
|
+
**Webhooks.** Verificá la firma sobre el cuerpo crudo, antes de parsearlo; el SDK trae la función. Las filas de `entitlements.changed` y de la sincronización tienen la misma forma, `[incluida, límite, resets_at, vencimiento]`, y el SDK trae otra función que les pone nombre.
|
|
65
|
+
|
|
66
|
+
## Lo que necesita cada función
|
|
67
|
+
|
|
68
|
+
| Qué hace | Ruta | Permiso |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| Planes, productos, features | `GET /v1/plans`, `/v1/products`, `/v1/features` | `plans`, `products`, `features` |
|
|
71
|
+
| Clientes | `GET /v1/clients`, `/v1/clients/{code}` | `clients` |
|
|
72
|
+
| Crear y editar clientes | `POST /v1/clients`, `PATCH /v1/clients/{code}` | `clients_write` |
|
|
73
|
+
| Verificar y sincronizar | `POST /v1/entitlements/verify`, `GET /v1/entitlements/sync` | `entitlements` |
|
|
74
|
+
| Consumir o fijar el consumo | `POST /v1/entitlements/consume`, `/set` | `entitlements_write` |
|
|
75
|
+
| Ver suscripciones y preview del cambio | `GET /v1/subscriptions`, `POST .../change-plan/preview` | `subscriptions` |
|
|
76
|
+
| Bajas y cambios de plan | `POST .../cancel`, `.../cancel/revert`, `.../change-plan`, `.../change-plan/cancel` | `subscriptions_write` |
|
|
77
|
+
| Ver cobros y pedir su link | `GET /v1/payments`, `POST /v1/payments/{id}/pay-link` | `payments` |
|
|
78
|
+
| Subir un comprobante | `POST /v1/payments/{id}/receipt` | `payments_receipt` |
|
|
79
|
+
| Documentos y su PDF | `GET /v1/documents` | `documents` |
|
|
80
|
+
| Vender | `POST /v1/checkout/subscription`, `/product` | `checkout` |
|
|
81
|
+
| Abrir el portal ya autenticado | `POST /v1/clients/{code}/portal-link` | `portal` |
|
|
82
|
+
|
|
83
|
+
Los rechazos son siempre `{"code": ..., "message": ..., "data": {...}}`, y el `code` es estable. Con la cuenta congelada, la clave lee pero no escribe (`account_frozen`); sin la API en el plan, contesta `plan_without_api`.
|
|
84
|
+
|
|
85
|
+
## Licencia
|
|
86
|
+
|
|
87
|
+
MIT. Ver [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cobrala"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for the Access API of Cobrala: plans, entitlements, usage and checkout"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Araitek" }]
|
|
13
|
+
keywords = ["cobrala", "suscripciones", "cobros", "entitlements", "checkout", "paraguay"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
"Natural Language :: Spanish",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://cobrala.com.py/"
|
|
30
|
+
"Documentación de la API" = "https://access-api.cobrala.com.py/docs"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7",
|
|
35
|
+
"mypy>=1.8",
|
|
36
|
+
"ruff>=0.4",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/cobrala"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 125
|
|
44
|
+
target-version = "py39"
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E", "F", "I", "B", "SIM"]
|
|
48
|
+
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
strict = true
|
|
51
|
+
files = ["src/cobrala"]
|
|
52
|
+
# The SDK returns the JSON of the API as it arrived, without its own types.
|
|
53
|
+
warn_return_any = false
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cobrala: the Python SDK of the Access API of Cobrala.
|
|
3
|
+
|
|
4
|
+
from cobrala import Cobrala
|
|
5
|
+
|
|
6
|
+
cobrala = Cobrala(api_key="sk_...", base_url="https://access-api.cobrala.com.py")
|
|
7
|
+
verdict = cobrala.verify("USR-42", "invoices")
|
|
8
|
+
if verdict["allowed"]:
|
|
9
|
+
cobrala.consume("USR-42", "invoices", idempotency_key="factura-1")
|
|
10
|
+
|
|
11
|
+
It keeps no answers: what the API says has to be right every time. Only the
|
|
12
|
+
standard library is needed. ``AsyncCobrala`` has the same methods, to
|
|
13
|
+
``await``.
|
|
14
|
+
"""
|
|
15
|
+
from .client import AsyncCobrala, Cobrala, CobralaError, entitlement_rows
|
|
16
|
+
from .webhooks import verify_webhook
|
|
17
|
+
|
|
18
|
+
__all__ = ["AsyncCobrala", "Cobrala", "CobralaError", "entitlement_rows", "verify_webhook"]
|