Source code for ten8t.ten8t_util

"""
This is the sad place for lonely functions that don't have a place
"""
import os
import pathlib
from typing import Sequence, TypeAlias

from ten8t.ten8t_exception import Ten8tTypeError, Ten8tValueError

# Type aliases.
# Note: the *OrNone are meant to be constructors that allow a None value to be passed
#       that code will take care to convert to a [] or a ''
StrOrNone: TypeAlias = str | None
"""Type alias for a string or None."""

StrList: TypeAlias = Sequence[str]
"""Type alias for a sequence of strings."""

StrListOrNone: TypeAlias = StrList | StrOrNone
"""Type alias for a sequence of strings or None."""

IntOrNone: TypeAlias = int | None
"""Type alias for an integer or None."""

IntList: TypeAlias = Sequence[int]
"""Type alias for a sequence of integers."""

IntListOrNone: TypeAlias = IntList | IntOrNone
"""Type alias for a sequence of integers or None."""

FloatOrNone: TypeAlias = float | None
"""Type alias for a float or None."""

FloatList: TypeAlias = Sequence[float]
"""Type alias for a sequence of floats."""

FloatListOrNone: TypeAlias = FloatList | FloatOrNone
"""Type alias for a sequence of floats or None."""

StrOrPath: TypeAlias = str | pathlib.Path
StrOrPathOrNone: TypeAlias = StrOrPath | None
StrOrPathList: TypeAlias = Sequence[StrOrPath]
StrOrPathListOrNone: TypeAlias = StrOrPathList | None

PathList: TypeAlias = Sequence[pathlib.Path]


