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 ADDED
@@ -0,0 +1,3 @@
1
+ """Elusion SDK namespace package."""
2
+
3
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,42 @@
1
+ """ZenoPay SDK for Python.
2
+
3
+ A modern Python SDK for the ZenoPay payment API with support for USSD payments,
4
+ order management, and webhook handling.
5
+ """
6
+
7
+ __version__ = "0.0.1"
8
+ __author__ = "Elution Hub"
9
+ __email__ = "elusion.lab@gmail.com"
10
+
11
+ from elusion.zenopay.client import ZenoPayClient as ZenoPay
12
+ from elusion.zenopay.exceptions import (
13
+ ZenoPayError,
14
+ ZenoPayAPIError,
15
+ ZenoPayAuthenticationError,
16
+ ZenoPayValidationError,
17
+ ZenoPayNetworkError,
18
+ )
19
+ from elusion.zenopay.models import (
20
+ Order,
21
+ NewOrder,
22
+ OrderStatus,
23
+ WebhookEvent,
24
+ WebhookPayload,
25
+ )
26
+
27
+ __all__ = [
28
+ # Main client
29
+ "ZenoPay",
30
+ # Exceptions
31
+ "ZenoPayError",
32
+ "ZenoPayAPIError",
33
+ "ZenoPayAuthenticationError",
34
+ "ZenoPayValidationError",
35
+ "ZenoPayNetworkError",
36
+ # Models
37
+ "Order",
38
+ "NewOrder",
39
+ "OrderStatus",
40
+ "WebhookEvent",
41
+ "WebhookPayload",
42
+ ]
@@ -0,0 +1,207 @@
1
+ """Main client for the ZenoPay SDK."""
2
+
3
+ import logging
4
+ from typing import Optional, Type
5
+ from types import TracebackType
6
+
7
+ from elusion.zenopay.config import ZenoPayConfig
8
+ from elusion.zenopay.http import HTTPClient
9
+ from elusion.zenopay.services import OrderService
10
+ from elusion.zenopay.services import WebhookService
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ZenoPayClient:
16
+ """Main client for interacting with the ZenoPay API.
17
+
18
+ This client provides access to all ZenoPay services including order management,
19
+ payment processing, and webhook handling. It supports both async and sync operations.
20
+
21
+ Examples:
22
+ Basic usage:
23
+ >>> client = ZenoPayClient(account_id="zp87778")
24
+
25
+ Async usage:
26
+ >>> async with ZenoPayClient(account_id="zp87778") as client:
27
+ ... order = await client.orders.create({
28
+ ... "buyer_email": "jackson@gmail.com",
29
+ ... "buyer_name": "Jackson Dastani",
30
+ ... "buyer_phone": "0652449389",
31
+ ... "amount": 1000,
32
+ ... "webhook_url": "https://yourwebsite.com/webhook"
33
+ ... })
34
+
35
+ Sync usage:
36
+ >>> with ZenoPayClient(account_id="zp87778") as client:
37
+ ... order = client.orders.create_sync({
38
+ ... "buyer_email": "jackson@gmail.com",
39
+ ... "buyer_name": "Jackson Dastani",
40
+ ... "buyer_phone": "0652449389",
41
+ ... "amount": 1000
42
+ ... })
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ account_id: str,
48
+ api_key: Optional[str] = None,
49
+ secret_key: Optional[str] = None,
50
+ base_url: Optional[str] = None,
51
+ timeout: Optional[float] = None,
52
+ max_retries: Optional[int] = None,
53
+ ):
54
+ """Initialize the ZenoPay client.
55
+
56
+ Args:
57
+ account_id: Your ZenoPay account ID (required).
58
+ api_key: API key (optional, can be set via environment variable).
59
+ secret_key: Secret key (optional, can be set via environment variable).
60
+ base_url: Base URL for the API (optional, defaults to production).
61
+ timeout: Request timeout in seconds (optional).
62
+ max_retries: Maximum number of retries for failed requests (optional).
63
+ **kwargs: Additional configuration options.
64
+
65
+ Examples:
66
+ >>> # Using environment variables for API keys
67
+ >>> client = ZenoPayClient(account_id="zp87778")
68
+
69
+ >>> # Explicit configuration
70
+ >>> client = ZenoPayClient(
71
+ ... account_id="zp87778",
72
+ ... api_key="your_api_key",
73
+ ... secret_key="your_secret_key",
74
+ ... timeout=30.0
75
+ ... )
76
+ """
77
+ self.config = ZenoPayConfig(
78
+ account_id=account_id,
79
+ api_key=api_key,
80
+ secret_key=secret_key,
81
+ base_url=base_url,
82
+ timeout=timeout,
83
+ max_retries=max_retries,
84
+ )
85
+
86
+ self.http_client = HTTPClient(self.config)
87
+
88
+ # Initialize services
89
+ self.orders = OrderService(self.http_client, self.config)
90
+ self.webhooks = WebhookService()
91
+
92
+ logger.info(f"ZenoPay client initialized for account: {account_id}")
93
+
94
+ async def __aenter__(self) -> "ZenoPayClient":
95
+ """Async context manager entry."""
96
+ await self.http_client.__aenter__()
97
+ return self
98
+
99
+ async def __aexit__(
100
+ self,
101
+ exc_type: Optional[Type[BaseException]],
102
+ exc_val: Optional[BaseException],
103
+ exc_tb: Optional[TracebackType],
104
+ ) -> None:
105
+ """Async context manager exit."""
106
+ await self.http_client.__aexit__(exc_type, exc_val, exc_tb)
107
+ await self.http_client.__aexit__(exc_type, exc_val, exc_tb)
108
+
109
+ def __enter__(self) -> "ZenoPayClient":
110
+ """Sync context manager entry."""
111
+ self.http_client.__enter__()
112
+ return self
113
+
114
+ def __exit__(
115
+ self,
116
+ exc_type: Optional[Type[BaseException]],
117
+ exc_val: Optional[BaseException],
118
+ exc_tb: Optional[TracebackType],
119
+ ) -> None:
120
+ """Sync context manager exit."""
121
+ self.http_client.__exit__(exc_type, exc_val, exc_tb)
122
+
123
+ async def close(self) -> None:
124
+ """Close the client and cleanup resources."""
125
+ await self.http_client.close()
126
+
127
+ def close_sync(self) -> None:
128
+ """Close the client and cleanup resources (sync version)."""
129
+ self.http_client.close_sync()
130
+
131
+ def test_connection(self) -> bool:
132
+ """Test the connection to ZenoPay API.
133
+
134
+ Returns:
135
+ True if connection is successful, False otherwise.
136
+
137
+ Examples:
138
+ >>> client = ZenoPayClient(account_id="zp87778")
139
+ >>> if client.test_connection():
140
+ ... print("Connection successful!")
141
+ ... else:
142
+ ... print("Connection failed!")
143
+ """
144
+ try:
145
+ self.orders.get_status_sync("test-connection-check")
146
+ return True
147
+ except Exception as e:
148
+ logger.debug(f"Connection test failed: {e}")
149
+ return False
150
+
151
+ async def test_connection_async(self) -> bool:
152
+ """Test the connection to ZenoPay API (async version).
153
+
154
+ Returns:
155
+ True if connection is successful, False otherwise.
156
+
157
+ Examples:
158
+ >>> async with ZenoPayClient(account_id="zp87778") as client:
159
+ ... if await client.test_connection_async():
160
+ ... print("Connection successful!")
161
+ ... else:
162
+ ... print("Connection failed!")
163
+ """
164
+ try:
165
+ await self.orders.get_status("test-connection-check")
166
+ return True
167
+ except Exception as e:
168
+ logger.debug(f"Connection test failed: {e}")
169
+ return False
170
+
171
+ @property
172
+ def account_id(self) -> str:
173
+ """Get the account ID."""
174
+ return self.config.account_id or ""
175
+
176
+ @property
177
+ def base_url(self) -> str:
178
+ """Get the base URL."""
179
+ return self.config.base_url
180
+
181
+ def get_config(self) -> ZenoPayConfig:
182
+ """Get the current configuration.
183
+
184
+ Returns:
185
+ Current ZenoPayConfig instance.
186
+ """
187
+ return self.config
188
+
189
+ def update_config(self, **kwargs: object) -> None:
190
+ """Update client configuration.
191
+
192
+ Args:
193
+ **kwargs: Configuration parameters to update.
194
+
195
+ Examples:
196
+ >>> client = ZenoPayClient(account_id="zp87778")
197
+ >>> client.update_config(timeout=60.0, max_retries=5)
198
+ """
199
+ for key, value in kwargs.items():
200
+ if hasattr(self.config, key):
201
+ setattr(self.config, key, value)
202
+ else:
203
+ logger.warning(f"Unknown configuration parameter: {key}")
204
+
205
+ def __repr__(self) -> str:
206
+ """String representation of the client."""
207
+ return f"ZenoPayClient(account_id='{self.account_id}', base_url='{self.base_url}')"
@@ -0,0 +1,116 @@
1
+ """Configuration and constants for the ZenoPay SDK."""
2
+
3
+ import os
4
+ from typing import Dict, Optional
5
+ from dotenv import load_dotenv
6
+
7
+ # Load environment variables from .env file if it exists
8
+ load_dotenv()
9
+
10
+ # Default configuration
11
+ DEFAULT_BASE_URL = "https://api.zeno.africa"
12
+ DEFAULT_TIMEOUT = 30.0
13
+ DEFAULT_MAX_RETRIES = 3
14
+ DEFAULT_RETRY_DELAY = 1.0
15
+
16
+ # Environment variable names
17
+ ENV_API_KEY = "ZENOPAY_API_KEY"
18
+ ENV_SECRET_KEY = "ZENOPAY_SECRET_KEY"
19
+ ENV_ACCOUNT_ID = "ZENOPAY_ACCOUNT_ID"
20
+ ENV_BASE_URL = "ZENOPAY_BASE_URL"
21
+ ENV_TIMEOUT = "ZENOPAY_TIMEOUT"
22
+
23
+ # HTTP headers
24
+ DEFAULT_HEADERS = {
25
+ "User-Agent": "zenopay-python-sdk",
26
+ "Accept": "application/json",
27
+ "Content-Type": "application/x-www-form-urlencoded",
28
+ }
29
+
30
+ # API endpoints
31
+ ENDPOINTS = {
32
+ "create_order": "",
33
+ "order_status": "/order-status",
34
+ }
35
+
36
+ # Payment statuses
37
+ PAYMENT_STATUSES = {
38
+ "PENDING": "PENDING",
39
+ "COMPLETED": "COMPLETED",
40
+ "FAILED": "FAILED",
41
+ "CANCELLED": "CANCELLED",
42
+ }
43
+
44
+
45
+ class ZenoPayConfig:
46
+ """Configuration class for the ZenoPay SDK."""
47
+
48
+ def __init__(
49
+ self,
50
+ api_key: Optional[str] = None,
51
+ secret_key: Optional[str] = None,
52
+ account_id: Optional[str] = None,
53
+ base_url: Optional[str] = None,
54
+ timeout: Optional[float] = None,
55
+ max_retries: Optional[int] = None,
56
+ retry_delay: Optional[float] = None,
57
+ headers: Optional[Dict[str, str]] = None,
58
+ ) -> None:
59
+ """Initialize configuration.
60
+
61
+ Args:
62
+ api_key: ZenoPay API key. If not provided, will try to get from environment.
63
+ secret_key: ZenoPay secret key. If not provided, will try to get from environment.
64
+ account_id: ZenoPay account ID. If not provided, will try to get from environment.
65
+ base_url: Base URL for the ZenoPay API.
66
+ timeout: Request timeout in seconds.
67
+ max_retries: Maximum number of retries for failed requests.
68
+ retry_delay: Delay between retries in seconds.
69
+ headers: Additional headers to include in requests.
70
+ """
71
+ self.api_key = api_key or os.getenv(ENV_API_KEY)
72
+ self.secret_key = secret_key or os.getenv(ENV_SECRET_KEY)
73
+ self.account_id = account_id or os.getenv(ENV_ACCOUNT_ID)
74
+
75
+ if not self.account_id:
76
+ raise ValueError(f"Account ID is required. Set {ENV_ACCOUNT_ID} environment variable " "or pass account_id parameter.")
77
+
78
+ self.base_url = base_url or os.getenv(ENV_BASE_URL, DEFAULT_BASE_URL)
79
+
80
+ # Parse timeout from environment if provided
81
+ env_timeout = os.getenv(ENV_TIMEOUT)
82
+ if env_timeout:
83
+ try:
84
+ env_timeout_float = float(env_timeout)
85
+ except ValueError:
86
+ env_timeout_float = DEFAULT_TIMEOUT
87
+ else:
88
+ env_timeout_float = DEFAULT_TIMEOUT
89
+
90
+ self.timeout = timeout or env_timeout_float
91
+ self.max_retries = max_retries or DEFAULT_MAX_RETRIES
92
+ self.retry_delay = retry_delay or DEFAULT_RETRY_DELAY
93
+
94
+ # Merge default headers with custom headers
95
+ self.headers = DEFAULT_HEADERS.copy()
96
+ if headers:
97
+ self.headers.update(headers)
98
+
99
+ def get_endpoint_url(self, endpoint: str) -> str:
100
+ """Get the full URL for an endpoint.
101
+
102
+ Args:
103
+ endpoint: Endpoint key from ENDPOINTS dict.
104
+
105
+ Returns:
106
+ Full URL for the endpoint.
107
+ """
108
+ if endpoint not in ENDPOINTS:
109
+ raise ValueError(f"Unknown endpoint: {endpoint}")
110
+
111
+ endpoint_path = ENDPOINTS[endpoint]
112
+ if endpoint_path:
113
+ return f"{self.base_url.rstrip('/')}{endpoint_path}"
114
+ else:
115
+ # For create_order, the base URL is the endpoint
116
+ return self.base_url
@@ -0,0 +1,227 @@
1
+ """Custom exceptions for the ZenoPay SDK."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class ZenoPayError(Exception):
7
+ """Base exception for all ZenoPay SDK errors."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: Optional[int] = None,
13
+ response_data: Optional[Dict[str, Any]] = None,
14
+ ) -> None:
15
+ """Initialize ZenoPayError.
16
+
17
+ Args:
18
+ message: Error message.
19
+ status_code: HTTP status code if applicable.
20
+ response_data: Response data from the API if available.
21
+ """
22
+ super().__init__(message)
23
+ self.message = message
24
+ self.status_code = status_code
25
+ self.response_data = response_data or {}
26
+
27
+
28
+ class ZenoPayAPIError(ZenoPayError):
29
+ """Exception raised for API errors from the ZenoPay service."""
30
+
31
+ def __init__(
32
+ self,
33
+ message: str,
34
+ status_code: int,
35
+ response_data: Optional[Dict[str, Any]] = None,
36
+ error_code: Optional[str] = None,
37
+ ) -> None:
38
+ """Initialize ZenoPayAPIError.
39
+
40
+ Args:
41
+ message: Error message from the API.
42
+ status_code: HTTP status code.
43
+ response_data: Full response data from the API.
44
+ error_code: Specific error code from the API.
45
+ """
46
+ super().__init__(message, status_code, response_data)
47
+ self.error_code = error_code
48
+
49
+
50
+ class ZenoPayAuthenticationError(ZenoPayAPIError):
51
+ """Exception raised for authentication errors (401)."""
52
+
53
+ def __init__(
54
+ self,
55
+ message: str = "Invalid API key or secret key",
56
+ response_data: Optional[Dict[str, Any]] = None,
57
+ ) -> None:
58
+ super().__init__(message, 401, response_data, "AUTHENTICATION_ERROR")
59
+
60
+
61
+ class ZenoPayAuthorizationError(ZenoPayAPIError):
62
+ """Exception raised for authorization errors (403)."""
63
+
64
+ def __init__(
65
+ self,
66
+ message: str = "Insufficient permissions for this operation",
67
+ response_data: Optional[Dict[str, Any]] = None,
68
+ ) -> None:
69
+ super().__init__(message, 403, response_data, "AUTHORIZATION_ERROR")
70
+
71
+
72
+ class ZenoPayNotFoundError(ZenoPayAPIError):
73
+ """Exception raised when a resource is not found (404)."""
74
+
75
+ def __init__(
76
+ self,
77
+ message: str = "The requested resource was not found",
78
+ response_data: Optional[Dict[str, Any]] = None,
79
+ ) -> None:
80
+ super().__init__(message, 404, response_data, "NOT_FOUND_ERROR")
81
+
82
+
83
+ class ZenoPayValidationError(ZenoPayAPIError):
84
+ """Exception raised for validation errors (400, 422)."""
85
+
86
+ def __init__(
87
+ self,
88
+ message: str,
89
+ status_code: int = 400,
90
+ response_data: Optional[Dict[str, Any]] = None,
91
+ validation_errors: Optional[Dict[str, Any]] = None,
92
+ ) -> None:
93
+ """Initialize ZenoPayValidationError.
94
+
95
+ Args:
96
+ message: Error message.
97
+ status_code: HTTP status code (400 or 422).
98
+ response_data: Response data from the API.
99
+ validation_errors: Detailed validation errors by field.
100
+ """
101
+ super().__init__(message, status_code, response_data, "VALIDATION_ERROR")
102
+ self.validation_errors = validation_errors or {}
103
+
104
+
105
+ class ZenoPayRateLimitError(ZenoPayAPIError):
106
+ """Exception raised when rate limit is exceeded (429)."""
107
+
108
+ def __init__(
109
+ self,
110
+ message: str = "Rate limit exceeded",
111
+ response_data: Optional[Dict[str, Any]] = None,
112
+ retry_after: Optional[int] = None,
113
+ ) -> None:
114
+ """Initialize ZenoPayRateLimitError.
115
+
116
+ Args:
117
+ message: Error message.
118
+ response_data: Response data from the API.
119
+ retry_after: Number of seconds to wait before retrying.
120
+ """
121
+ super().__init__(message, 429, response_data, "RATE_LIMIT_ERROR")
122
+ self.retry_after = retry_after
123
+
124
+
125
+ class ZenoPayServerError(ZenoPayAPIError):
126
+ """Exception raised for server errors (5xx)."""
127
+
128
+ def __init__(
129
+ self,
130
+ message: str = "Internal server error",
131
+ status_code: int = 500,
132
+ response_data: Optional[Dict[str, Any]] = None,
133
+ ) -> None:
134
+ super().__init__(message, status_code, response_data, "SERVER_ERROR")
135
+
136
+
137
+ class ZenoPayNetworkError(ZenoPayError):
138
+ """Exception raised for network-related errors."""
139
+
140
+ def __init__(
141
+ self,
142
+ message: str = "Network error occurred",
143
+ original_error: Optional[Exception] = None,
144
+ ) -> None:
145
+ """Initialize ZenoPayNetworkError.
146
+
147
+ Args:
148
+ message: Error message.
149
+ original_error: The original exception that caused this error.
150
+ """
151
+ super().__init__(message)
152
+ self.original_error = original_error
153
+
154
+
155
+ class ZenoPayTimeoutError(ZenoPayNetworkError):
156
+ """Exception raised when a request times out."""
157
+
158
+ def __init__(
159
+ self,
160
+ message: str = "Request timeout",
161
+ timeout_duration: Optional[float] = None,
162
+ ) -> None:
163
+ """Initialize ZenoPayTimeoutError.
164
+
165
+ Args:
166
+ message: Error message.
167
+ timeout_duration: The timeout duration that was exceeded.
168
+ """
169
+ super().__init__(message)
170
+ self.timeout_duration = timeout_duration
171
+
172
+
173
+ class ZenoPayWebhookError(ZenoPayError):
174
+ """Exception raised for webhook-related errors."""
175
+
176
+ def __init__(
177
+ self,
178
+ message: str = "Webhook processing error",
179
+ webhook_data: Optional[Dict[str, Any]] = None,
180
+ ) -> None:
181
+ """Initialize ZenoPayWebhookError.
182
+
183
+ Args:
184
+ message: Error message.
185
+ webhook_data: The webhook data that caused the error.
186
+ """
187
+ super().__init__(message)
188
+ self.webhook_data = webhook_data or {}
189
+
190
+
191
+ def create_api_error(
192
+ status_code: int,
193
+ message: str,
194
+ response_data: Optional[Dict[str, Any]] = None,
195
+ error_code: Optional[str] = None,
196
+ ) -> ZenoPayAPIError:
197
+ """Factory function to create appropriate API error based on status code.
198
+
199
+ Args:
200
+ status_code: HTTP status code.
201
+ message: Error message.
202
+ response_data: Response data from the API.
203
+ error_code: Specific error code from the API.
204
+
205
+ Returns:
206
+ Appropriate ZenoPayAPIError subclass instance.
207
+ """
208
+ if status_code == 401:
209
+ return ZenoPayAuthenticationError(message, response_data)
210
+ elif status_code == 403:
211
+ return ZenoPayAuthorizationError(message, response_data)
212
+ elif status_code == 404:
213
+ return ZenoPayNotFoundError(message, response_data)
214
+ elif status_code in (400, 422):
215
+ validation_errors = None
216
+ if response_data and "errors" in response_data:
217
+ validation_errors = response_data["errors"]
218
+ return ZenoPayValidationError(message, status_code, response_data, validation_errors)
219
+ elif status_code == 429:
220
+ retry_after = None
221
+ if response_data and "retry_after" in response_data:
222
+ retry_after = response_data["retry_after"]
223
+ return ZenoPayRateLimitError(message, response_data, retry_after)
224
+ elif status_code >= 500:
225
+ return ZenoPayServerError(message, status_code, response_data)
226
+ else:
227
+ return ZenoPayAPIError(message, status_code, response_data, error_code)
@@ -0,0 +1,5 @@
1
+ from elusion.zenopay.http.client import HTTPClient
2
+
3
+ __all__ = [
4
+ "HTTPClient",
5
+ ]