From 58e91fb15f9f00a7df16036c27b20d80cd31b8c3 Mon Sep 17 00:00:00 2001 From: khushal Date: Mon, 23 Dec 2024 16:27:52 +0530 Subject: [PATCH] (20241223) Zerodha auth now returns views. --- .gitignore | 4 +- .../finstitutions/trading/oauth/callback.py | 69 +++++- api/blueprints/mail/oauth/callback.py | 12 +- api/main.py | 2 +- .../finstitutions/trading/zerodha_kite.py | 1 + kill.sh | 1 + run.sh | 1 + socketio/main.py | 202 ------------------ .../trading/oauth/oauth_cancelled_v2.html | 2 +- .../trading/oauth/oauth_failure_v2.html | 4 +- .../trading/oauth/oauth_success_v2.html | 2 +- {socketio => wsocket}/__init__.py | 0 .../finstitutions/__init__.py | 0 .../finstitutions/trading/__init__.py | 0 .../trading/{main.py => live_feed.py} | 59 +---- wsocket/main.py | 47 ++-- 16 files changed, 112 insertions(+), 294 deletions(-) delete mode 100644 socketio/main.py rename {socketio => wsocket}/__init__.py (100%) rename {socketio => wsocket}/finstitutions/__init__.py (100%) rename {socketio => wsocket}/finstitutions/trading/__init__.py (100%) rename wsocket/finstitutions/trading/{main.py => live_feed.py} (77%) diff --git a/.gitignore b/.gitignore index ebb370b..def2155 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ __pycache__/ *.pem *.pyc -*.pyd \ No newline at end of file +*.pyd + +/creds/ diff --git a/api/blueprints/finstitutions/trading/oauth/callback.py b/api/blueprints/finstitutions/trading/oauth/callback.py index 623d92d..fd53062 100644 --- a/api/blueprints/finstitutions/trading/oauth/callback.py +++ b/api/blueprints/finstitutions/trading/oauth/callback.py @@ -57,7 +57,8 @@ from utils_v2.api.async_quart import ( only_whitelisted_ips, limit_rate, validate_input, - handle_cancelled_request + handle_cancelled_request, + handle_failed_request ) # GMail-related utils: @@ -112,6 +113,22 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- +async def handle_auth_exception(): + + print("Hi! Cleaning up...") + return await render_template( + "/finstitutions/trading/oauth/oauth_failure_v2.html", + client = g.client_label, + failure_hint = ( + f"Something went wrong (E). " + f"Please use log-id '{g.log_id}' to check with the support team." + ) + ) + + +# --------------------------------------------------------------------------------------------------------------------- + + @trading_oauth_callback_bp.route("/callback/", methods = ["POST", "GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @@ -127,6 +144,7 @@ def init(blueprint_setup_state): @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, inbound_headers: dict = None, @@ -145,6 +163,13 @@ async def trading_oauth_callback( :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 = "Zerodha (Kite)" # Start by assuming failure: success = None @@ -165,17 +190,43 @@ async def trading_oauth_callback( # ┛┗┗ ┛┣┛┗┛┛┗┛┗ # ┛ + # # No valid client: + # if success is None: return ResponseModel( + # status_code = StatusCodes.FAILED, + # http_code = HttpCodes.BAD_REQUEST, + # message = f"Invalid/unimplemented client '{trading_client}'." + # ) + # + # # When the client was valid: + # return ResponseModel( + # status_code = StatusCodes.OK if success else StatusCodes.FAILED, + # http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, + # ) + # No valid client: - if success is None: return ResponseModel( - status_code = StatusCodes.FAILED, - http_code = HttpCodes.BAD_REQUEST, - message = f"Invalid/unimplemented client '{trading_client}'." + if success is None: return await render_template( + "/finstitutions/trading/oauth/oauth_failure_v2.html", + client = g.client_label, + failure_hint = ( + f"Invalid client '{g.client_label}' selected. " + f"Please use log-id '{g.log_id}' to check with the support team." + ) ) - # When the client was valid: - return ResponseModel( - status_code = StatusCodes.OK if success else StatusCodes.FAILED, - http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, + # Successful auth: + if success: return await render_template( + "/finstitutions/trading/oauth/oauth_success_v2.html", + client = g.client_label + ) + + # Failed auth: + if success is None: return await render_template( + "/finstitutions/trading/oauth/oauth_failure_v2.html", + client = g.client_label, + failure_hint = ( + f"Something went wrong (NE). " + f"Please use log-id '{g.log_id}' to check with the support team." + ) ) diff --git a/api/blueprints/mail/oauth/callback.py b/api/blueprints/mail/oauth/callback.py index a72d192..721d963 100644 --- a/api/blueprints/mail/oauth/callback.py +++ b/api/blueprints/mail/oauth/callback.py @@ -134,7 +134,7 @@ async def handle_gmail_callback() -> render_template: # In case the user cancelled halfway through (on Google's screen): if g.inbound_data.get("error") == "access_denied": return await render_template( - "/mail/oauth/oauth_cancelled_v2.html", + "/message/mail/oauth/oauth_cancelled_v2.html", mail_client = g.mail_client.title() ) @@ -167,7 +167,7 @@ async def handle_gmail_callback() -> render_template: (not auth_token) or auth_token.clientUserId["email"] != str(google_tokens.email) ): return await render_template( - "/mail/oauth/oauth_failure_v2.html", + "/message/mail/oauth/oauth_failure_v2.html", mail_client = g.mail_client.title(), failure_hint = ( f"We were expecting authorization from '{auth_token.clientUserId['email']}', " @@ -224,13 +224,13 @@ async def handle_gmail_callback() -> render_template: # Return an HTML response for success: if tokens_saved: return await render_template( - "/mail/oauth/oauth_success_v2.html", + "/message/mail/oauth/oauth_success_v2.html", mail_client = g.mail_client.title() ) # Return an HTML response for failure: else: return await render_template( - "/mail/oauth/oauth_failure_v2.html", + "/message/mail/oauth/oauth_failure_v2.html", mail_client = g.mail_client.title(), failure_hint = f"Unknown error. Please use log-id '{g.log_id}' to check with the support team." ) @@ -292,7 +292,7 @@ async def mail_auth_callback( # If something goes wrong: except Exception as exception: return await render_template( - "/mail/oauth/oauth_failure_v2.html", + "/message/mail/oauth/oauth_failure_v2.html", mail_client = mail_client.title(), failure_hint = ( f"An internal server error occurred. " @@ -305,7 +305,7 @@ async def mail_auth_callback( # ┛┗┗┻┛┗┗┻┗┗ ┻┛┗┗┛┗┻┗┗┗┻ ┗┛┗┗┗ ┛┗┗ return await render_template( - "/mail/oauth/oauth_failure_v2.html", + "/message/mail/oauth/oauth_failure_v2.html", mail_client = mail_client.title(), failure_hint = ( f"Invalid client '{mail_client}' selected. " diff --git a/api/main.py b/api/main.py index 471a52e..62643ba 100644 --- a/api/main.py +++ b/api/main.py @@ -155,7 +155,7 @@ APP_VERSION = constants.APP_VERSION # The Quart app: -app = Quart(__name__, template_folder = r"../views") +app = Quart(__name__, template_folder = r"../views_v2") app = cors(app) # Mail Blueprints: diff --git a/controllers_v2/finstitutions/trading/zerodha_kite.py b/controllers_v2/finstitutions/trading/zerodha_kite.py index a387135..4c87d27 100644 --- a/controllers_v2/finstitutions/trading/zerodha_kite.py +++ b/controllers_v2/finstitutions/trading/zerodha_kite.py @@ -220,6 +220,7 @@ class ZerodhaKiteTradingController(TradingController): api_secret = auth_token.auth["apiSecret"] ) zerodha_auth_token = ZerodhaKiteAuthTokens(**session_data) + print(zerodha_auth_token) # Prepare the inputs to save to the database: auth_url = await self.get_authorization_url(api_key = auth_token.auth["apiKey"]) diff --git a/kill.sh b/kill.sh index 159505a..368f446 100644 --- a/kill.sh +++ b/kill.sh @@ -3,6 +3,7 @@ # Kill all the scripts: echo "Killing the script." pkill -9 -f "$(pwd)/api/main.py" +pkill -9 -f "$(pwd)/wsocket/main.py" # Get the name of the current directory, # and kill the monitors of the above scripts: diff --git a/run.sh b/run.sh index 702cf79..b5caa9c 100644 --- a/run.sh +++ b/run.sh @@ -3,6 +3,7 @@ # Use this to run the microservice without any docker setup. source .venv/bin/activate python3 "$(pwd)/api/main.py" --host "0.0.0.0" --port 5106 --workers 4 --script-id "kps_cnv_KBC3MoaU" & +python3 "$(pwd)/wsocket/main.py" & deactivate # All done: diff --git a/socketio/main.py b/socketio/main.py deleted file mode 100644 index 67d39f7..0000000 --- a/socketio/main.py +++ /dev/null @@ -1,202 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - Create: Saturday, 18th May, 2022 - Update: Thursday, 22nd Aug. 2024 - - OBJECTIVE: - - To provide an easy way to work with '.json' data and files. - - REFERENCES: - - 1) https://www.w3schools.com/python/python_json.asp - - DOWNLOADS: - - N/A - -""" - - -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys -sys.path.append(".") -sys.path.append("..") - -# System-level activities: -import io - -# To work with the JSON standard: -import json - -# To work with files: -from utils_v2.system import files - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -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 - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - pass diff --git a/views_v2/finstitutions/trading/oauth/oauth_cancelled_v2.html b/views_v2/finstitutions/trading/oauth/oauth_cancelled_v2.html index 368e9a2..0bff500 100644 --- a/views_v2/finstitutions/trading/oauth/oauth_cancelled_v2.html +++ b/views_v2/finstitutions/trading/oauth/oauth_cancelled_v2.html @@ -101,7 +101,7 @@
!

