paydeck 1.0.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.
paydeck/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from paydeck.client import PayDeckClient
2
+ from paydeck.exceptions import PayDeckError, PayDeckAPIError
3
+
4
+ __version__ = "1.0.0"
5
+ __all__ = ["PayDeckClient", "PayDeckError", "PayDeckAPIError"]
paydeck/client.py ADDED
@@ -0,0 +1,322 @@
1
+ import hmac
2
+ import hashlib
3
+ import json
4
+ import time
5
+ import urllib.request
6
+ import urllib.error
7
+ import urllib.parse
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ from paydeck.exceptions import PayDeckAPIError, PayDeckError
11
+
12
+
13
+ class PayDeckClient:
14
+ """Official Python SDK client for PayDeck billing microservice."""
15
+
16
+ def __init__(
17
+ self,
18
+ base_url: str,
19
+ api_key: Optional[str] = None,
20
+ admin_token: Optional[str] = None,
21
+ timeout: float = 30.0,
22
+ retries: int = 3,
23
+ ):
24
+ self.base_url = base_url.rstrip("/")
25
+ self.api_key = api_key
26
+ self.admin_token = admin_token
27
+ self.timeout = timeout
28
+ self.retries = retries
29
+
30
+ def _headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
31
+ headers = {
32
+ "Content-Type": "application/json",
33
+ "User-Agent": "PayDeck-Python-SDK/1.0.0",
34
+ }
35
+ if self.api_key:
36
+ headers["Authorization"] = f"Bearer {self.api_key}"
37
+ if self.admin_token:
38
+ headers["X-Admin-Token"] = self.admin_token
39
+ if idempotency_key:
40
+ headers["Idempotency-Key"] = idempotency_key
41
+ return headers
42
+
43
+ def _request(
44
+ self,
45
+ method: str,
46
+ path: str,
47
+ params: Optional[Dict[str, Any]] = None,
48
+ data: Optional[Dict[str, Any]] = None,
49
+ idempotency_key: Optional[str] = None,
50
+ ) -> Dict[str, Any]:
51
+ url = f"{self.base_url}{path}"
52
+ if params:
53
+ query = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
54
+ if query:
55
+ url = f"{url}?{query}"
56
+
57
+ body_bytes = None
58
+ if data is not None:
59
+ body_bytes = json.dumps(data).encode("utf-8")
60
+
61
+ headers = self._headers(idempotency_key=idempotency_key)
62
+ attempt = 0
63
+
64
+ while True:
65
+ attempt += 1
66
+ req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method.upper())
67
+
68
+ try:
69
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
70
+ resp_data = resp.read().decode("utf-8")
71
+ return json.loads(resp_data) if resp_data else {}
72
+
73
+ except urllib.error.HTTPError as e:
74
+ resp_data = e.read().decode("utf-8") if e.fp else ""
75
+ parsed_error = {}
76
+ try:
77
+ parsed_error = json.loads(resp_data) if resp_data else {}
78
+ except Exception:
79
+ pass
80
+
81
+ # Extract standard error envelope fields
82
+ err_obj = parsed_error.get("error", {})
83
+ if isinstance(err_obj, str):
84
+ error_msg = err_obj
85
+ error_code = "API_ERROR"
86
+ details = []
87
+ request_id = parsed_error.get("request_id")
88
+ elif isinstance(err_obj, dict):
89
+ error_msg = err_obj.get("message") or err_obj.get("type") or "API Error"
90
+ error_code = err_obj.get("code") or err_obj.get("type") or "API_ERROR"
91
+ details = err_obj.get("details", [])
92
+ request_id = err_obj.get("request_id") or parsed_error.get("request_id")
93
+ else:
94
+ error_msg = e.reason or str(e)
95
+ error_code = f"HTTP_{e.code}"
96
+ details = []
97
+ request_id = None
98
+
99
+ # Retry on 429 rate limit or 5xx server error
100
+ if e.code in (429, 500, 502, 503, 504) and attempt <= self.retries:
101
+ delay = 0.1 * (2 ** (attempt - 1))
102
+ time.sleep(delay)
103
+ continue
104
+
105
+ raise PayDeckAPIError(
106
+ message=error_msg,
107
+ status_code=e.code,
108
+ error_code=error_code,
109
+ details=details,
110
+ request_id=request_id,
111
+ ) from e
112
+
113
+ except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
114
+ if attempt <= self.retries:
115
+ delay = 0.1 * (2 ** (attempt - 1))
116
+ time.sleep(delay)
117
+ continue
118
+ raise PayDeckError(f"Network error communicating with PayDeck: {e}") from e
119
+
120
+ # --- Consumer Billing Methods ---
121
+
122
+ def create_order(
123
+ self,
124
+ plan: Optional[str] = None,
125
+ amount_paise: Optional[int] = None,
126
+ currency: str = "INR",
127
+ receipt: Optional[str] = None,
128
+ notes: Optional[Dict[str, str]] = None,
129
+ metadata: Optional[Dict[str, Any]] = None,
130
+ description: Optional[str] = None,
131
+ idempotency_key: Optional[str] = None,
132
+ customer_id: Optional[str] = None,
133
+ ) -> Dict[str, Any]:
134
+ payload: Dict[str, Any] = {"currency": currency}
135
+ if plan:
136
+ payload["plan"] = plan
137
+ if amount_paise is not None:
138
+ payload["amount_paise"] = amount_paise
139
+ if receipt:
140
+ payload["receipt"] = receipt
141
+ if notes:
142
+ payload["notes"] = notes
143
+ if metadata:
144
+ payload["metadata"] = metadata
145
+ if description:
146
+ payload["description"] = description
147
+ if customer_id:
148
+ payload["customer_id"] = customer_id
149
+
150
+ return self._request("POST", "/v1/orders", data=payload, idempotency_key=idempotency_key)
151
+
152
+ def verify(
153
+ self,
154
+ razorpay_order_id: str,
155
+ razorpay_payment_id: str,
156
+ razorpay_signature: str,
157
+ ) -> Dict[str, Any]:
158
+ payload = {
159
+ "razorpay_order_id": razorpay_order_id,
160
+ "razorpay_payment_id": razorpay_payment_id,
161
+ "razorpay_signature": razorpay_signature,
162
+ }
163
+ return self._request("POST", "/v1/verify", data=payload)
164
+
165
+ def get_payment(self, payment_id: str) -> Dict[str, Any]:
166
+ return self._request("GET", f"/v1/payments/{urllib.parse.quote(payment_id)}")
167
+
168
+ def list_payments(self, limit: int = 25, offset: int = 0) -> Dict[str, Any]:
169
+ return self._request("GET", "/v1/payments", params={"limit": limit, "offset": offset})
170
+
171
+ def list_plans(self) -> Dict[str, Any]:
172
+ return self._request("GET", "/v1/plans")
173
+
174
+ def refund_payment(
175
+ self,
176
+ payment_id: str,
177
+ amount_paise: Optional[int] = None,
178
+ reason: Optional[str] = None,
179
+ ) -> Dict[str, Any]:
180
+ payload: Dict[str, Any] = {}
181
+ if amount_paise is not None:
182
+ payload["amount_paise"] = amount_paise
183
+ if reason:
184
+ payload["reason"] = reason
185
+ return self._request("POST", f"/v1/payments/{urllib.parse.quote(payment_id)}/refund", data=payload)
186
+
187
+ # --- Customer Domain Methods ---
188
+
189
+ def create_customer(
190
+ self,
191
+ email: Optional[str] = None,
192
+ name: Optional[str] = None,
193
+ external_user_id: Optional[str] = None,
194
+ metadata: Optional[Dict[str, Any]] = None,
195
+ ) -> Dict[str, Any]:
196
+ payload: Dict[str, Any] = {}
197
+ if email:
198
+ payload["email"] = email
199
+ if name:
200
+ payload["name"] = name
201
+ if external_user_id:
202
+ payload["external_user_id"] = external_user_id
203
+ if metadata:
204
+ payload["metadata"] = metadata
205
+ return self._request("POST", "/v1/customers", data=payload)
206
+
207
+ def list_customers(self, limit: int = 25, offset: int = 0) -> Dict[str, Any]:
208
+ return self._request("GET", "/v1/customers", params={"limit": limit, "offset": offset})
209
+
210
+ def list_customer_payments(self, customer_id: str, limit: int = 25, offset: int = 0) -> Dict[str, Any]:
211
+ return self._request("GET", f"/v1/customers/{urllib.parse.quote(customer_id)}/payments", params={"limit": limit, "offset": offset})
212
+
213
+ # --- Subscription Methods ---
214
+
215
+ def create_subscription(
216
+ self,
217
+ customer_id: str,
218
+ plan_id: str,
219
+ period_days: int = 30,
220
+ ) -> Dict[str, Any]:
221
+ payload = {
222
+ "customer_id": customer_id,
223
+ "plan_id": plan_id,
224
+ "period_days": period_days,
225
+ }
226
+ return self._request("POST", "/v1/subscriptions", data=payload)
227
+
228
+ def list_subscriptions(self, limit: int = 25, offset: int = 0, customer_id: Optional[str] = None) -> Dict[str, Any]:
229
+ params = {"limit": limit, "offset": offset}
230
+ if customer_id:
231
+ params["customer_id"] = customer_id
232
+ return self._request("GET", "/v1/subscriptions", params=params)
233
+
234
+ def cancel_subscription(self, subscription_id: str, immediately: bool = True) -> Dict[str, Any]:
235
+ return self._request("POST", f"/v1/subscriptions/{urllib.parse.quote(subscription_id)}/cancel", data={"immediately": immediately})
236
+
237
+ # --- Admin Methods ---
238
+
239
+ def create_product(self, slug: str, name: str, rate_limit_per_hour: int = 200) -> Dict[str, Any]:
240
+ payload = {"slug": slug, "name": name, "rate_limit_per_hour": rate_limit_per_hour}
241
+ return self._request("POST", "/v1/admin/products", data=payload)
242
+
243
+ def list_products(self) -> Dict[str, Any]:
244
+ return self._request("GET", "/v1/admin/products")
245
+
246
+ def mint_key(self, slug: str, name: Optional[str] = None, environment: str = "live") -> Dict[str, Any]:
247
+ payload: Dict[str, Any] = {"environment": environment}
248
+ if name:
249
+ payload["name"] = name
250
+ return self._request("POST", f"/v1/admin/products/{urllib.parse.quote(slug)}/keys", data=payload)
251
+
252
+ def revoke_key(self, slug: str, key_id: str) -> Dict[str, Any]:
253
+ return self._request("POST", f"/v1/admin/products/{urllib.parse.quote(slug)}/keys/{urllib.parse.quote(key_id)}/revoke")
254
+
255
+ def create_plan(
256
+ self,
257
+ slug: str,
258
+ plan_slug: str,
259
+ name: str,
260
+ amount_paise: int,
261
+ currency: str = "INR",
262
+ interval: str = "month",
263
+ ) -> Dict[str, Any]:
264
+ payload = {
265
+ "slug": plan_slug,
266
+ "name": name,
267
+ "amount_paise": amount_paise,
268
+ "currency": currency,
269
+ "interval": interval,
270
+ }
271
+ return self._request("POST", f"/v1/admin/products/{urllib.parse.quote(slug)}/plans", data=payload)
272
+
273
+ def list_audit_logs(
274
+ self,
275
+ limit: int = 25,
276
+ offset: int = 0,
277
+ product_id: Optional[str] = None,
278
+ action: Optional[str] = None,
279
+ ) -> Dict[str, Any]:
280
+ params: Dict[str, Any] = {"limit": limit, "offset": offset}
281
+ if product_id:
282
+ params["product_id"] = product_id
283
+ if action:
284
+ params["action"] = action
285
+ return self._request("GET", "/v1/admin/audit-logs", params=params)
286
+
287
+ def list_webhooks(
288
+ self,
289
+ limit: int = 25,
290
+ offset: int = 0,
291
+ status: Optional[str] = None,
292
+ product_id: Optional[str] = None,
293
+ ) -> Dict[str, Any]:
294
+ params: Dict[str, Any] = {"limit": limit, "offset": offset}
295
+ if status:
296
+ params["status"] = status
297
+ if product_id:
298
+ params["product_id"] = product_id
299
+ return self._request("GET", "/v1/admin/webhooks", params=params)
300
+
301
+ def redeliver_webhook(self, delivery_id: str) -> Dict[str, Any]:
302
+ return self._request("POST", f"/v1/admin/webhooks/{urllib.parse.quote(delivery_id)}/redeliver")
303
+
304
+ # --- Static Helpers ---
305
+
306
+ @staticmethod
307
+ def verify_webhook_signature(
308
+ payload: Union[str, bytes],
309
+ signature: str,
310
+ secret: str,
311
+ ) -> bool:
312
+ """Statically verify downstream HMAC-SHA256 PayDeck webhook signatures."""
313
+ if not signature or not secret:
314
+ return False
315
+
316
+ if isinstance(payload, str):
317
+ payload_bytes = payload.encode("utf-8")
318
+ else:
319
+ payload_bytes = payload
320
+
321
+ expected_sig = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
322
+ return hmac.compare_digest(expected_sig.lower(), signature.strip().lower())
paydeck/exceptions.py ADDED
@@ -0,0 +1,18 @@
1
+ class PayDeckError(Exception):
2
+ """Base exception for all PayDeck SDK errors."""
3
+ pass
4
+
5
+
6
+ class PayDeckAPIError(PayDeckError):
7
+ """Raised when the PayDeck API returns an error response."""
8
+
9
+ def __init__(self, message: str, status_code: int, error_code: str = None, details: list = None, request_id: str = None):
10
+ super().__init__(message)
11
+ self.message = message
12
+ self.status_code = status_code
13
+ self.error_code = error_code or "API_ERROR"
14
+ self.details = details or []
15
+ self.request_id = request_id
16
+
17
+ def __repr__(self):
18
+ return f"PayDeckAPIError(status_code={self.status_code}, code={self.error_code!r}, message={self.message!r}, request_id={self.request_id!r})"
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: paydeck
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for PayDeck billing microservice
5
+ Author-email: PiSigma / PayDeck Team <dev@pisigma.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pisigmac/PayDeck
8
+ Project-URL: Repository, https://github.com/pisigmac/PayDeck.git
9
+ Keywords: paydeck,billing,razorpay,stripe,payments,sdk
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+
21
+ # PayDeck Python SDK
22
+
23
+ Official Python SDK for the [PayDeck](https://github.com/pisigmac/PayDeck) generic billing microservice. Zero third-party runtime dependencies (uses standard library `urllib`, `json`, `hmac`, `hashlib`).
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install paydeck
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ```python
34
+ from paydeck import PayDeckClient, PayDeckAPIError
35
+
36
+ # Initialize client with product API key
37
+ client = PayDeckClient(
38
+ base_url="http://localhost:8787",
39
+ api_key="pd_live_1234567890abcdef...",
40
+ retries=3,
41
+ )
42
+
43
+ # 1. Create an order for a plan or custom amount
44
+ order = client.create_order(
45
+ plan="pro",
46
+ idempotency_key="unique_checkout_session_456"
47
+ )
48
+ print(f"Order Created: {order['order_id']}, PayDeck ID: {order['id']}")
49
+
50
+ # 2. Verify payment HMAC signature after gateway checkout
51
+ verified = client.verify(
52
+ razorpay_order_id=order['order_id'],
53
+ razorpay_payment_id="pay_999888777",
54
+ razorpay_signature="signature_hash..."
55
+ )
56
+ print(f"Payment Status: {verified['payment']['status']}")
57
+
58
+ # 3. Create a Customer & Subscription
59
+ customer = client.create_customer(email="dev@example.com", name="Jane Developer", external_user_id="usr_42")
60
+ subscription = client.create_subscription(customer_id=customer["id"], plan_id="plan_pro_month")
61
+
62
+ # 4. Verify Downstream Webhook Signatures (Static Method)
63
+ is_valid = PayDeckClient.verify_webhook_signature(
64
+ payload=raw_body_bytes_or_str,
65
+ signature=request_headers.get("X-PayDeck-Signature"),
66
+ secret="your_product_webhook_secret"
67
+ )
68
+ ```
69
+
70
+ ## Admin Management
71
+
72
+ ```python
73
+ admin_client = PayDeckClient(
74
+ base_url="http://localhost:8787",
75
+ admin_token="admin_secret_token",
76
+ )
77
+
78
+ # Create Product
79
+ product = admin_client.create_product(slug="formrelay", name="FormRelay SaaS")
80
+
81
+ # Mint Product API Key
82
+ key = admin_client.mint_key(slug="formrelay", name="Production Worker Key", environment="live")
83
+ print(f"New Key: {key['key']}")
84
+ ```
@@ -0,0 +1,7 @@
1
+ paydeck/__init__.py,sha256=k2c6XZW1sJJr5JoYgiJOgqMYYSXedODe2CW2k5H14ZM,188
2
+ paydeck/client.py,sha256=cMwc11-oqL9cqbmXYP1Z2so6ye4j-WX0U4g9ESMFiyk,11951
3
+ paydeck/exceptions.py,sha256=OfHDNw1kGruCYcY4GMwCtqWbwya5YavKXoHE3jTTUHM,729
4
+ paydeck-1.0.0.dist-info/METADATA,sha256=5bxLNnHeLYydK5L1pUdZxDXjfE4g8PiT8izT4GWZJAs,2729
5
+ paydeck-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ paydeck-1.0.0.dist-info/top_level.txt,sha256=zlRMcJQYSnd4Ilg9VcwCBfcrAhX-6rKKX_KjBB7cbhM,8
7
+ paydeck-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ paydeck