comms-sdk 1.0.0__tar.gz

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,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: comms_sdk
3
+ Version: 1.0.0
4
+ Summary: EgoSMS Python SDK
5
+ Author-email: Pahappa Limited <systems@pahappa.com>
6
+ License-Expression: MIT
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests
9
+
10
+ # Usage
11
+
12
+ ```python
13
+ # install the package
14
+ pip install egosms_sdk
15
+ # Or for dev "pip install ."
16
+
17
+
18
+ # import in your project
19
+ from egosms_sdk.v1 import EgoSmsSDK, MessagePriority
20
+
21
+ # use
22
+ EgoSmsSDK.authenticate("username", "password")
23
+ EgoSmsSDK.send_sms("0712345678", "Message to send")
24
+ # send_sms(self, numbers: List[str] | str, message: str, sender_id: Optional[str] = None, priority: MessagePriority = MessagePriority.HIGHEST)
25
+ ```
@@ -0,0 +1,16 @@
1
+ # Usage
2
+
3
+ ```python
4
+ # install the package
5
+ pip install egosms_sdk
6
+ # Or for dev "pip install ."
7
+
8
+
9
+ # import in your project
10
+ from egosms_sdk.v1 import EgoSmsSDK, MessagePriority
11
+
12
+ # use
13
+ EgoSmsSDK.authenticate("username", "password")
14
+ EgoSmsSDK.send_sms("0712345678", "Message to send")
15
+ # send_sms(self, numbers: List[str] | str, message: str, sender_id: Optional[str] = None, priority: MessagePriority = MessagePriority.HIGHEST)
16
+ ```
@@ -0,0 +1,12 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools"]
4
+
5
+ [project]
6
+ name = "comms_sdk"
7
+ version = "1.0.0"
8
+ authors = [{name = "Pahappa Limited", email = "systems@pahappa.com"}]
9
+ description = "EgoSMS Python SDK"
10
+ license = "MIT"
11
+ dependencies = ["requests"]
12
+ readme = "README.md"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """
2
+ Pahappa Comms platform Python SDK
3
+
4
+ A Python SDK for integrating with the Comms platform API.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __author__ = "Pahappa Limited"
9
+ __email__ = "systems@pahappa.com"
10
+
11
+ from .v1 import CommsSDK, MessagePriority
12
+
13
+ __all__ = ['CommsSDK', 'MessagePriority']
@@ -0,0 +1,3 @@
1
+ from .comms_sdk import CommsSDK, MessagePriority
2
+
3
+ __all__ = ['CommsSDK', 'MessagePriority']
@@ -0,0 +1,139 @@
1
+ import sys
2
+ from typing import List, Optional
3
+ import requests
4
+ from .models import ApiRequest, ApiResponse, ApiResponseCode, MessageModel, MessagePriority, UserData
5
+ from .utils import NumberValidator, Validator
6
+
7
+ class CommsSDK:
8
+ API_URL = "https://comms.egosms.co/api/v1/json/"
9
+
10
+ def __init__(self):
11
+ self._api_key: Optional[str] = None
12
+ self._user_name: Optional[str] = None
13
+ self._sender_id: str = "EgoSMS"
14
+ self._is_authenticated: bool = False
15
+ self._client = requests.Session()
16
+
17
+ @property
18
+ def api_key(self) -> Optional[str]:
19
+ return self._api_key
20
+
21
+ @property
22
+ def user_name(self) -> Optional[str]:
23
+ return self._user_name
24
+
25
+ @property
26
+ def sender_id(self) -> str:
27
+ return self._sender_id
28
+
29
+ @property
30
+ def is_authenticated(self) -> bool:
31
+ return self._is_authenticated
32
+
33
+ def set_authenticated(self):
34
+ self._is_authenticated = True
35
+
36
+ @classmethod
37
+ def authenticate(cls, user_name: str, api_key: str):
38
+ sdk = cls()
39
+ sdk._user_name = user_name
40
+ sdk._api_key = api_key
41
+ Validator.validate_credentials(sdk)
42
+ return sdk
43
+
44
+ @staticmethod
45
+ def use_sandbox():
46
+ CommsSDK.API_URL = "https://comms-test.pahappa.net/api/v1/json"
47
+
48
+ @staticmethod
49
+ def use_live_server():
50
+ CommsSDK.API_URL = "https://comms.egosms.co/api/v1/json"
51
+
52
+ def with_sender_id(self, sender_id: str):
53
+ self._sender_id = sender_id
54
+ return self
55
+
56
+ def send_sms(self, numbers: str | List[str], message: str, sender_id: Optional[str] = None, priority: MessagePriority = MessagePriority.HIGHEST) -> bool:
57
+ if isinstance(numbers, str):
58
+ numbers = [numbers]
59
+
60
+ api_response = self.query_send_sms(numbers, message, sender_id or self._sender_id, priority)
61
+
62
+ if api_response is None:
63
+ print("Failed to get a response from the server.")
64
+ return False
65
+
66
+ if api_response.Status == ApiResponseCode.OK.value:
67
+ print("SMS sent successfully.")
68
+ print(f"MessageFollowUpUniqueCode: {api_response.MsgFollowUpUniqueCode}")
69
+ return True
70
+ elif api_response.Status == ApiResponseCode.FAILED.value:
71
+ print(f"Failed: {api_response.Message}")
72
+ return False
73
+ else:
74
+ raise RuntimeError(f"Unexpected response status: {api_response.Status}")
75
+
76
+ def query_send_sms(self, numbers: List[str], message: str, sender_id: str, priority: MessagePriority) -> Optional[ApiResponse]:
77
+ if self._sdk_not_authenticated():
78
+ return None
79
+
80
+ if not numbers:
81
+ raise ValueError("Numbers list cannot be empty")
82
+ if not message:
83
+ raise ValueError("Message cannot be empty")
84
+ if len(message) == 1:
85
+ raise ValueError("Message cannot be a single character")
86
+
87
+ if not sender_id or sender_id.strip() == "":
88
+ sender_id = self._sender_id
89
+ if sender_id and len(sender_id) > 11:
90
+ print("Warning: Sender ID length exceeds 11 characters. Some networks may truncate or reject messages.")
91
+
92
+ numbers = NumberValidator.validate_numbers(numbers)
93
+ if not numbers:
94
+ print("No valid phone numbers provided. Please check inputs.", file=sys.stderr)
95
+ return None
96
+
97
+ api_request = ApiRequest(method="SendSms", userdata=UserData(self._user_name, self._api_key))
98
+ message_models = []
99
+ for num in numbers:
100
+ message_model = MessageModel(number=num, message=message, senderid=sender_id, priority=priority.value)
101
+ message_models.append(message_model)
102
+ api_request.msgdata = message_models
103
+
104
+ try:
105
+ res = self._client.post(CommsSDK.API_URL, json=api_request.to_dict())
106
+ return ApiResponse(**res.json())
107
+ except Exception as e:
108
+ print(f"Failed to send SMS: {e}", file=sys.stderr)
109
+ try:
110
+ print(f"Request: {api_request.__dict__}", file=sys.stderr)
111
+ except Exception:
112
+ pass
113
+ return None
114
+
115
+ def _sdk_not_authenticated(self) -> bool:
116
+ if not self._is_authenticated:
117
+ print("SDK is not authenticated. Please authenticate before performing actions.", file=sys.stderr)
118
+ print("Attempting to re-authenticate with provided credentials...", file=sys.stderr)
119
+ return not Validator.validate_credentials(self)
120
+ return False
121
+
122
+ def __str__(self) -> str:
123
+ return f"SDK({self._user_name} => {self._api_key})"
124
+
125
+ def query_balance(self) -> Optional[ApiResponse]:
126
+ if self._sdk_not_authenticated():
127
+ return None
128
+
129
+ api_request = ApiRequest(method="Balance", userdata=UserData(self._user_name, self._api_key))
130
+
131
+ try:
132
+ res = self._client.post(CommsSDK.API_URL, json=api_request.to_dict())
133
+ return ApiResponse(**res.json())
134
+ except Exception as e:
135
+ raise RuntimeError(f"Failed to get balance: {e}") from e
136
+
137
+ def get_balance(self) -> Optional[float]:
138
+ response = self.query_balance()
139
+ return float(response.Balance) if response and response.Balance else None
@@ -0,0 +1,75 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass, asdict
3
+ from typing import List, Optional
4
+ import json
5
+
6
+ class ApiResponseCode(Enum):
7
+ OK = "OK"
8
+ FAILED = "Failed"
9
+
10
+ @classmethod
11
+ def from_json(cls, json_string: str):
12
+ for code in cls:
13
+ if code.value.lower() == json_string.lower():
14
+ return code
15
+ raise ValueError(f"Unknown value: {json_string}")
16
+
17
+ class MessagePriority(Enum):
18
+ HIGHEST = "0"
19
+ HIGH = "1"
20
+ MEDIUM = "2"
21
+ LOW = "3"
22
+ LOWEST = "4"
23
+
24
+ @classmethod
25
+ def from_value(cls, text: str):
26
+ for priority in cls:
27
+ if priority.value == text:
28
+ return priority
29
+ raise ValueError(f"Unknown priority value: {text}")
30
+
31
+ class JSONSerializable:
32
+ def to_dict(self):
33
+ return asdict(self)
34
+
35
+ def to_json(self):
36
+ return json.dumps(self.to_dict())
37
+
38
+ @dataclass
39
+ class UserData(JSONSerializable):
40
+ username: str
41
+ apikey: str
42
+
43
+ def to_dict(self):
44
+ return {
45
+ "username": self.username,
46
+ "password": self.apikey # Maps to "password" in JSON
47
+ }
48
+
49
+ @dataclass
50
+ class MessageModel(JSONSerializable):
51
+ number: str
52
+ message: str
53
+ senderid: str
54
+ priority: MessagePriority
55
+
56
+ @dataclass
57
+ class ApiRequest(JSONSerializable):
58
+ method: str
59
+ userdata: UserData
60
+ msgdata: Optional[List[MessageModel]] = None
61
+ def to_dict(self):
62
+ return {
63
+ "method": self.method,
64
+ "userdata": self.userdata.to_dict(),
65
+ "msgdata": [msg.to_dict() for msg in self.msgdata] if self.msgdata else None
66
+ }
67
+
68
+ @dataclass
69
+ class ApiResponse(JSONSerializable):
70
+ Status: str
71
+ Message: Optional[str] = None
72
+ Cost: Optional[int] = None
73
+ Currency: Optional[str] = None
74
+ MsgFollowUpUniqueCode: Optional[str] = None
75
+ Balance: Optional[str] = None
@@ -0,0 +1,80 @@
1
+ import re
2
+ from typing import List, Set
3
+
4
+ from .models import ApiRequest, ApiResponse, ApiResponseCode, UserData
5
+ import requests
6
+ import sys
7
+
8
+ class NumberValidator:
9
+ _regex = r"^\+?(0|\d{3})\d{9}$"
10
+
11
+ @staticmethod
12
+ def validate_numbers(numbers: List[str]) -> List[str]:
13
+ if not numbers:
14
+ print("Number list cannot be null or empty", file=sys.stderr)
15
+ return []
16
+
17
+ cleansed_numbers: Set[str] = set()
18
+ for number in numbers:
19
+ if not number or not number.strip():
20
+ print(f"Number ({number}) cannot be null or empty!", file=sys.stderr)
21
+ continue
22
+
23
+ number = number.strip().replace("-", "").replace(" ", "")
24
+ if re.match(NumberValidator._regex, number):
25
+ if number.startswith("0"):
26
+ number = "256" + number[1:]
27
+ elif number.startswith("+"):
28
+ number = number[1:]
29
+ cleansed_numbers.add(number)
30
+ else:
31
+ print(f"Number ({number}) is not valid!", file=sys.stderr)
32
+ return list(cleansed_numbers)
33
+
34
+ class Validator:
35
+ @staticmethod
36
+ def validate_credentials(sdk) -> bool:
37
+ if sdk is None:
38
+ raise ValueError("CommsSDK instance cannot be null")
39
+
40
+ if sdk.api_key is None and sdk.user_name is None:
41
+ raise ValueError("API Key and Username must be provided")
42
+
43
+
44
+ if not Validator._is_valid_credential(sdk):
45
+ print(" _ \n" +
46
+ " /\\ _|_ |_ _ ._ _|_ o _ _. _|_ o _ ._ |_ _. o | _ _| | | \n" +
47
+ " /--\\ |_| |_ | | (/_ | | |_ | (_ (_| |_ | (_) | | | (_| | | (/_ (_| o o \n" +
48
+ " \n" +
49
+ "\n")
50
+ return False
51
+
52
+ print("Validated using an api key")
53
+ sdk.set_authenticated()
54
+ return True
55
+
56
+ @staticmethod
57
+ def _is_valid_credential(sdk) -> bool:
58
+ client = requests.Session()
59
+ api_request = ApiRequest(method="Balance",userdata=UserData(sdk.user_name, sdk.api_key))
60
+ req = api_request.to_dict()
61
+
62
+ try:
63
+ res = client.post(sdk.API_URL, json=req)
64
+ # res.raise_for_status() # Raise an exception for HTTP errors
65
+
66
+ api_response = ApiResponse(**res.json())
67
+
68
+ if api_response.Status == ApiResponseCode.OK.value:
69
+ print("Credentials validated successfully.")
70
+ return True
71
+ elif api_response.Status == ApiResponseCode.FAILED.value:
72
+ raise Exception(api_response.Message)
73
+ else:
74
+ return False
75
+ except requests.exceptions.RequestException as e:
76
+ print(f"Error validating credentials: {e}", file=sys.stderr)
77
+ return False
78
+ except Exception as e:
79
+ print(f"Error validating credentials: {e}", file=sys.stderr)
80
+ return False
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: comms_sdk
3
+ Version: 1.0.0
4
+ Summary: EgoSMS Python SDK
5
+ Author-email: Pahappa Limited <systems@pahappa.com>
6
+ License-Expression: MIT
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests
9
+
10
+ # Usage
11
+
12
+ ```python
13
+ # install the package
14
+ pip install egosms_sdk
15
+ # Or for dev "pip install ."
16
+
17
+
18
+ # import in your project
19
+ from egosms_sdk.v1 import EgoSmsSDK, MessagePriority
20
+
21
+ # use
22
+ EgoSmsSDK.authenticate("username", "password")
23
+ EgoSmsSDK.send_sms("0712345678", "Message to send")
24
+ # send_sms(self, numbers: List[str] | str, message: str, sender_id: Optional[str] = None, priority: MessagePriority = MessagePriority.HIGHEST)
25
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/comms_sdk/__init__.py
4
+ src/comms_sdk.egg-info/PKG-INFO
5
+ src/comms_sdk.egg-info/SOURCES.txt
6
+ src/comms_sdk.egg-info/dependency_links.txt
7
+ src/comms_sdk.egg-info/requires.txt
8
+ src/comms_sdk.egg-info/top_level.txt
9
+ src/comms_sdk/v1/__init__.py
10
+ src/comms_sdk/v1/comms_sdk.py
11
+ src/comms_sdk/v1/models.py
12
+ src/comms_sdk/v1/utils.py
@@ -0,0 +1 @@
1
+ requests
@@ -0,0 +1 @@
1
+ comms_sdk