Authorization Cancelled

-

It seems that the authorization for your {{ mail_client }} account was cancelled unexpectedly. +

It seems that the authorization for your {{ client }} account was cancelled unexpectedly. Please feel free to try again whenever you feel like it. You can close this tab at any time.

diff --git a/views_v2/finstitutions/trading/oauth/oauth_failure_v2.html b/views_v2/finstitutions/trading/oauth/oauth_failure_v2.html index 03d0b65..e396f42 100644 --- a/views_v2/finstitutions/trading/oauth/oauth_failure_v2.html +++ b/views_v2/finstitutions/trading/oauth/oauth_failure_v2.html @@ -101,8 +101,8 @@

Authorization Failed

-

Something went wrong in getting authorization from your {{ mail_client }} account. -

Hint: {{ failure_hint }}

+

Something went wrong in getting authorization from your {{ client }} account. +

Hint: {{ failure_hint|safe }}

Please feel free to try the same steps again. You can close this tab at any time.

diff --git a/views_v2/finstitutions/trading/oauth/oauth_success_v2.html b/views_v2/finstitutions/trading/oauth/oauth_success_v2.html index 929f50c..33ee0b1 100644 --- a/views_v2/finstitutions/trading/oauth/oauth_success_v2.html +++ b/views_v2/finstitutions/trading/oauth/oauth_success_v2.html @@ -101,7 +101,7 @@

