solvapay-python 0.7.2__py3-none-any.whl → 0.8.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.
Files changed (43) hide show
  1. solvapay/__init__.py +31 -1
  2. solvapay/_async_client.py +105 -184
  3. solvapay/_http.py +16 -215
  4. solvapay/_stability.py +60 -0
  5. solvapay/_transport/__init__.py +106 -0
  6. solvapay/_transport/_recipe.py +63 -0
  7. solvapay/_transport/httpx_transport.py +397 -0
  8. solvapay/_transport/middleware.py +233 -0
  9. solvapay/adapters/__init__.py +3 -0
  10. solvapay/adapters/langchain.py +88 -0
  11. solvapay/adapters/mcp.py +158 -0
  12. solvapay/client.py +105 -182
  13. solvapay/langchain.py +9 -61
  14. solvapay/operations/__init__.py +7 -0
  15. solvapay/operations/_registry.py +117 -0
  16. solvapay/operations/checkout.py +75 -0
  17. solvapay/operations/customers.py +256 -0
  18. solvapay/operations/limits.py +77 -0
  19. solvapay/operations/merchant.py +71 -0
  20. solvapay/operations/plans.py +167 -0
  21. solvapay/operations/products.py +141 -0
  22. solvapay/operations/purchases.py +103 -0
  23. solvapay/operations/usage.py +75 -0
  24. solvapay/paywall/__init__.py +32 -0
  25. solvapay/paywall/core.py +152 -0
  26. solvapay/paywall/decorators.py +126 -0
  27. solvapay/paywall/meta.py +18 -0
  28. solvapay/paywall/policy.py +14 -0
  29. solvapay/paywall/resolvers.py +60 -0
  30. solvapay/paywall/state.py +23 -0
  31. solvapay/webhooks/__init__.py +28 -0
  32. solvapay/webhooks/envelope.py +16 -0
  33. solvapay/webhooks/pipeline.py +88 -0
  34. solvapay/webhooks/replay.py +42 -0
  35. solvapay/webhooks/rotation.py +32 -0
  36. solvapay/{webhooks.py → webhooks/verify.py} +4 -22
  37. solvapay_python-0.8.0.dist-info/METADATA +315 -0
  38. solvapay_python-0.8.0.dist-info/RECORD +48 -0
  39. solvapay/paywall.py +0 -153
  40. solvapay_python-0.7.2.dist-info/METADATA +0 -357
  41. solvapay_python-0.7.2.dist-info/RECORD +0 -19
  42. {solvapay_python-0.7.2.dist-info → solvapay_python-0.8.0.dist-info}/WHEEL +0 -0
  43. {solvapay_python-0.7.2.dist-info → solvapay_python-0.8.0.dist-info}/licenses/LICENSE +0 -0
solvapay/__init__.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from solvapay import paywall
6
6
  from solvapay._async_client import AsyncSolvaPay
7
+ from solvapay._stability import MANIFEST, deprecated, experimental, stable
7
8
  from solvapay.client import SolvaPay
