import functools
import hashlib
import logging
import math
import random
import string
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
[docs]def random_string(length: int = 8) -> str:
"""Create a random strings with digits and upper and lower case letters.
Args:
length (int, optional): Length of the generated string.. Defaults to 8.
Returns:
str: A random string.
"""
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
return "".join(random.choice(chars) for _ in range(length))
[docs]def float_or_none(value: any):
"""Tries to convert a value to float. If fails return None.
"""
try:
return float(value)
except:
return None
[docs]def str_separator(length: int = 80, title: str = "",
char: str = "-", new_line: bool = True) -> str:
"""Create a separator string with an optional title.
e.g: --------title--------
Args:
length (int, optional): Total character-length of the string.
Defaults to 80.
title (str, optional): Optional separator title. Defaults to "".
char (str, optional): Character used for the separator.
Defaults to "-".
new_line (bool, optional): Add a new line at the end. Defaults to True.
Returns:
str: The separator string.
"""
l2 = (length - len(title)) / 2
r = char * math.floor(l2)
r += title
r += char * math.ceil(l2)
if new_line:
r += "\n"
return r
[docs]def is_url(url: str) -> bool:
"""Check if a string is a valid URL.
"""
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except:
return False
[docs]def urljoin(*args: str) -> str:
"""Join the given arguments into a url. Trailing but not leading slashes
are stripped for each argument.
"""
return "/".join(map(lambda x: str(x).rstrip("/").strip("/"), args))
class _LoggingFormatter(logging.Formatter):
"""Logging colored formatter,
adapted from https://stackoverflow.com/a/56944256/3638629
"""
grey = "\x1b[38;20m"
green = "\x1b[32;20m"
yellow = "\x1b[33;20m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
date_format = "%Y-%d-%m %H:%M:%S"
FORMATS = {
logging.DEBUG: grey + format + reset,
logging.INFO: green + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt, self.date_format)
return formatter.format(record)
[docs]def get_logger(name: Optional[str] = None) -> logging.Logger:
"""Get the default logger.
Args:
name (Optional[str], optional): Name of the logger. Defaults to None.
Returns:
logging.Logger
"""
logger = logging.getLogger(name)
ch = logging.StreamHandler()
ch.setFormatter(_LoggingFormatter())
if (logger.hasHandlers()):
logger.handlers.clear()
logger.addHandler(ch)
logger.propagate = False
return logger
[docs]def hash_str(value: str, algorithm="sha1") -> str:
"""Return the hash of a string.
Args:
value (str): The string to hash.
algorithm (str, optional): Name of the hash algorithm.
One of ["md5","sha1","sha224","sha256","sha384","sha512"]
Defaults to "sha1".
Raises:
ValueError: If algorithm is not recognized.
Returns:
str: The hash string.
"""
valid_algorithms = ("md5","sha1","sha224","sha256","sha384","sha512")
if algorithm not in valid_algorithms:
raise ValueError(f"Algorithm must be one of {valid_algorithms} " \
f"({algorithm})")
hasher = getattr(hashlib, algorithm)
return hasher(value.encode()).hexdigest()
[docs]@functools.cache
def get_version():
version_file = Path(__file__).parent.parent.joinpath("version")
return version_file.read_text()