Files

253 lines
9.6 KiB
Python

"""
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
from models.finstitutions.trading.oauth import TradingOAuthCallbackResponse
# 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 <b>'{g.log_id}'</b> to check with the support team."
])
)
# ---------------------------------------------------------------------------------------------------------------------
@trading_oauth_callback_bp.route("/callback/<trading_client>", methods = ["GET", "POST"])
@trading_oauth_callback_bp.route("/callback/<trading_client>/<client_user_id>", 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 = TradingOAuthCallbackResponse(message = "")
# ┳┓ ┓ ┏┓ ┓ •
# ┣┫┏┓┏┓┃┏┏┓┏┓ ┗┓┏┓┃┏┓┏╋┓┏┓┏┓
# ┻┛┛ ┗┛┛┗┗ ┛ ┗┛┗ ┗┗ ┗┗┗┗┛┛┗
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 = (
"<br><br><b>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.</b>"
)
# 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 <b>'{g.log_id}'</b> 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 <b>'{g.log_id}'</b> to check with the support team."
])
)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass