""" AUTHOR: Khushal P Soonderji DATE: Create: Monday, 29th Sept., 2025 OBJECTIVE: To have a centralized Socket.IO app from where several namespaces can be registered. This is kind of like how you can have one Quart app and register several blueprints. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append("../wsio") sys.path.append("..") # System-level activities: import io import os import random # My utils: from utils_v2.string import json from utils_v2.string import regex from utils_v2.system import files from utils_v2.date_time import date_time from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.queue.kafka.controllers.async_kafka import ConsumerKafka, get_ssl_context from utils_v2.cache.async_redis_cache_v3 import AsyncRedisCache from utils_v2.serialization.json_serializer import JSONSerializer # To make HTTP calls: import httpx # To work with date and time: import datetime import time # Models: from models.core.user import CoreUserInfoModel from models.finstitutions.trading.ticks import TradingTick # To work with SocketIO: import socket import socketio # For asynchronous activities: import asyncio # To work with various datatypes: from typing import List # Debugging: from icecream import IceCreamDebugger # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # Debugging: printer = IceCreamDebugger(prefix = "WSIO | ", includeContext = True) no_context_printer = IceCreamDebugger(prefix = "WSIO | ", includeContext = False) # To make API calls: http_client = httpx.AsyncClient( limits = httpx.Limits( max_connections = 100, # ............ Maximum number of connections allowed in the pool. max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive. ), timeout = httpx.Timeout( pool = 120.0, # .... Time to wait for a free connection from the pool. connect = 2.5, # ... Time to wait for establishing a connection to the server. write = 10.0, # .... Time to wait for sending data. read = 9.9 # ....... Time to wait for receiving data. ) ) # General: SERVER_HOSTNAME = str(socket.gethostname()) # For SocketIO: # Namespaces: NAMESPACE_TICKS = "/ticks" NAMESPACE_ORDER_UPDATES = "/order-updates" # Events: EVENT_CONNECT = "connect" EVENT_DISCONNECT = "disconnect" EVENT_ECHO = "echo" EVENT_TICKS = "ticks" # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # For SocketIO: ALLOWED_ORIGINS = [] sio = socketio.AsyncServer( cors_allowed_origins = "*", async_mode = "asgi" ) app = socketio.ASGIApp(sio) # A custom class to maintain the app's state: class AppState: def __init__(self): self.script_data = {} self.init_done = False self.connected_clients = {} self.exclusive_lock = asyncio.Semaphore(1) app_state = AppState() # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** def origin_is_allowed(origin: str) -> bool: """ To check if a given origin is in the allowed list. :param origin: The origin of your request. :return: True if allowed, else False. """ # Start by assuming failure: is_allowed = False # Check through all the allowed origins: for allowed in ALLOWED_ORIGINS: try: if regex.match(origin, allowed): is_allowed = True break except Exception as exception: printer(exception) # Done here: return is_allowed # --------------------------------------------------------------------------------------------------------------------- async def init( script_id: str, debug: bool ): """ To initialize all credentials, instances, and connectivity for this whole script. :param script_id: The id to use to load cred and data from the internal service. :param debug: Whether, or not, you would like to print the debug messages. :return: True if initialized successfully, else False. """ # Declare the required global variables: global SCRIPT_DATA global ALLOWED_ORIGINS global redis_cache global kafka_consumer # Basic stuff: if debug: printer.enable() printer("Initializing.") # ┏┓ • • # ┃┃┏┓┓┏┓┓┏┓┏ # ┗┛┛ ┗┗┫┗┛┗┛ # ┛ response = await http_client.post( url = r"https://api.thecaoffice.com/ca/get/title", headers = {"Origin": "https://thecaoffice.com/"}, data = { "domainName": "127.0.0.1:1234", "screenWidth": 1920, "screenHeight": 1080 } ) if response.status_code not in [200]: print("FATAL: ALLOWED ORIGINS NOT FETCHED!") return False ALLOWED_ORIGINS = [origin["domain"] for origin in response.json().get("data", {}).get("rs2", [])] printer(ALLOWED_ORIGINS) if len(ALLOWED_ORIGINS) < 1: print("FATAL: ALLOWED ORIGINS IS EMPTY!") return False # ┏┓ ┓ ┓ ┳┓ # ┃ ┏┓┏┓┏┫ ┏┓┏┓┏┫ ┃┃┏┓╋┏┓ # ┗┛┛ ┗ ┗┻ ┗┻┛┗┗┻ ┻┛┗┻┗┗┻ # Get the script credentials: response = await http_client.get( url = r"https://nexcom.ditscentre.in/internal/cred/get", headers = {"X-Script-Id": script_id} ) if response.status_code not in [200]: print("FATAL: SCRIPT CREDENTIALS LOADING FAILED!") return False script_cred = response.json().get("data") # Get the script data: response = await http_client.get( url = r"https://nexcom.ditscentre.in/internal/data/get", headers = {"X-Script-Id": script_id} ) if response.status_code not in [200]: print("FATAL: SCRIPT DATA LOADING FAILED!") return False SCRIPT_DATA = response.json().get("data") # Done with this step: printer("Cred and Data loaded.") # ┓┏┓ ┏┓ ┏┓┓• # ┃┫ ┏┓╋┃┏┏┓ ┃ ┃┓┏┓┏┓╋┏ # ┛┗┛┗┻┛┛┗┗┻ ┗┛┗┗┗ ┛┗┗┛ # Create the consumer that will listen to changes in watchlist: consumer_creds = script_cred["kafka"]["consumer"] kafka_consumer = ConsumerKafka( topic = consumer_creds["topic"], bootstrap_servers = consumer_creds["config"]["bootstrapServers"], security_protocol = consumer_creds["config"].get("securityProtocol", "PLAINTEXT"), ssl_context = get_ssl_context( ca_file = consumer_creds["config"].get("caFile"), cert_file = consumer_creds["config"].get("certFile"), key_file = consumer_creds["config"].get("keyFile"), ), serializer = JSONSerializer(), debug = debug ) if not await kafka_consumer.connect(): print("FATAL: KAFKA CONSUMER NOT CREATED!") return False printer("Kafka consumer ready.") # ┳┓ ┓• ┏┓ ┓ # ┣┫┏┓┏┫┓┏ ━━ ┃ ┏┓┏┣┓┏┓ # ┛┗┗ ┗┻┗┛ ┗┛┗┻┗┛┗┗ redis_cache = AsyncRedisCache( connection_string = script_cred["redisCache"]["general"]["sentinelJson"], # connection_string = script_cred["redisCache"]["general"]["connectionString"], # serializer = JSONSerializer(), debug = debug, debug_prefix = "General Cache | " ) if not await redis_cache.connect(): print("FATAL: REDIS CACHE NOT CREATED!") return False printer("Redis cache ready.") # ┳┓ # ┃┃┏┓┏┓┏┓ # ┻┛┗┛┛┗┗ # If everything went well, we return with success: printer("Initialization done.") return True # --------------------------------------------------------------------------------------------------------------------- def register_namespaces(): """ A quick function that registers all the namespaces to the same Socket.IO app. """ printer("Namespaces registered.") # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": printer("Main.") # To get args from the terminal: import argparse # To run the ASGI: import uvicorn from multiprocessing import freeze_support # Get the config from the command-line: parser = argparse.ArgumentParser(description = f"SocketIO to serve live market data (and a general passthrough).") parser.add_argument( "-w", "--workers", type = int, help = "The no. of threads to spin up for this instance!", default = 2 ) parser.add_argument( "-a", "--host", type = str, help = "The host for the app. e.g.: '0.0.0.0' or '127.0.0.1'.", default = "0.0.0.0" ) parser.add_argument( "-p", "--port", type = int, help = "The port no. to bind the app to.", default = 8080 ) parser.add_argument( "-s", "--script-id", type = str, help = "The id of this script (will affect the loaded config)." ) parser.add_argument( "-d", "--debug", action = "store_true", help = "Whether, or not, you want to see debugging messages in the terminal.", default = False ) args = parser.parse_args() # Note down the config; os.environ["SCRIPT_ID"] = args.script_id os.environ["DEBUG"] = str(args.debug) # Startup message: printer.enable() printer(str(args.debug)) printer.disable() # Register all namespaces: register_namespaces() # Run the gateway: freeze_support() uvicorn.run( app = "tick_out:app", workers = args.workers, host = args.host, port = args.port )