""" AUTHOR: Khushal P Soonderji DATE: Saturday, 21st Dec., 2024 OBJECTIVE: To receive callbacks (webhooks) from stockbrokers for trading API integrations. REFERENCES: N/A DOWNLOADS: N/A NOTES: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # For using Quart: from quart import Blueprint, current_app, g, request, render_template # My utils: from utils_v2.string import json from utils_v2.logging.context import AsyncLoggerContext from utils_v2.api.codes import StatusCodes, HttpCodes from utils_v2.api.response import ResponseModel from utils_v2.api.async_quart import ( set_api_version, read_input, get_session_info, log_request_to_mongo, log_chain_to_mongo, should_not_be_under_maintenance, only_whitelisted_ips, limit_rate, validate_input, handle_cancelled_request, handle_failed_request ) # Data Models: from models.core.auth_token import CoreAuthTokenModel # Common: from shared import constants # For asynchronous activities: import asyncio # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # Related to Quart: trading_oauth_callback_bp = Blueprint("trading_oauth_cb", __name__) # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** @trading_oauth_callback_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. # Consider this to be a one-time setup for the whole blueprint: pass # --------------------------------------------------------------------------------------------------------------------- async def handle_auth_exception(): """ Use this to handle any exceptions that occur in the process of accepting authorization details. Zero's direct library, for instance, raise several exceptions for cases like expired tokens, checksum failures, etc. :return: A web-view that indicates failure. """ return await render_template( "/finstitutions/trading/oauth/oauth_failure_v2.html", client = g.client_label, failure_hint = " ".join([ f"Something went wrong (E).", g.client_response.message, f"Please use log-id '{g.log_id}' to check with the support team." ]) ) # --------------------------------------------------------------------------------------------------------------------- @trading_oauth_callback_bp.route("/callback/", methods = ["GET", "POST"]) @trading_oauth_callback_bp.route("/callback//", methods = ["GET", "POST"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @log_request_to_mongo( attr_name = "logs_mongo", project = constants.PROJECT_NAME, log_type = constants.MODULE_NAME, operation = "trdngOauthCllBckApi", log_input = True, log_output = True, sensitive_keys = None ) @log_chain_to_mongo(attr_name = "logs_mongo") @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @handle_cancelled_request() @handle_failed_request(cleanup_coro = handle_auth_exception) async def trading_oauth_callback( trading_client: str = None, client_user_id: str = None, inbound_headers: dict = None, inbound_data: dict = None, inbound_files: dict = None, **kwargs ): """ This endpoint gets triggered by the stockbroker's servers to let you know when a user accepted or rejected an authorization request. :param trading_client: The name of the stockbroker that you have received the callback from. :param client_user_id: How the trading client identifies this user. :param inbound_headers: auto-extracted by the decorators. :param inbound_data: auto-extracted by the decorators. :param inbound_files: auto-extracted by the decorators. :param kwargs: Any number of extra inputs supplied by the decorators. :return: A standard response structure. """ # ┓┏ ┓┓ ┓┏ • ┓ ┓ # ┣┫┏┓┏┓┏┫┃┏┓ ┃┃┏┓┏┓┓┏┓┣┓┃┏┓┏ # ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┛ ┗┗┻┗┛┗┗ ┛ # Store needed values in 'g': g.log_id = kwargs.get("log_id") g.client_label_map = { "zerodhaKite": "Zerodha (Kite)", "iciciBreeze": "ICICI (Breeze)" } # Start by assuming failure: g.client_response = None # ┳┓ ┓ ┏┓ ┓ • # ┣┫┏┓┏┓┃┏┏┓┏┓ ┗┓┏┓┃┏┓┏╋┓┏┓┏┓ # ┻┛┛ ┗┛┛┗┗ ┛ ┗┛┗ ┗┗ ┗┗┗┗┛┛┗ g.client_label = g.client_label_map.get(trading_client, trading_client) match trading_client: case "zerodhaKite": client_controller = current_app.zerodha_kite_controller case "iciciBreeze": client_controller = current_app.icici_breeze_controller case _: client_controller = None if client_controller is not None: g.client_response = await client_controller.handle_authorization_callback( sql_conn = current_app.sql_writer, mongo_data_conn = current_app.data_mongo, inbound_data = inbound_data, client_user_id = client_user_id ) # ┳┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ # ┛┗┗ ┛┣┛┗┛┛┗┛┗ # ┛ # Prepare a set of warning messages: callback_url_upgrade_warning = ( "

WARNING: You are using the old callback system which could be discontinued at any time. " "Please update the callback URL on your broker's portal to include your user id in it." ) # No valid client: if client_controller is None: return await render_template( "/finstitutions/trading/oauth/oauth_failure_v2.html", failure_hint = " ".join([ f"Invalid client '{g.client_label}' selected.", f"Please use log-id '{g.log_id}' to check with the support team." ]) ) # Successful auth: if g.client_response.success: return await render_template( "/finstitutions/trading/oauth/oauth_success_v2.html", client = g.client_label, extra_message = callback_url_upgrade_warning if client_user_id is None else "" ) # Failed auth: if g.client_response.success is False: return await render_template( "/finstitutions/trading/oauth/oauth_failure_v2.html", failure_hint = " ".join([ g.client_response.message, f"Please use log-id '{g.log_id}' to check with the support team." ]) ) # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass