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.
Files changed (70) hide show
  1. solvapay_python-0.7.2/CHANGELOG.md +68 -0
  2. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/PKG-INFO +86 -4
  3. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/README.md +81 -2
  4. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/pyproject.toml +1 -1
  5. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/pyproject.toml +1 -1
  6. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/pyproject.toml +5 -2
  7. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/__init__.py +23 -2
  8. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/_async_client.py +34 -6
  9. solvapay_python-0.7.2/src/solvapay/_http.py +220 -0
  10. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/client.py +29 -5
  11. solvapay_python-0.7.2/src/solvapay/exceptions.py +86 -0
  12. solvapay_python-0.7.2/src/solvapay/idempotency.py +16 -0
  13. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/paywall.py +23 -18
  14. solvapay_python-0.7.2/tests/__init__.py +0 -0
  15. solvapay_python-0.7.2/tests/test_errors.py +113 -0
  16. solvapay_python-0.7.2/tests/test_idempotency.py +62 -0
  17. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_lifecycle.py +3 -1
  18. solvapay_python-0.7.2/tests/test_packaging.py +9 -0
  19. solvapay_python-0.7.2/tests/test_redaction.py +57 -0
  20. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/uv.lock +1 -1
  21. solvapay_python-0.7.0/CHANGELOG.md +0 -45
  22. solvapay_python-0.7.0/src/solvapay/_http.py +0 -107
  23. solvapay_python-0.7.0/src/solvapay/exceptions.py +0 -16
  24. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.github/workflows/ci.yml +0 -0
  25. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.github/workflows/publish.yml +0 -0
  26. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.gitignore +0 -0
  27. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/.python-version +0 -0
  28. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/LICENSE +0 -0
  29. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/.env.example +0 -0
  30. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/.gitignore +0 -0
  31. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/README.md +0 -0
  32. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/claim.py +0 -0
  33. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/server.py +0 -0
  34. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/fastmcp-paywall/uv.lock +0 -0
  35. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/.env.example +0 -0
  36. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/.gitignore +0 -0
  37. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/README.md +0 -0
  38. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/langchain-paywall/agent.py +0 -0
  39. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/.env.example +0 -0
  40. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/.streamlit/config.toml +0 -0
  41. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/PLAN.md +0 -0
  42. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/README.md +0 -0
  43. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/agents.py +0 -0
  44. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/app.py +0 -0
  45. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/demo_customers.py +0 -0
  46. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/requirements.txt +0 -0
  47. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/sdk_gateway.py +0 -0
  48. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/examples/marketplace/ui_components.py +0 -0
  49. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/_config.py +0 -0
  50. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/events.py +0 -0
  51. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/fastapi.py +0 -0
  52. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/langchain.py +0 -0
  53. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/models.py +0 -0
  54. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/paywall_state.py +0 -0
  55. /solvapay_python-0.7.0/tests/__init__.py → /solvapay_python-0.7.2/src/solvapay/py.typed +0 -0
  56. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/src/solvapay/webhooks.py +0 -0
  57. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/conftest.py +0 -0
  58. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_admin.py +0 -0
  59. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_async_client.py +0 -0
  60. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_checkout.py +0 -0
  61. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_config.py +0 -0
  62. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_customer.py +0 -0
  63. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_http.py +0 -0
  64. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_invariants.py +0 -0
  65. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_langchain.py +0 -0
  66. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_limits.py +0 -0
  67. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_paywall.py +0 -0
  68. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_paywall_state.py +0 -0
  69. {solvapay_python-0.7.0 → solvapay_python-0.7.2}/tests/test_webhook_events.py +0 -0
  70. {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.0
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 :: 3 - Alpha
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.6, community-maintained. Available on PyPI. Pending official adoption.
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.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
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.6, community-maintained. Available on PyPI. Pending official adoption.
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.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
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 @ git+https://github.com/dhruv-sanan/solvapay-python@v0.3.0",
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] @ git+https://github.com/dhruv-sanan/solvapay-python@v0.5.0",
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.0"
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 :: 3 - Alpha",
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 SolvaPayAPIError, SolvaPayError
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.0"
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", "/v1/sdk/customers", json=req.model_dump(by_alias=True, exclude_none=True)
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, purchase_ref: str, *, reason: str | None = None
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(self, purchase_ref: str) -> dict[str, Any]:
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("POST", f"/v1/sdk/purchases/{purchase_ref}/reactivate")
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(self, *, name: str, type: str, default_currency: str) -> 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(self, product_ref: str, *, new_name: str) -> 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)