8
9
  from solvapay.events import (
9
10
  CheckoutSessionCreated,
@@ -38,7 +39,33 @@ from solvapay.models import BalanceResponse, Merchant, Plan, PlatformConfig, Pro
38
39
  from solvapay.paywall import PaywallRequired
39
40
  from solvapay.webhooks import verify_webhook
40
41
 
42
+ # Register stable exports in MANIFEST (HLD V1.2).
43
+ # stable(X) returns X unchanged — isinstance() continues to work (HLD SM1).
44
+ stable(SolvaPay)
45
+ stable(AsyncSolvaPay)
46
+ stable(SolvaPayError)
47
+ stable(APIError)
48
+ stable(AuthenticationError)
49
+ stable(PermissionError)
50
+ stable(NotFoundError)
51
+ stable(RateLimitError)
52
+ stable(InvalidRequestError)
53
+ stable(APIServerError)
54
+ stable(APIConnectionError)
55
+ stable(APITimeoutError)
56
+ stable(PaywallRequired)
57
+ stable(verify_webhook)
58
+ stable(BalanceResponse)
59
+ stable(Product)
60
+ stable(Plan)
61
+ stable(Merchant)
62
+ stable(PlatformConfig)
63
+ stable(WebhookEvent)
64
+ # SolvaPayAPIError is a back-compat alias — deprecated in v1.0, removed v2.0
65
+ deprecated(removed_in="2.0")(SolvaPayAPIError)
66
+
41
67
  __all__ = [
68
+ "MANIFEST",
42
69
  "APIConnectionError",
43
70
  "APIError",
44
71
  "APIServerError",
@@ -72,7 +99,10 @@ __all__ = [
72
99
  "SolvaPayAPIError",
73
100
  "SolvaPayError",
74
101
  "WebhookEvent",
102
+ "deprecated",
103
+ "experimental",
75
104
  "paywall",
105
+ "stable",
76
106
  "verify_webhook",
77
107
  ]
78
- __version__ = "0.7.2"
108
+ __version__ = "0.8.0"
solvapay/_async_client.py CHANGED
@@ -7,37 +7,52 @@ Use `async with AsyncSolvaPay() as sv: ...` for proper teardown.
7
7
  from __future__ import annotations
8
8
 
9
9
  import logging
10
- import time
10
+ import warnings
11
11
  from typing import Any
12
12
 
13
13
  from solvapay._config import resolve_api_key, resolve_base_url
14
- from solvapay._http import AsyncHttpClient, _RequestSpec
15
- from solvapay.exceptions import SolvaPayAPIError
14
+ from solvapay._http import AsyncHttpClient
16
15
  from solvapay.models import (
17
16
  BalanceResponse,
18
- CancelPurchaseRequest,
19
- CheckLimitsRequest,
20
17
  CheckoutSession,
21
- CheckoutSessionRequest,
22
- CloneProductRequest,
23
- CreateCustomerRequest,
24
- CreatePlanRequest,
25
- CreateProductRequest,
26
18
  Customer,
27
19
  LimitResponse,
28
20
  Merchant,
29
21
  Plan,
30
22
  PlatformConfig,
31
23
  Product,
32
- TrackUsageRequest,
33
- UpdateCustomerRequest,
34
- UpdatePlanRequest,
35
24
  )
25
+ from solvapay.operations.checkout import CheckoutOperations
26
+ from solvapay.operations.customers import CustomersOperations
27
+ from solvapay.operations.limits import LimitsOperations
28
+ from solvapay.operations.merchant import MerchantOperations
29
+ from solvapay.operations.plans import PlansOperations
30
+ from solvapay.operations.products import ProductsOperations
31
+ from solvapay.operations.purchases import PurchasesOperations
32
+ from solvapay.operations.usage import UsageOperations
33
+
34
+
35
+ def _shim_warn(new: str) -> None:
36
+ warnings.warn(
37
+ f"Flat method deprecated; use {new} instead",
38
+ DeprecationWarning,
39
+ stacklevel=3,
40
+ )
36
41
 
37
42
 
38
43
  class AsyncSolvaPay:
39
44
  """Async SolvaPay API client.
40
45
 
46
+ Resource namespaces (v0.8+):
47
+ sv.customers.aensure / aget / aupdate / abalance
48
+ sv.checkout.acreate_session
49
+ sv.limits.acheck
50
+ sv.purchases.acancel / areactivate
51
+ sv.usage.atrack
52
+ sv.products.alist / aget / acreate / adelete / aclone
53
+ sv.plans.alist / acreate / aupdate / adelete
54
+ sv.merchant.aget / aget_platform_config
55
+
41
56
  Args:
42
57
  api_key: SolvaPay secret key. Falls back to SOLVAPAY_SECRET_KEY env var.
43
58
  base_url: API base URL. Falls back to SOLVAPAY_API_BASE_URL env var,
@@ -46,7 +61,7 @@ class AsyncSolvaPay:
46
61
 
47
62
  Example:
48
63
  >>> async with AsyncSolvaPay() as sv:
49
- ... session = await sv.create_checkout_session(
64
+ ... session = await sv.checkout.acreate_session(
50
65
  ... customer_ref="cus_123", product_ref="prd_0QKI8NHF"
51
66
  ... )
52
67
  """
@@ -65,6 +80,16 @@ class AsyncSolvaPay:
65
80
  timeout=timeout,
66
81
  logger=logger,
67
82
  )
83
+ # Eager namespace construction (HLD RN1).
84
+ _t = self._http._transport
85
+ self.customers = CustomersOperations(sync_transport=None, async_transport=_t)
86
+ self.checkout = CheckoutOperations(sync_transport=None, async_transport=_t)
87
+ self.limits = LimitsOperations(sync_transport=None, async_transport=_t)
88
+ self.purchases = PurchasesOperations(sync_transport=None, async_transport=_t)
89
+ self.usage = UsageOperations(sync_transport=None, async_transport=_t)
90
+ self.products = ProductsOperations(sync_transport=None, async_transport=_t)
91
+ self.plans = PlansOperations(sync_transport=None, async_transport=_t)
92
+ self.merchant = MerchantOperations(sync_transport=None, async_transport=_t)
68
93
 
69
94
  async def aclose(self) -> None:
70
95
  await self._http.aclose()
@@ -75,6 +100,8 @@ class AsyncSolvaPay:
75
100
  async def __aexit__(self, *_: object) -> None:
76
101
  await self.aclose()
77
102
 
103
+ # ── Deprecated flat shims — removed in v2.0 ──
104
+
78
105
  async def create_checkout_session(
79
106
  self,
80
107
  *,
@@ -84,21 +111,14 @@ class AsyncSolvaPay:
84
111
  return_url: str | None = None,
85
112
  idempotency_key: str | None = None,
86
113
  ) -> CheckoutSession:
87
- req = CheckoutSessionRequest(
114
+ _shim_warn("sv.checkout.acreate_session()")
115
+ return await self.checkout.acreate_session(
88
116
  customer_ref=customer_ref,
89
117
  product_ref=product_ref,
90
118
  plan_ref=plan_ref,
91
119
  return_url=return_url,
120
+ idempotency_key=idempotency_key,
92
121
  )
93
- data = await self._http.send(
94
- _RequestSpec(
95
- "POST",
96
- "/v1/sdk/checkout-sessions",
97
- json=req.model_dump(by_alias=True, exclude_none=True),
98
- idempotency_key=idempotency_key,
99
- )
100
- )
101
- return CheckoutSession.model_validate(data)
102
122
 
103
123
  async def ensure_customer(
104
124
  self,
@@ -109,35 +129,14 @@ class AsyncSolvaPay:
109
129
  name: str | None = None,
110
130
  idempotency_key: str | None = None,
111
131
  ) -> str:
112
- lookup_ref = external_ref or customer_ref
113
- try:
114
- existing = await self._http.send(
115
- _RequestSpec("GET", "/v1/sdk/customers", params={"externalRef": lookup_ref})
116
- )
117
- ref = existing.get("reference") or existing.get("customerRef")
118
- if ref:
119
- return str(ref)
120
- except SolvaPayAPIError as exc:
121
- if exc.status_code != 404:
122
- raise
123
-
124
- req = CreateCustomerRequest(
125
- email=email or f"{customer_ref}-{int(time.time())}@auto-created.local",
126
- external_ref=lookup_ref,
132
+ _shim_warn("sv.customers.aensure()")
133
+ return await self.customers.aensure(
134
+ customer_ref,
135
+ external_ref,
136
+ email=email,
127
137
  name=name,
138
+ idempotency_key=idempotency_key,
128
139
  )
129
- created = await self._http.send(
130
- _RequestSpec(
131
- "POST",
132
- "/v1/sdk/customers",
133
- json=req.model_dump(by_alias=True, exclude_none=True),
134
- idempotency_key=idempotency_key,
135
- )
136
- )
137
- ref = created.get("reference") or created.get("customerRef")
138
- if not ref:
139
- raise SolvaPayAPIError(200, f"customer create returned no reference: {created!r}")
140
- return str(ref)
141
140
 
142
141
  async def get_customer(
143
142
  self,
@@ -146,19 +145,8 @@ class AsyncSolvaPay:
146
145
  external_ref: str | None = None,
147
146
  email: str | None = None,
148
147
  ) -> Customer:
149
- if customer_ref:
150
- data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/customers/{customer_ref}"))
151
- elif external_ref:
152
- data = await self._http.send(
153
- _RequestSpec("GET", "/v1/sdk/customers", params={"externalRef": external_ref})
154
- )
155
- elif email:
156
- data = await self._http.send(
157
- _RequestSpec("GET", "/v1/sdk/customers", params={"email": email})
158
- )
159
- else:
160
- raise ValueError("Must provide customer_ref, external_ref, or email")
161
- return Customer.model_validate(data)
148
+ _shim_warn("sv.customers.aget()")
149
+ return await self.customers.aget(customer_ref, external_ref=external_ref, email=email)
162
150
 
163
151
  async def check_limits(
164
152
  self,
@@ -169,19 +157,14 @@ class AsyncSolvaPay:
169
157
  meter_name: str | None = None,
170
158
  usage_type: str | None = None,
171
159
  ) -> LimitResponse:
172
- req = CheckLimitsRequest(
160
+ _shim_warn("sv.limits.acheck()")
161
+ return await self.limits.acheck(
173
162
  customer_ref=customer_ref,
174
163
  product_ref=product_ref,
175
164
  plan_ref=plan_ref,
176
165
  meter_name=meter_name,
177
166
  usage_type=usage_type,
178
167
  )
179
- data = await self._http.send(
180
- _RequestSpec(
181
- "POST", "/v1/sdk/limits", json=req.model_dump(by_alias=True, exclude_none=True)
182
- )
183
- )
184
- return LimitResponse.model_validate(data)
185
168
 
186
169
  async def track_usage(
187
170
  self,
@@ -192,20 +175,13 @@ class AsyncSolvaPay:
192
175
  units: float,
193
176
  idempotency_key: str | None = None,
194
177
  ) -> dict[str, Any]:
195
- """Record usage against a meter. Maps to POST /v1/sdk/usages."""
196
- req = TrackUsageRequest(
178
+ _shim_warn("sv.usage.atrack()")
179
+ return await self.usage.atrack(
197
180
  customer_ref=customer_ref,
198
181
  product_ref=product_ref,
199
182
  meter_name=meter_name,
200
183
  units=units,
201
- )
202
- return await self._http.send(
203
- _RequestSpec(
204
- "POST",
205
- "/v1/sdk/usages",
206
- json=req.model_dump(by_alias=True, exclude_none=True),
207
- idempotency_key=idempotency_key,
208
- )
184
+ idempotency_key=idempotency_key,
209
185
  )
210
186
 
211
187
  async def update_customer(
@@ -216,23 +192,14 @@ class AsyncSolvaPay:
216
192
  name: str | None = None,
217
193
  external_ref: str | None = None,
218
194
  ) -> Customer:
219
- """Update customer fields. Maps to PATCH /v1/sdk/customers/{ref}."""
220
- req = UpdateCustomerRequest(email=email, name=name, external_ref=external_ref)
221
- data = await self._http.send(
222
- _RequestSpec(
223
- "PATCH",
224
- f"/v1/sdk/customers/{customer_ref}",
225
- json=req.model_dump(by_alias=True, exclude_none=True),
226
- )
195
+ _shim_warn("sv.customers.aupdate()")
196
+ return await self.customers.aupdate(
197
+ customer_ref, email=email, name=name, external_ref=external_ref
227
198
  )
228
- return Customer.model_validate(data)
229
199
 
230
200
  async def get_customer_balance(self, customer_ref: str) -> BalanceResponse:
231
- """Get credit balance for a customer. Maps to GET /v1/sdk/customers/{ref}/balance."""
232
- data = await self._http.send(
233
- _RequestSpec("GET", f"/v1/sdk/customers/{customer_ref}/balance")
234
- )
235
- return BalanceResponse.model_validate(data)
201
+ _shim_warn("sv.customers.abalance()")
202
+ return await self.customers.abalance(customer_ref)
236
203
 
237
204
  async def cancel_purchase(
238
205
  self,
@@ -241,83 +208,48 @@ class AsyncSolvaPay:
241
208
  reason: str | None = None,
242
209
  idempotency_key: str | None = None,
243
210
  ) -> dict[str, Any]:
244
- """Cancel a purchase. Maps to POST /v1/sdk/purchases/{ref}/cancel."""
245
- req = CancelPurchaseRequest(reason=reason)
246
- return await self._http.send(
247
- _RequestSpec(
248
- "POST",
249
- f"/v1/sdk/purchases/{purchase_ref}/cancel",
250
- json=req.model_dump(by_alias=True, exclude_none=True),
251
- idempotency_key=idempotency_key,
252
- )
211
+ _shim_warn("sv.purchases.acancel()")
212
+ return await self.purchases.acancel(
213
+ purchase_ref, reason=reason, idempotency_key=idempotency_key
253
214
  )
254
215
 
255
216
  async def reactivate_purchase(
256
217
  self, purchase_ref: str, *, idempotency_key: str | None = None
257
218
  ) -> dict[str, Any]:
258
- """Reactivate a cancelled purchase. Maps to POST /v1/sdk/purchases/{ref}/reactivate."""
259
- return await self._http.send(
260
- _RequestSpec(
261
- "POST",
262
- f"/v1/sdk/purchases/{purchase_ref}/reactivate",
263
- idempotency_key=idempotency_key,
264
- )
265
- )
266
-
267
- # --- Admin: Products ---
219
+ _shim_warn("sv.purchases.areactivate()")
220
+ return await self.purchases.areactivate(purchase_ref, idempotency_key=idempotency_key)
268
221
 
269
222
  async def list_products(self) -> list[Product]:
270
- """List all products. Maps to GET /v1/sdk/products."""
271
- data = await self._http.send(_RequestSpec("GET", "/v1/sdk/products"))
272
- items: list[Any] = data if isinstance(data, list) else data.get("products", [])
273
- return [Product.model_validate(p) for p in items]
223
+ _shim_warn("sv.products.alist()")
224
+ return await self.products.alist()
274
225
 
275
226
  async def get_product(self, product_ref: str) -> Product:
276
- """Get a product by ref. Maps to GET /v1/sdk/products/{ref}."""
277
- data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/products/{product_ref}"))
278
- return Product.model_validate(data)
227
+ _shim_warn("sv.products.aget()")
228
+ return await self.products.aget(product_ref)
279
229
 
280
230
  async def create_product(
281
231
  self, *, name: str, type: str, default_currency: str, idempotency_key: str | None = None
282
232
  ) -> Product:
283
- """Create a product. Maps to POST /v1/sdk/products."""
284
- req = CreateProductRequest(name=name, type=type, default_currency=default_currency)
285
- data = await self._http.send(
286
- _RequestSpec(
287
- "POST",
288
- "/v1/sdk/products",
289
- json=req.model_dump(by_alias=True, exclude_none=True),
290
- idempotency_key=idempotency_key,
291
- )
233
+ _shim_warn("sv.products.acreate()")
234
+ return await self.products.acreate(
235
+ name=name, type=type, default_currency=default_currency, idempotency_key=idempotency_key
292
236
  )
293
- return Product.model_validate(data)
294
237
 
295
238
  async def delete_product(self, product_ref: str) -> dict[str, Any]:
296
- """Delete a product. Maps to DELETE /v1/sdk/products/{ref}."""
297
- return await self._http.send(_RequestSpec("DELETE", f"/v1/sdk/products/{product_ref}"))
239
+ _shim_warn("sv.products.adelete()")
240
+ return await self.products.adelete(product_ref)
298
241
 
299
242
  async def clone_product(
300
243
  self, product_ref: str, *, new_name: str, idempotency_key: str | None = None
301
244
  ) -> Product:
302
- """Clone a product with a new name. Maps to POST /v1/sdk/products/{ref}/clone."""
303
- req = CloneProductRequest(new_name=new_name)
304
- data = await self._http.send(
305
- _RequestSpec(
306
- "POST",
307
- f"/v1/sdk/products/{product_ref}/clone",
308
- json=req.model_dump(by_alias=True, exclude_none=True),
309
- idempotency_key=idempotency_key,
310
- )
245
+ _shim_warn("sv.products.aclone()")
246
+ return await self.products.aclone(
247
+ product_ref, new_name=new_name, idempotency_key=idempotency_key
311
248
  )
312
- return Product.model_validate(data)
313
-
314
- # --- Admin: Plans ---
315
249
 
316
250
  async def list_plans(self, product_ref: str) -> list[Plan]:
317
- """List plans for a product. Maps to GET /v1/sdk/products/{ref}/plans."""
318
- data = await self._http.send(_RequestSpec("GET", f"/v1/sdk/products/{product_ref}/plans"))
319
- items: list[Any] = data if isinstance(data, list) else data.get("plans", [])
320
- return [Plan.model_validate(p) for p in items]
251
+ _shim_warn("sv.plans.alist()")
252
+ return await self.plans.alist(product_ref)
321
253
 
322
254
  async def create_plan(
323
255
  self,
@@ -330,19 +262,16 @@ class AsyncSolvaPay:
330
262
  interval: str | None = None,
331
263
  idempotency_key: str | None = None,
332
264
  ) -> Plan:
333
- """Create a plan for a product. Maps to POST /v1/sdk/products/{ref}/plans."""
334
- req = CreatePlanRequest(
335
- name=name, type=type, price=price, currency=currency, interval=interval
336
- )
337
- data = await self._http.send(
338
- _RequestSpec(
339
- "POST",
340
- f"/v1/sdk/products/{product_ref}/plans",
341
- json=req.model_dump(by_alias=True, exclude_none=True),
342
- idempotency_key=idempotency_key,
343
- )
265
+ _shim_warn("sv.plans.acreate()")
266
+ return await self.plans.acreate(
267
+ product_ref,
268
+ name=name,
269
+ type=type,
270
+ price=price,
271
+ currency=currency,
272
+ interval=interval,
273
+ idempotency_key=idempotency_key,
344
274
  )
345
- return Plan.model_validate(data)
346
275
 
347
276
  async def update_plan(
348
277
  self,
@@ -355,33 +284,25 @@ class AsyncSolvaPay:
355
284
  currency: str | None = None,
356
285
  interval: str | None = None,
357
286
  ) -> Plan:
358
- """Update a plan. Maps to PUT /v1/sdk/products/{ref}/plans/{ref}."""
359
- req = UpdatePlanRequest(
360
- name=name, type=type, price=price, currency=currency, interval=interval
361
- )
362
- data = await self._http.send(
363
- _RequestSpec(
364
- "PUT",
365
- f"/v1/sdk/products/{product_ref}/plans/{plan_ref}",
366
- json=req.model_dump(by_alias=True, exclude_none=True),
367
- )
287
+ _shim_warn("sv.plans.aupdate()")
288
+ return await self.plans.aupdate(
289
+ product_ref,
290
+ plan_ref,
291
+ name=name,
292
+ type=type,
293
+ price=price,
294
+ currency=currency,
295
+ interval=interval,
368
296
  )
369
- return Plan.model_validate(data)
370
297
 
371
298
  async def delete_plan(self, product_ref: str, plan_ref: str) -> dict[str, Any]:
372
- """Delete a plan. Maps to DELETE /v1/sdk/products/{ref}/plans/{ref}."""
373
- return await self._http.send(
374
- _RequestSpec("DELETE", f"/v1/sdk/products/{product_ref}/plans/{plan_ref}")
375
- )
376
-
377
- # --- Admin: Merchant + Platform ---
299
+ _shim_warn("sv.plans.adelete()")
300
+ return await self.plans.adelete(product_ref, plan_ref)
378
301
 
379
302
  async def get_merchant(self) -> Merchant:
380
- """Get merchant account details. Maps to GET /v1/sdk/merchant."""
381
- data = await self._http.send(_RequestSpec("GET", "/v1/sdk/merchant"))
382
- return Merchant.model_validate(data)
303
+ _shim_warn("sv.merchant.aget()")
304
+ return await self.merchant.aget()
383
305
 
384
306
  async def get_platform_config(self) -> PlatformConfig:
385
- """Get platform-level configuration. Maps to GET /v1/sdk/platform-config."""
386
- data = await self._http.send(_RequestSpec("GET", "/v1/sdk/platform-config"))
387
- return PlatformConfig.model_validate(data)
307
+ _shim_warn("sv.merchant.aget_platform_config()")
308
+ return await self.merchant.aget_platform_config()