[docs] class NextIntValue: """ I had to create this class in order to make mypy happy. Mypy does not know how to handle dynamic functions and playing games """ def __init__(self): self.current_id: int = 1 # Initialize to 1 def __call__(self) -> int: self.current_id += 1 return self.current_id
# Create an instance of the callable class next_int_value = NextIntValue() # next_int_value can be called like a function and the class manages the count next_int_value.current_id = 1
[docs] def cwd_here(file_: StrOrPathOrNone = None) -> pathlib.Path: """ Change current working directory to the provided folder or to the parent folder if a file is provided. If 'file_' is: - None: the current file (__file__) location is used to set cwd. - a directory: the provided directory itself becomes the current working directory. - a file: the parent directory containing that file becomes the current working directory. Why? This is useful for running scripts in different locations without having to worry about setting the current working directy. Simplifies setup of demos and examples and allows you to NOT set the CWD in a configuration launch files. python foo/fum/demo.py And have code call cwd_here() and know that relative paths for demo.py will be correct. Args: file_ (str | pathlib.Path | None): The path to a file or directory, or None. Default is None. Returns: pathlib.Path: Explicitly returns path of the new current working directory. Example usage: -------- >>> cwd_here('/path/to/directory') # sets cwd to '/path/to/directory' >>> cwd_here('/path/to/file.txt') # sets cwd to '/path/to/' >>> cwd_here() # sets the cwd to currently running script (__FILE__) """ if file_ is None: file_ = __file__ path = pathlib.Path(file_).resolve() if path.is_file(): new_dir = path.parent else: new_dir = path os.chdir(new_dir) return new_dir
[docs] def str_to_bool(s: str, default=None) -> bool: """ Convert a string value to a boolean.""" s = s.strip().lower() # Remove spaces at the beginning/end and convert to lower case if s in ('pass', 'true', 'yes', '1', 't', 'y', 'on'): return True if s in ('fail', 'false', 'no', '0', 'f', 'n', 'off'): return False if default is not None: return default raise ValueError(f'Cannot convert {s} to a boolean.')
[docs] def any_to_str_list(param: StrListOrNone, sep=' ') -> StrList: """ Convert a string to a list of strings or if a list is given make sure it is all strings. Args: param: list of strings or string to convert to list of strings sep: separator character. Returns: """ if param is None: return [] if isinstance(param, str): param = param.strip() if param == '': return [] else: return param.split(sep) if isinstance(param, list): if all(isinstance(item, str) for item in param): return param raise ValueError(f'Invalid parameter type, expected all strings. {param}')
[docs] def any_to_path_list(param: StrOrPathListOrNone, sep=' ') -> PathList: """ Flexibly take a list of strings are pathlib objects and make a uniform list of pathlib objects. This is useful for normalizing data read from different sources without have a bunch of point of use parsing. The assumption is that this data could come from a config file, a command line parameter, a UI element that returns strings, or code. This should make all code just "fix" the data with this call. Args: param: StrOrPathListOrNone Data to normalize sep: Separator character. Should almost always be ' ' Returns: """ if param is None: return [] # Listify single path if isinstance(param, pathlib.Path): param = [param] # Given a string make it a list of strings if isinstance(param, str): param = param.strip() if param == '': param = [] else: # Space split is slightly different and preferable if sep == ' ': param = param.split() else: param = param.split(sep) # Now we have a list of paths and strings, covert them all th paths return [pathlib.Path(p) for p in param]
[docs] def any_to_int_list(param: IntListOrNone, sep=' ') -> IntList: """ Convert a string to a list of integers or if a list is given make sure it is all integers. Args: param: list of integers or string to convert to list of integers sep: separator character. Returns: list of integers """ if param is None: return [] if isinstance(param, str): param = param.strip() cleaned_param = param.split(sep) try: return [int(x) for x in cleaned_param] except ValueError as exc: raise ValueError( 'Invalid parameter value, expected numeric string values that can be converted to integers.') from exc if isinstance(param, list): return [int(x) for x in param] raise ValueError(f'Invalid parameter type in {param}, expected all integers.')
[docs] def clean_dict(d: dict, remove_nulls=True, keep_keys=None, remove_keys=None, empty_values=None): """ Recursively processes a dictionary to remove keys based on specified rules. Args: d (dict): The input dictionary to process. remove_nulls (bool, optional): If True, removes keys with "empty" values (as defined in `empty_values`), unless the keys are in `keep_keys`. Defaults to True. keep_keys (list[str], optional): A list of keys to preserve, ensuring they are not removed, even if their values are defined as empty. Defaults to None. remove_keys (list[str], optional): A list of keys to forcibly remove, regardless of their values. Defaults to None. empty_values (list, optional): A list of values that should be considered empty. By default, this is `['', [], {}]`. Returns: dict: A new dictionary where keys are removed based on `remove_nulls`, `keep_keys`, `remove_keys`, and `empty_values`. Raises: Ten8tTypeError: If the input is not a dictionary. Ten8tValueError: If there are conflicts between `keep_keys` and `remove_keys`. """ # Default to empty lists if keep_keys, remove_keys, or empty_values are not provided keep_keys = keep_keys or [] remove_keys = remove_keys or [] empty_values = empty_values or ['', [], {}, set(), tuple()] # Ensure the input is a dictionary if not isinstance(d, dict): raise Ten8tTypeError("Input must be a dictionary.") # Check for conflicts between keep_keys and remove_keys conflicting_keys = set(keep_keys).intersection(remove_keys) if conflicting_keys: raise Ten8tValueError(f"Conflicting keys between keep_keys and remove_keys: {conflicting_keys}") def _clean(d): """ This function does the work as a closure, allowing it to not need check and pass most parameters on each recursive call. """ result = {} for key, value in d.items(): # Skip keys in remove_keys (unless they're also in keep_keys) if key in remove_keys and key not in keep_keys: continue # Preserve keys in keep_keys, regardless of remove_nulls or empty_values if key in keep_keys: result[key] = value continue # Process nested dictionaries if isinstance(value, dict): cleaned_value = _clean(value) if cleaned_value or not remove_nulls: # Include non-empty nested dicts or if remove_nulls is False result[key] = cleaned_value elif remove_nulls and value in empty_values: # It is important for this case to go before the next two since this will # eliminate a case for being empty continue elif isinstance(value, list): # Process each element in the list, cleaning if it’s a dictionary; retain original value otherwise result[key] = [ _clean(item) if isinstance(item, dict) else item for item in value ] elif isinstance(value, tuple): # Process each element in the list, cleaning if it’s a dictionary; retain original value otherwise result[key] = tuple( _clean(item) if isinstance(item, dict) else item for item in value ) # Iterate over values to remove empty lists or custom empty values # Include all other valid values else: result[key] = value return result # Call the closure to clean the dict. return _clean(d)