Merge commit 'aa4fdc10876f29f7496cbac2cdf6fb7ed381db25' as 'utils_v2'

This commit is contained in:
2025-02-09 10:04:06 +05:30
202 changed files with 145058 additions and 0 deletions
View File
View File
@@ -0,0 +1,279 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 9th Sept., 2024
OBJECTIVE:
To be able to send SMSs from Nimbus's API and manage the templates and other things from one place.
REFERENCES:
01. https://github.com/innovativevijay/SmsHitApiSample
02. https://nimbusit.net/appforms/apimanual.php
DOWNLOADS:
N/A
WEB-PORTAL:
01. http://nimbusit.net/
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append("")
sys.path.append("..")
# To make API Calls:
import httpx
# Data models:
from utils_v2.sms.models.sms_message import SentSMSMessageModel
# For debugging:
from icecream import IceCreamDebugger
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class AsyncNimbusSMS:
MESSAGE_TYPE_REGULAR = 0
MESSAGE_TYPE_UNICODE = 1
MESSAGE_TYPE_NOT_FLASH = 0
MESSAGE_TYPE_FLASH = 1
def __init__(
self,
entity_id,
sender_id,
user_id,
api_key,
http_client: httpx.AsyncClient = None,
debug = True,
debug_prefix = "Nimbus SMS | ",
debug_only_errors = True
):
"""
Sets up an instance of the SMS sender through Nimbus IT.
:param entity_id: The entity id as registered with DLT.
:param sender_id: The 6-char code like "HDFCBK", "NSESMS", "ZRODHA" that you see in your SMS inbox.
:param user_id: The 6-digit id that Nimbus has assigned to you.
:param api_key: The API key generated through Nimbus's portal.
:param http_client: An instance of 'httpx' library's AsyncClient. If not given, one will be instantiated
internally. It is recommended that, for multi-auth use cases, you provide a common HTTP client from outside.
:param debug: Whether, or not, you would like to show debugging messages (can be changed on the fly).
:param debug_prefix: The prefix text to show with the debug string.
:param debug_only_errors: Whether you would like to show all debugging messages or just error messages.
"""
# Create the debugging tools:
self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
if not debug: self.__printer.disable()
self.__debug_only_errors = debug_only_errors
# Accept/create an HTTP client to work with:
if http_client: self.__http_client = http_client
else: self.__http_client = httpx.AsyncClient(
limits = httpx.Limits(
max_connections = 100, # ............ Maximum number of connections allowed in the pool.
max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive.
),
timeout = httpx.Timeout(
pool = 120.0, # .... Time to wait for a free connection from the pool.
connect = 2.5, # ... Time to wait for establishing a connection to the server.
write = 10.0, # .... Time to wait for sending data.
read = 2.5 # ....... Time to wait for receiving data.
)
)
# Capture the input config:
self.__entity_id = entity_id
self.__sender_id = sender_id
self.__user_id = user_id,
self.__api_key = api_key
def enable_debug(self):
self.__printer.enable()
def disable_debug(self):
self.__printer.disable()
def debug_only_errors(self):
self.__debug_only_errors = True
def debug_everything(self):
self.__debug_only_errors = False
async def get_balance(self):
"""
Checks the balance in the Nimbus wallet.
:return: The balance (float) if the request was successful, else None.
"""
balance = None
try:
# Call the API:
response = await self.__http_client.get(
url = r"http://nimbusit.net/api/balance",
params = {"user": self.__user_id, "authkey": self.__api_key}
)
# The response of a successful API call looks like "BALANCE:599". We need just the number:
if response.status_code in [200]: balance = float(response.content.decode().split(":")[-1].strip())
except Exception as exception:
self.__printer(exception)
return balance
async def send_sms(
self,
template_id: str,
recipient_number: str,
message: str,
message_type: int = MESSAGE_TYPE_REGULAR,
flash: int = MESSAGE_TYPE_NOT_FLASH
) -> SentSMSMessageModel:
"""
Sends one SMS through Nimbus IT's system. The text of the message must match the template that had been
submitted. A mismatch may cause the message to fail at best, and raise troubles in the real-world with
government bodies at worst. Be careful.
:param template_id: The id of the SMS template as registered on Nimbus's portal.
:param recipient_number: The phone number of the recipient. You can send an array of numbers, too, BUT IT IS
STRONGLY RECOMMENDED TO NOT DO THAT TO AVOID BEING BLOCKED BY DLT.
:param message: The message to send to the recipient. Should match the template that is being sent.
:param message_type: Choose between 'AsyncNimbusSMS.MESSAGE_TYPE_REGULAR' (default) and
'AsyncNimbusSMS.MESSAGE_TYPE_UNICODE' based on the type of characters being sent. Both are class variables.
:param flash: to choose between a regular inbox SMS, or a flash SMS.
:return: The dict of all the details of the message that was sent including whether, or not, it was successfully
sent. Other details depend on the service provider (Nimbus IT in this case).
"""
# Construct the basic structure of the response of this method:
summary = SentSMSMessageModel(
sender = {"senderId": self.__sender_id},
recipient = {"recipientNo": recipient_number},
text = message,
length = len(message),
isFlash = True if flash else False,
metadata = {"templateId": template_id}
)
try:
# Pre-process the recipient's number:
if not isinstance(recipient_number, (list, set, tuple)): recipient_number = [recipient_number]
# Call the API:
response = await self.__http_client.get(
url = r"http://nimbusit.net/api/pushsms",
params = {
"user": self.__user_id,
"authkey": self.__api_key,
"sender": self.__sender_id,
"mobile": ",".join([str(num) for num in recipient_number]),
"text": message,
"entityid": self.__entity_id,
"templateid": template_id,
"type": message_type,
"flash": flash
}
)
# For a successful API call:
if response.status_code == 200:
response_json = response.json()
summary.rawResponse = response_json
summary.success = True if response_json.get("STATUS", "ERROR").lower() in ["ok"] else False
if summary.success: summary.messageId = response_json.get("RESPONSE", {}).get("UID")
else: summary.message = response_json.get("RESPONSE", {}).get("INFO")
# For any other code that indicates some form of failure:
else: summary.rawResponse = response.content.decode()
except Exception as exception:
self.__printer(exception)
return summary
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
import asyncio
async def main():
client = AsyncNimbusSMS(
entity_id = "1701172465456946915",
sender_id = "TCAOFF",
user_id = "210844",
api_key = "92UnZwiiY7zps"
)
response = await client.send_sms(
template_id = "1707172474709021546",
recipient_number = "987039115511",
message = "OTP for The CA Office registration request is 583920. Please enter this to verify your identity and proceed with the registration request. - TCAOFF"
)
print("SMS API RESPONSE:", response)
my_balance = await client.get_balance()
print("REMAINING BALANCE:", my_balance)
asyncio.run(main())
@@ -0,0 +1,150 @@
"""
AUTHOR:
Bhushan C Thakkar
DATE:
Monday, 20th Jan., 2025.
OBJECTIVE:
To give a structure to how Nimbus's API will respond.
REFERENCES:
N/A
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For system-level activities:
import os
import io
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime
from typing import Optional, Literal, Union, Any
# My utils:
from utils_v2.string import regex
from utils_v2.date_time import date_time
# To work with MongoDB:
from bson.objectid import ObjectId
# To work with date and time:
import datetime
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
class NimbusWhatsappSendMessageResponse(BaseModel):
ts: AwareDatetime = Field(
description = "The time (UTC) at which this message was sent by the sender.",
frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
)
success: bool = Field(
description = "Whether, or not, the request was successful.",
frozen = True,
default = False
)
statusCode: str | int = Field(
description = "The response code as defined by Nimbus.",
frozen = True,
alias = "statuscode"
)
statusMessage: str = Field(
description = "A message about any error, or a general acknowledgement message.",
frozen = True,
alias = "msg"
)
requestId: str | int | None = Field(
description = "How Nimbus identifies your message.",
frozen = True,
default = None
)
messageCount: int = Field(
description = "The no. of messages sent.",
frozen = True,
default = 0
)
messageCost: float | int | None = Field(
description = "The cost (in INR) of messages sent.",
frozen = True,
default = None
)
balance: float | None = Field(
description = "The balance remaining in the account.",
frozen = True,
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "allow"
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
View File
@@ -0,0 +1,230 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Wednesday, 4th Dec., 2024
OBJECTIVE:
To be able to send SMSs from Savvy Bulk SMS's API.
REFERENCES:
N/A
DOWNLOADS:
N/A
WEB-PORTAL:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# To make API Calls:
import httpx
# Data models:
from utils_v2.sms.models.sms_message import SentSMSMessageModel
# For debugging:
from icecream import IceCreamDebugger
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class AsyncSavvyBulkSMS:
def __init__(
self,
api_key: str,
partner_id: str,
short_code: str,
http_client: httpx.AsyncClient = None,
debug = True,
debug_prefix = "Savvy SMS | ",
debug_only_errors = True
):
"""
Sets up an instance of an SMS sender that sends messages through Savvy Bulk SMS's API.
:param api_key: The API key received from Savvy Bulk SMS.
:param partner_id: Your id with Savvy Bulk SMS.
:param short_code: Your short code with Savvy Bulk SMS.
:param http_client: An instance of 'httpx' library's AsyncClient. If not given, one will be instantiated
internally. It is recommended that, for multi-auth use cases, you provide a common HTTP client from outside.
:param debug: Whether, or not, you would like to show debugging messages (can be changed on the fly).
:param debug_prefix: The prefix text to show with the debug string.
:param debug_only_errors: Whether you would like to show all debugging messages or just error messages.
"""
# Create the debugging tools:
self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
if not debug: self.__printer.disable()
self.__debug_only_errors = debug_only_errors
# Accept/create an HTTP client to work with:
if http_client: self.__http_client = http_client
else: self.__http_client = httpx.AsyncClient(
limits = httpx.Limits(
max_connections = 100, # ............ Maximum number of connections allowed in the pool.
max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive.
),
timeout = httpx.Timeout(
pool = 120.0, # .... Time to wait for a free connection from the pool.
connect = 2.5, # ... Time to wait for establishing a connection to the server.
write = 10.0, # .... Time to wait for sending data.
read = 2.5 # ....... Time to wait for receiving data.
)
)
# Capture the input config:
self.__api_key = api_key
self.__partner_id = partner_id
self.__short_code = short_code
def enable_debug(self):
self.__printer.enable()
def disable_debug(self):
self.__printer.disable()
def debug_only_errors(self):
self.__debug_only_errors = True
def debug_everything(self):
self.__debug_only_errors = False
async def send_sms(
self,
recipient_number,
message
) -> SentSMSMessageModel:
"""
Send one SMS from Savvy Bulk SMS's API.
:param recipient_number: The mobile no. of the target recipient.
:param message: The message to send to the target recipient.
:return:
"""
# Construct the basic structure of the response of this method:
summary = SentSMSMessageModel(
sender = {
"partnerId": self.__partner_id,
"shortCode": self.__short_code
},
recipient = {
"recipientNo": recipient_number
},
text = message,
length = len(message),
isFlash = False,
metadata = None
)
try:
# Call the API:
api_response = await self.__http_client.get(
url = r"https://sms.savvybulksms.com/api/services/sendsms/",
params = {
"apikey": self.__api_key,
"partnerID": self.__partner_id,
"message": message,
"shortcode": self.__short_code,
"mobile": recipient_number,
}
)
# For a successful API call:
if api_response.status_code in [200]:
api_json = api_response.json()
summary.rawResponse = api_json
first_response = api_json.get("responses", [{}])[0]
first_desc = first_response.get("response-description", "N/A").lower().strip()
summary.success = True if first_desc == "success" else False
if summary.success: summary.messageId = first_response.get("messageid")
else: summary.message = first_response.get("response-description")
# For any other code that indicates some form of failure:
else: summary.rawResponse = api_response.content.decode()
# If something goes wrong along the way:
except Exception as exception:
self.__printer(exception)
# Done here:
return summary
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
import asyncio
async def main():
sender = AsyncSavvyBulkSMS(
api_key = "19250bdd74050f7cec980c71e75f851a",
partner_id = "7462",
short_code = "HTL TV-NET"
)
response = await sender.send_sms(
recipient_number = "254748877373 123",
message = "Test 123"
)
print("SMS API RESPONSE:", response)
asyncio.run(main())
View File
+157
View File
@@ -0,0 +1,157 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 9th Dec., 2024.
OBJECTIVE:
To define how messages will be stored in the database.
REFERENCES:
N/A
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime
from typing import Optional, Literal, Union, Any
# My utils:
from utils_v2.string import regex
from utils_v2.date_time import date_time
# To work with MongoDB:
from bson.objectid import ObjectId
# To work with date and time:
import datetime
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
class SentSMSMessageModel(BaseModel):
ts: AwareDatetime = Field(
description = "the time (utc) at which this message was sent by the sender",
frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
)
sender: dict = Field(
description = "the sender of this message",
frozen = True
)
recipient: dict = Field(
description = "the recipient of this message",
frozen = True
)
text: str = Field(
description = "the actual text that was sent",
frozen = True
)
length: int = Field(
description = "the no. of chars in this message",
frozen = True
)
isFlash: bool = Field(
description = "to know whether this message is a flash message or a regular message",
frozen = True
)
success: bool = Field(
description = "to know whether this message was sent successfully or not",
default = False
)
message: str | None = Field(
description = "a brief message about what happened; useful when something goes wrong",
default = None
)
metadata: dict | None = Field(
description = "any extra data about this message",
frozen = True
)
rawResponse: Any | None = Field(
description = "the raw response from the third-party client",
default = None
)
messageId: int | str | None = Field(
description = "how the client recognizes this message",
frozen = False,
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "allow"
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass