diff --git a/api/blueprints/finstitutions/trading/oauth/request.py b/api/blueprints/finstitutions/trading/oauth/request.py index 6df0c83..4fab058 100644 --- a/api/blueprints/finstitutions/trading/oauth/request.py +++ b/api/blueprints/finstitutions/trading/oauth/request.py @@ -213,6 +213,8 @@ async def request_oauth_authorization_url( if inbound_data.client == "zerodhaKite": + print("Zerodha") + # Prepare the inputs: auth_url = await current_app.zerodha_kite_controller.get_authorization_url(api_key = inbound_data.auth.apiKey) @@ -244,6 +246,45 @@ async def request_oauth_authorization_url( # Check if things were successful: if not success: auth_url = None + # ┏┓ ┳┏┓┳┏┓┳ ┳┓ + # ┣ ┏┓┏┓ ┃┃ ┃┃ ┃ ┣┫┏┓┏┓┏┓┓┏┓ + # ┻ ┗┛┛ ┻┗┛┻┗┛┻ ┻┛┛ ┗ ┗ ┗┗ + + if inbound_data.client == "iciciBreeze": + + print("ICICI") + + # Prepare the inputs: + auth_url = await current_app.icici_breeze_controller.get_authorization_url(api_key = inbound_data.auth.apiKey) + + # Immediately save the details against that token id: + success = await current_app.icici_breeze_controller.set_token_direct( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + auth_token = CoreAuthTokenModel( + serviceType = "stockTrading", + client = inbound_data.client, + authType = "oauth", + user = kwargs["session_info"], + clientUserId = { + "apiKey": inbound_data.auth.apiKey + }, + auth = inbound_data.auth.model_dump(), + status = "active", + syncFreq = 1500 + ), + token_notes = { + "apiKey": inbound_data.auth.apiKey, + "authUrl": auth_url + }, + display_name = None, + display_picture = None, + session_token = inbound_headers["X-Session-Token"] + ) + + # Check if things were successful: + if not success: auth_url = None + # ┳┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ # ┛┗┗ ┛┣┛┗┛┛┗┛┗ diff --git a/api/main.py b/api/main.py index c127d9c..bea5092 100644 --- a/api/main.py +++ b/api/main.py @@ -82,6 +82,7 @@ from controllers_v2.message.sms.savvy_bulk_sms_kenya import SavvyBulkSMSKenyaCon # --- from controllers_v2.finstitutions.trading.all_trading import AllTradingController from controllers_v2.finstitutions.trading.zerodha_kite import ZerodhaKiteTradingController +from controllers_v2.finstitutions.trading.icici_breeze import ICICIBreezeTradingController from controllers_v2.finstitutions.trading.paper_trading import PaperTradingController # --- from controllers_v2.finstitutions.payments.all_payments import AllPaymentsController @@ -467,6 +468,12 @@ async def app_startup(**kwargs): alert_url = current_app.script_data["alerts"]["url"], debug = enable_debugging ) + current_app.icici_breeze_controller = ICICIBreezeTradingController( + cache = current_app.module_cache, + http_client = current_app.http_client, + alert_url = current_app.script_data["alerts"]["url"], + debug = enable_debugging + ) current_app.paper_trading_controller = PaperTradingController( cache = current_app.module_cache, http_client = current_app.http_client, diff --git a/background/finstitutions/trading/json.py b/background/finstitutions/trading/json.py index ba71942..2f51391 100644 --- a/background/finstitutions/trading/json.py +++ b/background/finstitutions/trading/json.py @@ -6,7 +6,7 @@ DATE: - Tuesday, 24th Aug. 2024 + Monday, 30th Dec. 2024 OBJECTIVE: @@ -14,7 +14,7 @@ REFERENCES: - 1) https://www.w3schools.com/python/python_json.asp + N/A DOWNLOADS: @@ -72,121 +72,7 @@ from utils_v2.system import files # ***************************************************************************************************************** -def from_string(json_data): - - """ - Decodes a JSON string to a pythonic variable like a dict. - :param json_data: The JSON string to decode. - :return: The decoded pythonic variable. - """ - - python_data = json.loads(json_data) - return python_data - - -# --------------------------------------------------------------------------------------------------------------------- - - -def to_string( - python_data, - indent = 4, - default = None, - separators = None, - no_space = False -): - - """ - Converts the given pythonic data to a JSON string. - :param python_data: The input data like a dict. - :param indent: The tab-width for pretty presentation. - :param default: The function to use on something that cannot be directly parsed into a JSON string. - :param separators: Custom separators to use. - :param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not - good for human readability, very good for saving memory. WARNING: THIS OVERRIDES EVERY OTHER PARAMETER EXCEPT - 'default'. - :return: The JSON string representation of the input pythonic data. - """ - - if no_space: - json_data = json.dumps( - python_data, - default = default, - separators = (',', ':') - ) - - else: - json_data = json.dumps( - python_data, - indent = indent, - default = default, - separators = separators - ) - - return json_data - - -# --------------------------------------------------------------------------------------------------------------------- - - -def from_file(file): - - """ - Reads a JSON file and returns it as a pythonic variable like a dict. - :param file: The path to the file on the disk or a file held in RAM as a BytesIO object. - :return: The decoded pythonic variable. - """ - - if isinstance(file, io.BytesIO): - file.seek(0) - json_data = file.getvalue() - else: json_data = files.read_file(file) - python_data = from_string(json_data) - return python_data - - -# --------------------------------------------------------------------------------------------------------------------- - - -def to_file( - file, - python_data, - indent = 4, - default = None, - separators = None, - no_space = False -): - - """ - - :param file: Either a path to a file on disk, or a buffer in RAM in the form of a BytesIO object. - :param python_data: The pythonic data to be converted to the JSON string. - :param indent: The tab-width for pretty presentation. - :param default: The function to use on something that cannot be directly parsed into a JSON string. - :param separators: Custom separators to use. - :param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not - good for human readability, very good for saving memory. WARNING: THIS OVERRIDES EVERY OTHER PARAMETER EXCEPT - 'default'. - :return: True/False if a path was given, else the same BytesIO object with the written JSON data. - """ - - json_data = to_string( - python_data, - indent = indent, - default = default, - separators = separators, - no_space = no_space - ) - - if isinstance(file, io.BytesIO): - file.write(json_data.encode("utf-8")) - file.seek(0) - return file - - else: - try: - files.write_file(file, json_data, mode = "w") - return True - except: return False +# --- Nothing Yet # ***************************************************************************************************************** diff --git a/controllers_v2/core/auth_token.py b/controllers_v2/core/auth_token.py index 6b50519..1f07f9c 100644 --- a/controllers_v2/core/auth_token.py +++ b/controllers_v2/core/auth_token.py @@ -188,11 +188,12 @@ class CoreAuthTokenController(CoreBaseModel): mongo_json = await mongo_data_conn.find_one_and_update( collection = self.AUTH_COLLECTION, filter = mongo_data_conn.dict_to_dot_notation({ - "serviceType": auth_token.serviceType, "user": { "entityId": auth_token.user.entityId, "billingAccountId": auth_token.user.billingAccountId }, + "serviceType": auth_token.serviceType, + "client": auth_token.client, "clientUserId": auth_token.clientUserId }), update = { diff --git a/controllers_v2/finstitutions/trading/icici_breeze.py b/controllers_v2/finstitutions/trading/icici_breeze.py new file mode 100644 index 0000000..3baf46e --- /dev/null +++ b/controllers_v2/finstitutions/trading/icici_breeze.py @@ -0,0 +1,228 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 30th Dec., 2024 + + OBJECTIVE: + + To handle all trading related behaviour for ICICI's Breeze platform. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.string import json +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.finstitutions.trading.base import TradingController + +# Models: +from models.core.auth_token import CoreAuthTokenModel +from models.api.finstitutions.trading.symbols.list import ( + TradingSymbolListRequestData, + TradingSymbolListBrokerResponse, + TradingSymbol +) + +# To work with MongoDB: +from bson.objectid import ObjectId + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx +import urllib + +# To work with Zerodha's Kite platform: +from kiteconnect import KiteConnect + +# To handle exceptions: +from pydantic import ValidationError + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class ICICIBreezeTradingController(TradingController): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + CLIENT_NAME = "iciciBreeze" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + debug: bool = True, + debug_prefix: str = "ICICI Breeze (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller for ICICI's Breeze platform. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Declare the client: + this_client = self.CLIENT_NAME + + # Prepare base filter: + this_filter = {"client": this_client} + + # Invoke the parent's constructor: + super().__init__( + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = this_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._client = this_client + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + @staticmethod + async def get_authorization_url( + **kwargs + ) -> str: + + """ + To generate an authorization URL for this broker. + :param kwargs: Any no. of things needed by your broker to generate the URL. + :return: The authorization URL. + """ + + return "https://api.icicidirect.com/apiuser/login?api_key=" + urllib.parse.quote_plus(kwargs["api_key"]) + + async def handle_authorization_callback( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + inbound_data: dict + ) -> bool: + + """ + To capture the callback from ICICI Breeze's authorization loop. This happens when the user successfully logs in + to his account through the login URL. + :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 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.. + """ + + raise NotImplementedError + + # ┏┳┓ ┓• ┏┓ ┓ ┓ + # ┃ ┏┓┏┓┏┫┓┏┓┏┓ ┗┓┓┏┏┳┓┣┓┏┓┃┏ + # ┻ ┛ ┗┻┗┻┗┛┗┗┫ ┗┛┗┫┛┗┗┗┛┗┛┗┛ + # ┛ ┛ + + async def list_symbols( + self, + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + inbound_data: TradingSymbolListRequestData + ) -> TradingSymbolListBrokerResponse: + + """ + To get the list of tradeable symbols offered by ICICI Breeze. + :param mongo_data_conn: The database connection to use to perform this activity. + :param auth_token: The token that has to be used to fetch the data. + :param inbound_data: The data that came in with the APi call. + :return: The structured response form the broker. + """ + + raise NotImplementedError + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/finstitutions/trading/zerodha_kite.py b/controllers_v2/finstitutions/trading/zerodha_kite.py index 7a0e3f1..2221513 100644 --- a/controllers_v2/finstitutions/trading/zerodha_kite.py +++ b/controllers_v2/finstitutions/trading/zerodha_kite.py @@ -124,7 +124,7 @@ class ZerodhaKiteTradingController(TradingController): http_client: httpx.AsyncClient = None, alert_url: str = None, debug: bool = True, - debug_prefix: str = "Zerodha kite (C) | ", + debug_prefix: str = "Zerodha Kite (C) | ", debug_only_errors: bool = True ): diff --git a/models/api/finstitutions/trading/auth/oauth.py b/models/api/finstitutions/trading/auth/oauth.py index 53cc161..d5b67b3 100644 --- a/models/api/finstitutions/trading/auth/oauth.py +++ b/models/api/finstitutions/trading/auth/oauth.py @@ -129,6 +129,30 @@ class ZerodhaKiteAuth(BaseModel): # --------------------------------------------------------------------------------------------------------------------- +class ICICIBreezeAuth(BaseModel): + + apiKey: str = Field( + description = "??", + frozen = True + ) + + apiSecret: str = Field( + description = "??", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + class TradingAuthRequestHeaders(BaseModel): sessionToken: str = Field( @@ -155,8 +179,8 @@ class TradingAuthRequestHeaders(BaseModel): class TradingAuthRequestData(BaseModel): - client: Literal["zerodhaKite", "paperTrading"] = Field(alias = "client") - auth: Union[ZerodhaKiteAuth, PaperTradingAuth] + client: Literal["zerodhaKite", "iciciBreeze", "paperTrading"] = Field(alias = "client") + auth: Union[ZerodhaKiteAuth, ICICIBreezeAuth, PaperTradingAuth] # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ @@ -175,7 +199,8 @@ class TradingAuthRequestData(BaseModel): client = values.client auth = values.auth harmony_map = { - "zerodhaKite": ZerodhaKiteAuth, + "zerodhaKite": (ZerodhaKiteAuth, ICICIBreezeAuth), + "iciciBreeze": (ICICIBreezeAuth, ZerodhaKiteAuth), "paperTrading": PaperTradingAuth } if not isinstance(auth, harmony_map[client]): diff --git a/models/core/auth_token.py b/models/core/auth_token.py index e36d800..067f252 100644 --- a/models/core/auth_token.py +++ b/models/core/auth_token.py @@ -105,12 +105,12 @@ class CoreAuthTokenModel(BaseModel): ) client: Literal[ - "gmail", "outlook", # ...................... Mail Clients - "telegram", "whatsapp", # .................. Chat Clients - "nimbusSmsIndia", "savvyBulkSmsKenya", # ... SMS Clients - "razorpay", "safaricomMPesaExpress", # ..... Payment Gateways - "zerodhaKite", "paperTrading", # ........... Stock Brokers - "theCaOfficeAi" # .......................... Software + "gmail", "outlook", # ............................. Mail Clients + "telegram", "whatsapp", # ......................... Chat Clients + "nimbusSmsIndia", "savvyBulkSmsKenya", # .......... SMS Clients + "razorpay", "safaricomMPesaExpress", # ............ Payment Gateways + "zerodhaKite", "iciciBreeze", "paperTrading", # ... Stock Brokers + "theCaOfficeAi" # ................................. Software ] = Field( description = "the third-part client that was used", frozen = True diff --git a/utils_v2/trading/zerodha_kite/controllers/async_zerodha_kite.py b/utils_v2/trading/zerodha_kite/controllers/async_zerodha_kite.py index f630001..e794ac7 100644 --- a/utils_v2/trading/zerodha_kite/controllers/async_zerodha_kite.py +++ b/utils_v2/trading/zerodha_kite/controllers/async_zerodha_kite.py @@ -49,6 +49,7 @@ from utils_v2.date_time import date_time # Data models: from utils_v2.trading.zerodha_kite.models.api_call import ZerodhaKiteApiResponse +from utils_v2.trading.zerodha_kite.models.auth_tokens import ZerodhaKiteAuthTokens # To make API calls: import httpx @@ -301,9 +302,10 @@ class AsyncZerodhaKite: """ When the user authorizes the login flow, Zerodha's serve will send you a GET request on the callback URL that - you set on the PI portal for your app. This callback will have, among other things, a 'request_toke'. The + you set on the PI portal for your app. This callback will have, among other things, a 'request_token'. The request token is valid only for a very short period, and must be used to get a longer token called - 'access_token' for actual activities. + 'access_token' for actual activities. A checksum is needed for verification. Read about it in the official + documentation on Kite's API docs. DOCUMENTATION: 01. https://kite.trade/docs/connect/v3/user/ :param request_token: The request token received from Zerodha when the user logs in. @@ -313,15 +315,45 @@ class AsyncZerodhaKite: self.__request_token = request_token hasher = Hasher() hasher.update(self.__api_key + request_token + self.__api_secret) - self.__checksum = hasher.digest().decode() - print(self.__checksum) + self.__checksum = hasher.hexdigest() - # async def get_access_token(self): - # - # client_response = self.__get( - # url = r"https://kite.zerodha.com/session/token", - # - # ) + async def generate_session( + self, + raise_exception = False + ) -> ZerodhaKiteAuthTokens | None: + + """ + Once we have the 'request_token' from Zerodha's callback, we must generate a session by fetching an access + token. The access token will be used to perform most of the actual activities. + DOCUMENTATION: + 01. https://kite.trade/docs/connect/v3/user/ + :return: Either the auth-token model of Zerodha, or null if the process failed. + """ + + # Start by assuming failure: + session = None + + # Try to get a session from Zerodha: + client_response = await self.__post( + url = r"https://api.kite.trade/session/token", + headers = {"X-Kite-Version": "3"}, + data = { + "api_key": self.__api_key, + "request_token": self.__request_token, + "checksum": self.__checksum + } + ) + + # If the API call was successful, we have a valid session: + if client_response.success: + client_json = await client_response.get_json() + + self.__access_token = client_json["data"]["access_token"] + self.__ + + print(client_response.to_markdown()) + print("CLIENT RESPONSE;", json.to_string(await client_response.get_json())) + print(json.to_string(client_response.model_dump(), default = str)) # ***************************************************************************************************************** @@ -349,5 +381,6 @@ if __name__ == "__main__": # Login flow: print("LOGIN URL:", my_kite.login_url) my_kite.set_request_token(input("Request Token: ")) + await my_kite.get_access_token() asyncio.run(main())