coding_utils.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """ Various coding utils (e.g. around function decoration) """
  2. from __future__ import annotations
  3. import functools
  4. import time
  5. import inspect
  6. import importlib
  7. import pkgutil
  8. from flask import current_app
  9. def delete_key_recursive(value, key):
  10. """Delete key in a multilevel dictionary"""
  11. if isinstance(value, dict):
  12. if key in value:
  13. del value[key]
  14. for k, v in value.items():
  15. value[k] = delete_key_recursive(v, key)
  16. # value.pop(key, None)
  17. elif isinstance(value, list):
  18. for i, v in enumerate(value):
  19. value[i] = delete_key_recursive(v, key)
  20. return value
  21. def optional_arg_decorator(fn):
  22. """
  23. A decorator which _optionally_ accepts arguments.
  24. So a decorator like this:
  25. @optional_arg_decorator
  26. def register_something(fn, optional_arg = 'Default Value'):
  27. ...
  28. return fn
  29. will work in both of these usage scenarios:
  30. @register_something('Custom Name')
  31. def custom_name():
  32. pass
  33. @register_something
  34. def default_name():
  35. pass
  36. Thanks to https://stackoverflow.com/questions/3888158/making-decorators-with-optional-arguments#comment65959042_24617244
  37. """
  38. def wrapped_decorator(*args):
  39. if len(args) == 1 and callable(args[0]):
  40. return fn(args[0])
  41. else:
  42. def real_decorator(decoratee):
  43. return fn(decoratee, *args)
  44. return real_decorator
  45. return wrapped_decorator
  46. def sort_dict(unsorted_dict: dict) -> dict:
  47. sorted_dict = dict(sorted(unsorted_dict.items(), key=lambda item: item[0]))
  48. return sorted_dict
  49. # This function is used for sensors_to_show in follow-up PR it will be moved and renamed to flatten_sensors_to_show
  50. def flatten_unique(nested_list_of_objects: list) -> list:
  51. """
  52. Get unique sensor IDs from a list of `sensors_to_show`.
  53. Handles:
  54. - Lists of sensor IDs
  55. - Dictionaries with a `sensors` key
  56. - Nested lists (one level)
  57. Example:
  58. Input:
  59. [1, [2, 20, 6], 10, [6, 2], {"title":None,"sensors": [10, 15]}, 15]
  60. Output:
  61. [1, 2, 20, 6, 10, 15]
  62. """
  63. all_objects = []
  64. for s in nested_list_of_objects:
  65. if isinstance(s, list):
  66. all_objects.extend(s)
  67. elif isinstance(s, dict):
  68. all_objects.extend(s["sensors"])
  69. else:
  70. all_objects.append(s)
  71. return list(dict.fromkeys(all_objects).keys())
  72. def timeit(func):
  73. """Decorator for printing the time it took to execute the decorated function."""
  74. @functools.wraps(func)
  75. def new_func(*args, **kwargs):
  76. start_time = time.time()
  77. result = func(*args, **kwargs)
  78. elapsed_time = time.time() - start_time
  79. print(f"{func.__name__} finished in {int(elapsed_time * 1_000)} ms")
  80. return result
  81. return new_func
  82. def deprecated(alternative, version: str | None = None):
  83. """Decorator for printing a warning error.
  84. alternative: importable object to use as an alternative to the function/method decorated
  85. version: version in which the function will be sunset
  86. """
  87. def decorator(func):
  88. @functools.wraps(func)
  89. def wrapper(*args, **kwargs):
  90. current_app.logger.warning(
  91. f"The method or function {func.__name__} is deprecated and it is expected to be sunset in version {version}. Please, switch to using {inspect.getmodule(alternative).__name__}:{alternative.__name__} to suppress this warning."
  92. )
  93. return func(*args, **kwargs)
  94. return wrapper
  95. return decorator
  96. def find_classes_module(module, superclass):
  97. classes = []
  98. module_object = importlib.import_module(f"{module}")
  99. module_classes = inspect.getmembers(module_object, inspect.isclass)
  100. classes.extend(
  101. [
  102. (class_name, klass)
  103. for class_name, klass in module_classes
  104. if issubclass(klass, superclass) and klass != superclass
  105. ]
  106. )
  107. return classes
  108. def find_classes_modules(module, superclass, skiptest=True):
  109. classes = []
  110. base_module = importlib.import_module(module)
  111. # root (__init__.py) of the base module
  112. classes += find_classes_module(module, superclass)
  113. for submodule in pkgutil.iter_modules(base_module.__path__):
  114. if skiptest and ("test" in f"{module}.{submodule.name}"):
  115. continue
  116. if submodule.ispkg:
  117. classes.extend(
  118. find_classes_modules(
  119. f"{module}.{submodule.name}", superclass, skiptest=skiptest
  120. )
  121. )
  122. else:
  123. classes += find_classes_module(f"{module}.{submodule.name}", superclass)
  124. return classes
  125. def get_classes_module(module, superclass, skiptest=True) -> dict:
  126. return dict(find_classes_modules(module, superclass, skiptest=skiptest))