(20241221) Zerodha Auth Ready. Users can now integrate Kite.

This commit is contained in:
2024-12-21 15:22:55 +05:30
parent 8904ea394d
commit 1249841b05
15 changed files with 568 additions and 464 deletions
+43
View File
@@ -103,6 +103,10 @@ class CoreAuthTokenController(CoreBaseModel):
# For MongoDB:
AUTH_COLLECTION = "_authTokens"
# Other variables:
_service_type = None
_client = None
# ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
# ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛
@@ -166,6 +170,12 @@ class CoreAuthTokenController(CoreBaseModel):
:return: An ObjectId to later store the granted tokens.
"""
# Safety check for consistency:
if self._service_type is not None and self._service_type != auth_token.serviceType:
raise ValueError(f"Expected service type '{self._service_type}', got '{auth_token.serviceType}'")
if self._client is not None and self._client != auth_token.client:
raise ValueError(f"Expected service type '{self._client}', got '{auth_token.client}'")
# Note down the timestamp at which this event occurred:
request_ts = date_time.get_current_utc_date_time(as_string = False)
@@ -255,6 +265,12 @@ class CoreAuthTokenController(CoreBaseModel):
:return: True if saved, False if failed.
"""
# Safety check for consistency:
if self._service_type is not None and self._service_type != auth_token.serviceType:
raise ValueError(f"Expected service type '{self._service_type}', got '{auth_token.serviceType}'")
if self._client is not None and self._client != auth_token.client:
raise ValueError(f"Expected service type '{self._client}', got '{auth_token.client}'")
# Start by assuming failure:
token_saved = False
@@ -430,6 +446,33 @@ class CoreAuthTokenController(CoreBaseModel):
# Done here:
return CoreAuthTokenModel(**token) if token else None
async def get_token_from_filter(
self,
mongo_data_conn: AsyncMongo,
filter_json: dict
) -> CoreAuthTokenModel | None:
"""
To retrieve stored tokens from the database. One token at a time.
:param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
:param filter_json: The filter conditions to use.
:return: The retrieved record that has the token, and information about the service and client if found, else
None when there is no matching record.
"""
# Prepare the filter:
if self._base_filter:
for k, v in self._base_filter.items(): filter_json[k] = v
# If there is some filtering possible, we fetch the token:
token = await mongo_data_conn.find_one(
collection = self.AUTH_COLLECTION,
filter = filter_json
)
# Done here:
return CoreAuthTokenModel(**token) if token else None
async def get_tokens_from_ids(
self,
mongo_data_conn: AsyncMongo,
@@ -40,7 +40,7 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers:
from controllers_v2.core.auth_token import CoreAuthTokenController
from controllers_v2.finstitutions.trading.base import TradingController
# Models:
from models.core.auth_token import CoreAuthTokenModel
@@ -89,7 +89,7 @@ import httpx
# *****************************************************************************************************************
class TradingController(CoreAuthTokenController):
class AllTradingController(TradingController):
# ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
@@ -100,37 +100,28 @@ class TradingController(CoreAuthTokenController):
cache: AsyncRedisCache = None,
http_client: httpx.AsyncClient = None,
alert_url: str = None,
base_filter: dict = None,
debug: bool = True,
debug_prefix: str = "Trading (C) | ",
debug_prefix: str = "All Trading (C) | ",
debug_only_errors: bool = True
):
"""
This is the foundational controller for all SMS services. This is built on top of the core message controller,
and, in turn, all individual SMS client controllers must be built on top of this.
This is the foundational controller for all trading services. Use this for any smaller common tasks where you
may not know the exact client beforehand.
:param cache: The object to use for caching results from database calls.
:param http_client: The HTTP client
:param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE
FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY.
: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.
"""
# Prepare the combined base filter:
this_filter = {}
for k, v in (base_filter or {}).items(): this_filter[k] = v
this_filter["serviceType"] = "stockTrading"
# Invoke the parent's constructor:
CoreAuthTokenController.__init__(
self,
super().__init__(
cache = cache,
alert_url = alert_url,
http_client = http_client,
base_filter = this_filter,
base_filter = None,
debug = debug,
debug_prefix = debug_prefix,
debug_only_errors = debug_only_errors
@@ -140,23 +131,7 @@ class TradingController(CoreAuthTokenController):
# ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗
# async def login_url(
# self,
# mongo_data_conn: AsyncMongo,
# auth_token: CoreAuthTokenModel
# ) -> SMSSendOneResult:
#
# """
# To send one SMS message through the third-party client.
# :param mongo_data_conn: The database connection to use to perform this task.
# :param auth_token: The auth token that will be used to send this message.
# :param client: The third-party SMS client to use to send this message.
# :param message: The actual message that needs to be sent.
# :param tags: Any tags to attach with this SMS for filtering when querying in the listing service.
# :return: The structured result of sending one message.
# """
#
# pass
pass
# *****************************************************************************************************************
+22 -68
View File
@@ -6,11 +6,11 @@
DATE:
Thursday, 19th Dec., 2024
Saturday, 21st Dec., 2024
OBJECTIVE:
To handle all SMS related behaviour from one place.
To handle all trading related behaviour from one place.
REFERENCES:
@@ -40,20 +40,10 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers:
from controllers_v2.core.message import CoreMessageController
from controllers_v2.core.auth_token import CoreAuthTokenController
# Models:
from models.core.auth_token import CoreAuthTokenModel
from models.api.sms.send import (
NimbusSMSIndiaMessage,
SavvyBulkSMSKenyaMessage,
SMSSendOneResult,
SMSSendManyResults
)
# SMS clients:
from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS
from utils_v2.sms.kenya.savvy_bulk_sms.controllers.async_savvy_bulk_sms import AsyncSavvyBulkSMS
# To work with datatypes:
from typing import List, Any
@@ -102,7 +92,7 @@ from abc import ABC, abstractmethod
# *****************************************************************************************************************
class SMSController(CoreMessageController, ABC):
class TradingController(CoreAuthTokenController, ABC):
# ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
@@ -115,13 +105,13 @@ class SMSController(CoreMessageController, ABC):
alert_url: str = None,
base_filter: dict = None,
debug: bool = True,
debug_prefix: str = "SMS (C) | ",
debug_prefix: str = "Trading (C) | ",
debug_only_errors: bool = True
):
"""
This is the foundational controller for all SMS services. This is built on top of the core message controller,
and, in turn, all individual SMS client controllers must be built on top of this.
This is the foundational controller for all trading/stockbroking services. This is built on top of the
authorization model, and, in turn, the individual stockbroking clients should be built on top of this.
:param cache: The object to use for caching results from database calls.
:param http_client: The HTTP client
:param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE
@@ -132,70 +122,34 @@ class SMSController(CoreMessageController, ABC):
:return: None.
"""
# Prepare the combined base filter:
sms_filter = {}
for k, v in (base_filter or {}).items(): sms_filter[k] = v
sms_filter["serviceType"] = "sms"
# Declare the service type:
this_service_type = "stockTrading"
# Prepare base filter:
this_filter = {}
for k, v in (base_filter or {}).items(): this_filter[k] = v
this_filter["serviceType"] = this_service_type
# Invoke the parent's constructor:
CoreMessageController.__init__(
CoreAuthTokenController.__init__(
self,
cache = cache,
alert_url = alert_url,
http_client = http_client,
base_filter = sms_filter,
base_filter = this_filter,
debug = debug,
debug_prefix = debug_prefix,
debug_only_errors = debug_only_errors
)
# ┏┓┳┳┓┏┓ ┏┓ ┓•
# ┗┓┃┃┃┗┓ ┗┓┏┓┏┓┏┫┓┏┓┏┓
# ┗┛┛ ┗┗┛ ┗┛┗ ┛┗┗┻┗┛┗┗┫
# ┛
# Init a variable in a parent:
self._service_type = this_service_type
async def send_one_sms(
self,
mongo_data_conn: AsyncMongo,
auth_token: CoreAuthTokenModel,
client: AsyncNimbusSMS | AsyncSavvyBulkSMS,
message: NimbusSMSIndiaMessage,
tags: List[Any]
) -> SMSSendOneResult:
# ┏┓ ┓
# ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗
"""
To send one SMS message through the third-party client.
:param mongo_data_conn: The database connection to use to perform this task.
:param auth_token: The auth token that will be used to send this message.
:param client: The third-party SMS client to use to send this message.
:param message: The actual message that needs to be sent.
:param tags: Any tags to attach with this SMS for filtering when querying in the listing service.
:return: The structured result of sending one message.
"""
pass
@abstractmethod
async def send_many_sms(
self,
mongo_data_conn: AsyncMongo,
auth_token: CoreAuthTokenModel,
messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage],
tags: List[Any]
) -> SMSSendManyResults:
"""
To send multiple SMS messages through the third-party client.
individual message, and then aggregates the results.
:param mongo_data_conn: The database connection to use to perform this task.
:param auth_token: The auth token that will be used to send this message.
:param messages: The list of messages to send out.
:param tags: Any tags to attach with these SMS for filtering when querying in the listing service. The same tags
will be applied to all messages. Do not call this method if you need to have different tags for all of them.
:return: The structured result of sending many SMS messages.
"""
pass
pass
# *****************************************************************************************************************
@@ -10,7 +10,7 @@
OBJECTIVE:
To handle all trading related behaviour from one place.
To handle all trading related behaviour for Zerodha's Kite platform.
REFERENCES:
@@ -36,6 +36,8 @@ 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
@@ -44,6 +46,10 @@ from controllers_v2.finstitutions.trading.base import TradingController
# Models:
from models.core.auth_token import CoreAuthTokenModel
from utils_v2.trading.zerodha_kite.models.auth_tokens import ZerodhaKiteAuthTokens
# To work with MongoDB:
from bson.objectid import ObjectId
# To work with datatypes:
from typing import List, Any
@@ -51,6 +57,9 @@ from typing import List, Any
# To make HTTP requests:
import httpx
# To work with Zerodha's Kite platform:
from kiteconnect import KiteConnect
# *****************************************************************************************************************
# ***** ****
@@ -89,7 +98,7 @@ import httpx
# *****************************************************************************************************************
class AllTradingController(TradingController):
class ZerodhaKiteTradingController(TradingController):
# ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
@@ -101,12 +110,12 @@ class AllTradingController(TradingController):
http_client: httpx.AsyncClient = None,
alert_url: str = None,
debug: bool = True,
debug_prefix: str = "All SMS (C) | ",
debug_prefix: str = "Zerodha kite (C) | ",
debug_only_errors: bool = True
):
"""
This is the foundational controller for all trading services. Use this for any smaller common tasks where you
may not know the exact client beforehand.
This is the foundational controller for Zerodha's Kite 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:
@@ -115,22 +124,110 @@ class AllTradingController(TradingController):
:return: None.
"""
# Declare the client:
this_client = "zerodhaKite"
# 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 = None,
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
# ┏┓ ┓
# ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗
pass
@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 f"https://kite.zerodha.com/connect/login?api_key={kwargs['api_key']}"
async def handle_authorization_callback(
self,
sql_conn: AsyncMySQL,
mongo_data_conn: AsyncMongo,
inbound_data: dict
) -> bool:
"""
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
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.
NOTE: Please ensure that you set the 'Redirect URL' such that is passes back Kite's 'api_key' back through the
callback URL. This can be one by setting the value manually as a query param on the app's configuration
page. E.g.: http://127.0.0.1:5999/auth/callback?api_key=user_api_key
: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: The model that hold the access tokens, or None if something failed.
"""
# Start by assuming failure:
success = False
zerodha_auth_token = None
# Get the token from the database:
auth_token = await self.get_token_from_filter(
mongo_data_conn = mongo_data_conn,
filter_json = mongo_data_conn.dict_to_dot_notation({
"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 auth_token: return success
# Get the final access tokens set from Zerodha Kite:
kite = KiteConnect(api_key = auth_token.auth["apiKey"])
session_data = kite.generate_session(
request_token = inbound_data["request_token"],
api_secret = auth_token.auth["apiSecret"]
)
zerodha_auth_token = ZerodhaKiteAuthTokens(**session_data)
# Prepare the inputs to save to the database:
auth_url = await self.get_authorization_url(api_key = auth_token.auth["apiKey"])
auth_token.token = zerodha_auth_token.model_dump()
# Save the additional auth info to the database:
success = await self.set_token(
sql_conn = sql_conn,
mongo_data_conn = mongo_data_conn,
token_key = auth_token.key,
auth_token = auth_token,
token_notes = {
"apiKey": auth_token.auth["apiKey"],
"authUrl": auth_url
}
)
# Done here:
return success
# *****************************************************************************************************************
@@ -41,7 +41,7 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers:
from controllers_v2.sms.base import SMSController
from controllers_v2.message.sms.base import SMSController
# Models:
from models.core.auth_token import CoreAuthTokenModel
@@ -42,7 +42,7 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers:
from controllers_v2.sms.base import SMSController
from controllers_v2.message.sms.base import SMSController
# Models:
from models.core.auth_token import CoreAuthTokenModel
@@ -42,7 +42,7 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers:
from controllers_v2.sms.base import SMSController
from controllers_v2.message.sms.base import SMSController
# Models:
from models.core.auth_token import CoreAuthTokenModel