Source code for arbor_imago.utils

import bcrypt
import uuid
import secrets
from pathlib import Path
import os
import json
import yaml
import tomllib
import tomli_w
import configparser


[docs] def deep_merge_dicts(primary_dict: dict, secondary_dict: dict) -> dict: """ Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys, dict_merge recurses down into dicts nested to an arbitrary depth, updating keys. The ``secondary_dict`` is merged into ``primary_dict``. :param primary_dict: dict onto which the merge is executed :param secondary_dict: primary_dict merged into primary_dict :return: None """ for k in secondary_dict: if (k in primary_dict and isinstance(primary_dict[k], dict) and isinstance(secondary_dict[k], dict)): # noqa deep_merge_dicts(primary_dict[k], secondary_dict[k]) else: primary_dict[k] = secondary_dict[k] return primary_dict
[docs] def hash_password(password: str) -> str: salt = bcrypt.gensalt() hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt) return hashed_password.decode('utf-8')
[docs] def verify_password(plain_password: str, hashed_password: str) -> bool: return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
[docs] def generate_uuid() -> str: return str(uuid.uuid4())
[docs] def generate_jwt_secret_key() -> str: return secrets.token_hex(32)
[docs] def resolve_path(root_dir: Path, path: str | os.PathLike[str]) -> Path: """Resolve a path to absolute, using root_dir for relative paths.""" p = Path(path) return p if p.is_absolute() else (root_dir / p).resolve()
[docs] def load_dict_from_file(config_path: Path) -> dict: suffix = config_path.suffix.lower() match suffix: case '.json': with config_path.open('r') as f: return json.load(f) case '.yaml' | '.yml': with config_path.open('r') as f: return yaml.safe_load(f) case '.toml': with config_path.open('rb') as f: return tomllib.load(f) case '.ini': config = configparser.ConfigParser() config.read(config_path) # Convert ConfigParser to dict return {section: dict(config.items(section)) for section in config.sections()} case _: raise ValueError( f'Config file {config_path} has an unsupported extension. Supported extensions are .json, .yaml, .yml' )
[docs] def write_dict_to_file(config: dict, config_path: Path) -> None: """Write a dictionary to a file in JSON, YAML, TOML, or INI format based on file extension.""" suffix = config_path.suffix.lower() match suffix: case '.json': config_path.write_text(json.dumps(config)) case '.yaml' | '.yml': config_path.write_text(yaml.dump(config)) case '.toml': config_path.write_text(tomli_w.dumps(config)) case '.ini': config_parser = configparser.ConfigParser() for section, values in config.items(): config_parser[section] = values with config_path.open('w') as f: config_parser.write(f) case _: raise ValueError( f'Config file {config_path} has an unsupported extension. Supported extensions are .json, .yaml, .yml, .toml, .ini' )