mirror of
https://github.com/hyugogirubato/KeyDive.git
synced 2026-07-16 02:44:22 +02:00
release v3.0.0
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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
|
||||
Reference in New Issue
Block a user