solvapay-python 0.6.0__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 +57 -0
- solvapay/_async_client.py +355 -0
- solvapay/_config.py +22 -0
- solvapay/_http.py +107 -0
- solvapay/client.py +352 -0
- solvapay/events.py +90 -0
- solvapay/exceptions.py +16 -0
- solvapay/fastapi.py +65 -0
- solvapay/langchain.py +67 -0
- solvapay/models.py +182 -0
- solvapay/paywall.py +130 -0
- solvapay/paywall_state.py +121 -0
- solvapay/webhooks.py +86 -0
- solvapay_python-0.6.0.dist-info/METADATA +268 -0
- solvapay_python-0.6.0.dist-info/RECORD +17 -0
- solvapay_python-0.6.0.dist-info/WHEEL +4 -0
- solvapay_python-0.6.0.dist-info/licenses/LICENSE +21 -0
solvapay/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""SolvaPay community Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from solvapay import paywall
|
|
6
|
+
from solvapay._async_client import AsyncSolvaPay
|
|
7
|
+
from solvapay.client import SolvaPay
|
|
8
|
+
from solvapay.events import (
|
|
9
|
+
CheckoutSessionCreated,
|
|
10
|
+
CustomerCreated,
|
|
11
|
+
CustomerDeleted,
|
|
12
|
+
CustomerUpdated,
|
|
13
|
+
PaymentFailed,
|
|
14
|
+
PaymentRefunded,
|
|
15
|
+
PaymentRefundFailed,
|
|
16
|
+
PaymentSucceeded,
|
|
17
|
+
PurchaseCancelled,
|
|
18
|
+
PurchaseCreated,
|
|
19
|
+
PurchaseExpired,
|
|
20
|
+
PurchaseSuspended,
|
|
21
|
+
PurchaseUpdated,
|
|
22
|
+
WebhookEvent,
|
|
23
|
+
)
|
|
24
|
+
from solvapay.exceptions import SolvaPayAPIError, SolvaPayError
|
|
25
|
+
from solvapay.models import BalanceResponse, Merchant, Plan, PlatformConfig, Product
|
|
26
|
+
from solvapay.paywall import PaywallRequired
|
|
27
|
+
from solvapay.webhooks import verify_webhook
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AsyncSolvaPay",
|
|
31
|
+
"BalanceResponse",
|
|
32
|
+
"CheckoutSessionCreated",
|
|
33
|
+
"CustomerCreated",
|
|
34
|
+
"CustomerDeleted",
|
|
35
|
+
"CustomerUpdated",
|
|
36
|
+
"Merchant",
|
|
37
|
+
"PaymentFailed",
|
|
38
|
+
"PaymentRefundFailed",
|
|
39
|
+
"PaymentRefunded",
|
|
40
|
+
"PaymentSucceeded",
|
|
41
|
+
"PaywallRequired",
|
|
42
|
+
"Plan",
|
|
43
|
+
"PlatformConfig",
|
|
44
|
+
"Product",
|
|
45
|
+
"PurchaseCancelled",
|
|
46
|
+
"PurchaseCreated",
|
|
47
|
+
"PurchaseExpired",
|
|
48
|
+
"PurchaseSuspended",
|
|
49
|
+
"PurchaseUpdated",
|
|
50
|
+
"SolvaPay",
|
|
51
|
+
"SolvaPayAPIError",
|
|
52
|
+
"SolvaPayError",
|
|
53
|
+
"WebhookEvent",
|
|
54
|
+
"paywall",
|
|
55
|
+
"verify_webhook",
|
|
56
|
+
]
|
|
57
|
+
__version__ = "0.6.0"
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Async SolvaPay client. Mirrors SolvaPay sync surface 1:1.
|
|
2
|
+
|
|
3
|
+
Constructor signature identical to `SolvaPay`. All ops are `async def`.
|
|
4
|
+
Use `async with AsyncSolvaPay() as sv: ...` for proper teardown.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from solvapay._config import resolve_api_key, resolve_base_url
|
|
13
|
+
from solvapay._http import AsyncHttpClient, _RequestSpec
|
|
14
|
+
from solvapay.exceptions import SolvaPayAPIError
|
|
15
|
+
from solvapay.models import (
|
|
16
|
+
BalanceResponse,
|
|
17
|
+
CancelPurchaseRequest,
|
|
18
|
+
CheckLimitsRequest,
|
|
19
|
+
CheckoutSession,
|
|
20
|
+
CheckoutSessionRequest,
|
|
21
|
+
CloneProductRequest,
|
|
22
|
+
CreateCustomerRequest,
|
|
23
|
+
CreatePlanRequest,
|
|
24
|
+
CreateProductRequest,
|
|
25
|
+
Customer,
|
|
26
|
+
LimitResponse,
|
|
27
|
+
Merchant,
|
|
28
|
+
Plan,
|
|
29
|
+
PlatformConfig,
|
|
30
|
+
Product,
|
|
31
|
+
TrackUsageRequest,
|
|
32
|
+
UpdateCustomerRequest,
|
|
33
|
+
UpdatePlanRequest,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncSolvaPay:
|
|
38
|
+
"""Async SolvaPay API client.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
api_key: SolvaPay secret key. Falls back to SOLVAPAY_SECRET_KEY env var.
|
|
42
|
+
base_url: API base URL. Falls back to SOLVAPAY_API_BASE_URL env var,
|
|
43
|
+
then to https://api.solvapay.com.
|
|
44
|
+
timeout: HTTP timeout in seconds. Default 30.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> async with AsyncSolvaPay() as sv:
|
|
48
|
+
... session = await sv.create_checkout_session(
|
|
49
|
+
... customer_ref="cus_123", product_ref="prd_0QKI8NHF"
|
|
50
|
+
... )
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
api_key: str | None = None,
|
|
56
|
+
*,
|
|
57
|
+
base_url: str | None = None,
|
|
58
|
+
timeout: float = 30.0,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._http = AsyncHttpClient(
|
|
61
|
+
api_key=resolve_api_key(api_key),
|
|
62
|
+
base_url=resolve_base_url(base_url),
|
|
63
|
+
timeout=timeout,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def aclose(self) -> None:
|
|
67
|
+
await self._http.aclose()
|
|
68
|
+
|
|
69
|
+
async def __aenter__(self) -> AsyncSolvaPay:
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def __aexit__(self, *_: object) -> None:
|
|
73
|
+
await self.aclose()
|
|
74
|
+
|
|
75
|
+
async def create_checkout_session(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
customer_ref: str,
|
|
79
|
+
product_ref: str,
|
|
80
|
+
plan_ref: str | None = None,
|
|
81
|
+
return_url: str | None = None,
|
|
82
|
+
) -> CheckoutSession:
|
|
83
|
+
req = CheckoutSessionRequest(
|
|
84
|
+
customer_ref=customer_ref,
|
|
85
|
+
product_ref=product_ref,
|
|
86
|
+
plan_ref=plan_ref,
|
|
87
|
+
return_url=return_url,
|
|
88
|
+
)
|
|
89
|
+
data = await self._http.send(
|
|
90
|
+
_RequestSpec(
|
|
91
|
+
"POST",
|
|
92
|
+
"/v1/sdk/checkout-sessions",
|
|
93
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return CheckoutSession.model_validate(data)
|
|
97
|
+
|
|
98
|
+
async def ensure_customer(
|
|
99
|
+
self,
|
|
100
|
+
customer_ref: str,
|
|
101
|
+
external_ref: str | None = None,
|
|
102
|
+
*,
|
|
103
|
+
email: str | None = None,
|
|
104
|
+
name: str | None = None,
|
|
105
|
+
) -> str:
|
|
106
|
+
lookup_ref = external_ref or customer_ref
|
|
107
|
+
try:
|
|
108
|
+
existing = await self._http.send(
|
|
109
|
+
_RequestSpec("GET", "/v1/sdk/customers", params={"externalRef": lookup_ref})
|
|
110
|
+
)
|
|
111
|
+
if existing.get("customerRef"):
|
|
112
|
+
return str(existing["customerRef"])
|
|
113
|
+
except SolvaPayAPIError as exc:
|
|
114
|
+
if exc.status_code != 404:
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
req = CreateCustomerRequest(
|
|
118
|
+
email=email or f"{customer_ref}-{int(time.time())}@auto-created.local",
|
|
119
|
+
external_ref=lookup_ref,
|
|
120
|
+
name=name,
|
|
121
|
+
)
|
|
122
|
+
created = await self._http.send(
|
|
123
|
+
_RequestSpec(
|
|
124
|
+
"POST", "/v1/sdk/customers", json=req.model_dump(by_alias=True, exclude_none=True)
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return str(created["customerRef"])
|
|
128
|
+
|
|
129
|
+
async def get_customer(
|
|
130
|
+
self,
|
|
131
|
+
customer_ref: str | None = None,
|
|
132
|
+
*,
|
|
133
|
+
external_ref: str | None = None,
|
|
134
|
+
email: str | None = None,
|
|
135
|
+
) -> Customer:
|
|
136
|
+
if customer_ref:
|
|
137
|
+
data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/customers/{customer_ref}"))
|
|
138
|
+
elif external_ref:
|
|
139
|
+
data = await self._http.send(
|
|
140
|
+
_RequestSpec("GET", "/v1/sdk/customers", params={"externalRef": external_ref})
|
|
141
|
+
)
|
|
142
|
+
elif email:
|
|
143
|
+
data = await self._http.send(
|
|
144
|
+
_RequestSpec("GET", "/v1/sdk/customers", params={"email": email})
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError("Must provide customer_ref, external_ref, or email")
|
|
148
|
+
return Customer.model_validate(data)
|
|
149
|
+
|
|
150
|
+
async def check_limits(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
customer_ref: str,
|
|
154
|
+
product_ref: str,
|
|
155
|
+
plan_ref: str | None = None,
|
|
156
|
+
meter_name: str | None = None,
|
|
157
|
+
usage_type: str | None = None,
|
|
158
|
+
) -> LimitResponse:
|
|
159
|
+
req = CheckLimitsRequest(
|
|
160
|
+
customer_ref=customer_ref,
|
|
161
|
+
product_ref=product_ref,
|
|
162
|
+
plan_ref=plan_ref,
|
|
163
|
+
meter_name=meter_name,
|
|
164
|
+
usage_type=usage_type,
|
|
165
|
+
)
|
|
166
|
+
data = await self._http.send(
|
|
167
|
+
_RequestSpec(
|
|
168
|
+
"POST", "/v1/sdk/limits", json=req.model_dump(by_alias=True, exclude_none=True)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return LimitResponse.model_validate(data)
|
|
172
|
+
|
|
173
|
+
async def track_usage(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
customer_ref: str,
|
|
177
|
+
product_ref: str,
|
|
178
|
+
meter_name: str,
|
|
179
|
+
units: float,
|
|
180
|
+
idempotency_key: str | None = None,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""Record usage against a meter. Maps to POST /v1/sdk/usages."""
|
|
183
|
+
req = TrackUsageRequest(
|
|
184
|
+
customer_ref=customer_ref,
|
|
185
|
+
product_ref=product_ref,
|
|
186
|
+
meter_name=meter_name,
|
|
187
|
+
units=units,
|
|
188
|
+
)
|
|
189
|
+
return await self._http.send(
|
|
190
|
+
_RequestSpec(
|
|
191
|
+
"POST",
|
|
192
|
+
"/v1/sdk/usages",
|
|
193
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
194
|
+
idempotency_key=idempotency_key,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def update_customer(
|
|
199
|
+
self,
|
|
200
|
+
customer_ref: str,
|
|
201
|
+
*,
|
|
202
|
+
email: str | None = None,
|
|
203
|
+
name: str | None = None,
|
|
204
|
+
external_ref: str | None = None,
|
|
205
|
+
) -> Customer:
|
|
206
|
+
"""Update customer fields. Maps to PATCH /v1/sdk/customers/{ref}."""
|
|
207
|
+
req = UpdateCustomerRequest(email=email, name=name, external_ref=external_ref)
|
|
208
|
+
data = await self._http.send(
|
|
209
|
+
_RequestSpec(
|
|
210
|
+
"PATCH",
|
|
211
|
+
f"/v1/sdk/customers/{customer_ref}",
|
|
212
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return Customer.model_validate(data)
|
|
216
|
+
|
|
217
|
+
async def get_customer_balance(self, customer_ref: str) -> BalanceResponse:
|
|
218
|
+
"""Get credit balance for a customer. Maps to GET /v1/sdk/customers/{ref}/balance."""
|
|
219
|
+
data = await self._http.send(
|
|
220
|
+
_RequestSpec("GET", f"/v1/sdk/customers/{customer_ref}/balance")
|
|
221
|
+
)
|
|
222
|
+
return BalanceResponse.model_validate(data)
|
|
223
|
+
|
|
224
|
+
async def cancel_purchase(
|
|
225
|
+
self, purchase_ref: str, *, reason: str | None = None
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Cancel a purchase. Maps to POST /v1/sdk/purchases/{ref}/cancel."""
|
|
228
|
+
req = CancelPurchaseRequest(reason=reason)
|
|
229
|
+
return await self._http.send(
|
|
230
|
+
_RequestSpec(
|
|
231
|
+
"POST",
|
|
232
|
+
f"/v1/sdk/purchases/{purchase_ref}/cancel",
|
|
233
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
async def reactivate_purchase(self, purchase_ref: str) -> dict[str, Any]:
|
|
238
|
+
"""Reactivate a cancelled purchase. Maps to POST /v1/sdk/purchases/{ref}/reactivate."""
|
|
239
|
+
return await self._http.send(
|
|
240
|
+
_RequestSpec("POST", f"/v1/sdk/purchases/{purchase_ref}/reactivate")
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# --- Admin: Products ---
|
|
244
|
+
|
|
245
|
+
async def list_products(self) -> list[Product]:
|
|
246
|
+
"""List all products. Maps to GET /v1/sdk/products."""
|
|
247
|
+
data = await self._http.send(_RequestSpec("GET", "/v1/sdk/products"))
|
|
248
|
+
items: list[Any] = data if isinstance(data, list) else data.get("products", [])
|
|
249
|
+
return [Product.model_validate(p) for p in items]
|
|
250
|
+
|
|
251
|
+
async def get_product(self, product_ref: str) -> Product:
|
|
252
|
+
"""Get a product by ref. Maps to GET /v1/sdk/products/{ref}."""
|
|
253
|
+
data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/products/{product_ref}"))
|
|
254
|
+
return Product.model_validate(data)
|
|
255
|
+
|
|
256
|
+
async def create_product(self, *, name: str, type: str, default_currency: str) -> Product:
|
|
257
|
+
"""Create a product. Maps to POST /v1/sdk/products."""
|
|
258
|
+
req = CreateProductRequest(name=name, type=type, default_currency=default_currency)
|
|
259
|
+
data = await self._http.send(
|
|
260
|
+
_RequestSpec(
|
|
261
|
+
"POST",
|
|
262
|
+
"/v1/sdk/products",
|
|
263
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
return Product.model_validate(data)
|
|
267
|
+
|
|
268
|
+
async def delete_product(self, product_ref: str) -> dict[str, Any]:
|
|
269
|
+
"""Delete a product. Maps to DELETE /v1/sdk/products/{ref}."""
|
|
270
|
+
return await self._http.send(_RequestSpec("DELETE", f"/v1/sdk/products/{product_ref}"))
|
|
271
|
+
|
|
272
|
+
async def clone_product(self, product_ref: str, *, new_name: str) -> Product:
|
|
273
|
+
"""Clone a product with a new name. Maps to POST /v1/sdk/products/{ref}/clone."""
|
|
274
|
+
req = CloneProductRequest(new_name=new_name)
|
|
275
|
+
data = await self._http.send(
|
|
276
|
+
_RequestSpec(
|
|
277
|
+
"POST",
|
|
278
|
+
f"/v1/sdk/products/{product_ref}/clone",
|
|
279
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return Product.model_validate(data)
|
|
283
|
+
|
|
284
|
+
# --- Admin: Plans ---
|
|
285
|
+
|
|
286
|
+
async def list_plans(self, product_ref: str) -> list[Plan]:
|
|
287
|
+
"""List plans for a product. Maps to GET /v1/sdk/products/{ref}/plans."""
|
|
288
|
+
data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/products/{product_ref}/plans"))
|
|
289
|
+
items: list[Any] = data if isinstance(data, list) else data.get("plans", [])
|
|
290
|
+
return [Plan.model_validate(p) for p in items]
|
|
291
|
+
|
|
292
|
+
async def create_plan(
|
|
293
|
+
self,
|
|
294
|
+
product_ref: str,
|
|
295
|
+
*,
|
|
296
|
+
name: str,
|
|
297
|
+
type: str,
|
|
298
|
+
price: float | None = None,
|
|
299
|
+
currency: str | None = None,
|
|
300
|
+
interval: str | None = None,
|
|
301
|
+
) -> Plan:
|
|
302
|
+
"""Create a plan for a product. Maps to POST /v1/sdk/products/{ref}/plans."""
|
|
303
|
+
req = CreatePlanRequest(
|
|
304
|
+
name=name, type=type, price=price, currency=currency, interval=interval
|
|
305
|
+
)
|
|
306
|
+
data = await self._http.send(
|
|
307
|
+
_RequestSpec(
|
|
308
|
+
"POST",
|
|
309
|
+
f"/v1/sdk/products/{product_ref}/plans",
|
|
310
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
return Plan.model_validate(data)
|
|
314
|
+
|
|
315
|
+
async def update_plan(
|
|
316
|
+
self,
|
|
317
|
+
product_ref: str,
|
|
318
|
+
plan_ref: str,
|
|
319
|
+
*,
|
|
320
|
+
name: str | None = None,
|
|
321
|
+
type: str | None = None,
|
|
322
|
+
price: float | None = None,
|
|
323
|
+
currency: str | None = None,
|
|
324
|
+
interval: str | None = None,
|
|
325
|
+
) -> Plan:
|
|
326
|
+
"""Update a plan. Maps to PUT /v1/sdk/products/{ref}/plans/{ref}."""
|
|
327
|
+
req = UpdatePlanRequest(
|
|
328
|
+
name=name, type=type, price=price, currency=currency, interval=interval
|
|
329
|
+
)
|
|
330
|
+
data = await self._http.send(
|
|
331
|
+
_RequestSpec(
|
|
332
|
+
"PUT",
|
|
333
|
+
f"/v1/sdk/products/{product_ref}/plans/{plan_ref}",
|
|
334
|
+
json=req.model_dump(by_alias=True, exclude_none=True),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return Plan.model_validate(data)
|
|
338
|
+
|
|
339
|
+
async def delete_plan(self, product_ref: str, plan_ref: str) -> dict[str, Any]:
|
|
340
|
+
"""Delete a plan. Maps to DELETE /v1/sdk/products/{ref}/plans/{ref}."""
|
|
341
|
+
return await self._http.send(
|
|
342
|
+
_RequestSpec("DELETE", f"/v1/sdk/products/{product_ref}/plans/{plan_ref}")
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# --- Admin: Merchant + Platform ---
|
|
346
|
+
|
|
347
|
+
async def get_merchant(self) -> Merchant:
|
|
348
|
+
"""Get merchant account details. Maps to GET /v1/sdk/merchant."""
|
|
349
|
+
data = await self._http.send(_RequestSpec("GET", "/v1/sdk/merchant"))
|
|
350
|
+
return Merchant.model_validate(data)
|
|
351
|
+
|
|
352
|
+
async def get_platform_config(self) -> PlatformConfig:
|
|
353
|
+
"""Get platform-level configuration. Maps to GET /v1/sdk/platform-config."""
|
|
354
|
+
data = await self._http.send(_RequestSpec("GET", "/v1/sdk/platform-config"))
|
|
355
|
+
return PlatformConfig.model_validate(data)
|
solvapay/_config.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Environment configuration loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://api.solvapay.com"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_api_key(explicit: str | None) -> str:
|
|
11
|
+
key = explicit or os.environ.get("SOLVAPAY_SECRET_KEY")
|
|
12
|
+
if not key:
|
|
13
|
+
from solvapay.exceptions import SolvaPayError
|
|
14
|
+
|
|
15
|
+
raise SolvaPayError(
|
|
16
|
+
"SolvaPay API key not provided. Pass api_key=... or set SOLVAPAY_SECRET_KEY env var."
|
|
17
|
+
)
|
|
18
|
+
return key
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_base_url(explicit: str | None) -> str:
|
|
22
|
+
return explicit or os.environ.get("SOLVAPAY_API_BASE_URL") or DEFAULT_BASE_URL
|
solvapay/_http.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Internal HTTP transport. Not part of public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from solvapay.exceptions import SolvaPayAPIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class _RequestSpec:
|
|
15
|
+
method: str
|
|
16
|
+
path: str
|
|
17
|
+
json: dict[str, Any] | None = None
|
|
18
|
+
params: dict[str, Any] | None = None
|
|
19
|
+
idempotency_key: str | None = None
|
|
20
|
+
|
|
21
|
+
def headers(self) -> dict[str, str]:
|
|
22
|
+
return {"Idempotency-Key": self.idempotency_key} if self.idempotency_key else {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _handle(response: httpx.Response) -> dict[str, Any]:
|
|
26
|
+
if not response.is_success:
|
|
27
|
+
raise SolvaPayAPIError(response.status_code, response.text)
|
|
28
|
+
if response.status_code == 204 or not response.content:
|
|
29
|
+
return {}
|
|
30
|
+
return response.json() # type: ignore[no-any-return]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HttpClient:
|
|
34
|
+
def __init__(self, *, api_key: str, base_url: str, timeout: float = 30.0) -> None:
|
|
35
|
+
self._client = httpx.Client(
|
|
36
|
+
base_url=base_url,
|
|
37
|
+
headers={
|
|
38
|
+
"Authorization": f"Bearer {api_key}",
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"User-Agent": "solvapay-python/0.4.0",
|
|
41
|
+
},
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
self._client.close()
|
|
47
|
+
|
|
48
|
+
def __enter__(self) -> HttpClient:
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __exit__(self, *_: object) -> None:
|
|
52
|
+
self.close()
|
|
53
|
+
|
|
54
|
+
def send(self, spec: _RequestSpec) -> dict[str, Any]:
|
|
55
|
+
return _handle(
|
|
56
|
+
self._client.request(
|
|
57
|
+
spec.method,
|
|
58
|
+
spec.path,
|
|
59
|
+
json=spec.json,
|
|
60
|
+
params=spec.params,
|
|
61
|
+
headers=spec.headers(),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def request(
|
|
66
|
+
self,
|
|
67
|
+
method: str,
|
|
68
|
+
path: str,
|
|
69
|
+
*,
|
|
70
|
+
json: dict[str, Any] | None = None,
|
|
71
|
+
params: dict[str, Any] | None = None,
|
|
72
|
+
idempotency_key: str | None = None,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
return self.send(_RequestSpec(method, path, json, params, idempotency_key))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AsyncHttpClient:
|
|
78
|
+
def __init__(self, *, api_key: str, base_url: str, timeout: float = 30.0) -> None:
|
|
79
|
+
self._client = httpx.AsyncClient(
|
|
80
|
+
base_url=base_url,
|
|
81
|
+
headers={
|
|
82
|
+
"Authorization": f"Bearer {api_key}",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"User-Agent": "solvapay-python/0.4.0",
|
|
85
|
+
},
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def aclose(self) -> None:
|
|
90
|
+
await self._client.aclose()
|
|
91
|
+
|
|
92
|
+
async def __aenter__(self) -> AsyncHttpClient:
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
async def __aexit__(self, *_: object) -> None:
|
|
96
|
+
await self.aclose()
|
|
97
|
+
|
|
98
|
+
async def send(self, spec: _RequestSpec) -> dict[str, Any]:
|
|
99
|
+
return _handle(
|
|
100
|
+
await self._client.request(
|
|
101
|
+
spec.method,
|
|
102
|
+
spec.path,
|
|
103
|
+
json=spec.json,
|
|
104
|
+
params=spec.params,
|
|
105
|
+
headers=spec.headers(),
|
|
106
|
+
)
|
|
107
|
+
)
|