import functools import logging import os import re import sys import typing as t from pathlib import Path class Sprinkles: HEADER = "\033[95m" OKBLUE = "\033[94m" OKCYAN = "\033[96m" OKGREEN = "\033[92m" WARNING = "\033[93m" FAIL = "\033[91m" BOLD = "\033[1m" UNDERLINE = "\033[4m" END = "\033[0m" def deprecated(message: str): def func_wrapper(func): @functools.wraps(func) def proc_function(*args, **kwargs): logging.critical( f"{Sprinkles.FAIL}Function deprecated: {message}{Sprinkles.END}" ) return func(*args, **kwargs) return proc_function return func_wrapper def if_env_replace( env_value: t.Optional[t.Any], ignore_missing_env_variables: bool = False ) -> t.Any: """ Looks for the replacement pattern to swap out values in the config file with environment variables. """ pattern = re.compile(r"<(.*?)>") if isinstance(env_value, str): if re.match(pattern, env_value): env_var = re.findall(pattern, env_value)[0] if env_var: if os.environ.get(env_var): return parse_config_env_var(os.environ.get(env_var)) if ignore_missing_env_variables: return None raise ValueError(f"Environment variable {env_value} not found") return env_value def process_dict( this_dict: t.Optional[dict], key_case_switch: str = "upper", ignore_missing_env_variables: bool = False, crawl: bool = False, ) -> dict: """ Used to process the config from_file dictionary and replace environment variables. Turns all keys to upper case. """ if this_dict is None: return {} return_dict = {} for key, value in this_dict.items(): if key_case_switch == "ignore": cs_key = key else: cs_key = key.upper() if key_case_switch == "upper" else key.lower() if crawl: if isinstance(value, dict): return_dict[cs_key] = process_dict( value, key_case_switch, ignore_missing_env_variables, crawl ) continue return_dict[cs_key] = if_env_replace(value, ignore_missing_env_variables) return return_dict def cast_to_import_str(app_name: str, folder_path: Path) -> str: """ Takes the folder path and converts it to a string that can be imported """ folder_parts = folder_path.parts parts = folder_parts[folder_parts.index(app_name) :] if sys.version_info.major == 3: if sys.version_info.minor < 9: return ".".join(parts).replace(".py", "") return ".".join(parts).removesuffix(".py") raise NotImplementedError("Python version not supported") def snake(value: str) -> str: """ Switches name of the class CamelCase to snake_case """ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", value) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() def class_field(class_: str, field: str) -> str: """ Switches name of the class CamelCase to snake_case and tacks on the field name Used for SQLAlchemy foreign key assignments INFO ::: This function may not produce the correct information if you are using __tablename__ in your class """ return f"{snake(class_)}.{field}" def cast_to_bool(value: t.Union[str, bool, None]) -> bool: """ Casts an array of truly string values to a boolean. Used for config files. """ if value is None: return False if isinstance(value, bool): return value if isinstance(value, str): true_str = ("true", "yes", "y", "1") false_str = ("false", "no", "n", "0") if value.lower() in true_str: return True elif value.lower() in false_str: return False else: raise TypeError(f"Cannot cast {value} to bool") else: raise TypeError(f"Cannot cast {value} to bool") def parse_config_env_var(value: t.Optional[str]) -> t.Optional[t.Union[bool, str, int]]: """ Casts value to a boolean, string, or int if possible. If not, returns none. """ if value == "None": return None if isinstance(value, str): true_str = ("true", "yes", "y", "1") false_str = ("false", "no", "n", "0") if value.lower() in true_str: return True elif value.lower() in false_str: return False else: try: return int(value) except ValueError: return value return None