(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
@@ -6,11 +6,11 @@
DATE: DATE:
Monday, 25th Nov., 2024 Saturday, 21st Dec., 2024
OBJECTIVE: OBJECTIVE:
To receive callbacks (webhooks). To receive callbacks (webhooks) from stockbrokers for trading API integrations.
REFERENCES: REFERENCES:
@@ -81,7 +81,7 @@ import asyncio
# Related to Quart: # Related to Quart:
mail_oauth_callback_bp = Blueprint("mail_cb", __name__) trading_oauth_callback_bp = Blueprint("trading_cb", __name__)
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -101,7 +101,7 @@ mail_oauth_callback_bp = Blueprint("mail_cb", __name__)
# ***************************************************************************************************************** # *****************************************************************************************************************
@mail_oauth_callback_bp.record_once @trading_oauth_callback_bp.record_once
def init(blueprint_setup_state): def init(blueprint_setup_state):
# This gets called when the blueprint is registered. # This gets called when the blueprint is registered.
@@ -112,141 +112,14 @@ def init(blueprint_setup_state):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
@AsyncLoggerContext.log_it( @trading_oauth_callback_bp.route("oauth/callback/<trading_client>", methods = ["POST", "GET"])
api_version = "1.0.0",
project = constants.PROJECT_NAME,
log_type = constants.MODULE_NAME,
operation = "gmailClbk",
log_input = 2,
log_output = 1,
sensitive_keys = ["sessionToken", "X-Session-Token"]
)
async def handle_gmail_callback() -> render_template:
"""
To handle the callbacks from GMail specifically. Refer to the individual comments to check what hap[pens at each
step of the process.
:return: A rendered template (HTML) of the final status of the authorization.
"""
# Start by assuming failure:
tokens_saved = False
# 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",
mail_client = g.mail_client.title()
)
# Generate the tokens from the callback. Google sends all the needed params in the callback as the URL's query
# params. We can simply use the exact URL that was hit to generate the tokens. In Quart (and Flask) this can be
# achieved by 'request.url' like this:
google_tokens = await current_app.gmail_client.get_authorization_tokens(
redirect_url = request.url,
scopes = None
)
if google_tokens:
# Get the e-mail id that granted authorization. We will be comparing this to the e-mail id that had been given
# to us when the authorization was initiated. We don't mind any e-mail id being used, but we need them to be the
# same at both ends:
user_profile = await current_app.gmail_client.get_user_profile(tokens = google_tokens)
if user_profile.success:
google_tokens.email = user_profile.data["emailAddress"]
google_tokens.displayName = user_profile.data["displayName"]
google_tokens.displayPictureUrl = user_profile.data["displayPictureUrl"]
# Here's where we do the checking of the e-mails,
# if they don't match, we reject the authorization:
auth_token = await current_app.mail_controller.get_token_from_key(
mongo_conn = current_app.data_mongo,
token_key = g.inbound_data["state"]
)
if (
(not auth_token) or
auth_token.clientUserId["email"] != str(google_tokens.email)
): return await render_template(
"/mail/oauth/oauth_failure_v2.html",
mail_client = g.mail_client.title(),
failure_hint = (
f"We were expecting authorization from '{auth_token.clientUserId['email']}', "
f"but got authorization from '{google_tokens.email}' instead."
)
)
# We create standard labels that we will use:
labels = [
{
"name": "TCAOFF",
"textColor": "#434343",
"backgroundColor": "#e7e7e7"
},
{
"name": "CA-Doc",
"textColor": "#434343",
"backgroundColor": "#e7e7e7"
},
{
"name": "CA-AI",
"textColor": "#434343",
"backgroundColor": "#e7e7e7"
}
]
tasks = [
current_app.gmail_client.create_label(
tokens = google_tokens,
label_name = label["name"],
label_visibility = "labelShow",
message_visibility = "show",
label_text_color = label["textColor"],
label_background_color = label["backgroundColor"]
) for label in labels
]
client_responses = await asyncio.gather(*tasks)
# Add the labels to the tokens data:
client_response = await current_app.gmail_client.list_labels(tokens = google_tokens)
google_tokens.labels = client_response.data if client_response.success else None
# Now that we have passed the check,
# we save the tokens to the database:
auth_token.clientUserId = google_tokens.client_user_id
auth_token.token = google_tokens.model_dump()
auth_token.status = "active"
tokens_saved = await current_app.mail_controller.set_token(
db_conn = current_app.sql_writer,
mongo_conn = current_app.data_mongo,
session_token = g.inbound_headers.get("X-Session-Token"),
token_key = g.inbound_data["state"],
auth_token = auth_token
)
# Return an HTML response for success:
if tokens_saved: return await render_template(
"/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",
mail_client = g.mail_client.title(),
failure_hint = f"Unknown error. Please use log-id '{g.log_id}' to check with the support team."
)
# ---------------------------------------------------------------------------------------------------------------------
@mail_oauth_callback_bp.route("/callback/<mail_client>", methods = ["POST", "GET"])
@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(
attr_name = "logs_mongo", attr_name = "logs_mongo",
project = constants.PROJECT_NAME, project = constants.PROJECT_NAME,
log_type = constants.MODULE_NAME, log_type = constants.MODULE_NAME,
operation = "gmailCllBckApi", operation = "trdngOauthCllBckApi",
log_input = True, log_input = True,
log_output = True, log_output = True,
sensitive_keys = None sensitive_keys = None
@@ -254,8 +127,8 @@ async def handle_gmail_callback() -> render_template:
@log_chain_to_mongo(attr_name = "logs_mongo") @log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance") @should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@handle_cancelled_request() @handle_cancelled_request()
async def mail_auth_callback( async def trading_oauth_callback(
mail_client: str = None, trading_client: 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,
@@ -263,8 +136,9 @@ async def mail_auth_callback(
): ):
""" """
Use this when authorizing access to someone's GMail account. This can be used to capture the authentication token. This endpoint gets triggered by the stockbroker's servers to let you know when a user accepted or rejected an
:param mail_client: The mail company/brand that you want the authorization from. authorization request.
:param trading_client: The name of the stockbroker that you have received the callback from.
: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.
@@ -272,45 +146,36 @@ async def mail_auth_callback(
:return: A standard response structure. :return: A standard response structure.
""" """
# ┓┏ ┓┓ ┓┏ • ┓ ┓ # Start by assuming failure:
# ┣┫┏┓┏┓┏┫┃┏┓ ┃┃┏┓┏┓┓┏┓┣┓┃┏┓┏ success = None
# ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┛ ┗┗┻┗┛┗┗ ┛
# Here we make various variables available in the scope of the current request through 'g': # ┏┓ ┏┓ ┓┓ ┓┏┓•
g.log_id = kwargs.get("log_id") # ┣ ┏┓┏┓ ┏┛┏┓┏┓┏┓┏┫┣┓┏┓ ┃┫ ┓╋┏┓
g.inbound_headers = inbound_headers # ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗
g.inbound_data = inbound_data
g.mail_client = mail_client
# ┓┏ ┓┓ ┏┓ ┓┓┓ ┓ if trading_client == "zerodha":
# ┣┫┏┓┏┓┏┫┃┏┓ ┃ ┏┓┃┃┣┓┏┓┏┃┏┏ success = await current_app.zerodha_kite_controller.handle_authorization_callback(
# ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┗┗┗┛┗┻┗┛┗┛ sql_conn = current_app.sql_writer,
mongo_data_conn = current_app.data_mongo,
try: inbound_data = inbound_data
if mail_client == "gmail": return await handle_gmail_callback()
# If something goes wrong:
except Exception as exception: return await render_template(
"/mail/oauth/oauth_failure_v2.html",
mail_client = mail_client.title(),
failure_hint = (
f"An internal server error occurred. "
f"Please use log-id '{g.log_id}' to check with the support team."
) )
# ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
# ┛
# 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,
return await render_template(
"/mail/oauth/oauth_failure_v2.html",
mail_client = mail_client.title(),
failure_hint = (
f"Invalid client '{mail_client}' selected. "
f"Please use log-id '{g.log_id}' to check with the support team."
)
) )
@@ -59,18 +59,15 @@ from utils_v2.api.async_quart import (
handle_cancelled_request handle_cancelled_request
) )
# GMail-related utils:
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
# Common: # Common:
from shared import constants from shared import constants
# Data Models: # Data Models:
from models.api.mail.oauth import (
OAuthMailAuthorizationRequestHeaders,
OAuthMailAuthorizationRequestData
)
from models.core.auth_token import CoreAuthTokenModel from models.core.auth_token import CoreAuthTokenModel
from models.api.finstitutions.trading.auth import (
TradingAuthRequestHeaders,
TradingAuthRequestData
)
# For asynchronous activities: # For asynchronous activities:
import asyncio import asyncio
@@ -131,13 +128,13 @@ def init(blueprint_setup_state):
@log_chain_to_mongo(attr_name = "logs_mongo") @log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance") @should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input( @validate_input(
header_validator = lambda x: OAuthMailAuthorizationRequestHeaders(**x).model_dump(), header_validator = lambda x: TradingAuthRequestHeaders(**x).model_dump(),
data_validator = lambda x: OAuthMailAuthorizationRequestData(**x) data_validator = lambda x: TradingAuthRequestData(**x)
) )
@handle_cancelled_request() @handle_cancelled_request()
async def request_oauth_authorization_url( async def request_oauth_authorization_url(
inbound_headers: dict | OAuthMailAuthorizationRequestHeaders = None, inbound_headers: dict | TradingAuthRequestHeaders = None,
inbound_data: dict | OAuthMailAuthorizationRequestData = None, inbound_data: dict | TradingAuthRequestData = None,
inbound_files: dict = None, inbound_files: dict = None,
**kwargs **kwargs
): ):
@@ -168,48 +165,47 @@ async def request_oauth_authorization_url(
# Start by assuming failure: # Start by assuming failure:
auth_url = None auth_url = None
# ┏┓ ┏┳┓ ┓ ┓┏┓ # ┏┓ ┏┓ ┓┓ ┓┏┓
# ┃┓┏┓┏┓┏┓┏┓┏┓┏┓ ┃ ┏┓┃┏┏┓┏┓ ┃┫ ┏┓┓┏ # ┏┓┏┓ ┏┛┏┓┏┓┏┓┏┫┣┓┏┓ ┃┫ ┓╋┏┓
# ┗┛┗ ┛┗ ┛ ┗┻┗┗ ┻ ┗┛┛┗┗ ┛┗ ┛┗┛┗ ┗┫ # ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┗┗┻ ┛┗┛┗┗┗
# ┛
# Make a user identifier from the session info: # PLANNED FLOW FOR ZERODHA-KITE:
token_key = await current_app.mail_controller.generate_token_key( # Step 01.: (One time) The user will go to the integrations page and add his API Key and SPI Secret there. We store
db_conn = current_app.sql_writer, # these values without verification.
mongo_conn = current_app.data_mongo, # Step 02.: (Daily) The user will go to the investments tab and click on his Zerodha account, which will give him
auth_token = CoreAuthTokenModel( # a URL that will take him to Zerodha's official site to log in. When he logs in, Zerodha will hit our
serviceType = "email", # callback URL and give us the authentication details.
client = inbound_data.mailClient,
authType = "oauth", if inbound_data.client == "zerodhaKite":
user = kwargs["session_info"],
clientUserId = {"email": inbound_data.mailId}, # Prepare the inputs:
status = "pending", auth_url = await current_app.zerodha_kite_controller.get_authorization_url(api_key = inbound_data.auth.apiKey)
syncFreq = inbound_data.syncFreq,
), # Immediately save the details against that token id:
session_token = inbound_headers["X-Session-Token"] success = await current_app.zerodha_kite_controller.set_token_direct(
) sql_conn = current_app.sql_writer,
if token_key is None: mongo_data_conn = current_app.data_mongo,
return ResponseModel( auth_token = CoreAuthTokenModel(
status_code = StatusCodes.FAILED, serviceType = "stockTrading",
http_code = HttpCodes.INTERNAL_SERVER_ERROR, client = inbound_data.client,
message = "Failed to generate token key." 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
},
session_token = inbound_headers["X-Session-Token"]
) )
# ┏┓ ┏┓┳┳┓ •┓ # Check if things were successful:
# ┣ ┏┓┏┓ ┃┓┃┃┃┏┓┓┃ if not success: auth_url = None
# ┻ ┗┛┛ ┗┛┛ ┗┗┻┗┗
if inbound_data.mailClient == "gmail":
# Get the authorization URL:
auth_url = await current_app.gmail_client.get_authorization_url(
scopes = SCOPES_GMAIL_MAIL_MANAGEMENT,
state = str(token_key),
access_type = "offline",
approval_prompt = "force",
include_granted_scopes = "true",
user_email = inbound_data.mailId
)
# ┳┓ # ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
@@ -221,7 +217,7 @@ async def request_oauth_authorization_url(
status_code = StatusCodes.OK if auth_url else StatusCodes.FAILED, status_code = StatusCodes.OK if auth_url else StatusCodes.FAILED,
http_code = HttpCodes.SUCCESS if auth_url else HttpCodes.INTERNAL_SERVER_ERROR, http_code = HttpCodes.SUCCESS if auth_url else HttpCodes.INTERNAL_SERVER_ERROR,
data = { data = {
"client": inbound_data.mailClient, "client": inbound_data.client,
"authorizationUrl": auth_url "authorizationUrl": auth_url
} }
) )
+32 -6
View File
@@ -75,9 +75,13 @@ from controllers.api.payment import PaymentController
# Controllers V2: # Controllers V2:
from controllers_v2.core.auth_token import CoreAuthTokenController from controllers_v2.core.auth_token import CoreAuthTokenController
from controllers_v2.sms.all_sms import AllSMSController # ---
from controllers_v2.sms.nimbus_sms_india import NimbusSMSIndiaController from controllers_v2.message.sms.all_sms import AllSMSController
from controllers_v2.sms.savvy_bulk_sms_kenya import SavvyBulkSMSKenyaController from controllers_v2.message.sms.nimbus_sms_india import NimbusSMSIndiaController
from controllers_v2.message.sms.savvy_bulk_sms_kenya import SavvyBulkSMSKenyaController
# ---
from controllers_v2.finstitutions.trading.all_trading import AllTradingController
from controllers_v2.finstitutions.trading.zerodha_kite import ZerodhaKiteTradingController
# To make REST API calls: # To make REST API calls:
import httpx import httpx
@@ -107,7 +111,7 @@ from api.blueprints.sms.tags import sms_update_tags_bp
# Software Blueprints: # Software Blueprints:
from api.blueprints.software.auth import sw_auth_bp from api.blueprints.software.auth import sw_auth_bp
# Payment Blueprints: # Finstitutions / Payment Blueprints:
from api.blueprints.finstitutions.payments.auth import pg_auth_bp from api.blueprints.finstitutions.payments.auth import pg_auth_bp
from api.blueprints.finstitutions.payments.request import pg_request_bp from api.blueprints.finstitutions.payments.request import pg_request_bp
from api.blueprints.finstitutions.payments.callback import pg_callback_bp from api.blueprints.finstitutions.payments.callback import pg_callback_bp
@@ -115,6 +119,10 @@ from api.blueprints.finstitutions.payments.list import pg_list_bp
from api.blueprints.finstitutions.payments.get import pg_get_bp from api.blueprints.finstitutions.payments.get import pg_get_bp
from api.blueprints.finstitutions.payments.tags import pg_tags_update_bp from api.blueprints.finstitutions.payments.tags import pg_tags_update_bp
# Finstitutions / Trading Blueprints:
from api.blueprints.finstitutions.trading.oauth.request import trading_oauth_request_bp
from api.blueprints.finstitutions.trading.oauth.callback import trading_oauth_callback_bp
# AI Blueprints: # AI Blueprints:
from api.blueprints.ai.llm.invoke import llm_invoke_bp from api.blueprints.ai.llm.invoke import llm_invoke_bp
@@ -171,7 +179,7 @@ app.register_blueprint(sms_update_tags_bp, url_prefix = f"/{MODULE_BASE}/sms")
# Software Blueprints: # Software Blueprints:
app.register_blueprint(sw_auth_bp, url_prefix = f"/{MODULE_BASE}/software") app.register_blueprint(sw_auth_bp, url_prefix = f"/{MODULE_BASE}/software")
# Payment Blueprints: # Finstitutions / Payment Blueprints:
app.register_blueprint(pg_auth_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_auth_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(pg_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(pg_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
@@ -179,6 +187,10 @@ app.register_blueprint(pg_list_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/p
app.register_blueprint(pg_get_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_get_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
app.register_blueprint(pg_tags_update_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") app.register_blueprint(pg_tags_update_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments")
# Finstitutions / Trading Blueprints:
app.register_blueprint(trading_oauth_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading")
app.register_blueprint(trading_oauth_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading")
# AI Blueprints: # AI Blueprints:
app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai") app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai")
@@ -416,7 +428,7 @@ async def app_startup(**kwargs):
debug = enable_debugging debug = enable_debugging
) )
# SMS Controllers: # Messages / SMS Controllers:
current_app.sms_controller = AllSMSController( current_app.sms_controller = AllSMSController(
cache = current_app.module_cache, cache = current_app.module_cache,
http_client = current_app.http_client, http_client = current_app.http_client,
@@ -436,6 +448,20 @@ async def app_startup(**kwargs):
debug = enable_debugging debug = enable_debugging
) )
# Finstitutions / Trading Controllers:
current_app.trading_controller = AllTradingController(
cache = current_app.module_cache,
http_client = current_app.http_client,
alert_url = current_app.script_data["alerts"]["url"],
debug = enable_debugging
)
current_app.zerodha_kite_controller = ZerodhaKiteTradingController(
cache = current_app.module_cache,
http_client = current_app.http_client,
alert_url = current_app.script_data["alerts"]["url"],
debug = enable_debugging
)
# ┏┓ ┓ ┏┓┓• # ┏┓ ┓ ┏┓┓•
# ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ ┏┓┏┓┏┫ ┃ ┃┓┏┓┏┓╋┏ # ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ ┏┓┏┓┏┫ ┃ ┃┓┏┓┏┓╋┏
# ┗┛┗┛┛┗┛┗┗ ┗┗┗┛┛ ┛ ┗┻┛┗┗┻ ┗┛┗┗┗ ┛┗┗┛ # ┗┛┗┛┛┗┛┗┗ ┗┗┗┛┛ ┛ ┗┻┛┗┗┻ ┗┛┗┗┗ ┛┗┗┛
+43
View File
@@ -103,6 +103,10 @@ class CoreAuthTokenController(CoreBaseModel):
# For MongoDB: # For MongoDB:
AUTH_COLLECTION = "_authTokens" 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. :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: # Note down the timestamp at which this event occurred:
request_ts = date_time.get_current_utc_date_time(as_string = False) 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. :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: # Start by assuming failure:
token_saved = False token_saved = False
@@ -430,6 +446,33 @@ class CoreAuthTokenController(CoreBaseModel):
# Done here: # Done here:
return CoreAuthTokenModel(**token) if token else None 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( async def get_tokens_from_ids(
self, self,
mongo_data_conn: AsyncMongo, 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 from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers: # Controllers:
from controllers_v2.core.auth_token import CoreAuthTokenController from controllers_v2.finstitutions.trading.base import TradingController
# Models: # Models:
from models.core.auth_token import CoreAuthTokenModel 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, cache: AsyncRedisCache = None,
http_client: httpx.AsyncClient = None, http_client: httpx.AsyncClient = None,
alert_url: str = None, alert_url: str = None,
base_filter: dict = None,
debug: bool = True, debug: bool = True,
debug_prefix: str = "Trading (C) | ", debug_prefix: str = "All Trading (C) | ",
debug_only_errors: bool = True debug_only_errors: bool = True
): ):
""" """
This is the foundational controller for all SMS services. This is built on top of the core message controller, This is the foundational controller for all trading services. Use this for any smaller common tasks where you
and, in turn, all individual SMS client controllers must be built on top of this. may not know the exact client beforehand.
:param cache: The object to use for caching results from database calls. :param cache: The object to use for caching results from database calls.
:param http_client: The HTTP client :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: Whether, or not, you would like to print debugging messages:
:param debug_prefix: The prefix to print with the 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. :param debug_only_errors: Whether you would like to print only error messages or all messages.
:return: None. :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: # Invoke the parent's constructor:
CoreAuthTokenController.__init__( super().__init__(
self,
cache = cache, cache = cache,
alert_url = alert_url, alert_url = alert_url,
http_client = http_client, http_client = http_client,
base_filter = this_filter, base_filter = None,
debug = debug, debug = debug,
debug_prefix = debug_prefix, debug_prefix = debug_prefix,
debug_only_errors = debug_only_errors debug_only_errors = debug_only_errors
@@ -140,23 +131,7 @@ class TradingController(CoreAuthTokenController):
# ┣┫┓┏╋┣┓ # ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗ # ┛┗┗┻┗┛┗
# async def login_url( pass
# 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
# ***************************************************************************************************************** # *****************************************************************************************************************
+22 -68
View File
@@ -6,11 +6,11 @@
DATE: DATE:
Thursday, 19th Dec., 2024 Saturday, 21st Dec., 2024
OBJECTIVE: OBJECTIVE:
To handle all SMS related behaviour from one place. To handle all trading related behaviour from one place.
REFERENCES: REFERENCES:
@@ -40,20 +40,10 @@ from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers: # Controllers:
from controllers_v2.core.message import CoreMessageController 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.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: # To work with datatypes:
from typing import List, Any 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, alert_url: str = None,
base_filter: dict = None, base_filter: dict = None,
debug: bool = True, debug: bool = True,
debug_prefix: str = "SMS (C) | ", debug_prefix: str = "Trading (C) | ",
debug_only_errors: bool = True debug_only_errors: bool = True
): ):
""" """
This is the foundational controller for all SMS services. This is built on top of the core message controller, This is the foundational controller for all trading/stockbroking services. This is built on top of the
and, in turn, all individual SMS client controllers must be built on top of this. 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 cache: The object to use for caching results from database calls.
:param http_client: The HTTP client :param http_client: The HTTP client
:param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE :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. :return: None.
""" """
# Prepare the combined base filter: # Declare the service type:
sms_filter = {} this_service_type = "stockTrading"
for k, v in (base_filter or {}).items(): sms_filter[k] = v
sms_filter["serviceType"] = "sms" # 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: # Invoke the parent's constructor:
CoreMessageController.__init__( CoreAuthTokenController.__init__(
self, self,
cache = cache, cache = cache,
alert_url = alert_url, alert_url = alert_url,
http_client = http_client, http_client = http_client,
base_filter = sms_filter, base_filter = this_filter,
debug = debug, debug = debug,
debug_prefix = debug_prefix, debug_prefix = debug_prefix,
debug_only_errors = debug_only_errors 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:
""" pass
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
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -10,7 +10,7 @@
OBJECTIVE: OBJECTIVE:
To handle all trading related behaviour from one place. To handle all trading related behaviour for Zerodha's Kite platform.
REFERENCES: REFERENCES:
@@ -36,6 +36,8 @@ sys.path.append(".")
sys.path.append("..") sys.path.append("..")
# My async utils: # 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.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
@@ -44,6 +46,10 @@ 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 utils_v2.trading.zerodha_kite.models.auth_tokens import ZerodhaKiteAuthTokens
# To work with MongoDB:
from bson.objectid import ObjectId
# To work with datatypes: # To work with datatypes:
from typing import List, Any from typing import List, Any
@@ -51,6 +57,9 @@ from typing import List, Any
# To make HTTP requests: # To make HTTP requests:
import httpx 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, http_client: httpx.AsyncClient = None,
alert_url: str = None, alert_url: str = None,
debug: bool = True, debug: bool = True,
debug_prefix: str = "All SMS (C) | ", debug_prefix: str = "Zerodha kite (C) | ",
debug_only_errors: bool = True debug_only_errors: bool = True
): ):
""" """
This is the foundational controller for all trading services. Use this for any smaller common tasks where you This is the foundational controller for Zerodha's Kite platform.
may not know the exact client beforehand.
:param cache: The object to use for caching results from database calls. :param cache: The object to use for caching results from database calls.
:param http_client: The HTTP client :param http_client: The HTTP client
:param debug: Whether, or not, you would like to print debugging messages: :param debug: Whether, or not, you would like to print debugging messages:
@@ -115,22 +124,110 @@ class AllTradingController(TradingController):
:return: None. :return: None.
""" """
# Declare the client:
this_client = "zerodhaKite"
# Prepare base filter:
this_filter = {"client": this_client}
# Invoke the parent's constructor: # Invoke the parent's constructor:
super().__init__( super().__init__(
cache = cache, cache = cache,
alert_url = alert_url, alert_url = alert_url,
http_client = http_client, http_client = http_client,
base_filter = None, base_filter = this_filter,
debug = debug, debug = debug,
debug_prefix = debug_prefix, debug_prefix = debug_prefix,
debug_only_errors = debug_only_errors 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 from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers: # Controllers:
from controllers_v2.sms.base import SMSController from controllers_v2.message.sms.base import SMSController
# Models: # Models:
from models.core.auth_token import CoreAuthTokenModel 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 from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers: # Controllers:
from controllers_v2.sms.base import SMSController from controllers_v2.message.sms.base import SMSController
# Models: # Models:
from models.core.auth_token import CoreAuthTokenModel 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 from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
# Controllers: # Controllers:
from controllers_v2.sms.base import SMSController from controllers_v2.message.sms.base import SMSController
# Models: # Models:
from models.core.auth_token import CoreAuthTokenModel from models.core.auth_token import CoreAuthTokenModel
+16 -20
View File
@@ -75,25 +75,21 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]
# ***************************************************************************************************************** # *****************************************************************************************************************
class SafaricomMPesaExpressAuth(BaseModel): class ZerodhaKiteAuth(BaseModel):
consumerKey: str = Field( apiKey: str = Field(
description = "the app's consumer key given by safaricom; found in 'my apps'", description = (
"the api key of your kite connect app; "
"this remains constant throughout the life of the app"
),
frozen = True frozen = True
) )
consumerSecret: str = Field( apiSecret: str = Field(
description = "the app's consumer secret given by safaricom; found in 'my apps'", description = (
frozen = True "the api secret of your kite connect app; "
) "this can change if you think the security of your app has been compromised"
),
businessShortCode: str = Field(
description = "your app's business short code; found in 'my apps'",
frozen = True
)
appPasskey: str = Field(
description = "your app's passkey; taken from human representative",
frozen = True frozen = True
) )
@@ -109,7 +105,7 @@ class SafaricomMPesaExpressAuth(BaseModel):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
class PGAuthRequestHeaders(BaseModel): class TradingAuthRequestHeaders(BaseModel):
sessionToken: str = Field( sessionToken: str = Field(
description = "the session token of the user who is requesting the service", description = "the session token of the user who is requesting the service",
@@ -133,10 +129,10 @@ class PGAuthRequestHeaders(BaseModel):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
class PGAuthRequestData(BaseModel): class TradingAuthRequestData(BaseModel):
client: Literal["safaricomMPesaExpress"] = Field(alias = "client") client: Literal["zerodhaKite"] = Field(alias = "client")
auth: Union[SafaricomMPesaExpressAuth] auth: Union[ZerodhaKiteAuth]
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
@@ -155,7 +151,7 @@ class PGAuthRequestData(BaseModel):
client = values.client client = values.client
auth = values.auth auth = values.auth
harmony_map = { harmony_map = {
"safaricomMPesaExpress": SafaricomMPesaExpressAuth "zerodhaKite": ZerodhaKiteAuth
} }
if not isinstance(auth, harmony_map[client]): if not isinstance(auth, harmony_map[client]):
raise ValueError(f"incorrect 'auth' for selected client '{client}'") raise ValueError(f"incorrect 'auth' for selected client '{client}'")
+6 -1
View File
@@ -95,7 +95,11 @@ class CoreAuthTokenModel(BaseModel):
default_factory = lambda: ObjectId() default_factory = lambda: ObjectId()
) )
serviceType: Literal["software", "email", "sms", "chat", "paymentGateway"] = Field( serviceType: Literal[
"email", "sms", "chat", # ............. Message
"paymentGateway", "stockTrading", # ... Finstitutions
"software", # ......................... God knows
] = Field(
description = "the kind of service this message was sent/received from", description = "the kind of service this message was sent/received from",
frozen = True frozen = True
) )
@@ -105,6 +109,7 @@ class CoreAuthTokenModel(BaseModel):
"telegram", "whatsapp", # .................. Chat Clients "telegram", "whatsapp", # .................. Chat Clients
"nimbusSmsIndia", "savvyBulkSmsKenya", # ... SMS Clients "nimbusSmsIndia", "savvyBulkSmsKenya", # ... SMS Clients
"razorpay", "safaricomMPesaExpress", # ..... Payment Gateways "razorpay", "safaricomMPesaExpress", # ..... Payment Gateways
"zerodhaKite", # ........................... Stock Brokers
"theCaOfficeAi" # .......................... Software "theCaOfficeAi" # .......................... Software
] = Field( ] = Field(
description = "the third-part client that was used", description = "the third-part client that was used",
+251 -104
View File
@@ -6,16 +6,15 @@
DATE: DATE:
Create: Saturday, 18th May, 2022 Saturday, 21st Dec. 2024
Update: Thursday, 22nd Aug. 2024
OBJECTIVE: OBJECTIVE:
To provide an easy way to work with '.json' data and files. To simulate stock market updates to test on SocketIO.
REFERENCES: REFERENCES:
1) https://www.w3schools.com/python/python_json.asp N/A
DOWNLOADS: DOWNLOADS:
@@ -38,12 +37,27 @@ sys.path.append("..")
# System-level activities: # System-level activities:
import io import io
import os
# To work with the JSON standard: # My utils:
import json from utils_v2.string import json
# To work with files: # For pseudo-random simulations:
from utils_v2.system import files import random
# To work with SocketIO
import socketio
from aiohttp import web
# To make HTTP calls:
import httpx
# To work with date and time:
import datetime
import time
# For asynchronous behaviour:
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -53,7 +67,64 @@ from utils_v2.system import files
# ***************************************************************************************************************** # *****************************************************************************************************************
# --- Nothing Yet # For SocketIO:
# Create a Socket.IO server instance
sio = socketio.AsyncServer(cors_allowed_origins = "*")
app = web.Application()
sio.attach(app)
# A list of stocks to simulate:
SYMBOL_TO_PRICE_MAP = {
"HDFCBANK": {
"prevClose": 1_763.95,
"ltp": 1_771.50,
"totVol": 55_96_931,
"buyVol": 16_79_079,
"sellVol": 39_17_852,
},
"RELIANCE": {
"prevClose": 1_213.35,
"ltp": 1_205.30,
"totVol": 7_34_568,
"buyVol": 1_04_873,
"sellVol": 6_29_695,
},
"INFY": {
"prevClose": 1_925.70,
"ltp": 1_922.15,
"totVol": 5_54_108,
"buyVol": 2_61_593,
"sellVol": 2_92_515,
},
"TCS": {
"prevClose": 4_203.50,
"ltp": 4_170.30,
"totVol": 7_24_932,
"buyVol": 1_34_666,
"sellVol": 5_90_266,
},
"HINDUNILVR": {
"prevClose": 2_312.95,
"ltp": 2_333.90,
"totVol": 5_04_533,
"buyVol": 9_252,
"sellVol": 4_95_281,
},
"ITC": {
"prevClose": 463.20,
"ltp": 464.65,
"totVol": 7_07_905,
"buyVol": 3_27_422,
"sellVol": 3_80_483,
},
"KOTAKBANK": {
"prevClose": 1_751.65,
"ltp": 1_743.55,
"totVol": 4_49_104,
"buyVol": 2_47_489,
"sellVol": 2_01_615,
}
}
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -73,121 +144,181 @@ from utils_v2.system import files
# ***************************************************************************************************************** # *****************************************************************************************************************
def from_string(json_data): @sio.event
async def connect(sid, environ):
""" print(f"Client {sid} connected")
Decodes a JSON string to a pythonic variable like a dict. async with httpx.AsyncClient() as client:
:param json_data: The JSON string to decode. try: await client.post(
:return: The decoded pythonic variable. url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend",
""" json = {
"type": "info",
python_data = json.loads(json_data) "chatClient": "telegram",
return python_data "chatId": "-4206946032",
# "chatId": "1275560043",
"message": f"*SocketIO Connected!*\n👍 SID: {sid}"
# --------------------------------------------------------------------------------------------------------------------- }
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 = (',', ':')
) )
except: pass
else:
json_data = json.dumps( # ---------------------------------------------------------------------------------------------------------------------
python_data,
indent = indent,
default = default, @sio.event
separators = separators async def disconnect(sid):
print(f"Client {sid} disconnected")
async with httpx.AsyncClient() as client:
try: await client.post(
url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend",
json = {
"type": "info",
"chatClient": "telegram",
"chatId": "-4206946032",
# "chatId": "1275560043",
"message": f"*SocketIO Disconnected!*\n❌ SID: {sid}"
}
) )
except: pass
return json_data
def round_tick(price):
return round(price * 20) / 20
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
def from_file(file): def simulate_one_stock(symbol, price):
""" global SYMBOL_TO_PRICE_MAP
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): # Simulate a change in the price:
file.seek(0) pos_bias = [1] * 10
json_data = file.getvalue() no_bias = [0] * 1
else: json_data = files.read_file(file) neg_bias = [-1] * 10
python_data = from_string(json_data) bias = random.choice(pos_bias + no_bias + neg_bias)
return python_data change_factor = random.random() / 100.0
change = price * change_factor * bias
ltp = round_tick(price + change)
# Simulate the volume.
# Assume a trade qty. worth 1L to 10L rupees:
traded_amt = random.uniform(1_00_000, 10_00_000)
ltq = int(traded_amt / price)
SYMBOL_TO_PRICE_MAP[symbol]["totVol"] += ltq
if bias >= 0: SYMBOL_TO_PRICE_MAP[symbol]["buyVol"] += ltq
else: SYMBOL_TO_PRICE_MAP[symbol]["sellVol"] += ltq
# Create the basic JSON payload:
stock_json = {
"symbol": symbol,
"last_traded_quantity": ltq,
"average_traded_price": round_tick(price + (bias * price * (random.random() / 100.0))),
"volume_traded": SYMBOL_TO_PRICE_MAP[symbol]["totVol"],
"total_buy_quantity": SYMBOL_TO_PRICE_MAP[symbol]["buyVol"],
"total_sell_quantity": SYMBOL_TO_PRICE_MAP[symbol]["sellVol"],
"ohlc": {
"open": round_tick(price + (price * 0.005)),
"high": round_tick(price + (price * 0.015)),
"low": round_tick(price - (price * 0.015)),
"close": ltp
},
"change": ((ltp - SYMBOL_TO_PRICE_MAP[symbol]["prevClose"]) / SYMBOL_TO_PRICE_MAP[symbol]["prevClose"]) * 100,
"last_trade_time": (datetime.datetime.now() - datetime.timedelta(seconds = random.uniform(0.0, 2.5))).strftime("%Y-%m-%d %H:%M:%S"),
"oi": 0,
"oi_day_high": 0,
"oi_day_low": 0,
"exchange_timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"depth": {
"buy": [
{
"quantity": random.randint(0, 100),
"price": round(ltp - 0.05, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp - 0.10, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp - 0.15, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp - 0.20, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp - 0.25, 2),
"orders": random.randint(0, 10)
}
],
"sell": [
{
"quantity": random.randint(0, 100),
"price": round(ltp + 0.05, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp + 0.10, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp + 0.15, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp + 0.20, 2),
"orders": random.randint(0, 10)
},
{
"quantity": random.randint(0, 100),
"price": round(ltp + 0.25, 2),
"orders": random.randint(0, 10)
}
]
}
}
# Done here:
return stock_json
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
def to_file( def simulate_ticks_once():
file,
python_data,
indent = 4,
default = None,
separators = None,
no_space = False
):
""" # Pick a no. of stocks to simulate:
count = random.randint(1, len(SYMBOL_TO_PRICE_MAP))
symbols = random.sample(list(SYMBOL_TO_PRICE_MAP.keys()), count)
:param file: Either a path to a file on disk, or a buffer in RAM in the form of a BytesIO object. # Create the tick JSON:
:param python_data: The pythonic data to be converted to the JSON string. tick_json = [
:param indent: The tab-width for pretty presentation. simulate_one_stock(
:param default: The function to use on something that cannot be directly parsed into a JSON string. symbol = symbol,
:param separators: Custom separators to use. price = SYMBOL_TO_PRICE_MAP[symbol]["ltp"]
:param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not ) for symbol in symbols
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( # Done here:
python_data, return tick_json
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 async def broadcast_random_data():
except: return False while True:
await sio.emit("ticks", simulate_ticks_once())
await asyncio.sleep(random.uniform(0.15, 1.0))
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -199,4 +330,20 @@ def to_file(
if __name__ == "__main__": if __name__ == "__main__":
pass async def server():
# Start broadcasting random data in the background
asyncio.create_task(broadcast_random_data())
# Run the web server
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", 5000)
print("Server running on http://0.0.0.0:5000")
await site.start()
# Keep the server running
while True:
await asyncio.sleep(3600)
asyncio.run(server())