""" AUTHOR: Khushal P Soonderji DATE: Wednesday, 30th Oct., 2024. OBJECTIVE: To provide a data model for describing the tokens to be used for Google's APIs. 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, AwareDatetime from typing import Optional, Literal, Union, Dict, List, Any # Related to Google: from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials # My utils: from utils_v2.string import json from utils_v2.string import regex from utils_v2.date_time import date_time # To work with date and time: import datetime import dateparser # To make API calls: import httpx # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** class GoogleAuthTokens(BaseModel): accessToken: str = Field(description = "the main 'bearer' token") refreshToken: str = Field(description = "token to be used to refresh the access token") expiresAt: AwareDatetime = Field(description = "the time (utc) at which the token will expire") scopes: List[str] = Field(description = "the list of permissions", default = []) email: str | None = Field(description = "the email id of the user", default = None) # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ # ┛ class Config: extra = "allow" # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ @field_validator("expiresAt", mode = "before") def parse_dates(cls, value): if not isinstance(value, datetime.datetime): parsed = date_time.parse_date_time(value, date_formats = ["%Y%m%d", "%Y-%m-%d"]) value = parsed if isinstance(parsed, datetime.datetime) else dateparser.parse(value) if isinstance(value, datetime.datetime): value = date_time.to_timezone(value, date_time.TIMEZONE_UTC) return value # ┏┓ • # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏ # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛ # ┛ @property def expired(self): return True if date_time.get_current_utc_date_time() >= self.expiresAt else False @property def ttl(self): return (self.expiresAt - date_time.get_current_utc_date_time()).total_seconds() # ┏┓ ┏┓ # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ def refresh( self, client_id: str, client_secret: str, force_refresh: bool = False ) -> bool: """ Synchronously (blocking) refreshes the existing access tokens in place. :param client_id: The id of the client (OAuth JSON) for which these tokens were granted. :param client_secret: The secret of the client (OAuth JSON) for which these tokens were granted. :param force_refresh: Whether you want to refresh the token even if it hasn't expired. :return: True if refreshed, else False. """ # Start by assuming failure: success = False try: # Go ahead only if either the token has expired, # or the user has asked to forcefully refresh the tokens: if self.expired or force_refresh: # Create the credentials: credentials = Credentials.from_authorized_user_info( info = { "client_id": client_id, "client_secret": client_secret, "refresh_token": self.refreshToken, "expires_at": self.expiresAt } ) # Request a refresh: credentials.refresh(Request()) # Note down the new credentials: if credentials.token != self.accessToken: success = True self.accessToken = credentials.token self.refreshToken = credentials.refresh_token self.expiresAt = date_time.as_if_timezone(credentials.expiry, timezone = date_time.TIMEZONE_UTC) # In case something goes wrong: except Exception as exception: success = False # Done here: return success async def arefresh( self, http_client: httpx.AsyncClient, client_id: str, client_secret: str, force_refresh: bool = False ) -> bool: """ Asynchronously refreshes the existing access tokens in place. :param http_client: The HTTP client to use to make the refresh request. :param client_id: The id of the client (OAuth JSON) for which these tokens were granted. :param client_secret: The secret of the client (OAuth JSON) for which these tokens were granted. :param force_refresh: Whether you want to refresh the token even if it hasn't expired. :return: True if refreshed, else False. """ # Currently we don't really know how to refresh tokens through low-level API calls, # so we will pass on the intent to the regular, synchronous function. return self.refresh( client_id = client_id, client_secret = client_secret, force_refresh = force_refresh ) def has_scopes(self, scopes: List[str]) -> bool: """ Checks if all the specified scopes were granted. :param scopes: The list of scopes to check. These are the permissions you need. :return: True if all specified scoped are present, else False. """ # Start by assuming success: has_scopes = True # Now loop through the needed scopes and check: for scope in scopes: if scope not in self.scopes: has_scopes = False break # Done here: return has_scopes # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass