diff --git a/api/blueprints/mail/oauth/__init__.py b/api/blueprints/mail/oauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/mail/oauth_callback.py b/api/blueprints/mail/oauth/callback.py similarity index 100% rename from api/blueprints/mail/oauth_callback.py rename to api/blueprints/mail/oauth/callback.py diff --git a/api/blueprints/mail/oauth_request.py b/api/blueprints/mail/oauth/request.py similarity index 100% rename from api/blueprints/mail/oauth_request.py rename to api/blueprints/mail/oauth/request.py diff --git a/api/blueprints/mail/retrieve/__init__.py b/api/blueprints/mail/retrieve/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/mail/retrieve.py b/api/blueprints/mail/retrieve/get.py similarity index 91% rename from api/blueprints/mail/retrieve.py rename to api/blueprints/mail/retrieve/get.py index 6a91dd2..e4efffe 100644 --- a/api/blueprints/mail/retrieve.py +++ b/api/blueprints/mail/retrieve/get.py @@ -69,7 +69,7 @@ from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens from shared import constants # Data Models: -from models.data.api.mail.retrieve import MailGetRequestHeaders, MailGetRequestData +from models.api.mail.get import MailGetRequestHeaders, MailGetRequestData # To work with datatypes: from typing import Literal @@ -89,7 +89,7 @@ import datetime # Related to Quart: -mail_retrieve_bp = Blueprint("mail_retrieve", __name__) +mail_get_bp = Blueprint("mail_get", __name__) # ***************************************************************************************************************** @@ -109,7 +109,7 @@ mail_retrieve_bp = Blueprint("mail_retrieve", __name__) # ***************************************************************************************************************** -@mail_retrieve_bp.record_once +@mail_get_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. @@ -120,7 +120,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@mail_retrieve_bp.route("/get", methods = ["GET"]) +@mail_get_bp.route("", methods = ["GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") @@ -148,7 +148,7 @@ async def get_one_mail( ): """ - Use this endpoint when the user wants to fetch one mail. + Use this endpoint when the user wants to fetch one mail's full payload. :param inbound_headers: auto-extracted by the decorators. :param inbound_data: auto-extracted by the decorators. :param inbound_files: auto-extracted by the decorators. @@ -164,16 +164,17 @@ async def get_one_mail( ) # Get the mail: - mail_data = await current_app.mail_retrieve_model.get_mail( + message = await current_app.mail_controller.get_one_mail( mongo_conn = current_app.data_mongo, - mail_id = inbound_data.mailId + token_id = inbound_data.tokenId, + message_id = inbound_data.messageId ) # Done here: return ResponseModel( - status_code = StatusCodes.OK if mail_data else StatusCodes.FAILED, - http_code = HttpCodes.SUCCESS if mail_data else HttpCodes.NOT_FOUND, - data = mail_data + status_code = StatusCodes.OK if message else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if message else HttpCodes.NOT_FOUND, + data = message.full ) diff --git a/api/blueprints/mail/list.py b/api/blueprints/mail/retrieve/list.py similarity index 84% rename from api/blueprints/mail/list.py rename to api/blueprints/mail/retrieve/list.py index 0000fb7..7f6bb86 100644 --- a/api/blueprints/mail/list.py +++ b/api/blueprints/mail/retrieve/list.py @@ -10,8 +10,8 @@ OBJECTIVE: - To list e-mails by their account identifier. Remember that the 'account identifier' is the '_id' of the document - in MongoDB that holds the tokens to authorize the e-mail id whose mails are being accessed. + To list e-mails by their token identifier. Remember that the 'token identifier' is the '_id' of the document in + MongoDB that holds the tokens to authorize the e-mail id whose mails are being accessed. REFERENCES: @@ -70,7 +70,7 @@ from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens from shared import constants # Data Models: -from models.data.api.mail.list import MailListRequestHeaders, MailListByAccountIdRequestData +from models.api.mail.list import MailListRequestHeaders, MailListRequestData # To work with datatypes: from typing import Literal @@ -121,7 +121,9 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- +@mail_list_bp.route("/list", methods = ["GET"]) @mail_list_bp.route("/list/id/token", methods = ["GET", "POST"]) +@mail_list_bp.route("/list/tags", methods = ["GET", "POST"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") @@ -138,18 +140,19 @@ def init(blueprint_setup_state): @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @validate_input( header_validator = lambda x: MailListRequestHeaders(**x).model_dump(), - data_validator = lambda x: MailListByAccountIdRequestData(**x) + data_validator = lambda x: MailListRequestData(**x) ) @handle_cancelled_request() -async def list_mails_for_account_id( +async def list_mails( inbound_headers: dict | MailListRequestHeaders = None, - inbound_data: dict | MailListByAccountIdRequestData = None, + inbound_data: dict | MailListRequestData = None, inbound_files: dict = None, **kwargs ): """ - Use this endpoint when the user wants to fetch the list of mails. + Use this endpoint when the user wants to fetch the list of mails. The shortlisting here will be done by way of the + token identifier. Any no. of 'tokenId' objects will be given by the client. :param inbound_headers: auto-extracted by the decorators. :param inbound_data: auto-extracted by the decorators. :param inbound_files: auto-extracted by the decorators. @@ -164,19 +167,26 @@ async def list_mails_for_account_id( http_code = HttpCodes.UNAUTHORIZED ) - # Get the mail: - mails_list = await current_app.mail_retrieve_model.list_for_token_id( + # Build the additional filter: + additional_filter = {} + if inbound_data.tags: additional_filter["tags"] = {"$in": inbound_data.tags} + additional_filter = additional_filter or None + print("AddFil:", additional_filter) + + # Get the mails: + mails_list = await current_app.mail_controller.list_mails( mongo_conn = current_app.data_mongo, - token_id = inbound_data.tokenId, + token_ids = inbound_data.tokenIds, limit = inbound_data.count, - skip = inbound_data.fromCount + skip = inbound_data.fromCount, + additional_filter = additional_filter ) # Done here: return ResponseModel( status_code = StatusCodes.OK if mails_list else StatusCodes.FAILED, http_code = HttpCodes.SUCCESS if mails_list else HttpCodes.NOT_FOUND, - data = mails_list, + data = [mail.preview for mail in mails_list], message = f"{len(mails_list) if mails_list else 0} mail(s) found" ) diff --git a/api/blueprints/mail/sync/__init__.py b/api/blueprints/mail/sync/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/mail/sync_v2.py b/api/blueprints/mail/sync/sync_v2.py similarity index 100% rename from api/blueprints/mail/sync_v2.py rename to api/blueprints/mail/sync/sync_v2.py diff --git a/api/blueprints/mail/tags/__init__.py b/api/blueprints/mail/tags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/mail/sync_old.py b/api/blueprints/mail/tags/update.py similarity index 58% rename from api/blueprints/mail/sync_old.py rename to api/blueprints/mail/tags/update.py index ab18f59..92abd11 100644 --- a/api/blueprints/mail/sync_old.py +++ b/api/blueprints/mail/tags/update.py @@ -6,13 +6,11 @@ DATE: - Monday, 2nd Dec., 2024 + Friday, 13th Dec., 2024 OBJECTIVE: - To receive requests for synchronising mails from various mail clients to the database. Sync'ing means we pull - the mail from the mail client (like GMail) and store it to our database. The mail is then ready for showing on - the UI at any time. + To update the tags on one mail message. REFERENCES: @@ -50,6 +48,7 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.api.codes import StatusCodes, HttpCodes from utils_v2.api.response import ResponseModel from utils_v2.api.async_quart import ( + make_ordered_json, set_api_version, read_input, get_session_info, @@ -70,9 +69,7 @@ from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens from shared import constants # Data Models: -from models.data.api.mail.sync import MailSyncRequestHeaders, MailSyncRequestData -from models.data.api.mail.sync import MailSyncOneResult, MailSyncManyResults -from models.data.core.user import CoreUserInfoModel +from models.api.mail.tags import MailUpdateTagsRequestHeaders, MailUpdateTagsRequestData # To work with datatypes: from typing import Literal @@ -80,9 +77,6 @@ from typing import Literal # For asynchronous activities: import asyncio -# To work with LLMs: -from langchain_openai import ChatOpenAI - # To work with date and time: import datetime @@ -95,7 +89,7 @@ import datetime # Related to Quart: -mail_sync_bp = Blueprint("mail_sync", __name__) +mail_tags_update_bp = Blueprint("mail_tags_update", __name__) # ***************************************************************************************************************** @@ -115,7 +109,7 @@ mail_sync_bp = Blueprint("mail_sync", __name__) # ***************************************************************************************************************** -@mail_sync_bp.record_once +@mail_tags_update_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. @@ -126,44 +120,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -async def sync_mails( - user_info: CoreUserInfoModel, - mongo_conn: AsyncMongo, - llm: ChatOpenAI, - inbound_headers: dict, - inbound_data: MailSyncRequestData -) -> MailSyncManyResults: - - """ - A very simple function, but kept separate so that we get the option to switch between running it in the foreground - and running it in the background. - :param user_info: The information of the user as extracted from the session token. - :param mongo_conn: The instance of the database connector to use to sync the mails. - :param llm: The instance of the LLM to use to summarize the mails. - :param inbound_headers: The headers that came in with the request. - :param inbound_data: The data that came in with the request. - :return: The results of the mail-sync'ing attempt. - """ - - # Try to sync the mails: - return await current_app.mail_sync_model.sync( - session_token = inbound_headers["X-Session-Token"], - user_info = user_info, - mongo_conn = mongo_conn, - token_id = inbound_data.tokenId, - llm = llm, - force_sync = inbound_data.forceSync, - start_date = inbound_data.startDate, - end_date = inbound_data.endDate, - max_count = inbound_data.maxCount - ) - - -# --------------------------------------------------------------------------------------------------------------------- - - -@mail_sync_bp.route("/sync", methods = ["POST"]) -@mail_sync_bp.route("/sync/", methods = ["POST"]) +@mail_tags_update_bp.route("/tags", methods = ["PATCH"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") @@ -171,7 +128,7 @@ async def sync_mails( attr_name = "logs_mongo", project = constants.PROJECT_NAME, log_type = constants.MODULE_NAME, - operation = "mailSyncApi", + operation = "mailTagsUpdtApi", log_input = True, log_output = True, sensitive_keys = ["sessionToken", "X-Session-Token"] @@ -179,22 +136,19 @@ async def sync_mails( @log_chain_to_mongo(attr_name = "logs_mongo") @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @validate_input( - header_validator = lambda x: MailSyncRequestHeaders(**x).model_dump(), - data_validator = lambda x: MailSyncRequestData(**x) + header_validator = lambda x: MailGetRequestHeaders(**x).model_dump(), + data_validator = lambda x: MailGetRequestData(**x) ) @handle_cancelled_request() -async def sync_mail( - mode: Literal["background", "bg"] = None, - inbound_headers: dict | MailSyncRequestHeaders = None, - inbound_data: dict | MailSyncRequestData = None, +async def update_mail_tags( + inbound_headers: dict | MailGetRequestHeaders = None, + inbound_data: dict | MailGetRequestData = None, inbound_files: dict = None, **kwargs ): """ - Use this when the user wants to pull old mails from some mail client (like GMail) and save it to the database for - ready access on the UI. - :param mode: Set it to one of the specified options to make the sync'ing process go to the background. + Use this endpoint to update the tags on an e-mail message. :param inbound_headers: auto-extracted by the decorators. :param inbound_data: auto-extracted by the decorators. :param inbound_files: auto-extracted by the decorators. @@ -209,45 +163,18 @@ async def sync_mail( http_code = HttpCodes.UNAUTHORIZED ) - # Make the variables available in the scope of the current request: - g.inbound_headers = inbound_headers - g.inbound_data = inbound_data - - # If we've been asked to sync the mails in the background: - if mode in ["background", "bg"]: - current_app.add_background_task( - sync_mails, - user_info = CoreUserInfoModel(**kwargs["session_info"]), - mongo_conn = current_app.data_mongo, - llm = current_app.llm, - inbound_headers = inbound_headers, - inbound_data = inbound_data - ) - return ResponseModel( - status_code = StatusCodes.OK, - http_code = HttpCodes.ACCEPTED, - message = "your mails are being sync'd in the background" - ) - - # Otherwise we process it right here: - sync_results = await sync_mails( - user_info = CoreUserInfoModel(**kwargs["session_info"]), + # Get the mail: + message = await current_app.mail_controller.get_one_mail( mongo_conn = current_app.data_mongo, - llm = current_app.llm, - inbound_headers = inbound_headers, - inbound_data = inbound_data + token_id = inbound_data.tokenId, + message_id = inbound_data.messageId ) - # Response: + # Done here: return ResponseModel( - status_code = StatusCodes.FAILED if sync_results.failureCount > 0 else StatusCodes.OK, - http_code = HttpCodes.INTERNAL_SERVER_ERROR if sync_results.failureCount > 0 else HttpCodes.SUCCESS, - message = sync_results.message, - data = { - "totalCount": sync_results.totalCount, - "successCount": sync_results.successCount, - "failureCount": sync_results.failureCount - } + status_code = StatusCodes.OK if message else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if message else HttpCodes.NOT_FOUND, + data = message.full ) diff --git a/models/api/mail/retrieve.py b/models/api/mail/get.py similarity index 96% rename from models/api/mail/retrieve.py rename to models/api/mail/get.py index d4e6abb..1b4aee1 100644 --- a/models/api/mail/retrieve.py +++ b/models/api/mail/get.py @@ -101,7 +101,12 @@ class MailGetRequestHeaders(BaseModel): class MailGetRequestData(BaseModel): - mailId: str = Field( + tokenId: str = Field( + description = "the id of the token associated with the mail; needed for security", + frozen = True + ) + + messageId: str = Field( description = "the mail identifier (Mongo ObjectId) of the document that holds the mail", frozen = True ) diff --git a/models/api/mail/tags.py b/models/api/mail/tags.py new file mode 100644 index 0000000..1b4aee1 --- /dev/null +++ b/models/api/mail/tags.py @@ -0,0 +1,132 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Tuesday, 3rd Dec., 2024. + + OBJECTIVE: + + To provide a structure to query the full payload of an email. + + 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 + +# 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 MailGetRequestHeaders(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 MailGetRequestData(BaseModel): + + tokenId: str = Field( + description = "the id of the token associated with the mail; needed for security", + frozen = True + ) + + messageId: str = Field( + description = "the mail identifier (Mongo ObjectId) of the document that holds the mail", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/utils_v2/api/response.py b/utils_v2/api/response.py index 02e3d72..7783cfe 100644 --- a/utils_v2/api/response.py +++ b/utils_v2/api/response.py @@ -86,10 +86,13 @@ class ResponseModel(BaseModel): :return: The output as expected by Flask and Quart. """ + # Figure out the HTTP code: + response_http_code = self.http_code.value if self.http_code is not None else self.status_code.value[2] + # Construct the basic structure: response_dict = { "status": 1 if self.status_code.value[0] else 0, - "code": self.status_code.value[1], + "code": response_http_code, "message": self.message or self.status_code.name.replace("_", " ").lower(), "data": self.data, "apiVer": self.api_version @@ -99,9 +102,6 @@ class ResponseModel(BaseModel): if self.seconds is not None: response_dict["seconds"] = self.seconds if self.log_id is not None: response_dict["logId"] = self.log_id - # Figure out the HTTP code: - response_http_code = self.http_code.value if self.http_code is not None else self.status_code.value[2] - # Done here: return response_dict, response_http_code