solvapay-python 0.7.0__py3-none-any.whl → 0.7.2__py3-none-any.whl
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/__init__.py +23 -2
- solvapay/_async_client.py +34 -6
- solvapay/_http.py +135 -22
- solvapay/client.py +29 -5
- solvapay/exceptions.py +72 -2
- solvapay/idempotency.py +16 -0
- solvapay/paywall.py +23 -18
- solvapay/py.typed +0 -0
- {solvapay_python-0.7.0.dist-info → solvapay_python-0.7.2.dist-info}/METADATA +86 -4
- solvapay_python-0.7.2.dist-info/RECORD +19 -0
- solvapay_python-0.7.0.dist-info/RECORD +0 -17
- {solvapay_python-0.7.0.dist-info → solvapay_python-0.7.2.dist-info}/WHEEL +0 -0
- {solvapay_python-0.7.0.dist-info → solvapay_python-0.7.2.dist-info}/licenses/LICENSE +0 -0
solvapay/__init__.py
CHANGED
|
@@ -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"
|
solvapay/_async_client.py
CHANGED
|
@@ -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)
|
solvapay/_http.py
CHANGED
|
@@ -2,12 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
8
10
|
import httpx
|
|
9
11
|
|
|
10
|
-
from solvapay.exceptions import
|
|
12
|
+
from solvapay.exceptions import (
|
|
13
|
+
APIConnectionError,
|
|
14
|
+
APIError,
|
|
15
|
+
APIServerError,
|
|
16
|
+
APITimeoutError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
InvalidRequestError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
PermissionError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_logger = logging.getLogger("solvapay.http")
|
|
11
25
|
|
|
12
26
|
|
|
13
27
|
@dataclass(frozen=True)
|
|
@@ -22,22 +36,109 @@ class _RequestSpec:
|
|
|
22
36
|
return {"Idempotency-Key": self.idempotency_key} if self.idempotency_key else {}
|
|
23
37
|
|
|
24
38
|
|
|
39
|
+
def _parse_error(response: httpx.Response) -> APIError:
|
|
40
|
+
request_id = response.headers.get("x-request-id") or response.headers.get("x-correlation-id")
|
|
41
|
+
error_code: str | None = None
|
|
42
|
+
error_message: str | None = None
|
|
43
|
+
try:
|
|
44
|
+
payload = response.json()
|
|
45
|
+
if isinstance(payload, dict):
|
|
46
|
+
err = payload.get("error", payload)
|
|
47
|
+
if isinstance(err, dict):
|
|
48
|
+
code = err.get("code")
|
|
49
|
+
msg = err.get("message")
|
|
50
|
+
if isinstance(code, str):
|
|
51
|
+
error_code = code
|
|
52
|
+
if isinstance(msg, str):
|
|
53
|
+
error_message = msg
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
status = response.status_code
|
|
58
|
+
body = response.text
|
|
59
|
+
|
|
60
|
+
if status == 401:
|
|
61
|
+
return AuthenticationError(
|
|
62
|
+
status, body, request_id=request_id, error_code=error_code, error_message=error_message
|
|
63
|
+
)
|
|
64
|
+
if status == 403:
|
|
65
|
+
return PermissionError(
|
|
66
|
+
status, body, request_id=request_id, error_code=error_code, error_message=error_message
|
|
67
|
+
)
|
|
68
|
+
if status == 404:
|
|
69
|
+
return NotFoundError(
|
|
70
|
+
status, body, request_id=request_id, error_code=error_code, error_message=error_message
|
|
71
|
+
)
|
|
72
|
+
if status == 429:
|
|
73
|
+
return RateLimitError(
|
|
74
|
+
status,
|
|
75
|
+
body,
|
|
76
|
+
request_id=request_id,
|
|
77
|
+
error_code=error_code,
|
|
78
|
+
error_message=error_message,
|
|
79
|
+
retry_after=response.headers.get("Retry-After"),
|
|
80
|
+
)
|
|
81
|
+
if 400 <= status < 500:
|
|
82
|
+
return InvalidRequestError(
|
|
83
|
+
status, body, request_id=request_id, error_code=error_code, error_message=error_message
|
|
84
|
+
)
|
|
85
|
+
return APIServerError(
|
|
86
|
+
status, body, request_id=request_id, error_code=error_code, error_message=error_message
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
25
90
|
def _handle(response: httpx.Response) -> dict[str, Any]:
|
|
26
91
|
if not response.is_success:
|
|
27
|
-
raise
|
|
92
|
+
raise _parse_error(response)
|
|
28
93
|
if response.status_code == 204 or not response.content:
|
|
29
94
|
return {}
|
|
30
95
|
return response.json() # type: ignore[no-any-return]
|
|
31
96
|
|
|
32
97
|
|
|
98
|
+
def _log_response(
|
|
99
|
+
logger: logging.Logger,
|
|
100
|
+
spec: _RequestSpec,
|
|
101
|
+
response: httpx.Response,
|
|
102
|
+
duration_ms: int,
|
|
103
|
+
) -> None:
|
|
104
|
+
request_id = response.headers.get("x-request-id") or response.headers.get("x-correlation-id")
|
|
105
|
+
extra: dict[str, object] = {"request_id": request_id, "duration_ms": duration_ms}
|
|
106
|
+
if response.is_success:
|
|
107
|
+
logger.info(
|
|
108
|
+
"%s %s → %d (%dms)",
|
|
109
|
+
spec.method,
|
|
110
|
+
spec.path,
|
|
111
|
+
response.status_code,
|
|
112
|
+
duration_ms,
|
|
113
|
+
extra=extra,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
logger.warning(
|
|
117
|
+
"%s %s → %d (%dms)",
|
|
118
|
+
spec.method,
|
|
119
|
+
spec.path,
|
|
120
|
+
response.status_code,
|
|
121
|
+
duration_ms,
|
|
122
|
+
extra={**extra, "body_excerpt": response.text[:200]},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
33
126
|
class HttpClient:
|
|
34
|
-
def __init__(
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
api_key: str,
|
|
131
|
+
base_url: str,
|
|
132
|
+
timeout: float = 30.0,
|
|
133
|
+
logger: logging.Logger | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
self._logger = logger or _logger
|
|
35
136
|
self._client = httpx.Client(
|
|
36
137
|
base_url=base_url,
|
|
37
138
|
headers={
|
|
38
139
|
"Authorization": f"Bearer {api_key}",
|
|
39
140
|
"Content-Type": "application/json",
|
|
40
|
-
"User-Agent": "solvapay-python/0.7.
|
|
141
|
+
"User-Agent": "solvapay-python/0.7.1",
|
|
41
142
|
},
|
|
42
143
|
timeout=timeout,
|
|
43
144
|
)
|
|
@@ -52,15 +153,17 @@ class HttpClient:
|
|
|
52
153
|
self.close()
|
|
53
154
|
|
|
54
155
|
def send(self, spec: _RequestSpec) -> dict[str, Any]:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
spec.path,
|
|
59
|
-
json=spec.json,
|
|
60
|
-
params=spec.params,
|
|
61
|
-
headers=spec.headers(),
|
|
156
|
+
t0 = time.perf_counter()
|
|
157
|
+
try:
|
|
158
|
+
response = self._client.request(
|
|
159
|
+
spec.method, spec.path, json=spec.json, params=spec.params, headers=spec.headers()
|
|
62
160
|
)
|
|
63
|
-
|
|
161
|
+
except httpx.TimeoutException as exc:
|
|
162
|
+
raise APITimeoutError(str(exc)) from exc
|
|
163
|
+
except (httpx.ConnectError, httpx.ReadError) as exc:
|
|
164
|
+
raise APIConnectionError(str(exc)) from exc
|
|
165
|
+
_log_response(self._logger, spec, response, int((time.perf_counter() - t0) * 1000))
|
|
166
|
+
return _handle(response)
|
|
64
167
|
|
|
65
168
|
def request(
|
|
66
169
|
self,
|
|
@@ -75,13 +178,21 @@ class HttpClient:
|
|
|
75
178
|
|
|
76
179
|
|
|
77
180
|
class AsyncHttpClient:
|
|
78
|
-
def __init__(
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
api_key: str,
|
|
185
|
+
base_url: str,
|
|
186
|
+
timeout: float = 30.0,
|
|
187
|
+
logger: logging.Logger | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
self._logger = logger or _logger
|
|
79
190
|
self._client = httpx.AsyncClient(
|
|
80
191
|
base_url=base_url,
|
|
81
192
|
headers={
|
|
82
193
|
"Authorization": f"Bearer {api_key}",
|
|
83
194
|
"Content-Type": "application/json",
|
|
84
|
-
"User-Agent": "solvapay-python/0.7.
|
|
195
|
+
"User-Agent": "solvapay-python/0.7.1",
|
|
85
196
|
},
|
|
86
197
|
timeout=timeout,
|
|
87
198
|
)
|
|
@@ -96,12 +207,14 @@ class AsyncHttpClient:
|
|
|
96
207
|
await self.aclose()
|
|
97
208
|
|
|
98
209
|
async def send(self, spec: _RequestSpec) -> dict[str, Any]:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
spec.path,
|
|
103
|
-
json=spec.json,
|
|
104
|
-
params=spec.params,
|
|
105
|
-
headers=spec.headers(),
|
|
210
|
+
t0 = time.perf_counter()
|
|
211
|
+
try:
|
|
212
|
+
response = await self._client.request(
|
|
213
|
+
spec.method, spec.path, json=spec.json, params=spec.params, headers=spec.headers()
|
|
106
214
|
)
|
|
107
|
-
|
|
215
|
+
except httpx.TimeoutException as exc:
|
|
216
|
+
raise APITimeoutError(str(exc)) from exc
|
|
217
|
+
except (httpx.ConnectError, httpx.ReadError) as exc:
|
|
218
|
+
raise APIConnectionError(str(exc)) from exc
|
|
219
|
+
_log_response(self._logger, spec, response, int((time.perf_counter() - t0) * 1000))
|
|
220
|
+
return _handle(response)
|
solvapay/client.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import time
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -54,11 +55,13 @@ class SolvaPay:
|
|
|
54
55
|
*,
|
|
55
56
|
base_url: str | None = None,
|
|
56
57
|
timeout: float = 30.0,
|
|
58
|
+
logger: logging.Logger | None = None,
|
|
57
59
|
) -> None:
|
|
58
60
|
self._http = HttpClient(
|
|
59
61
|
api_key=resolve_api_key(api_key),
|
|
60
62
|
base_url=resolve_base_url(base_url),
|
|
61
63
|
timeout=timeout,
|
|
64
|
+
logger=logger,
|
|
62
65
|
)
|
|
63
66
|
|
|
64
67
|
def close(self) -> None:
|
|
@@ -77,6 +80,7 @@ class SolvaPay:
|
|
|
77
80
|
product_ref: str,
|
|
78
81
|
plan_ref: str | None = None,
|
|
79
82
|
return_url: str | None = None,
|
|
83
|
+
idempotency_key: str | None = None,
|
|
80
84
|
) -> CheckoutSession:
|
|
81
85
|
"""Create a hosted checkout session.
|
|
82
86
|
|
|
@@ -101,6 +105,7 @@ class SolvaPay:
|
|
|
101
105
|
"POST",
|
|
102
106
|
"/v1/sdk/checkout-sessions",
|
|
103
107
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
108
|
+
idempotency_key=idempotency_key,
|
|
104
109
|
)
|
|
105
110
|
return CheckoutSession.model_validate(data)
|
|
106
111
|
|
|
@@ -111,6 +116,7 @@ class SolvaPay:
|
|
|
111
116
|
*,
|
|
112
117
|
email: str | None = None,
|
|
113
118
|
name: str | None = None,
|
|
119
|
+
idempotency_key: str | None = None,
|
|
114
120
|
) -> str:
|
|
115
121
|
"""Idempotently create or look up a customer.
|
|
116
122
|
|
|
@@ -140,6 +146,7 @@ class SolvaPay:
|
|
|
140
146
|
"POST",
|
|
141
147
|
"/v1/sdk/customers",
|
|
142
148
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
149
|
+
idempotency_key=idempotency_key,
|
|
143
150
|
)
|
|
144
151
|
ref = created.get("reference") or created.get("customerRef")
|
|
145
152
|
if not ref:
|
|
@@ -238,18 +245,27 @@ class SolvaPay:
|
|
|
238
245
|
data = self._http.request("GET", f"/v1/sdk/customers/{customer_ref}/balance")
|
|
239
246
|
return BalanceResponse.model_validate(data)
|
|
240
247
|
|
|
241
|
-
def cancel_purchase(
|
|
248
|
+
def cancel_purchase(
|
|
249
|
+
self, purchase_ref: str, *, reason: str | None = None, idempotency_key: str | None = None
|
|
250
|
+
) -> dict[str, Any]:
|
|
242
251
|
"""Cancel a purchase. Maps to POST /v1/sdk/purchases/{ref}/cancel."""
|
|
243
252
|
req = CancelPurchaseRequest(reason=reason)
|
|
244
253
|
return self._http.request(
|
|
245
254
|
"POST",
|
|
246
255
|
f"/v1/sdk/purchases/{purchase_ref}/cancel",
|
|
247
256
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
257
|
+
idempotency_key=idempotency_key,
|
|
248
258
|
)
|
|
249
259
|
|
|
250
|
-
def reactivate_purchase(
|
|
260
|
+
def reactivate_purchase(
|
|
261
|
+
self, purchase_ref: str, *, idempotency_key: str | None = None
|
|
262
|
+
) -> dict[str, Any]:
|
|
251
263
|
"""Reactivate a cancelled purchase. Maps to POST /v1/sdk/purchases/{ref}/reactivate."""
|
|
252
|
-
return self._http.request(
|
|
264
|
+
return self._http.request(
|
|
265
|
+
"POST",
|
|
266
|
+
f"/v1/sdk/purchases/{purchase_ref}/reactivate",
|
|
267
|
+
idempotency_key=idempotency_key,
|
|
268
|
+
)
|
|
253
269
|
|
|
254
270
|
# --- Admin: Products ---
|
|
255
271
|
|
|
@@ -264,13 +280,16 @@ class SolvaPay:
|
|
|
264
280
|
data = self._http.request("GET", f"/v1/sdk/products/{product_ref}")
|
|
265
281
|
return Product.model_validate(data)
|
|
266
282
|
|
|
267
|
-
def create_product(
|
|
283
|
+
def create_product(
|
|
284
|
+
self, *, name: str, type: str, default_currency: str, idempotency_key: str | None = None
|
|
285
|
+
) -> Product:
|
|
268
286
|
"""Create a product. Maps to POST /v1/sdk/products."""
|
|
269
287
|
req = CreateProductRequest(name=name, type=type, default_currency=default_currency)
|
|
270
288
|
data = self._http.request(
|
|
271
289
|
"POST",
|
|
272
290
|
"/v1/sdk/products",
|
|
273
291
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
292
|
+
idempotency_key=idempotency_key,
|
|
274
293
|
)
|
|
275
294
|
return Product.model_validate(data)
|
|
276
295
|
|
|
@@ -278,13 +297,16 @@ class SolvaPay:
|
|
|
278
297
|
"""Delete a product. Maps to DELETE /v1/sdk/products/{ref}."""
|
|
279
298
|
return self._http.request("DELETE", f"/v1/sdk/products/{product_ref}")
|
|
280
299
|
|
|
281
|
-
def clone_product(
|
|
300
|
+
def clone_product(
|
|
301
|
+
self, product_ref: str, *, new_name: str, idempotency_key: str | None = None
|
|
302
|
+
) -> Product:
|
|
282
303
|
"""Clone a product with a new name. Maps to POST /v1/sdk/products/{ref}/clone."""
|
|
283
304
|
req = CloneProductRequest(new_name=new_name)
|
|
284
305
|
data = self._http.request(
|
|
285
306
|
"POST",
|
|
286
307
|
f"/v1/sdk/products/{product_ref}/clone",
|
|
287
308
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
309
|
+
idempotency_key=idempotency_key,
|
|
288
310
|
)
|
|
289
311
|
return Product.model_validate(data)
|
|
290
312
|
|
|
@@ -305,6 +327,7 @@ class SolvaPay:
|
|
|
305
327
|
price: float | None = None,
|
|
306
328
|
currency: str | None = None,
|
|
307
329
|
interval: str | None = None,
|
|
330
|
+
idempotency_key: str | None = None,
|
|
308
331
|
) -> Plan:
|
|
309
332
|
"""Create a plan for a product. Maps to POST /v1/sdk/products/{ref}/plans."""
|
|
310
333
|
req = CreatePlanRequest(
|
|
@@ -314,6 +337,7 @@ class SolvaPay:
|
|
|
314
337
|
"POST",
|
|
315
338
|
f"/v1/sdk/products/{product_ref}/plans",
|
|
316
339
|
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
340
|
+
idempotency_key=idempotency_key,
|
|
317
341
|
)
|
|
318
342
|
return Plan.model_validate(data)
|
|
319
343
|
|
solvapay/exceptions.py
CHANGED
|
@@ -7,10 +7,80 @@ class SolvaPayError(Exception):
|
|
|
7
7
|
"""Base exception for all SolvaPay SDK errors."""
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class APIError(SolvaPayError):
|
|
11
11
|
"""Raised when the SolvaPay API returns a non-2xx response."""
|
|
12
12
|
|
|
13
|
-
def __init__(
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
status_code: int,
|
|
16
|
+
body: str,
|
|
17
|
+
*,
|
|
18
|
+
request_id: str | None = None,
|
|
19
|
+
error_code: str | None = None,
|
|
20
|
+
error_message: str | None = None,
|
|
21
|
+
message: str | None = None,
|
|
22
|
+
) -> None:
|
|
14
23
|
self.status_code = status_code
|
|
15
24
|
self.body = body
|
|
25
|
+
self.request_id = request_id
|
|
26
|
+
self.error_code = error_code
|
|
27
|
+
self.error_message = error_message
|
|
16
28
|
super().__init__(message or f"SolvaPay API error {status_code}: {body}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthenticationError(APIError):
|
|
32
|
+
"""401 — invalid or missing API key."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PermissionError(APIError): # shadows built-in; intentional in SDK module namespace
|
|
36
|
+
"""403 — insufficient permissions for this operation."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotFoundError(APIError):
|
|
40
|
+
"""404 — requested resource does not exist."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RateLimitError(APIError):
|
|
44
|
+
"""429 — rate limit exceeded. Check .retry_after for the suggested delay (seconds)."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
status_code: int,
|
|
49
|
+
body: str,
|
|
50
|
+
*,
|
|
51
|
+
request_id: str | None = None,
|
|
52
|
+
error_code: str | None = None,
|
|
53
|
+
error_message: str | None = None,
|
|
54
|
+
message: str | None = None,
|
|
55
|
+
retry_after: str | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__(
|
|
58
|
+
status_code,
|
|
59
|
+
body,
|
|
60
|
+
request_id=request_id,
|
|
61
|
+
error_code=error_code,
|
|
62
|
+
error_message=error_message,
|
|
63
|
+
message=message,
|
|
64
|
+
)
|
|
65
|
+
self.retry_after = retry_after
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InvalidRequestError(APIError):
|
|
69
|
+
"""4xx (non-401/403/404/429) — malformed or invalid request."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class APIServerError(APIError):
|
|
73
|
+
"""5xx — server-side error."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class APIConnectionError(SolvaPayError):
|
|
77
|
+
"""Network-level error — connection refused, reset, or DNS failure."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class APITimeoutError(SolvaPayError):
|
|
81
|
+
"""Request timed out before the server responded."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Back-compat alias: existing `except SolvaPayAPIError` catches and `.status_code` access
|
|
85
|
+
# continue to work. Will emit DeprecationWarning in v1.0 pre-tag.
|
|
86
|
+
SolvaPayAPIError = APIError
|
solvapay/idempotency.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Idempotency key helpers for SolvaPay SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def from_payload(*parts: str | int | float | None) -> str:
|
|
10
|
+
"""SHA256 of stable-serialized payload parts → 32-hex-char key.
|
|
11
|
+
|
|
12
|
+
Deterministic, 24h-safe. Caller is responsible for input stability.
|
|
13
|
+
Retried POSTs must supply the *same* key as the original call.
|
|
14
|
+
"""
|
|
15
|
+
data = json.dumps(parts, separators=(",", ":"), default=str)
|
|
16
|
+
return hashlib.sha256(data.encode()).hexdigest()[:32]
|
solvapay/paywall.py
CHANGED
|
@@ -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
|
|
solvapay/py.typed
ADDED
|
File without changes
|
|
@@ -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
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
solvapay/__init__.py,sha256=1GgHO6-iQViSW0vZGKghMV1NMHDtWEy-gM-SRr06Ch0,1774
|
|
2
|
+
solvapay/_async_client.py,sha256=4RtP3K5NlkqOSfqDNS26Hzit4aUbh68HSvCDUVrmYoY,13509
|
|
3
|
+
solvapay/_config.py,sha256=yPU_GM77pXwPfIzuncuLRNv4322q5bB9qaawN3izEB8,606
|
|
4
|
+
solvapay/_http.py,sha256=ug396rug0EsKCgs_rgi8iYMkfm0hKOlQQ2sEjyUz22o,6709
|
|
5
|
+
solvapay/client.py,sha256=D-tvloBnTCweh_AwFYSEYgQaqXaSMT3j_eQ1UjlUYnM,13321
|
|
6
|
+
solvapay/events.py,sha256=m2VLrbLLJpiPfuTryNSL2hB6pHfq4YO0a9kjhT4hniY,1844
|
|
7
|
+
solvapay/exceptions.py,sha256=QKWARp-l-7-WnwJC03qbmktgjGI9q0sNmckzE0NPf6U,2384
|
|
8
|
+
solvapay/fastapi.py,sha256=vouWqbc_Yurdg8dIZ7zhO4-J_QxpYnyNgwyLHgDEoHs,2125
|
|
9
|
+
solvapay/idempotency.py,sha256=LzsLw6TeaeLJbMVqZSEQ-038f6oIUomCmQDBqzlKMGE,514
|
|
10
|
+
solvapay/langchain.py,sha256=VOrze5-7COE4--kn8JqbyrmwhWwv3XwN3Ol3YMpffgc,2353
|
|
11
|
+
solvapay/models.py,sha256=SsmkSLH5xp-Hgf2GjhDcrqGt5FoWKtXJkDZUQE7wwWI,6810
|
|
12
|
+
solvapay/paywall.py,sha256=OQCX8XT73IWDaFyRHDz_19ZYOkXY891O_zDbpI73D7U,5771
|
|
13
|
+
solvapay/paywall_state.py,sha256=pfP_-5B6LRcWrvItwgDwh-9f-DSowYaFDKXxGJvYPrg,7048
|
|
14
|
+
solvapay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
solvapay/webhooks.py,sha256=6EuoISjJzrKEUIoNpt__l2FnJvQR4T7XnT7fzOFHl5U,2738
|
|
16
|
+
solvapay_python-0.7.2.dist-info/METADATA,sha256=2Fbd-hzM9PsqJEEBiWXbYuIh6OWrq5CUlZieDfMueCg,12767
|
|
17
|
+
solvapay_python-0.7.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
18
|
+
solvapay_python-0.7.2.dist-info/licenses/LICENSE,sha256=wJURmEXLdSdApQdHG-RCwBoZVka1Oux8zNrLxGC30S8,1068
|
|
19
|
+
solvapay_python-0.7.2.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
solvapay/__init__.py,sha256=m9gn3b3cyL4ZExnevrrkSWfIScB_ELHSDXb-G7PA5CQ,1365
|
|
2
|
-
solvapay/_async_client.py,sha256=Y9zGFPMNwqA-vAdr8hnTVjvRRpr05piaX2el5bkQCSc,12644
|
|
3
|
-
solvapay/_config.py,sha256=yPU_GM77pXwPfIzuncuLRNv4322q5bB9qaawN3izEB8,606
|
|
4
|
-
solvapay/_http.py,sha256=LsOdoVycSWFW8GimGq4dZpfKFGz1ZvHgWAPhmDpuVsM,3026
|
|
5
|
-
solvapay/client.py,sha256=3x-SjM3sLRi86A1_tIGX2snmo7IMaJXd93qaU_h86CA,12548
|
|
6
|
-
solvapay/events.py,sha256=m2VLrbLLJpiPfuTryNSL2hB6pHfq4YO0a9kjhT4hniY,1844
|
|
7
|
-
solvapay/exceptions.py,sha256=PLWXtAo8UsBqxScqq2AnWANNRFlzryq2vljt_SnB1jw,511
|
|
8
|
-
solvapay/fastapi.py,sha256=vouWqbc_Yurdg8dIZ7zhO4-J_QxpYnyNgwyLHgDEoHs,2125
|
|
9
|
-
solvapay/langchain.py,sha256=VOrze5-7COE4--kn8JqbyrmwhWwv3XwN3Ol3YMpffgc,2353
|
|
10
|
-
solvapay/models.py,sha256=SsmkSLH5xp-Hgf2GjhDcrqGt5FoWKtXJkDZUQE7wwWI,6810
|
|
11
|
-
solvapay/paywall.py,sha256=jlMZ9LG6GNL010R8fV30cKQmSub3VRPW079Fm1wbsO8,5522
|
|
12
|
-
solvapay/paywall_state.py,sha256=pfP_-5B6LRcWrvItwgDwh-9f-DSowYaFDKXxGJvYPrg,7048
|
|
13
|
-
solvapay/webhooks.py,sha256=6EuoISjJzrKEUIoNpt__l2FnJvQR4T7XnT7fzOFHl5U,2738
|
|
14
|
-
solvapay_python-0.7.0.dist-info/METADATA,sha256=Zwth8LdaDTeW9LMwb8XPhLPAGHwM_o0pC-KK6Fcaeew,9084
|
|
15
|
-
solvapay_python-0.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
16
|
-
solvapay_python-0.7.0.dist-info/licenses/LICENSE,sha256=wJURmEXLdSdApQdHG-RCwBoZVka1Oux8zNrLxGC30S8,1068
|
|
17
|
-
solvapay_python-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|