Authorization Successful

-

We have received authorization from your {{ mail_client }} account. You can close this tab at any time.

+

We have received authorization from your {{ client }} account. You can close this tab at any time.

diff --git a/socketio/__init__.py b/wsocket/__init__.py similarity index 100% rename from socketio/__init__.py rename to wsocket/__init__.py diff --git a/socketio/finstitutions/__init__.py b/wsocket/finstitutions/__init__.py similarity index 100% rename from socketio/finstitutions/__init__.py rename to wsocket/finstitutions/__init__.py diff --git a/socketio/finstitutions/trading/__init__.py b/wsocket/finstitutions/trading/__init__.py similarity index 100% rename from socketio/finstitutions/trading/__init__.py rename to wsocket/finstitutions/trading/__init__.py diff --git a/wsocket/finstitutions/trading/main.py b/wsocket/finstitutions/trading/live_feed.py similarity index 77% rename from wsocket/finstitutions/trading/main.py rename to wsocket/finstitutions/trading/live_feed.py index 42dedba..606a752 100644 --- a/wsocket/finstitutions/trading/main.py +++ b/wsocket/finstitutions/trading/live_feed.py @@ -42,10 +42,6 @@ import os # my utils: from utils_v2.string import json -# To work with SocketIO: -import socketio -from aiohttp import web - # For asynchronous activities: import asyncio @@ -60,7 +56,8 @@ from icecream import IceCreamDebugger # ***************************************************************************************************************** -# --- Nothing Yet +# Debugging: +printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) # ***************************************************************************************************************** @@ -70,13 +67,7 @@ from icecream import IceCreamDebugger # ***************************************************************************************************************** -# The SocketIo server: -sio = socketio.AsyncServer(cors_allowed_origins = "*") -app = web.Application() -sio.attach(app) - -# Debugging: -printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) +# --- Nothing Yet # ***************************************************************************************************************** @@ -86,30 +77,8 @@ printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) # ***************************************************************************************************************** -@sio.event -async def connect(sid, environ): - printer(sid) - - -# --------------------------------------------------------------------------------------------------------------------- - - -@sio.event -async def disconnect(sid): - printer(sid) - - -# --------------------------------------------------------------------------------------------------------------------- - - -async def init(): - - """ - Initialize stuff here. - :return: ? - """ - - pass +async def subscribe(sid, data): + printer(data) # ***************************************************************************************************************** @@ -121,20 +90,4 @@ async def init(): if __name__ == "__main__": - async def main(): - - # Start receiving live market data in the background: - # asyncio.create_task(start_live_feed()) - - # Run the web server: - runner = web.AppRunner(app) - await runner.setup() - site = web.TCPSite(runner, "0.0.0.0", 5214) - printer("Server running.") - await site.start() - - # Keep the server running: - while True: await asyncio.sleep(3_600) - - # Let's go: - asyncio.run(main()) + pass diff --git a/wsocket/main.py b/wsocket/main.py index 42dedba..70c2fff 100644 --- a/wsocket/main.py +++ b/wsocket/main.py @@ -52,6 +52,13 @@ import asyncio # for debugging: from icecream import IceCreamDebugger +# To work with date and time: +import time +import datetime + +# To work with Zerodha's Kite platform: +from kiteconnect import KiteConnect, KiteTicker + # ***************************************************************************************************************** # ***** **** @@ -60,7 +67,23 @@ from icecream import IceCreamDebugger # ***************************************************************************************************************** -# --- Nothing Yet +INSTRUMENT_MAP = { + 256265: "NIFTY 50", + 260617: "NIFTY 100", + 259849: "NIFTY IT", + 341249: "HDFCBANK", + 738561: "RELIANCE", + 408065: "INFY", + 2953217: "TCS", + 356865: "HINDUNILVR", + 1270529: "ICICIBANK", + 492033: "KOTAKBANK", + 110630919: "GOLD25JAN75800CE", + 110050823: "SILVER25FEB76000CE", + 10670594: "NIFTY24DEC23650PE", + 17167874: "BANKNIFTY24DEC45000PE", +} +INSTRUMENT_TOKENS = list(INSTRUMENT_MAP.keys()) # ***************************************************************************************************************** @@ -99,19 +122,6 @@ async def disconnect(sid): printer(sid) -# --------------------------------------------------------------------------------------------------------------------- - - -async def init(): - - """ - Initialize stuff here. - :return: ? - """ - - pass - - # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** @@ -121,20 +131,21 @@ async def init(): if __name__ == "__main__": + # Import the needed events: + from wsocket.finstitutions.trading import live_feed + async def main(): - # Start receiving live market data in the background: - # asyncio.create_task(start_live_feed()) + # register all the events: + sio.on("subscribe", live_feed.subscribe) # Run the web server: runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, "0.0.0.0", 5214) - printer("Server running.") await site.start() # Keep the server running: while True: await asyncio.sleep(3_600) - # Let's go: asyncio.run(main())