""" AUTHOR: Khushal P Soonderji DATE: Thursday, 5th Dec., 2024 OBJECTIVE: To provide a way to retrieve the list of holidays from NSE and to check if today is a holiday. NOTE: This method involves web scraping. It is good for proof-of-concept development, but it is recommended that more professional data-sources be used when the product starts becoming mature. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys from time import timezone sys.path.append(".") sys.path.append("..") # System-level activities: import io # My utils: from utils_v2.string import json from utils_v2.date_time import date_time # NSE-related utils: from utils_v2.nse.controllers.base import AsyncNSEBase from utils_v2.nse.models.api_call import NSEApiResponse # To make REST-ful API calls: import httpx # To work with date and time: import datetime # To work with datatypes: from typing import Any, List # For asynchronous activities: import asyncio # For debugging: import inspect # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** class NSETradingHolidayCalendar(AsyncNSEBase): def __init__( self, http_client: httpx.AsyncClient, debug = True, debug_prefix = "NSE (Hol) | ", debug_only_errors = True ): # Pass on the initialization to the parent: super().__init__( base_url = r"https://www.nseindia.com/resources/exchange-communication-holidays", data_url = r"https://www.nseindia.com/api/holiday-master?type=trading", http_client = http_client, debug = debug, debug_prefix = debug_prefix, debug_only_errors = debug_only_errors ) async def get_data( self, return_raw: bool = False, ) -> NSEApiResponse: """ To get the data of the holiday calendar. :param return_raw: Whether you want the raw JSON from NSE or you want it formatted. :return: The raw or formatted event calendar data in the 'data' field of the response model. """ # Make the API call: api_response = await self.get() # If the API call was successful: if api_response.httpCode in [200]: api_response.success = True if return_raw: api_response.data = await api_response.get_json() else: try: api_response.data = self.format_data( raw_json = await api_response.get_json(), timestamp = date_time.get_current_utc_date_time(as_string = True), raise_exception = True ) except Exception as exception: api_response.exception = exception api_response.success = False # Done here: return api_response @staticmethod def format_data( raw_json: dict, timestamp: datetime.datetime = None, raise_exception: bool = False ) -> List[dict] | None: """ We format the data here to be able to retrieve it properly later. :param raw_json: The raw data as scraped from NSE. :param timestamp: The timestamp at which the data was scraped. This shall be useful for data retrieval from the database, later. :param raise_exception: If set to True, any exception will be propagated. If set to False, any exception will be suppressed internally. :return: The formatted data if successful, else None. """ # Can't do anything if the chain itself is null: if raw_json is None: return raw_json # Start by assuming failure: formatted_data = None # Ensure that we've got a proper timestamp: if timestamp is None: timestamp = date_time.get_current_utc_date_time(as_string = False) try: # Start by creating an empty dict: formatted_data = {} # Process each category in the original data for category, events in raw_json.items(): for event in events: trading_date = event["tradingDate"] # If the tradingDate is not in the dictionary, initialize it if trading_date not in formatted_data: formatted_data[trading_date] = { "scrapeTs": timestamp, "date": date_time.to_timezone( date_time.as_if_timezone( datetime.datetime.strptime(trading_date, "%d-%b-%Y"), timezone = date_time.TIMEZONE_IST ), timezone = date_time.TIMEZONE_UTC ), "weekDay": event["weekDay"], "event": event["description"], "morningSession": event["morning_session"], "eveningSession": event["evening_session"], "holidayType": [] } # Add the current category to the "type" list formatted_data[trading_date]["holidayType"].append(category) # Now we get rid of the unnecessary keys and make it a list: formatted_data = [v for v in formatted_data.values()] # If something goes wrong: except Exception as exception: formatted_data = None if raise_exception: raise # Done here: return formatted_data async def is_holiday( self, timestamp: datetime.datetime ) -> NSEApiResponse: """ Checks if a given date is a trading holiday, or not. :param timestamp: The datetime instance that you want to check for it being a holiday. Preferably make it an aware instance. If a naive instance is passed, it will be assumed to be in UTC. :return: True if it is a holiday, False if it isn't. Can be None if something goes wrong in fetching the data. The value will be in the 'data' field of the response model. """ # We first prepare UTC and IST versions of the incoming datetime: timestamp_utc = date_time.to_timezone(timestamp, timezone = date_time.TIMEZONE_UTC) timestamp_ist = date_time.to_timezone(timestamp_utc, timezone = date_time.TIMEZONE_IST) # If this day is a weekend, it is a holiday by default: ist_dow = timestamp_ist.weekday() if ist_dow in [5, 6]: return NSEApiResponse( action = inspect.stack()[0].function, url = None, method = None, success = True, data = True, message = f"Weekend ({'Saturday' if ist_dow == 5 else 'Sunday'})" ) # First we fetch the data from NSE: api_response = await self.get_data(return_raw = False) api_response.action = inspect.stack()[0].function if not api_response.success: return api_response # Now we check if our date to check is in the list: holidays_list = [h["date"].date() for h in api_response.data] api_response.data = True if timestamp_utc.date() in holidays_list else False # Done here: return api_response # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": async def main(): # Create an HTTP client: test_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 = 2.5 # ....... Time to wait for receiving data. ) ) # Create an instance of the scraper, and refresh its cookies: my_nse = NSETradingHolidayCalendar(http_client = test_client) # Get and show the data: api_response = await my_nse.get_data(return_raw = False) print("SUMMARY:", api_response.to_markdown(), "\n---\n\n") if api_response.success: print("HOLIDAY CALENDAR:", json.to_string(api_response.data, default = str)) print("\n---\n\n") # Check for a holiday: date_to_check = date_time.parse_date_time( input_value = "2024-12-25 00:00:00", date_formats = ["%Y-%m-%d %H:%M:%S"], timezone = date_time.TIMEZONE_IST ) api_response = await my_nse.is_holiday(date_to_check) print("SUMMARY:", api_response.to_markdown(), "\n---\n\n") print("DATETIME:", date_to_check) print("IS HOLIDAY:", api_response.data) asyncio.run(main())