|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# BSD 2-Clause License
|
|
|
|
#
|
|
|
|
# Apprise - Push Notification Library.
|
|
|
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
|
|
|
#
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
# modification, are permitted provided that the following conditions are met:
|
|
|
|
#
|
|
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
|
|
# this list of conditions and the following disclaimer.
|
|
|
|
#
|
|
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
|
|
# and/or other materials provided with the distribution.
|
|
|
|
#
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import hashlib
|
|
|
|
import inspect
|
|
|
|
import threading
|
|
|
|
from .utils import import_module
|
|
|
|
from .utils import Singleton
|
|
|
|
from .utils import parse_list
|
|
|
|
from os.path import dirname
|
|
|
|
from os.path import abspath
|
|
|
|
from os.path import join
|
|
|
|
|
|
|
|
from .logger import logger
|
|
|
|
|
|
|
|
|
|
|
|
class PluginManager(metaclass=Singleton):
|
|
|
|
"""
|
|
|
|
Designed to be a singleton object to maintain all initialized loading
|
|
|
|
of modules in memory.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Description (used for logging)
|
|
|
|
name = 'Singleton Plugin'
|
|
|
|
|
|
|
|
# Memory Space
|
|
|
|
_id = 'undefined'
|
|
|
|
|
|
|
|
# Our Module Python path name
|
|
|
|
module_name_prefix = f'apprise.{_id}'
|
|
|
|
|
|
|
|
# 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
|
|
|
|
"""
|
|
|
|
|
|
|
|
self._module_map = None
|
|
|
|
self._schema_map = None
|
|
|
|
|
|
|
|
# This contains a mapping of all plugins dynamicaly loaded at runtime
|
|
|
|
# from external modules such as the @notify decorator
|
|
|
|
#
|
|
|
|
# The elements here will be additionally added to the _schema_map if
|
|
|
|
# there is no conflict otherwise.
|
|
|
|
# The structure looks like the following:
|
|
|
|
# Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py
|
|
|
|
# {
|
|
|
|
# 'path': path,
|
|
|
|
#
|
|
|
|
# 'notify': {
|
|
|
|
# 'schema': {
|
|
|
|
# 'name': 'Custom schema name',
|
|
|
|
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
|
|
|
# 'url': 'schema://any/additional/info/found/on/url'
|
|
|
|
# 'plugin': <CustomNotifyWrapperPlugin>
|
|
|
|
# },
|
|
|
|
# 'schema2': {
|
|
|
|
# 'name': 'Custom schema name',
|
|
|
|
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
|
|
|
# 'url': 'schema://any/additional/info/found/on/url'
|
|
|
|
# 'plugin': <CustomNotifyWrapperPlugin>
|
|
|
|
# }
|
|
|
|
# }
|
|
|
|
# Note: that the <CustomNotifyWrapperPlugin> inherits from
|
|
|
|
# NotifyBase
|
|
|
|
self._custom_module_map = {}
|
|
|
|
|
|
|
|
# Track manually disabled modules (by their schema)
|
|
|
|
self._disabled = set()
|
|
|
|
|
|
|
|
# Hash of all paths previously scanned so we don't waste
|
|
|
|
# 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
|
|
|
|
"""
|
|
|
|
|
|
|
|
with self._lock:
|
|
|
|
if self._custom_module_map:
|
|
|
|
# Handle Custom Module Assignments
|
|
|
|
for meta in 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 sys.modules.keys()
|
|
|
|
if m.startswith(
|
|
|
|
self._module_map[meta['name']]['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 our variables
|
|
|
|
self._schema_map = {}
|
|
|
|
self._custom_module_map = {}
|
|
|
|
if disable_native:
|
|
|
|
self._module_map = {}
|
|
|
|
|
|
|
|
else:
|
|
|
|
self._module_map = None
|
|
|
|
self._loaded = set()
|
|
|
|
|
|
|
|
# Reset our path cache
|
|
|
|
self._paths_previously_scanned = set()
|
|
|
|
|
|
|
|
def load_modules(self, path=None, name=None, force=False):
|
|
|
|
"""
|
|
|
|
Load our modules into memory
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Default value
|
|
|
|
module_name_prefix = self.module_name_prefix if name is None else name
|
|
|
|
module_path = self.module_path if path is None else path
|
|
|
|
|
|
|
|
with self._lock:
|
|
|
|
if not force and module_path in self._loaded:
|
|
|
|
# We're done
|
|
|
|
return
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
if not self:
|
|
|
|
# Initialize our maps
|
|
|
|
self._module_map = {}
|
|
|
|
self._schema_map = {}
|
|
|
|
self._custom_module_map = {}
|
|
|
|
|
|
|
|
# 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<name>' + 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
|
|
|
|
|
|
|
|
elif match.group('name') == f'{self.fname_prefix}Base':
|
|
|
|
# keep going
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Store our notification/plugin name:
|
|
|
|
module_name = match.group('name')
|
|
|
|
module_pyname = '{}.{}'.format(module_name_prefix, module_name)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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.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):
|
|
|
|
"""
|
|
|
|
Leverage the @notify decorator and load all objects found matching
|
|
|
|
this.
|
|
|
|
"""
|
|
|
|
# A simple restriction that we don't allow periods in the filename at
|
|
|
|
# all so it can't be hidden (Linux OS's) and it won't conflict with
|
|
|
|
# Python path naming. This also prevents us from loading any python
|
|
|
|
# file that starts with an underscore or dash
|
|
|
|
# We allow for __init__.py as well
|
|
|
|
module_re = re.compile(
|
|
|
|
r'^(?P<name>[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I)
|
|
|
|
|
|
|
|
# Validate if we're a loadable Python file or not
|
|
|
|
valid_python_file_re = re.compile(r'.+\.py(o|c)?$', re.IGNORECASE)
|
|
|
|
|
|
|
|
if isinstance(paths, str):
|
|
|
|
paths = [paths, ]
|
|
|
|
|
|
|
|
if not paths or not isinstance(paths, (tuple, list)):
|
|
|
|
# We're done
|
|
|
|
return
|
|
|
|
|
|
|
|
def _import_module(path):
|
|
|
|
# Since our plugin name can conflict (as a module) with another
|
|
|
|
# we want to generate random strings to avoid steping on
|
|
|
|
# another's namespace
|
|
|
|
if not (path and valid_python_file_re.match(path)):
|
|
|
|
# Ignore file/module type
|
|
|
|
logger.trace('Plugin Scan: Skipping %s', path)
|
|
|
|
return
|
|
|
|
|
|
|
|
t_start = time.time()
|
|
|
|
module_name = hashlib.sha1(path.encode('utf-8')).hexdigest()
|
|
|
|
module_pyname = "{prefix}.{name}".format(
|
|
|
|
prefix='apprise.custom.module', name=module_name)
|
|
|
|
|
|
|
|
if module_pyname in self._custom_module_map:
|
|
|
|
# First clear out existing entries
|
|
|
|
for schema in \
|
|
|
|
self._custom_module_map[module_pyname]['notify']\
|
|
|
|
.keys():
|
|
|
|
|
|
|
|
# Remove any mapped modules to this file
|
|
|
|
del self._schema_map[schema]
|
|
|
|
|
|
|
|
# Reset
|
|
|
|
del self._custom_module_map[module_pyname]
|
|
|
|
|
|
|
|
# Load our module
|
|
|
|
module = import_module(path, module_pyname)
|
|
|
|
if not module:
|
|
|
|
# No problem, we can't use this object
|
|
|
|
logger.warning('Failed to load custom module: %s', _path)
|
|
|
|
return
|
|
|
|
|
|
|
|
# Print our loaded modules if any
|
|
|
|
if module_pyname in self._custom_module_map:
|
|
|
|
logger.debug(
|
|
|
|
'Custom module %s - %d schema(s) (name=%s) '
|
|
|
|
'loaded in %.6fs', _path,
|
|
|
|
len(self._custom_module_map[module_pyname]['notify']),
|
|
|
|
module_name, (time.time() - t_start))
|
|
|
|
|
|
|
|
# Add our plugin name to our module map
|
|
|
|
self._module_map[module_name] = {
|
|
|
|
'plugin': set(),
|
|
|
|
'module': module,
|
|
|
|
'path': module_pyname,
|
|
|
|
'native': False,
|
|
|
|
}
|
|
|
|
|
|
|
|
for schema, meta in\
|
|
|
|
self._custom_module_map[module_pyname]['notify']\
|
|
|
|
.items():
|
|
|
|
|
|
|
|
# For mapping purposes; map our element in our main list
|
|
|
|
self._module_map[module_name]['plugin'].add(
|
|
|
|
self._schema_map[schema])
|
|
|
|
|
|
|
|
# Log our success
|
|
|
|
logger.info('Loaded custom notification: %s://', schema)
|
|
|
|
else:
|
|
|
|
# The code reaches here if we successfully loaded the Python
|
|
|
|
# module but no hooks/triggers were found. So we can safely
|
|
|
|
# just remove/ignore this entry
|
|
|
|
del sys.modules[module_pyname]
|
|
|
|
return
|
|
|
|
|
|
|
|
# end of _import_module()
|
|
|
|
return
|
|
|
|
|
|
|
|
with self._lock:
|
|
|
|
for _path in paths:
|
|
|
|
path = os.path.abspath(os.path.expanduser(_path))
|
|
|
|
if (cache and path in self._paths_previously_scanned) \
|
|
|
|
or not os.path.exists(path):
|
|
|
|
# We're done as we've already scanned this
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Store our path as a way of hashing it has been handled
|
|
|
|
self._paths_previously_scanned.add(path)
|
|
|
|
|
|
|
|
if os.path.isdir(path) and not \
|
|
|
|
os.path.isfile(os.path.join(path, '__init__.py')):
|
|
|
|
|
|
|
|
logger.debug('Scanning for custom plugins in: %s', path)
|
|
|
|
for entry in os.listdir(path):
|
|
|
|
re_match = module_re.match(entry)
|
|
|
|
if not re_match:
|
|
|
|
# keep going
|
|
|
|
logger.trace('Plugin Scan: Ignoring %s', entry)
|
|
|
|
continue
|
|
|
|
|
|
|
|
new_path = os.path.join(path, entry)
|
|
|
|
if os.path.isdir(new_path):
|
|
|
|
# Update our path
|
|
|
|
new_path = os.path.join(path, entry, '__init__.py')
|
|
|
|
if not os.path.isfile(new_path):
|
|
|
|
logger.trace(
|
|
|
|
'Plugin Scan: Ignoring %s',
|
|
|
|
os.path.join(path, entry))
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not cache or \
|
|
|
|
(cache and new_path not in
|
|
|
|
self._paths_previously_scanned):
|
|
|
|
# Load our module
|
|
|
|
_import_module(new_path)
|
|
|
|
|
|
|
|
# Add our subdir 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
|
|
|
|
path = os.path.join(path, '__init__.py')
|
|
|
|
if cache and path in self._paths_previously_scanned:
|
|
|
|
continue
|
|
|
|
|
|
|
|
self._paths_previously_scanned.add(path)
|
|
|
|
|
|
|
|
# directly load as is
|
|
|
|
re_match = module_re.match(os.path.basename(path))
|
|
|
|
# must be a match and must have a .py extension
|
|
|
|
if not re_match or not re_match.group(1):
|
|
|
|
# keep going
|
|
|
|
logger.trace('Plugin Scan: Ignoring %s', path)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Load our module
|
|
|
|
_import_module(path)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def add(self, plugin, schemas=None, url=None, send_func=None):
|
|
|
|
"""
|
|
|
|
Ability to manually add Notification services to our stack
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
# Acquire a list of schemas
|
|
|
|
p_schemas = parse_list(plugin.secure_protocol, plugin.protocol)
|
|
|
|
if isinstance(schemas, str):
|
|
|
|
schemas = [schemas, ]
|
|
|
|
|
|
|
|
elif schemas is None:
|
|
|
|
# Default
|
|
|
|
schemas = p_schemas
|
|
|
|
|
|
|
|
if not schemas or not isinstance(schemas, (set, tuple, list)):
|
|
|
|
# We're done
|
|
|
|
logger.error(
|
|
|
|
'The schemas provided (type %s) is unsupported; '
|
|
|
|
'loaded from %s.',
|
|
|
|
type(schemas),
|
|
|
|
send_func.__name__ if send_func else plugin.__class__.__name__)
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Convert our schemas into a set
|
|
|
|
schemas = set([s.lower() for s in schemas]) | set(p_schemas)
|
|
|
|
|
|
|
|
# Valdation
|
|
|
|
conflict = [s for s in schemas if s in self]
|
|
|
|
if conflict:
|
|
|
|
# we're already handling this schema
|
|
|
|
logger.warning(
|
|
|
|
'The schema(s) (%s) are already defined and could not be '
|
|
|
|
'loaded from %s%s.',
|
|
|
|
', '.join(conflict),
|
|
|
|
'custom notify function ' if send_func else '',
|
|
|
|
send_func.__name__ if send_func else plugin.__class__.__name__)
|
|
|
|
return False
|
|
|
|
|
|
|
|
if send_func:
|
|
|
|
# Acquire the function name
|
|
|
|
fn_name = send_func.__name__
|
|
|
|
|
|
|
|
# Acquire the python filename path
|
|
|
|
path = inspect.getfile(send_func)
|
|
|
|
|
|
|
|
# Acquire our path to our module
|
|
|
|
module_name = str(send_func.__module__)
|
|
|
|
|
|
|
|
if module_name not in self._custom_module_map:
|
|
|
|
# Support non-dynamic includes as well...
|
|
|
|
self._custom_module_map[module_name] = {
|
|
|
|
# Name can be useful for indexing back into the
|
|
|
|
# _module_map object; this is the key to do it with:
|
|
|
|
'name': module_name.split('.')[-1],
|
|
|
|
|
|
|
|
# The path to the module loaded
|
|
|
|
'path': path,
|
|
|
|
|
|
|
|
# Initialize our template
|
|
|
|
'notify': {},
|
|
|
|
}
|
|
|
|
|
|
|
|
for schema in schemas:
|
|
|
|
self._custom_module_map[module_name]['notify'][schema] = {
|
|
|
|
# The name of the send function the @notify decorator
|
|
|
|
# wrapped
|
|
|
|
'fn_name': fn_name,
|
|
|
|
# The URL that was provided in the @notify decorator call
|
|
|
|
# associated with the 'on='
|
|
|
|
'url': url,
|
|
|
|
}
|
|
|
|
|
|
|
|
else:
|
|
|
|
module_name = hashlib.sha1(
|
|
|
|
''.join(schemas).encode('utf-8')).hexdigest()
|
|
|
|
module_pyname = "{prefix}.{name}".format(
|
|
|
|
prefix='apprise.adhoc.module', name=module_name)
|
|
|
|
|
|
|
|
# Add our plugin name to our module map
|
|
|
|
self._module_map[module_name] = {
|
|
|
|
'plugin': set([plugin]),
|
|
|
|
'module': None,
|
|
|
|
'path': module_pyname,
|
|
|
|
'native': False,
|
|
|
|
}
|
|
|
|
|
|
|
|
for schema in schemas:
|
|
|
|
# Assign our mapping
|
|
|
|
self._schema_map[schema] = plugin
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def remove(self, *schemas):
|
|
|
|
"""
|
|
|
|
Removes a loaded element (if defined)
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
for schema in schemas:
|
|
|
|
try:
|
|
|
|
del self[schema]
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def plugins(self, include_disabled=True):
|
|
|
|
"""
|
|
|
|
Return all of our loaded plugins
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
for module in self._module_map.values():
|
|
|
|
for plugin in module['plugin']:
|
|
|
|
if not include_disabled and not plugin.enabled:
|
|
|
|
continue
|
|
|
|
yield plugin
|
|
|
|
|
|
|
|
def schemas(self, include_disabled=True):
|
|
|
|
"""
|
|
|
|
Return all of our loaded schemas
|
|
|
|
|
|
|
|
if include_disabled == True, then even disabled notifications are
|
|
|
|
returned
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
# Return our list
|
|
|
|
return list(self._schema_map.keys()) if include_disabled else \
|
|
|
|
[s for s in self._schema_map.keys() if self._schema_map[s].enabled]
|
|
|
|
|
|
|
|
def disable(self, *schemas):
|
|
|
|
"""
|
|
|
|
Disables the modules associated with the specified schemas
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
for schema in schemas:
|
|
|
|
if schema not in self._schema_map:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not self._schema_map[schema].enabled:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Disable
|
|
|
|
self._schema_map[schema].enabled = False
|
|
|
|
self._disabled.add(schema)
|
|
|
|
|
|
|
|
def enable_only(self, *schemas):
|
|
|
|
"""
|
|
|
|
Disables the modules associated with the specified schemas
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
# convert to set for faster indexing
|
|
|
|
schemas = set(schemas)
|
|
|
|
|
|
|
|
for plugin in self.plugins():
|
|
|
|
# Get our plugin's schema list
|
|
|
|
p_schemas = set(
|
|
|
|
parse_list(plugin.secure_protocol, plugin.protocol))
|
|
|
|
|
|
|
|
if not schemas & p_schemas:
|
|
|
|
if plugin.enabled:
|
|
|
|
# Disable it (only if previously enabled); this prevents us
|
|
|
|
# from adjusting schemas that were disabled due to missing
|
|
|
|
# libraries or other environment reasons
|
|
|
|
plugin.enabled = False
|
|
|
|
self._disabled |= p_schemas
|
|
|
|
continue
|
|
|
|
|
|
|
|
# If we reach here, our schema was flagged to be enabled
|
|
|
|
if p_schemas & self._disabled:
|
|
|
|
# Previously disabled; no worries, let's clear this up
|
|
|
|
self._disabled -= p_schemas
|
|
|
|
plugin.enabled = True
|
|
|
|
|
|
|
|
def __contains__(self, schema):
|
|
|
|
"""
|
|
|
|
Checks if a schema exists
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
return schema in self._schema_map
|
|
|
|
|
|
|
|
def __delitem__(self, schema):
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
# Get our plugin (otherwise we throw a KeyError) which is
|
|
|
|
# intended on del action that doesn't align
|
|
|
|
plugin = self._schema_map[schema]
|
|
|
|
|
|
|
|
# Our list of all schema entries
|
|
|
|
p_schemas = set([schema])
|
|
|
|
|
|
|
|
for key in list(self._module_map.keys()):
|
|
|
|
if plugin in self._module_map[key]['plugin']:
|
|
|
|
# Remove our plugin
|
|
|
|
self._module_map[key]['plugin'].remove(plugin)
|
|
|
|
|
|
|
|
# Custom Plugin Entry; Clean up cross reference
|
|
|
|
module_pyname = self._module_map[key]['path']
|
|
|
|
if not self._module_map[key]['native'] and \
|
|
|
|
module_pyname in self._custom_module_map:
|
|
|
|
|
|
|
|
del self.\
|
|
|
|
_custom_module_map[module_pyname]['notify'][schema]
|
|
|
|
|
|
|
|
if not self._custom_module_map[module_pyname]['notify']:
|
|
|
|
#
|
|
|
|
# Last custom loaded element
|
|
|
|
#
|
|
|
|
|
|
|
|
# Free up custom object entry
|
|
|
|
del self._custom_module_map[module_pyname]
|
|
|
|
|
|
|
|
if not self._module_map[key]['plugin']:
|
|
|
|
#
|
|
|
|
# Last element
|
|
|
|
#
|
|
|
|
if self._module_map[key]['native']:
|
|
|
|
# Get our plugin's schema list
|
|
|
|
p_schemas = \
|
|
|
|
set([s for s in parse_list(
|
|
|
|
plugin.secure_protocol, plugin.protocol)
|
|
|
|
if s in self._schema_map])
|
|
|
|
|
|
|
|
# free system memory
|
|
|
|
if self._module_map[key]['module']:
|
|
|
|
del sys.modules[self._module_map[key]['path']]
|
|
|
|
|
|
|
|
# free last remaining pointer in module map
|
|
|
|
del self._module_map[key]
|
|
|
|
|
|
|
|
for schema in p_schemas:
|
|
|
|
# Final Tidy
|
|
|
|
del self._schema_map[schema]
|
|
|
|
|
|
|
|
def __setitem__(self, schema, plugin):
|
|
|
|
"""
|
|
|
|
Support fast assigning of Plugin/Notification Objects
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
# Set default values if not otherwise set
|
|
|
|
if not plugin.service_name:
|
|
|
|
# Assign service name if one doesn't exist
|
|
|
|
plugin.service_name = f'{schema}://'
|
|
|
|
|
|
|
|
p_schemas = set(
|
|
|
|
parse_list(plugin.secure_protocol, plugin.protocol))
|
|
|
|
if not p_schemas:
|
|
|
|
# Assign our protocol
|
|
|
|
plugin.secure_protocol = schema
|
|
|
|
p_schemas.add(schema)
|
|
|
|
|
|
|
|
elif schema not in p_schemas:
|
|
|
|
# Add our others (if defined)
|
|
|
|
plugin.secure_protocol = \
|
|
|
|
set([schema] + parse_list(plugin.secure_protocol))
|
|
|
|
p_schemas.add(schema)
|
|
|
|
|
|
|
|
if not self.add(plugin, schemas=p_schemas):
|
|
|
|
raise KeyError('Conflicting Assignment')
|
|
|
|
|
|
|
|
def __getitem__(self, schema):
|
|
|
|
"""
|
|
|
|
Returns the indexed plugin identified by the schema specified
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
return self._schema_map[schema]
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
"""
|
|
|
|
Returns an iterator so we can iterate over our loaded modules
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
return iter(self._module_map.values())
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
"""
|
|
|
|
Returns the number of modules/plugins loaded
|
|
|
|
"""
|
|
|
|
if not self:
|
|
|
|
# Lazy load
|
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
return len(self._module_map)
|
|
|
|
|
|
|
|
def __bool__(self):
|
|
|
|
"""
|
|
|
|
Determines if object has loaded or not
|
|
|
|
"""
|
|
|
|
return True if self._loaded and self._module_map is not None else False
|