From c5699de003d49b5b4ca707c688eadc1516c920ff Mon Sep 17 00:00:00 2001 From: khushal Date: Tue, 26 Nov 2024 12:59:54 +0530 Subject: [PATCH] (20241126) Message-listing done. --- utils_v2/goog/gmail.py | 147 +++++++++++++++++++++++++++++--- utils_v2/oauth/services/goog.py | 1 + 2 files changed, 137 insertions(+), 11 deletions(-) diff --git a/utils_v2/goog/gmail.py b/utils_v2/goog/gmail.py index 437f191..599fcf0 100644 --- a/utils_v2/goog/gmail.py +++ b/utils_v2/goog/gmail.py @@ -60,11 +60,14 @@ import asyncio import datetime # For working with datatypes: -from typing import Dict, Literal, List +from typing import Dict, Literal, List, Any # For debugging: from icecream import IceCreamDebugger +# For computational help: +import math + # ***************************************************************************************************************** # ***** **** @@ -507,7 +510,7 @@ class AsyncGMailClient: # ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ # ┛ - async def __list_messages_in_page( + async def __list_messages_on_page( self, count: int = 100, query: str = None, @@ -515,7 +518,25 @@ class AsyncGMailClient: include_spam_and_trash: bool = False, next_page_token: str = None, raise_exception: bool = False - ): + ) -> Dict[str, Any] | None: + + """ + To enlist messages on one page. Google allows at most 500 results on one page. This method respects that + pagination limit and returns only what Google gives. This method should be used internally by the class and the + class should expose another method that calls this one in loop to get any arbitrary no. of messages as the user + desires. + DOCUMENTATION: + 1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list + 2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message + :param count: The no. of messages to fetch. + :param query: Any query filter that is supported by GMail. + :param label_ids: The list of labels' ids that the mails must have on them. + :param include_spam_and_trash: Whether, or not, you would like to include mails categorized as spam and trash. + :param next_page_token: The token to fetch the next set of results. + :param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be + suppressed. + :return: The messages that matched the given conditions if the call was successful, else None. + """ # Start by assuming failure: page_messages = None @@ -526,14 +547,21 @@ class AsyncGMailClient: await self.__ensure_token() # Build the needed params: - params_json = {"count": count} + params_json = { + "maxResults": count, + "includeSpamTrash": include_spam_and_trash + } if query: params_json["q"] = query if next_page_token: params_json["pageToken"] = next_page_token if label_ids: params_json["labelIds"] = label_ids if isinstance(label_ids, list) else [label_ids] - if include_spam_and_trash: params_json["includeSpamTrash"] = include_spam_and_trash # Make the API call: - if not self._debug_only_errors: self._printer("Listing All Labels.", self.__user_email) + if not self._debug_only_errors: self._printer( + "Listing Messages for Page.", + self.__user_email, + count, + next_page_token + ) api_response = await self.__http_client.get( url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/messages", headers = {"Authorization": f"Bearer {self.__credentials.token}"}, @@ -541,19 +569,114 @@ class AsyncGMailClient: ) # If the API call failed: - if api_response.status_code not in [200]: return labels + if api_response.status_code not in [200]: return page_messages # Else we format the response: - labels = {label.pop("name"): label for label in api_response.json().get("labels", [])} + api_json = api_response.json() + page_messages = { + "messages": {m.pop("id"): m for m in api_json.get("messages", [])}, + "nextPageToken": api_json.get("nextPageToken"), + "resultSizeEstimate": api_json["resultSizeEstimate"], + } # In case something goes wrong along the way: except Exception as exception: if raise_exception: raise self._printer(exception) - labels = None + page_messages = None # Done here: - return labels + return page_messages + + async def list_messages( + self, + count: int = 100, + query: str = None, + label_ids: List[str] | str = None, + include_spam_and_trash: bool = False, + raise_exception: bool = False + ) -> Dict[str, Any]: + + """ + To enlist mail messages from a user's account. + DOCUMENTATION: + 1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list + 2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message + :param count: The no. of messages to fetch. + :param query: Any query filter that is supported by GMail. + :param label_ids: The list of labels' ids that the mails must have on them. + :param include_spam_and_trash: Whether, or not, you would like to include mails categorized as spam and trash. + :param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be + suppressed. + :return: The messages that matched the given conditions if the call was successful, else None. + """ + + # Start by assuming failure: + messages = None + + try: + + # Standard token-refresh check: + await self.__ensure_token() + + # We convert the messages to a dict: + messages = { + "messages": {}, + "nextPageToken": None, + "resultSizeEstimate": 0 + } + + # Let's figure out how many times we'll have to loop through the process to retrieve the target no. of + # messages. Google allows you to fetch info about at most 500 messages in one go. + max_per_call = 500 # ... because Google allows at most 500 entries in one call. + iterations_needed = int(math.ceil(count / max_per_call)) + last_iteration_count = count - int((max_per_call * (iterations_needed - 1))) + + # Run the loop those many times: + next_page_token = None + results_size_estimate = 0 + for iteration_no in range(iterations_needed): + + # Figure out the count for this page: + if iterations_needed > 1: + if iteration_no < (iterations_needed - 1): iteration_count = max_per_call + else: iteration_count = last_iteration_count + else: iteration_count = count + + # Retrieve the messages for this page: + iteration_messages = await self.__list_messages_on_page( + count = iteration_count, + query = query, + label_ids = label_ids, + include_spam_and_trash = include_spam_and_trash, + next_page_token = next_page_token, + raise_exception = True + ) + + # Check if no data was received: + if iteration_messages is None: break + if not iteration_messages.get("messages"): break + + # Now that we know that messages were received: + for k, v in iteration_messages["messages"].items(): messages["messages"][k] = v + results_size_estimate += iteration_messages["resultSizeEstimate"] + + # If there is no next page after this, we break out of the loop: + next_page_token = iteration_messages["nextPageToken"] + if next_page_token is None: break + + # Format the final response: + messages["nextPageToken"] = next_page_token + messages["resultSizeEstimate"] = results_size_estimate + + # In case something goes wrong along the way: + except Exception as exception: + if raise_exception: raise + self._printer(exception) + messages = None + + # Done here: + return messages # ***************************************************************************************************************** @@ -631,7 +754,9 @@ if __name__ == "__main__": # label_background_color = "#7a2e0b" # ))) - print(json.to_string(await my_gmail.delete_label(label_id = "Label_6"))) + # print(json.to_string(await my_gmail.delete_label(label_id = "Label_6"))) + + print(json.to_string(await my_gmail.list_messages(count = 10))) asyncio.run(main()) diff --git a/utils_v2/oauth/services/goog.py b/utils_v2/oauth/services/goog.py index 2a66191..88249a3 100644 --- a/utils_v2/oauth/services/goog.py +++ b/utils_v2/oauth/services/goog.py @@ -307,6 +307,7 @@ class GoogleOAuth(OAuthBase): credentials.expiry, timezone = date_time.TIMEZONE_UTC ) + print("EXP. AT:", expires_at) # Return the assessment based on the evaluation with the timezone considered: return True if date_time.get_current_utc_date_time() >= expires_at else False