From 064495163f9a61f95ea043dc94a3b1a9d5bc8ee6 Mon Sep 17 00:00:00 2001 From: khushal Date: Mon, 16 Dec 2024 16:00:09 +0530 Subject: [PATCH] (20241216) Re-organizing code to MVC-style structuring. --- controllers/api/payment.py | 437 ++++++++++++++++++ controllers/core/payment.py | 411 ++++++++++++++++ models/api/finstitutions/payments/request.py | 159 +++++++ .../behaviour => controllers}/__init__.py | 0 .../{models/behaviour => controllers}/base.py | 4 +- utils_v2/goog/gmail/gmail_client.py | 6 +- utils_v2/goog/models/{data => }/api_call.py | 0 .../goog/models/{data => }/auth_tokens.py | 0 .../safaricom/controllers}/__init__.py | 0 .../m_pesa_express.py | 8 +- .../safaricom/models/{data => }/api_call.py | 0 .../safaricom/models/{data => }/auth.py | 0 .../models/behaviour => rest}/__init__.py | 0 .../data => rest/controllers}/__init__.py | 0 utils_v2/rest/controllers/async_base.py | 350 ++++++++++++++ .../behaviour => rest/models}/__init__.py | 0 utils_v2/rest/models/api_call.py | 134 ++++++ .../behaviour/nimbus => india}/__init__.py | 0 .../nimbus}/__init__.py | 0 .../nimbus/controllers}/__init__.py | 0 .../nimbus/controllers}/async_nimbus.py | 4 +- .../behaviour => sms/kenya}/__init__.py | 0 .../kenya/savvy_bulk_sms}/__init__.py | 0 .../savvy_bulk_sms/controllers/__init__.py | 0 .../controllers}/async_savvy_bulk_sms.py | 2 +- utils_v2/sms/models/{data => }/sms_message.py | 0 utils_v2/telegram/controllers/__init__.py | 0 .../async_bot.py} | 0 .../telegram/models/{data => }/api_call.py | 0 utils_v2/telegram/models/{data => }/update.py | 0 30 files changed, 1503 insertions(+), 12 deletions(-) create mode 100644 controllers/api/payment.py create mode 100644 controllers/core/payment.py create mode 100644 models/api/finstitutions/payments/request.py rename utils_v2/goog/{models/behaviour => controllers}/__init__.py (100%) rename utils_v2/goog/{models/behaviour => controllers}/base.py (99%) rename utils_v2/goog/models/{data => }/api_call.py (100%) rename utils_v2/goog/models/{data => }/auth_tokens.py (100%) rename utils_v2/{goog/models/data => payments/safaricom/controllers}/__init__.py (100%) rename utils_v2/payments/safaricom/{models/behaviour => controllers}/m_pesa_express.py (98%) rename utils_v2/payments/safaricom/models/{data => }/api_call.py (100%) rename utils_v2/payments/safaricom/models/{data => }/auth.py (100%) rename utils_v2/{payments/safaricom/models/behaviour => rest}/__init__.py (100%) rename utils_v2/{payments/safaricom/models/data => rest/controllers}/__init__.py (100%) create mode 100644 utils_v2/rest/controllers/async_base.py rename utils_v2/{sms/models/behaviour => rest/models}/__init__.py (100%) create mode 100644 utils_v2/rest/models/api_call.py rename utils_v2/sms/{models/behaviour/nimbus => india}/__init__.py (100%) rename utils_v2/sms/{models/behaviour/savvy_bulk_sms => india/nimbus}/__init__.py (100%) rename utils_v2/sms/{models/data => india/nimbus/controllers}/__init__.py (100%) rename utils_v2/sms/{models/behaviour/nimbus => india/nimbus/controllers}/async_nimbus.py (99%) rename utils_v2/{telegram/models/behaviour => sms/kenya}/__init__.py (100%) rename utils_v2/{telegram/models/data => sms/kenya/savvy_bulk_sms}/__init__.py (100%) create mode 100644 utils_v2/sms/kenya/savvy_bulk_sms/controllers/__init__.py rename utils_v2/sms/{models/behaviour/savvy_bulk_sms => kenya/savvy_bulk_sms/controllers}/async_savvy_bulk_sms.py (99%) rename utils_v2/sms/models/{data => }/sms_message.py (100%) create mode 100644 utils_v2/telegram/controllers/__init__.py rename utils_v2/telegram/{models/behaviour/telegram_bot.py => controllers/async_bot.py} (100%) rename utils_v2/telegram/models/{data => }/api_call.py (100%) rename utils_v2/telegram/models/{data => }/update.py (100%) diff --git a/controllers/api/payment.py b/controllers/api/payment.py new file mode 100644 index 0000000..e225858 --- /dev/null +++ b/controllers/api/payment.py @@ -0,0 +1,437 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 13th Dec., 2024 + + OBJECTIVE: + + To handle all SMS related behaviour from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" +import asyncio +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For Quart: +from quart import current_app + +# My async utils: +from utils_v2.string import json +from utils_v2.date_time import date_time +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage + +# Base model: +from controllers.base import BaseModel + +# Data models: +from models.core.user import CoreUserInfoModel +from models.core.auth_token import CoreAuthTokenModel +from models.core.message import CoreMessageModel +from models.api.sms.send import ( + SMSSendRequestData, + NimbusSMSIndiaMessage, + SavvyBulkSMSKenyaMessage, + SMSSendManyResults +) + +# SMS Clients: +from utils_v2.sms.models.behaviour.nimbus.async_nimbus import AsyncNimbusSMS +from utils_v2.sms.models.behaviour.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS +from utils_v2.sms.models.data.sms_message import SentSMSMessageModel + +# To work with MongoDB: +from bson import ObjectId +from pymongo import InsertOne + +# To work with datatypes: +from typing import Literal, List, Dict, Any + +# To make API calls: +import httpx + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSController: + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + pass + + # ┓┏ ┓ + # ┣┫┏┓┃┏┓┏┓┏┓┏ + # ┛┗┗ ┗┣┛┗ ┛ ┛ + # ┛ + + pass + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + @staticmethod + async def set_token_direct( + db_conn: AsyncMySQL, + mongo_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + session_token: str = None + ) -> bool: + + # Start by assuming failure: + success = False + + # Get a token id: + token_key = await current_app.core_auth_token_controller.get_token_key( + db_conn = db_conn, + mongo_conn = mongo_conn, + auth_token = auth_token, + token_notes = {}, + session_token = session_token + ) + + # Immediately save the details against that token id: + success = await current_app.core_auth_token_controller.set_token( + db_conn = db_conn, + mongo_conn = mongo_conn, + token_key = token_key, + auth_token = auth_token, + token_notes = {}, + session_token = session_token + ) + + # Done here: + return success + + @staticmethod + async def get_token( + mongo_conn: AsyncMongo, + token_key: ObjectId | str = None, + ) -> CoreAuthTokenModel | None: + + # Simply call the core model: + return await current_app.core_auth_token_controller.get_token( + mongo_conn = mongo_conn, + token_key = token_key + ) + + # ┏┓ ┓ + # ┗┓┏┓┏┓┏┫ + # ┗┛┗ ┛┗┗┻ + + @staticmethod + async def __send_from_nimbus_sms_india( + http_client: httpx.AsyncClient, + auth_token: CoreAuthTokenModel, + messages: List[NimbusSMSIndiaMessage], + ) -> SMSSendManyResults: + + # Start with a blank variable: + send_results = SMSSendManyResults() + + # Initialize the third-party client: + client = AsyncNimbusSMS( + entity_id = auth_token.auth["entityId"], + sender_id = auth_token.auth["senderId"], + user_id = auth_token.auth["userId"], + api_key = auth_token.auth["apiKey"], + http_client = http_client + ) + + # Iterate over all the messages you need to send: + for message in messages: + + # Send the SMS and return the response: + client_response = await client.send_sms( + recipient_number = message.recipientNo, + message = message.text, + template_id = message.templateId + ) + + # Note down the results: + send_results.totalCount += 1 + if client_response.success: send_results.successCount += 1 + else: send_results.failureCount += 1 + send_results.smsMessages.append(CoreMessageModel( + ts = client_response.ts, + syncTs = date_time.get_current_utc_date_time(as_string = False), + tokenId = auth_token.authTokenId, + serviceType = auth_token.serviceType, + client = auth_token.client, + clientMessageId = client_response.messageId, + clientThreadId = message.recipientNo, + isSent = True, + isBroadcast = False, + sentSuccessfully = client_response.success, + sender = None, + recipient = message.recipientNo, + chat = None, + message = client_response.model_dump(), + snippet = message.text, + aiSnippet = None, + tags = ["sms", "nimbusSmsIndia"] + )) + + # Done here: + return send_results + + @staticmethod + async def __send_from_savvy_bulk_sms_kenya( + http_client: httpx.AsyncClient, + auth_token: CoreAuthTokenModel, + messages: List[SavvyBulkSMSKenyaMessage], + ) -> SMSSendManyResults: + + # Start with a blank variable: + send_results = SMSSendManyResults() + + # Initialize the third-party client: + client = AsyncSavvyBulkSMS( + partner_id = auth_token.auth["partnerId"], + short_code = auth_token.auth["shortCode"], + api_key = auth_token.auth["apiKey"], + http_client = http_client + ) + + # Iterate over all the messages you need to send: + for message in messages: + + # Send the SMS and return the response: + client_response = await client.send_sms( + recipient_number = message.recipientNo, + message = message.text + ) + + # Note down the results: + send_results.totalCount += 1 + if client_response.success: send_results.successCount += 1 + else: send_results.failureCount += 1 + send_results.smsMessages.append(CoreMessageModel( + ts = client_response.ts, + syncTs = date_time.get_current_utc_date_time(as_string = False), + tokenId = auth_token.authTokenId, + serviceType = auth_token.serviceType, + client = auth_token.client, + clientMessageId = client_response.messageId, + clientThreadId = message.recipientNo, + isSent = True, + isBroadcast = False, + sentSuccessfully = client_response.success, + sender = None, + recipient = message.recipientNo, + chat = None, + message = client_response.model_dump(), + snippet = message.text, + aiSnippet = None, + tags = ["sms", "savvyBulkSmsKenya"] + )) + + # Done here: + return send_results + + async def send( + self, + mongo_conn: AsyncMongo, + http_client: httpx.AsyncClient, + # token_id: ObjectId | str, + auth_token: CoreAuthTokenModel, + messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage] + ) -> SMSSendManyResults: + + # Start by assuming failure: + send_results = SMSSendManyResults() + + # # We first load the authorization tokens: + # auth_token = await self.get_token( + # mongo_conn = mongo_conn, + # token_id = token_id, + # ) + # + # # If we failed to load the authorization tokens: + # if not auth_token: + # send_results.message = f"no such token id '{token_id}'" + # return send_results + + # Now we route the message to the appropriate client: + match auth_token.client: + case "nimbusSmsIndia": + send_results = await self.__send_from_nimbus_sms_india( + http_client = http_client, + auth_token = auth_token, + messages = messages + ) + case "savvyBulkSmsKenya": + send_results = await self.__send_from_savvy_bulk_sms_kenya( + http_client = http_client, + auth_token = auth_token, + messages = messages + ) + case _: + send_results.message = f"invalid client {auth_token.client}" + + # Save the results to MongoDB: + tasks = [] + for sms in send_results.smsMessages: + message_json = sms.model_dump() + message_json.pop("_id", None) + tasks.append(current_app.core_message_controller.insert( + mongo_conn = mongo_conn, + message = message_json + )) + results = await asyncio.gather(*tasks) + + # Done here: + send_results.message = f"{send_results.successCount}/{send_results.totalCount} message(s) sent" + return send_results + + # ┓ • ┏┓ ┏┓ ┳┳┓ + # ┃ ┓┏╋ ┣╋ ┃┓┏┓╋ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏ + # ┗┛┗┛┗ ┗┻ ┗┛┗ ┗ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ + # ┛ + + # These are simply for retrieving sms messages. + # You need to already have them saved to the database. + + # @staticmethod + # async def list_messages( + # mongo_conn: AsyncMongo, + # token_ids: List[ObjectId | str], + # limit: int = 100, + # skip: int = 0, + # additional_filter: dict = None + # ) -> List[CoreMessageModel] | None: + # + # # regardless of what additional filter is provided from outside, + # # we add a mail-selecting filter here: + # if additional_filter is None: additional_filter = {} + # additional_filter["serviceType"] = "sms" + # + # # Simply call the core model: + # return await current_app.core_message_controller.get_message( + # mongo_conn = mongo_conn, + # token_ids = token_ids, + # limit = limit, + # skip = skip, + # additional_filter = additional_filter + # ) + # + # @staticmethod + # async def get_one_mail( + # mongo_conn: AsyncMongo, + # token_id: ObjectId | str, + # message_id: ObjectId | str + # ) -> CoreMessageModel | None: + # + # # Simply call the core model: + # return await current_app.core_message_controller.get_message( + # mongo_conn = mongo_conn, + # token_id = token_id, + # message_id = message_id + # ) + + # ┳┳ ┓ + # ┃┃┏┓┏┫┏┓╋┏┓ + # ┗┛┣┛┗┻┗┻┗┗ + # ┛ + + @staticmethod + async def update_tags( + mongo_conn: AsyncMongo, + token_id: ObjectId | str, + message_id: ObjectId | str, + unset_tags: List[str] = None, + set_tags: List[str] = None + ) -> bool: + + # Simply call the core model: + return await current_app.core_message_controller.update_tags( + mongo_conn = mongo_conn, + token_id = token_id, + message_id = message_id, + unset_tags = unset_tags, + set_tags = set_tags + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass + + # from utils_v2.string import json + # + # file_options = [ + # r"/home/developer/Downloads/recursive parts parse - 20241210.json", + # r"/home/developer/Downloads/recursive parts parse (no attachment) - 20241210.json", + # ] + # + # raw_mail_json = json.from_file(file_options[1]) + # print("FROM FILE:", json.to_string(raw_mail_json["payload"])) + # print("\n\n---------\n\n") + # mail_controller = MailController() + # print(json.to_string(mail_controller.drop_attachments(raw_mail_json["payload"]))) diff --git a/controllers/core/payment.py b/controllers/core/payment.py new file mode 100644 index 0000000..cb21c5e --- /dev/null +++ b/controllers/core/payment.py @@ -0,0 +1,411 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 12th Dec., 2024 + + OBJECTIVE: + + To handle all messages from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +from pyexpat.errors import messages + +sys.path.append(".") +sys.path.append("..") + +# For Quart: +from quart import current_app + +# My async utils: +from utils_v2.string import json +from utils_v2.date_time import date_time +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage + +# Base model: +from controllers.base import BaseModel + +# Data models: +from models.core.auth_token import CoreAuthTokenModel +from models.core.message import CoreMessageModel +from models.core.user import CoreUserInfoModel + +# To work with MongoDB: +from bson import ObjectId +from pymongo import InsertOne, UpdateOne, ReplaceOne + +# To work with datatypes: +from typing import Literal, List, Dict, Any + +# To make deep-copies: +import copy + +# To work with base-64 encoding: +import base64 + +# To work with date and time: +import datetime + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class MessageController(BaseModel): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + # For MongoDB: + MESSAGES_COLLECTION = "_messages" + + # ┏┓┳┓┳┳┳┓ ┏┓ + # ┃ ┣┫┃┃┃┃ ━━ ┃ ┏┓┏┓┏┓╋┏┓ + # ┗┛┛┗┗┛┻┛ ┗┛┛ ┗ ┗┻┗┗ + + async def insert( + self, + mongo_conn: AsyncMongo, + message: CoreMessageModel + ) -> ObjectId: + + """ + Simply insert one message document into the database. + :param mongo_conn: The instance of the database connector to use for the operation. + :param message: The message to save into the database. + :return: The object id of the inserted document. + """ + + # Simply insert the document: + return await mongo_conn.insert_one( + collection = self.MESSAGES_COLLECTION, + document = message, + raise_exception = True + ) + + async def bulk_write( + self, + mongo_conn: AsyncMongo, + mongo_operations: list + ) -> int: + + """ + Needed in cases like forcing re-sync of mails where you need to perform actions like bulk replacements of + existing documents. Not recommended to use. Please use very carefully to ensure document integrity. + :param mongo_conn: The instance of the database connector to use for the operation. + :param mongo_operations: The list operations that are supported by MongoDB's Bulk Write system. + :return: The no. of documents affected. + """ + + return await mongo_conn.bulk_write( + collection = self.MESSAGES_COLLECTION, + requests = mongo_operations, + raise_exception = True + ) + + # ┏┓┳┓┳┳┳┓ ┳┓ • + # ┃ ┣┫┃┃┃┃ ━━ ┣┫┏┓╋┏┓┓┏┓┓┏┏┓ + # ┗┛┛┗┗┛┻┛ ┛┗┗ ┗┛ ┗┗ ┗┛┗ + + async def count_messages( + self, + mongo_conn: AsyncMongo, + token_ids: List[ObjectId | str], + additional_filter: dict = None + ) -> int: + + """ + Just counts the no. of messages that match a given set of conditions. + :param mongo_conn: The instance of the database connector to use for the operation. + :param token_ids: The token ids of the accounts from which these messages must be fetched. + :param additional_filter: Any addition filters to use. + :return: The no. of messages that match the given conditions. + """ + + # Prepare the filter: + if not isinstance(token_ids, list): token_ids = [token_ids] + token_ids = [ObjectId(t) for t in token_ids] + filter_json = {"tokenId": {"$in": token_ids}} + if additional_filter: + for k, v in additional_filter.items(): + filter_json[k] = v + + # Get the count of the documents that match the criteria: + count = await mongo_conn.count( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + raise_exception = True + ) + + # Done here: + return count + + async def get_previews( + self, + mongo_conn: AsyncMongo, + token_ids: List[ObjectId | str], + limit: int = 100, + skip: int = 0, + additional_filter: dict = None + ) -> List[CoreMessageModel] | None: + + """ + Fetches many messages in one call, but leaves out the full payloads. + :param mongo_conn: The instance of the database connector to use for the operation. + :param token_ids: The token ids of the accounts from which these messages must be fetched. + :param limit: The max. no. of messages to retrieve in this call. + :param skip: The no. of initial messages to skip. Useful for pagination. + :param additional_filter: Any addition filters to use. + :return: The list of messages (as the message model). This list can be empty. + """ + + # Prepare the filter: + if not isinstance(token_ids, list): token_ids = [token_ids] + token_ids = [ObjectId(t) for t in token_ids] + filter_json = {"tokenId": {"$in": token_ids}} + if additional_filter: + for k, v in additional_filter.items(): + filter_json[k] = v + + # We fetch the messages that are identified by a specific token id, + # with the specified fetching limits, while enforcing the sorting condition: + records = await mongo_conn.find_many( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + limit = limit, + skip = skip, + sort = {"ts": -1}, + projection = { + "_id": True, + "ts": True, + "syncTs": True, + "tokenId": True, + "serviceType": True, + "client": True, + "clientMessageId": True, + "clientThreadId": True, + "isSent": True, + "isBroadcast": True, + "sentSuccessfully": True, + "sender": True, + "chat": True, + "snippet": True, + "aiSnippet": True, + "tags": True + }, + raise_exception = True + ) + + # Convert the fetched records to instances of the data model and return: + for record in records: record["message"] = {} + return [CoreMessageModel(**record) for record in records] + + async def get_messages( + self, + mongo_conn: AsyncMongo, + token_ids: List[ObjectId | str], + limit: int = 100, + skip: int = 0, + additional_filter: dict = None + ) -> List[CoreMessageModel] | None: + + """ + Fetches many full messages in one call. + :param mongo_conn: The instance of the database connector to use for the operation. + :param token_ids: The token ids of the accounts from which these messages must be fetched. + :param limit: The max. no. of messages to retrieve in this call. + :param skip: The no. of initial messages to skip. Useful for pagination. + :param additional_filter: Any addition filters to use. + :return: The list of messages (as the message model). This list can be empty. + """ + + # Prepare the filter: + if not isinstance(token_ids, list): token_ids = [token_ids] + token_ids = [ObjectId(t) for t in token_ids] + filter_json = {"tokenId": {"$in": token_ids}} + if additional_filter: + for k, v in additional_filter.items(): + filter_json[k] = v + + # We fetch the messages that are identified by a specific token id, + # with the specified fetching limits, while enforcing the sorting condition: + records = await mongo_conn.find_many( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + limit = limit, + skip = skip, + sort = {"ts": -1}, + raise_exception = True + ) + + # Convert the fetched records to instances of the data model and return: + return [CoreMessageModel(**record) for record in records] + + async def get_message( + self, + mongo_conn: AsyncMongo, + token_id: ObjectId | str, + message_id: ObjectId | str, + ) -> CoreMessageModel | None: + + """ + Gets one message if you know its message id. + :param mongo_conn: The instance of the database connector to use for the operation. + :param token_id: The id of the auth-token associated with the message. Needed for security. + :param message_id: The id of the message that needs to be read. + :return: The contents of that one message in a structured format. + """ + + # We fetch the whole payload of that one message: + record = await mongo_conn.find_one( + collection = self.MESSAGES_COLLECTION, + filter = { + "_id": ObjectId(message_id), + "tokenId": ObjectId(token_id) + }, + raise_exception = True + ) + + # If no such message was found: + if record is None: return None + + # If a record was found, + # we return it as our data model: + return CoreMessageModel(**record) + + # ┏┓┳┓┳┳┳┓ ┳┳ ┓ + # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓ + # ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗ + # ┛ + + # We don't support updating messages themselves, + # but we will allow updating fields like tags, marking as read or unread, etc. + + async def update_tags( + self, + mongo_conn: AsyncMongo, + token_id: ObjectId | str, + message_id: ObjectId | str, + unset_tags: List[str] = None, + set_tags: List[str] = None + ) -> bool: + + """ + Updates the tags on one message. The tags to remove are processed first, the ones to add are processed later. + :param mongo_conn: The instance of the database connector to use for the operation. + :param token_id: The id of the auth-token associated with the message. Needed for security. + :param message_id: The id of the message that needs to be read. + :param unset_tags: The tags to remove from the message. + :param set_tags: The tags to add to the message. + :return: True if the update was successful, else False. + """ + + # Update the tags: + return await mongo_conn.update_one( + collection = self.MESSAGES_COLLECTION, + filter = { + "_id": ObjectId(message_id), + "tokenId": ObjectId(token_id) + }, + update = [{ + "$set": { + "tags": { + "$let": { + "vars": { + "removed_tags": { + "$setDifference": [ + "$tags", + unset_tags + ] + } + }, + "in": { + "$setUnion": [ + "$$removed_tags", + set_tags + ] + } + } + } + } + }], + raise_exception = True + ) + + # ┏┓┳┓┳┳┳┓ ┳┓ ┓ + # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┃┏┓╋┏┓ + # ┗┛┛┗┗┛┻┛ ┻┛┗ ┗┗ ┗┗ + + # No support whatsoever for deleting messages. + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/finstitutions/payments/request.py b/models/api/finstitutions/payments/request.py new file mode 100644 index 0000000..bd6c515 --- /dev/null +++ b/models/api/finstitutions/payments/request.py @@ -0,0 +1,159 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 13th Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive auth details of various software. + + 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 +from typing import Optional, Literal, Union + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class SafaricomMPesaExpressAuth(BaseModel): + + consumerKey: str = Field( + description = "the app's consumer key given by safaricom; found in 'my apps'", + frozen = True + ) + + consumerSecret: str = Field( + description = "the app's consumer secret given by safaricom; found in 'my apps'", + frozen = True + ) + + businessShortCode: str = Field( + description = "your app's business short code; found in 'my apps'", + frozen = True + ) + + appPasskey: str = Field( + description = "your app's passkey; taken from human representative", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class PGAuthRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class PGAuthRequestData(BaseModel): + + client: Literal["safaricomMPesaExpress"] = Field(alias = "client") + auth: Union[SafaricomMPesaExpressAuth] + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/utils_v2/goog/models/behaviour/__init__.py b/utils_v2/goog/controllers/__init__.py similarity index 100% rename from utils_v2/goog/models/behaviour/__init__.py rename to utils_v2/goog/controllers/__init__.py diff --git a/utils_v2/goog/models/behaviour/base.py b/utils_v2/goog/controllers/base.py similarity index 99% rename from utils_v2/goog/models/behaviour/base.py rename to utils_v2/goog/controllers/base.py index 5651316..d5f51b4 100644 --- a/utils_v2/goog/models/behaviour/base.py +++ b/utils_v2/goog/controllers/base.py @@ -40,8 +40,8 @@ import io # My utils: from utils_v2.date_time import date_time -from utils_v2.goog.models.data.api_call import GoogleApiResponse -from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens +from utils_v2.goog.models.api_call import GoogleApiResponse +from utils_v2.goog.models.auth_tokens import GoogleAuthTokens # Related to Google: from google_auth_oauthlib.flow import InstalledAppFlow diff --git a/utils_v2/goog/gmail/gmail_client.py b/utils_v2/goog/gmail/gmail_client.py index 600ea8f..63e61d7 100644 --- a/utils_v2/goog/gmail/gmail_client.py +++ b/utils_v2/goog/gmail/gmail_client.py @@ -48,9 +48,9 @@ from utils_v2.date_time import date_time from utils_v2.mail import mail_parser # My Google utils: -from utils_v2.goog.models.behaviour.base import AsyncGoogleBase -from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens -from utils_v2.goog.models.data.api_call import GoogleApiResponse +from utils_v2.goog.controllers.base import AsyncGoogleBase +from utils_v2.goog.models.auth_tokens import GoogleAuthTokens +from utils_v2.goog.models.api_call import GoogleApiResponse from utils_v2.goog.gmail.gmail_message import GMailMessage # Related to Google: diff --git a/utils_v2/goog/models/data/api_call.py b/utils_v2/goog/models/api_call.py similarity index 100% rename from utils_v2/goog/models/data/api_call.py rename to utils_v2/goog/models/api_call.py diff --git a/utils_v2/goog/models/data/auth_tokens.py b/utils_v2/goog/models/auth_tokens.py similarity index 100% rename from utils_v2/goog/models/data/auth_tokens.py rename to utils_v2/goog/models/auth_tokens.py diff --git a/utils_v2/goog/models/data/__init__.py b/utils_v2/payments/safaricom/controllers/__init__.py similarity index 100% rename from utils_v2/goog/models/data/__init__.py rename to utils_v2/payments/safaricom/controllers/__init__.py diff --git a/utils_v2/payments/safaricom/models/behaviour/m_pesa_express.py b/utils_v2/payments/safaricom/controllers/m_pesa_express.py similarity index 98% rename from utils_v2/payments/safaricom/models/behaviour/m_pesa_express.py rename to utils_v2/payments/safaricom/controllers/m_pesa_express.py index d23cec7..1839cd0 100644 --- a/utils_v2/payments/safaricom/models/behaviour/m_pesa_express.py +++ b/utils_v2/payments/safaricom/controllers/m_pesa_express.py @@ -44,8 +44,8 @@ from utils_v2.string import json from utils_v2.date_time import date_time # Data models: -from utils_v2.payments.safaricom.models.data.auth import MPesaExpressAuthorization -from utils_v2.payments.safaricom.models.data.api_call import MPesaExpressApiResponse +from utils_v2.payments.safaricom.models.auth import MPesaExpressAuthorization +from utils_v2.payments.safaricom.models.api_call import MPesaExpressApiResponse # To make REST-ful requests: import httpx @@ -101,7 +101,7 @@ import inspect # ***************************************************************************************************************** -class MPesaExpress: +class SafaricomMPesaExpress: def __init__( self, @@ -380,7 +380,7 @@ if __name__ == "__main__": callbackUrl = None ) - my_m_pesa = MPesaExpress( + my_m_pesa = SafaricomMPesaExpress( auth = m_pesa_auth ) diff --git a/utils_v2/payments/safaricom/models/data/api_call.py b/utils_v2/payments/safaricom/models/api_call.py similarity index 100% rename from utils_v2/payments/safaricom/models/data/api_call.py rename to utils_v2/payments/safaricom/models/api_call.py diff --git a/utils_v2/payments/safaricom/models/data/auth.py b/utils_v2/payments/safaricom/models/auth.py similarity index 100% rename from utils_v2/payments/safaricom/models/data/auth.py rename to utils_v2/payments/safaricom/models/auth.py diff --git a/utils_v2/payments/safaricom/models/behaviour/__init__.py b/utils_v2/rest/__init__.py similarity index 100% rename from utils_v2/payments/safaricom/models/behaviour/__init__.py rename to utils_v2/rest/__init__.py diff --git a/utils_v2/payments/safaricom/models/data/__init__.py b/utils_v2/rest/controllers/__init__.py similarity index 100% rename from utils_v2/payments/safaricom/models/data/__init__.py rename to utils_v2/rest/controllers/__init__.py diff --git a/utils_v2/rest/controllers/async_base.py b/utils_v2/rest/controllers/async_base.py new file mode 100644 index 0000000..560c203 --- /dev/null +++ b/utils_v2/rest/controllers/async_base.py @@ -0,0 +1,350 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 16th Dec., 2024 + + OBJECTIVE: + + To provide a class to make REST-ful API calls and have a structured approach for the inputs and outputs. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# System-level activities: +import io + +# The base model: +from utils_v2.rest.models.api_call import ApiResponse + +# My utils: +from utils_v2.date_time import date_time + +# To make API calls: +import httpx + +# To work with date and time: +import datetime + +# For working with datatypes: +from typing import Literal, List + +# For debugging: +from icecream import IceCreamDebugger +import inspect + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class AsyncRestBase: + + def __init__( + self, + http_client: httpx.AsyncClient = None, + debug = True, + debug_prefix = "GMail | ", + debug_only_errors = True + ): + + """ + To initialize the base class. + :param http_client: An asynchronous HTTP client to make API calls. + :param debug: Whether, or not, you would like to show debugging messages on the terminal. + :param debug_prefix: The prefix string to identify the debugging messages. + :param debug_only_errors: Whether you would like to show all debugging messages or just error messages. + """ + + # Prepare the debugging utility: + self._debug_prefix = debug_prefix + 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 = 5.0, # ... Time to wait for establishing a connection to the server. + write = 10.0, # .... Time to wait for sending data. + read = 120.0 # ..... Time to wait for receiving data. + ) + ) + + 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( + self, + url: str, + headers: dict = None, + params: dict = None + ) -> ApiResponse: + + """ + To call an API using the GET method. + :param url: The URL to call. + :param headers: The headers to pass. + :param params: The params to send in the query string itself. + :return: A structured response that includes the raw response, the exception (if any), and so on. + """ + + # Prepare the structure of the response: + api_response = ApiResponse( + action = inspect.stack()[1].function, + url = url, + method = "GET" + ) + + try: + + # Make the API call: + response = await self._http_client.get( + url = url, + headers = headers, + params = params + ) + + # Note down the results: + api_response.response = response + api_response.httpCode = response.status_code + api_response.message = response.reason_phrase + + # If something goes wrong: + except Exception as exception: + api_response.exception = exception + api_response.message = str(exception) + self._printer(exception, api_response.url, api_response.method, headers, params) + + # Done here: + return api_response + + async def post( + self, + url: str, + headers: dict = None, + json: dict = None, + data: dict = None, + content: str | bytes = None + ) -> ApiResponse: + + """ + To call an API using the POST method. + :param url: The URL to call. + :param headers: The headers to pass. + :param json: The params to send in the JSON body. + :param data: The params to send in the form-data in the body. + :param content: The raw content to be sent in the body (typically as an octet-stream). + :return: A structured response that includes the raw response, the exception (if any), and so on. + """ + + # Prepare the structure of the response: + api_response = ApiResponse( + action = inspect.stack()[1].function, + url = url, + method = "POST" + ) + + try: + + # Make the API call: + response = await self._http_client.post( + url = url, + headers = headers, + json = json, + data = data, + content = content + ) + + # Note down the results: + api_response.response = response + api_response.httpCode = response.status_code + api_response.message = response.reason_phrase + + # If something goes wrong: + except Exception as exception: + api_response.exception = exception + api_response.message = str(exception) + self._printer(exception, api_response.url, api_response.method, headers, json, data) + + # Done here: + return api_response + + async def put( + self, + url: str, + headers: dict = None, + json: dict = None, + data: dict = None + ) -> ApiResponse: + + """ + To call an API using the PUT method. + :param url: The URL to call. + :param headers: The headers to pass. + :param json: The params to send in the JSON body. + :param data: The params to send in the form-data in the body. + :return: A structured response that includes the raw response, the exception (if any), and so on. + """ + + # Prepare the structure of the response: + api_response = ApiResponse( + action = inspect.stack()[1].function, + url = url, + method = "PUT" + ) + + try: + + # Make the API call: + response = await self._http_client.put( + url = url, + headers = headers, + json = json, + data = data + ) + + # Note down the results: + api_response.response = response + api_response.httpCode = response.status_code + api_response.message = response.reason_phrase + + # If something goes wrong: + except Exception as exception: + api_response.exception = exception + api_response.message = str(exception) + self._printer(exception, api_response.url, api_response.method, headers, json, data) + + # Done here: + return api_response + + async def delete( + self, + url: str, + headers: dict = None + ) -> ApiResponse: + + """ + To call an API using the DELETE method. + :param url: The URL to call. + :param headers: The headers to pass. + :return: A structured response that includes the raw response, the exception (if any), and so on. + """ + + # Prepare the structure of the response: + api_response = ApiResponse( + action = inspect.stack()[1].function, + url = url, + method = "DELETE" + ) + + try: + + # Make the API call: + response = await self._http_client.delete( + url = url, + headers = headers + ) + + # Note down the results: + api_response.response = response + api_response.httpCode = response.status_code + api_response.message = response.reason_phrase + + # If something goes wrong: + except Exception as exception: + api_response.exception = exception + api_response.message = str(exception) + self._printer(exception, api_response.url, api_response.method, headers) + + # Done here: + return api_response + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/utils_v2/sms/models/behaviour/__init__.py b/utils_v2/rest/models/__init__.py similarity index 100% rename from utils_v2/sms/models/behaviour/__init__.py rename to utils_v2/rest/models/__init__.py diff --git a/utils_v2/rest/models/api_call.py b/utils_v2/rest/models/api_call.py new file mode 100644 index 0000000..954f30f --- /dev/null +++ b/utils_v2/rest/models/api_call.py @@ -0,0 +1,134 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 16th Dec., 2024. + + OBJECTIVE: + + To provide a data model for giving a general structure to API responses. + + 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, model_validator +from typing import Optional, Literal, Union, Dict, List, Any + +# My utils: +from utils_v2.string import json +from utils_v2.string import regex + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class ApiResponse(BaseModel): + + action: str = Field( + description = "to know what was being done; initially intended to just hold the name of the calling function", + frozen = True, + default = None + ) + + url: str = Field(frozen = True) + method: str = Field(frozen = True) + response: Any = None + httpCode: int = None + + success: bool = False + message: str = None + data: Any = None + + exception: Any = None + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + def to_markdown(self): + if self.exception: message = "❌ *API EXCEPTION:* ❌\n\n" + else: message = "*API RESPONSE:*\n\n" + message += f"*ACTION:*\n`{self.action}`\n\n" + message += f"*URL:*\n`{self.url}`\n\n" + message += f"*METHOD:*\n`{self.method}`\n\n" + message += f"*RESPONSE:*\n`{self.response}`\n\n" + message += f"*MESSAGE:*\n`{self.message}`\n\n" + message += f"*EXCEPTION:*\n`{self.exception.__class__.__name__}: {str(self.exception)}`\n\n" + return message + + async def get_json(self): + try: return self.response.json() + except: return {} + + async def get_content(self): + try: return self.response.content + except: return b"" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/utils_v2/sms/models/behaviour/nimbus/__init__.py b/utils_v2/sms/india/__init__.py similarity index 100% rename from utils_v2/sms/models/behaviour/nimbus/__init__.py rename to utils_v2/sms/india/__init__.py diff --git a/utils_v2/sms/models/behaviour/savvy_bulk_sms/__init__.py b/utils_v2/sms/india/nimbus/__init__.py similarity index 100% rename from utils_v2/sms/models/behaviour/savvy_bulk_sms/__init__.py rename to utils_v2/sms/india/nimbus/__init__.py diff --git a/utils_v2/sms/models/data/__init__.py b/utils_v2/sms/india/nimbus/controllers/__init__.py similarity index 100% rename from utils_v2/sms/models/data/__init__.py rename to utils_v2/sms/india/nimbus/controllers/__init__.py diff --git a/utils_v2/sms/models/behaviour/nimbus/async_nimbus.py b/utils_v2/sms/india/nimbus/controllers/async_nimbus.py similarity index 99% rename from utils_v2/sms/models/behaviour/nimbus/async_nimbus.py rename to utils_v2/sms/india/nimbus/controllers/async_nimbus.py index 5999dd0..0830cbf 100644 --- a/utils_v2/sms/models/behaviour/nimbus/async_nimbus.py +++ b/utils_v2/sms/india/nimbus/controllers/async_nimbus.py @@ -37,14 +37,14 @@ # To make sibling directories accessible for imports: import sys -sys.path.append(".") +sys.path.append("") sys.path.append("..") # To make API Calls: import httpx # Data models: -from utils_v2.sms.models.data.sms_message import SentSMSMessageModel +from utils_v2.sms.models.sms_message import SentSMSMessageModel # For debugging: from icecream import IceCreamDebugger diff --git a/utils_v2/telegram/models/behaviour/__init__.py b/utils_v2/sms/kenya/__init__.py similarity index 100% rename from utils_v2/telegram/models/behaviour/__init__.py rename to utils_v2/sms/kenya/__init__.py diff --git a/utils_v2/telegram/models/data/__init__.py b/utils_v2/sms/kenya/savvy_bulk_sms/__init__.py similarity index 100% rename from utils_v2/telegram/models/data/__init__.py rename to utils_v2/sms/kenya/savvy_bulk_sms/__init__.py diff --git a/utils_v2/sms/kenya/savvy_bulk_sms/controllers/__init__.py b/utils_v2/sms/kenya/savvy_bulk_sms/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py b/utils_v2/sms/kenya/savvy_bulk_sms/controllers/async_savvy_bulk_sms.py similarity index 99% rename from utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py rename to utils_v2/sms/kenya/savvy_bulk_sms/controllers/async_savvy_bulk_sms.py index 490b34a..16b9f17 100644 --- a/utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py +++ b/utils_v2/sms/kenya/savvy_bulk_sms/controllers/async_savvy_bulk_sms.py @@ -43,7 +43,7 @@ sys.path.append("..") import httpx # Data models: -from utils_v2.sms.models.data.sms_message import SentSMSMessageModel +from utils_v2.sms.models.sms_message import SentSMSMessageModel # For debugging: from icecream import IceCreamDebugger diff --git a/utils_v2/sms/models/data/sms_message.py b/utils_v2/sms/models/sms_message.py similarity index 100% rename from utils_v2/sms/models/data/sms_message.py rename to utils_v2/sms/models/sms_message.py diff --git a/utils_v2/telegram/controllers/__init__.py b/utils_v2/telegram/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils_v2/telegram/models/behaviour/telegram_bot.py b/utils_v2/telegram/controllers/async_bot.py similarity index 100% rename from utils_v2/telegram/models/behaviour/telegram_bot.py rename to utils_v2/telegram/controllers/async_bot.py diff --git a/utils_v2/telegram/models/data/api_call.py b/utils_v2/telegram/models/api_call.py similarity index 100% rename from utils_v2/telegram/models/data/api_call.py rename to utils_v2/telegram/models/api_call.py diff --git a/utils_v2/telegram/models/data/update.py b/utils_v2/telegram/models/update.py similarity index 100% rename from utils_v2/telegram/models/data/update.py rename to utils_v2/telegram/models/update.py