import logging
import mimetypes
import re
from abc import ABCMeta
from pathlib import Path
from typing import Generic, Optional, TypeVar
try:
import magic
except ImportError as e:
[docs] magic = None # type:ignore
logging.warning(
(
"Libmagic is not available: %s. "
"It's OK if you don't use fix-filename-suffix-mime-type."
),
e,
)
[docs]def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]):
guessed = magic.from_file(path, mime=True)
if guessed == mime_type:
log.debug("Magic and given MIME match")
else:
log.debug("Magic (%s) and given MIME (%s) differ", guessed, mime_type)
mime_type = guessed
valid_suffix_list = mimetypes.guess_all_extensions(mime_type, strict=False)
if file_name:
name = Path(file_name)
else:
name = Path(path.name)
suffix = name.suffix
if suffix in valid_suffix_list:
log.debug("Suffix %s is in %s", suffix, valid_suffix_list)
return name
valid_suffix = mimetypes.guess_extension(mime_type.split(";")[0], strict=False)
if valid_suffix is None:
log.debug("No valid suffix found")
return name
log.debug("Changing suffix of %s to %s", file_name or path.name, valid_suffix)
return name.with_suffix(valid_suffix)
[docs]KeyType = TypeVar("KeyType")
[docs]ValueType = TypeVar("ValueType")
[docs]class BiDict(Generic[KeyType, ValueType], dict[KeyType, ValueType]):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.inverse: dict[ValueType, KeyType] = {}
for key, value in self.items():
self.inverse[value] = key
[docs] def __setitem__(self, key: KeyType, value: ValueType):
if key in self:
del self.inverse[self[key]]
super().__setitem__(key, value)
self.inverse[value] = key
[docs]class SubclassableOnce(type):
[docs] TEST_MODE = False # To allow importing everything, including plugins, during tests
def __init__(cls, name, bases, dct):
for b in bases:
if type(b) in (SubclassableOnce, ABCSubclassableOnceAtMost):
if hasattr(b, "_subclass") and not cls.TEST_MODE:
raise RuntimeError(
"This class must be subclassed once at most!",
cls,
name,
bases,
dct,
)
else:
log.debug("Setting %s as subclass for %s", cls, b)
b._subclass = cls
super().__init__(name, bases, dct)
[docs] def get_self_or_unique_subclass(cls):
try:
return cls.get_unique_subclass()
except AttributeError:
return cls
[docs] def get_unique_subclass(cls):
r = getattr(cls, "_subclass", None)
if r is None:
raise AttributeError("Could not find any subclass", cls)
return r
[docs] def reset_subclass(cls):
try:
log.debug("Resetting subclass of %s", cls)
delattr(cls, "_subclass")
except AttributeError:
log.debug("No subclass were registered for %s", cls)
[docs]class ABCSubclassableOnceAtMost(ABCMeta, SubclassableOnce):
pass
[docs]def is_valid_phone_number(phone: Optional[str]):
if phone is None:
return False
match = re.match(r"\+\d.*", phone)
if match is None:
return False
return match[0] == phone
[docs]def strip_illegal_chars(s: str):
return ILLEGAL_XML_CHARS_RE.sub("", s)
# from https://stackoverflow.com/a/64570125/5902284 and Link Mauve
[docs]ILLEGAL = [
(0x00, 0x08),
(0x0B, 0x0C),
(0x0E, 0x1F),
(0x7F, 0x84),
(0x86, 0x9F),
(0xFDD0, 0xFDDF),
(0xFFFE, 0xFFFF),
(0x1FFFE, 0x1FFFF),
(0x2FFFE, 0x2FFFF),
(0x3FFFE, 0x3FFFF),
(0x4FFFE, 0x4FFFF),
(0x5FFFE, 0x5FFFF),
(0x6FFFE, 0x6FFFF),
(0x7FFFE, 0x7FFFF),
(0x8FFFE, 0x8FFFF),
(0x9FFFE, 0x9FFFF),
(0xAFFFE, 0xAFFFF),
(0xBFFFE, 0xBFFFF),
(0xCFFFE, 0xCFFFF),
(0xDFFFE, 0xDFFFF),
(0xEFFFE, 0xEFFFF),
(0xFFFFE, 0xFFFFF),
(0x10FFFE, 0x10FFFF),
]
[docs]ILLEGAL_RANGES = [rf"{chr(low)}-{chr(high)}" for (low, high) in ILLEGAL]
[docs]XML_ILLEGAL_CHARACTER_REGEX = "[" + "".join(ILLEGAL_RANGES) + "]"
[docs]ILLEGAL_XML_CHARS_RE = re.compile(XML_ILLEGAL_CHARACTER_REGEX)
# from https://stackoverflow.com/a/35804945/5902284
[docs]def addLoggingLevel(
levelName: str = "TRACE", levelNum: int = logging.DEBUG - 5, methodName=None
):
"""
Comprehensively adds a new logging level to the `logging` module and the
currently configured logging class.
`levelName` becomes an attribute of the `logging` module with the value
`levelNum`. `methodName` becomes a convenience method for both `logging`
itself and the class returned by `logging.getLoggerClass()` (usually just
`logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
used.
To avoid accidental clobberings of existing attributes, this method will
raise an `AttributeError` if the level name is already an attribute of the
`logging` module or if the method name is already present
Example
-------
>>> addLoggingLevel('TRACE', logging.DEBUG - 5)
>>> logging.getLogger(__name__).setLevel("TRACE")
>>> logging.getLogger(__name__).trace('that worked')
>>> logging.trace('so did this')
>>> logging.TRACE
5
"""
if not methodName:
methodName = levelName.lower()
if hasattr(logging, levelName):
raise AttributeError("{} already defined in logging module".format(levelName))
if hasattr(logging, methodName):
raise AttributeError("{} already defined in logging module".format(methodName))
if hasattr(logging.getLoggerClass(), methodName):
raise AttributeError("{} already defined in logger class".format(methodName))
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# http://stackoverflow.com/a/13638084/2988730
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
self._log(levelNum, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
logging.addLevelName(levelNum, levelName)
setattr(logging, levelName, levelNum)
setattr(logging.getLoggerClass(), methodName, logForLevel)
setattr(logging, methodName, logToRoot)
[docs]class SlidgeLogger(logging.Logger):
[docs]log = logging.getLogger(__name__)