From 897c32c59502ed1df2d593cdfa528484db2c8a29 Mon Sep 17 00:00:00 2001 From: khushal Date: Wed, 11 Dec 2024 13:20:54 +0530 Subject: [PATCH] (20241211) Started more work on the files part and added validation to the phone no. in the API exposed for Nimbus. --- models/behaviour/file/file.py | 371 ---------- models/behaviour/file/file_object.py | 672 +++++++++++++++++++ models/data/api/sms/send.py | 26 +- models/data/core/auth_token.py | 11 +- models/data/core/{file.py => file_object.py} | 116 ++-- models/data/core/message.py | 11 +- models/data/core/payment.py | 11 +- models/data/core/user.py | 7 - 8 files changed, 792 insertions(+), 433 deletions(-) delete mode 100644 models/behaviour/file/file.py create mode 100644 models/behaviour/file/file_object.py rename models/data/core/{file.py => file_object.py} (73%) diff --git a/models/behaviour/file/file.py b/models/behaviour/file/file.py deleted file mode 100644 index c9260cc..0000000 --- a/models/behaviour/file/file.py +++ /dev/null @@ -1,371 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - Tuesday, 10th Dec., 2024 - - OBJECTIVE: - - To define all file-management activities in one place. - - REFERENCES: - - N/A - - DOWNLOADS: - - N/A - -""" -import io -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys - -from sqlalchemy.orm.collections import collection - -sys.path.append(".") -sys.path.append("..") - -# 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 models.behaviour.base import BaseModel - -# Data models: -from models.data.core.user import CoreUserInfoModel -from models.data.core.file import CoreFileInfoModel, CoreFileSharingModel, CoreFilePermissionsModel - -# To work with MongoDB: -from bson import ObjectId - -# To work with datatypes: -from typing import Any, Literal - -# To make deep-copies: -import copy - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** CLASSES *** -# ***** **** -# ***************************************************************************************************************** - - -class FileManagementModel: - - # Define class-level variables: - USER_COLLECTION = "_filesUser" - FILE_COLLECTION = "_filesInfo" - DIRS_COLLECTION = "_dirsInfo" - LOGS_COLLECTION = "_filesLogs" - - # ┳┓ ┓• - # ┣┫┏┓┏┓┏┫┓┏┓┏┓ - # ┛┗┗ ┗┻┗┻┗┛┗┗┫ - # ┛ - - async def exists( - self, - mongo_conn: AsyncMongoStorage, - file_id: ObjectId | str - ) -> bool: - - """ - To check whether, or not, a particular file's record exists in the database. - :param mongo_conn: The instance of the database connection to perform this action. - :param file_id: The id of the file to check. - :return: True if it exists, else False. - """ - - # Run the query: - record = await mongo_conn.find_one( - collection = self.FILE_COLLECTION, - filter = {"_id": ObjectId(file_id)}, - projection = {"_id": True, "user": True} - ) - - # Return the result: - if not record: return False - else: return True - - async def info( - self, - mongo_conn: AsyncMongoStorage, - file_id: ObjectId - ) -> CoreFileInfoModel | None: - - """ - To get the information about this file. - :param mongo_conn: The instance of the database connection to perform this action. - :param file_id: The id of the file to check. - :return: None if the file doesn't exist, else its summary. - """ - - # Run the query: - record = await mongo_conn.find_one( - collection = self.FILE_COLLECTION, - filter = {"_id": ObjectId(file_id)} - ) - - # return the results: - if not record: return None - else: return CoreFileInfoModel(**record) - - async def is_private( - self, - mongo_conn: AsyncMongoStorage, - file_id: ObjectId | str - ) -> bool | None: - - """ - To check whether, or not, a particular file is publicly readable. - :param mongo_conn: The instance of the database connection to perform this action. - :param file_id: The id of the file to check. - :return: True if private, else False. None if it doesn't exist at all. - """ - - # Run the query: - record = await mongo_conn.find_one( - collection = self.FILE_COLLECTION, - filter = {"_id": ObjectId(file_id)}, - projection = {"_id": False, "isPrivate": True} - ) - - # Return the result: - if not record: return None - else: return record["isPrivate"] - - async def is_public( - self, - mongo_conn: AsyncMongoStorage, - file_id: ObjectId | str - ) -> bool | None: - - """ - A wrapper around the is_private, method that returns the opposite value. - :param mongo_conn: The instance of the database connection to perform this action. - :param file_id: The id of the file to check. - :return: True if public, else False. None if it doesn't exist at all. - """ - - # Call the existing function: - is_private = await self.is_private( - mongo_conn = mongo_conn, - file_id = file_id - ) - - # Return the opposite result: - if is_private is None: return is_private - else: return not is_private - - async def is_owner( - self, - mongo_conn: AsyncMongoStorage, - user_info: CoreUserInfoModel, - file_id: ObjectId | str, - ) -> bool | None: - - """ - To check if a specific user is the owner of a specific file. - :param mongo_conn: The instance of the database connection to perform this action. - :param user_info: The details of the user who needs to have permissions to this file. - :param file_id: The id of the file to check. - :return: True if owner, else False. None if something goes wrong. - """ - - # Parse the user conditions by ignoring the null values: - user_conditions = {k: v for k, v in user_info.model_dump().items() if v is not None} - - # Run the query: - record = await mongo_conn.find_one( - collection = self.FILE_COLLECTION, - filter = mongo_conn.dict_to_dot_notation({ - "_id": ObjectId(file_id), - "user": user_conditions, - }), - projection = { - "user": True - } - ) - - # Return the result: - if record is None: return False - else: return True - - async def has_permission( - self, - mongo_conn: AsyncMongoStorage, - user_info: CoreUserInfoModel, - file_id: ObjectId | str, - permission: Literal["read", "delete", "changePermissions"] - ) -> bool | None: - - """ - To check if a particular user has permissions to a given file. - :param mongo_conn: The instance of the database connection to perform this action. - :param user_info: The details of the user who needs to have permissions to this file. - :param file_id: The id of the file to check. - :param permission: The name of the permission that the said user must have on this file. - :return: True if the user has said permission, else False. None if something goes wrong. - """ - - # Parse the user conditions by ignoring the null values: - user_conditions = {k: v for k, v in user_info.model_dump().items() if v is not None} - - # # If the user is the owner, - # # he has all the permissions: - # is_owner = await self.is_owner( - # mongo_conn = mongo_conn, - # user_info = user_info, - # file_id = file_id - # ) - # if is_owner: return True - - # otherwise, we run the query to check for granted permissions: - record = await mongo_conn.find_one( - collection = self.FILE_COLLECTION, - filter = mongo_conn.dict_to_dot_notation({ - "_id": ObjectId(file_id), - "sharedWith.user": user_conditions, - }), - projection = { - "sharedWith.$": True - } - ) - - # Return the result: - if record is None: return False - else: return record["sharedWith"][0]["permissions"][permission] - - @staticmethod - async def download( - mongo_conn: AsyncMongoStorage, - file_id: ObjectId - ) -> io.BytesIO | None: - - """ - To quickly download small files. Do not use this for larger files because the file will be held in RAM first - and any large file will end up filling RAM fast. It's okay for smaller files that won't block up the memory. - :param mongo_conn: The instance of the database connection to perform this action. - :param file_id: The id of the file to fetch. - :return: The file in a BytesIO buffer, or None if the file doesn't exist. - """ - - # Get the file from the database: - buffer = io.BytesIO() - success = await mongo_conn.easy_download( - destination = buffer, - file_id = ObjectId(file_id), - raise_exception = True - ) - buffer.seek(0) - - # Return the result: - if not success: return None - else: return buffer - - async def download_from_stream( - self, - mongo_conn: AsyncMongoStorage, - file_id: ObjectId - ) -> Any: - - """ - To download any file as a stream. Better than the simple 'download' method because it doesn't block RAM. - :param mongo_conn: The instance of the database connection to perform this action. - :param file_id: The id of the file to fetch. - :return: YET TO BE IMPLEMENTED. - """ - - pass - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - import asyncio - import time - - async def main(): - - files_mongo = AsyncMongoStorage( - connection_string = r"mongodb://del.ditscentre.in:27017,wtt.ditscentre.in:27017,mum.arh.001.ditscentre.in:27017/admin?tls=true&tlsCAFile=%2Fetc%2Fssl%2Fcerts%2Fmongo_data_ca.pem&tlsCertificateKeyFile=%2Fetc%2Fssl%2Fcerts%2Fmongo_data_cert.pem&replicaSet=dits_mongod_rep&readPreference=primary&authMechanism=MONGODB-X509&authSource=%24external", - database_name = "converseStore", - max_connections = 10, - debug = True - ) - await files_mongo.connect() - - my_fs = FileManagementModel() - - start_time = time.time() - - tasks = [my_fs.has_permission( - mongo_conn = files_mongo, - user_info = CoreUserInfoModel( - userId = _, - # industry = "fashion" - ), - file_id = "67583b64f35e2c7d3cab7955", - permission = "read" - ) for _ in range(1_000)] - results = await asyncio.gather(*tasks) - for i, r in enumerate(results): - print(f"{i: >4}: {r}") - - print(f"Done in {time.time() - start_time:.5f} seconds") - - - asyncio.run(main()) diff --git a/models/behaviour/file/file_object.py b/models/behaviour/file/file_object.py new file mode 100644 index 0000000..b13ef54 --- /dev/null +++ b/models/behaviour/file/file_object.py @@ -0,0 +1,672 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Tuesday, 10th Dec., 2024 + + OBJECTIVE: + + To define all file-management activities in one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" +import io +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys + +from sqlalchemy.orm.collections import collection + +from utils_v2.mail.mail_parser_v2 import parse_addr + +sys.path.append(".") +sys.path.append("..") + +# 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 models.behaviour.base import BaseModel + +# Data models: +from models.data.core.user import CoreUserInfoModel +from models.data.core.file_object import ( + CoreFileObjectInfoModel, + CoreFileObjectSharingModel, + CoreFileObjectPermissionsModel +) + +# To work with MongoDB: +from bson import ObjectId + +# To work with datatypes: +from typing import Any, Literal, List + +# To make deep copies: +import copy + +# To make deep-copies: +import copy + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class FileObjectManagementModel: + + # Define class-level variables: + FILE_OBJECTS_COLLECTION = "_fileObjects" + + # ┓┏ ┓ + # ┣┫┏┓┃┏┓┏┓┏┓┏ + # ┛┗┗ ┗┣┛┗ ┛ ┛ + # ┛ + + def match_lists( + self, + list_a: list, + list_b: list + ): + + """ + To match items in a List. The lists should have values that can be sorted. Mixed value dicts will raise + exceptions. + :param list_a: One of the lists to check. + :param list_b: The other list to check. + :return: True if they match, else False. + """ + + # Start by assuming success: + are_matching = True + + # If the lengths don't match, + # there is no way the lists are exactly the same: + if len(list_a) != len(list_b): + are_matching = False + return are_matching + + # Sort the lists first for better comparison: + sorted_list_a = sorted(copy.deepcopy(list_a)) + sorted_list_b = sorted(copy.deepcopy(list_b)) + + # Now loop through them and compare elements at the same index: + for index in range(len(sorted_list_a)): + av = sorted_list_a[index] + bv = sorted_list_b[index] + + # If both of them are some sorts of lists: + if isinstance(av, (list, tuple, set)): + if not self.match_lists(list_a = list(av), list_b = list(bv)): + are_matching = False + break + + # If both of them are dicts, we go recursive: + elif isinstance(av, dict): + if not self.match_dicts(dict_a = av, dict_b = bv): + are_matching = False + break + + # Finally, we make direct comparison for things like strings, ints, etc. + else: + if av != bv: + are_matching = False + break + + # Done here: + return are_matching + + def match_dicts( + self, + dict_a: dict, + dict_b: dict + ): + + """ + To match items in a dict. Any lists in the dict should have values that can be sorted. Mixed value dicts will + raise exceptions. + :param dict_a: One of the dicts to check. + :param dict_b: The other dict to check. + :return: True if they match, else False. + """ + + # Start by assuming success: + are_matching = True + + # Iterate through the required items: + for ak, av in dict_a.items(): + + # Fetch the corresponding value from the other dict: + bv = dict_b.get(ak) + + # If the types are themselves mismatched, + # no point in further comparison: + if not isinstance(av, type(bv)): + are_matching = False + break + + # If both of them are some sorts of lists: + if isinstance(av, (list, tuple, set)): + if not self.match_lists(list_a = list(av), list_b = list(bv)): + are_matching = False + break + + # If both of them are dicts, we go recursive: + elif isinstance(av, dict): + if not self.match_dicts(dict_a = av, dict_b = bv): + are_matching = False + break + + # Finally, we make direct comparison for things like strings, ints, etc. + else: + if av != bv: + are_matching = False + break + + # Done here: + return are_matching + + # ┏┓ • ┓ ┏┓ • + # ┃┃┓┏┓┏┃┏ ┃┃┓┏┏┓┏┓┓┏┓┏ + # ┗┻┗┻┗┗┛┗ ┗┻┗┻┗ ┛ ┗┗ ┛ + + async def exists( + self, + mongo_conn: AsyncMongoStorage, + file_object_id: ObjectId | str + ) -> bool: + + """ + To check whether, or not, a particular file's record exists in the database. + :param mongo_conn: The instance of the database connection to perform this action. + :param file_object_id: The id of the file to check. + :return: True if it exists, else False. + """ + + # Run the query: + record = await mongo_conn.find_one( + collection = self.FILE_OBJECTS_COLLECTION, + filter = {"_id": ObjectId(file_object_id)}, + projection = {"_id": True, "user": True} + ) + + # Return the result: + if not record: return False + else: return True + + async def info( + self, + mongo_conn: AsyncMongoStorage, + file_object_id: ObjectId + ) -> CoreFileObjectInfoModel | None: + + """ + To get the information about this file. + :param mongo_conn: The instance of the database connection to perform this action. + :param file_object_id: The id of the file to check. + :return: None if the file doesn't exist, else its summary. + """ + + # Run the query: + record = await mongo_conn.find_one( + collection = self.FILE_OBJECTS_COLLECTION, + filter = {"_id": ObjectId(file_object_id)} + ) + + # return the results: + if not record: return None + else: return CoreFileObjectInfoModel(**record) + + async def is_private( + self, + mongo_conn: AsyncMongoStorage, + file_object_id: ObjectId | str + ) -> bool | None: + + """ + To check whether, or not, a particular file is publicly readable. + :param mongo_conn: The instance of the database connection to perform this action. + :param file_object_id: The id of the file to check. + :return: True if private, else False. None if it doesn't exist at all. + """ + + # Run the query: + record = await mongo_conn.find_one( + collection = self.FILE_OBJECTS_COLLECTION, + filter = {"_id": ObjectId(file_object_id)}, + projection = {"_id": False, "isPrivate": True} + ) + + # Return the result: + if not record: return None + else: return record["isPrivate"] + + async def is_public( + self, + mongo_conn: AsyncMongoStorage, + file_object_id: ObjectId | str + ) -> bool | None: + + """ + A wrapper around the is_private, method that returns the opposite value. + :param mongo_conn: The instance of the database connection to perform this action. + :param file_object_id: The id of the file to check. + :return: True if public, else False. None if it doesn't exist at all. + """ + + # Call the existing function: + is_private = await self.is_private( + mongo_conn = mongo_conn, + file_object_id = file_object_id + ) + + # Return the opposite result: + if is_private is None: return is_private + else: return not is_private + + async def is_owner( + self, + mongo_conn: AsyncMongoStorage, + user_info: CoreUserInfoModel, + file_object_id: ObjectId | str, + ) -> bool | None: + + """ + To check if a specific user is the owner of a specific file. + :param mongo_conn: The instance of the database connection to perform this action. + :param user_info: The details of the user who needs to have permissions to this file. + :param file_object_id: The id of the file to check. + :return: True if owner, else False. None if something goes wrong. + """ + + # Parse the user conditions by ignoring the null values: + user_conditions = {k: v for k, v in user_info.model_dump().items() if v is not None} + + # Run the query: + record = await mongo_conn.find_one( + collection = self.FILE_OBJECTS_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "_id": ObjectId(file_object_id), + "user": user_conditions, + }), + projection = { + "user": True + } + ) + + # Return the result: + if record is None: return False + else: return True + + async def has_permission( + self, + mongo_conn: AsyncMongoStorage, + user_info: CoreUserInfoModel, + permission: Literal["read", "delete", "changePermissions"], + file_object_id: ObjectId | str = None, + check_ownership: bool = True + ) -> bool | None: + + """ + To check if a particular user has permissions to a given file obj. You may pass either an instance of the file's + info, or you may send the file's id to check. If you pass just the file's id, a database call will be needed. + :param mongo_conn: The instance of the database connection to perform this action. + :param user_info: The details of the user who needs to have permissions to this file. + :param permission: The name of the permission that the said user must have on this file. + :param file_object_id: The id of the file to check. + :param check_ownership: Owners of files have all rights to their own files. If this field is set to True, + ownership will be tested, and if the user is the owner of the file, the permission is assumed to be granted. + If this field is set to False, only the sharing section of the file's record will be checked. + :return: True if the user has said permission, else False. None if something goes wrong. + """ + + # Parse the user conditions by ignoring the null values: + user_conditions = {k: v for k, v in user_info.model_dump().items() if v is not None} + + # If we have to test ownership: + if check_ownership: + is_owner = await self.is_owner( + mongo_conn = mongo_conn, + user_info = user_info, + file_object_id = file_object_id + ) + if is_owner: return True + + # otherwise, we run the query to check for granted permissions: + record = await mongo_conn.find_one( + collection = self.FILE_OBJECTS_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "_id": ObjectId(file_object_id), + "sharedWith.user": user_conditions, + }), + projection = { + "sharedWith.$": True + } + ) + + # Return the result: + if record is None: return False + else: return record["sharedWith"][0]["permissions"][permission] + + # ┓ • • + # ┃ ┓┏╋┓┏┓┏┓ + # ┗┛┗┛┗┗┛┗┗┫ + # ┛ + + async def list_owned_dirs( + self, + mongo_conn: AsyncMongoStorage, + user_info: CoreUserInfoModel, + limit: int = 50, + skip: int = 0, + ) -> List[CoreFileObjectInfoModel] | None: + + """ + To list all the dirs that are owned by the described user. + :param mongo_conn: The connection instance to use to make the check. + :param user_info: The information about the user that we must match. + :param limit: How many max. records to fetch. + :param skip: How many initial records to skip. Useful for pagination. + :return: The list of files owned by the user (can be empty), or None if something goes wrong. + """ + + # Run the query: + records = await mongo_conn.find_many( + collection = self.FILE_OBJECTS_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "user": {k: v for k, v in user_info.model_dump().items() if v is not None}, + "isDir": True + }), + limit = limit, + skip = skip + ) + + # If something went wrong, we receive null for the records. + # We pass that null on: + if records is None: return None + + # Otherwise, we format and return the records: + return [CoreFileObjectInfoModel(**record) for record in records] + + async def list_shared_dirs( + self, + mongo_conn: AsyncMongoStorage, + user_info: CoreUserInfoModel, + limit: int = 50, + skip: int = 0, + ) -> List[CoreFileObjectInfoModel] | None: + + """ + To list all the dirs that have been shared with the described user. + :param mongo_conn: The connection instance to use to make the check. + :param user_info: The information about the user that we must match. + :param limit: How many max. records to fetch. + :param skip: How many initial records to skip. Useful for pagination. + :return: The list of files owned by the user (can be empty), or None if something goes wrong. + """ + + # Run the query: + records = await mongo_conn.find_many( + collection = self.FILE_OBJECTS_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "sharedWith.user": {k: v for k, v in user_info.model_dump().items() if v is not None}, + "isDir": True + }), + limit = limit, + skip = skip + ) + + # If something went wrong, we receive null for the records. + # We pass that null on: + if records is None: return None + + # Otherwise, we format and return the records: + return [CoreFileObjectInfoModel(**record) for record in records] + + async def list_owned_files( + self, + mongo_conn: AsyncMongoStorage, + user_info: CoreUserInfoModel, + limit: int = 50, + skip: int = 0, + ) -> List[CoreFileObjectInfoModel] | None: + + """ + To list all the files that are owned by the described user. + :param mongo_conn: The connection instance to use to make the check. + :param user_info: The information about the user that we must match. + :param limit: How many max. records to fetch. + :param skip: How many initial records to skip. Useful for pagination. + :return: The list of files owned by the user (can be empty), or None if something goes wrong. + """ + + # Run the query: + records = await mongo_conn.find_many( + collection = self.FILE_OBJECTS_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "user": {k: v for k, v in user_info.model_dump().items() if v is not None}, + "isDir": False + }), + limit = limit, + skip = skip + ) + + # If something went wrong, we receive null for the records. + # We pass that null on: + if records is None: return None + + # Otherwise, we format and return the records: + return [CoreFileObjectInfoModel(**record) for record in records] + + async def list_shared_files( + self, + mongo_conn: AsyncMongoStorage, + user_info: CoreUserInfoModel, + limit: int = 50, + skip: int = 0, + ) -> List[CoreFileObjectInfoModel] | None: + + """ + To list all the files that have been shared with the described user. + :param mongo_conn: The connection instance to use to make the check. + :param user_info: The information about the user that we must match. + :param limit: How many max. records to fetch. + :param skip: How many initial records to skip. Useful for pagination. + :return: The list of files owned by the user (can be empty), or None if something goes wrong. + """ + + # Run the query: + records = await mongo_conn.find_many( + collection = self.FILE_OBJECTS_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "sharedWith.user": {k: v for k, v in user_info.model_dump().items() if v is not None}, + "isDir": False + }), + limit = limit, + skip = skip + ) + + # If something went wrong, we receive null for the records. + # We pass that null on: + if records is None: return None + + # Otherwise, we format and return the records: + return [CoreFileObjectInfoModel(**record) for record in records] + + # ┳┓ ┓• + # ┣┫┏┓┏┓┏┫┓┏┓┏┓ + # ┛┗┗ ┗┻┗┻┗┛┗┗┫ + # ┛ + + @staticmethod + async def download_file( + mongo_conn: AsyncMongoStorage, + file_id: ObjectId + ) -> io.BytesIO | None: + + """ + To quickly download small files. Do not use this for larger files because the file will be held in RAM first + and any large file will end up filling RAM fast. It's okay for smaller files that won't block up the memory. + WARNING: Check for permissions before using this method. + :param mongo_conn: The instance of the database connection to perform this action. + :param file_id: The id of the file to fetch. + :return: The file in a BytesIO buffer, or None if the file doesn't exist. + """ + + # Get the file from the database: + buffer = io.BytesIO() + success = await mongo_conn.easy_download( + destination = buffer, + file_id = ObjectId(file_id), + raise_exception = True + ) + buffer.seek(0) + + # Return the result: + if not success: return None + else: return buffer + + @staticmethod + async def get_file_download_stream( + mongo_conn: AsyncMongoStorage, + file_id: ObjectId + ) -> Any: + + """ + To download any file as a stream. Better than the simple 'download' method because it doesn't block RAM. Once + the stream is created you can read from it, and terminate it like this: + READ: await stream.read(chunk_size) + CLOSE (without awaiting): stream.close() + WARNING: Check for permissions before using this method. + :param mongo_conn: The instance of the database connection to perform this action. + :param file_id: The id of the file whose stream you would like to fetch. + :return: Returns the stream object that will allow more efficient downloads of files on the user's end. + """ + + return await mongo_conn.get_download_stream(file_id = file_id) + + # ┓ ┏ • • + # ┃┃┃┏┓┓╋┓┏┓┏┓ + # ┗┻┛┛ ┗┗┗┛┗┗┫ + # ┛ + + async def make_dir(self): pass + + async def upload_file(self): pass + + async def get_file_upload_stream(self): pass + + # ┳┓ ┓ • + # ┃┃┏┓┃┏┓╋┓┏┓┏┓ + # ┻┛┗ ┗┗ ┗┗┛┗┗┫ + # ┛ + + async def delete_dir(self): pass + + async def delete_file(self): pass + + # ┏┓ • • + # ┃┃┏┓┏┓┏┳┓┓┏┏┓┏┓┏┓┏ + # ┣┛┗ ┛ ┛┗┗┗┛┛┗┗┛┛┗┛ + + async def update_permissions(self): pass + + async def make_public(self): pass + + async def mak_private(self): pass + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + import asyncio + import time + + async def main(): + + files_mongo = AsyncMongoStorage( + connection_string = r"mongodb://del.ditscentre.in:27017,wtt.ditscentre.in:27017,mum.arh.001.ditscentre.in:27017/admin?tls=true&tlsCAFile=%2Fetc%2Fssl%2Fcerts%2Fmongo_data_ca.pem&tlsCertificateKeyFile=%2Fetc%2Fssl%2Fcerts%2Fmongo_data_cert.pem&replicaSet=dits_mongod_rep&readPreference=primary&authMechanism=MONGODB-X509&authSource=%24external", + database_name = "converseStore", + max_connections = 10, + debug = True + ) + await files_mongo.connect() + my_fs = FileObjectManagementModel() + + user_a = CoreUserInfoModel( + userId = 1 + ) + + user_b = CoreUserInfoModel( + userId = 6 + ) + + owned_files = await my_fs.list_shared_files( + mongo_conn = files_mongo, + user_info = user_b + ) + + if owned_files is None: print("ERROR!") + else: print(f"FILES ({len(owned_files)}):", json.to_string(owned_files, default = str)) + + + asyncio.run(main()) diff --git a/models/data/api/sms/send.py b/models/data/api/sms/send.py index 92a5dbe..f149c7b 100644 --- a/models/data/api/sms/send.py +++ b/models/data/api/sms/send.py @@ -82,7 +82,7 @@ class NimbusSMSIndiaMessage(BaseModel): recipientNo: str = Field( description = "the phone no. of the target recipient", - min_length = 1, + pattern = r"\+?\d{0,3}\s*\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}", frozen = True ) @@ -106,6 +106,17 @@ class NimbusSMSIndiaMessage(BaseModel): class Config: extra = "forbid" + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("recipientNo", mode = "before") + def validate_contact_nos(cls, value): + value = regex.replace(text = str(value), pattern = r"[^\d]", substitute_text = "") + if len(value) > 10: value = regex.replace(text = str(value), pattern = r"^(91)", substitute_text = "") + value = regex.find_first(text = str(value), pattern = r"^[\d]{10}") + return value + # --------------------------------------------------------------------------------------------------------------------- @@ -194,4 +205,15 @@ class SMSSendRequestData(BaseModel): if __name__ == "__main__": - pass + from utils_v2.string import json + + my_request = SMSSendRequestData( + tokenId = "670f580d7cda4ebc1adc3444", + message = NimbusSMSIndiaMessage( + recipientNo = "+.9.1 ---> 93261 3642fgnn193261", + text = "Hello, Nimbus!", + templateId = "12345678" + ) + ) + + print("NIMBUS SMS API MODEL:", json.to_string(my_request.model_dump(), default = str)) diff --git a/models/data/core/auth_token.py b/models/data/core/auth_token.py index 7305817..5e3a56f 100644 --- a/models/data/core/auth_token.py +++ b/models/data/core/auth_token.py @@ -82,11 +82,11 @@ import datetime class CoreAuthTokenModel(BaseModel): - version: str = Field( - description = "a hint about the version no. of this message", - min_length = 1, + authTokenId: ObjectId = Field( + description = "the id of the document in mongodb that holds this information", frozen = True, - default = "2.0.0" + default = None, + alias = "_id" ) serviceType: Literal["email", "sms", "chat"] = Field( @@ -176,6 +176,9 @@ class CoreAuthTokenModel(BaseModel): extra = "allow" arbitrary_types_allowed = True + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ diff --git a/models/data/core/file.py b/models/data/core/file_object.py similarity index 73% rename from models/data/core/file.py rename to models/data/core/file_object.py index 3707290..4e5f79e 100644 --- a/models/data/core/file.py +++ b/models/data/core/file_object.py @@ -80,22 +80,28 @@ import datetime # ***************************************************************************************************************** -class CoreFilePermissionsModel(BaseModel): +class CoreFileObjectPermissionsModel(BaseModel): read: bool = Field( - description = "grants permissions to read/view this file", + description = "grants permissions to read/view this file/dir", frozen = True, default = True ) + write: bool = Field( + description = "grants permissions to write new files this dir; irrelevant for files", + frozen = True, + default = False + ) + delete: bool = Field( - description = "grants permissions to delete this file entirely", + description = "grants permissions to delete this file/dir entirely", frozen = True, default = False ) changePermissions: bool = Field( - description = "grants permissions to modify the permissions of this file", + description = "grants permissions to modify the permissions of this file/dir", frozen = True, default = False ) @@ -113,14 +119,14 @@ class CoreFilePermissionsModel(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class CoreFileSharingModel(BaseModel): +class CoreFileObjectSharingModel(BaseModel): user: CoreUserInfoModel = Field( description = "to identify the user who has access to this file", frozen = True ) - permissions: CoreFilePermissionsModel = Field( + permissions: CoreFileObjectPermissionsModel = Field( description = "the permissions that the above mentioned user has to this file", frozen = True ) @@ -138,66 +144,73 @@ class CoreFileSharingModel(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class CoreFileInfoModel(BaseModel): +class CoreFileObjectInfoModel(BaseModel): - version: str = Field( - description = "a hint about the version no. of this message", - min_length = 1, + fileObjectId: ObjectId = Field( + description = "the id of the document in mongodb that holds this information", frozen = True, - default = "2.0.0" + default = None, + alias = "_id" ) user: CoreUserInfoModel = Field( - description = "to identify the user who owns this file", + description = "to identify the user who owns this file/dir", frozen = True ) - filename: str = Field( - description = "the name of this file", - frozen = True - ) - - uploadTs: AwareDatetime = Field( - description = "the time (utc) at which this message was sent by the sender", - frozen = True - ) - - length: int = Field( - description = "to note when the user has marked this message as unread", - frozen = False, + isDir: bool = Field( + description = "to know whether this object is s file or a directory", + frozen = True, default = False ) - hash: str = Field( - description = "a simple hash to verify the integrity of the uploaded data", + name: str = Field( + description = "the name of this file/dir", frozen = True ) + createTs: AwareDatetime = Field( + description = "the time (utc) at which this file/dir was created", + frozen = True + ) + + length: int | None = Field( + description = "to note the size of the file; irrelevant for dirs", + frozen = False, + default = None + ) + + hash: str | None = Field( + description = "a simple hash to verify the integrity of the uploaded data; irrelevant for dirs", + frozen = True, + default = None + ) + metadata: Dict[str, Any] = Field( - description = "any addition data about this file to filter it later", + description = "any addition data about this file/dir to filter it later", frozen = False, default = {} ) tags: List[str] = Field( - description = "a list of keywords to apply to this file to filter it later", + description = "a list of keywords to apply to this file/dir to filter it later", frozen = False, default = [], examples = ["Bank Statement", "PDF", "bhopli@orange.com"] ) parentId: ObjectId | None = Field( - description = "to identify the parent directory of this file; null means root directory", + description = "to identify the parent dir of this file/dir; null means root dir", frozen = False ) isPrivate: bool = Field( - description = "whether, or not, this file is a private file", + description = "whether, or not, this file/dir is a private file", frozen = True ) - sharedWith: List[CoreFileSharingModel] = Field( - description = "sharing settings; specially relevant when the file is private", + sharedWith: List[CoreFileObjectSharingModel] = Field( + description = "sharing settings; specially relevant when the file/dir is private", frozen = True, default = [] ) @@ -211,20 +224,39 @@ class CoreFileInfoModel(BaseModel): extra = "allow" arbitrary_types_allowed = True + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ - @field_validator("uploadTs", mode = "before") + @field_validator("createTs", mode = "before") def parse_date_time(cls, value): return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC) - @field_validator("parentId", mode = "before") + @field_validator("fileObjectId", "parentId", mode = "before") def parse_oid(cls, value): try: value = ObjectId(value) except: pass return value + # ┏┓ • + # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏ + # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛ + # ┛ + + @property + def summary(self): + return { + "user": self.user, + "isDir": self.isDir, + "name": self.name, + "createTs": self.createTs, + "parentId": self.parentId, + "isPrivate": self.isPrivate, + } + # ***************************************************************************************************************** # ***** **** @@ -237,7 +269,8 @@ if __name__ == "__main__": from utils_v2.string import json - file_info = CoreFileInfoModel( + file_info = CoreFileObjectInfoModel( + # _id = "67519cf3a7804fcbc6f12452", user = CoreUserInfoModel( fullName = "Bhopli Narangi", userId = 1, @@ -247,10 +280,10 @@ if __name__ == "__main__": branchId = 5, industry = "fashion" ), - filename = "Giga-Cat.png", - uploadTs = date_time.get_current_utc_date_time(as_string = False), + name = "Midnight-Snack.png", + createTs = date_time.get_current_utc_date_time(as_string = False), length = 1024, - hash = "abcdefgh", + hash = "abcdefgh12345678", metadata = { "camera": "iPhone 1000 Pro Max XS" }, @@ -262,7 +295,7 @@ if __name__ == "__main__": parentId = "67519cf3a7804fcbc6f12452", isPrivate = True, sharedWith = [ - CoreFileSharingModel( + CoreFileObjectSharingModel( user = CoreUserInfoModel( fullName = "Polki Muchhwaali", userId = 6, @@ -272,8 +305,9 @@ if __name__ == "__main__": branchId = 10, industry = "entertainment" ), - permissions = CoreFilePermissionsModel( + permissions = CoreFileObjectPermissionsModel( read = True, + write = False, delete = False, changePermissions = False ) diff --git a/models/data/core/message.py b/models/data/core/message.py index 4c262df..f66a2ee 100644 --- a/models/data/core/message.py +++ b/models/data/core/message.py @@ -79,11 +79,11 @@ import datetime class CoreMessageModel(BaseModel): - version: str = Field( - description = "a hint about the version no. of this message", - min_length = 1, + messageId: ObjectId = Field( + description = "the id of the document in mongodb that holds this information", frozen = True, - default = "2.0.0" + default = None, + alias = "_id" ) ts: AwareDatetime = Field( @@ -170,6 +170,9 @@ class CoreMessageModel(BaseModel): extra = "allow" arbitrary_types_allowed = True + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ diff --git a/models/data/core/payment.py b/models/data/core/payment.py index 860ce40..5f902ed 100644 --- a/models/data/core/payment.py +++ b/models/data/core/payment.py @@ -126,11 +126,11 @@ class PaymentEvent(BaseModel): class CorePaymentModel(BaseModel): - version: str = Field( - description = "a hint about the version no. of this message", - min_length = 1, + paymentId: ObjectId = Field( + description = "the id of the document in mongodb that holds this information", frozen = True, - default = "2.0.0" + default = None, + alias = "_id" ) paymentStatus: Literal[ @@ -196,6 +196,9 @@ class CorePaymentModel(BaseModel): extra = "allow" arbitrary_types_allowed = True + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ diff --git a/models/data/core/user.py b/models/data/core/user.py index 760b409..b9b8a7c 100644 --- a/models/data/core/user.py +++ b/models/data/core/user.py @@ -79,13 +79,6 @@ import datetime class CoreUserInfoModel(BaseModel): - version: str = Field( - description = "a hint about the version no. of this message", - min_length = 1, - frozen = True, - default = "2.0.0" - ) - fullName: str | None = Field( description = "the full name of the user as found in the database", frozen = True,