(20250104) Breeze authorization will be accepted now.
This commit is contained in:
@@ -61,9 +61,6 @@ from utils_v2.api.async_quart import (
|
|||||||
handle_failed_request
|
handle_failed_request
|
||||||
)
|
)
|
||||||
|
|
||||||
# GMail-related utils:
|
|
||||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
|
||||||
|
|
||||||
# Data Models:
|
# Data Models:
|
||||||
from models.core.auth_token import CoreAuthTokenModel
|
from models.core.auth_token import CoreAuthTokenModel
|
||||||
|
|
||||||
@@ -124,17 +121,19 @@ async def handle_auth_exception():
|
|||||||
return await render_template(
|
return await render_template(
|
||||||
"/finstitutions/trading/oauth/oauth_failure_v2.html",
|
"/finstitutions/trading/oauth/oauth_failure_v2.html",
|
||||||
client = g.client_label,
|
client = g.client_label,
|
||||||
failure_hint = (
|
failure_hint = " ".join([
|
||||||
f"Something went wrong (E). "
|
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."
|
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 = ["POST", "GET"])
|
@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")
|
@set_api_version(api_version = "1.0.0")
|
||||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||||
@log_request_to_mongo(
|
@log_request_to_mongo(
|
||||||
@@ -152,6 +151,7 @@ async def handle_auth_exception():
|
|||||||
@handle_failed_request(cleanup_coro = handle_auth_exception)
|
@handle_failed_request(cleanup_coro = handle_auth_exception)
|
||||||
async def trading_oauth_callback(
|
async def trading_oauth_callback(
|
||||||
trading_client: str = None,
|
trading_client: str = None,
|
||||||
|
client_user_id: str = None,
|
||||||
inbound_headers: dict = None,
|
inbound_headers: dict = None,
|
||||||
inbound_data: dict = None,
|
inbound_data: dict = None,
|
||||||
inbound_files: dict = None,
|
inbound_files: dict = None,
|
||||||
@@ -162,6 +162,7 @@ async def trading_oauth_callback(
|
|||||||
This endpoint gets triggered by the stockbroker's servers to let you know when a user accepted or rejected an
|
This endpoint gets triggered by the stockbroker's servers to let you know when a user accepted or rejected an
|
||||||
authorization request.
|
authorization request.
|
||||||
:param trading_client: The name of the stockbroker that you have received the callback from.
|
: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_headers: auto-extracted by the decorators.
|
||||||
:param inbound_data: auto-extracted by the decorators.
|
:param inbound_data: auto-extracted by the decorators.
|
||||||
:param inbound_files: auto-extracted by the decorators.
|
:param inbound_files: auto-extracted by the decorators.
|
||||||
@@ -174,20 +175,31 @@ async def trading_oauth_callback(
|
|||||||
|
|
||||||
# Store needed values in 'g':
|
# Store needed values in 'g':
|
||||||
g.log_id = kwargs.get("log_id")
|
g.log_id = kwargs.get("log_id")
|
||||||
g.client_label = "Zerodha (Kite)"
|
g.client_label_map = {
|
||||||
|
"zerodhaKite": "Zerodha (Kite)",
|
||||||
|
"iciciBreeze": "ICICI (Breeze)"
|
||||||
|
}
|
||||||
|
|
||||||
# Start by assuming failure:
|
# Start by assuming failure:
|
||||||
success = None
|
g.client_response = None
|
||||||
|
|
||||||
# ┏┓ ┏┓ ┓┓ ┓┏┓•
|
# ┳┓ ┓ ┏┓ ┓ •
|
||||||
# ┣ ┏┓┏┓ ┏┛┏┓┏┓┏┓┏┫┣┓┏┓ ┃┫ ┓╋┏┓
|
# ┣┫┏┓┏┓┃┏┏┓┏┓ ┗┓┏┓┃┏┓┏╋┓┏┓┏┓
|
||||||
# ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗
|
# ┻┛┛ ┗┛┛┗┗ ┛ ┗┛┗ ┗┗ ┗┗┗┗┛┛┗
|
||||||
|
|
||||||
if trading_client == "zerodha":
|
g.client_label = g.client_label_map.get(trading_client, trading_client)
|
||||||
success = await current_app.zerodha_kite_controller.handle_authorization_callback(
|
|
||||||
|
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,
|
sql_conn = current_app.sql_writer,
|
||||||
mongo_data_conn = current_app.data_mongo,
|
mongo_data_conn = current_app.data_mongo,
|
||||||
inbound_data = inbound_data
|
inbound_data = inbound_data,
|
||||||
|
client_user_id = client_user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# ┳┓
|
# ┳┓
|
||||||
@@ -195,30 +207,35 @@ async def trading_oauth_callback(
|
|||||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||||
# ┛
|
# ┛
|
||||||
|
|
||||||
# No valid client:
|
# Prepare a set of warning messages:
|
||||||
if success is None: return await render_template(
|
callback_url_upgrade_warning = (
|
||||||
"/finstitutions/trading/oauth/oauth_failure_v2.html",
|
"<br><br><b>WARNING: You are using the old callback system which could be discontinued at any time. "
|
||||||
client = g.client_label,
|
"Please update the callback URL on your broker's portal to include your user id in it.</b>"
|
||||||
failure_hint = (
|
|
||||||
f"Invalid client '{g.client_label}' selected. "
|
|
||||||
f"Please use log-id '{g.log_id}' to check with the support team."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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:
|
# Successful auth:
|
||||||
if success: return await render_template(
|
if g.client_response.success: return await render_template(
|
||||||
"/finstitutions/trading/oauth/oauth_success_v2.html",
|
"/finstitutions/trading/oauth/oauth_success_v2.html",
|
||||||
client = g.client_label
|
client = g.client_label,
|
||||||
|
extra_message = callback_url_upgrade_warning if client_user_id is None else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Failed auth:
|
# Failed auth:
|
||||||
if success is None: return await render_template(
|
if g.client_response.success is False: return await render_template(
|
||||||
"/finstitutions/trading/oauth/oauth_failure_v2.html",
|
"/finstitutions/trading/oauth/oauth_failure_v2.html",
|
||||||
client = g.client_label,
|
failure_hint = " ".join([
|
||||||
failure_hint = (
|
g.client_response.message,
|
||||||
f"Something went wrong (NE). "
|
f"Please use log-id <b>'{g.log_id}'</b> to check with the support team."
|
||||||
f"Please use log-id '{g.log_id}' to check with the support team."
|
])
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ async def request_oauth_authorization_url(
|
|||||||
# ┻ ┗┛┛ ┣┛┗┻┣┛┗ ┛ ┻ ┛ ┗┻┗┻┗┛┗┗┫
|
# ┻ ┗┛┛ ┣┛┗┻┣┛┗ ┛ ┻ ┛ ┗┻┗┻┗┛┗┗┫
|
||||||
# ┛ ┛
|
# ┛ ┛
|
||||||
|
|
||||||
|
# This is an internal paper-trading account.
|
||||||
|
# It won't need daily logins for usage.
|
||||||
|
|
||||||
if inbound_data.client == "paperTrading":
|
if inbound_data.client == "paperTrading":
|
||||||
|
|
||||||
# Immediately save the details against that token id:
|
# Immediately save the details against that token id:
|
||||||
@@ -187,7 +190,7 @@ async def request_oauth_authorization_url(
|
|||||||
},
|
},
|
||||||
auth = inbound_data.auth.model_dump(),
|
auth = inbound_data.auth.model_dump(),
|
||||||
status = "active",
|
status = "active",
|
||||||
syncFreq = 1500
|
syncFreq = None
|
||||||
),
|
),
|
||||||
token_notes = {
|
token_notes = {
|
||||||
"username": inbound_data.auth.username
|
"username": inbound_data.auth.username
|
||||||
@@ -205,7 +208,7 @@ async def request_oauth_authorization_url(
|
|||||||
# ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗
|
# ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗
|
||||||
|
|
||||||
# PLANNED FLOW FOR ZERODHA-KITE:
|
# PLANNED FLOW FOR ZERODHA-KITE:
|
||||||
# Step 01.: (One time) The user will go to the integrations page and add his API Key and SPI Secret there. We store
|
# Step 01.: (One time) The user will go to the integrations page and add his API Key and API Secret there. We store
|
||||||
# these values without verification.
|
# these values without verification.
|
||||||
# Step 02.: (Daily) The user will go to the investments tab and click on his Zerodha account, which will give him
|
# Step 02.: (Daily) The user will go to the investments tab and click on his Zerodha account, which will give him
|
||||||
# a URL that will take him to Zerodha's official site to log in. When he logs in, Zerodha will hit our
|
# a URL that will take him to Zerodha's official site to log in. When he logs in, Zerodha will hit our
|
||||||
@@ -226,17 +229,19 @@ async def request_oauth_authorization_url(
|
|||||||
authType = "oauth",
|
authType = "oauth",
|
||||||
user = kwargs["session_info"],
|
user = kwargs["session_info"],
|
||||||
clientUserId = {
|
clientUserId = {
|
||||||
|
"userId": inbound_data.auth.userId,
|
||||||
"apiKey": inbound_data.auth.apiKey
|
"apiKey": inbound_data.auth.apiKey
|
||||||
},
|
},
|
||||||
auth = inbound_data.auth.model_dump(),
|
auth = inbound_data.auth.model_dump(),
|
||||||
status = "active",
|
status = "active",
|
||||||
syncFreq = 1500
|
syncFreq = None
|
||||||
),
|
),
|
||||||
token_notes = {
|
token_notes = {
|
||||||
|
"userId": inbound_data.auth.userId,
|
||||||
"apiKey": inbound_data.auth.apiKey,
|
"apiKey": inbound_data.auth.apiKey,
|
||||||
"authUrl": auth_url
|
"authUrl": auth_url
|
||||||
},
|
},
|
||||||
display_name = None,
|
display_name = inbound_data.auth.userId,
|
||||||
display_picture = None,
|
display_picture = None,
|
||||||
session_token = inbound_headers["X-Session-Token"]
|
session_token = inbound_headers["X-Session-Token"]
|
||||||
)
|
)
|
||||||
@@ -248,6 +253,13 @@ async def request_oauth_authorization_url(
|
|||||||
# ┣ ┏┓┏┓ ┃┃ ┃┃ ┃ ┣┫┏┓┏┓┏┓┓┏┓
|
# ┣ ┏┓┏┓ ┃┃ ┃┃ ┃ ┣┫┏┓┏┓┏┓┓┏┓
|
||||||
# ┻ ┗┛┛ ┻┗┛┻┗┛┻ ┻┛┛ ┗ ┗ ┗┗
|
# ┻ ┗┛┛ ┻┗┛┻┗┛┻ ┻┛┛ ┗ ┗ ┗┗
|
||||||
|
|
||||||
|
# PLANNED FLOW FOR ICICI-BREEZE:
|
||||||
|
# Step 01.: (One time) The user will go to the integrations page and add his Client User ID, API Key, and API Secret
|
||||||
|
# there. We store these values without verification.
|
||||||
|
# Step 02.: (Daily) The user will go to the investments tab and click on his Breeze account, which will give him a
|
||||||
|
# URL that will take him to ICICI's official site to log in. When he logs in, ICICI will hit our callback
|
||||||
|
# URL and give us the authentication details.
|
||||||
|
|
||||||
if inbound_data.client == "iciciBreeze":
|
if inbound_data.client == "iciciBreeze":
|
||||||
|
|
||||||
# Prepare the inputs:
|
# Prepare the inputs:
|
||||||
@@ -263,17 +275,19 @@ async def request_oauth_authorization_url(
|
|||||||
authType = "oauth",
|
authType = "oauth",
|
||||||
user = kwargs["session_info"],
|
user = kwargs["session_info"],
|
||||||
clientUserId = {
|
clientUserId = {
|
||||||
|
"userId": inbound_data.auth.userId,
|
||||||
"apiKey": inbound_data.auth.apiKey
|
"apiKey": inbound_data.auth.apiKey
|
||||||
},
|
},
|
||||||
auth = inbound_data.auth.model_dump(),
|
auth = inbound_data.auth.model_dump(),
|
||||||
status = "active",
|
status = "active",
|
||||||
syncFreq = 1500
|
syncFreq = None
|
||||||
),
|
),
|
||||||
token_notes = {
|
token_notes = {
|
||||||
|
"userId": inbound_data.auth.userId,
|
||||||
"apiKey": inbound_data.auth.apiKey,
|
"apiKey": inbound_data.auth.apiKey,
|
||||||
"authUrl": auth_url
|
"authUrl": auth_url
|
||||||
},
|
},
|
||||||
display_name = None,
|
display_name = inbound_data.auth.userId,
|
||||||
display_picture = None,
|
display_picture = None,
|
||||||
session_token = inbound_headers["X-Session-Token"]
|
session_token = inbound_headers["X-Session-Token"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from controllers_v2.finstitutions.trading.base import TradingController
|
|||||||
# Models:
|
# Models:
|
||||||
from models.core.auth_token import CoreAuthTokenModel
|
from models.core.auth_token import CoreAuthTokenModel
|
||||||
from models.api.finstitutions.trading.symbols.list import TradingSymbolListRequestData, TradingSymbolListBrokerResponse
|
from models.api.finstitutions.trading.symbols.list import TradingSymbolListRequestData, TradingSymbolListBrokerResponse
|
||||||
|
from models.finstitutions.trading.oauth import TradingOAuthCallbackResponse
|
||||||
|
|
||||||
# To work with datatypes:
|
# To work with datatypes:
|
||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
@@ -150,8 +151,9 @@ class AllTradingController(TradingController):
|
|||||||
self,
|
self,
|
||||||
sql_conn: AsyncMySQL,
|
sql_conn: AsyncMySQL,
|
||||||
mongo_data_conn: AsyncMongo,
|
mongo_data_conn: AsyncMongo,
|
||||||
inbound_data: dict
|
inbound_data: dict,
|
||||||
) -> bool:
|
client_user_id: str
|
||||||
|
) -> TradingOAuthCallbackResponse:
|
||||||
"""
|
"""
|
||||||
When the end user interacts with their broker's APIs, the broker's servers would usually issue a callback.
|
When the end user interacts with their broker's APIs, the broker's servers would usually issue a callback.
|
||||||
We've seen this in the case of Zerodha Kite and ICICI Breeze. Use this method to handle the callback loop to
|
We've seen this in the case of Zerodha Kite and ICICI Breeze. Use this method to handle the callback loop to
|
||||||
@@ -159,7 +161,8 @@ class AllTradingController(TradingController):
|
|||||||
:param sql_conn: The database connection to use to perform this activity.
|
:param sql_conn: The database connection to use to perform this activity.
|
||||||
:param mongo_data_conn: The database connection to use to perform this activity.
|
:param mongo_data_conn: The database connection to use to perform this activity.
|
||||||
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
||||||
:return: True if the callback loop was completed successfully, else False.
|
:param client_user_id: How the trading client identifies this user.
|
||||||
|
:return: A structured response to capture the process of callback handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from controllers_v2.core.auth_token import CoreAuthTokenController
|
|||||||
# Models:
|
# Models:
|
||||||
from models.core.auth_token import CoreAuthTokenModel
|
from models.core.auth_token import CoreAuthTokenModel
|
||||||
from models.api.finstitutions.trading.symbols.list import TradingSymbolListRequestData, TradingSymbolListBrokerResponse
|
from models.api.finstitutions.trading.symbols.list import TradingSymbolListRequestData, TradingSymbolListBrokerResponse
|
||||||
|
from models.finstitutions.trading.oauth import TradingOAuthCallbackResponse
|
||||||
|
|
||||||
# To work with datatypes:
|
# To work with datatypes:
|
||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
@@ -170,8 +171,9 @@ class TradingController(CoreAuthTokenController, ABC):
|
|||||||
self,
|
self,
|
||||||
sql_conn: AsyncMySQL,
|
sql_conn: AsyncMySQL,
|
||||||
mongo_data_conn: AsyncMongo,
|
mongo_data_conn: AsyncMongo,
|
||||||
inbound_data: dict
|
inbound_data: dict,
|
||||||
) -> bool:
|
client_user_id: str
|
||||||
|
) -> TradingOAuthCallbackResponse:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
When the end user interacts with their broker's APIs, the broker's servers would usually issue a callback.
|
When the end user interacts with their broker's APIs, the broker's servers would usually issue a callback.
|
||||||
@@ -180,7 +182,8 @@ class TradingController(CoreAuthTokenController, ABC):
|
|||||||
:param sql_conn: The database connection to use to perform this activity.
|
:param sql_conn: The database connection to use to perform this activity.
|
||||||
:param mongo_data_conn: The database connection to use to perform this activity.
|
:param mongo_data_conn: The database connection to use to perform this activity.
|
||||||
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
||||||
:return: True if the callback loop was completed successfully, else False.
|
:param client_user_id: How the trading client identifies this user.
|
||||||
|
:return: A structured response to capture the process of callback handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from models.api.finstitutions.trading.symbols.list import (
|
|||||||
TradingSymbolListBrokerResponse,
|
TradingSymbolListBrokerResponse,
|
||||||
TradingSymbol
|
TradingSymbol
|
||||||
)
|
)
|
||||||
|
from models.finstitutions.trading.oauth import TradingOAuthCallbackResponse
|
||||||
|
|
||||||
# To work with MongoDB:
|
# To work with MongoDB:
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
@@ -62,8 +63,8 @@ from typing import List, Any
|
|||||||
import httpx
|
import httpx
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
# To work with Zerodha's Kite platform:
|
# To work with ICICI Breeze's platform:
|
||||||
from kiteconnect import KiteConnect
|
from breeze_connect import BreezeConnect
|
||||||
|
|
||||||
# To handle exceptions:
|
# To handle exceptions:
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@@ -179,8 +180,9 @@ class ICICIBreezeTradingController(TradingController):
|
|||||||
self,
|
self,
|
||||||
sql_conn: AsyncMySQL,
|
sql_conn: AsyncMySQL,
|
||||||
mongo_data_conn: AsyncMongo,
|
mongo_data_conn: AsyncMongo,
|
||||||
inbound_data: dict
|
inbound_data: dict,
|
||||||
) -> bool:
|
client_user_id: str
|
||||||
|
) -> TradingOAuthCallbackResponse:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
To capture the callback from ICICI Breeze's authorization loop. This happens when the user successfully logs in
|
To capture the callback from ICICI Breeze's authorization loop. This happens when the user successfully logs in
|
||||||
@@ -188,7 +190,8 @@ class ICICIBreezeTradingController(TradingController):
|
|||||||
:param sql_conn: The database connection to use to perform this activity.
|
:param sql_conn: The database connection to use to perform this activity.
|
||||||
:param mongo_data_conn: The database connection to use to perform this activity.
|
:param mongo_data_conn: The database connection to use to perform this activity.
|
||||||
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
||||||
:return: True if the callback loop was completed successfully, else False..
|
:param client_user_id: How the trading client identifies this user.
|
||||||
|
:return: A structured response to capture the process of callback handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from models.api.finstitutions.trading.symbols.list import (
|
|||||||
TradingSymbolListBrokerResponse,
|
TradingSymbolListBrokerResponse,
|
||||||
TradingSymbol
|
TradingSymbol
|
||||||
)
|
)
|
||||||
|
from models.finstitutions.trading.oauth import TradingOAuthCallbackResponse
|
||||||
|
|
||||||
# To work with MongoDB:
|
# To work with MongoDB:
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
@@ -179,15 +180,17 @@ class PaperTradingController(TradingController):
|
|||||||
self,
|
self,
|
||||||
sql_conn: AsyncMySQL,
|
sql_conn: AsyncMySQL,
|
||||||
mongo_data_conn: AsyncMongo,
|
mongo_data_conn: AsyncMongo,
|
||||||
inbound_data: dict
|
inbound_data: dict,
|
||||||
) -> bool:
|
client_user_id: str
|
||||||
|
) -> TradingOAuthCallbackResponse:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Not needed for paper trading.
|
Not needed for paper trading.
|
||||||
:param sql_conn: The database connection to use to perform this activity.
|
:param sql_conn: The database connection to use to perform this activity.
|
||||||
:param mongo_data_conn: The database connection to use to perform this activity.
|
:param mongo_data_conn: The database connection to use to perform this activity.
|
||||||
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
||||||
:return: True if the callback loop was completed successfully, else False..
|
:param client_user_id: How the trading client identifies this user.
|
||||||
|
:return: A structured response to capture the process of callback handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from models.api.finstitutions.trading.symbols.list import (
|
|||||||
TradingSymbolListBrokerResponse,
|
TradingSymbolListBrokerResponse,
|
||||||
TradingSymbol
|
TradingSymbol
|
||||||
)
|
)
|
||||||
|
from models.finstitutions.trading.oauth import TradingOAuthCallbackResponse
|
||||||
|
|
||||||
# To work with MongoDB:
|
# To work with MongoDB:
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
@@ -179,50 +180,79 @@ class ZerodhaKiteTradingController(TradingController):
|
|||||||
self,
|
self,
|
||||||
sql_conn: AsyncMySQL,
|
sql_conn: AsyncMySQL,
|
||||||
mongo_data_conn: AsyncMongo,
|
mongo_data_conn: AsyncMongo,
|
||||||
inbound_data: dict
|
inbound_data: dict,
|
||||||
) -> bool:
|
client_user_id: str
|
||||||
|
) -> TradingOAuthCallbackResponse:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
When the end user interacts with Zerodha's APIs, Zerodha's servers issue a callback like this:
|
When the end user interacts with Zerodha's APIs, Zerodha's servers issue a callback like this:
|
||||||
http://127.0.0.1:5999/auth/callback?action=login&type=login&status=success&request_token=the-request-token
|
http://127.0.0.1:5999/auth/callback?action=login&type=login&status=success&request_token=the-request-token
|
||||||
We must use the request token to get the access token. The access token is the thing that we must hold onto for
|
We must use the request token to get the access token. The access token is the thing that we must hold onto for
|
||||||
executing actual actions like subscribing to live market feed, placing trades, etc.
|
executing actual actions like subscribing to live market feed, placing trades, etc.
|
||||||
NOTE: Please ensure that you set the 'Redirect URL' such that is passes back Kite's 'api_key' back through the
|
NOTE: Please ensure that you set the 'Redirect URL' in the format as shown below:
|
||||||
callback URL. This can be one by setting the value manually as a query param on the app's configuration
|
01. http://127.0.0.1:5106/converse/finstitutions/trading/oauth/callback/zerodhaKite/<client_user_id>
|
||||||
page. E.g.: http://127.0.0.1:5999/auth/callback?api_key=user_api_key
|
02. https://api.thecaoffice.com/converse/finstitutions/trading/oauth/callback/zerodhaKite/<client_user_id>
|
||||||
|
BACKWARD COMPATIBILITY:
|
||||||
|
Earlier, we used to set the same API key in the callback URL as a query param like shown below:
|
||||||
|
E.g.: http://127.0.0.1:5106/converse/finstitutions/trading/oauth/callback/zerodhaKite?api_key=user_api_key
|
||||||
:param sql_conn: The database connection to use to perform this activity.
|
:param sql_conn: The database connection to use to perform this activity.
|
||||||
:param mongo_data_conn: The database connection to use to perform this activity.
|
:param mongo_data_conn: The database connection to use to perform this activity.
|
||||||
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
:param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc.
|
||||||
:return: True if the callback loop was completed successfully, else False..
|
:param client_user_id: How the trading client identifies this user.
|
||||||
|
:return: A structured response to capture the process of callback handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Start by assuming failure:
|
# Start by assuming failure:
|
||||||
success = False
|
response = TradingOAuthCallbackResponse()
|
||||||
zerodha_auth_token = None
|
zerodha_auth_token = None
|
||||||
|
|
||||||
|
# Check if either the new system or the old system is being followed.
|
||||||
|
# At least one is needed:
|
||||||
|
api_key = inbound_data.get("api_key")
|
||||||
|
if not api_key and not client_user_id:
|
||||||
|
response.message = "Your callback URL hasn't been configured properly."
|
||||||
|
return response
|
||||||
|
|
||||||
# Get the token from the database:
|
# Get the token from the database:
|
||||||
|
old_condition = mongo_data_conn.dict_to_dot_notation({"auth": {"apiKey": api_key}})
|
||||||
|
condition = mongo_data_conn.dict_to_dot_notation({"auth": {"userId": client_user_id}})
|
||||||
auth_token = await self.get_token_from_filter(
|
auth_token = await self.get_token_from_filter(
|
||||||
mongo_data_conn = mongo_data_conn,
|
mongo_data_conn = mongo_data_conn,
|
||||||
filter_json = mongo_data_conn.dict_to_dot_notation({
|
filter_json = {"$or": [old_condition, condition]}
|
||||||
"auth": {
|
|
||||||
"apiKey": inbound_data.get(
|
|
||||||
"api_key",
|
|
||||||
"Hint: Put the user's app's key in the query params of the 'Redirect URL'"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# If not such auth token exists:
|
# If not such auth token exists:
|
||||||
if not auth_token: return success
|
if not auth_token:
|
||||||
|
response.message = (
|
||||||
|
f"No such integration found in our system. "
|
||||||
|
"Please add this integration first and then try again."
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
# Get the final access tokens set from Zerodha Kite:
|
# Get the final access tokens set from Zerodha Kite:
|
||||||
|
try:
|
||||||
kite = KiteConnect(api_key = auth_token.auth["apiKey"])
|
kite = KiteConnect(api_key = auth_token.auth["apiKey"])
|
||||||
session_data = kite.generate_session(
|
session_data = kite.generate_session(
|
||||||
request_token = inbound_data["request_token"],
|
request_token = inbound_data["request_token"],
|
||||||
api_secret = auth_token.auth["apiSecret"]
|
api_secret = auth_token.auth["apiSecret"]
|
||||||
)
|
)
|
||||||
zerodha_auth_token = ZerodhaKiteAuthTokens(**session_data)
|
zerodha_auth_token = ZerodhaKiteAuthTokens(**session_data)
|
||||||
|
except Exception as exception:
|
||||||
|
response.exception = exception
|
||||||
|
response.message = str(exception)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Ensure that the client user id of the incoming callback and the one given in Zerodha's session data match:
|
||||||
|
if (
|
||||||
|
client_user_id is not None and # ................ For backward compatibility.
|
||||||
|
zerodha_auth_token.userId != client_user_id # ... New mechanism that verifies account match.
|
||||||
|
):
|
||||||
|
response.message = (
|
||||||
|
f"We were expecting authorization for the account '{client_user_id}', "
|
||||||
|
f"but Zerodha says the authorization was granted for the account '{zerodha_auth_token.userId}'. "
|
||||||
|
"This could be because of a misconfigured callback URL."
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
# Prepare the inputs to save to the database:
|
# Prepare the inputs to save to the database:
|
||||||
auth_url = await self.get_authorization_url(api_key = auth_token.auth["apiKey"])
|
auth_url = await self.get_authorization_url(api_key = auth_token.auth["apiKey"])
|
||||||
@@ -242,8 +272,15 @@ class ZerodhaKiteTradingController(TradingController):
|
|||||||
display_picture = zerodha_auth_token.displayPictureUrl
|
display_picture = zerodha_auth_token.displayPictureUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If saving the token fails:
|
||||||
|
if not success:
|
||||||
|
response.message = "Something went wrong towards the end of the authorization cycle."
|
||||||
|
return response
|
||||||
|
|
||||||
# Done here:
|
# Done here:
|
||||||
return success
|
response.success = True
|
||||||
|
response.message = "Authorization cycle successfully completed."
|
||||||
|
return response
|
||||||
|
|
||||||
# ┏┳┓ ┓• ┏┓ ┓ ┓
|
# ┏┳┓ ┓• ┏┓ ┓ ┓
|
||||||
# ┃ ┏┓┏┓┏┫┓┏┓┏┓ ┗┓┓┏┏┳┓┣┓┏┓┃┏
|
# ┃ ┏┓┏┓┏┫┓┏┓┏┓ ┗┓┓┏┏┳┓┣┓┏┓┃┏
|
||||||
|
|||||||
@@ -101,18 +101,24 @@ class PaperTradingAuth(BaseModel):
|
|||||||
|
|
||||||
class ZerodhaKiteAuth(BaseModel):
|
class ZerodhaKiteAuth(BaseModel):
|
||||||
|
|
||||||
|
userId: str = Field(
|
||||||
|
description = "How Zerodha's Kite platform identifies this user.",
|
||||||
|
frozen = True,
|
||||||
|
alias = "clientId"
|
||||||
|
)
|
||||||
|
|
||||||
apiKey: str = Field(
|
apiKey: str = Field(
|
||||||
description = (
|
description = (
|
||||||
"the api key of your kite connect app; "
|
"The API key of your Kite app. "
|
||||||
"this remains constant throughout the life of the app"
|
"This remains constant throughout the life of the app."
|
||||||
),
|
),
|
||||||
frozen = True
|
frozen = True
|
||||||
)
|
)
|
||||||
|
|
||||||
apiSecret: str = Field(
|
apiSecret: str = Field(
|
||||||
description = (
|
description = (
|
||||||
"the api secret of your kite connect app; "
|
"The API secret of your Kite app. "
|
||||||
"this can change if you think the security of your app has been compromised"
|
"This can be changed if you think the security of your app has been compromised."
|
||||||
),
|
),
|
||||||
frozen = True
|
frozen = True
|
||||||
)
|
)
|
||||||
@@ -124,6 +130,7 @@ class ZerodhaKiteAuth(BaseModel):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "forbid"
|
extra = "forbid"
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -131,13 +138,25 @@ class ZerodhaKiteAuth(BaseModel):
|
|||||||
|
|
||||||
class ICICIBreezeAuth(BaseModel):
|
class ICICIBreezeAuth(BaseModel):
|
||||||
|
|
||||||
|
userId: str = Field(
|
||||||
|
description = "How ICICI's Breeze platform identifies this user.",
|
||||||
|
frozen = True,
|
||||||
|
alias = "clientId"
|
||||||
|
)
|
||||||
|
|
||||||
apiKey: str = Field(
|
apiKey: str = Field(
|
||||||
description = "??",
|
description = (
|
||||||
|
"The API key of your Breeze app. "
|
||||||
|
"This remains constant throughout the life of the app."
|
||||||
|
),
|
||||||
frozen = True
|
frozen = True
|
||||||
)
|
)
|
||||||
|
|
||||||
apiSecret: str = Field(
|
apiSecret: str = Field(
|
||||||
description = "??",
|
description = (
|
||||||
|
"The API secret of your Breeze app. "
|
||||||
|
"This can be changed if you think the security of your app has been compromised."
|
||||||
|
),
|
||||||
frozen = True
|
frozen = True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,6 +167,7 @@ class ICICIBreezeAuth(BaseModel):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "forbid"
|
extra = "forbid"
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -179,8 +199,8 @@ class TradingAuthRequestHeaders(BaseModel):
|
|||||||
|
|
||||||
class TradingAuthRequestData(BaseModel):
|
class TradingAuthRequestData(BaseModel):
|
||||||
|
|
||||||
client: Literal["zerodhaKite", "iciciBreeze", "paperTrading"] = Field(alias = "client")
|
client: Literal["paperTrading", "zerodhaKite", "iciciBreeze"] = Field(alias = "client")
|
||||||
auth: Union[ZerodhaKiteAuth, ICICIBreezeAuth, PaperTradingAuth]
|
auth: Union[PaperTradingAuth, ZerodhaKiteAuth, ICICIBreezeAuth]
|
||||||
|
|
||||||
# ┏┓ ┏•
|
# ┏┓ ┏•
|
||||||
# ┃ ┏┓┏┓╋┓┏┓
|
# ┃ ┏┓┏┓╋┓┏┓
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
AUTHOR:
|
||||||
|
|
||||||
|
Khushal P Soonderji
|
||||||
|
|
||||||
|
DATE:
|
||||||
|
|
||||||
|
Saturday, 4th Jan., 2025.
|
||||||
|
|
||||||
|
OBJECTIVE:
|
||||||
|
|
||||||
|
To provide a structure to represent OAuth callback responses.
|
||||||
|
|
||||||
|
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, PastDatetime, model_validator, AwareDatetime
|
||||||
|
from typing import Optional, Literal, Union, List, Any
|
||||||
|
|
||||||
|
# My utils:
|
||||||
|
from utils_v2.string import regex
|
||||||
|
from utils_v2.date_time import date_time
|
||||||
|
|
||||||
|
# To work with date and time:
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# ***** ****
|
||||||
|
# *** MACROS / ONE-TIME INIT ***
|
||||||
|
# ***** ****
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
# --- Nothing Yet
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# ***** ****
|
||||||
|
# *** VARIABLES ***
|
||||||
|
# ***** ****
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
# --- Nothing Yet
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# ***** ****
|
||||||
|
# *** FUNCTIONS ***
|
||||||
|
# ***** ****
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
class TradingOAuthCallbackResponse(BaseModel):
|
||||||
|
|
||||||
|
success: bool = Field(
|
||||||
|
description = "To indicate whether or not, the action was a success",
|
||||||
|
frozen = False,
|
||||||
|
default = False
|
||||||
|
)
|
||||||
|
|
||||||
|
message: str = Field(
|
||||||
|
description = "To explain what happened in the process of handling the OAuth callback.",
|
||||||
|
frozen = False,
|
||||||
|
default = "ERR: Message not captured."
|
||||||
|
)
|
||||||
|
|
||||||
|
exception: Any = Field(
|
||||||
|
description = "To pass on any exception that occurred in the process.",
|
||||||
|
frozen = False,
|
||||||
|
default = None
|
||||||
|
)
|
||||||
|
|
||||||
|
# ┏┓ ┏•
|
||||||
|
# ┃ ┏┓┏┓╋┓┏┓
|
||||||
|
# ┗┛┗┛┛┗┛┗┗┫
|
||||||
|
# ┛
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "forbid"
|
||||||
|
|
||||||
|
# ┏┓ ┏┓
|
||||||
|
# ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏
|
||||||
|
# ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ┓┏ ┓• ┓ •
|
||||||
|
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
|
||||||
|
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# ***** ****
|
||||||
|
# *** MAIN PROGRAM ***
|
||||||
|
# ***** ****
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -101,9 +101,11 @@
|
|||||||
<div class="message-container">
|
<div class="message-container">
|
||||||
<div class="failure-circle">✘</div> <!-- Red circle with failure icon -->
|
<div class="failure-circle">✘</div> <!-- Red circle with failure icon -->
|
||||||
<h1><b>Authorization Failed</b></h1>
|
<h1><b>Authorization Failed</b></h1>
|
||||||
<p>Something went wrong in getting authorization from your <b>{{ client }}</b> account.
|
<p>
|
||||||
|
Something went wrong in getting authorization from your <b>{{ client }}</b> account.
|
||||||
<br><br><b>Hint:</b> {{ failure_hint|safe }}<br><br>
|
<br><br><b>Hint:</b> {{ failure_hint|safe }}<br><br>
|
||||||
Please feel free to try the same steps again. You can close this tab at any time.</p>
|
Please feel free to try the same steps again. You can close this tab at any time.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
<button class="btn btn-secondary" onclick="window.close();">Close</button>
|
<button class="btn btn-secondary" onclick="window.close();">Close</button>
|
||||||
|
|||||||
@@ -101,7 +101,11 @@
|
|||||||
<div class="message-container">
|
<div class="message-container">
|
||||||
<div class="success-circle">✔</div> <!-- Red circle with failure icon -->
|
<div class="success-circle">✔</div> <!-- Red circle with failure icon -->
|
||||||
<h1><b>Authorization Successful</b></h1>
|
<h1><b>Authorization Successful</b></h1>
|
||||||
<p>We have received authorization from your <b>{{ client }}</b> account. You can close this tab at any time.</p>
|
<p>
|
||||||
|
We have received authorization from your <b>{{ client }}</b> account.
|
||||||
|
{{ extra_message|safe }}
|
||||||
|
<br><br>You can close this tab at any time.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
<button class="btn btn-secondary" onclick="window.close();">Close</button>
|
<button class="btn btn-secondary" onclick="window.close();">Close</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user