zenopay-sdk 0.0.1__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.
- elusion/__init__.py +3 -0
- elusion/zenopay/__init__.py +42 -0
- elusion/zenopay/client.py +207 -0
- elusion/zenopay/config.py +116 -0
- elusion/zenopay/exceptions.py +227 -0
- elusion/zenopay/http/__init__.py +5 -0
- elusion/zenopay/http/client.py +320 -0
- elusion/zenopay/models/__init__.py +43 -0
- elusion/zenopay/models/common.py +93 -0
- elusion/zenopay/models/order.py +251 -0
- elusion/zenopay/models/payment.py +304 -0
- elusion/zenopay/models/webhook.py +122 -0
- elusion/zenopay/services/__init__.py +7 -0
- elusion/zenopay/services/base.py +175 -0
- elusion/zenopay/services/orders.py +135 -0
- elusion/zenopay/services/webhooks.py +188 -0
- elusion/zenopay/utils/__init__.py +9 -0
- elusion/zenopay/utils/helpers.py +35 -0
- zenopay_sdk-0.0.1.dist-info/METADATA +387 -0
- zenopay_sdk-0.0.1.dist-info/RECORD +22 -0
- zenopay_sdk-0.0.1.dist-info/WHEEL +4 -0
- zenopay_sdk-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,320 @@
|
|
1
|
+
"""HTTP client for the ZenoPay SDK."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any, Dict, Optional
|
5
|
+
|
6
|
+
import httpx
|
7
|
+
|
8
|
+
from elusion.zenopay.config import ZenoPayConfig
|
9
|
+
from elusion.zenopay.exceptions import (
|
10
|
+
ZenoPayNetworkError,
|
11
|
+
ZenoPayTimeoutError,
|
12
|
+
create_api_error,
|
13
|
+
)
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class HTTPClient:
|
19
|
+
"""HTTP client for making requests to the ZenoPay API."""
|
20
|
+
|
21
|
+
def __init__(self, config: ZenoPayConfig) -> None:
|
22
|
+
"""Initialize the HTTP client.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
config: ZenoPay configuration instance.
|
26
|
+
"""
|
27
|
+
self.config = config
|
28
|
+
self._client: Optional[httpx.AsyncClient] = None
|
29
|
+
self._sync_client: Optional[httpx.Client] = None
|
30
|
+
|
31
|
+
async def __aenter__(self) -> "HTTPClient":
|
32
|
+
"""Async context manager entry."""
|
33
|
+
await self._ensure_client()
|
34
|
+
return self
|
35
|
+
|
36
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
37
|
+
"""Async context manager exit."""
|
38
|
+
await self.close()
|
39
|
+
|
40
|
+
def __enter__(self) -> "HTTPClient":
|
41
|
+
"""Sync context manager entry."""
|
42
|
+
self._ensure_sync_client()
|
43
|
+
return self
|
44
|
+
|
45
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
46
|
+
"""Sync context manager exit."""
|
47
|
+
self.close_sync()
|
48
|
+
|
49
|
+
async def _ensure_client(self) -> None:
|
50
|
+
"""Ensure async client is initialized."""
|
51
|
+
if self._client is None:
|
52
|
+
self._client = httpx.AsyncClient(
|
53
|
+
timeout=self.config.timeout,
|
54
|
+
headers=self.config.headers.copy(),
|
55
|
+
)
|
56
|
+
|
57
|
+
def _ensure_sync_client(self) -> None:
|
58
|
+
"""Ensure sync client is initialized."""
|
59
|
+
if self._sync_client is None:
|
60
|
+
self._sync_client = httpx.Client(
|
61
|
+
timeout=self.config.timeout,
|
62
|
+
headers=self.config.headers.copy(),
|
63
|
+
)
|
64
|
+
|
65
|
+
async def close(self) -> None:
|
66
|
+
"""Close the async HTTP client."""
|
67
|
+
if self._client is not None:
|
68
|
+
await self._client.aclose()
|
69
|
+
self._client = None
|
70
|
+
|
71
|
+
def close_sync(self) -> None:
|
72
|
+
"""Close the sync HTTP client."""
|
73
|
+
if self._sync_client is not None:
|
74
|
+
self._sync_client.close()
|
75
|
+
self._sync_client = None
|
76
|
+
|
77
|
+
async def request(
|
78
|
+
self,
|
79
|
+
method: str,
|
80
|
+
url: str,
|
81
|
+
data: Optional[Dict[str, Any]] = None,
|
82
|
+
headers: Optional[Dict[str, str]] = None,
|
83
|
+
**kwargs: Any,
|
84
|
+
) -> Dict[str, Any]:
|
85
|
+
"""Make an async HTTP request.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.).
|
89
|
+
url: Request URL.
|
90
|
+
data: Form data to send.
|
91
|
+
headers: Additional headers.
|
92
|
+
**kwargs: Additional arguments for httpx.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
Parsed response data.
|
96
|
+
|
97
|
+
Raises:
|
98
|
+
ZenoPayAPIError: For API errors.
|
99
|
+
ZenoPayNetworkError: For network errors.
|
100
|
+
ZenoPayTimeoutError: For timeout errors.
|
101
|
+
"""
|
102
|
+
await self._ensure_client()
|
103
|
+
|
104
|
+
request_headers = self.config.headers.copy()
|
105
|
+
if headers:
|
106
|
+
request_headers.update(headers)
|
107
|
+
|
108
|
+
if data:
|
109
|
+
cleaned_data = {}
|
110
|
+
for key, value in data.items():
|
111
|
+
if value is not None:
|
112
|
+
cleaned_data[key] = str(value)
|
113
|
+
data = cleaned_data
|
114
|
+
|
115
|
+
try:
|
116
|
+
logger.debug(f"Making {method} request to {url}")
|
117
|
+
|
118
|
+
if self._client is None:
|
119
|
+
raise ZenoPayNetworkError("Async HTTP client is not initialized.", None)
|
120
|
+
|
121
|
+
response = await self._client.request(
|
122
|
+
method=method,
|
123
|
+
url=url,
|
124
|
+
data=data,
|
125
|
+
headers=request_headers,
|
126
|
+
**kwargs,
|
127
|
+
)
|
128
|
+
|
129
|
+
return await self._handle_response(response)
|
130
|
+
|
131
|
+
except httpx.TimeoutException as e:
|
132
|
+
logger.error(f"Request timeout: {e}")
|
133
|
+
raise ZenoPayTimeoutError(
|
134
|
+
f"Request timeout after {self.config.timeout} seconds",
|
135
|
+
self.config.timeout,
|
136
|
+
) from e
|
137
|
+
except httpx.NetworkError as e:
|
138
|
+
logger.error(f"Network error: {e}")
|
139
|
+
raise ZenoPayNetworkError(f"Network error: {str(e)}", e) from e
|
140
|
+
except Exception as e:
|
141
|
+
logger.error(f"Unexpected error: {e}")
|
142
|
+
raise ZenoPayNetworkError(f"Unexpected error: {str(e)}", e) from e
|
143
|
+
|
144
|
+
def request_sync(
|
145
|
+
self,
|
146
|
+
method: str,
|
147
|
+
url: str,
|
148
|
+
data: Optional[Dict[str, Any]] = None,
|
149
|
+
headers: Optional[Dict[str, str]] = None,
|
150
|
+
**kwargs: Any,
|
151
|
+
) -> Dict[str, Any]:
|
152
|
+
"""Make a sync HTTP request.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.).
|
156
|
+
url: Request URL.
|
157
|
+
data: Form data to send.
|
158
|
+
headers: Additional headers.
|
159
|
+
**kwargs: Additional arguments for httpx.
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
Parsed response data.
|
163
|
+
|
164
|
+
Raises:
|
165
|
+
ZenoPayAPIError: For API errors.
|
166
|
+
ZenoPayNetworkError: For network errors.
|
167
|
+
ZenoPayTimeoutError: For timeout errors.
|
168
|
+
"""
|
169
|
+
self._ensure_sync_client()
|
170
|
+
|
171
|
+
request_headers = self.config.headers.copy()
|
172
|
+
if headers:
|
173
|
+
request_headers.update(headers)
|
174
|
+
|
175
|
+
if data:
|
176
|
+
cleaned_data = {}
|
177
|
+
for key, value in data.items():
|
178
|
+
if value is not None:
|
179
|
+
cleaned_data[key] = str(value)
|
180
|
+
data = cleaned_data
|
181
|
+
|
182
|
+
try:
|
183
|
+
logger.debug(f"Making {method} request to {url}")
|
184
|
+
|
185
|
+
if self._sync_client is None:
|
186
|
+
raise ZenoPayNetworkError("Sync HTTP client is not initialized.", None)
|
187
|
+
|
188
|
+
response = self._sync_client.request(
|
189
|
+
method=method,
|
190
|
+
url=url,
|
191
|
+
data=data,
|
192
|
+
headers=request_headers,
|
193
|
+
**kwargs,
|
194
|
+
)
|
195
|
+
|
196
|
+
return self._handle_response_sync(response)
|
197
|
+
|
198
|
+
except httpx.TimeoutException as e:
|
199
|
+
logger.error(f"Request timeout: {e}")
|
200
|
+
raise ZenoPayTimeoutError(
|
201
|
+
f"Request timeout after {self.config.timeout} seconds",
|
202
|
+
self.config.timeout,
|
203
|
+
) from e
|
204
|
+
except httpx.NetworkError as e:
|
205
|
+
logger.error(f"Network error: {e}")
|
206
|
+
raise ZenoPayNetworkError(f"Network error: {str(e)}", e) from e
|
207
|
+
except Exception as e:
|
208
|
+
logger.error(f"Unexpected error: {e}")
|
209
|
+
raise ZenoPayNetworkError(f"Unexpected error: {str(e)}", e) from e
|
210
|
+
|
211
|
+
async def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
212
|
+
"""Handle HTTP response for async requests.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
response: HTTP response object.
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
Parsed response data.
|
219
|
+
|
220
|
+
Raises:
|
221
|
+
ZenoPayAPIError: For API errors.
|
222
|
+
"""
|
223
|
+
logger.debug(f"Response status: {response.status_code}")
|
224
|
+
|
225
|
+
try:
|
226
|
+
# Try to parse as JSON first
|
227
|
+
response_data = response.json()
|
228
|
+
except Exception:
|
229
|
+
# If JSON parsing fails, treat as text response
|
230
|
+
response_text = response.text
|
231
|
+
logger.debug(f"Non-JSON response: {response_text}")
|
232
|
+
|
233
|
+
# For successful responses that aren't JSON, create a basic structure
|
234
|
+
if response.is_success:
|
235
|
+
return {
|
236
|
+
"success": True,
|
237
|
+
"data": response_text,
|
238
|
+
"message": "Request successful",
|
239
|
+
}
|
240
|
+
else:
|
241
|
+
response_data: Dict[str, Any] = {
|
242
|
+
"success": False,
|
243
|
+
"error": response_text or f"HTTP {response.status_code}",
|
244
|
+
"message": f"Request failed with status {response.status_code}",
|
245
|
+
}
|
246
|
+
|
247
|
+
if response.is_success:
|
248
|
+
return response_data
|
249
|
+
|
250
|
+
# Handle API errors
|
251
|
+
error_message = response_data.get("error", f"HTTP {response.status_code}")
|
252
|
+
error_code = response_data.get("code")
|
253
|
+
|
254
|
+
logger.error(f"API error: {response.status_code} - {error_message}")
|
255
|
+
|
256
|
+
raise create_api_error(
|
257
|
+
status_code=response.status_code,
|
258
|
+
message=error_message,
|
259
|
+
response_data=response_data,
|
260
|
+
error_code=error_code,
|
261
|
+
)
|
262
|
+
|
263
|
+
def _handle_response_sync(self, response: httpx.Response) -> Dict[str, Any]:
|
264
|
+
"""Handle HTTP response for sync requests.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
response: HTTP response object.
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
Parsed response data.
|
271
|
+
|
272
|
+
Raises:
|
273
|
+
ZenoPayAPIError: For API errors.
|
274
|
+
"""
|
275
|
+
logger.debug(f"Response status: {response.status_code}")
|
276
|
+
|
277
|
+
try:
|
278
|
+
# Try to parse as JSON first
|
279
|
+
response_data = response.json()
|
280
|
+
except Exception:
|
281
|
+
# If JSON parsing fails, treat as text response
|
282
|
+
response_text = response.text
|
283
|
+
logger.debug(f"Non-JSON response: {response_text}")
|
284
|
+
|
285
|
+
# For successful responses that aren't JSON, create a basic structure
|
286
|
+
if response.is_success:
|
287
|
+
return {
|
288
|
+
"success": True,
|
289
|
+
"data": response_text,
|
290
|
+
"message": "Request successful",
|
291
|
+
}
|
292
|
+
else:
|
293
|
+
response_data: Dict[str, Any] = {
|
294
|
+
"success": False,
|
295
|
+
"error": response_text or f"HTTP {response.status_code}",
|
296
|
+
"message": f"Request failed with status {response.status_code}",
|
297
|
+
}
|
298
|
+
|
299
|
+
if response.is_success:
|
300
|
+
return response_data
|
301
|
+
|
302
|
+
error_message = response_data.get("error", f"HTTP {response.status_code}")
|
303
|
+
error_code = response_data.get("code")
|
304
|
+
|
305
|
+
logger.error(f"API error: {response.status_code} - {error_message}")
|
306
|
+
|
307
|
+
raise create_api_error(
|
308
|
+
status_code=response.status_code,
|
309
|
+
message=error_message,
|
310
|
+
response_data=response_data,
|
311
|
+
error_code=error_code,
|
312
|
+
)
|
313
|
+
|
314
|
+
async def post(self, url: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
315
|
+
"""Make a POST request."""
|
316
|
+
return await self.request("POST", url, data=data, **kwargs)
|
317
|
+
|
318
|
+
def post_sync(self, url: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
319
|
+
"""Make a sync POST request."""
|
320
|
+
return self.request_sync("POST", url, data=data, **kwargs)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Models package for the ZenoPay SDK."""
|
2
|
+
|
3
|
+
from elusion.zenopay.models.common import (
|
4
|
+
PAYMENT_STATUSES,
|
5
|
+
APIResponse,
|
6
|
+
StatusCheckRequest,
|
7
|
+
)
|
8
|
+
|
9
|
+
from elusion.zenopay.models.order import (
|
10
|
+
OrderBase,
|
11
|
+
NewOrder,
|
12
|
+
OrderStatus,
|
13
|
+
Order,
|
14
|
+
OrderResponse,
|
15
|
+
OrderStatusResponse,
|
16
|
+
OrderListParams,
|
17
|
+
)
|
18
|
+
|
19
|
+
from elusion.zenopay.models.webhook import (
|
20
|
+
WebhookPayload,
|
21
|
+
WebhookEvent,
|
22
|
+
WebhookResponse,
|
23
|
+
)
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
# Constants and utilities
|
27
|
+
"PAYMENT_STATUSES",
|
28
|
+
# Common models
|
29
|
+
"APIResponse",
|
30
|
+
"StatusCheckRequest",
|
31
|
+
# Order models
|
32
|
+
"OrderBase",
|
33
|
+
"NewOrder",
|
34
|
+
"OrderStatus",
|
35
|
+
"Order",
|
36
|
+
"OrderResponse",
|
37
|
+
"OrderStatusResponse",
|
38
|
+
"OrderListParams",
|
39
|
+
# Webhook models
|
40
|
+
"WebhookPayload",
|
41
|
+
"WebhookEvent",
|
42
|
+
"WebhookResponse",
|
43
|
+
]
|
@@ -0,0 +1,93 @@
|
|
1
|
+
"""Common models and types used across the ZenoPay SDK."""
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Generic, List, Optional, TypeVar
|
5
|
+
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
7
|
+
|
8
|
+
T = TypeVar("T")
|
9
|
+
|
10
|
+
|
11
|
+
class APIResponse(BaseModel, Generic[T]):
|
12
|
+
"""Generic API response wrapper."""
|
13
|
+
|
14
|
+
success: bool = Field(..., description="Whether the request was successful")
|
15
|
+
data: T = Field(..., description="Response data")
|
16
|
+
message: Optional[str] = Field(None, description="Response message")
|
17
|
+
error: Optional[str] = Field(None, description="Error message if applicable")
|
18
|
+
|
19
|
+
|
20
|
+
class TimestampedModel(BaseModel):
|
21
|
+
"""Base model with timestamp fields."""
|
22
|
+
|
23
|
+
created_at: Optional[datetime] = Field(None, description="Creation timestamp")
|
24
|
+
updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
|
25
|
+
|
26
|
+
|
27
|
+
class ErrorDetail(BaseModel):
|
28
|
+
"""Detailed error information."""
|
29
|
+
|
30
|
+
field: Optional[str] = Field(None, description="Field that caused the error")
|
31
|
+
code: str = Field(..., description="Error code")
|
32
|
+
message: str = Field(..., description="Human-readable error message")
|
33
|
+
|
34
|
+
|
35
|
+
class ValidationError(BaseModel):
|
36
|
+
"""Validation error response."""
|
37
|
+
|
38
|
+
success: bool = Field(False, description="Always false for errors")
|
39
|
+
error: str = Field(..., description="General error message")
|
40
|
+
errors: List[ErrorDetail]
|
41
|
+
|
42
|
+
|
43
|
+
class ZenoPayAPIRequest(BaseModel):
|
44
|
+
"""Base model for ZenoPay API requests."""
|
45
|
+
|
46
|
+
api_key: Optional[str] = Field(None, description="API key (usually null in requests)")
|
47
|
+
secret_key: Optional[str] = Field(None, description="Secret key (usually null in requests)")
|
48
|
+
account_id: str = Field(..., description="ZenoPay account ID")
|
49
|
+
|
50
|
+
def to_form_data(self) -> dict[str, str]:
|
51
|
+
"""Convert to form data format as expected by ZenoPay API."""
|
52
|
+
data = self.model_dump(exclude_unset=True, by_alias=True)
|
53
|
+
|
54
|
+
form_data: dict[str, str] = {}
|
55
|
+
for key, value in data.items():
|
56
|
+
if value is not None:
|
57
|
+
form_data[key] = str(value)
|
58
|
+
|
59
|
+
return form_data
|
60
|
+
|
61
|
+
|
62
|
+
class StatusCheckRequest(ZenoPayAPIRequest):
|
63
|
+
"""Request model for checking order status."""
|
64
|
+
|
65
|
+
check_status: int = Field(1, description="Always 1 for status check requests")
|
66
|
+
order_id: str = Field(..., description="Order ID to check")
|
67
|
+
|
68
|
+
model_config = ConfigDict(
|
69
|
+
json_schema_extra={
|
70
|
+
"example": {
|
71
|
+
"check_status": 1,
|
72
|
+
"order_id": "66c4bb9c9abb1",
|
73
|
+
"account_id": "zp87778",
|
74
|
+
"api_key": "null",
|
75
|
+
"secret_key": "null",
|
76
|
+
}
|
77
|
+
}
|
78
|
+
)
|
79
|
+
|
80
|
+
|
81
|
+
# Common status constants
|
82
|
+
PAYMENT_STATUSES = {
|
83
|
+
"PENDING": "PENDING",
|
84
|
+
"COMPLETED": "COMPLETED",
|
85
|
+
"FAILED": "FAILED",
|
86
|
+
"CANCELLED": "CANCELLED",
|
87
|
+
}
|
88
|
+
|
89
|
+
MAX_NAME_LENGTH = 100
|
90
|
+
MAX_EMAIL_LENGTH = 255
|
91
|
+
MAX_PHONE_LENGTH = 20
|
92
|
+
MAX_WEBHOOK_URL_LENGTH = 500
|
93
|
+
MAX_METADATA_LENGTH = 1000
|