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.
@@ -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