"""Routine functions for OpenBB Platform CLI.""" import re from datetime import datetime, timedelta from typing import Dict, List, Match, Optional, Tuple, Union from dateutil.relativedelta import relativedelta from openbb_cli.session import Session session = Session() # pylint: disable=too-many-statements,eval-used,consider-iterating-dictionary # pylint: disable=too-many-branches,too-many-return-statements # Necessary for OpenBB keywords MONTHS_VALUE = { "JANUARY": 1, "FEBRUARY": 2, "MARCH": 3, "APRIL": 4, "MAY": 5, "JUNE": 6, "JULY": 7, "AUGUST": 8, "SEPTEMBER": 9, "OCTOBER": 10, "NOVEMBER": 11, "DECEMBER": 12, } WEEKDAY_VALUE = { "MONDAY": 0, "TUESDAY": 1, "WEDNESDAY": 2, "THURSDAY": 3, "FRIDAY": 4, "SATURDAY": 5, "SUNDAY": 6, } def is_reset(command: str) -> bool: """Test whether a command is a reset command. Parameters ---------- command : str The command to test Returns ------- answer : bool Whether the command is a reset command """ if "reset" in command: return True if command == "r": return True if command == "r\n": return True return False def match_and_return_openbb_keyword_date(keyword: str) -> str: # noqa: PLR0911 """Return OpenBB keyword into date. Parameters ---------- keyword : str String with potential OpenBB keyword (e.g. 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY) Returns ---------- str: Date with format YYYY-MM-DD """ now = datetime.now() for i, regex in enumerate([r"^\$(\d+)([A-Z]+)AGO$", r"^\$(\d+)([A-Z]+)FROMNOW$"]): match = re.match(regex, keyword) if match: integer_value = int(match.group(1)) time_unit = match.group(2) clean_time = time_unit.upper() if "DAYS" in clean_time or "MONTHS" in clean_time or "YEARS" in clean_time: kwargs = {time_unit.lower(): integer_value} if i == 0: return (now - relativedelta(**kwargs)).strftime("%Y-%m-%d") # type: ignore return (now + relativedelta(**kwargs)).strftime("%Y-%m-%d") # type: ignore match = re.search(r"\$LAST(\w+)", keyword) if match: time_unit = match.group(1) # Check if it corresponds to a month if time_unit in list(MONTHS_VALUE.keys()): the_year = now.year # Calculate the year and month for last month date if now.month <= MONTHS_VALUE[time_unit]: # If the current month is greater than the last date month, it means it is this year the_year = now.year - 1 return datetime(the_year, MONTHS_VALUE[time_unit], 1).strftime("%Y-%m-%d") # Check if it corresponds to a week day if time_unit in list(WEEKDAY_VALUE.keys()): if datetime.weekday(now) > WEEKDAY_VALUE[time_unit]: return ( now - timedelta(datetime.weekday(now)) + timedelta(WEEKDAY_VALUE[time_unit]) ).strftime("%Y-%m-%d") return ( now - timedelta(7) - timedelta(datetime.weekday(now)) + timedelta(WEEKDAY_VALUE[time_unit]) ).strftime("%Y-%m-%d") match = re.search(r"\$NEXT(\w+)", keyword) if match: time_unit = match.group(1) # Check if it corresponds to a month if time_unit in list(MONTHS_VALUE.keys()): # Calculate the year and month for next month date if now.month < MONTHS_VALUE[time_unit]: # If the current month is greater than the last date month, it means it is this year return datetime(now.year, MONTHS_VALUE[time_unit], 1).strftime( "%Y-%m-%d" ) return datetime(now.year + 1, MONTHS_VALUE[time_unit], 1).strftime( "%Y-%m-%d" ) # Check if it corresponds to a week day if time_unit in list(WEEKDAY_VALUE.keys()): if datetime.weekday(now) < WEEKDAY_VALUE[time_unit]: return ( now - timedelta(datetime.weekday(now)) + timedelta(WEEKDAY_VALUE[time_unit]) ).strftime("%Y-%m-%d") return ( now + timedelta(7) - timedelta(datetime.weekday(now)) + timedelta(WEEKDAY_VALUE[time_unit]) ).strftime("%Y-%m-%d") return "" def parse_openbb_script( # noqa: PLR0911,PLR0912 raw_lines: List[str], script_inputs: Optional[List[str]] = None, ) -> Tuple[str, str]: """Parse .openbb script. Parameters ---------- raw_lines : List[str] Lines from .openbb script script_inputs: str, optional Inputs to the script that come externally Returns ------- str Error that occurred - if empty means no error str Processed string from .openbb script that can be run by the OpenBB Platform CLI """ ROUTINE_VARS: Dict[str, Union[str, List[str]]] = dict() if script_inputs: ROUTINE_VARS["$ARGV"] = script_inputs ## PRE PROCESSING # Remove reset commands, comments, empty lines and trailing/leading whitespaces raw_lines = [ x.strip() for x in raw_lines if (not is_reset(x)) and ("#" not in x) and x.strip() ] ## LOOK FOR NEW VARIABLES BEING DECLARED FROM USERS lines_without_declarations = list() for line in raw_lines: # Check if this line has a variable attribution # This currently allows user to override ARGV parameter if "$" in line and "=" in line: match = re.search(r"\$(\w+)\s*=\s*([\w\d,-.\s]+)", line) if match: VAR_NAME = match.group(1) VAR_VALUES = match.group(2) ROUTINE_VARS["$" + VAR_NAME] = ( VAR_VALUES if "," not in VAR_VALUES else VAR_VALUES.split(",") ) # Just throw a warning when user uses wrong convention numdollars = len(re.findall(r"\$", line)) if numdollars > 1: session.console.print( f"The variable {VAR_NAME} should not be declared as " f"{'$' * numdollars}{VAR_NAME}. Instead it will be " f"converted into ${VAR_NAME}." ) else: lines_without_declarations.append(line) else: lines_without_declarations.append(line) # At this stage our ROUTINE_VARS should be completed coming from external AND from internal # Now we want to replace the ROUTINE_VARS to where applicable throughout the .openbb script # Due to this implementation, a variable declared at the end will still be effective lines_with_vars_replaced = list() foreach_loop_found = False for line in lines_without_declarations: # Save temporary line to ensure that all vars get replaced by correct vars templine = line # Found 'end' keyword which means that a loop has terminated if re.match(r"^\s*end\s*$", line, re.IGNORECASE): # Check whether the foreach loop has started or not if not foreach_loop_found: return ( "[red]The script has a foreach loop that terminates before it gets started. " "Add the keyword 'foreach' to explicitly start loop[/red]", "", ) foreach_loop_found = False else: # Found 'foreach' keyword which means there needs to be a matching 'end' if re.search(r"foreach", line, re.IGNORECASE): foreach_loop_found = True # Regular expression pattern to match variables starting with $ pattern = r"(?