mobiska 0.1.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.
mobiska/__init__.py ADDED
@@ -0,0 +1,95 @@
1
+ """Mobiska Python SDK.
2
+
3
+ Public API::
4
+
5
+ from mobiska import InitSMS, InitPayment
6
+
7
+ sms = InitSMS(username=…, password=…, service_id=…, sender_id=…)
8
+ payment = InitPayment(username=…, password=…, service_id=…)
9
+
10
+ Both factories also accept ``timeout``, ``retry_count`` and ``retry_delay``
11
+ keyword arguments. When called with no arguments they read credentials from
12
+ environment variables (``MOBISKA_USER_NAME``, ``MOBISKA_PASSWORD``,
13
+ ``MOBISKA_SERVICE_ID``, ``MOBISKA_SMS_SENDER_ID``, ``MOBISKA_API_URL``).
14
+ """
15
+
16
+ from typing import Optional
17
+
18
+ from .client import BaseClient
19
+ from .exceptions import (
20
+ APIError,
21
+ AuthenticationError,
22
+ MobiskaError,
23
+ NotFoundError,
24
+ RateLimitError,
25
+ ValidationError,
26
+ )
27
+ from .resources import PaymentClient, SMSClient
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ __all__ = [
32
+ "InitSMS",
33
+ "InitPayment",
34
+ "SMSClient",
35
+ "PaymentClient",
36
+ "BaseClient",
37
+ "MobiskaError",
38
+ "AuthenticationError",
39
+ "NotFoundError",
40
+ "RateLimitError",
41
+ "ValidationError",
42
+ "APIError",
43
+ ]
44
+
45
+
46
+ def InitSMS(
47
+ username: Optional[str] = None,
48
+ password: Optional[str] = None,
49
+ service_id: Optional[int] = None,
50
+ sender_id: Optional[str] = None,
51
+ api_url: Optional[str] = None,
52
+ timeout: int = 30,
53
+ retry_count: int = 0,
54
+ retry_delay: float = 0.5,
55
+ ) -> SMSClient:
56
+ """Create and return an :class:`SMSClient`.
57
+
58
+ All arguments are optional — when omitted the values are read from
59
+ environment variables.
60
+ """
61
+ return SMSClient(
62
+ username=username,
63
+ password=password,
64
+ service_id=service_id,
65
+ sender_id=sender_id,
66
+ api_url=api_url,
67
+ timeout=timeout,
68
+ retry_count=retry_count,
69
+ retry_delay=retry_delay,
70
+ )
71
+
72
+
73
+ def InitPayment(
74
+ username: Optional[str] = None,
75
+ password: Optional[str] = None,
76
+ service_id: Optional[int] = None,
77
+ api_url: Optional[str] = None,
78
+ timeout: int = 30,
79
+ retry_count: int = 0,
80
+ retry_delay: float = 0.5,
81
+ ) -> PaymentClient:
82
+ """Create and return a :class:`PaymentClient`.
83
+
84
+ All arguments are optional — when omitted the values are read from
85
+ environment variables.
86
+ """
87
+ return PaymentClient(
88
+ username=username,
89
+ password=password,
90
+ service_id=service_id,
91
+ api_url=api_url,
92
+ timeout=timeout,
93
+ retry_count=retry_count,
94
+ retry_delay=retry_delay,
95
+ )
mobiska/auth.py ADDED
@@ -0,0 +1,32 @@
1
+ """Basic Authentication helper for the Mobiska API.
2
+
3
+ Mobiska uses HTTP Basic Auth: the client_key (username) and secret_key
4
+ (password) are combined as ``username:password``, base64-encoded, and sent
5
+ in the ``Authorization`` header prefixed with ``Basic``.
6
+ """
7
+
8
+ import base64
9
+ from typing import Dict
10
+
11
+
12
+ class BasicAuth:
13
+ """Generates the Basic-auth Authorization header for API requests."""
14
+
15
+ def __init__(self, username: str, password: str):
16
+ if not username or not password:
17
+ raise ValueError("username and password must not be empty.")
18
+ self.username = username
19
+ self.password = password
20
+
21
+ @property
22
+ def _encoded(self) -> str:
23
+ raw = f"{self.username}:{self.password}".encode("utf-8")
24
+ return base64.b64encode(raw).decode("ascii")
25
+
26
+ def get_headers(self) -> Dict[str, str]:
27
+ """Return the auth headers to attach to every request."""
28
+ return {
29
+ "Authorization": f"Basic {self._encoded}",
30
+ "Content-Type": "application/json",
31
+ "Accept": "application/json",
32
+ }
mobiska/client.py ADDED
@@ -0,0 +1,127 @@
1
+ """Base HTTP client for the Mobiska API.
2
+
3
+ Provides the shared transport layer used by :class:`SMSClient` and
4
+ :class:`PaymentClient`:
5
+
6
+ * Configuration resolution (kwargs → environment variables)
7
+ * Basic-auth header setup
8
+ * Requests session with configurable timeout
9
+ * Automatic retry on transient failures (network errors and 5xx)
10
+ * Centralised response parsing and error mapping
11
+ """
12
+
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, Optional
16
+
17
+ import requests
18
+
19
+ from .auth import BasicAuth
20
+ from .config import Config
21
+ from .exceptions import (
22
+ APIError,
23
+ AuthenticationError,
24
+ NotFoundError,
25
+ RateLimitError,
26
+ )
27
+
28
+ DEFAULT_TIMEOUT = 30
29
+ DEFAULT_RETRY_COUNT = 0
30
+ DEFAULT_RETRY_DELAY = 0.5
31
+
32
+
33
+ class BaseClient:
34
+ """Shared HTTP client for all Mobiska API resources."""
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ username: Optional[str] = None,
40
+ password: Optional[str] = None,
41
+ service_id: Optional[int] = None,
42
+ sender_id: Optional[str] = None,
43
+ api_url: Optional[str] = None,
44
+ timeout: int = DEFAULT_TIMEOUT,
45
+ retry_count: int = DEFAULT_RETRY_COUNT,
46
+ retry_delay: float = DEFAULT_RETRY_DELAY,
47
+ ):
48
+ self.config = Config.resolve(
49
+ username=username,
50
+ password=password,
51
+ service_id=service_id,
52
+ sender_id=sender_id,
53
+ api_url=api_url,
54
+ )
55
+ self.auth = BasicAuth(self.config.username, self.config.password)
56
+ self.timeout = timeout
57
+ self.retry_count = retry_count
58
+ self.retry_delay = retry_delay
59
+
60
+ self._session = requests.Session()
61
+ self._session.headers.update(self.auth.get_headers())
62
+
63
+ # ------------------------------------------------------------------
64
+ # Helpers
65
+ # ------------------------------------------------------------------
66
+
67
+ @staticmethod
68
+ def _request_time() -> str:
69
+ """Return the current UTC timestamp in ISO-8601 format."""
70
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
71
+
72
+ def _build_payload(self, **extra: Any) -> Dict[str, Any]:
73
+ """Start every payload with the common fields, then merge extras."""
74
+ payload: Dict[str, Any] = {
75
+ "service_id": self.config.service_id,
76
+ }
77
+ payload.update(extra)
78
+ return payload
79
+
80
+ # ------------------------------------------------------------------
81
+ # HTTP
82
+ # ------------------------------------------------------------------
83
+
84
+ def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
85
+ """POST *payload* to *path*, retrying on transient failures."""
86
+ url = f"{self.config.api_url}/{path.lstrip('/')}"
87
+ last_exc: Optional[Exception] = None
88
+
89
+ for attempt in range(self.retry_count + 1):
90
+ try:
91
+ response = self._session.post(url, json=payload, timeout=self.timeout)
92
+ return self._handle_response(response)
93
+ except (APIError, requests.RequestException) as exc:
94
+ last_exc = exc
95
+ if attempt < self.retry_count:
96
+ time.sleep(self.retry_delay)
97
+ continue
98
+ raise
99
+
100
+ # Should never reach here, but satisfy the type checker.
101
+ if last_exc:
102
+ raise last_exc
103
+ raise APIError("Request failed for an unknown reason.")
104
+
105
+ def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
106
+ """Map HTTP status codes to exceptions, otherwise return JSON."""
107
+ status = response.status_code
108
+
109
+ if status == 401:
110
+ raise AuthenticationError("Invalid or missing credentials.")
111
+ if status == 403:
112
+ raise AuthenticationError("Insufficient permissions.")
113
+ if status == 404:
114
+ raise NotFoundError(f"Resource not found: {response.url}")
115
+ if status == 429:
116
+ raise RateLimitError("API rate limit exceeded. Please slow down.")
117
+ if status >= 500:
118
+ raise APIError(
119
+ f"Server error ({status}): {response.text}",
120
+ status_code=status,
121
+ )
122
+
123
+ response.raise_for_status()
124
+
125
+ if response.content:
126
+ return response.json()
127
+ return {}
mobiska/config.py ADDED
@@ -0,0 +1,73 @@
1
+ """Configuration management for the Mobiska SDK.
2
+
3
+ Supports two configuration strategies:
4
+ 1. Environment variables (MOBISKA_USER_NAME, MOBISKA_PASSWORD, …)
5
+ 2. Explicit keyword arguments passed to the client constructor.
6
+
7
+ Keyword arguments always take precedence; any missing value falls back to
8
+ the corresponding environment variable.
9
+ """
10
+
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import Optional
14
+
15
+ DEFAULT_API_URL = "https://api.mobiska.com"
16
+
17
+ # Environment-variable names
18
+ ENV_USERNAME = "MOBISKA_USER_NAME"
19
+ ENV_PASSWORD = "MOBISKA_PASSWORD"
20
+ ENV_SERVICE_ID = "MOBISKA_SERVICE_ID"
21
+ ENV_SENDER_ID = "MOBISKA_SMS_SENDER_ID"
22
+ ENV_API_URL = "MOBISKA_API_URL"
23
+
24
+ _REQUIRED_MSG = (
25
+ "Missing required configuration: username, password, and service_id. "
26
+ "Pass them as keyword arguments or set the environment variables "
27
+ f"{ENV_USERNAME}, {ENV_PASSWORD}, {ENV_SERVICE_ID}."
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class Config:
33
+ """Resolved SDK configuration."""
34
+
35
+ username: str
36
+ password: str
37
+ service_id: int
38
+ sender_id: Optional[str] = None
39
+ api_url: str = DEFAULT_API_URL
40
+
41
+ @classmethod
42
+ def resolve(
43
+ cls,
44
+ username: Optional[str] = None,
45
+ password: Optional[str] = None,
46
+ service_id: Optional[int] = None,
47
+ sender_id: Optional[str] = None,
48
+ api_url: Optional[str] = None,
49
+ ) -> "Config":
50
+ """Build a Config from kwargs, falling back to env vars.
51
+
52
+ Raises ValueError if any required value is missing.
53
+ """
54
+ username = username or os.environ.get(ENV_USERNAME)
55
+ password = password or os.environ.get(ENV_PASSWORD)
56
+
57
+ if service_id is None:
58
+ service_id_str = os.environ.get(ENV_SERVICE_ID)
59
+ service_id = int(service_id_str) if service_id_str else None
60
+
61
+ sender_id = sender_id or os.environ.get(ENV_SENDER_ID)
62
+ api_url = api_url or os.environ.get(ENV_API_URL, DEFAULT_API_URL)
63
+
64
+ if not username or not password or service_id is None:
65
+ raise ValueError(_REQUIRED_MSG)
66
+
67
+ return cls(
68
+ username=username,
69
+ password=password,
70
+ service_id=service_id,
71
+ sender_id=sender_id,
72
+ api_url=api_url.rstrip("/"),
73
+ )
@@ -0,0 +1,39 @@
1
+ """Exception hierarchy for the Mobiska SDK."""
2
+
3
+
4
+ class MobiskaError(Exception):
5
+ """Base exception for all Mobiska SDK errors."""
6
+
7
+ pass
8
+
9
+
10
+ class AuthenticationError(MobiskaError):
11
+ """Raised when API authentication fails (401/403)."""
12
+
13
+ pass
14
+
15
+
16
+ class NotFoundError(MobiskaError):
17
+ """Raised when a resource is not found (404)."""
18
+
19
+ pass
20
+
21
+
22
+ class RateLimitError(MobiskaError):
23
+ """Raised when the API rate limit is exceeded (429)."""
24
+
25
+ pass
26
+
27
+
28
+ class ValidationError(MobiskaError):
29
+ """Raised when input validation fails before the request is sent."""
30
+
31
+ pass
32
+
33
+
34
+ class APIError(MobiskaError):
35
+ """Raised for unexpected API errors (5xx or non-standard responses)."""
36
+
37
+ def __init__(self, message: str, status_code: int = None):
38
+ super().__init__(message)
39
+ self.status_code = status_code
@@ -0,0 +1,6 @@
1
+ """API resource clients for Mobiska."""
2
+
3
+ from .payment import PaymentClient
4
+ from .sms import SMSClient
5
+
6
+ __all__ = ["SMSClient", "PaymentClient"]
@@ -0,0 +1,139 @@
1
+ """Payment resource — make payments, check status, and look up accounts."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from ..client import BaseClient
6
+ from ..exceptions import ValidationError
7
+
8
+ # Allowed enum-like values (kept as plain sets for zero-overhead validation)
9
+ _NETWORKS = {"MTN", "VOD", "AIR", "VIS", "MAS"}
10
+ _PAYMENT_OPTIONS = {"MOM", "CRD", "CRM"}
11
+
12
+
13
+ class PaymentClient(BaseClient):
14
+ """Client for Mobiska Payment endpoints."""
15
+
16
+ # ------------------------------------------------------------------
17
+ # make_payment → POST /make_payment
18
+ # ------------------------------------------------------------------
19
+
20
+ def make_payment(
21
+ self,
22
+ *,
23
+ reference: str,
24
+ nickname: str,
25
+ transaction_id: str,
26
+ trans_type: str,
27
+ customer_number: str,
28
+ nw: str,
29
+ amount: float,
30
+ payment_option: str,
31
+ callback_url: str,
32
+ currency_code: str,
33
+ landing_page: Optional[str] = None,
34
+ currency_val: float = 1,
35
+ ) -> Dict[str, str]:
36
+ """Initiate a payment transaction.
37
+
38
+ :returns: The raw API response dict
39
+ (``response_code`` / ``response_message``).
40
+ """
41
+ if nw not in _NETWORKS:
42
+ raise ValidationError(f"nw must be one of {sorted(_NETWORKS)}, got '{nw}'.")
43
+ if payment_option not in _PAYMENT_OPTIONS:
44
+ raise ValidationError(
45
+ f"payment_option must be one of {sorted(_PAYMENT_OPTIONS)}, "
46
+ f"got '{payment_option}'."
47
+ )
48
+ if amount <= 0:
49
+ raise ValidationError("amount must be greater than zero.")
50
+
51
+ extra: Dict[str, Any] = dict(
52
+ reference=reference,
53
+ nickname=nickname,
54
+ transaction_id=transaction_id,
55
+ trans_type=trans_type,
56
+ customer_number=customer_number,
57
+ nw=nw,
58
+ amount=amount,
59
+ payment_option=payment_option,
60
+ callback_url=callback_url,
61
+ currency_code=currency_code,
62
+ currency_val=currency_val,
63
+ request_time=self._request_time(),
64
+ )
65
+ if landing_page:
66
+ extra["landing_page"] = landing_page
67
+
68
+ payload = self._build_payload(**extra)
69
+ return self._post("make_payment", payload)
70
+
71
+ # ------------------------------------------------------------------
72
+ # check_transaction_status → POST /check_transaction_status
73
+ # ------------------------------------------------------------------
74
+
75
+ def check_transaction_status(self, transaction_id: str) -> Dict[str, Any]:
76
+ """Check the status of a payment transaction.
77
+
78
+ :returns: The raw API response dict
79
+ (``response_data`` contains ``transaction_id``,
80
+ ``status_desc``, ``status``).
81
+ """
82
+ if not transaction_id:
83
+ raise ValidationError("transaction_id must not be empty.")
84
+
85
+ payload = self._build_payload(transaction_id=transaction_id)
86
+ return self._post("check_transaction_status", payload)
87
+
88
+ # ------------------------------------------------------------------
89
+ # payment_account_lookup → POST /payment_account_lookup
90
+ # ------------------------------------------------------------------
91
+
92
+ def payment_account_lookup(
93
+ self,
94
+ pan: Optional[str] = None,
95
+ nw: Optional[str] = None,
96
+ accounts: Optional[List[Dict[str, str]]] = None,
97
+ ) -> Dict[str, Any]:
98
+ """Look up payment account details (single or multiple).
99
+
100
+ Pass ``pan`` + ``nw`` for a single lookup, or ``accounts`` (a list
101
+ of ``{"pan": …, "nw": …}`` dicts) for a batch lookup.
102
+
103
+ :returns: The raw API response dict.
104
+ """
105
+ if accounts is not None:
106
+ return self._lookup_multiple(accounts)
107
+
108
+ if not pan or not nw:
109
+ raise ValidationError(
110
+ "Provide pan + nw for a single lookup, or accounts for a "
111
+ "batch lookup."
112
+ )
113
+ if nw not in _NETWORKS:
114
+ raise ValidationError(f"nw must be one of {sorted(_NETWORKS)}, got '{nw}'.")
115
+ return self._lookup_single(pan, nw)
116
+
117
+ # -- private helpers ----------------------------------------------
118
+
119
+ def _lookup_single(self, pan: str, nw: str) -> Dict[str, Any]:
120
+ payload = self._build_payload(pan=pan, nw=nw)
121
+ return self._post("payment_account_lookup", payload)
122
+
123
+ def _lookup_multiple(self, accounts: List[Dict[str, str]]) -> Dict[str, Any]:
124
+ if not accounts:
125
+ raise ValidationError("accounts list must not be empty.")
126
+
127
+ for idx, acct in enumerate(accounts):
128
+ if not acct.get("pan") or not acct.get("nw"):
129
+ raise ValidationError(
130
+ f"accounts[{idx}] must contain both 'pan' and 'nw'."
131
+ )
132
+ if acct["nw"] not in _NETWORKS:
133
+ raise ValidationError(
134
+ f"accounts[{idx}].nw must be one of "
135
+ f"{sorted(_NETWORKS)}, got '{acct['nw']}'."
136
+ )
137
+
138
+ payload = self._build_payload(accounts=accounts)
139
+ return self._post("payment_account_lookup", payload)
@@ -0,0 +1,68 @@
1
+ """SMS resource — send messages and check balance."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from typing import Dict, List, Union
6
+
7
+ from ..client import BaseClient
8
+ from ..exceptions import ValidationError
9
+
10
+
11
+ class SMSClient(BaseClient):
12
+ """Client for Mobiska SMS endpoints."""
13
+
14
+ # ------------------------------------------------------------------
15
+ # send_sms → POST /send_sms
16
+ # ------------------------------------------------------------------
17
+
18
+ def send_sms(
19
+ self,
20
+ phone: Union[str, List[str]],
21
+ message: str,
22
+ ) -> Dict[str, str]:
23
+ """Send an SMS to one or many recipients.
24
+
25
+ :param phone: A single phone number or a list of phone numbers.
26
+ :param message: The body of the SMS message.
27
+ :returns: The raw API response dict
28
+ (``response_code`` / ``response_message``).
29
+ """
30
+ if not phone:
31
+ raise ValidationError("phone must not be empty.")
32
+ if not message:
33
+ raise ValidationError("message must not be empty.")
34
+
35
+ if not self.config.sender_id:
36
+ raise ValidationError(
37
+ "sender_id is required. Set MOBISKA_SMS_SENDER_ID or pass "
38
+ "sender_id when initializing the client."
39
+ )
40
+
41
+ recipients = [phone] if isinstance(phone, str) else list(phone)
42
+
43
+ payload = self._build_payload(
44
+ sender_id=self.config.sender_id,
45
+ msg_body=message,
46
+ recipient_number=recipients,
47
+ unique_id=str(uuid.uuid4()),
48
+ )
49
+ return self._post("send_sms", payload)
50
+
51
+ # ------------------------------------------------------------------
52
+ # check_balance → POST /check_sms_balance
53
+ # ------------------------------------------------------------------
54
+
55
+ def check_balance(self, date: datetime = None) -> Dict[str, object]:
56
+ """Retrieve the current SMS balance.
57
+
58
+ :param date: Optional datetime used to generate ``request_time``.
59
+ Defaults to the current UTC time.
60
+ :returns: The raw API response dict
61
+ (``response_code`` / ``response_message`` /
62
+ ``response_data`` with ``sms_balance`` and ``service_id``).
63
+ """
64
+ request_time = (
65
+ date.strftime("%Y-%m-%dT%H:%M:%SZ") if date else self._request_time()
66
+ )
67
+ payload = self._build_payload(request_time=request_time)
68
+ return self._post("check_sms_balance", payload)
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: mobiska
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Mobiska REST API
5
+ Author-email: Mobiska <dev@mobiska.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Your Name
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/mobiska/mobiska-py
29
+ Project-URL: Documentation, https://github.com/mobiska/mobiska-py#readme
30
+ Project-URL: Issues, https://github.com/mobiska/mobiska-py/issues
31
+ Keywords: mobiska,sdk,api,client
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: requests>=2.28
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=7.0; extra == "dev"
43
+ Requires-Dist: pytest-cov; extra == "dev"
44
+ Requires-Dist: responses; extra == "dev"
45
+ Requires-Dist: black; extra == "dev"
46
+ Requires-Dist: ruff; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ # Mobiska Python SDK
50
+
51
+ The official Python SDK for the Mobiska REST API — payments and SMS for businesses across Africa.
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install mobiska
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ### Configuration
62
+
63
+ Credentials can be provided via keyword arguments **or** environment variables:
64
+
65
+ ```bash
66
+ MOBISKA_USER_NAME=your_client_key
67
+ MOBISKA_PASSWORD=your_secret_key
68
+ MOBISKA_SERVICE_ID=1
69
+ MOBISKA_SMS_SENDER_ID=YourSender # SMS only
70
+ MOBISKA_API_URL=https://api.mobiska.com # optional, this is the default
71
+ ```
72
+
73
+ ### SMS
74
+
75
+ ```python
76
+ from mobiska import InitSMS
77
+
78
+ # Uses env vars when no arguments are passed
79
+ sms = InitSMS()
80
+
81
+ # Or pass credentials explicitly
82
+ sms = InitSMS(
83
+ username="your_client_key",
84
+ password="your_secret_key",
85
+ service_id=1,
86
+ sender_id="YourSender",
87
+ )
88
+
89
+ # Send a message
90
+ response = sms.send_sms("0540000000", "Welcome to Mobiska!")
91
+ print(response["response_code"])
92
+
93
+ # Send to multiple recipients
94
+ sms.send_sms(["0540000000", "0200000000"], "Batch message")
95
+
96
+ # Check balance
97
+ balance = sms.check_balance()
98
+ print(balance["response_data"]["sms_balance"])
99
+ ```
100
+
101
+ ### Payments
102
+
103
+ ```python
104
+ from mobiska import InitPayment
105
+
106
+ payment = InitPayment(
107
+ username="your_client_key",
108
+ password="your_secret_key",
109
+ service_id=2,
110
+ )
111
+
112
+ # Make a payment
113
+ result = payment.make_payment(
114
+ reference="Order #123",
115
+ nickname="MyShop",
116
+ transaction_id="txn_001",
117
+ trans_type="CTM",
118
+ customer_number="0541840988",
119
+ nw="MTN",
120
+ amount=100,
121
+ payment_option="MOM",
122
+ callback_url="https://example.com/webhook",
123
+ currency_code="GHS",
124
+ )
125
+
126
+ # Check transaction status
127
+ status = payment.check_transaction_status("txn_001")
128
+ print(status["response_data"]["status"])
129
+
130
+ # Account lookup (single)
131
+ info = payment.payment_account_lookup(pan="0541840988", nw="MTN")
132
+ print(info["response_data"]["name"])
133
+
134
+ # Account lookup (batch)
135
+ accounts = payment.payment_account_lookup(accounts=[
136
+ {"pan": "0541840988", "nw": "MTN"},
137
+ {"pan": "0201234567", "nw": "VOD"},
138
+ ])
139
+ ```
140
+
141
+ ### Advanced options
142
+
143
+ Both `InitSMS` and `InitPayment` accept `timeout`, `retry_count`, and `retry_delay`:
144
+
145
+ ```python
146
+ sms = InitSMS(timeout=10, retry_count=3, retry_delay=0.5)
147
+ ```
148
+
149
+ ## Error Handling
150
+
151
+ ```python
152
+ from mobiska import (
153
+ AuthenticationError,
154
+ NotFoundError,
155
+ RateLimitError,
156
+ ValidationError,
157
+ APIError,
158
+ )
159
+
160
+ try:
161
+ payment.make_payment(...)
162
+ except ValidationError as e:
163
+ print(f"Invalid input: {e}")
164
+ except AuthenticationError:
165
+ print("Check your credentials.")
166
+ except RateLimitError:
167
+ print("Slow down — rate limit hit.")
168
+ except APIError as e:
169
+ print(f"Server error ({e.status_code}): {e}")
170
+ ```
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ # Create a virtual environment and install
176
+ python3 -m venv .venv
177
+ source .venv/bin/activate
178
+ pip install -e ".[dev]"
179
+
180
+ # Run tests
181
+ pytest
182
+
183
+ # Format code
184
+ black mobiska tests
185
+
186
+ # Lint
187
+ ruff check mobiska tests
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
193
+
194
+ # Mobiska Python SDK
195
+
196
+ The official Python SDK for the Mobiska REST API.
197
+
198
+ ## Installation
199
+
200
+ ```bash
201
+ pip install mobiska
202
+ ```
203
+
204
+ ## Quick Start
205
+
206
+ ```python
207
+ from mobiska import MobiskaClient
208
+
209
+ client = MobiskaClient(api_key="your-api-key")
210
+
211
+ # GET request
212
+ users = client.get("/users")
213
+
214
+ # POST request
215
+ new_user = client.post("/users", json={"name": "Alice"})
216
+
217
+ # PATCH request
218
+ client.patch(f"/users/{new_user['id']}", json={"name": "Alice Updated"})
219
+
220
+ # DELETE request
221
+ client.delete(f"/users/{new_user['id']}")
222
+ ```
223
+
224
+ ## Error Handling
225
+
226
+ ```python
227
+ from mobiska import MobiskaClient, AuthenticationError, NotFoundError, RateLimitError
228
+
229
+ try:
230
+ client.get("/users/999")
231
+ except AuthenticationError:
232
+ print("Check your API key.")
233
+ except NotFoundError:
234
+ print("Resource not found.")
235
+ except RateLimitError:
236
+ print("Slow down — rate limit hit.")
237
+ ```
238
+
239
+ ## Development
240
+
241
+ ```bash
242
+ # Install with dev dependencies
243
+ pip install -e ".[dev]"
244
+
245
+ # Run tests
246
+ pytest
247
+
248
+ # Format code
249
+ black mobiska tests
250
+
251
+ # Lint
252
+ ruff check mobiska tests
253
+ ```
254
+
255
+ ## License
256
+
257
+ MIT
@@ -0,0 +1,13 @@
1
+ mobiska/__init__.py,sha256=BFHHkfFvKuzto3eFdCflC9W5DK3xe0x7vglquWXTAVw,2408
2
+ mobiska/auth.py,sha256=nD-lN7S1BsbTYzEu-LxE2MJHWg5EwThZzDYeZ1bGon8,1075
3
+ mobiska/client.py,sha256=s-FHhHWnrvvz8RcBmkFze0sU8GGfWw0tp4UYF-i7bBU,4250
4
+ mobiska/config.py,sha256=a17isBrBH6MWDH6yyzghBp4arnP9kMevTcy_oN9sJq8,2228
5
+ mobiska/exceptions/__init__.py,sha256=KHrcKeZE6e2UR19bmgK3Bxtyqa6sZxJD09IN9PDqHZM,829
6
+ mobiska/resources/__init__.py,sha256=VXwQazZx-4Bdob8-59ZU11iRuFHgE581loXGwLT2UCo,145
7
+ mobiska/resources/payment.py,sha256=F-FgM_aTiHbesrjro0AQNjAnqyht5I79t6wPe6gKzMo,5183
8
+ mobiska/resources/sms.py,sha256=99X67DTK071bMHbf2fJMgXa_Q4NRFUZNaYuZY9ypaqM,2481
9
+ mobiska-0.1.0.dist-info/licenses/LICENSE,sha256=v2spsd7N1pKFFh2G8wGP_45iwe5S0DYiJzG4im8Rupc,1066
10
+ mobiska-0.1.0.dist-info/METADATA,sha256=CIAcQkTE_p38p4_fRs_-Ow13Uls5eoHSBG-KCSci0aI,6134
11
+ mobiska-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ mobiska-0.1.0.dist-info/top_level.txt,sha256=47440n5ee_9T0Xgot0DuzYlR-JtLfS01mFs11gEJCPo,8
13
+ mobiska-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mobiska