Files
KeyDive/keydive/utils.py
T

214 lines
6.4 KiB
Python
Raw Permalink Normal View History

2025-06-09 18:12:49 +02:00
import base64
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Union, Optional
from uuid import UUID
import coloredlogs
import xmltodict
from unidecode import unidecode
def unidec(data: str) -> str:
"""
Normalizes a string by removing diacritics and accents using ASCII transliteration.
Args:
data (str): The input string to normalize.
Returns:
str: The transliterated and stripped version of the input string.
"""
return unidecode(data).strip()
def b64enc(data: Union[bytes, str], safe: bool = False) -> str:
"""
Encodes data to Base64 format.
Args:
data (bytes or str): The data to encode.
safe (bool): Use URL-safe encoding if True.
Returns:
str: Base64-encoded string.
"""
if isinstance(data, str):
data = data.encode('utf-8')
return (base64.urlsafe_b64encode(data) if safe else base64.b64encode(data)).decode('utf-8')
def b64dec(data: str, safe: bool = False) -> bytes:
"""
Decodes Base64-encoded string to bytes.
Args:
data (str): Base64-encoded string.
safe (bool): Use URL-safe decoding if True.
Returns:
bytes: Decoded byte data.
"""
# Fix missing padding
data = data + '=' * (-len(data) % 4)
return base64.urlsafe_b64decode(data) if safe else base64.b64decode(data)
def xmldec(data: Union[bytes, str], force_list: Optional[list] = None) -> dict:
"""
Parses XML string or bytes into a dictionary.
Args:
data (str or bytes): The XML data to parse.
force_list (list, optional): Tags to always parse as lists.
Returns:
dict: Parsed XML structure.
"""
return xmltodict.parse(data, force_list=set(force_list or []))
def dumps(data: Union[dict, list], beauty: bool = False) -> str:
"""
Serializes a Python object into JSON, handling binary, nested, and custom types.
Args:
data (dict or list): The data to serialize.
beauty (bool): If True, formats JSON with indentation for readability.
Returns:
str: A JSON-formatted string.
"""
def __string(value):
# Recursively process lists
if isinstance(value, list):
return [__string(v) for v in value]
# Recursively process dictionaries
elif isinstance(value, dict):
return {__string(k): __string(v) for k, v in value.items()}
# Handle byte values (e.g., raw data, encoded JSON, binary keys)
elif isinstance(value, bytes):
if value:
# Try decoding as JSON
try:
return __string(json.loads(value))
except ValueError:
pass
# Try decoding as UTF-8 string
try:
return value.decode('utf-8')
except UnicodeDecodeError:
pass
# Handle specific known binary formats (e.g., 16-byte AES keys)
if len(value) == 16:
return value.hex()
# Fallback: encode to base64 string for general binary data
return b64enc(value)
# Return None for empty bytes
return None
# Recurse again for nested structures
elif isinstance(value, (dict, list)):
return __string(value)
# Convert known non-serializable types to string
elif isinstance(value, (UUID, datetime, Path)):
return str(value)
# If value is a string, try parsing it as JSON
elif isinstance(value, str):
try:
return __string(json.loads(value))
except ValueError:
pass
# Return all other values unchanged
return value
# Perform final JSON serialization with optional pretty-printing
return json.dumps(
__string(data),
indent=2 if beauty else None,
separators=None if beauty else (',', ':') # Compact output if not pretty
)
def configure_logging(path: Optional[Path] = None, verbose: bool = False) -> Optional[Path]:
"""
Configures logging to file and console with optional verbosity.
Args:
path (Path, optional): Directory to save the log file.
verbose (bool): Enable debug-level logging if True.
Returns:
Path or None: Path to log file if created, otherwise None.
"""
# Set up the root logger with the desired logging level
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
# Clear any existing handlers (optional, to avoid duplicate logs if reconfiguring)
if root_logger.hasHandlers():
root_logger.handlers.clear()
file_path = None
if path:
# Ensure the log directory exists
if path.is_file():
path = path.parent
path.mkdir(parents=True, exist_ok=True)
# Create a file handler
file_path = path / ('keydive_%s.log' % datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
file_path = file_path.resolve(strict=False)
file_handler = logging.FileHandler(file_path)
file_handler.setLevel(logging.DEBUG)
# Set log formatting
formatter = logging.Formatter(
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
# Add the file handler to the root logger
root_logger.addHandler(file_handler)
# Configure coloredlogs for console output
# 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
coloredlogs.install(
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=logging.DEBUG if verbose else logging.INFO,
logger=root_logger,
field_styles={
'asctime': {'color': 'green'}, # timestamp
'hostname': {'color': 'magenta'},
'levelname': {'bold': True, 'color': 'blue'}, # level e.g., [I]
'name': {'color': 'magenta'}, # logger name
'programname': {'color': 'cyan'},
'message': {'color': 'white'} # message text
},
level_styles={
'debug': {'color': 'cyan'},
'info': {'color': 'white'},
'warning': {'color': 'yellow'},
'error': {'color': 'red'},
'critical': {'bold': True, 'color': 'red'}
}
)
return file_path