solvapay-python 0.7.0__tar.gz → 0.7.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.
- solvapay_python-0.7.2/CHANGELOG.md +68 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/PKG-INFO +86 -4
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/README.md +81 -2
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/pyproject.toml +1 -1
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/pyproject.toml +1 -1
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/pyproject.toml +5 -2
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/__init__.py +23 -2
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/_async_client.py +34 -6
- solvapay_python-0.7.2/src/solvapay/_http.py +220 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/client.py +29 -5
- solvapay_python-0.7.2/src/solvapay/exceptions.py +86 -0
- solvapay_python-0.7.2/src/solvapay/idempotency.py +16 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/paywall.py +23 -18
- solvapay_python-0.7.2/tests/__init__.py +0 -0
- solvapay_python-0.7.2/tests/test_errors.py +113 -0
- solvapay_python-0.7.2/tests/test_idempotency.py +62 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_lifecycle.py +3 -1
- solvapay_python-0.7.2/tests/test_packaging.py +9 -0
- solvapay_python-0.7.2/tests/test_redaction.py +57 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/uv.lock +1 -1
- solvapay_python-0.7.0/CHANGELOG.md +0 -45
- solvapay_python-0.7.0/src/solvapay/_http.py +0 -107
- solvapay_python-0.7.0/src/solvapay/exceptions.py +0 -16
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.github/workflows/ci.yml +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.github/workflows/publish.yml +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.gitignore +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.python-version +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/LICENSE +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/.env.example +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/.gitignore +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/README.md +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/claim.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/server.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/uv.lock +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/.env.example +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/.gitignore +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/README.md +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/agent.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/.env.example +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/.streamlit/config.toml +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/PLAN.md +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/README.md +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/agents.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/app.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/demo_customers.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/requirements.txt +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/sdk_gateway.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/ui_components.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/_config.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/events.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/fastapi.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/langchain.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/models.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/paywall_state.py +0 -0
- /solvapay_python-0.7.0/tests/__init__.py → /solvapay_python-0.7.2/src/solvapay/py.typed +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/webhooks.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/conftest.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_admin.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_async_client.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_checkout.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_config.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_customer.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_http.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_invariants.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_langchain.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_limits.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_paywall.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_paywall_state.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_webhook_events.py +0 -0
- {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_webhooks.py +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.2 — 2026-05-18
|
|
4
|
+
|
|
5
|
+
Bug fixes and documentation update.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`paywall.require_async`**: `AsyncSolvaPay()` instantiated without a caller-supplied `client=` was not closed after the decorated function returned, leaking the underlying `httpx.AsyncClient` connection pool. Wrapped in `try/finally`; `await sv.aclose()` called when the decorator owns the client.
|
|
9
|
+
- **`examples/fastmcp-paywall/pyproject.toml`**: dependency used wrong PyPI dist name (`solvapay`) and a stale `@v0.3.0` pin. Updated to `solvapay-python>=0.7.2`.
|
|
10
|
+
- **`examples/langchain-paywall/pyproject.toml`**: same wrong dist name and stale `@v0.5.0` pin. Updated to `solvapay-python[langchain]>=0.7.2`.
|
|
11
|
+
|
|
12
|
+
### Docs
|
|
13
|
+
- **README** updated to v0.7.1 surface: `paywall_state.gate()`, error hierarchy, idempotency keys, admin ops table, marketplace example, roadmap entries for v0.7.0 and v0.7.1.
|
|
14
|
+
|
|
15
|
+
### Internal
|
|
16
|
+
- `tests/test_lifecycle.py` reformatted (ruff format compliance)
|
|
17
|
+
|
|
18
|
+
## 0.7.1 — 2026-05-17
|
|
19
|
+
|
|
20
|
+
Payments-grade hardening: structured errors, idempotency keys, py.typed, structured logging.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Structured error hierarchy** (`exceptions.py`): `APIError` base with `status_code`, `request_id`, `error_code`, `error_message`. Subclasses: `AuthenticationError` (401), `PermissionError` (403), `NotFoundError` (404), `RateLimitError` (429, adds `.retry_after`), `InvalidRequestError` (4xx), `APIServerError` (5xx), `APIConnectionError`, `APITimeoutError`. `SolvaPayAPIError` aliased to `APIError` for back-compat.
|
|
24
|
+
- **Idempotency keys** on all mutating ops: `create_checkout_session`, `ensure_customer`, `track_usage`, `cancel_purchase`, `reactivate_purchase`, `create_product`, `clone_product`, `create_plan` all accept `idempotency_key: str | None = None`. Header casing: `Idempotency-Key` (matches TS SDK).
|
|
25
|
+
- **`solvapay.idempotency.from_payload(*parts)`** — SHA256 of stable-serialized payload parts → 32-hex-char deterministic key.
|
|
26
|
+
- **PEP 561 `py.typed` marker** — downstream mypy users no longer see `Any` on `solvapay.*` imports.
|
|
27
|
+
- **Structured logging** at `solvapay.http` logger: INFO on success (method, path, status, request_id, duration_ms), WARNING on 4xx/5xx (adds body_excerpt ≤200 chars). Never calls `logging.basicConfig`. Optional `logger=` injection on `SolvaPay`/`AsyncSolvaPay` constructors for `loguru`/`structlog`.
|
|
28
|
+
- **Secret redaction**: `Authorization` header never appears in logs. Verified by `tests/test_redaction.py`.
|
|
29
|
+
- **pyproject classifiers**: `Development Status :: 4 - Beta`, `Framework :: AsyncIO`, `Topic :: Office/Business :: Financial`, `Typing :: Typed`.
|
|
30
|
+
|
|
31
|
+
### Internal
|
|
32
|
+
- User-Agent bumped to `solvapay-python/0.7.1`
|
|
33
|
+
- 142 tests (up from 125), `mypy --strict` clean, `ruff` clean
|
|
34
|
+
|
|
35
|
+
## 0.7.0 — 2026-05-17
|
|
36
|
+
|
|
37
|
+
Real-API alignment after testing against the SolvaPay sandbox revealed wire-format mismatches.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **`Customer.customer_ref`** now accepts `reference` (real API) in addition to `customerRef` via `validation_alias=AliasChoices(...)`.
|
|
41
|
+
- **`ensure_customer()`** reads `reference` from API response; falls back to `customerRef`. Raises if neither present.
|
|
42
|
+
- **`BalanceResponse`** rewritten: `credits`, `display_currency`, `credits_per_minor_unit`, `display_exchange_rate`; `balance` and `currency` kept as computed properties.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **`paywall_state.gate()`** — one-call enrichment helper (limits + checkout URL + plan via `get_customer`).
|
|
46
|
+
- **`paywall.require` / `require_async`**: auto-mints checkout URL when `LimitResponse.checkout_url is None`.
|
|
47
|
+
- **`examples/marketplace/`** — Streamlit demo with 4 paywalled AI agents (Google Gemini) against real SolvaPay sandbox.
|
|
48
|
+
|
|
49
|
+
### Internal
|
|
50
|
+
- User-Agent `solvapay-python/0.7.0`. 125 tests (up from 121). `mypy --strict` clean.
|
|
51
|
+
|
|
52
|
+
## 0.6.0
|
|
53
|
+
- Admin endpoints (products, plans, merchant config). GitHub Actions trusted-publish to PyPI.
|
|
54
|
+
|
|
55
|
+
## 0.5.0
|
|
56
|
+
- `paywall_state` classifier (ACTIVATION_REQUIRED / TOPUP_REQUIRED / UPGRADE_REQUIRED). LangChain `monetize_tool`.
|
|
57
|
+
|
|
58
|
+
## 0.4.0
|
|
59
|
+
- Full async client. 5 lifecycle operations. 13 typed webhook event classes.
|
|
60
|
+
|
|
61
|
+
## 0.3.0
|
|
62
|
+
- FastMCP example: AI agent with two paywalled tools.
|
|
63
|
+
|
|
64
|
+
## 0.2.0
|
|
65
|
+
- `@paywall.require` decorator. FastAPI webhook router.
|
|
66
|
+
|
|
67
|
+
## 0.1.0
|
|
68
|
+
- Sync client, HMAC-SHA256 webhook verification, Pydantic v2 models, CI on 3 Python versions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solvapay-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Community Python SDK for SolvaPay (agent-native payment rails)
|
|
5
5
|
Project-URL: Homepage, https://github.com/dhruv-sanan/solvapay-python
|
|
6
6
|
Project-URL: Issues, https://github.com/dhruv-sanan/solvapay-python/issues
|
|
@@ -9,14 +9,17 @@ Author: Dhruv Sanan
|
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: agents,fintech,mcp,payments,solvapay
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
13
14
|
Classifier: Intended Audience :: Developers
|
|
14
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
16
|
Classifier: Programming Language :: Python :: 3
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
20
23
|
Requires-Python: >=3.10
|
|
21
24
|
Requires-Dist: httpx>=0.27
|
|
22
25
|
Requires-Dist: pydantic>=2.5
|
|
@@ -32,12 +35,15 @@ Description-Content-Type: text/markdown
|
|
|
32
35
|
|
|
33
36
|
Community Python SDK for [SolvaPay](https://solvapay.com) — payment rails for the agentic economy.
|
|
34
37
|
|
|
35
|
-
> **Status:** v0.
|
|
38
|
+
> **Status:** v0.7.2, community-maintained. Available on PyPI. Pending official adoption.
|
|
36
39
|
> Mirrors the most-used surface of [@solvapay/core](https://github.com/solvapay/solvapay-sdk).
|
|
37
40
|
|
|
38
41
|
Python is the dominant language for agent frameworks (LangChain, FastMCP, CrewAI, AutoGen). SolvaPay's official SDK is TypeScript-only. This SDK brings first-class Python support so agent developers can gate tools behind paywalls without switching ecosystems.
|
|
39
42
|
|
|
40
|
-
> **New in v0.
|
|
43
|
+
> **New in v0.7.2:** Async resource leak fix in `@paywall.require_async` — `AsyncSolvaPay` now properly closed when owned by the decorator. Example dep fixes.
|
|
44
|
+
> **v0.7.1:** Full error hierarchy (`AuthenticationError`, `NotFoundError`, `RateLimitError`, `APIConnectionError`, `APITimeoutError`), idempotency keys on all mutating ops, `py.typed` PEP 561 marker, structured HTTP logging.
|
|
45
|
+
> **New in v0.7.0:** Real-API alignment (wire-format fixes), `paywall_state.gate()` enrichment helper, marketplace Streamlit demo.
|
|
46
|
+
> **v0.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
|
|
41
47
|
> **v0.5:** Paywall state classifier (`paywall_state` module) and LangChain `monetize_tool` decorator — gate any LangChain tool behind a SolvaPay paywall with one line.
|
|
42
48
|
> **v0.4:** Async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events.
|
|
43
49
|
|
|
@@ -147,12 +153,69 @@ if not limits.within_limits:
|
|
|
147
153
|
print(d.checkout_url) # "https://solvapay.com/c/..."
|
|
148
154
|
```
|
|
149
155
|
|
|
156
|
+
For real-API calls use `gate()` instead — it enriches the bare `/v1/sdk/limits` response (which has no `plan` or `checkout_url`) in one call:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from solvapay.paywall_state import gate
|
|
160
|
+
|
|
161
|
+
decision = gate(sv, customer_ref="cus_x", product_ref="prd_y")
|
|
162
|
+
# decision.state — PaywallState.UPGRADE_REQUIRED (etc)
|
|
163
|
+
# decision.checkout_url — minted via create_checkout_session if needed
|
|
164
|
+
# decision.message — TS-style copy with URL inlined
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Error handling
|
|
168
|
+
|
|
169
|
+
v0.7.1 ships a structured exception hierarchy under `SolvaPayError`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import time
|
|
173
|
+
from solvapay import (
|
|
174
|
+
SolvaPayError, # catch-all
|
|
175
|
+
APIError, # base for all HTTP errors — has .status_code, .request_id
|
|
176
|
+
AuthenticationError, # 401
|
|
177
|
+
NotFoundError, # 404
|
|
178
|
+
RateLimitError, # 429 — adds .retry_after
|
|
179
|
+
APIConnectionError, # network failure
|
|
180
|
+
APITimeoutError, # request timeout
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
sv.get_customer("cus_missing")
|
|
185
|
+
except NotFoundError as e:
|
|
186
|
+
print(e.status_code, e.request_id)
|
|
187
|
+
except RateLimitError as e:
|
|
188
|
+
time.sleep(float(e.retry_after or 1))
|
|
189
|
+
except SolvaPayError:
|
|
190
|
+
raise
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Use `except SolvaPayError` as the catch-all. Prefer specific subclasses over checking `.status_code`.
|
|
194
|
+
|
|
195
|
+
## Idempotency keys
|
|
196
|
+
|
|
197
|
+
All mutating ops accept an optional `idempotency_key` to make retries safe:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from solvapay.idempotency import from_payload
|
|
201
|
+
|
|
202
|
+
key = from_payload("checkout", customer_ref, product_ref)
|
|
203
|
+
session = sv.create_checkout_session(
|
|
204
|
+
customer_ref=customer_ref,
|
|
205
|
+
product_ref="prd_xyz",
|
|
206
|
+
idempotency_key=key,
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`from_payload(*parts)` hashes its args to a 32-hex-char deterministic key. Pass the same key on retry — SolvaPay deduplicates server-side.
|
|
211
|
+
|
|
150
212
|
## Examples
|
|
151
213
|
|
|
152
214
|
| Path | What it shows |
|
|
153
215
|
|---|---|
|
|
154
216
|
| [`examples/fastmcp-paywall/`](examples/fastmcp-paywall/) | FastMCP server with two paywalled tools. Demo for `@paywall.require` + MCP. |
|
|
155
217
|
| [`examples/langchain-paywall/`](examples/langchain-paywall/) | LangChain agent with `monetize_tool`. Shows paywall response in agent trace. |
|
|
218
|
+
| [`examples/marketplace/`](examples/marketplace/) | Streamlit demo — paywalled AI-agent marketplace. Real sandbox, real Gemini LLM, two demo customers (one subscribed, one free tier). Shows `paywall_state.gate()` in action. |
|
|
156
219
|
|
|
157
220
|
## TS ↔ Python parity
|
|
158
221
|
|
|
@@ -196,6 +259,22 @@ session = sv.create_checkout_session(
|
|
|
196
259
|
| `cancel_purchase` | `POST /v1/sdk/purchases/{ref}/cancel` | Cancel a subscription |
|
|
197
260
|
| `reactivate_purchase` | `POST /v1/sdk/purchases/{ref}/reactivate` | Reactivate cancelled purchase |
|
|
198
261
|
|
|
262
|
+
**Admin (new in v0.6):**
|
|
263
|
+
|
|
264
|
+
| Python | Verb + path | Description |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| `list_products` | `GET /v1/sdk/products` | List all products |
|
|
267
|
+
| `get_product` | `GET /v1/sdk/products/{ref}` | Fetch product |
|
|
268
|
+
| `create_product` | `POST /v1/sdk/products` | Create product |
|
|
269
|
+
| `delete_product` | `DELETE /v1/sdk/products/{ref}` | Delete product |
|
|
270
|
+
| `clone_product` | `POST /v1/sdk/products/{ref}/clone` | Clone product |
|
|
271
|
+
| `list_plans` | `GET /v1/sdk/products/{ref}/plans` | List plans |
|
|
272
|
+
| `create_plan` | `POST /v1/sdk/products/{ref}/plans` | Create plan |
|
|
273
|
+
| `update_plan` | `PUT /v1/sdk/products/{ref}/plans/{ref}` | Update plan |
|
|
274
|
+
| `delete_plan` | `DELETE /v1/sdk/products/{ref}/plans/{ref}` | Delete plan |
|
|
275
|
+
| `get_merchant` | `GET /v1/sdk/merchant` | Merchant info |
|
|
276
|
+
| `get_platform_config` | `GET /v1/sdk/platform-config` | Platform config |
|
|
277
|
+
|
|
199
278
|
All methods available on both `SolvaPay` (sync) and `AsyncSolvaPay` (async).
|
|
200
279
|
|
|
201
280
|
## Webhook handler (FastAPI)
|
|
@@ -258,6 +337,9 @@ async def handle_webhook(request: Request) -> dict:
|
|
|
258
337
|
- v0.4 — async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events ✅
|
|
259
338
|
- v0.5 — paywall state classifier, LangChain `monetize_tool` decorator ✅
|
|
260
339
|
- v0.6 — admin endpoints (products, plans, merchant, platform config), PyPI publish ✅
|
|
340
|
+
- v0.7.0 — real-API wire-format fixes, `paywall_state.gate()`, marketplace demo ✅
|
|
341
|
+
- v0.7.1 — structured error hierarchy, idempotency keys, `py.typed`, structured logging ✅
|
|
342
|
+
- v0.7.2 — async resource leak fix (`require_async`), example dep fixes ✅
|
|
261
343
|
|
|
262
344
|
## Contributing
|
|
263
345
|
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
Community Python SDK for [SolvaPay](https://solvapay.com) — payment rails for the agentic economy.
|
|
6
6
|
|
|
7
|
-
> **Status:** v0.
|
|
7
|
+
> **Status:** v0.7.2, community-maintained. Available on PyPI. Pending official adoption.
|
|
8
8
|
> Mirrors the most-used surface of [@solvapay/core](https://github.com/solvapay/solvapay-sdk).
|
|
9
9
|
|
|
10
10
|
Python is the dominant language for agent frameworks (LangChain, FastMCP, CrewAI, AutoGen). SolvaPay's official SDK is TypeScript-only. This SDK brings first-class Python support so agent developers can gate tools behind paywalls without switching ecosystems.
|
|
11
11
|
|
|
12
|
-
> **New in v0.
|
|
12
|
+
> **New in v0.7.2:** Async resource leak fix in `@paywall.require_async` — `AsyncSolvaPay` now properly closed when owned by the decorator. Example dep fixes.
|
|
13
|
+
> **v0.7.1:** Full error hierarchy (`AuthenticationError`, `NotFoundError`, `RateLimitError`, `APIConnectionError`, `APITimeoutError`), idempotency keys on all mutating ops, `py.typed` PEP 561 marker, structured HTTP logging.
|
|
14
|
+
> **New in v0.7.0:** Real-API alignment (wire-format fixes), `paywall_state.gate()` enrichment helper, marketplace Streamlit demo.
|
|
15
|
+
> **v0.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
|
|
13
16
|
> **v0.5:** Paywall state classifier (`paywall_state` module) and LangChain `monetize_tool` decorator — gate any LangChain tool behind a SolvaPay paywall with one line.
|
|
14
17
|
> **v0.4:** Async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events.
|
|
15
18
|
|
|
@@ -119,12 +122,69 @@ if not limits.within_limits:
|
|
|
119
122
|
print(d.checkout_url) # "https://solvapay.com/c/..."
|
|
120
123
|
```
|
|
121
124
|
|
|
125
|
+
For real-API calls use `gate()` instead — it enriches the bare `/v1/sdk/limits` response (which has no `plan` or `checkout_url`) in one call:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from solvapay.paywall_state import gate
|
|
129
|
+
|
|
130
|
+
decision = gate(sv, customer_ref="cus_x", product_ref="prd_y")
|
|
131
|
+
# decision.state — PaywallState.UPGRADE_REQUIRED (etc)
|
|
132
|
+
# decision.checkout_url — minted via create_checkout_session if needed
|
|
133
|
+
# decision.message — TS-style copy with URL inlined
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Error handling
|
|
137
|
+
|
|
138
|
+
v0.7.1 ships a structured exception hierarchy under `SolvaPayError`:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import time
|
|
142
|
+
from solvapay import (
|
|
143
|
+
SolvaPayError, # catch-all
|
|
144
|
+
APIError, # base for all HTTP errors — has .status_code, .request_id
|
|
145
|
+
AuthenticationError, # 401
|
|
146
|
+
NotFoundError, # 404
|
|
147
|
+
RateLimitError, # 429 — adds .retry_after
|
|
148
|
+
APIConnectionError, # network failure
|
|
149
|
+
APITimeoutError, # request timeout
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
sv.get_customer("cus_missing")
|
|
154
|
+
except NotFoundError as e:
|
|
155
|
+
print(e.status_code, e.request_id)
|
|
156
|
+
except RateLimitError as e:
|
|
157
|
+
time.sleep(float(e.retry_after or 1))
|
|
158
|
+
except SolvaPayError:
|
|
159
|
+
raise
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Use `except SolvaPayError` as the catch-all. Prefer specific subclasses over checking `.status_code`.
|
|
163
|
+
|
|
164
|
+
## Idempotency keys
|
|
165
|
+
|
|
166
|
+
All mutating ops accept an optional `idempotency_key` to make retries safe:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from solvapay.idempotency import from_payload
|
|
170
|
+
|
|
171
|
+
key = from_payload("checkout", customer_ref, product_ref)
|
|
172
|
+
session = sv.create_checkout_session(
|
|
173
|
+
customer_ref=customer_ref,
|
|
174
|
+
product_ref="prd_xyz",
|
|
175
|
+
idempotency_key=key,
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`from_payload(*parts)` hashes its args to a 32-hex-char deterministic key. Pass the same key on retry — SolvaPay deduplicates server-side.
|
|
180
|
+
|
|
122
181
|
## Examples
|
|
123
182
|
|
|
124
183
|
| Path | What it shows |
|
|
125
184
|
|---|---|
|
|
126
185
|
| [`examples/fastmcp-paywall/`](examples/fastmcp-paywall/) | FastMCP server with two paywalled tools. Demo for `@paywall.require` + MCP. |
|
|
127
186
|
| [`examples/langchain-paywall/`](examples/langchain-paywall/) | LangChain agent with `monetize_tool`. Shows paywall response in agent trace. |
|
|
187
|
+
| [`examples/marketplace/`](examples/marketplace/) | Streamlit demo — paywalled AI-agent marketplace. Real sandbox, real Gemini LLM, two demo customers (one subscribed, one free tier). Shows `paywall_state.gate()` in action. |
|
|
128
188
|
|
|
129
189
|
## TS ↔ Python parity
|
|
130
190
|
|
|
@@ -168,6 +228,22 @@ session = sv.create_checkout_session(
|
|
|
168
228
|
| `cancel_purchase` | `POST /v1/sdk/purchases/{ref}/cancel` | Cancel a subscription |
|
|
169
229
|
| `reactivate_purchase` | `POST /v1/sdk/purchases/{ref}/reactivate` | Reactivate cancelled purchase |
|
|
170
230
|
|
|
231
|
+
**Admin (new in v0.6):**
|
|
232
|
+
|
|
233
|
+
| Python | Verb + path | Description |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `list_products` | `GET /v1/sdk/products` | List all products |
|
|
236
|
+
| `get_product` | `GET /v1/sdk/products/{ref}` | Fetch product |
|
|
237
|
+
| `create_product` | `POST /v1/sdk/products` | Create product |
|
|
238
|
+
| `delete_product` | `DELETE /v1/sdk/products/{ref}` | Delete product |
|
|
239
|
+
| `clone_product` | `POST /v1/sdk/products/{ref}/clone` | Clone product |
|
|
240
|
+
| `list_plans` | `GET /v1/sdk/products/{ref}/plans` | List plans |
|
|
241
|
+
| `create_plan` | `POST /v1/sdk/products/{ref}/plans` | Create plan |
|
|
242
|
+
| `update_plan` | `PUT /v1/sdk/products/{ref}/plans/{ref}` | Update plan |
|
|
243
|
+
| `delete_plan` | `DELETE /v1/sdk/products/{ref}/plans/{ref}` | Delete plan |
|
|
244
|
+
| `get_merchant` | `GET /v1/sdk/merchant` | Merchant info |
|
|
245
|
+
| `get_platform_config` | `GET /v1/sdk/platform-config` | Platform config |
|
|
246
|
+
|
|
171
247
|
All methods available on both `SolvaPay` (sync) and `AsyncSolvaPay` (async).
|
|
172
248
|
|
|
173
249
|
## Webhook handler (FastAPI)
|
|
@@ -230,6 +306,9 @@ async def handle_webhook(request: Request) -> dict:
|
|
|
230
306
|
- v0.4 — async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events ✅
|
|
231
307
|
- v0.5 — paywall state classifier, LangChain `monetize_tool` decorator ✅
|
|
232
308
|
- v0.6 — admin endpoints (products, plans, merchant, platform config), PyPI publish ✅
|
|
309
|
+
- v0.7.0 — real-API wire-format fixes, `paywall_state.gate()`, marketplace demo ✅
|
|
310
|
+
- v0.7.1 — structured error hierarchy, idempotency keys, `py.typed`, structured logging ✅
|
|
311
|
+
- v0.7.2 — async resource leak fix (`require_async`), example dep fixes ✅
|
|
233
312
|
|
|
234
313
|
## Contributing
|
|
235
314
|
|
|
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|
|
4
4
|
description = "FastMCP server gated by SolvaPay paywall — demo for solvapay-python"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"solvapay
|
|
7
|
+
"solvapay-python>=0.7.1",
|
|
8
8
|
"fastmcp>=0.4",
|
|
9
9
|
"httpx>=0.27",
|
|
10
10
|
"python-dotenv>=1.0",
|
|
@@ -3,7 +3,7 @@ name = "langchain-paywall-example"
|
|
|
3
3
|
version = "0.1.0"
|
|
4
4
|
requires-python = ">=3.10"
|
|
5
5
|
dependencies = [
|
|
6
|
-
"solvapay[langchain]
|
|
6
|
+
"solvapay-python[langchain]>=0.7.1",
|
|
7
7
|
"langchain>=0.3",
|
|
8
8
|
"langchain-openai>=0.2",
|
|
9
9
|
"python-dotenv>=1.0",
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solvapay-python"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Community Python SDK for SolvaPay (agent-native payment rails)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -12,14 +12,17 @@ authors = [{ name = "Dhruv Sanan" }]
|
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
keywords = ["solvapay", "payments", "agents", "mcp", "fintech"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Framework :: AsyncIO",
|
|
16
17
|
"Intended Audience :: Developers",
|
|
17
18
|
"License :: OSI Approved :: MIT License",
|
|
18
19
|
"Programming Language :: Python :: 3",
|
|
19
20
|
"Programming Language :: Python :: 3.10",
|
|
20
21
|
"Programming Language :: Python :: 3.11",
|
|
21
22
|
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Office/Business :: Financial",
|
|
22
24
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Typing :: Typed",
|
|
23
26
|
]
|
|
24
27
|
dependencies = [
|
|
25
28
|
"httpx>=0.27",
|
|
@@ -21,24 +21,44 @@ from solvapay.events import (
|
|
|
21
21
|
PurchaseUpdated,
|
|
22
22
|
WebhookEvent,
|
|
23
23
|
)
|
|
24
|
-
from solvapay.exceptions import
|
|
24
|
+
from solvapay.exceptions import (
|
|
25
|
+
APIConnectionError,
|
|
26
|
+
APIError,
|
|
27
|
+
APIServerError,
|
|
28
|
+
APITimeoutError,
|
|
29
|
+
AuthenticationError,
|
|
30
|
+
InvalidRequestError,
|
|
31
|
+
NotFoundError,
|
|
32
|
+
PermissionError,
|
|
33
|
+
RateLimitError,
|
|
34
|
+
SolvaPayAPIError,
|
|
35
|
+
SolvaPayError,
|
|
36
|
+
)
|
|
25
37
|
from solvapay.models import BalanceResponse, Merchant, Plan, PlatformConfig, Product
|
|
26
38
|
from solvapay.paywall import PaywallRequired
|
|
27
39
|
from solvapay.webhooks import verify_webhook
|
|
28
40
|
|
|
29
41
|
__all__ = [
|
|
42
|
+
"APIConnectionError",
|
|
43
|
+
"APIError",
|
|
44
|
+
"APIServerError",
|
|
45
|
+
"APITimeoutError",
|
|
30
46
|
"AsyncSolvaPay",
|
|
47
|
+
"AuthenticationError",
|
|
31
48
|
"BalanceResponse",
|
|
32
49
|
"CheckoutSessionCreated",
|
|
33
50
|
"CustomerCreated",
|
|
34
51
|
"CustomerDeleted",
|
|
35
52
|
"CustomerUpdated",
|
|
53
|
+
"InvalidRequestError",
|
|
36
54
|
"Merchant",
|
|
55
|
+
"NotFoundError",
|
|
37
56
|
"PaymentFailed",
|
|
38
57
|
"PaymentRefundFailed",
|
|
39
58
|
"PaymentRefunded",
|
|
40
59
|
"PaymentSucceeded",
|
|
41
60
|
"PaywallRequired",
|
|
61
|
+
"PermissionError",
|
|
42
62
|
"Plan",
|
|
43
63
|
"PlatformConfig",
|
|
44
64
|
"Product",
|
|
@@ -47,6 +67,7 @@ __all__ = [
|
|
|
47
67
|
"PurchaseExpired",
|
|
48
68
|
"PurchaseSuspended",
|
|
49
69
|
"PurchaseUpdated",
|
|
70
|
+
"RateLimitError",
|
|
50
71
|
"SolvaPay",
|
|
51
72
|
"SolvaPayAPIError",
|
|
52
73
|
"SolvaPayError",
|
|
@@ -54,4 +75,4 @@ __all__ = [
|
|
|
54
75
|
"paywall",
|
|
55
76
|
"verify_webhook",
|
|
56
77
|
]
|
|
57
|
-
__version__ = "0.7.
|
|
78
|
+
__version__ = "0.7.2"
|
|
@@ -6,6 +6,7 @@ Use `async with AsyncSolvaPay() as sv: ...` for proper teardown.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
import time
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
@@ -56,11 +57,13 @@ class AsyncSolvaPay:
|
|
|
56
57
|
*,
|
|
57
58
|
base_url: str | None = None,
|
|
58
59
|
timeout: float = 30.0,
|
|
60
|
+
logger: logging.Logger | None = None,
|
|
59
61
|
) -> None:
|
|
60
62
|
self._http = AsyncHttpClient(
|
|
61
63
|
api_key=resolve_api_key(api_key),
|
|
62
64
|
base_url=resolve_base_url(base_url),
|
|
63
65
|
timeout=timeout,
|
|
66
|
+
logger=logger,
|
|
64
67
|
)
|
|
65
68
|
|
|
66
69
|
async def aclose(self) -> None:
|
|
@@ -79,6 +82,7 @@ class AsyncSolvaPay:
|
|
|
79
82
|
product_ref: str,
|
|
80
83
|
plan_ref: str | None = None,
|
|
81
84
|
return_url: str | None = None,
|
|
85
|
+
idempotency_key: str | None = None,
|
|
82
86
|
) -> CheckoutSession:
|
|
83
87
|
req = CheckoutSessionRequest(
|
|
84
88
|
customer_ref=customer_ref,
|
|
@@ -91,6 +95,7 @@ class AsyncSolvaPay:
|
|
|
91
95
|
"POST",
|
|
92
96
|
"/v1/sdk/checkout-sessions",
|
|
93
97
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
98
|
+
idempotency_key=idempotency_key,
|
|
94
99
|
)
|
|
95
100
|
)
|
|
96
101
|
return CheckoutSession.model_validate(data)
|
|
@@ -102,6 +107,7 @@ class AsyncSolvaPay:
|
|
|
102
107
|
*,
|
|
103
108
|
email: str | None = None,
|
|
104
109
|
name: str | None = None,
|
|
110
|
+
idempotency_key: str | None = None,
|
|
105
111
|
) -> str:
|
|
106
112
|
lookup_ref = external_ref or customer_ref
|
|
107
113
|
try:
|
|
@@ -122,7 +128,10 @@ class AsyncSolvaPay:
|
|
|
122
128
|
)
|
|
123
129
|
created = await self._http.send(
|
|
124
130
|
_RequestSpec(
|
|
125
|
-
"POST",
|
|
131
|
+
"POST",
|
|
132
|
+
"/v1/sdk/customers",
|
|
133
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
134
|
+
idempotency_key=idempotency_key,
|
|
126
135
|
)
|
|
127
136
|
)
|
|
128
137
|
ref = created.get("reference") or created.get("customerRef")
|
|
@@ -226,7 +235,11 @@ class AsyncSolvaPay:
|
|
|
226
235
|
return BalanceResponse.model_validate(data)
|
|
227
236
|
|
|
228
237
|
async def cancel_purchase(
|
|
229
|
-
self,
|
|
238
|
+
self,
|
|
239
|
+
purchase_ref: str,
|
|
240
|
+
*,
|
|
241
|
+
reason: str | None = None,
|
|
242
|
+
idempotency_key: str | None = None,
|
|
230
243
|
) -> dict[str, Any]:
|
|
231
244
|
"""Cancel a purchase. Maps to POST /v1/sdk/purchases/{ref}/cancel."""
|
|
232
245
|
req = CancelPurchaseRequest(reason=reason)
|
|
@@ -235,13 +248,20 @@ class AsyncSolvaPay:
|
|
|
235
248
|
"POST",
|
|
236
249
|
f"/v1/sdk/purchases/{purchase_ref}/cancel",
|
|
237
250
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
251
|
+
idempotency_key=idempotency_key,
|
|
238
252
|
)
|
|
239
253
|
)
|
|
240
254
|
|
|
241
|
-
async def reactivate_purchase(
|
|
255
|
+
async def reactivate_purchase(
|
|
256
|
+
self, purchase_ref: str, *, idempotency_key: str | None = None
|
|
257
|
+
) -> dict[str, Any]:
|
|
242
258
|
"""Reactivate a cancelled purchase. Maps to POST /v1/sdk/purchases/{ref}/reactivate."""
|
|
243
259
|
return await self._http.send(
|
|
244
|
-
_RequestSpec(
|
|
260
|
+
_RequestSpec(
|
|
261
|
+
"POST",
|
|
262
|
+
f"/v1/sdk/purchases/{purchase_ref}/reactivate",
|
|
263
|
+
idempotency_key=idempotency_key,
|
|
264
|
+
)
|
|
245
265
|
)
|
|
246
266
|
|
|
247
267
|
# --- Admin: Products ---
|
|
@@ -257,7 +277,9 @@ class AsyncSolvaPay:
|
|
|
257
277
|
data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/products/{product_ref}"))
|
|
258
278
|
return Product.model_validate(data)
|
|
259
279
|
|
|
260
|
-
async def create_product(
|
|
280
|
+
async def create_product(
|
|
281
|
+
self, *, name: str, type: str, default_currency: str, idempotency_key: str | None = None
|
|
282
|
+
) -> Product:
|
|
261
283
|
"""Create a product. Maps to POST /v1/sdk/products."""
|
|
262
284
|
req = CreateProductRequest(name=name, type=type, default_currency=default_currency)
|
|
263
285
|
data = await self._http.send(
|
|
@@ -265,6 +287,7 @@ class AsyncSolvaPay:
|
|
|
265
287
|
"POST",
|
|
266
288
|
"/v1/sdk/products",
|
|
267
289
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
290
|
+
idempotency_key=idempotency_key,
|
|
268
291
|
)
|
|
269
292
|
)
|
|
270
293
|
return Product.model_validate(data)
|
|
@@ -273,7 +296,9 @@ class AsyncSolvaPay:
|
|
|
273
296
|
"""Delete a product. Maps to DELETE /v1/sdk/products/{ref}."""
|
|
274
297
|
return await self._http.send(_RequestSpec("DELETE", f"/v1/sdk/products/{product_ref}"))
|
|
275
298
|
|
|
276
|
-
async def clone_product(
|
|
299
|
+
async def clone_product(
|
|
300
|
+
self, product_ref: str, *, new_name: str, idempotency_key: str | None = None
|
|
301
|
+
) -> Product:
|
|
277
302
|
"""Clone a product with a new name. Maps to POST /v1/sdk/products/{ref}/clone."""
|
|
278
303
|
req = CloneProductRequest(new_name=new_name)
|
|
279
304
|
data = await self._http.send(
|
|
@@ -281,6 +306,7 @@ class AsyncSolvaPay:
|
|
|
281
306
|
"POST",
|
|
282
307
|
f"/v1/sdk/products/{product_ref}/clone",
|
|
283
308
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
309
|
+
idempotency_key=idempotency_key,
|
|
284
310
|
)
|
|
285
311
|
)
|
|
286
312
|
return Product.model_validate(data)
|
|
@@ -302,6 +328,7 @@ class AsyncSolvaPay:
|
|
|
302
328
|
price: float | None = None,
|
|
303
329
|
currency: str | None = None,
|
|
304
330
|
interval: str | None = None,
|
|
331
|
+
idempotency_key: str | None = None,
|
|
305
332
|
) -> Plan:
|
|
306
333
|
"""Create a plan for a product. Maps to POST /v1/sdk/products/{ref}/plans."""
|
|
307
334
|
req = CreatePlanRequest(
|
|
@@ -312,6 +339,7 @@ class AsyncSolvaPay:
|
|
|
312
339
|
"POST",
|
|
313
340
|
f"/v1/sdk/products/{product_ref}/plans",
|
|
314
341
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
342
|
+
idempotency_key=idempotency_key,
|
|
315
343
|
)
|
|
316
344
|
)
|
|
317
345
|
return Plan.model_validate(data)
|