Source code for boss.helper
import importlib
import urllib
import urllib.request
import base64
from io import IOBase
import time
from heros import RemoteHERO
from heros.helper import get_logger
from typing import Any, Callable
import hashlib
import json
log = get_logger("boss")
[docs]
def get_module(path, relative_path=None):
module_name = ".".join([relative_path, path]) if relative_path is not None else path
module = importlib.import_module(module_name)
return module
[docs]
def get_class_by_name(name, relative_path=None):
"""
Resolve class by name
:param name: (str) "%s.%s" % (module.name, class.name)
:return: (class)
"""
assert name is not None
module_path, class_name = name.rsplit(".", 1)
module__ = get_module(module_path, relative_path=relative_path)
class_ = getattr(module__, class_name)
return class_
[docs]
def file_from_url(url: str) -> IOBase:
"""
Load content from a file specified by a URL.
This can be every type of URL supported by pythons urllib
(e.g. http://, file://, etc ). Giving the basic auth credentials in the URL in the form
http://user:password@hostname:port/path is supported.
Returns:
file handle on the file
"""
parsed = urllib.parse.urlparse(url)
if parsed.username and parsed.password:
request = urllib.request.Request(parsed._replace(netloc=parsed.netloc.split("@")[1]).geturl())
base64string = base64.b64encode(bytes("%s:%s" % (parsed.username, parsed.password), "ascii"))
request.add_header("Authorization", "Basic %s" % base64string.decode("utf-8"))
f_handle = urllib.request.urlopen(request)
else:
f_handle = urllib.request.urlopen(url)
return f_handle
[docs]
def extend_none_allowed_list(list1: list | None, list2: list | None) -> list | None:
"""Extend a list with another list which both can be None.
If one of the lists is None, the other list is returned. If both lists are None, None is returned.
Args:
list1: list to extend
list2: list to extend with
"""
if list1 is None:
return list2
if list2 is None:
return list1
return list1 + list2
[docs]
def get_remote_hero(name: str, realm: str, trials: int = 10, delay: float = 1.0):
"""Try multiple times to get RemoteHERO.
Args:
name: name of the remote HERO.
realm: realm of the remote HERO.
trials: number of trials before giving up
delays: time delay between trials in seconds.
"""
for i in range(trials):
try:
return RemoteHERO(name, realm)
except Exception:
log.warning(f"Could not get RemoteHERO {name} in trial {i + 1}/{trials}.")
time.sleep(delay)
raise NameError(f"Could not get HERO {name} after {trials} trials! Giving up.")
[docs]
def add_class_descriptor(cls: type, attr_name: str, descriptor) -> None: # noqa: ANN001
"""
Add a descriptor to a class.
This is a simple helper function which uses `setattr` to add an attribute to the class and then also calls
`__set_name__` on the attribute.
Args:
cls: Class to add the descriptor to
attr_name: Name of the attribute the descriptor will be added to
descriptor: The descriptor to be added
"""
setattr(cls, attr_name, descriptor)
getattr(cls, attr_name).__set_name__(cls, attr_name)
[docs]
def get_or_create_dynamic_subclass(base_cls: Any, *args: Any, **kwargs: Any) -> type:
"""Return a cached dynamic subclass of ``base_cls`` based on the input arguments.
This helper generates a subclass of ``base_cls`` whose identity is determined by ``*args`` and ``**kwargs``.
The argument signature is serialized into a hash, which is then used as both a cache key and the dynamic subclass
name. If the subclass for a given argument combination already exists, it is returned from cache.
The generated subclass replaces ``__new__`` with a dummy implementation to prevent recursive invocation of
``base_cls.__new__``.
Args:
base_cls: The base class to derive from. Must be passed positionally.
*args: Positional values that should influence subclass identity.
**kwargs: Keyword values that should influence subclass identity.
Returns:
A dynamically generated subclass of ``base_cls``.
Raises:
TypeError: If arguments cannot be serialized for hashing.
"""
# normalize args+kwargs
signature = json.dumps(
{"args": args, "kwargs": kwargs},
sort_keys=True,
default=str,
)
arg_hash = hashlib.blake2s(signature.encode()).hexdigest()
new_cls_name = f"{base_cls.__name__}_{arg_hash}"
# generate cache
generated = getattr(base_cls, "_generated_classes", {})
if new_cls_name in generated:
return generated[new_cls_name]
# dummy __new__ method for subclasses
def _no_new(subcls: type, *_a, **_kw) -> Callable:
return object.__new__(subcls)
# generate new class
new_cls = type(
new_cls_name,
(base_cls,),
{"__new__": _no_new},
)
# cache it
if not hasattr(base_cls, "_generated_classes"):
base_cls._generated_classes = {}
base_cls._generated_classes[new_cls_name] = new_cls
return new_cls