solvapay-python 0.7.1__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.1 → solvapay_python-0.7.2}/CHANGELOG.md +15 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/PKG-INFO +82 -3
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/README.md +81 -2
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/pyproject.toml +1 -1
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/langchain-paywall/pyproject.toml +1 -1
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/pyproject.toml +1 -1
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/__init__.py +1 -1
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/paywall.py +23 -18
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_lifecycle.py +3 -1
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/uv.lock +1 -1
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/.github/workflows/ci.yml +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/.github/workflows/publish.yml +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/.gitignore +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/.python-version +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/LICENSE +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/.env.example +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/.gitignore +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/README.md +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/claim.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/server.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/fastmcp-paywall/uv.lock +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/langchain-paywall/.env.example +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/langchain-paywall/.gitignore +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/langchain-paywall/README.md +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/langchain-paywall/agent.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/.env.example +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/.streamlit/config.toml +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/PLAN.md +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/README.md +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/agents.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/app.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/demo_customers.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/requirements.txt +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/sdk_gateway.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/examples/marketplace/ui_components.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/_async_client.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/_config.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/_http.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/client.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/events.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/exceptions.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/fastapi.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/idempotency.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/langchain.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/models.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/paywall_state.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/py.typed +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/src/solvapay/webhooks.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/__init__.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/conftest.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_admin.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_async_client.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_checkout.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_config.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_customer.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_errors.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_http.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_idempotency.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_invariants.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_langchain.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_limits.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_packaging.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_paywall.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_paywall_state.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_redaction.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_webhook_events.py +0 -0
- {solvapay_python-0.7.1 → solvapay_python-0.7.2}/tests/test_webhooks.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
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
|
+
|
|
3
18
|
## 0.7.1 — 2026-05-17
|
|
4
19
|
|
|
5
20
|
Payments-grade hardening: structured errors, idempotency keys, py.typed, structured logging.
|
|
@@ -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
|
|
@@ -35,12 +35,15 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
|
|
36
36
|
Community Python SDK for [SolvaPay](https://solvapay.com) — payment rails for the agentic economy.
|
|
37
37
|
|
|
38
|
-
> **Status:** v0.
|
|
38
|
+
> **Status:** v0.7.2, community-maintained. Available on PyPI. Pending official adoption.
|
|
39
39
|
> Mirrors the most-used surface of [@solvapay/core](https://github.com/solvapay/solvapay-sdk).
|
|
40
40
|
|
|
41
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.
|
|
42
42
|
|
|
43
|
-
> **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.
|
|
44
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.
|
|
45
48
|
> **v0.4:** Async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events.
|
|
46
49
|
|
|
@@ -150,12 +153,69 @@ if not limits.within_limits:
|
|
|
150
153
|
print(d.checkout_url) # "https://solvapay.com/c/..."
|
|
151
154
|
```
|
|
152
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
|
+
|
|
153
212
|
## Examples
|
|
154
213
|
|
|
155
214
|
| Path | What it shows |
|
|
156
215
|
|---|---|
|
|
157
216
|
| [`examples/fastmcp-paywall/`](examples/fastmcp-paywall/) | FastMCP server with two paywalled tools. Demo for `@paywall.require` + MCP. |
|
|
158
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. |
|
|
159
219
|
|
|
160
220
|
## TS ↔ Python parity
|
|
161
221
|
|
|
@@ -199,6 +259,22 @@ session = sv.create_checkout_session(
|
|
|
199
259
|
| `cancel_purchase` | `POST /v1/sdk/purchases/{ref}/cancel` | Cancel a subscription |
|
|
200
260
|
| `reactivate_purchase` | `POST /v1/sdk/purchases/{ref}/reactivate` | Reactivate cancelled purchase |
|
|
201
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
|
+
|
|
202
278
|
All methods available on both `SolvaPay` (sync) and `AsyncSolvaPay` (async).
|
|
203
279
|
|
|
204
280
|
## Webhook handler (FastAPI)
|
|
@@ -261,6 +337,9 @@ async def handle_webhook(request: Request) -> dict:
|
|
|
261
337
|
- v0.4 — async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events ✅
|
|
262
338
|
- v0.5 — paywall state classifier, LangChain `monetize_tool` decorator ✅
|
|
263
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 ✅
|
|
264
343
|
|
|
265
344
|
## Contributing
|
|
266
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",
|
|
@@ -124,24 +124,29 @@ def require_async(
|
|
|
124
124
|
f"@paywall.require_async expected str kwarg '{customer_ref_arg}', "
|
|
125
125
|
f"got {type(customer_ref).__name__}"
|
|
126
126
|
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
_owns_client = not isinstance(client, AsyncSolvaPay)
|
|
128
|
+
sv: AsyncSolvaPay = AsyncSolvaPay() if _owns_client else client # type: ignore[assignment]
|
|
129
|
+
try:
|
|
130
|
+
limits = await sv.check_limits(
|
|
131
|
+
customer_ref=customer_ref,
|
|
132
|
+
product_ref=product,
|
|
133
|
+
plan_ref=plan,
|
|
134
|
+
)
|
|
135
|
+
if not limits.within_limits:
|
|
136
|
+
checkout_url = limits.checkout_url
|
|
137
|
+
if checkout_url is None:
|
|
138
|
+
try:
|
|
139
|
+
session = await sv.create_checkout_session(
|
|
140
|
+
customer_ref=customer_ref, product_ref=product, plan_ref=plan
|
|
141
|
+
)
|
|
142
|
+
checkout_url = session.checkout_url
|
|
143
|
+
except SolvaPayError:
|
|
144
|
+
pass
|
|
145
|
+
raise PaywallRequired(checkout_url=checkout_url)
|
|
146
|
+
return await fn(*args, **kwargs)
|
|
147
|
+
finally:
|
|
148
|
+
if _owns_client:
|
|
149
|
+
await sv.aclose()
|
|
145
150
|
|
|
146
151
|
return wrapper
|
|
147
152
|
|
|
@@ -109,7 +109,9 @@ def test_get_customer_balance_returns_balance(client: SolvaPay) -> None:
|
|
|
109
109
|
assert balance.customer_ref == "cus_1"
|
|
110
110
|
assert balance.credits == 4250
|
|
111
111
|
assert balance.credits_per_minor_unit == 100
|
|
112
|
-
assert
|
|
112
|
+
assert (
|
|
113
|
+
balance.balance == 0.425
|
|
114
|
+
) # derived: credits / credits_per_minor_unit / 100 (minor→major unit)
|
|
113
115
|
assert balance.currency == "USD"
|
|
114
116
|
|
|
115
117
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|