# Copyright 2020 Karlsruhe Institute of Technology
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import operator
from collections import namedtuple
from datetime import datetime
from datetime import timezone
from importlib import import_module
from flask import current_app
from flask import json
[docs]class SimpleReprMixin:
"""Mixin to add a simple implementation of ``__repr__`` to a class.
The provided implementation uses all instance or class attributes specified in the
``Meta.representation`` attribute of the inheriting class. It should be a list of
strings specifying the attributes to use in the representation.
**Example:**
.. code-block:: python3
class Foo:
class Meta:
representation = ["bar", "baz"]
bar = 1
baz = 2
"""
def __repr__(self):
attrs = ", ".join(
f"{attr}={getattr(self, attr)!r}" for attr in self.Meta.representation
)
return f"{self.__class__.__name__}({attrs})"
class _StringEnumMeta(type):
def __new__(cls, name, bases, class_dict):
values_key = "__values__"
if values_key not in class_dict:
class_dict[values_key] = []
for value in class_dict[values_key]:
class_dict[value.upper()] = value
return super().__new__(cls, name, bases, class_dict)
[docs]class StringEnum(metaclass=_StringEnumMeta):
"""Custom enum-like class that uses regular strings as values.
An inheriting class needs to specify a ``__values__`` attribute for all possible
enum string values. Each value will be added as a class attribute using its
respective value as key in uppercase.
**Example:**
.. code-block:: python3
class Foo:
__values__ = ["bar"]
"""
[docs]def named_tuple(tuple_name, **kwargs):
r"""Convenience function to build a ``namedtuple`` from keyword arguments.
:param tuple_name: The name of the tuple.
:param \**kwargs: The keys and values of the tuple.
:return: The ``namedtuple`` instance.
"""
NamedTuple = namedtuple(tuple_name, list(kwargs))
return NamedTuple(*list(kwargs.values()))
[docs]def compare(left, op, right):
"""Compare two values with a given operator.
:param left: The left value.
:param op: One of ``"=="``, ``"!="``, ``">"``, ``"<"``, ``">="`` or ``"<="``.
:param right: The right value.
:return: The boolean result of the comparison.
"""
ops = {
"==": operator.eq,
"!=": operator.ne,
">": operator.gt,
"<": operator.lt,
">=": operator.ge,
"<=": operator.le,
}
return ops[op](left, right)
[docs]def rgetattr(obj, name, default=None):
"""Get a nested attribute of an object.
:param obj: The object to get the attribute from.
:param name: The name of the attribute in the form of ``"foo.bar.baz"``.
:param default: (optional) The default value to return if the attribute could not be
found.
:return: The attribute or the default value if it could not be found.
"""
attr = obj
for _name in name.split("."):
try:
attr = getattr(attr, _name)
except AttributeError:
return default
return attr
[docs]def get_class_by_name(name):
"""Get a class given its name.
:param name: The complete name of the class in the form of ``"foo.bar.Baz"``.
:return: The class or ``None`` if it could not be found.
"""
names = name.rsplit(".", 1)
if len(names) <= 1:
return None
try:
mod = import_module(names[0])
except ImportError:
return None
return getattr(mod, names[1], None)
[docs]def utcnow():
"""Create a timezone aware datetime object of the current time in UTC.
:return: A datetime object as specified in Python's ``datetime`` module.
"""
return datetime.now(timezone.utc)
[docs]def compact_json(data, ensure_ascii=False, sort_keys=True):
"""Serialize data to a compact JSON formatted string.
Uses the JSON encoder provided by Flask, which can deal with some additional types.
:param data: The data to serialize.
:param ensure_ascii: (optional) Whether to escape non-ASCII characters.
:param sort_keys: (optional) Whether to sort the output of dictionaries by key.
:return: The JSON formatted string.
"""
return json.dumps(
data, ensure_ascii=ensure_ascii, sort_keys=sort_keys, separators=(",", ":")
)
[docs]def is_special_float(value):
"""Check if a float value is a special value, i.e. ``nan`` or ``inf``.
:param value: The float value to check.
:return: ``True`` if the value is a special float value, ``False`` otherwise.
"""
return math.isnan(value) or math.isinf(value)
[docs]def is_iterable(value, include_string=False):
"""Check if a value is an iterable.
:param value: The value to check.
:param include_string: (optional) Flag indicating whether a string value should be
treated as a valid iterable or not.
:return: ``True`` if the value is iterable, ``False`` otherwise.
"""
if not include_string and isinstance(value, str):
return False
try:
iter(value)
except TypeError:
return False
return True
[docs]def is_quoted(value):
"""Check if a string value is quoted, i.e. surrounded by double quotes.
:return: ``True`` if the value is quoted, ``False`` otherwise.
"""
return value.startswith('"') and value.endswith('"') and len(value) >= 2
[docs]def find_dict_in_list(dict_list, key, value):
"""Find a dictionary with a specific key and value in a list.
:param dict_list: A list of dictionaries to search.
:param key: The key to search for.
:param value: The value to search for.
:return: The dictionary or ``None`` if it was not found.
"""
for item in dict_list:
if key in item and item[key] == value:
return item
return None
[docs]def flatten_list(values):
"""Flattens a list.
Flattens a list containing single values or nested lists. Multiple layers of nested
lists are not supported.
:param values: List containing single values or nested lists.
:return: The flattened list.
"""
flattened_result = []
for value in values:
if isinstance(value, list):
flattened_result += value
else:
flattened_result.append(value)
return flattened_result
[docs]def as_list(value):
"""Wrap a value inside a list if the given value is not a list already.
:param value: The value to potentially wrap.
:return: The original or wrapped value. If the given value is ``None``, the value
will always be returned as is.
"""
return value if isinstance(value, list) else [value] if value else None
[docs]def has_capabilities(*capabilities):
"""Check if the current Kadi instance has all given capabilities.
:param capabilities: One or more capabilities to check.
:return: ``True`` if all given capabilities are available, ``False`` otherwise.
"""
satisfied_capabilities = [
c in current_app.config["CAPABILITIES"] for c in capabilities
]
return all(satisfied_capabilities)