LINT fixes for the `utils` module + additional documentation

This commit is contained in:
Fabio Manganiello 2023-02-05 18:05:41 +01:00
parent b8fca97891
commit 4849e14414
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
1 changed files with 109 additions and 33 deletions

View File

@ -8,7 +8,6 @@ import logging
import os import os
import pathlib import pathlib
import re import re
import rsa
import signal import signal
import socket import socket
import ssl import ssl
@ -17,14 +16,16 @@ from typing import Optional, Tuple, Union
from dateutil import parser, tz from dateutil import parser, tz
from redis import Redis from redis import Redis
from rsa.key import PublicKey, PrivateKey from rsa.key import PublicKey, PrivateKey, newkeys
logger = logging.getLogger('utils') logger = logging.getLogger('utils')
def get_module_and_method_from_action(action): def get_module_and_method_from_action(action):
"""Input : action=music.mpd.play """
Output : ('music.mpd', 'play')""" Input: action=music.mpd.play
Output: ('music.mpd', 'play')
"""
tokens = action.split('.') tokens = action.split('.')
module_name = str.join('.', tokens[:-1]) module_name = str.join('.', tokens[:-1])
@ -38,22 +39,21 @@ def get_message_class_by_type(msgtype):
try: try:
module = importlib.import_module('platypush.message.' + msgtype) module = importlib.import_module('platypush.message.' + msgtype)
except ImportError as e: except ImportError as e:
logger.warning('Unsupported message type {}'.format(msgtype)) logger.warning('Unsupported message type %s', msgtype)
raise RuntimeError(e) raise RuntimeError(e) from e
cls_name = msgtype[0].upper() + msgtype[1:] cls_name = msgtype[0].upper() + msgtype[1:]
try: try:
msgclass = getattr(module, cls_name) msgclass = getattr(module, cls_name)
except AttributeError as e: except AttributeError as e:
logger.warning('No such class in {}: {}'.format(module.__name__, cls_name)) logger.warning('No such class in %s: %s', module.__name__, cls_name)
raise RuntimeError(e) raise RuntimeError(e) from e
return msgclass return msgclass
# noinspection PyShadowingBuiltins def get_event_class_by_type(type): # pylint: disable=redefined-builtin
def get_event_class_by_type(type):
"""Gets an event class by type name""" """Gets an event class by type name"""
event_module = importlib.import_module('.'.join(type.split('.')[:-1])) event_module = importlib.import_module('.'.join(type.split('.')[:-1]))
return getattr(event_module, type.split('.')[-1]) return getattr(event_module, type.split('.')[-1])
@ -66,7 +66,7 @@ def get_plugin_module_by_name(plugin_name):
try: try:
return importlib.import_module('platypush.plugins.' + plugin_name) return importlib.import_module('platypush.plugins.' + plugin_name)
except ImportError as e: except ImportError as e:
logger.error('Cannot import {}: {}'.format(module_name, str(e))) logger.error('Cannot import %s: %s', module_name, e)
return None return None
@ -85,7 +85,7 @@ def get_plugin_class_by_name(plugin_name):
module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin' module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin'
) )
except Exception as e: except Exception as e:
logger.error('Cannot import class {}: {}'.format(class_name, str(e))) logger.error('Cannot import class %s: %s', class_name, e)
return None return None
@ -191,13 +191,20 @@ def get_decorators(cls, climb_class_hierarchy=False):
return decorators return decorators
def get_redis_queue_name_by_message(msg): def get_redis_queue_name_by_message(msg) -> Optional[str]:
from platypush.message import Message """
Get the Redis queue name for the response(s) associated to a request
message.
if not isinstance(msg, Message): :param msg: Input message, as a :class:`platypush.message.request.Request`
logger.warning('Not a valid message (type: {}): {}'.format(type(msg), msg)) object.
"""
from platypush.message.request import Request
return 'platypush/responses/{}'.format(msg.id) if msg.id else None if not isinstance(msg, Request):
logger.warning('Not a valid request (type: %s): %s', type(msg), msg)
return None
return f'platypush/responses/{msg.id}' if msg.id else None
def _get_ssl_context( def _get_ssl_context(
@ -220,6 +227,9 @@ def _get_ssl_context(
def get_ssl_context(ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None): def get_ssl_context(ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None):
"""
Generic builder for SSL context.
"""
return _get_ssl_context( return _get_ssl_context(
context_type=None, context_type=None,
ssl_cert=ssl_cert, ssl_cert=ssl_cert,
@ -232,6 +242,9 @@ def get_ssl_context(ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=Non
def get_ssl_server_context( def get_ssl_server_context(
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None
): ):
"""
Builder for a server-side SSL context.
"""
return _get_ssl_context( return _get_ssl_context(
context_type=ssl.PROTOCOL_TLS_SERVER, context_type=ssl.PROTOCOL_TLS_SERVER,
ssl_cert=ssl_cert, ssl_cert=ssl_cert,
@ -244,6 +257,9 @@ def get_ssl_server_context(
def get_ssl_client_context( def get_ssl_client_context(
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None
): ):
"""
Builder for a client-side SSL context.
"""
return _get_ssl_context( return _get_ssl_context(
context_type=ssl.PROTOCOL_TLS_CLIENT, context_type=ssl.PROTOCOL_TLS_CLIENT,
ssl_cert=ssl_cert, ssl_cert=ssl_cert,
@ -253,19 +269,22 @@ def get_ssl_client_context(
) )
def set_thread_name(name): def set_thread_name(name: str):
global logger """
Set the name of the current thread.
"""
try: try:
import prctl import prctl
# noinspection PyUnresolvedReferences prctl.set_name(name) # pylint: disable=no-member
prctl.set_name(name)
except ImportError: except ImportError:
logger.debug('Unable to set thread name: prctl module is missing') logger.debug('Unable to set thread name: prctl module is missing')
def find_bins_in_path(bin_name): def find_bins_in_path(bin_name):
"""
Search for a binary in the PATH variable.
"""
return [ return [
os.path.join(p, bin_name) os.path.join(p, bin_name)
for p in os.environ.get('PATH', '').split(':') for p in os.environ.get('PATH', '').split(':')
@ -276,14 +295,14 @@ def find_bins_in_path(bin_name):
def find_files_by_ext(directory, *exts): def find_files_by_ext(directory, *exts):
""" """
Finds all the files in the given directory with the provided extensions Finds all the files in the given directory with the provided extensions.
""" """
if not exts: if not exts:
raise AttributeError('No extensions provided') raise AttributeError('No extensions provided')
if not os.path.isdir(directory): if not os.path.isdir(directory):
raise AttributeError('{} is not a valid directory'.format(directory)) raise AttributeError(f'{directory} is not a valid directory')
min_len = len(min(exts, key=len)) min_len = len(min(exts, key=len))
max_len = len(max(exts, key=len)) max_len = len(max(exts, key=len))
@ -296,7 +315,11 @@ def find_files_by_ext(directory, *exts):
return result return result
def is_process_alive(pid): def is_process_alive(pid: int) -> bool:
"""
:param pid: Process ID.
:return: True if the process with the given PID is alive.
"""
try: try:
os.kill(pid, 0) os.kill(pid, 0)
return True return True
@ -304,7 +327,10 @@ def is_process_alive(pid):
return False return False
def get_ip_or_hostname(): def get_ip_or_hostname() -> str:
"""
Get the the default IP address or hostname of the machine.
"""
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
if ip.startswith('127.') or ip.startswith('::1'): if ip.startswith('127.') or ip.startswith('::1'):
try: try:
@ -318,11 +344,18 @@ def get_ip_or_hostname():
return ip return ip
def get_mime_type(resource): def get_mime_type(resource: str) -> Optional[str]:
"""
Get the MIME type of the given resource.
:param resource: The resource to get the MIME type for - it can be a file
path or a URL.
"""
import magic import magic
if resource.startswith('file://'): if resource.startswith('file://'):
resource = resource[len('file://') :] offset = len('file://')
resource = resource[offset:]
# noinspection HttpUrlsUsage # noinspection HttpUrlsUsage
if resource.startswith('http://') or resource.startswith('https://'): if resource.startswith('http://') or resource.startswith('https://'):
@ -341,8 +374,13 @@ def get_mime_type(resource):
if mime: if mime:
return mime.mime_type if hasattr(mime, 'mime_type') else mime return mime.mime_type if hasattr(mime, 'mime_type') else mime
return None
def camel_case_to_snake_case(string): def camel_case_to_snake_case(string):
"""
Utility function to convert CamelCase to snake_case.
"""
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
@ -364,18 +402,34 @@ def grouper(n, iterable, fillvalue=None):
def is_functional_procedure(obj) -> bool: def is_functional_procedure(obj) -> bool:
"""
Check if the given object is a functional procedure.
"""
return callable(obj) and hasattr(obj, 'procedure') return callable(obj) and hasattr(obj, 'procedure')
def is_functional_hook(obj) -> bool: def is_functional_hook(obj) -> bool:
"""
Check if the given object is a functional hook.
"""
return callable(obj) and hasattr(obj, 'hook') return callable(obj) and hasattr(obj, 'hook')
def is_functional_cron(obj) -> bool: def is_functional_cron(obj) -> bool:
"""
Check if the given object is a functional cron.
"""
return callable(obj) and hasattr(obj, 'cron') and hasattr(obj, 'cron_expression') return callable(obj) and hasattr(obj, 'cron') and hasattr(obj, 'cron_expression')
def run(action, *args, **kwargs): def run(action, *args, **kwargs):
"""
Run the given action with the given arguments. Example:
>>> from platypush.utils import run
>>> run('music.mpd.play', resource='file:///home/user/music.mp3')
"""
from platypush.context import get_plugin from platypush.context import get_plugin
(module_name, method_name) = get_module_and_method_from_action(action) (module_name, method_name) = get_module_and_method_from_action(action)
@ -410,13 +464,13 @@ def generate_rsa_key_pair(
:return: A tuple with the generated ``(priv_key_str, pub_key_str)``. :return: A tuple with the generated ``(priv_key_str, pub_key_str)``.
""" """
logger.info('Generating RSA keypair') logger.info('Generating RSA keypair')
pub_key, priv_key = rsa.newkeys(size) pub_key, priv_key = newkeys(size)
logger.info('Generated RSA keypair') logger.info('Generated RSA keypair')
public_key_str = pub_key.save_pkcs1('PEM').decode() public_key_str = pub_key.save_pkcs1('PEM').decode()
private_key_str = priv_key.save_pkcs1('PEM').decode() private_key_str = priv_key.save_pkcs1('PEM').decode()
if key_file: if key_file:
logger.info('Saving private key to {}'.format(key_file)) logger.info('Saving private key to %s', key_file)
with open(os.path.expanduser(key_file), 'w') as f1, open( with open(os.path.expanduser(key_file), 'w') as f1, open(
os.path.expanduser(key_file) + '.pub', 'w' os.path.expanduser(key_file) + '.pub', 'w'
) as f2: ) as f2:
@ -428,6 +482,9 @@ def generate_rsa_key_pair(
def get_or_generate_jwt_rsa_key_pair(): def get_or_generate_jwt_rsa_key_pair():
"""
Get or generate a JWT RSA key pair.
"""
from platypush.config import Config from platypush.config import Config
key_dir = os.path.join(Config.get('workdir'), 'jwt') key_dir = os.path.join(Config.get('workdir'), 'jwt')
@ -437,8 +494,8 @@ def get_or_generate_jwt_rsa_key_pair():
if os.path.isfile(priv_key_file) and os.path.isfile(pub_key_file): if os.path.isfile(priv_key_file) and os.path.isfile(pub_key_file):
with open(pub_key_file, 'r') as f1, open(priv_key_file, 'r') as f2: with open(pub_key_file, 'r') as f1, open(priv_key_file, 'r') as f2:
return ( return (
rsa.PublicKey.load_pkcs1(f1.read().encode()), PublicKey.load_pkcs1(f1.read().encode()),
rsa.PrivateKey.load_pkcs1(f2.read().encode()), PrivateKey.load_pkcs1(f2.read().encode()),
) )
pathlib.Path(key_dir).mkdir(parents=True, exist_ok=True, mode=0o755) pathlib.Path(key_dir).mkdir(parents=True, exist_ok=True, mode=0o755)
@ -446,6 +503,12 @@ def get_or_generate_jwt_rsa_key_pair():
def get_enabled_plugins() -> dict: def get_enabled_plugins() -> dict:
"""
Get the enabled plugins.
:return: A dictionary with the enabled plugins, in the format ``name`` ->
:class:`platypush.plugins.Plugin` instance.
"""
from platypush.config import Config from platypush.config import Config
from platypush.context import get_plugin from platypush.context import get_plugin
@ -456,13 +519,22 @@ def get_enabled_plugins() -> dict:
if plugin: if plugin:
plugins[name] = plugin plugins[name] = plugin
except Exception as e: except Exception as e:
logger.warning(f'Could not initialize plugin {name}') logger.warning('Could not initialize plugin %s', name)
logger.exception(e) logger.exception(e)
return plugins return plugins
def get_redis() -> Redis: def get_redis() -> Redis:
"""
Get a Redis client on the basis of the Redis configuration.
The Redis configuration can be loaded from:
1. The ``backend.redis`` configuration (``redis_args`` attribute)
2. The ``redis`` plugin.
"""
from platypush.config import Config from platypush.config import Config
return Redis( return Redis(
@ -475,6 +547,10 @@ def get_redis() -> Redis:
def to_datetime(t: Union[str, int, float, datetime.datetime]) -> datetime.datetime: def to_datetime(t: Union[str, int, float, datetime.datetime]) -> datetime.datetime:
"""
Utility function to convert a datetime/timestamp provided as a
string/integer/float/datetime to a ``datetime.datetime`` instance.
"""
if isinstance(t, (int, float)): if isinstance(t, (int, float)):
return datetime.datetime.fromtimestamp(t, tz=tz.tzutc()) return datetime.datetime.fromtimestamp(t, tz=tz.tzutc())
if isinstance(t, str): if isinstance(t, str):