diff --git a/default/app.conf b/default/app.conf index 234a897..07e46a6 100644 --- a/default/app.conf +++ b/default/app.conf @@ -12,5 +12,5 @@ label = Apprise Alert Action [launcher] author = Michael Clayfield -version = 1.1.0 +version = 1.1.1 description = Alert Action based on Apprise, for sending alerts to many different sources. diff --git a/lib/apprise/URLBase.py b/lib/apprise/URLBase.py index a023170..28100cf 100644 --- a/lib/apprise/URLBase.py +++ b/lib/apprise/URLBase.py @@ -28,7 +28,7 @@ import re from .logger import logger -from time import sleep +import time from datetime import datetime from xml.sax.saxutils import escape as sax_escape @@ -298,12 +298,12 @@ def throttle(self, last_io=None, wait=None): if wait is not None: self.logger.debug('Throttling forced for {}s...'.format(wait)) - sleep(wait) + time.sleep(wait) elif elapsed < self.request_rate_per_sec: self.logger.debug('Throttling for {}s...'.format( self.request_rate_per_sec - elapsed)) - sleep(self.request_rate_per_sec - elapsed) + time.sleep(self.request_rate_per_sec - elapsed) # Update our timestamp before we leave self._last_io_datetime = datetime.now() diff --git a/lib/apprise/__init__.py b/lib/apprise/__init__.py index 6b9f039..bb18eae 100644 --- a/lib/apprise/__init__.py +++ b/lib/apprise/__init__.py @@ -27,7 +27,7 @@ # POSSIBILITY OF SUCH DAMAGE. __title__ = 'Apprise' -__version__ = '1.7.2' +__version__ = '1.7.4' __author__ = 'Chris Caron' __license__ = 'BSD' __copywrite__ = 'Copyright (C) 2024 Chris Caron ' diff --git a/lib/apprise/cli.py b/lib/apprise/cli.py index 0e16b99..11a6cbc 100644 --- a/lib/apprise/cli.py +++ b/lib/apprise/cli.py @@ -67,21 +67,32 @@ DEFAULT_CONFIG_PATHS = ( # Legacy Path Support '~/.apprise', + '~/.apprise.conf', '~/.apprise.yml', + '~/.apprise.yaml', '~/.config/apprise', + '~/.config/apprise.conf', '~/.config/apprise.yml', + '~/.config/apprise.yaml', # Plugin Support Extended Directory Search Paths '~/.apprise/apprise', + '~/.apprise/apprise.conf', '~/.apprise/apprise.yml', + '~/.apprise/apprise.yaml', '~/.config/apprise/apprise', + '~/.config/apprise/apprise.conf', '~/.config/apprise/apprise.yml', + '~/.config/apprise/apprise.yaml', - # Global Configuration Support + # Global Configuration File Support '/etc/apprise', '/etc/apprise.yml', + '/etc/apprise.yaml', '/etc/apprise/apprise', + '/etc/apprise/apprise.conf', '/etc/apprise/apprise.yml', + '/etc/apprise/apprise.yaml', ) # Define our paths to search for plugins @@ -98,9 +109,13 @@ # Default Config Search Path for Windows Users DEFAULT_CONFIG_PATHS = ( expandvars('%APPDATA%\\Apprise\\apprise'), + expandvars('%APPDATA%\\Apprise\\apprise.conf'), expandvars('%APPDATA%\\Apprise\\apprise.yml'), + expandvars('%APPDATA%\\Apprise\\apprise.yaml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise'), + expandvars('%LOCALAPPDATA%\\Apprise\\apprise.conf'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'), + expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'), # # Global Support @@ -108,15 +123,21 @@ # C:\ProgramData\Apprise\ expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'), + expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.conf'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'), + expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'), # C:\Program Files\Apprise expandvars('%PROGRAMFILES%\\Apprise\\apprise'), + expandvars('%PROGRAMFILES%\\Apprise\\apprise.conf'), expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'), + expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'), # C:\Program Files\Common Files expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'), + expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.conf'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'), + expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'), ) # Default Plugin Search Path for Windows Users diff --git a/lib/apprise/manager.py b/lib/apprise/manager.py index 45a6cf0..f07983e 100644 --- a/lib/apprise/manager.py +++ b/lib/apprise/manager.py @@ -32,6 +32,7 @@ import time import hashlib import inspect +import threading from .utils import import_module from .utils import Singleton from .utils import parse_list @@ -60,6 +61,9 @@ class PluginManager(metaclass=Singleton): # The module path to scan module_path = join(abspath(dirname(__file__)), _id) + # thread safe loading + _lock = threading.Lock() + def __init__(self, *args, **kwargs): """ Over-ride our class instantiation to provide a singleton @@ -103,40 +107,49 @@ def __init__(self, *args, **kwargs): # effort/overhead doing it again self._paths_previously_scanned = set() + # Track loaded module paths to prevent from loading them again + self._loaded = set() + def unload_modules(self, disable_native=False): """ Reset our object and unload all modules """ - if self._custom_module_map: - # Handle Custom Module Assignments - for meta in list(self._custom_module_map.values()): - if meta['name'] not in self._module_map: - # Nothing to remove - continue + with self._lock: + if self._custom_module_map: + # Handle Custom Module Assignments + for meta in list(self._custom_module_map.values()): + if meta['name'] not in self._module_map: + # Nothing to remove + continue - # For the purpose of tidying up un-used modules in memory - loaded = [m for m in list(sys.modules.keys()) - if m.startswith( - self._module_map[meta['name']]['path'])] + # For the purpose of tidying up un-used modules in memory + loaded = [m for m in list(sys.modules.keys()) + if m.startswith( + self._module_map[meta['name']]['path'])] - for module_path in loaded: - del sys.modules[module_path] + for module_path in loaded: + del sys.modules[module_path] - # Reset disabled plugins (if any) - for schema in self._disabled: - self._schema_map[schema].enabled = True - self._disabled.clear() + # Reset disabled plugins (if any) + for schema in self._disabled: + self._schema_map[schema].enabled = True + self._disabled.clear() - # Reset our variables - self._module_map = None if not disable_native else {} - self._schema_map = {} - self._custom_module_map = {} + # Reset our variables + self._schema_map = {} + self._custom_module_map = {} + if disable_native: + self._module_map = {} - # Reset our path cache - self._paths_previously_scanned = set() + else: + self._module_map = None + self._loaded = set() - def load_modules(self, path=None, name=None): + # Reset our path cache + self._paths_previously_scanned = set() + + def load_modules(self, path=None, name=None, force=False): """ Load our modules into memory """ @@ -145,102 +158,120 @@ def load_modules(self, path=None, name=None): module_name_prefix = self.module_name_prefix if name is None else name module_path = self.module_path if path is None else path - if not self: - # Initialize our maps - self._module_map = {} - self._schema_map = {} - self._custom_module_map = {} + with self._lock: + if not force and module_path in self._loaded: + # We're done + return - # Used for the detection of additional Notify Services objects - # The .py extension is optional as we support loading directories too - module_re = re.compile( - r'^(?P' + self.fname_prefix + r'[a-z0-9]+)(\.py)?$', re.I) - - t_start = time.time() - for f in os.listdir(module_path): - tl_start = time.time() - match = module_re.match(f) - if not match: - # keep going - continue + # Our base reference + module_count = len(self._module_map) if self._module_map else 0 + schema_count = len(self._schema_map) if self._schema_map else 0 - elif match.group('name') == f'{self.fname_prefix}Base': - # keep going - continue + if not self: + # Initialize our maps + self._module_map = {} + self._schema_map = {} + self._custom_module_map = {} - # Store our notification/plugin name: - module_name = match.group('name') - module_pyname = '{}.{}'.format(module_name_prefix, module_name) + # Used for the detection of additional Notify Services objects + # The .py extension is optional as we support loading directories + # too + module_re = re.compile( + r'^(?P' + self.fname_prefix + r'[a-z0-9]+)(\.py)?$', + re.I) - if module_name in self._module_map: - logger.warning( - "%s(s) (%s) already loaded; ignoring %s", - self.name, module_name, os.path.join(module_path, f)) - continue + t_start = time.time() + for f in os.listdir(module_path): + tl_start = time.time() + match = module_re.match(f) + if not match: + # keep going + continue - try: - module = __import__( - module_pyname, - globals(), locals(), - fromlist=[module_name]) - - except ImportError: - # No problem, we can try again another way... - module = import_module( - os.path.join(module_path, f), module_pyname) - if not module: - # logging found in import_module and not needed here + elif match.group('name') == f'{self.fname_prefix}Base': + # keep going continue - if not hasattr(module, module_name): - # Not a library we can load as it doesn't follow the simple - # rule that the class must bear the same name as the - # notification file itself. - logger.trace( - "%s (%s) import failed; no filename/Class " - "match found in %s", - self.name, module_name, os.path.join(module_path, f)) - continue + # Store our notification/plugin name: + module_name = match.group('name') + module_pyname = '{}.{}'.format(module_name_prefix, module_name) - # Get our plugin - plugin = getattr(module, module_name) - if not hasattr(plugin, 'app_id'): - # Filter out non-notification modules - logger.trace( - "(%s) import failed; no app_id defined in %s", - self.name, module_name, os.path.join(module_path, f)) - continue + if module_name in self._module_map: + logger.warning( + "%s(s) (%s) already loaded; ignoring %s", + self.name, module_name, os.path.join(module_path, f)) + continue - # Add our plugin name to our module map - self._module_map[module_name] = { - 'plugin': set([plugin]), - 'module': module, - 'path': '{}.{}'.format(module_name_prefix, module_name), - 'native': True, - } + try: + module = __import__( + module_pyname, + globals(), locals(), + fromlist=[module_name]) + + except ImportError: + # No problem, we can try again another way... + module = import_module( + os.path.join(module_path, f), module_pyname) + if not module: + # logging found in import_module and not needed here + continue - fn = getattr(plugin, 'schemas', None) - schemas = set([]) if not callable(fn) else fn(plugin) + if not hasattr(module, module_name): + # Not a library we can load as it doesn't follow the simple + # rule that the class must bear the same name as the + # notification file itself. + logger.trace( + "%s (%s) import failed; no filename/Class " + "match found in %s", + self.name, module_name, os.path.join(module_path, f)) + continue - # map our schema to our plugin - for schema in schemas: - if schema in self._schema_map: - logger.error( - "{} schema ({}) mismatch detected - {} to {}" - .format(self.name, schema, self._schema_map, plugin)) + # Get our plugin + plugin = getattr(module, module_name) + if not hasattr(plugin, 'app_id'): + # Filter out non-notification modules + logger.trace( + "(%s) import failed; no app_id defined in %s", + self.name, module_name, os.path.join(module_path, f)) continue - # Assign plugin - self._schema_map[schema] = plugin + # Add our plugin name to our module map + self._module_map[module_name] = { + 'plugin': set([plugin]), + 'module': module, + 'path': '{}.{}'.format(module_name_prefix, module_name), + 'native': True, + } + + fn = getattr(plugin, 'schemas', None) + schemas = set([]) if not callable(fn) else fn(plugin) + + # map our schema to our plugin + for schema in schemas: + if schema in self._schema_map: + logger.error( + "{} schema ({}) mismatch detected - {} to {}" + .format(self.name, schema, self._schema_map, + plugin)) + continue + + # Assign plugin + self._schema_map[schema] = plugin + + logger.trace( + '{} {} loaded in {:.6f}s'.format( + self.name, module_name, (time.time() - tl_start))) + + # Track the directory loaded so we never load it again + self._loaded.add(module_path) - logger.trace( - '{} {} loaded in {:.6f}s'.format( - self.name, module_name, (time.time() - tl_start))) - logger.debug( - '{} {}(s) and {} Schema(s) loaded in {:.4f}s' - .format( - self.name, len(self._module_map), len(self._schema_map), - (time.time() - t_start))) + logger.debug( + '{} {}(s) and {} Schema(s) loaded in {:.4f}s' + .format( + self.name, + len(self._module_map) - module_count, + len(self._schema_map) - schema_count, + (time.time() - t_start))) def module_detection(self, paths, cache=True): """ @@ -364,8 +395,8 @@ def _import_module(path): continue if not cache or \ - (cache and - new_path not in self._paths_previously_scanned): + (cache and new_path not in + self._paths_previously_scanned): # Load our module _import_module(new_path) @@ -373,8 +404,9 @@ def _import_module(path): self._paths_previously_scanned.add(new_path) else: if os.path.isdir(path): - # This logic is safe to apply because we already validated - # the directories state above; update our path + # This logic is safe to apply because we already + # validated the directories state above; update our + # path path = os.path.join(path, '__init__.py') if cache and path in self._paths_previously_scanned: continue @@ -392,7 +424,7 @@ def _import_module(path): # Load our module _import_module(path) - return None + return None def add(self, plugin, schemas=None, url=None, send_func=None): """ @@ -712,4 +744,4 @@ def __bool__(self): """ Determines if object has loaded or not """ - return True if self._module_map is not None else False + return True if self._loaded and self._module_map is not None else False diff --git a/lib/apprise/plugins/NotifyEmail.py b/lib/apprise/plugins/NotifyEmail.py index 99f6ac1..1a3e1e3 100644 --- a/lib/apprise/plugins/NotifyEmail.py +++ b/lib/apprise/plugins/NotifyEmail.py @@ -295,6 +295,21 @@ class SecureMailMode: }, ), + # Comcast.net + ( + 'Comcast.net', + re.compile( + r'^((?P