(20241209) Standardizing the data models for the database. Started with mail authentication.

This commit is contained in:
2024-12-09 12:05:12 +05:30
parent 30088dcea4
commit e2c9200941
11 changed files with 262 additions and 244 deletions
+24 -17
View File
@@ -138,36 +138,38 @@ async def handle_gmail_callback() -> render_template:
# Generate the tokens from the callback. Google sends all the needed params in the callback as the URL's query # Generate the tokens from the callback. Google sends all the needed params in the callback as the URL's query
# params. We can simply use the exact URL that was hit to generate the tokens. In Quart (and Flask) this can be # params. We can simply use the exact URL that was hit to generate the tokens. In Quart (and Flask) this can be
# achieved by 'request.url' like this: # achieved by 'request.url' like this:
tokens = await current_app.gmail_client.get_authorization_tokens( google_tokens = await current_app.gmail_client.get_authorization_tokens(
redirect_url = request.url, redirect_url = request.url,
# scopes = g.inbound_data["scope"].split(" ")
scopes = None scopes = None
) )
if tokens: if google_tokens:
# Get the e-mail id that granted authorization. We will be comparing this to the e-mail id that had been given # Get the e-mail id that granted authorization. We will be comparing this to the e-mail id that had been given
# to us when the authorization was initiated. We don't mind any e-mail id being used, but we need them to be the # to us when the authorization was initiated. We don't mind any e-mail id being used, but we need them to be the
# same at both ends: # same at both ends:
user_profile = await current_app.gmail_client.get_user_profile(tokens = tokens) user_profile = await current_app.gmail_client.get_user_profile(tokens = google_tokens)
if user_profile.success: if user_profile.success:
tokens.email = user_profile.data["emailAddress"] google_tokens.email = user_profile.data["emailAddress"]
tokens.displayName = user_profile.data["displayName"] google_tokens.displayName = user_profile.data["displayName"]
tokens.displayPictureUrl = user_profile.data["displayPictureUrl"] google_tokens.displayPictureUrl = user_profile.data["displayPictureUrl"]
# Here's where we do the checking of the e-mails, # Here's where we do the checking of the e-mails,
# if they don't match, we reject the authorization: # if they don't match, we reject the authorization:
placeholder_token = await current_app.mail_oauth_model.get_token( auth_token = await current_app.mail_oauth_model.get_token(
mongo_conn = current_app.data_mongo, mongo_conn = current_app.data_mongo,
token_id = g.inbound_data["state"] token_id = g.inbound_data["state"]
) )
if ( if (
(not placeholder_token) or (not auth_token) or
placeholder_token["clientUserId"]["email"] != str(tokens.email) auth_token.clientUserId["email"] != str(google_tokens.email)
): return await render_template( ): return await render_template(
"/mail/oauth/oauth_failure_v2.html", "/mail/oauth/oauth_failure_v2.html",
mail_client = g.mail_client.title(), mail_client = g.mail_client.title(),
failure_hint = f"We were expecting authorization from '{placeholder_token['clientUserId']['email']}' but got authorization from '{tokens.email}' instead." failure_hint = (
f"We were expecting authorization from '{auth_token.clientUserId['email']}', "
f"but got authorization from '{google_tokens.email}' instead."
)
) )
# We create standard labels that we will use: # We create standard labels that we will use:
@@ -190,7 +192,7 @@ async def handle_gmail_callback() -> render_template:
] ]
tasks = [ tasks = [
current_app.gmail_client.create_label( current_app.gmail_client.create_label(
tokens = tokens, tokens = google_tokens,
label_name = label["name"], label_name = label["name"],
label_visibility = "labelShow", label_visibility = "labelShow",
message_visibility = "show", message_visibility = "show",
@@ -201,18 +203,20 @@ async def handle_gmail_callback() -> render_template:
client_responses = await asyncio.gather(*tasks) client_responses = await asyncio.gather(*tasks)
# Add the labels to the tokens data: # Add the labels to the tokens data:
client_response = await current_app.gmail_client.list_labels(tokens = tokens) client_response = await current_app.gmail_client.list_labels(tokens = google_tokens)
tokens.labels = client_response.data if client_response.success else None google_tokens.labels = client_response.data if client_response.success else None
# Now that we have passed the check, # Now that we have passed the check,
# we save the tokens to the database: # we save the tokens to the database:
auth_token.clientUserId = google_tokens.client_user_id
auth_token.token = google_tokens.model_dump()
auth_token.status = "active"
tokens_saved = await current_app.mail_oauth_model.set_token( tokens_saved = await current_app.mail_oauth_model.set_token(
db_conn = current_app.sql_writer, db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo, mongo_conn = current_app.data_mongo,
session_token = g.inbound_headers.get("X-Session-Token"), session_token = g.inbound_headers.get("X-Session-Token"),
token_id = g.inbound_data["state"], token_id = g.inbound_data["state"],
client_user_id = tokens.client_user_id, auth_token = auth_token
token = tokens.model_dump()
) )
# Return an HTML response for success: # Return an HTML response for success:
@@ -294,7 +298,10 @@ async def mail_auth_callback(
return await render_template( return await render_template(
"/mail/oauth/oauth_failure_v2.html", "/mail/oauth/oauth_failure_v2.html",
mail_client = mail_client.title(), mail_client = mail_client.title(),
failure_hint = f"Invalid client '{mail_client}' selected. Please use log-id '{g.log_id}' to check with the support team." failure_hint = (
f"Invalid client '{mail_client}' selected. "
"Please use log-id '{g.log_id}' to check with the support team."
)
) )
+14 -11
View File
@@ -71,6 +71,7 @@ from models.data.api.mail.oauth import (
OAuthMailAuthorizationRequestHeaders, OAuthMailAuthorizationRequestHeaders,
OAuthMailAuthorizationRequestData OAuthMailAuthorizationRequestData
) )
from models.data.core.auth_token import CoreAuthTokenModel
# For asynchronous activities: # For asynchronous activities:
import asyncio import asyncio
@@ -169,22 +170,24 @@ async def request_oauth_authorization_url(
# Start by assuming failure: # Start by assuming failure:
auth_url = None auth_url = None
# ┳ •┏ # ┏┓ ┏┳┓
# ┃┏┫┏┓┏┓┓╋┓┏ ┃┃┏┏┓┏┓ # ┃┓┏┓┏┓┏┓┏┓┓╋┏┓ ┃ ┏┓┃┏┏┓┏┓ ┃┏┫
# ┻┗┻┗ ┛┗┗┗┛┗┫ ┗┛┛┗ ┛ # ┗┛┗ ┛┗┗ ┛ ┗┻┗┗ ┻ ┗┛┛┗┗ ┻┗┻
# ┛
# Make a user identifier from the session info: # Make a user identifier from the session info:
token_id = await current_app.mail_oauth_model.get_token_id( token_id = await current_app.mail_oauth_model.get_token_id(
db_conn = current_app.sql_writer, db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo, mongo_conn = current_app.data_mongo,
session_token = inbound_headers["X-Session-Token"], auth_token = CoreAuthTokenModel(
user_info = kwargs["session_info"], serviceType = "email",
client_user_id = {"email": inbound_data.mailId}, client = inbound_data.mailClient,
auth = None, authType = "oauth",
service_client = inbound_data.mailClient, user = kwargs["session_info"],
auth_type = "oauth", clientUserId = {"email": inbound_data.mailId},
sync_freq = inbound_data.syncFreq status = "pending",
syncFreq = inbound_data.syncFreq,
),
session_token = inbound_headers["X-Session-Token"]
) )
if token_id is None: if token_id is None:
return ResponseModel( return ResponseModel(
+4 -1
View File
@@ -111,6 +111,7 @@ def init(blueprint_setup_state):
@test_callback_bp.route("/callback", methods = ["POST", "GET"]) @test_callback_bp.route("/callback", methods = ["POST", "GET"])
@set_api_version(api_version = "1.0.0") @set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False) @read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
@log_request_to_mongo( @log_request_to_mongo(
attr_name = "logs_mongo", attr_name = "logs_mongo",
project = constants.PROJECT_NAME, project = constants.PROJECT_NAME,
@@ -118,7 +119,7 @@ def init(blueprint_setup_state):
operation = "testCllBckApi", operation = "testCllBckApi",
log_input = True, log_input = True,
log_output = True, log_output = True,
sensitive_keys = None sensitive_keys = ["sessionToken", "X-Session-Token"]
) )
@log_chain_to_mongo(attr_name = "logs_mongo") @log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance") @should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@@ -130,6 +131,8 @@ async def callback_test(
**kwargs **kwargs
): ):
print("SESSION INFO:", kwargs.get("session_info"))
# Return a random page: # Return a random page:
return await render_template( return await render_template(
random.choice([ random.choice([
+21 -9
View File
@@ -37,6 +37,9 @@ sys.path.append("..")
# To use Quart: # To use Quart:
from quart import current_app from quart import current_app
# The data model:
from models.data.core.user_info import CoreUserInfoModel
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
@@ -84,18 +87,27 @@ async def get_session(session_token):
""" """
try: try:
# Fetch the raw info from cache:
raw_info = await current_app.module_cache.get(key = session_token) raw_info = await current_app.module_cache.get(key = session_token)
session_info = {
"fullName": raw_info["value"]["full_name"], # Feed needed field into the core model:
"userId": raw_info["value"]["user_id"], session_info = CoreUserInfoModel(
"entityId": raw_info["value"]["entity_id"], fullName = raw_info["value"]["full_name"],
"billingAccountId": raw_info["value"]["billing_account_id"], userId = raw_info["value"]["user_id"],
"departmentId": raw_info["value"]["department_id"], entityId = raw_info["value"]["entity_id"],
"branchId": raw_info["value"]["branch_id"], billingAccountId = raw_info["value"]["billing_account_id"],
"industry": raw_info["value"]["industry"] departmentId = raw_info["value"]["department_id"],
} branchId = raw_info["value"]["branch_id"],
industry = raw_info["value"]["industry"]
)
# Done here:
return session_info return session_info
# In case something goes wrong:
except Exception as exception: except Exception as exception:
current_app.printer(exception)
return None return None
+1 -1
View File
@@ -71,7 +71,7 @@ from utils_v2.api.async_quart import (
from utils_v2.goog.gmail.gmail_client import AsyncGMailClient from utils_v2.goog.gmail.gmail_client import AsyncGMailClient
# Behaviour Models: # Behaviour Models:
from models.behaviour.mail.oauth_v2 import MailOAuthModel from models.behaviour.mail.oauth_v3 import MailOAuthModel
from models.behaviour.mail.sync_v2 import MailSyncModel from models.behaviour.mail.sync_v2 import MailSyncModel
from models.behaviour.mail.retrieve import MailRetrieveModel from models.behaviour.mail.retrieve import MailRetrieveModel
from models.behaviour.sms.auth import SMSAuthModel from models.behaviour.sms.auth import SMSAuthModel
+47 -61
View File
@@ -6,7 +6,8 @@
DATE: DATE:
Monday, 2nd Dec., 2024 ORIGINAL: Monday, 2nd Dec., 2024
UPGRADE: Monday, 9th Dec., 2024
OBJECTIVE: OBJECTIVE:
@@ -44,6 +45,9 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
# Base model: # Base model:
from models.behaviour.base import BaseModel from models.behaviour.base import BaseModel
# Data models:
from models.data.core.auth_token import CoreAuthTokenModel
# To work with MongoDB: # To work with MongoDB:
from bson import ObjectId from bson import ObjectId
@@ -99,12 +103,7 @@ class MailOAuthModel(BaseModel):
self, self,
db_conn: AsyncMySQL, db_conn: AsyncMySQL,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
user_info: dict, auth_token: CoreAuthTokenModel,
client_user_id: dict,
auth: dict,
service_client: Literal["gmail"],
auth_type: Literal["oauth"],
sync_freq: Literal[60, 300, 900] = 300,
session_token: str = None session_token: str = None
) -> ObjectId: ) -> ObjectId:
@@ -113,13 +112,7 @@ class MailOAuthModel(BaseModel):
user requests an authorization URL to link your service to another service (like GMail). user requests an authorization URL to link your service to another service (like GMail).
:param db_conn: The database connection (MariaDB) to use to perform the action. :param db_conn: The database connection (MariaDB) to use to perform the action.
:param mongo_conn: The database connection (MongoDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action.
:param user_info: The dictionary that has the user's session information. :param auth_token: An instance of the core auth-token model that holds data in the database.
:param client_user_id: The way the third-party client recognizes your user.
:param auth: The authentication details of the account.
:param service_client: The name of the company or brand that is providing this service that is being integrated.
:param auth_type: To identify the type of authentication being done here. This could indicate simple password
authentication, more advance OAuth2.0 authentication, etc.
:param sync_freq: The time interval in which mails need to be sync'd. Specify this in seconds.
:param session_token: The session token of the user who requested this service. :param session_token: The session token of the user who requested this service.
:return: An ObjectId to later store the granted tokens. :return: An ObjectId to later store the granted tokens.
""" """
@@ -127,35 +120,36 @@ class MailOAuthModel(BaseModel):
# Note down the timestamp at which this event occurred: # Note down the timestamp at which this event occurred:
request_ts = date_time.get_current_utc_date_time(as_string = False) request_ts = date_time.get_current_utc_date_time(as_string = False)
# Get the identifier from the database: # Get the identifier from the database.
# BE CAREFUL WITH THE KEYS HERE, THEY SHOULD MATCH THE FIELDS OF THE CORE AUTH-TOKEN MODEL:
mongo_json = await mongo_conn.find_one_and_update( mongo_json = await mongo_conn.find_one_and_update(
collection = MailOAuthModel.AUTH_COLLECTION, collection = MailOAuthModel.AUTH_COLLECTION,
filter = mongo_conn.dict_to_dot_notation({ filter = mongo_conn.dict_to_dot_notation({
"serviceType": "email", "serviceType": "email",
"user": { "user": {
"entityId": user_info["entityId"], "entityId": auth_token.user.entityId,
"billingAccountId": user_info["billingAccountId"] "billingAccountId": auth_token.user.billingAccountId
}, },
"clientUserId": client_user_id "clientUserId": auth_token.clientUserId
}), }),
update = { update = {
"$set": { "$set": {
"lastRequestTs": request_ts, "lastRequestTs": request_ts,
"status": "active", "status": auth_token.status,
"syncFreq": max(sync_freq, 60) "syncFreq": max(auth_token.syncFreq, 60)
}, },
"$setOnInsert": { "$setOnInsert": {
"version": "1.1.1", "version": auth_token.version,
"serviceType": "email", "serviceType": auth_token.serviceType,
"client": service_client, "client": auth_token.client,
"authType": auth_type, "authType": auth_token.authType,
"user": user_info, "user": auth_token.user.model_dump(),
"clientUserId": client_user_id, "clientUserId": auth_token.clientUserId,
"auth": auth, "auth": auth_token.auth,
"token": None, "token": auth_token.token,
"firstRefreshTs": None, "firstRefreshTs": auth_token.firstRefreshTs,
"lastRefreshTs": None, "lastRefreshTs": auth_token.lastRefreshTs,
"firstRequestTs": request_ts, "firstRequestTs": auth_token.firstRequestTs,
} }
}, },
projection = { projection = {
@@ -172,15 +166,15 @@ class MailOAuthModel(BaseModel):
db_conn = db_conn, db_conn = db_conn,
proc_name = "entity_integration_save", proc_name = "entity_integration_save",
proc_args = ( proc_args = (
user_info["entityId"], # ............................................ 'p_entity_id' auth_token.user.entityId, # ......................................... 'p_entity_id'
service_client, # ................................................... 'p_provider' auth_token.client, # ................................................ 'p_provider'
"Pending", # ........................................................ 'p_current_status' "Pending", # ........................................................ 'p_current_status'
"Auth Requested", # ................................................. 'p_last_action' "Auth Requested", # ................................................. 'p_last_action'
None, # ............................................................. 'p_display_name' None, # ............................................................. 'p_display_name'
None, # ............................................................. 'p_display_picture' None, # ............................................................. 'p_display_picture'
str(mongo_json["_id"]), # ........................................... 'p_token_id' str(mongo_json["_id"]), # ........................................... 'p_token_id'
json.to_string(python_data = {"email": None}, no_space = True), # ... 'p_notes' json.to_string(python_data = {"email": None}, no_space = True), # ... 'p_notes'
user_info["userId"] # ............................................... 'p_created_by' auth_token.user.userId # ............................................ 'p_created_by'
), ),
session_token = session_token session_token = session_token
) )
@@ -193,8 +187,7 @@ class MailOAuthModel(BaseModel):
db_conn: AsyncMySQL, db_conn: AsyncMySQL,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str, token_id: ObjectId | str,
client_user_id: dict, auth_token: CoreAuthTokenModel,
token: dict,
session_token: str = None session_token: str = None
) -> bool: ) -> bool:
@@ -205,9 +198,7 @@ class MailOAuthModel(BaseModel):
:param db_conn: The database connection (MariaDB) to use to perform the action. :param db_conn: The database connection (MariaDB) to use to perform the action.
:param mongo_conn: The database connection (MongoDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action.
:param token_id: The identifier granted by the 'get_token_id' method. :param token_id: The identifier granted by the 'get_token_id' method.
:param client_user_id: The way the third-party client recognizes your user. These details should match the :param auth_token: The actual auth/token data to be saved to the database.
details furnished while requesting the authorization through 'get_token_id' method.
:param token: The token granted by the third-party service.
:param session_token: The session token of the user who requested this service. :param session_token: The session token of the user who requested this service.
:return: True if saved, False if failed. :return: True if saved, False if failed.
""" """
@@ -218,17 +209,18 @@ class MailOAuthModel(BaseModel):
# Note down the timestamp at which this event occurred: # Note down the timestamp at which this event occurred:
request_ts = date_time.get_current_utc_date_time(as_string = False) request_ts = date_time.get_current_utc_date_time(as_string = False)
# Save the token to MongoDB: # Save the token to MongoDB.
# BE CAREFUL WITH THE KEYS HERE, THEY SHOULD MATCH THE FIELDS OF THE CORE AUTH-TOKEN MODEL:
mongo_json = await mongo_conn.find_one_and_update( mongo_json = await mongo_conn.find_one_and_update(
collection = MailOAuthModel.AUTH_COLLECTION, collection = MailOAuthModel.AUTH_COLLECTION,
filter = mongo_conn.dict_to_dot_notation({ filter = mongo_conn.dict_to_dot_notation({
"_id": ObjectId(token_id), "_id": ObjectId(token_id),
"clientUserId": client_user_id "clientUserId": auth_token.clientUserId
}), }),
update = [{ update = [{
"$set": { "$set": {
"token": token, "token": auth_token.token,
"status": "active", "status": auth_token.status,
"lastRefreshTs": request_ts, "lastRefreshTs": request_ts,
"firstRefreshTs": { "firstRefreshTs": {
"$cond": { "$cond": {
@@ -252,9 +244,9 @@ class MailOAuthModel(BaseModel):
# Tell MariaDB that the token was saved: # Tell MariaDB that the token was saved:
if mongo_json is not None: if mongo_json is not None:
token_notes = { token_notes = {
"email": token["email"], "email": auth_token.token["email"],
"displayName": token.get("displayName"), "displayName": auth_token.token.get("displayName"),
"displayPictureUrl": token.get("displayPictureUrl"), "displayPictureUrl": auth_token.token.get("displayPictureUrl"),
} }
db_json = await self.call_procedure( db_json = await self.call_procedure(
db_conn = db_conn, db_conn = db_conn,
@@ -264,11 +256,11 @@ class MailOAuthModel(BaseModel):
mongo_json["client"], # ......................................... 'p_provider' mongo_json["client"], # ......................................... 'p_provider'
"Active", # ..................................................... 'p_current_status' "Active", # ..................................................... 'p_current_status'
"Auth Granted", # ............................................... 'p_last_action' "Auth Granted", # ............................................... 'p_last_action'
token["displayName"], # ......................................... 'p_display_name' auth_token.token.get("displayName"), # .......................... 'p_display_name'
token["displayPictureUrl"], # ................................... 'p_display_picture' auth_token.token.get("displayPictureUrl"), # .................... 'p_display_picture'
token_id, # ..................................................... 'p_token_id' token_id, # ..................................................... 'p_token_id'
json.to_string(python_data = token_notes, no_space = True), # ... 'p_notes' json.to_string(python_data = token_notes, no_space = True), # ... 'p_notes'
mongo_json["user"]["userId"] # .................................. 'p_created_by' auth_token.user.userId # ........................................ 'p_created_by'
), ),
session_token = session_token session_token = session_token
) )
@@ -282,7 +274,7 @@ class MailOAuthModel(BaseModel):
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str = None, token_id: ObjectId | str = None,
**kwargs **kwargs
) -> dict | None: ) -> CoreAuthTokenModel | None:
""" """
To retrieve stored tokens from the database. To retrieve stored tokens from the database.
@@ -301,21 +293,15 @@ class MailOAuthModel(BaseModel):
# If there is no search criteria, we exit with failure: # If there is no search criteria, we exit with failure:
if not filter_json: return None if not filter_json: return None
# If there is some filtering possible, # If there is some filtering possible, we fetch the token:
# we fetch and return the token: token = await mongo_conn.find_one(
return await mongo_conn.find_one(
collection = self.AUTH_COLLECTION, collection = self.AUTH_COLLECTION,
filter = filter_json, filter = filter_json,
projection = {
"_id": True,
"serviceType": True,
"authType": True,
"client": True,
"clientUserId": True,
"token": True
}
) )
# Done here:
return CoreAuthTokenModel(**token) if token else None
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
@@ -43,6 +43,9 @@ from typing import Optional, Literal, Union
from utils_v2.string import regex from utils_v2.string import regex
from utils_v2.date_time import date_time from utils_v2.date_time import date_time
# Other core models:
from models.data.core.user_info import CoreUserInfoModel
# To work with MongoDB: # To work with MongoDB:
from bson.objectid import ObjectId from bson.objectid import ObjectId
@@ -108,31 +111,41 @@ class CoreAuthTokenModel(BaseModel):
firstRequestTs: AwareDatetime = Field( firstRequestTs: AwareDatetime = Field(
description = "the time (utc) at which authorization was first requested", description = "the time (utc) at which authorization was first requested",
frozen = True frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
) )
lastRequestTs: AwareDatetime = Field( lastRequestTs: AwareDatetime = Field(
description = "the time (utc) at which authorization was last requested", description = "the time (utc) at which authorization was last requested",
frozen = False frozen = False,
default = None
) )
firstRefreshTs: AwareDatetime = Field( firstRefreshTs: AwareDatetime = Field(
description = "the time (utc) at which the tokens were first refreshed", description = "the time (utc) at which the tokens were first refreshed",
frozen = False frozen = False,
default = None
) )
lastRefreshTs: AwareDatetime = Field( lastRefreshTs: AwareDatetime = Field(
description = "the time (utc) at which the tokens were last refreshed", description = "the time (utc) at which the tokens were last refreshed",
frozen = False frozen = False,
default = None
)
auth: dict | None = Field(
description = "any direct auth details like api keys or passwords; will differ for each client",
frozen = True,
default = None
) )
token: dict | None = Field( token: dict | None = Field(
description = "the actual auth tokens of that client; will differ for each client", description = "the actual auth tokens of that client; will differ for each client",
frozen = True, frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False) default = None
) )
user: dict = Field( user: CoreUserInfoModel = Field(
description = "how you identify your user", description = "how you identify your user",
frozen = True frozen = True
) )
@@ -142,6 +155,18 @@ class CoreAuthTokenModel(BaseModel):
frozen = True frozen = True
) )
status: Literal["pending", "active", "disabled"] = Field(
description = "to indicate the status of this account",
frozen = False,
default = "pending"
)
syncFreq: Literal[60, 300, 1500] = Field(
description = "the no. of seconds after which to poll for updates from the client (if applicable)",
frozen = False,
default = 300
)
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫ # ┗┛┗┛┛┗┛┗┗┫
@@ -37,7 +37,7 @@ sys.path.append("..")
# For making data behaviour_models: # For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime
from typing import Optional, Literal, Union from typing import Optional, Literal, Union, List
# My utils: # My utils:
from utils_v2.string import regex from utils_v2.string import regex
@@ -80,6 +80,50 @@ import pycountry
# ***************************************************************************************************************** # *****************************************************************************************************************
class PaymentEvent(BaseModel):
eventTs: AwareDatetime = Field(
description = "to know the date and time (utc) of this update",
frozen = True
)
initByPG: bool = Field(
description = "to figure out whether the payment gateway initiated this event or we did",
frozen = True
)
httpCode: int | None = Field(
description = "the http code generated by the event",
frozen = True,
examples = [200, 400, 401]
)
payload: dict = Field(
description = "the json payload or set of query params received from an event from the payment gateway",
frozen = True
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "allow"
arbitrary_types_allowed = True
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("eventTs", mode = "before")
def parse_date_time(cls, value):
return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)
# ---------------------------------------------------------------------------------------------------------------------
class CorePaymentModel(BaseModel): class CorePaymentModel(BaseModel):
version: str = Field( version: str = Field(
@@ -89,22 +133,22 @@ class CorePaymentModel(BaseModel):
default = "1.0.0" default = "1.0.0"
) )
paymentStatus: Literal["requested", "paid", "rejected"] = Field( paymentStatus: Literal[
"initFailed", # ... When we tried to initiate the request, but the payment gateway (PG) rejected it.
"initiated", # .... When we made a successful payment request, or the customer initiated one from the PG.
"failed", # ....... When the customer tried paying, but it failed (e.g.: because of an incorrect pin).
"rejected", # ..... When the customer explicitly rejected the payment.
"authorized", # ... When the customer made the payment (but it hasn't been settled in your account yet).
"settled", # ...... When the PG sends the money to your account.
"refunded", # ..... When the money was refunded to the client.
] = Field(
description = "the status of the payment request to see what stage of the process we are in", description = "the status of the payment request to see what stage of the process we are in",
frozen = False, frozen = False
default = "requested"
) )
requestTs: AwareDatetime = Field( lastEventTs: AwareDatetime = Field(
description = "the time (utc) at which the payment request was initiated", description = "the time (utc) at which the latest payment event occurred",
frozen = True, frozen = True
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
)
responseTs: AwareDatetime | None = Field(
description = "the time (utc) at which the payer responded to the payment request",
frozen = False,
default = None
) )
tokenId: ObjectId = Field( tokenId: ObjectId = Field(
@@ -122,7 +166,7 @@ class CorePaymentModel(BaseModel):
examples = ["INR", "USD", "KES"] examples = ["INR", "USD", "KES"]
) )
metadata: dict = Field( metadata: dict | None = Field(
description = "any arbitrary amount of data to identify the user and payment details", description = "any arbitrary amount of data to identify the user and payment details",
frozen = True frozen = True
) )
@@ -138,28 +182,9 @@ class CorePaymentModel(BaseModel):
default = None default = None
) )
clientPaymentRequestHttpCode: int | str = Field( events: List[PaymentEvent] = Field(
description = "the http code the third-party client returned when you requested the payment", description = "an array of all the events that happened in the process of this payment",
frozen = False, frozen = False
default = None
)
clientPaymentRequestJSON: str | int = Field(
description = "how the third-party client responded when you requested the payment",
frozen = False,
default = None
)
clientPaymentResponseHttpCode: int | str = Field(
description = "the http code the third-party client returned when your user responded to the payment request",
frozen = False,
default = None
)
clientPaymentResponseJSON: str | int | None = Field(
description = "how the third-party client responded when your user responded to the payment request",
frozen = False,
default = None
) )
# ┏┓ ┏• # ┏┓ ┏•
@@ -175,7 +200,7 @@ class CorePaymentModel(BaseModel):
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("requestTs", "responseTs", mode = "before") @field_validator("lastEventTs", mode = "before")
def parse_date_time(cls, value): def parse_date_time(cls, value):
return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC) return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)
@@ -203,16 +228,42 @@ if __name__ == "__main__":
from utils_v2.string import json from utils_v2.string import json
now = date_time.get_current_utc_date_time(as_string = False)
payment = CorePaymentModel( payment = CorePaymentModel(
paymentStatus = "requested", paymentStatus = "authorized",
tokenId = "67519cf3a7804fcbc6f12452", tokenId = "67519cf3a7804fcbc6f12452",
amount = 1.00, amount = 1.00,
currencyCode = "INR", currencyCode = "INR",
metadata = { metadata = {
"userId": 1, "userId": 1,
"name": "My Test" "name": "Bhopli"
}, },
client = "safaricomMPesaExpress" client = "razorpay",
clientPaymentReferenceId = "txn_123_abc",
lastEventTs = now,
events = [
PaymentEvent(
eventTs = now - datetime.timedelta(minutes = 1, seconds = 12),
initByPG = True,
httpCode = None,
payload = {
"status": "captured",
"from": "Barfi",
}
),
PaymentEvent(
eventTs = now,
initByPG = True,
httpCode = None,
payload = {
"status": "authorized",
"from": "Barfi",
"amount": -100.00,
"description": "meow"
}
)
]
) )
print("PAYMENT TXN. MODEL:", json.to_string(payment.model_dump(), default = str)) print("PAYMENT TXN. MODEL:", json.to_string(payment.model_dump(), default = str))
+28 -97
View File
@@ -6,11 +6,11 @@
DATE: DATE:
Saturday, 7th Dec., 2024. Monday, 9th Dec., 2024.
OBJECTIVE: OBJECTIVE:
To define how auth tokens will be stored in the database. To define how user info will be stored in the database.
REFERENCES: REFERENCES:
@@ -77,7 +77,7 @@ import datetime
# ***************************************************************************************************************** # *****************************************************************************************************************
class CoreAuthTokenModel(BaseModel): class CoreUserInfoModel(BaseModel):
version: str = Field( version: str = Field(
description = "a hint about the version no. of this message", description = "a hint about the version no. of this message",
@@ -86,72 +86,40 @@ class CoreAuthTokenModel(BaseModel):
default = "1.0.0" default = "1.0.0"
) )
serviceType: Literal["email", "sms", "chat"] = Field( fullName: str | None = Field(
description = "the kind of service this message was sent/received from", description = "the full name of the user as found in the database",
frozen = True
)
client: Literal[
"gmail", "outlook", # ...................... Mail Clients
"telegram", "whatsapp", # .................. Chat Clients
"nimbusSmsIndia", "savvyBulkSmsKenya", # ... SMS Clients
"razorpay", "safaricomMPesaExpress" # ...... Payment Gateways
] = Field(
description = "the third-part client that was used",
frozen = True
)
authType: Literal["oauth", "auth"] = Field(
description = "the type of authentication procedure used",
frozen = True
)
firstRequestTs: AwareDatetime = Field(
description = "the time (utc) at which authorization was first requested",
frozen = True
)
lastRequestTs: AwareDatetime = Field(
description = "the time (utc) at which authorization was last requested",
frozen = False
)
firstRefreshTs: AwareDatetime = Field(
description = "the time (utc) at which the tokens were first refreshed",
frozen = False
)
lastRefreshTs: AwareDatetime = Field(
description = "the time (utc) at which the tokens were last refreshed",
frozen = False
)
token: dict | None = Field(
description = "the actual auth tokens of that client; will differ for each client",
frozen = True, frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False) examples = ["Bhopli Narangi"]
) )
user: dict = Field( userId: int | str | None = Field(
description = "how you identify your user", description = "the id of the user as found in the database",
frozen = True frozen = True
) )
clientUserId: dict = Field( entityId: int | str | None = Field(
description = "how third-party client identifies the same user", description = "the id of the entity with which this user is associated",
frozen = True frozen = True
) )
status: Literal["active", "disabled"] = Field( billingAccountId: int | str | None = Field(
description = "to indicate the status of this account", description = "the id of the billing account with which this user is associated",
frozen = False, frozen = True
default = "active"
) )
syncFreq: Literal[60, 300, 1500] = Field( departmentId: int | str | None = Field(
description = "the no. of seconds after which to poll for updates from the client (if applicable)", description = "the id of the dept. in which this user is working",
frozen = False, frozen = True
default = 300 )
branchId: int | str | None = Field(
description = "the id of the branch in which this user is working",
frozen = True
)
industry: str | None = Field(
description = "the name of the industry this user is working in",
frozen = True
) )
# ┏┓ ┏• # ┏┓ ┏•
@@ -160,21 +128,9 @@ class CoreAuthTokenModel(BaseModel):
# ┛ # ┛
class Config: class Config:
extra = "allow" extra = "ignore"
arbitrary_types_allowed = True arbitrary_types_allowed = True
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator(
"firstRequestTs",
"lastRequestTs", "firstRefreshTs", "lastRefreshTs",
mode = "before"
)
def parse_date_time(cls, value):
return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
@@ -184,29 +140,4 @@ class CoreAuthTokenModel(BaseModel):
if __name__ == "__main__": if __name__ == "__main__":
from utils_v2.string import json pass
auth_token = CoreAuthTokenModel(
serviceType = "email",
client = "gmail",
authType = "oauth",
firstRequestTs = date_time.get_current_utc_date_time(as_string = False),
lastRequestTs = date_time.get_current_utc_date_time(as_string = False),
firstRefreshTs = date_time.get_current_utc_date_time(as_string = False),
lastRefreshTs = date_time.get_current_utc_date_time(as_string = False),
token = {
"username": "testing123",
"password": "abcdefgh"
},
user = {
"userId": 0,
"entityId": 1,
"billingAccountId": 2,
"fullName": "Bhopli"
},
clientUserId = {
"email": "bhopli@gmail.com"
}
)
print("AUTH-TOKEN MODEL:", json.to_string(auth_token.model_dump(), default = str))