mirror of
https://github.com/caronc/apprise.git
synced 2024-11-25 09:33:50 +01:00
Load Dynamic Libraries and Emoji Engine on Demand (#1020)
This commit is contained in:
parent
34a26da4f4
commit
9dcf769397
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
# expanded using subsequent `include` items.
|
# expanded using subsequent `include` items.
|
||||||
matrix:
|
matrix:
|
||||||
os: ["ubuntu-latest"]
|
os: ["ubuntu-latest"]
|
||||||
python-version: ["3.10"]
|
python-version: ["3.11"]
|
||||||
bare: [false]
|
bare: [false]
|
||||||
|
|
||||||
include:
|
include:
|
||||||
@ -39,14 +39,14 @@ jobs:
|
|||||||
# Within the `bare` environment, `all-plugin-requirements.txt` will NOT be
|
# Within the `bare` environment, `all-plugin-requirements.txt` will NOT be
|
||||||
# installed, to verify the application also works without those dependencies.
|
# installed, to verify the application also works without those dependencies.
|
||||||
- os: "ubuntu-latest"
|
- os: "ubuntu-latest"
|
||||||
python-version: "3.10"
|
python-version: "3.11"
|
||||||
bare: true
|
bare: true
|
||||||
|
|
||||||
# Let's save resources and only build a single slot on macOS- and Windows.
|
# Let's save resources and only build a single slot on macOS- and Windows.
|
||||||
- os: "macos-latest"
|
- os: "macos-latest"
|
||||||
python-version: "3.10"
|
python-version: "3.11"
|
||||||
- os: "windows-latest"
|
- os: "windows-latest"
|
||||||
python-version: "3.10"
|
python-version: "3.11"
|
||||||
|
|
||||||
# Test more available versions of CPython on Linux.
|
# Test more available versions of CPython on Linux.
|
||||||
- os: "ubuntu-20.04"
|
- os: "ubuntu-20.04"
|
||||||
|
@ -33,6 +33,7 @@ from itertools import chain
|
|||||||
from . import common
|
from . import common
|
||||||
from .conversion import convert_between
|
from .conversion import convert_between
|
||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
|
from .NotificationManager import NotificationManager
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .utils import parse_urls
|
from .utils import parse_urls
|
||||||
from .utils import cwe312_url
|
from .utils import cwe312_url
|
||||||
@ -45,10 +46,12 @@ from .AppriseLocale import AppriseLocale
|
|||||||
from .config.ConfigBase import ConfigBase
|
from .config.ConfigBase import ConfigBase
|
||||||
from .plugins.NotifyBase import NotifyBase
|
from .plugins.NotifyBase import NotifyBase
|
||||||
|
|
||||||
|
|
||||||
from . import plugins
|
from . import plugins
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
class Apprise:
|
class Apprise:
|
||||||
"""
|
"""
|
||||||
@ -138,7 +141,7 @@ class Apprise:
|
|||||||
# We already have our result set
|
# We already have our result set
|
||||||
results = url
|
results = url
|
||||||
|
|
||||||
if results.get('schema') not in common.NOTIFY_SCHEMA_MAP:
|
if results.get('schema') not in N_MGR:
|
||||||
# schema is a mandatory dictionary item as it is the only way
|
# schema is a mandatory dictionary item as it is the only way
|
||||||
# we can index into our loaded plugins
|
# we can index into our loaded plugins
|
||||||
logger.error('Dictionary does not include a "schema" entry.')
|
logger.error('Dictionary does not include a "schema" entry.')
|
||||||
@ -161,7 +164,7 @@ class Apprise:
|
|||||||
type(url))
|
type(url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not common.NOTIFY_SCHEMA_MAP[results['schema']].enabled:
|
if not N_MGR[results['schema']].enabled:
|
||||||
#
|
#
|
||||||
# First Plugin Enable Check (Pre Initialization)
|
# First Plugin Enable Check (Pre Initialization)
|
||||||
#
|
#
|
||||||
@ -181,13 +184,12 @@ class Apprise:
|
|||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
plugin = N_MGR[results['schema']](**results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Loaded {} URL: {}'.format(
|
'Loaded {} URL: {}'.format(
|
||||||
common.
|
N_MGR[results['schema']].service_name,
|
||||||
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
|
||||||
plugin.url(privacy=asset.secure_logging)))
|
plugin.url(privacy=asset.secure_logging)))
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -198,15 +200,14 @@ class Apprise:
|
|||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
logger.error(
|
logger.error(
|
||||||
'Could not load {} URL: {}'.format(
|
'Could not load {} URL: {}'.format(
|
||||||
common.
|
N_MGR[results['schema']].service_name,
|
||||||
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
|
||||||
loggable_url))
|
loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
plugin = N_MGR[results['schema']](**results)
|
||||||
|
|
||||||
if not plugin.enabled:
|
if not plugin.enabled:
|
||||||
#
|
#
|
||||||
@ -690,7 +691,7 @@ class Apprise:
|
|||||||
'asset': self.asset.details(),
|
'asset': self.asset.details(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for plugin in set(common.NOTIFY_SCHEMA_MAP.values()):
|
for plugin in N_MGR.plugins():
|
||||||
# Iterate over our hashed plugins and dynamically build details on
|
# Iterate over our hashed plugins and dynamically build details on
|
||||||
# their status:
|
# their status:
|
||||||
|
|
||||||
|
@ -33,7 +33,11 @@ from os.path import dirname
|
|||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
from .common import NotifyType
|
from .common import NotifyType
|
||||||
from .utils import module_detection
|
from .NotificationManager import NotificationManager
|
||||||
|
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
class AppriseAsset:
|
class AppriseAsset:
|
||||||
@ -180,7 +184,7 @@ class AppriseAsset:
|
|||||||
|
|
||||||
if plugin_paths:
|
if plugin_paths:
|
||||||
# Load any decorated modules if defined
|
# Load any decorated modules if defined
|
||||||
module_detection(plugin_paths)
|
N_MGR.module_detection(plugin_paths)
|
||||||
|
|
||||||
def color(self, notify_type, color_type=None):
|
def color(self, notify_type, color_type=None):
|
||||||
"""
|
"""
|
||||||
|
@ -26,15 +26,18 @@
|
|||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from . import attachment
|
|
||||||
from . import URLBase
|
from . import URLBase
|
||||||
|
from .attachment.AttachBase import AttachBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
|
from .AttachmentManager import AttachmentManager
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .common import ContentLocation
|
from .common import ContentLocation
|
||||||
from .common import CONTENT_LOCATIONS
|
from .common import CONTENT_LOCATIONS
|
||||||
from .common import ATTACHMENT_SCHEMA_MAP
|
|
||||||
from .utils import GET_SCHEMA_RE
|
from .utils import GET_SCHEMA_RE
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
|
||||||
class AppriseAttachment:
|
class AppriseAttachment:
|
||||||
"""
|
"""
|
||||||
@ -139,7 +142,7 @@ class AppriseAttachment:
|
|||||||
# prepare default asset
|
# prepare default asset
|
||||||
asset = self.asset
|
asset = self.asset
|
||||||
|
|
||||||
if isinstance(attachments, attachment.AttachBase):
|
if isinstance(attachments, AttachBase):
|
||||||
# Go ahead and just add our attachments into our list
|
# Go ahead and just add our attachments into our list
|
||||||
self.attachments.append(attachments)
|
self.attachments.append(attachments)
|
||||||
return True
|
return True
|
||||||
@ -169,7 +172,7 @@ class AppriseAttachment:
|
|||||||
# returns None if it fails
|
# returns None if it fails
|
||||||
instance = AppriseAttachment.instantiate(
|
instance = AppriseAttachment.instantiate(
|
||||||
_attachment, asset=asset, cache=cache)
|
_attachment, asset=asset, cache=cache)
|
||||||
if not isinstance(instance, attachment.AttachBase):
|
if not isinstance(instance, AttachBase):
|
||||||
return_status = False
|
return_status = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -178,7 +181,7 @@ class AppriseAttachment:
|
|||||||
# append our content together
|
# append our content together
|
||||||
instance = _attachment.attachments
|
instance = _attachment.attachments
|
||||||
|
|
||||||
elif not isinstance(_attachment, attachment.AttachBase):
|
elif not isinstance(_attachment, AttachBase):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"An invalid attachment (type={}) was specified.".format(
|
"An invalid attachment (type={}) was specified.".format(
|
||||||
type(_attachment)))
|
type(_attachment)))
|
||||||
@ -228,7 +231,7 @@ class AppriseAttachment:
|
|||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
# Plan B is to assume we're dealing with a file
|
# Plan B is to assume we're dealing with a file
|
||||||
schema = attachment.AttachFile.protocol
|
schema = 'file'
|
||||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -236,13 +239,13 @@ class AppriseAttachment:
|
|||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in ATTACHMENT_SCHEMA_MAP:
|
if schema not in A_MGR:
|
||||||
logger.warning('Unsupported schema {}.'.format(schema))
|
logger.warning('Unsupported schema {}.'.format(schema))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary containing
|
# Parse our url details of the server object as dictionary containing
|
||||||
# all of the information parsed from our URL
|
# all of the information parsed from our URL
|
||||||
results = ATTACHMENT_SCHEMA_MAP[schema].parse_url(url)
|
results = A_MGR[schema].parse_url(url)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
@ -261,8 +264,7 @@ class AppriseAttachment:
|
|||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
attach_plugin = \
|
attach_plugin = A_MGR[results['schema']](**results)
|
||||||
ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
@ -272,7 +274,7 @@ class AppriseAttachment:
|
|||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
attach_plugin = ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
attach_plugin = A_MGR[results['schema']](**results)
|
||||||
|
|
||||||
return attach_plugin
|
return attach_plugin
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
from . import config
|
from . import config
|
||||||
from . import ConfigBase
|
from . import ConfigBase
|
||||||
from . import CONFIG_FORMATS
|
from . import CONFIG_FORMATS
|
||||||
|
from .ConfigurationManager import ConfigurationManager
|
||||||
from . import URLBase
|
from . import URLBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
from . import common
|
from . import common
|
||||||
@ -37,6 +38,9 @@ from .utils import parse_list
|
|||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
|
# Grant access to our Configuration Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
|
||||||
class AppriseConfig:
|
class AppriseConfig:
|
||||||
"""
|
"""
|
||||||
@ -251,7 +255,7 @@ class AppriseConfig:
|
|||||||
logger.debug("Loading raw configuration: {}".format(content))
|
logger.debug("Loading raw configuration: {}".format(content))
|
||||||
|
|
||||||
# Create ourselves a ConfigMemory Object to store our configuration
|
# Create ourselves a ConfigMemory Object to store our configuration
|
||||||
instance = config.ConfigMemory(
|
instance = config.ConfigMemory.ConfigMemory(
|
||||||
content=content, format=format, asset=asset, tag=tag,
|
content=content, format=format, asset=asset, tag=tag,
|
||||||
recursion=recursion, insecure_includes=insecure_includes)
|
recursion=recursion, insecure_includes=insecure_includes)
|
||||||
|
|
||||||
@ -326,7 +330,7 @@ class AppriseConfig:
|
|||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
# Plan B is to assume we're dealing with a file
|
# Plan B is to assume we're dealing with a file
|
||||||
schema = config.ConfigFile.protocol
|
schema = config.ConfigFile.ConfigFile.protocol
|
||||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -334,13 +338,13 @@ class AppriseConfig:
|
|||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in common.CONFIG_SCHEMA_MAP:
|
if schema not in C_MGR:
|
||||||
logger.warning('Unsupported schema {}.'.format(schema))
|
logger.warning('Unsupported schema {}.'.format(schema))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary containing
|
# Parse our url details of the server object as dictionary containing
|
||||||
# all of the information parsed from our URL
|
# all of the information parsed from our URL
|
||||||
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
results = C_MGR[schema].parse_url(url)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
@ -368,8 +372,7 @@ class AppriseConfig:
|
|||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
cfg_plugin = \
|
cfg_plugin = C_MGR[results['schema']](**results)
|
||||||
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
@ -379,7 +382,7 @@ class AppriseConfig:
|
|||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
cfg_plugin = common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
cfg_plugin = C_MGR[results['schema']](**results)
|
||||||
|
|
||||||
return cfg_plugin
|
return cfg_plugin
|
||||||
|
|
||||||
|
54
apprise/AttachmentManager.py
Normal file
54
apprise/AttachmentManager.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, 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.
|
||||||
|
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentManager(PluginManager):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized
|
||||||
|
attachment plugins/modules in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Attachment Plugin'
|
||||||
|
|
||||||
|
# Filename Prefix to filter on
|
||||||
|
fname_prefix = 'Attach'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'attachment'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
54
apprise/ConfigurationManager.py
Normal file
54
apprise/ConfigurationManager.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, 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.
|
||||||
|
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationManager(PluginManager):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized
|
||||||
|
configuration plugins/modules in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Configuration Plugin'
|
||||||
|
|
||||||
|
# Filename Prefix to filter on
|
||||||
|
fname_prefix = 'Config'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'config'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
54
apprise/NotificationManager.py
Normal file
54
apprise/NotificationManager.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, 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.
|
||||||
|
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationManager(PluginManager):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized notifications
|
||||||
|
in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Notification Plugin'
|
||||||
|
|
||||||
|
# Filename Prefix to filter on
|
||||||
|
fname_prefix = 'Notify'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'plugins'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
@ -26,93 +26,14 @@
|
|||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import re
|
# Used for testing
|
||||||
from os import listdir
|
from .AttachBase import AttachBase
|
||||||
from os.path import dirname
|
from ..AttachmentManager import AttachmentManager
|
||||||
from os.path import abspath
|
|
||||||
from ..common import ATTACHMENT_SCHEMA_MAP
|
|
||||||
|
|
||||||
__all__ = []
|
# Initalize our Attachment Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
# Load our Lookup Matrix
|
# Reference
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
|
'AttachBase',
|
||||||
"""
|
]
|
||||||
Dynamically load our schema map; this allows us to gracefully
|
|
||||||
skip over modules we simply don't have the dependencies for.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Used for the detection of additional Attachment Services objects
|
|
||||||
# The .py extension is optional as we support loading directories too
|
|
||||||
module_re = re.compile(r'^(?P<name>Attach[a-z0-9]+)(\.py)?$', re.I)
|
|
||||||
|
|
||||||
for f in listdir(path):
|
|
||||||
match = module_re.match(f)
|
|
||||||
if not match:
|
|
||||||
# keep going
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store our notification/plugin name:
|
|
||||||
plugin_name = match.group('name')
|
|
||||||
try:
|
|
||||||
module = __import__(
|
|
||||||
'{}.{}'.format(name, plugin_name),
|
|
||||||
globals(), locals(),
|
|
||||||
fromlist=[plugin_name])
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem, we can't use this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not hasattr(module, plugin_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.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get our plugin
|
|
||||||
plugin = getattr(module, plugin_name)
|
|
||||||
if not hasattr(plugin, 'app_id'):
|
|
||||||
# Filter out non-notification modules
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif plugin_name in __all__:
|
|
||||||
# we're already handling this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add our module name to our __all__
|
|
||||||
__all__.append(plugin_name)
|
|
||||||
|
|
||||||
# Ensure we provide the class as the reference to this directory and
|
|
||||||
# not the module:
|
|
||||||
globals()[plugin_name] = plugin
|
|
||||||
|
|
||||||
# Load protocol(s) if defined
|
|
||||||
proto = getattr(plugin, 'protocol', None)
|
|
||||||
if isinstance(proto, str):
|
|
||||||
if proto not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[proto] = plugin
|
|
||||||
|
|
||||||
elif isinstance(proto, (set, list, tuple)):
|
|
||||||
# Support iterables list types
|
|
||||||
for p in proto:
|
|
||||||
if p not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
|
||||||
|
|
||||||
# Load secure protocol(s) if defined
|
|
||||||
protos = getattr(plugin, 'secure_protocol', None)
|
|
||||||
if isinstance(protos, str):
|
|
||||||
if protos not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[protos] = plugin
|
|
||||||
|
|
||||||
if isinstance(protos, (set, list, tuple)):
|
|
||||||
# Support iterables list types
|
|
||||||
for p in protos:
|
|
||||||
if p not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
|
||||||
|
|
||||||
return ATTACHMENT_SCHEMA_MAP
|
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
|
||||||
__load_matrix()
|
|
||||||
|
@ -26,50 +26,6 @@
|
|||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
# we mirror our base purely for the ability to reset everything; this
|
|
||||||
# is generally only used in testing and should not be used by developers
|
|
||||||
# It is also used as a means of preventing a module from being reloaded
|
|
||||||
# in the event it already exists
|
|
||||||
NOTIFY_MODULE_MAP = {}
|
|
||||||
|
|
||||||
# Maintains a mapping of all of the Notification services
|
|
||||||
NOTIFY_SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
# 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 NOTIFY_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
|
|
||||||
NOTIFY_CUSTOM_MODULE_MAP = {}
|
|
||||||
|
|
||||||
# Maintains a mapping of all configuration schema's supported
|
|
||||||
CONFIG_SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
# Maintains a mapping of all attachment schema's supported
|
|
||||||
ATTACHMENT_SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
|
|
||||||
class NotifyType:
|
class NotifyType:
|
||||||
"""
|
"""
|
||||||
|
@ -35,16 +35,24 @@ from .. import plugins
|
|||||||
from .. import common
|
from .. import common
|
||||||
from ..AppriseAsset import AppriseAsset
|
from ..AppriseAsset import AppriseAsset
|
||||||
from ..URLBase import URLBase
|
from ..URLBase import URLBase
|
||||||
|
from ..ConfigurationManager import ConfigurationManager
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import parse_urls
|
from ..utils import parse_urls
|
||||||
from ..utils import cwe312_url
|
from ..utils import cwe312_url
|
||||||
|
from ..NotificationManager import NotificationManager
|
||||||
|
|
||||||
# Test whether token is valid or not
|
# Test whether token is valid or not
|
||||||
VALID_TOKEN = re.compile(
|
VALID_TOKEN = re.compile(
|
||||||
r'(?P<token>[a-z0-9][a-z0-9_]+)', re.I)
|
r'(?P<token>[a-z0-9][a-z0-9_]+)', re.I)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
# Grant access to our Configuration Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
|
||||||
class ConfigBase(URLBase):
|
class ConfigBase(URLBase):
|
||||||
"""
|
"""
|
||||||
@ -229,7 +237,7 @@ class ConfigBase(URLBase):
|
|||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in common.CONFIG_SCHEMA_MAP:
|
if schema not in C_MGR:
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Unsupported include schema {}.'.format(schema))
|
'Unsupported include schema {}.'.format(schema))
|
||||||
continue
|
continue
|
||||||
@ -240,7 +248,7 @@ class ConfigBase(URLBase):
|
|||||||
|
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
results = C_MGR[schema].parse_url(url)
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
@ -248,11 +256,10 @@ class ConfigBase(URLBase):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle cross inclusion based on allow_cross_includes rules
|
# Handle cross inclusion based on allow_cross_includes rules
|
||||||
if (common.CONFIG_SCHEMA_MAP[schema].allow_cross_includes ==
|
if (C_MGR[schema].allow_cross_includes ==
|
||||||
common.ContentIncludeMode.STRICT
|
common.ContentIncludeMode.STRICT
|
||||||
and schema not in self.schemas()
|
and schema not in self.schemas()
|
||||||
and not self.insecure_includes) or \
|
and not self.insecure_includes) or C_MGR[schema] \
|
||||||
common.CONFIG_SCHEMA_MAP[schema] \
|
|
||||||
.allow_cross_includes == \
|
.allow_cross_includes == \
|
||||||
common.ContentIncludeMode.NEVER:
|
common.ContentIncludeMode.NEVER:
|
||||||
|
|
||||||
@ -280,8 +287,7 @@ class ConfigBase(URLBase):
|
|||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
cfg_plugin = \
|
cfg_plugin = C_MGR[results['schema']](**results)
|
||||||
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
@ -765,8 +771,7 @@ class ConfigBase(URLBase):
|
|||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
plugin = common.NOTIFY_SCHEMA_MAP[
|
plugin = N_MGR[results['schema']](**results)
|
||||||
results['schema']](**results)
|
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
@ -1094,7 +1099,7 @@ class ConfigBase(URLBase):
|
|||||||
del entries['schema']
|
del entries['schema']
|
||||||
|
|
||||||
# support our special tokens (if they're present)
|
# support our special tokens (if they're present)
|
||||||
if schema in common.NOTIFY_SCHEMA_MAP:
|
if schema in N_MGR:
|
||||||
entries = ConfigBase._special_token_handler(
|
entries = ConfigBase._special_token_handler(
|
||||||
schema, entries)
|
schema, entries)
|
||||||
|
|
||||||
@ -1106,7 +1111,7 @@ class ConfigBase(URLBase):
|
|||||||
|
|
||||||
elif isinstance(tokens, dict):
|
elif isinstance(tokens, dict):
|
||||||
# support our special tokens (if they're present)
|
# support our special tokens (if they're present)
|
||||||
if schema in common.NOTIFY_SCHEMA_MAP:
|
if schema in N_MGR:
|
||||||
tokens = ConfigBase._special_token_handler(
|
tokens = ConfigBase._special_token_handler(
|
||||||
schema, tokens)
|
schema, tokens)
|
||||||
|
|
||||||
@ -1140,7 +1145,7 @@ class ConfigBase(URLBase):
|
|||||||
# Grab our first item
|
# Grab our first item
|
||||||
_results = results.pop(0)
|
_results = results.pop(0)
|
||||||
|
|
||||||
if _results['schema'] not in common.NOTIFY_SCHEMA_MAP:
|
if _results['schema'] not in N_MGR:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'An invalid Apprise schema ({}) in YAML configuration '
|
'An invalid Apprise schema ({}) in YAML configuration '
|
||||||
@ -1213,8 +1218,7 @@ class ConfigBase(URLBase):
|
|||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
plugin = common.\
|
plugin = N_MGR[results['schema']](**results)
|
||||||
NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
@ -1267,8 +1271,7 @@ class ConfigBase(URLBase):
|
|||||||
# Create a copy of our dictionary
|
# Create a copy of our dictionary
|
||||||
tokens = tokens.copy()
|
tokens = tokens.copy()
|
||||||
|
|
||||||
for kw, meta in common.NOTIFY_SCHEMA_MAP[schema]\
|
for kw, meta in N_MGR[schema].template_kwargs.items():
|
||||||
.template_kwargs.items():
|
|
||||||
|
|
||||||
# Determine our prefix:
|
# Determine our prefix:
|
||||||
prefix = meta.get('prefix', '+')
|
prefix = meta.get('prefix', '+')
|
||||||
@ -1311,8 +1314,7 @@ class ConfigBase(URLBase):
|
|||||||
#
|
#
|
||||||
# This function here allows these mappings to take place within the
|
# This function here allows these mappings to take place within the
|
||||||
# YAML file as independant arguments.
|
# YAML file as independant arguments.
|
||||||
class_templates = \
|
class_templates = plugins.details(N_MGR[schema])
|
||||||
plugins.details(common.NOTIFY_SCHEMA_MAP[schema])
|
|
||||||
|
|
||||||
for key in list(tokens.keys()):
|
for key in list(tokens.keys()):
|
||||||
|
|
||||||
|
@ -26,84 +26,14 @@
|
|||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import re
|
# Used for testing
|
||||||
from os import listdir
|
from .ConfigBase import ConfigBase
|
||||||
from os.path import dirname
|
from ..ConfigurationManager import ConfigurationManager
|
||||||
from os.path import abspath
|
|
||||||
from ..logger import logger
|
|
||||||
from ..common import CONFIG_SCHEMA_MAP
|
|
||||||
|
|
||||||
__all__ = []
|
# Initalize our Config Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
# Load our Lookup Matrix
|
# Reference
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
|
'ConfigBase',
|
||||||
"""
|
]
|
||||||
Dynamically load our schema map; this allows us to gracefully
|
|
||||||
skip over modules we simply don't have the dependencies for.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Used for the detection of additional Configuration Services objects
|
|
||||||
# The .py extension is optional as we support loading directories too
|
|
||||||
module_re = re.compile(r'^(?P<name>Config[a-z0-9]+)(\.py)?$', re.I)
|
|
||||||
|
|
||||||
for f in listdir(path):
|
|
||||||
match = module_re.match(f)
|
|
||||||
if not match:
|
|
||||||
# keep going
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store our notification/plugin name:
|
|
||||||
plugin_name = match.group('name')
|
|
||||||
try:
|
|
||||||
module = __import__(
|
|
||||||
'{}.{}'.format(name, plugin_name),
|
|
||||||
globals(), locals(),
|
|
||||||
fromlist=[plugin_name])
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem, we can't use this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not hasattr(module, plugin_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.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get our plugin
|
|
||||||
plugin = getattr(module, plugin_name)
|
|
||||||
if not hasattr(plugin, 'app_id'):
|
|
||||||
# Filter out non-notification modules
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif plugin_name in __all__:
|
|
||||||
# we're already handling this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add our module name to our __all__
|
|
||||||
__all__.append(plugin_name)
|
|
||||||
|
|
||||||
# Ensure we provide the class as the reference to this directory and
|
|
||||||
# not the module:
|
|
||||||
globals()[plugin_name] = plugin
|
|
||||||
|
|
||||||
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 CONFIG_SCHEMA_MAP:
|
|
||||||
logger.error(
|
|
||||||
"Config schema ({}) mismatch detected - {} to {}"
|
|
||||||
.format(schema, CONFIG_SCHEMA_MAP[schema], plugin))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Assign plugin
|
|
||||||
CONFIG_SCHEMA_MAP[schema] = plugin
|
|
||||||
|
|
||||||
return CONFIG_SCHEMA_MAP
|
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
|
||||||
__load_matrix()
|
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from ..plugins.NotifyBase import NotifyBase
|
from ..plugins.NotifyBase import NotifyBase
|
||||||
|
from ..NotificationManager import NotificationManager
|
||||||
from ..utils import URL_DETAILS_RE
|
from ..utils import URL_DETAILS_RE
|
||||||
from ..utils import parse_url
|
from ..utils import parse_url
|
||||||
from ..utils import url_assembly
|
from ..utils import url_assembly
|
||||||
@ -36,6 +37,9 @@ from .. import common
|
|||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
class CustomNotifyPlugin(NotifyBase):
|
class CustomNotifyPlugin(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -91,17 +95,17 @@ class CustomNotifyPlugin(NotifyBase):
|
|||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Acquire our plugin name
|
# Acquire our schema
|
||||||
plugin_name = re_match.group('schema').lower()
|
schema = re_match.group('schema').lower()
|
||||||
|
|
||||||
if not re_match.group('base'):
|
if not re_match.group('base'):
|
||||||
url = '{}://'.format(plugin_name)
|
url = '{}://'.format(schema)
|
||||||
|
|
||||||
# Keep a default set of arguments to apply to all called references
|
# Keep a default set of arguments to apply to all called references
|
||||||
base_args = parse_url(
|
base_args = parse_url(
|
||||||
url, default_schema=plugin_name, verify_host=False, simple=True)
|
url, default_schema=schema, verify_host=False, simple=True)
|
||||||
|
|
||||||
if plugin_name in common.NOTIFY_SCHEMA_MAP:
|
if schema in N_MGR:
|
||||||
# we're already handling this object
|
# we're already handling this object
|
||||||
msg = 'The schema ({}) is already defined and could not be ' \
|
msg = 'The schema ({}) is already defined and could not be ' \
|
||||||
'loaded from custom notify function {}.' \
|
'loaded from custom notify function {}.' \
|
||||||
@ -117,10 +121,10 @@ class CustomNotifyPlugin(NotifyBase):
|
|||||||
|
|
||||||
# Our Service Name
|
# Our Service Name
|
||||||
service_name = name if isinstance(name, str) \
|
service_name = name if isinstance(name, str) \
|
||||||
and name else 'Custom - {}'.format(plugin_name)
|
and name else 'Custom - {}'.format(schema)
|
||||||
|
|
||||||
# Store our matched schema
|
# Store our matched schema
|
||||||
secure_protocol = plugin_name
|
secure_protocol = schema
|
||||||
|
|
||||||
requirements = {
|
requirements = {
|
||||||
# Define our required packaging in order to work
|
# Define our required packaging in order to work
|
||||||
@ -181,51 +185,26 @@ class CustomNotifyPlugin(NotifyBase):
|
|||||||
# Unhandled Exception
|
# Unhandled Exception
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'An exception occured sending a %s notification.',
|
'An exception occured sending a %s notification.',
|
||||||
common.
|
N_MGR[self.secure_protocol].service_name)
|
||||||
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'%s Exception: %s',
|
'%s Exception: %s',
|
||||||
common.NOTIFY_SCHEMA_MAP[self.secure_protocol], str(e))
|
N_MGR[self.secure_protocol], str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Sent %s notification.',
|
'Sent %s notification.',
|
||||||
common.
|
N_MGR[self.secure_protocol].service_name)
|
||||||
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send %s notification.',
|
'Failed to send %s notification.',
|
||||||
common.
|
N_MGR[self.secure_protocol].service_name)
|
||||||
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Store our plugin into our core map file
|
# Store our plugin into our core map file
|
||||||
common.NOTIFY_SCHEMA_MAP[plugin_name] = CustomNotifyPluginWrapper
|
return N_MGR.add(
|
||||||
|
plugin=CustomNotifyPluginWrapper,
|
||||||
# Update our custom plugin map
|
schemas=schema,
|
||||||
module_pyname = str(send_func.__module__)
|
send_func=send_func,
|
||||||
if module_pyname not in common.NOTIFY_CUSTOM_MODULE_MAP:
|
url=url,
|
||||||
# Support non-dynamic includes as well...
|
)
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname] = {
|
|
||||||
'path': inspect.getfile(send_func),
|
|
||||||
|
|
||||||
# Initialize our template
|
|
||||||
'notify': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
common.\
|
|
||||||
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'][plugin_name] = {
|
|
||||||
# Our Serivice Description (for API and CLI --details view)
|
|
||||||
'name': CustomNotifyPluginWrapper.service_name,
|
|
||||||
# The name of the send function the @notify decorator wrapped
|
|
||||||
'fn_name': send_func.__name__,
|
|
||||||
# The URL that was provided in the @notify decorator call
|
|
||||||
# associated with the 'on='
|
|
||||||
'url': url,
|
|
||||||
# The Initialized Plugin that was generated based on the above
|
|
||||||
# parameters
|
|
||||||
'plugin': CustomNotifyPluginWrapper}
|
|
||||||
|
|
||||||
# return our plugin
|
|
||||||
return common.NOTIFY_SCHEMA_MAP[plugin_name]
|
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
from .logger import logger
|
||||||
|
|
||||||
# All Emoji's are wrapped in this character
|
# All Emoji's are wrapped in this character
|
||||||
DELIM = ':'
|
DELIM = ':'
|
||||||
@ -2242,10 +2244,8 @@ EMOJI_MAP = {
|
|||||||
DELIM + r'wales' + DELIM: '🏴',
|
DELIM + r'wales' + DELIM: '🏴',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Our compiled mapping
|
# Define our singlton
|
||||||
EMOJI_COMPILED_MAP = re.compile(
|
EMOJI_COMPILED_MAP = None
|
||||||
r'(' + '|'.join(EMOJI_MAP.keys()) + r')',
|
|
||||||
re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_emojis(content):
|
def apply_emojis(content):
|
||||||
@ -2254,6 +2254,17 @@ def apply_emojis(content):
|
|||||||
utf-8 encoded mapping
|
utf-8 encoded mapping
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
global EMOJI_COMPILED_MAP
|
||||||
|
|
||||||
|
if EMOJI_COMPILED_MAP is None:
|
||||||
|
t_start = time.time()
|
||||||
|
# Perform our compilation
|
||||||
|
EMOJI_COMPILED_MAP = re.compile(
|
||||||
|
r'(' + '|'.join(EMOJI_MAP.keys()) + r')',
|
||||||
|
re.IGNORECASE)
|
||||||
|
logger.trace(
|
||||||
|
'Emoji engine loaded in {:.4f}s'.format((time.time() - t_start)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return EMOJI_COMPILED_MAP.sub(lambda x: EMOJI_MAP[x.group()], content)
|
return EMOJI_COMPILED_MAP.sub(lambda x: EMOJI_MAP[x.group()], content)
|
||||||
|
|
||||||
|
718
apprise/manager.py
Normal file
718
apprise/manager.py
Normal file
@ -0,0 +1,718 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, 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
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 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._module_map = None if not disable_native else {}
|
||||||
|
self._schema_map = {}
|
||||||
|
self._custom_module_map = {}
|
||||||
|
|
||||||
|
# Reset our path cache
|
||||||
|
self._paths_previously_scanned = set()
|
||||||
|
|
||||||
|
def load_modules(self, path=None, name=None):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
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)))
|
||||||
|
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)))
|
||||||
|
|
||||||
|
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 {:.6f}s',
|
||||||
|
_path, module_name,
|
||||||
|
len(self._custom_module_map[module_pyname]['notify']),
|
||||||
|
(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
|
||||||
|
|
||||||
|
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._module_map is not None else False
|
@ -40,7 +40,6 @@ from ..AppriseLocale import gettext_lazy as _
|
|||||||
NOTIFY_MACOSX_SUPPORT_ENABLED = False
|
NOTIFY_MACOSX_SUPPORT_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
# TODO: The module will be easier to test without module-level code.
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
# Check this is Mac OS X 10.8, or higher
|
# Check this is Mac OS X 10.8, or higher
|
||||||
major, minor = platform.mac_ver()[0].split('.')[:2]
|
major, minor = platform.mac_ver()[0].split('.')[:2]
|
||||||
|
@ -27,12 +27,8 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from os.path import dirname
|
|
||||||
from os.path import abspath
|
|
||||||
|
|
||||||
# Used for testing
|
# Used for testing
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
|
|
||||||
@ -40,13 +36,17 @@ from ..common import NotifyImageSize
|
|||||||
from ..common import NOTIFY_IMAGE_SIZES
|
from ..common import NOTIFY_IMAGE_SIZES
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NOTIFY_TYPES
|
from ..common import NOTIFY_TYPES
|
||||||
from .. import common
|
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import cwe312_url
|
from ..utils import cwe312_url
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
from ..AppriseLocale import LazyTranslation
|
from ..AppriseLocale import LazyTranslation
|
||||||
|
from ..NotificationManager import NotificationManager
|
||||||
|
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Reference
|
# Reference
|
||||||
@ -58,101 +58,6 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Load our Lookup Matrix
|
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
|
||||||
"""
|
|
||||||
Dynamically load our schema map; this allows us to gracefully
|
|
||||||
skip over modules we simply don't have the dependencies for.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# 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>Notify[a-z0-9]+)(\.py)?$', re.I)
|
|
||||||
|
|
||||||
for f in os.listdir(path):
|
|
||||||
match = module_re.match(f)
|
|
||||||
if not match:
|
|
||||||
# keep going
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store our notification/plugin name:
|
|
||||||
plugin_name = match.group('name')
|
|
||||||
try:
|
|
||||||
module = __import__(
|
|
||||||
'{}.{}'.format(name, plugin_name),
|
|
||||||
globals(), locals(),
|
|
||||||
fromlist=[plugin_name])
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem, we can't use this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not hasattr(module, plugin_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.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get our plugin
|
|
||||||
plugin = getattr(module, plugin_name)
|
|
||||||
if not hasattr(plugin, 'app_id'):
|
|
||||||
# Filter out non-notification modules
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif plugin_name in common.NOTIFY_MODULE_MAP:
|
|
||||||
# we're already handling this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add our plugin name to our module map
|
|
||||||
common.NOTIFY_MODULE_MAP[plugin_name] = {
|
|
||||||
'plugin': plugin,
|
|
||||||
'module': module,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add our module name to our __all__
|
|
||||||
__all__.append(plugin_name)
|
|
||||||
|
|
||||||
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 common.NOTIFY_SCHEMA_MAP:
|
|
||||||
logger.error(
|
|
||||||
"Notification schema ({}) mismatch detected - {} to {}"
|
|
||||||
.format(schema, common.NOTIFY_SCHEMA_MAP[schema], plugin))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Assign plugin
|
|
||||||
common.NOTIFY_SCHEMA_MAP[schema] = plugin
|
|
||||||
|
|
||||||
return common.NOTIFY_SCHEMA_MAP
|
|
||||||
|
|
||||||
|
|
||||||
# Reset our Lookup Matrix
|
|
||||||
def __reset_matrix():
|
|
||||||
"""
|
|
||||||
Restores the Lookup matrix to it's base setting. This is only used through
|
|
||||||
testing and should not be directly called.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Reset our schema map
|
|
||||||
common.NOTIFY_SCHEMA_MAP.clear()
|
|
||||||
|
|
||||||
# Iterate over our module map so we can clear out our __all__ and globals
|
|
||||||
for plugin_name in common.NOTIFY_MODULE_MAP.keys():
|
|
||||||
|
|
||||||
# Remove element from plugins
|
|
||||||
__all__.remove(plugin_name)
|
|
||||||
|
|
||||||
# Clear out our module map
|
|
||||||
common.NOTIFY_MODULE_MAP.clear()
|
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_token(tokens, default_delimiter):
|
def _sanitize_token(tokens, default_delimiter):
|
||||||
"""
|
"""
|
||||||
This is called by the details() function and santizes the output by
|
This is called by the details() function and santizes the output by
|
||||||
@ -176,6 +81,10 @@ def _sanitize_token(tokens, default_delimiter):
|
|||||||
# Do not touch this field
|
# Do not touch this field
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
elif 'name' not in tokens[key]:
|
||||||
|
# Default to key
|
||||||
|
tokens[key]['name'] = key
|
||||||
|
|
||||||
if 'map_to' not in tokens[key]:
|
if 'map_to' not in tokens[key]:
|
||||||
# Default type to key
|
# Default type to key
|
||||||
tokens[key]['map_to'] = key
|
tokens[key]['map_to'] = key
|
||||||
@ -538,16 +447,16 @@ def url_to_dict(url, secure_logging=True):
|
|||||||
|
|
||||||
# Ensure our schema is always in lower case
|
# Ensure our schema is always in lower case
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
if schema not in common.NOTIFY_SCHEMA_MAP:
|
if schema not in N_MGR:
|
||||||
# Give the user the benefit of the doubt that the user may be using
|
# Give the user the benefit of the doubt that the user may be using
|
||||||
# one of the URLs provided to them by their notification service.
|
# one of the URLs provided to them by their notification service.
|
||||||
# Before we fail for good, just scan all the plugins that support the
|
# Before we fail for good, just scan all the plugins that support the
|
||||||
# native_url() parse function
|
# native_url() parse function
|
||||||
results = \
|
results = None
|
||||||
next((r['plugin'].parse_native_url(_url)
|
for plugin in N_MGR.plugins():
|
||||||
for r in common.NOTIFY_MODULE_MAP.values()
|
results = plugin.parse_native_url(_url)
|
||||||
if r['plugin'].parse_native_url(_url) is not None),
|
if results:
|
||||||
None)
|
break
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable URL {}'.format(loggable_url))
|
logger.error('Unparseable URL {}'.format(loggable_url))
|
||||||
@ -560,14 +469,14 @@ def url_to_dict(url, secure_logging=True):
|
|||||||
else:
|
else:
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = common.NOTIFY_SCHEMA_MAP[schema].parse_url(_url)
|
results = N_MGR[schema].parse_url(_url)
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable {} URL {}'.format(
|
logger.error('Unparseable {} URL {}'.format(
|
||||||
common.NOTIFY_SCHEMA_MAP[schema].service_name, loggable_url))
|
N_MGR[schema].service_name, loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
||||||
common.NOTIFY_SCHEMA_MAP[schema].service_name, url,
|
N_MGR[schema].service_name, url,
|
||||||
os.linesep, os.linesep.join(
|
os.linesep, os.linesep.join(
|
||||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||||
|
|
||||||
|
160
apprise/utils.py
160
apprise/utils.py
@ -31,14 +31,12 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import hashlib
|
|
||||||
import locale
|
import locale
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from . import common
|
from . import common
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@ -70,16 +68,12 @@ def import_module(path, name):
|
|||||||
module = None
|
module = None
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Custom module exception raised from %s (name=%s) %s',
|
'Module exception raised from %s (name=%s) %s',
|
||||||
path, name, str(e))
|
path, name, str(e))
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
# Hash of all paths previously scanned so we don't waste effort/overhead doing
|
|
||||||
# it again
|
|
||||||
PATHS_PREVIOUSLY_SCANNED = set()
|
|
||||||
|
|
||||||
# URL Indexing Table for returns via parse_url()
|
# URL Indexing Table for returns via parse_url()
|
||||||
# The below accepts and scans for:
|
# The below accepts and scans for:
|
||||||
# - schema://
|
# - schema://
|
||||||
@ -214,6 +208,22 @@ VALID_PYTHON_FILE_RE = re.compile(r'.+\.py(o|c)?$', re.IGNORECASE)
|
|||||||
REGEX_VALIDATE_LOOKUP = {}
|
REGEX_VALIDATE_LOOKUP = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton(type):
|
||||||
|
"""
|
||||||
|
Our Singleton MetaClass
|
||||||
|
"""
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
instantiate our singleton meta entry
|
||||||
|
"""
|
||||||
|
if cls not in cls._instances:
|
||||||
|
# we have not every built an instance before. Build one now.
|
||||||
|
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||||
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
class TemplateType:
|
class TemplateType:
|
||||||
"""
|
"""
|
||||||
Defines the different template types we can perform parsing on
|
Defines the different template types we can perform parsing on
|
||||||
@ -1561,142 +1571,6 @@ def remove_suffix(value, suffix):
|
|||||||
return value[:-len(suffix)] if value.endswith(suffix) else value
|
return value[:-len(suffix)] if value.endswith(suffix) else value
|
||||||
|
|
||||||
|
|
||||||
def module_detection(paths, cache=True):
|
|
||||||
"""
|
|
||||||
Iterates over a defined path for apprise decorators to load such as
|
|
||||||
@notify.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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 __init__.py as well
|
|
||||||
module_re = re.compile(
|
|
||||||
r'^(?P<name>[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I)
|
|
||||||
|
|
||||||
if isinstance(paths, str):
|
|
||||||
paths = [paths, ]
|
|
||||||
|
|
||||||
if not paths or not isinstance(paths, (tuple, list)):
|
|
||||||
# We're done
|
|
||||||
return None
|
|
||||||
|
|
||||||
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 None
|
|
||||||
|
|
||||||
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 common.NOTIFY_CUSTOM_MODULE_MAP:
|
|
||||||
# First clear out existing entries
|
|
||||||
for schema in common.\
|
|
||||||
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'] \
|
|
||||||
.keys():
|
|
||||||
# Remove any mapped modules to this file
|
|
||||||
del common.NOTIFY_SCHEMA_MAP[schema]
|
|
||||||
|
|
||||||
# Reset
|
|
||||||
del common.NOTIFY_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 None
|
|
||||||
|
|
||||||
# Print our loaded modules if any
|
|
||||||
if module_pyname in common.NOTIFY_CUSTOM_MODULE_MAP:
|
|
||||||
logger.debug(
|
|
||||||
'Loaded custom module: %s (name=%s)',
|
|
||||||
_path, module_name)
|
|
||||||
|
|
||||||
for schema, meta in common.\
|
|
||||||
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify']\
|
|
||||||
.items():
|
|
||||||
|
|
||||||
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 None
|
|
||||||
|
|
||||||
# end of _import_module()
|
|
||||||
return None
|
|
||||||
|
|
||||||
for _path in paths:
|
|
||||||
path = os.path.abspath(os.path.expanduser(_path))
|
|
||||||
if (cache and path in 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
|
|
||||||
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 PATHS_PREVIOUSLY_SCANNED):
|
|
||||||
# Load our module
|
|
||||||
_import_module(new_path)
|
|
||||||
|
|
||||||
# Add our subdir path
|
|
||||||
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 PATHS_PREVIOUSLY_SCANNED:
|
|
||||||
continue
|
|
||||||
|
|
||||||
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 dict_full_update(dict1, dict2):
|
def dict_full_update(dict1, dict2):
|
||||||
"""
|
"""
|
||||||
Takes 2 dictionaries (dict1 and dict2) that contain sub-dictionaries and
|
Takes 2 dictionaries (dict1 and dict2) that contain sub-dictionaries and
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
diff -Naur apprise-1.2.0/test/test_plugin_macosx.py apprise-1.2.0.patched/test/test_plugin_macosx.py
|
diff -Naur apprise-1.6.0/test/test_plugin_macosx.py apprise-1.6.0.patched/test/test_plugin_macosx.py
|
||||||
--- apprise-1.2.0/test/test_plugin_macosx.py 2022-11-02 14:59:59.000000000 -0400
|
--- apprise-1.6.0/test/test_plugin_macosx.py 2023-12-22 16:51:24.000000000 -0500
|
||||||
+++ apprise-1.2.0.patched/test/test_plugin_macosx.py 2022-11-15 13:25:27.991284405 -0500
|
+++ apprise-1.6.0.patched/test/test_plugin_macosx.py 2023-12-22 17:38:35.720131819 -0500
|
||||||
@@ -38,7 +38,7 @@
|
@@ -42,9 +42,8 @@
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
-if sys.platform not in ["darwin", "linux"]:
|
-if sys.platform not in ["darwin", "linux"]:
|
||||||
+if sys.platform not in ["darwin"]:
|
- pytest.skip("Only makes sense on macOS, but testable in Linux",
|
||||||
pytest.skip("Only makes sense on macOS, but also works on Linux",
|
- allow_module_level=True)
|
||||||
allow_module_level=True)
|
+if sys.platform != "darwin":
|
||||||
|
+ pytest.skip("MacOS test only", allow_module_level=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
diff -Naur apprise-1.2.0/test/conftest.py apprise-1.2.0.patched/test/conftest.py
|
diff -Naur apprise-1.6.0/test/conftest.py apprise-1.6.0-patched/test/conftest.py
|
||||||
--- apprise-1.2.0/test/conftest.py 2022-11-02 14:00:13.000000000 -0400
|
--- apprise-1.6.0/test/conftest.py 2023-12-27 11:20:40.000000000 -0500
|
||||||
+++ apprise-1.2.0.patched/test/conftest.py 2022-11-15 10:28:23.793969418 -0500
|
+++ apprise-1.6.0-patched/test/conftest.py 2023-12-27 13:43:22.583100037 -0500
|
||||||
@@ -32,12 +32,12 @@
|
@@ -45,8 +45,8 @@
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers'))
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
|
||||||
-@pytest.fixture(scope="session", autouse=True)
|
-@pytest.fixture(scope="function", autouse=True)
|
||||||
-def no_throttling_everywhere(session_mocker):
|
-def no_throttling_everywhere(session_mocker):
|
||||||
+@pytest.fixture(autouse=True)
|
+@pytest.fixture(autouse=True)
|
||||||
+def no_throttling_everywhere(mocker):
|
+def no_throttling_everywhere(mocker):
|
||||||
"""
|
"""
|
||||||
A pytest session fixture which disables throttling on all notifiers.
|
A pytest session fixture which disables throttling on all notifiers.
|
||||||
It is automatically enabled.
|
It is automatically enabled.
|
||||||
"""
|
@@ -57,4 +57,4 @@
|
||||||
for notifier in NOTIFY_MODULE_MAP.values():
|
A_MGR.unload_modules()
|
||||||
plugin = notifier["plugin"]
|
|
||||||
|
for plugin in N_MGR.plugins():
|
||||||
- session_mocker.patch.object(plugin, "request_rate_per_sec", 0)
|
- session_mocker.patch.object(plugin, "request_rate_per_sec", 0)
|
||||||
+ mocker.patch.object(plugin, "request_rate_per_sec", 0)
|
+ mocker.patch.object(plugin, "request_rate_per_sec", 0)
|
||||||
|
@ -31,17 +31,30 @@ import os
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from apprise.common import NOTIFY_MODULE_MAP
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
from apprise.ConfigurationManager import ConfigurationManager
|
||||||
|
from apprise.AttachmentManager import AttachmentManager
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers'))
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
# Grant access to our Config Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
# Grant access to our Attachment Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
def no_throttling_everywhere(session_mocker):
|
def no_throttling_everywhere(session_mocker):
|
||||||
"""
|
"""
|
||||||
A pytest session fixture which disables throttling on all notifiers.
|
A pytest session fixture which disables throttling on all notifiers.
|
||||||
It is automatically enabled.
|
It is automatically enabled.
|
||||||
"""
|
"""
|
||||||
for notifier in NOTIFY_MODULE_MAP.values():
|
# Ensure we're working with a clean slate for each test
|
||||||
plugin = notifier["plugin"]
|
N_MGR.unload_modules()
|
||||||
|
C_MGR.unload_modules()
|
||||||
|
A_MGR.unload_modules()
|
||||||
|
|
||||||
|
for plugin in N_MGR.plugins():
|
||||||
session_mocker.patch.object(plugin, "request_rate_per_sec", 0)
|
session_mocker.patch.object(plugin, "request_rate_per_sec", 0)
|
||||||
|
@ -26,11 +26,21 @@
|
|||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
from importlib import import_module, reload
|
from importlib import import_module, reload
|
||||||
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
from apprise.AttachmentManager import AttachmentManager
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
# Grant access to our Attachment Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
|
||||||
def reload_plugin(name, replace_in=None):
|
def reload_plugin(name):
|
||||||
"""
|
"""
|
||||||
Reload built-in plugin module, e.g. `NotifyGnome`.
|
Reload built-in plugin module, e.g. `NotifyGnome`.
|
||||||
|
|
||||||
@ -40,21 +50,72 @@ def reload_plugin(name, replace_in=None):
|
|||||||
See also https://stackoverflow.com/questions/31363311.
|
See also https://stackoverflow.com/questions/31363311.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
module_name = f"apprise.plugins.{name}"
|
A_MGR.unload_modules()
|
||||||
|
|
||||||
reload(sys.modules['apprise.common'])
|
reload(sys.modules['apprise.attachment.AttachBase'])
|
||||||
reload(sys.modules['apprise.attachment'])
|
reload(sys.modules['apprise.AppriseAttachment'])
|
||||||
reload(sys.modules['apprise.config'])
|
new_apprise_attachment_mod = import_module('apprise.AppriseAttachment')
|
||||||
if module_name in sys.modules:
|
reload(sys.modules['apprise.AttachmentManager'])
|
||||||
reload(sys.modules[module_name])
|
|
||||||
reload(sys.modules['apprise.plugins'])
|
module_pyname = '{}.{}'.format(N_MGR.module_name_prefix, name)
|
||||||
|
if module_pyname in sys.modules:
|
||||||
|
reload(sys.modules[module_pyname])
|
||||||
|
new_notify_mod = import_module(module_pyname)
|
||||||
|
|
||||||
|
reload(sys.modules['apprise.NotificationManager'])
|
||||||
reload(sys.modules['apprise.Apprise'])
|
reload(sys.modules['apprise.Apprise'])
|
||||||
reload(sys.modules['apprise.utils'])
|
reload(sys.modules['apprise.utils'])
|
||||||
reload(sys.modules['apprise'])
|
reload(sys.modules['apprise'])
|
||||||
|
|
||||||
# Fix reference to new plugin class in given module.
|
# Filter our keys
|
||||||
# Needed for updating the module-level import reference like
|
tests = [k for k in sys.modules.keys() if re.match(r'^test_.+$', k)]
|
||||||
# `from apprise.plugins.NotifyMacOSX import NotifyMacOSX`.
|
for module_name in tests:
|
||||||
if replace_in is not None:
|
possible_matches = \
|
||||||
mod = import_module(module_name)
|
[m for m in dir(sys.modules[module_name])
|
||||||
setattr(replace_in, name, getattr(mod, name))
|
if re.match(r'^(?P<name>Notify[a-z0-9]+)$', m, re.I)]
|
||||||
|
if not possible_matches:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fix reference to new plugin class in given module.
|
||||||
|
# Needed for updating the module-level import reference like
|
||||||
|
# `from apprise.plugins.NotifyABCDE import NotifyABCDE`.
|
||||||
|
#
|
||||||
|
# We reload NotifyABCDE and place it back in its spot
|
||||||
|
test_mod = import_module(module_name)
|
||||||
|
setattr(test_mod, name, getattr(new_notify_mod, name))
|
||||||
|
|
||||||
|
# Detect our Apprise Modules (include helpers)
|
||||||
|
apprise_modules = \
|
||||||
|
[k for k in sys.modules.keys()
|
||||||
|
if re.match(r'^(apprise|helpers)(\.|.+)$', k)]
|
||||||
|
|
||||||
|
for entry in A_MGR:
|
||||||
|
reload(sys.modules[entry['path']])
|
||||||
|
for module_pyname in chain(apprise_modules, tests):
|
||||||
|
detect = re.compile(
|
||||||
|
r'^(?P<name>(AppriseAttachment|AttachBase|' +
|
||||||
|
entry['path'].split('.')[-1] + r'))$')
|
||||||
|
|
||||||
|
possible_matches = \
|
||||||
|
[m for m in dir(sys.modules[module_pyname]) if detect.match(m)]
|
||||||
|
if not possible_matches:
|
||||||
|
continue
|
||||||
|
|
||||||
|
apprise_mod = import_module(module_pyname)
|
||||||
|
# Fix reference to new plugin class in given module.
|
||||||
|
# Needed for updating the module-level import reference
|
||||||
|
# like `from apprise.<etc> import NotifyABCDE`.
|
||||||
|
#
|
||||||
|
# We reload NotifyABCDE and place it back in its spot
|
||||||
|
# new_attach = import_module(entry['path'])
|
||||||
|
for name in possible_matches:
|
||||||
|
if name == 'AppriseAttachment':
|
||||||
|
setattr(
|
||||||
|
apprise_mod, name,
|
||||||
|
getattr(new_apprise_attachment_mod, name))
|
||||||
|
|
||||||
|
else:
|
||||||
|
module_pyname = '{}.{}'.format(
|
||||||
|
A_MGR.module_name_prefix, name)
|
||||||
|
new_attach_mod = import_module(module_pyname)
|
||||||
|
setattr(apprise_mod, name, getattr(new_attach_mod, name))
|
||||||
|
196
test/test_api.py
196
test/test_api.py
@ -32,6 +32,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
from inspect import cleandoc
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
@ -50,9 +51,7 @@ from apprise import PrivacyMode
|
|||||||
from apprise.AppriseLocale import LazyTranslation
|
from apprise.AppriseLocale import LazyTranslation
|
||||||
from apprise.AppriseLocale import gettext_lazy as _
|
from apprise.AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
from apprise import common
|
from apprise.NotificationManager import NotificationManager
|
||||||
from apprise.plugins import __load_matrix
|
|
||||||
from apprise.plugins import __reset_matrix
|
|
||||||
from apprise.utils import parse_list
|
from apprise.utils import parse_list
|
||||||
from helpers import OuterEventLoop
|
from helpers import OuterEventLoop
|
||||||
import inspect
|
import inspect
|
||||||
@ -64,8 +63,11 @@ logging.disable(logging.CRITICAL)
|
|||||||
# Attachment Directory
|
# Attachment Directory
|
||||||
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
def test_apprise():
|
|
||||||
|
def test_apprise_object():
|
||||||
"""
|
"""
|
||||||
API: Apprise() object
|
API: Apprise() object
|
||||||
|
|
||||||
@ -90,11 +92,6 @@ def test_apprise_async():
|
|||||||
|
|
||||||
|
|
||||||
def apprise_test(do_notify):
|
def apprise_test(do_notify):
|
||||||
# Caling load matix a second time which is an internal function causes it
|
|
||||||
# to skip over content already loaded into our matrix and thefore accesses
|
|
||||||
# other if/else parts of the code that aren't otherwise called
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
a = Apprise()
|
a = Apprise()
|
||||||
|
|
||||||
# no items
|
# no items
|
||||||
@ -217,10 +214,10 @@ def apprise_test(do_notify):
|
|||||||
return NotifyBase.parse_url(url, verify_host=False)
|
return NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
|
||||||
# Store our bad notification in our schema map
|
# Store our bad notification in our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['bad'] = BadNotification
|
N_MGR['bad'] = BadNotification
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
# Just to explain what is happening here, we would have parsed the
|
# Just to explain what is happening here, we would have parsed the
|
||||||
# url properly but failed when we went to go and create an instance
|
# url properly but failed when we went to go and create an instance
|
||||||
@ -334,13 +331,13 @@ def apprise_test(do_notify):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Store our bad notification in our schema map
|
# Store our bad notification in our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['throw'] = ThrowNotification
|
N_MGR['throw'] = ThrowNotification
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['fail'] = FailNotification
|
N_MGR['fail'] = FailNotification
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['runtime'] = RuntimeNotification
|
N_MGR['runtime'] = RuntimeNotification
|
||||||
|
|
||||||
for async_mode in (True, False):
|
for async_mode in (True, False):
|
||||||
# Create an Asset object
|
# Create an Asset object
|
||||||
@ -368,7 +365,11 @@ def apprise_test(do_notify):
|
|||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['throw'] = ThrowInstantiateNotification
|
N_MGR.unload_modules()
|
||||||
|
N_MGR['throw'] = ThrowInstantiateNotification
|
||||||
|
|
||||||
|
# Store our good notification in our schema map
|
||||||
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
# Reset our object
|
# Reset our object
|
||||||
a.clear()
|
a.clear()
|
||||||
@ -680,11 +681,7 @@ def test_apprise_schemas(tmpdir):
|
|||||||
API: Apprise().schema() tests
|
API: Apprise().schema() tests
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Caling load matix a second time which is an internal function causes it
|
# Clear loaded modules
|
||||||
# to skip over content already loaded into our matrix and thefore accesses
|
|
||||||
# other if/else parts of the code that aren't otherwise called
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
a = Apprise()
|
a = Apprise()
|
||||||
|
|
||||||
# no items
|
# no items
|
||||||
@ -711,16 +708,23 @@ def test_apprise_schemas(tmpdir):
|
|||||||
|
|
||||||
secure_protocol = 'markdowns'
|
secure_protocol = 'markdowns'
|
||||||
|
|
||||||
# Store our notifications into our schema map
|
|
||||||
common.NOTIFY_SCHEMA_MAP['text'] = TextNotification
|
|
||||||
common.NOTIFY_SCHEMA_MAP['html'] = HtmlNotification
|
|
||||||
common.NOTIFY_SCHEMA_MAP['markdown'] = MarkDownNotification
|
|
||||||
|
|
||||||
schemas = URLBase.schemas(TextNotification)
|
schemas = URLBase.schemas(TextNotification)
|
||||||
assert isinstance(schemas, set) is True
|
assert isinstance(schemas, set) is True
|
||||||
# We didn't define a protocol or secure protocol
|
# We didn't define a protocol or secure protocol
|
||||||
assert len(schemas) == 0
|
assert len(schemas) == 0
|
||||||
|
|
||||||
|
# Store our notifications into our schema map
|
||||||
|
N_MGR['text'] = TextNotification
|
||||||
|
N_MGR['html'] = HtmlNotification
|
||||||
|
N_MGR['markdown'] = MarkDownNotification
|
||||||
|
|
||||||
|
schemas = URLBase.schemas(TextNotification)
|
||||||
|
assert isinstance(schemas, set) is True
|
||||||
|
# We didn't define a protocol or secure protocol one
|
||||||
|
# but one got assigned in he above N_MGR call
|
||||||
|
assert len(schemas) == 1
|
||||||
|
assert 'text' in schemas
|
||||||
|
|
||||||
schemas = URLBase.schemas(HtmlNotification)
|
schemas = URLBase.schemas(HtmlNotification)
|
||||||
assert isinstance(schemas, set) is True
|
assert isinstance(schemas, set) is True
|
||||||
assert len(schemas) == 4
|
assert len(schemas) == 4
|
||||||
@ -784,11 +788,6 @@ def test_apprise_notify_formats(tmpdir):
|
|||||||
API: Apprise() Input Formats tests
|
API: Apprise() Input Formats tests
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Caling load matix a second time which is an internal function causes it
|
|
||||||
# to skip over content already loaded into our matrix and thefore accesses
|
|
||||||
# other if/else parts of the code that aren't otherwise called
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
# Need to set async_mode=False to call notify() instead of async_notify().
|
# Need to set async_mode=False to call notify() instead of async_notify().
|
||||||
asset = AppriseAsset(async_mode=False)
|
asset = AppriseAsset(async_mode=False)
|
||||||
|
|
||||||
@ -845,9 +844,9 @@ def test_apprise_notify_formats(tmpdir):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Store our notifications into our schema map
|
# Store our notifications into our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['text'] = TextNotification
|
N_MGR['text'] = TextNotification
|
||||||
common.NOTIFY_SCHEMA_MAP['html'] = HtmlNotification
|
N_MGR['html'] = HtmlNotification
|
||||||
common.NOTIFY_SCHEMA_MAP['markdown'] = MarkDownNotification
|
N_MGR['markdown'] = MarkDownNotification
|
||||||
|
|
||||||
# Test Markdown; the above calls the markdown because our good://
|
# Test Markdown; the above calls the markdown because our good://
|
||||||
# defined plugin above was defined to default to HTML which triggers
|
# defined plugin above was defined to default to HTML which triggers
|
||||||
@ -1021,8 +1020,9 @@ def test_apprise_disabled_plugins():
|
|||||||
API: Apprise() Disabled Plugin States
|
API: Apprise() Disabled Plugin States
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Reset our matrix
|
# Ensure there are no other drives loaded
|
||||||
__reset_matrix()
|
N_MGR.unload_modules(disable_native=True)
|
||||||
|
assert len(N_MGR) == 0
|
||||||
|
|
||||||
class TestDisabled01Notification(NotifyBase):
|
class TestDisabled01Notification(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -1044,7 +1044,7 @@ def test_apprise_disabled_plugins():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['na01'] = TestDisabled01Notification
|
N_MGR['na01'] = TestDisabled01Notification
|
||||||
|
|
||||||
class TestDisabled02Notification(NotifyBase):
|
class TestDisabled02Notification(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -1069,7 +1069,7 @@ def test_apprise_disabled_plugins():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['na02'] = TestDisabled02Notification
|
N_MGR['na02'] = TestDisabled02Notification
|
||||||
|
|
||||||
# Create our Apprise instance
|
# Create our Apprise instance
|
||||||
a = Apprise()
|
a = Apprise()
|
||||||
@ -1127,7 +1127,7 @@ def test_apprise_disabled_plugins():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['good'] = TesEnabled01Notification
|
N_MGR['good'] = TesEnabled01Notification
|
||||||
|
|
||||||
# The last thing we'll simulate is a case where the plugin is just
|
# The last thing we'll simulate is a case where the plugin is just
|
||||||
# disabled at a later time long into it's life. this is just to allow
|
# disabled at a later time long into it's life. this is just to allow
|
||||||
@ -1148,9 +1148,8 @@ def test_apprise_disabled_plugins():
|
|||||||
# our notifications will go okay now
|
# our notifications will go okay now
|
||||||
assert plugin.notify("My Message") is True
|
assert plugin.notify("My Message") is True
|
||||||
|
|
||||||
# Reset our matrix
|
# Restore our modules
|
||||||
__reset_matrix()
|
N_MGR.unload_modules()
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_details():
|
def test_apprise_details():
|
||||||
@ -1158,9 +1157,6 @@ def test_apprise_details():
|
|||||||
API: Apprise() Details
|
API: Apprise() Details
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Reset our matrix
|
|
||||||
__reset_matrix()
|
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestDetailNotification(NotifyBase):
|
class TestDetailNotification(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -1190,22 +1186,23 @@ def test_apprise_details():
|
|||||||
# previously defined in the base package and builds onto them
|
# previously defined in the base package and builds onto them
|
||||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
'notype': {
|
'notype': {
|
||||||
# Nothing defined is still valid
|
# name is a minimum requirement
|
||||||
|
'name': _('no type'),
|
||||||
},
|
},
|
||||||
'regex_test01': {
|
'regex_test01': {
|
||||||
'name': _('RegexTest'),
|
'name': _('RegexTest'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'regex': r'[A-Z0-9]',
|
'regex': r'^[A-Z0-9]$',
|
||||||
},
|
},
|
||||||
'regex_test02': {
|
'regex_test02': {
|
||||||
'name': _('RegexTest'),
|
'name': _('RegexTest'),
|
||||||
# Support regex options too
|
# Support regex options too
|
||||||
'regex': (r'[A-Z0-9]', 'i'),
|
'regex': (r'^[A-Z0-9]$', 'i'),
|
||||||
},
|
},
|
||||||
'regex_test03': {
|
'regex_test03': {
|
||||||
'name': _('RegexTest'),
|
'name': _('RegexTest'),
|
||||||
# Support regex option without a second option
|
# Support regex option without a second option
|
||||||
'regex': (r'[A-Z0-9]'),
|
'regex': (r'^[A-Z0-9]$'),
|
||||||
},
|
},
|
||||||
'regex_test04': {
|
'regex_test04': {
|
||||||
# this entry would just end up getting removed
|
# this entry would just end up getting removed
|
||||||
@ -1267,7 +1264,7 @@ def test_apprise_details():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Store our good detail notification in our schema map
|
# Store our good detail notification in our schema map
|
||||||
common.NOTIFY_SCHEMA_MAP['details'] = TestDetailNotification
|
N_MGR['details'] = TestDetailNotification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq01Notification(NotifyBase):
|
class TestReq01Notification(NotifyBase):
|
||||||
@ -1292,7 +1289,7 @@ def test_apprise_details():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['req01'] = TestReq01Notification
|
N_MGR['req01'] = TestReq01Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq02Notification(NotifyBase):
|
class TestReq02Notification(NotifyBase):
|
||||||
@ -1322,7 +1319,7 @@ def test_apprise_details():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['req02'] = TestReq02Notification
|
N_MGR['req02'] = TestReq02Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq03Notification(NotifyBase):
|
class TestReq03Notification(NotifyBase):
|
||||||
@ -1348,7 +1345,7 @@ def test_apprise_details():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['req03'] = TestReq03Notification
|
N_MGR['req03'] = TestReq03Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq04Notification(NotifyBase):
|
class TestReq04Notification(NotifyBase):
|
||||||
@ -1368,7 +1365,7 @@ def test_apprise_details():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['req04'] = TestReq04Notification
|
N_MGR['req04'] = TestReq04Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq05Notification(NotifyBase):
|
class TestReq05Notification(NotifyBase):
|
||||||
@ -1390,7 +1387,7 @@ def test_apprise_details():
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
common.NOTIFY_SCHEMA_MAP['req05'] = TestReq05Notification
|
N_MGR['req05'] = TestReq05Notification
|
||||||
|
|
||||||
# Create our Apprise instance
|
# Create our Apprise instance
|
||||||
a = Apprise()
|
a = Apprise()
|
||||||
@ -1451,21 +1448,13 @@ def test_apprise_details():
|
|||||||
assert isinstance(entry['requirements']['packages_required'], list)
|
assert isinstance(entry['requirements']['packages_required'], list)
|
||||||
assert isinstance(entry['requirements']['packages_recommended'], list)
|
assert isinstance(entry['requirements']['packages_recommended'], list)
|
||||||
|
|
||||||
# Reset our matrix
|
|
||||||
__reset_matrix()
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_details_plugin_verification():
|
def test_apprise_details_plugin_verification():
|
||||||
"""
|
"""
|
||||||
API: Apprise() Details Plugin Verification
|
API: Apprise() Details Plugin Verification
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Prepare our object
|
||||||
# Reset our matrix
|
|
||||||
__reset_matrix()
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
a = Apprise()
|
a = Apprise()
|
||||||
|
|
||||||
# Details object
|
# Details object
|
||||||
@ -1757,7 +1746,7 @@ def test_apprise_details_plugin_verification():
|
|||||||
)
|
)
|
||||||
|
|
||||||
spec = inspect.getfullargspec(
|
spec = inspect.getfullargspec(
|
||||||
common.NOTIFY_SCHEMA_MAP[protocols[0]].__init__)
|
N_MGR._schema_map[protocols[0]].__init__)
|
||||||
|
|
||||||
function_args = \
|
function_args = \
|
||||||
(set(parse_list(spec.varkw)) - set(['kwargs'])) \
|
(set(parse_list(spec.varkw)) - set(['kwargs'])) \
|
||||||
@ -1773,7 +1762,8 @@ def test_apprise_details_plugin_verification():
|
|||||||
'{}.__init__() expects a {}=None entry according to '
|
'{}.__init__() expects a {}=None entry according to '
|
||||||
'template configuration'
|
'template configuration'
|
||||||
.format(
|
.format(
|
||||||
common.NOTIFY_SCHEMA_MAP[protocols[0]].__name__, arg))
|
N_MGR._schema_map
|
||||||
|
[protocols[0]].__name__, arg))
|
||||||
|
|
||||||
# Iterate over all of the function arguments and make sure that
|
# Iterate over all of the function arguments and make sure that
|
||||||
# it maps back to a key
|
# it maps back to a key
|
||||||
@ -1783,8 +1773,7 @@ def test_apprise_details_plugin_verification():
|
|||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
'{}.__init__({}) found but not defined in the '
|
'{}.__init__({}) found but not defined in the '
|
||||||
'template configuration'
|
'template configuration'
|
||||||
.format(
|
.format(N_MGR._schema_map[protocols[0]].__name__, arg))
|
||||||
common.NOTIFY_SCHEMA_MAP[protocols[0]].__name__, arg))
|
|
||||||
|
|
||||||
# Iterate over our map_to_aliases and make sure they were defined in
|
# Iterate over our map_to_aliases and make sure they were defined in
|
||||||
# either the as a token or arg
|
# either the as a token or arg
|
||||||
@ -1935,56 +1924,61 @@ def test_notify_matrix_dynamic_importing(tmpdir):
|
|||||||
base.join("__init__.py").write('')
|
base.join("__init__.py").write('')
|
||||||
|
|
||||||
# Test no app_id
|
# Test no app_id
|
||||||
base.join('NotifyBadFile1.py').write(
|
base.join('NotifyBadFile1.py').write(cleandoc(
|
||||||
"""
|
"""
|
||||||
class NotifyBadFile1:
|
class NotifyBadFile1:
|
||||||
pass""")
|
pass
|
||||||
|
"""))
|
||||||
|
|
||||||
# No class of the same name
|
# No class of the same name
|
||||||
base.join('NotifyBadFile2.py').write(
|
base.join('NotifyBadFile2.py').write(cleandoc(
|
||||||
"""
|
"""
|
||||||
class BadClassName:
|
class BadClassName:
|
||||||
pass""")
|
pass
|
||||||
|
"""))
|
||||||
|
|
||||||
# Exception thrown
|
# Exception thrown
|
||||||
base.join('NotifyBadFile3.py').write("""raise ImportError()""")
|
base.join('NotifyBadFile3.py').write("""raise ImportError()""")
|
||||||
|
|
||||||
# Utilizes a schema:// already occupied (as string)
|
# Utilizes a schema:// already occupied (as string)
|
||||||
base.join('NotifyGoober.py').write(
|
base.join('NotifyGoober.py').write(cleandoc(
|
||||||
"""
|
"""
|
||||||
from apprise import NotifyBase
|
from apprise import NotifyBase
|
||||||
class NotifyGoober(NotifyBase):
|
class NotifyGoober(NotifyBase):
|
||||||
# This class tests the fact we have a new class name, but we're
|
# This class tests the fact we have a new class name, but we're
|
||||||
# trying to over-ride items previously used
|
# trying to over-ride items previously used
|
||||||
|
|
||||||
# The default simple (insecure) protocol (used by NotifyMail)
|
# The default simple (insecure) protocol (used by NotifyMail)
|
||||||
protocol = ('mailto', 'goober')
|
protocol = ('mailto', 'goober')
|
||||||
|
|
||||||
# The default secure protocol (used by NotifyMail)
|
# The default secure protocol (used by NotifyMail)
|
||||||
secure_protocol = 'mailtos'
|
secure_protocol = 'mailtos'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url, *args, **kwargs):
|
def parse_url(url, *args, **kwargs):
|
||||||
# always parseable
|
# always parseable
|
||||||
return ConfigBase.parse_url(url, verify_host=False)""")
|
return ConfigBase.parse_url(url, verify_host=False)
|
||||||
|
"""))
|
||||||
|
|
||||||
# Utilizes a schema:// already occupied (as tuple)
|
# Utilizes a schema:// already occupied (as tuple)
|
||||||
base.join('NotifyBugger.py').write("""
|
base.join('NotifyBugger.py').write(cleandoc(
|
||||||
from apprise import NotifyBase
|
"""
|
||||||
class NotifyBugger(NotifyBase):
|
from apprise import NotifyBase
|
||||||
# This class tests the fact we have a new class name, but we're
|
class NotifyBugger(NotifyBase):
|
||||||
# trying to over-ride items previously used
|
# This class tests the fact we have a new class name, but we're
|
||||||
|
# trying to over-ride items previously used
|
||||||
|
|
||||||
# The default simple (insecure) protocol (used by NotifyMail), the other
|
# The default simple (insecure) protocol (used by NotifyMail), the
|
||||||
# isn't
|
# other isn't
|
||||||
protocol = ('mailto', 'bugger-test' )
|
protocol = ('mailto', 'bugger-test' )
|
||||||
|
|
||||||
# The default secure protocol (used by NotifyMail), the other isn't
|
# The default secure protocol (used by NotifyMail), the other isn't
|
||||||
secure_protocol = ('mailtos', ['garbage'])
|
secure_protocol = ('mailtos', ['garbage'])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url, *args, **kwargs):
|
def parse_url(url, *args, **kwargs):
|
||||||
# always parseable
|
# always parseable
|
||||||
return ConfigBase.parse_url(url, verify_host=False)""")
|
return ConfigBase.parse_url(url, verify_host=False)
|
||||||
|
"""))
|
||||||
|
|
||||||
__load_matrix(path=str(base), name=module_name)
|
N_MGR.load_modules(path=str(base), name=module_name)
|
||||||
|
@ -26,16 +26,15 @@
|
|||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import sys
|
|
||||||
import pytest
|
import pytest
|
||||||
from os.path import getsize
|
from os.path import getsize
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
|
from inspect import cleandoc
|
||||||
|
from apprise.AttachmentManager import AttachmentManager
|
||||||
from apprise.AppriseAttachment import AppriseAttachment
|
from apprise.AppriseAttachment import AppriseAttachment
|
||||||
from apprise.AppriseAsset import AppriseAsset
|
from apprise.AppriseAsset import AppriseAsset
|
||||||
from apprise.attachment.AttachBase import AttachBase
|
from apprise.attachment.AttachBase import AttachBase
|
||||||
from apprise.common import ATTACHMENT_SCHEMA_MAP
|
|
||||||
from apprise.attachment import __load_matrix
|
|
||||||
from apprise.common import ContentLocation
|
from apprise.common import ContentLocation
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
@ -44,6 +43,9 @@ logging.disable(logging.CRITICAL)
|
|||||||
|
|
||||||
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
||||||
|
|
||||||
|
# Grant access to our Attachment Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_attachment():
|
def test_apprise_attachment():
|
||||||
"""
|
"""
|
||||||
@ -278,7 +280,7 @@ def test_apprise_attachment_instantiate():
|
|||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
# Store our bad attachment type in our schema map
|
# Store our bad attachment type in our schema map
|
||||||
ATTACHMENT_SCHEMA_MAP['bad'] = BadAttachType
|
A_MGR['bad'] = BadAttachType
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
AppriseAttachment.instantiate(
|
AppriseAttachment.instantiate(
|
||||||
@ -289,77 +291,6 @@ def test_apprise_attachment_instantiate():
|
|||||||
'bad://path', suppress_exceptions=True) is None
|
'bad://path', suppress_exceptions=True) is None
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_attachment_matrix_load():
|
|
||||||
"""
|
|
||||||
API: AppriseAttachment() matrix initialization
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import apprise
|
|
||||||
|
|
||||||
class AttachmentDummy(AttachBase):
|
|
||||||
"""
|
|
||||||
A dummy wrapper for testing the different options in the load_matrix
|
|
||||||
function
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
|
||||||
service_name = 'dummy'
|
|
||||||
|
|
||||||
# protocol as tuple
|
|
||||||
protocol = ('uh', 'oh')
|
|
||||||
|
|
||||||
# secure protocol as tuple
|
|
||||||
secure_protocol = ('no', 'yes')
|
|
||||||
|
|
||||||
class AttachmentDummy2(AttachBase):
|
|
||||||
"""
|
|
||||||
A dummy wrapper for testing the different options in the load_matrix
|
|
||||||
function
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
|
||||||
service_name = 'dummy2'
|
|
||||||
|
|
||||||
# secure protocol as tuple
|
|
||||||
secure_protocol = ('true', 'false')
|
|
||||||
|
|
||||||
class AttachmentDummy3(AttachBase):
|
|
||||||
"""
|
|
||||||
A dummy wrapper for testing the different options in the load_matrix
|
|
||||||
function
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
|
||||||
service_name = 'dummy3'
|
|
||||||
|
|
||||||
# secure protocol as string
|
|
||||||
secure_protocol = 'true'
|
|
||||||
|
|
||||||
class AttachmentDummy4(AttachBase):
|
|
||||||
"""
|
|
||||||
A dummy wrapper for testing the different options in the load_matrix
|
|
||||||
function
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
|
||||||
service_name = 'dummy4'
|
|
||||||
|
|
||||||
# protocol as string
|
|
||||||
protocol = 'true'
|
|
||||||
|
|
||||||
# Generate ourselves a fake entry
|
|
||||||
apprise.attachment.AttachmentDummy = AttachmentDummy
|
|
||||||
apprise.attachment.AttachmentDummy2 = AttachmentDummy2
|
|
||||||
apprise.attachment.AttachmentDummy3 = AttachmentDummy3
|
|
||||||
apprise.attachment.AttachmentDummy4 = AttachmentDummy4
|
|
||||||
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
# Call it again so we detect our entries already loaded
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
def test_attachment_matrix_dynamic_importing(tmpdir):
|
def test_attachment_matrix_dynamic_importing(tmpdir):
|
||||||
"""
|
"""
|
||||||
API: Apprise() Attachment Matrix Importing
|
API: Apprise() Attachment Matrix Importing
|
||||||
@ -372,53 +303,55 @@ def test_attachment_matrix_dynamic_importing(tmpdir):
|
|||||||
|
|
||||||
module_name = 'badattach'
|
module_name = 'badattach'
|
||||||
|
|
||||||
# Update our path to point to our new test suite
|
|
||||||
sys.path.insert(0, str(suite))
|
|
||||||
|
|
||||||
# Create a base area to work within
|
# Create a base area to work within
|
||||||
base = suite.mkdir(module_name)
|
base = suite.mkdir(module_name)
|
||||||
base.join("__init__.py").write('')
|
base.join("__init__.py").write('')
|
||||||
|
|
||||||
# Test no app_id
|
# Test no app_id
|
||||||
base.join('AttachBadFile1.py').write(
|
base.join('AttachBadFile1.py').write(cleandoc(
|
||||||
"""
|
"""
|
||||||
class AttachBadFile1:
|
class AttachBadFile1:
|
||||||
pass""")
|
pass
|
||||||
|
"""))
|
||||||
|
|
||||||
# No class of the same name
|
# No class of the same name
|
||||||
base.join('AttachBadFile2.py').write(
|
base.join('AttachBadFile2.py').write(cleandoc(
|
||||||
"""
|
"""
|
||||||
class BadClassName:
|
class BadClassName:
|
||||||
pass""")
|
pass
|
||||||
|
"""))
|
||||||
|
|
||||||
# Exception thrown
|
# Exception thrown
|
||||||
base.join('AttachBadFile3.py').write("""raise ImportError()""")
|
base.join('AttachBadFile3.py').write("""raise ImportError()""")
|
||||||
|
|
||||||
# Utilizes a schema:// already occupied (as string)
|
# Utilizes a schema:// already occupied (as string)
|
||||||
base.join('AttachGoober.py').write(
|
base.join('AttachGoober.py').write(cleandoc(
|
||||||
"""
|
"""
|
||||||
from apprise import AttachBase
|
from apprise import AttachBase
|
||||||
class AttachGoober(AttachBase):
|
class AttachGoober(AttachBase):
|
||||||
# This class tests the fact we have a new class name, but we're
|
# This class tests the fact we have a new class name, but we're
|
||||||
# trying to over-ride items previously used
|
# trying to over-ride items previously used
|
||||||
|
|
||||||
# The default simple (insecure) protocol
|
# The default simple (insecure) protocol
|
||||||
protocol = 'http'
|
protocol = 'http'
|
||||||
|
|
||||||
# The default secure protocol
|
# The default secure protocol
|
||||||
secure_protocol = 'https'""")
|
secure_protocol = 'https'
|
||||||
|
"""))
|
||||||
|
|
||||||
# Utilizes a schema:// already occupied (as tuple)
|
# Utilizes a schema:// already occupied (as tuple)
|
||||||
base.join('AttachBugger.py').write("""
|
base.join('AttachBugger.py').write(cleandoc(
|
||||||
from apprise import AttachBase
|
"""
|
||||||
class AttachBugger(AttachBase):
|
from apprise import AttachBase
|
||||||
# This class tests the fact we have a new class name, but we're
|
class AttachBugger(AttachBase):
|
||||||
# trying to over-ride items previously used
|
# This class tests the fact we have a new class name, but we're
|
||||||
|
# trying to over-ride items previously used
|
||||||
|
|
||||||
# The default simple (insecure) protocol
|
# The default simple (insecure) protocol
|
||||||
protocol = ('http', 'bugger-test' )
|
protocol = ('http', 'bugger-test' )
|
||||||
|
|
||||||
# The default secure protocol
|
# The default secure protocol
|
||||||
secure_protocol = ('https', 'bugger-tests')""")
|
secure_protocol = ('https', 'bugger-tests')
|
||||||
|
"""))
|
||||||
|
|
||||||
__load_matrix(path=str(base), name=module_name)
|
A_MGR.load_modules(path=str(base), name=module_name)
|
||||||
|
@ -36,23 +36,21 @@ from os.path import dirname
|
|||||||
from os.path import join
|
from os.path import join
|
||||||
from apprise import cli
|
from apprise import cli
|
||||||
from apprise import NotifyBase
|
from apprise import NotifyBase
|
||||||
from apprise.common import NOTIFY_CUSTOM_MODULE_MAP
|
from apprise.NotificationManager import NotificationManager
|
||||||
from apprise.utils import PATHS_PREVIOUSLY_SCANNED
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from apprise.common import NOTIFY_SCHEMA_MAP
|
|
||||||
from apprise.utils import environ
|
from apprise.utils import environ
|
||||||
from apprise.plugins import __load_matrix
|
|
||||||
from apprise.plugins import __reset_matrix
|
|
||||||
|
|
||||||
from apprise.AppriseLocale import gettext_lazy as _
|
from apprise.AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_cli_nux_env(tmpdir):
|
def test_apprise_cli_nux_env(tmpdir):
|
||||||
"""
|
"""
|
||||||
@ -89,8 +87,8 @@ def test_apprise_cli_nux_env(tmpdir):
|
|||||||
return 'bad://'
|
return 'bad://'
|
||||||
|
|
||||||
# Set up our notification types
|
# Set up our notification types
|
||||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR['good'] = GoodNotification
|
||||||
NOTIFY_SCHEMA_MAP['bad'] = BadNotification
|
N_MGR['bad'] = BadNotification
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli.main)
|
result = runner.invoke(cli.main)
|
||||||
@ -639,8 +637,8 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
])
|
])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
# Reset our matrix
|
# Clear loaded modules
|
||||||
__reset_matrix()
|
N_MGR.unload_modules()
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq01Notification(NotifyBase):
|
class TestReq01Notification(NotifyBase):
|
||||||
@ -665,7 +663,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['req01'] = TestReq01Notification
|
N_MGR['req01'] = TestReq01Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq02Notification(NotifyBase):
|
class TestReq02Notification(NotifyBase):
|
||||||
@ -695,7 +693,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['req02'] = TestReq02Notification
|
N_MGR['req02'] = TestReq02Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq03Notification(NotifyBase):
|
class TestReq03Notification(NotifyBase):
|
||||||
@ -721,7 +719,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['req03'] = TestReq03Notification
|
N_MGR['req03'] = TestReq03Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq04Notification(NotifyBase):
|
class TestReq04Notification(NotifyBase):
|
||||||
@ -741,7 +739,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['req04'] = TestReq04Notification
|
N_MGR['req04'] = TestReq04Notification
|
||||||
|
|
||||||
# This is a made up class that is just used to verify
|
# This is a made up class that is just used to verify
|
||||||
class TestReq05Notification(NotifyBase):
|
class TestReq05Notification(NotifyBase):
|
||||||
@ -762,7 +760,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['req05'] = TestReq05Notification
|
N_MGR['req05'] = TestReq05Notification
|
||||||
|
|
||||||
class TestDisabled01Notification(NotifyBase):
|
class TestDisabled01Notification(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -784,7 +782,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['na01'] = TestDisabled01Notification
|
N_MGR['na01'] = TestDisabled01Notification
|
||||||
|
|
||||||
class TestDisabled02Notification(NotifyBase):
|
class TestDisabled02Notification(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -809,7 +807,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['na02'] = TestDisabled02Notification
|
N_MGR['na02'] = TestDisabled02Notification
|
||||||
|
|
||||||
# We'll add a good notification to our list
|
# We'll add a good notification to our list
|
||||||
class TesEnabled01Notification(NotifyBase):
|
class TesEnabled01Notification(NotifyBase):
|
||||||
@ -829,7 +827,7 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
# Pretend everything is okay (so we don't break other tests)
|
# Pretend everything is okay (so we don't break other tests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NOTIFY_SCHEMA_MAP['good'] = TesEnabled01Notification
|
N_MGR['good'] = TesEnabled01Notification
|
||||||
|
|
||||||
# Verify that we can pass through all of our different details
|
# Verify that we can pass through all of our different details
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
@ -842,9 +840,8 @@ def test_apprise_cli_details(tmpdir):
|
|||||||
])
|
])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
# Reset our matrix
|
# Clear loaded modules
|
||||||
__reset_matrix()
|
N_MGR.unload_modules()
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('requests.post')
|
@mock.patch('requests.post')
|
||||||
@ -863,8 +860,8 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
# This simulates an actual call from the CLI. Unfortunately through
|
# This simulates an actual call from the CLI. Unfortunately through
|
||||||
# testing were occupying the same memory space so our singleton's
|
# testing were occupying the same memory space so our singleton's
|
||||||
# have already been populated
|
# have already been populated
|
||||||
PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Test a path that has no files to load in it
|
# Test a path that has no files to load in it
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
@ -877,8 +874,8 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
# Directories that don't exist passed in by the CLI aren't even scanned
|
# Directories that don't exist passed in by the CLI aren't even scanned
|
||||||
assert len(PATHS_PREVIOUSLY_SCANNED) == 0
|
assert len(N_MGR._paths_previously_scanned) == 0
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Test our current existing path that has no entries in it
|
# Test our current existing path that has no entries in it
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
@ -889,18 +886,19 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
# The path is silently loaded but fails... it's okay because the
|
# The path is silently loaded but fails... it's okay because the
|
||||||
# notification we're choosing to notify does exist
|
# notification we're choosing to notify does exist
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert len(PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert join(str(tmpdir), 'empty') in PATHS_PREVIOUSLY_SCANNED
|
assert join(str(tmpdir), 'empty') in \
|
||||||
|
N_MGR._paths_previously_scanned
|
||||||
|
|
||||||
# However there was nothing to load
|
# However there was nothing to load
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Clear our working variables so they don't obstruct the next test
|
# Clear our working variables so they don't obstruct the next test
|
||||||
# This simulates an actual call from the CLI. Unfortunately through
|
# This simulates an actual call from the CLI. Unfortunately through
|
||||||
# testing were occupying the same memory space so our singleton's
|
# testing were occupying the same memory space so our singleton's
|
||||||
# have already been populated
|
# have already been populated
|
||||||
PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Prepare ourselves a file to work with
|
# Prepare ourselves a file to work with
|
||||||
notify_hook_a_base = tmpdir.mkdir('random')
|
notify_hook_a_base = tmpdir.mkdir('random')
|
||||||
@ -921,10 +919,10 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
|
|
||||||
# The path is silently loaded but fails... it's okay because the
|
# The path is silently loaded but fails... it's okay because the
|
||||||
# notification we're choosing to notify does exist
|
# notification we're choosing to notify does exist
|
||||||
assert len(PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert str(notify_hook_a) in PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_a) in N_MGR._paths_previously_scanned
|
||||||
# However there was nothing to load
|
# However there was nothing to load
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Prepare ourselves a file to work with
|
# Prepare ourselves a file to work with
|
||||||
notify_hook_aa = notify_hook_a_base.join('myhook02.py')
|
notify_hook_aa = notify_hook_a_base.join('myhook02.py')
|
||||||
@ -932,11 +930,12 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
garbage entry
|
garbage entry
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
N_MGR.plugins()
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
'--plugin-path', str(notify_hook_aa),
|
'--plugin-path', str(notify_hook_aa),
|
||||||
'-b', 'test\nbody',
|
'-b', 'test\nbody',
|
||||||
# A custom hook:
|
# A custom hook:
|
||||||
'clihook://',
|
'clihook://custom',
|
||||||
])
|
])
|
||||||
# It doesn't exist so it will fail
|
# It doesn't exist so it will fail
|
||||||
# meanwhile we would have failed to load the myhook path
|
# meanwhile we would have failed to load the myhook path
|
||||||
@ -944,18 +943,20 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
|
|
||||||
# The path is silently loaded but fails...
|
# The path is silently loaded but fails...
|
||||||
# as a result the path stacks with the last
|
# as a result the path stacks with the last
|
||||||
assert len(PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert str(notify_hook_a) in PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_a) in \
|
||||||
assert str(notify_hook_aa) in PATHS_PREVIOUSLY_SCANNED
|
N_MGR._paths_previously_scanned
|
||||||
|
assert str(notify_hook_aa) in \
|
||||||
|
N_MGR._paths_previously_scanned
|
||||||
# However there was nothing to load
|
# However there was nothing to load
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Clear our working variables so they don't obstruct the next test
|
# Clear our working variables so they don't obstruct the next test
|
||||||
# This simulates an actual call from the CLI. Unfortunately through
|
# This simulates an actual call from the CLI. Unfortunately through
|
||||||
# testing were occupying the same memory space so our singleton's
|
# testing were occupying the same memory space so our singleton's
|
||||||
# have already been populated
|
# have already been populated
|
||||||
PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Prepare ourselves a file to work with
|
# Prepare ourselves a file to work with
|
||||||
notify_hook_b = tmpdir.mkdir('goodmodule').join('__init__.py')
|
notify_hook_b = tmpdir.mkdir('goodmodule').join('__init__.py')
|
||||||
@ -968,6 +969,14 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
# A simple test - print to screen
|
# A simple test - print to screen
|
||||||
print("{}: {} - {}".format(notify_type, title, body))
|
print("{}: {} - {}".format(notify_type, title, body))
|
||||||
|
|
||||||
|
# No return (so a return of None) get's translated to True
|
||||||
|
|
||||||
|
# Define another in the same file
|
||||||
|
@notify(on="clihookA")
|
||||||
|
def mywrapper(body, title, notify_type, *args, **kwargs):
|
||||||
|
# A simple test - print to screen
|
||||||
|
print("!! {}: {} - {}".format(notify_type, title, body))
|
||||||
|
|
||||||
# No return (so a return of None) get's translated to True
|
# No return (so a return of None) get's translated to True
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
@ -975,7 +984,7 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
'--plugin-path', str(tmpdir),
|
'--plugin-path', str(tmpdir),
|
||||||
'-b', 'test body',
|
'-b', 'test body',
|
||||||
# A custom hook:
|
# A custom hook:
|
||||||
'clihook://',
|
'clihook://still/valid',
|
||||||
])
|
])
|
||||||
|
|
||||||
# We can detect the goodmodule (which has an __init__.py in it)
|
# We can detect the goodmodule (which has an __init__.py in it)
|
||||||
@ -983,51 +992,52 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
# Let's see how things got loaded:
|
# Let's see how things got loaded:
|
||||||
assert len(PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert str(tmpdir) in PATHS_PREVIOUSLY_SCANNED
|
assert str(tmpdir) in N_MGR._paths_previously_scanned
|
||||||
# absolute path to detected module is also added
|
# absolute path to detected module is also added
|
||||||
assert join(str(tmpdir), 'goodmodule', '__init__.py') \
|
assert join(str(tmpdir), 'goodmodule', '__init__.py') \
|
||||||
in PATHS_PREVIOUSLY_SCANNED
|
in N_MGR._paths_previously_scanned
|
||||||
|
|
||||||
# We also loaded our clihook properly
|
# We also loaded our clihook properly
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# We can find our new hook loaded in our NOTIFY_SCHEMA_MAP now...
|
# We can find our new hook loaded in our schema map now...
|
||||||
assert 'clihook' in NOTIFY_SCHEMA_MAP
|
assert 'clihook' in N_MGR
|
||||||
|
|
||||||
# Capture our key for reference
|
# Capture our key for reference
|
||||||
key = [k for k in NOTIFY_CUSTOM_MODULE_MAP.keys()][0]
|
key = [k for k in N_MGR._custom_module_map.keys()][0]
|
||||||
|
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP[key]['notify']) == 1
|
# We loaded 2 entries from the same file
|
||||||
assert 'clihook' in NOTIFY_CUSTOM_MODULE_MAP[key]['notify']
|
assert len(N_MGR._custom_module_map[key]['notify']) == 2
|
||||||
|
assert 'clihook' in N_MGR._custom_module_map[key]['notify']
|
||||||
|
# Converted to lower case
|
||||||
|
assert 'clihooka' in N_MGR._custom_module_map[key]['notify']
|
||||||
|
|
||||||
# Our function name
|
# Our function name
|
||||||
assert NOTIFY_CUSTOM_MODULE_MAP[key]['notify']['clihook']['fn_name'] \
|
assert N_MGR._custom_module_map[key]['notify']['clihook']['fn_name'] \
|
||||||
== 'mywrapper'
|
== 'mywrapper'
|
||||||
# What we parsed from the `on` keyword in the @notify decorator
|
# What we parsed from the `on` keyword in the @notify decorator
|
||||||
assert NOTIFY_CUSTOM_MODULE_MAP[key]['notify']['clihook']['url'] \
|
assert N_MGR._custom_module_map[key]['notify']['clihook']['url'] \
|
||||||
== 'clihook://'
|
== 'clihook://'
|
||||||
# our default name Assignment. This can be-overridden on the @notify
|
# our default name Assignment. This can be-overridden on the @notify
|
||||||
# decorator by just adding a name= to the parameter list
|
# decorator by just adding a name= to the parameter list
|
||||||
assert NOTIFY_CUSTOM_MODULE_MAP[key]['notify']['clihook']['name'] \
|
assert N_MGR['clihook'].service_name == 'Custom - clihook'
|
||||||
== 'Custom - clihook'
|
|
||||||
|
|
||||||
# Our Base Notification object when initialized:
|
# Our Base Notification object when initialized:
|
||||||
assert isinstance(
|
assert len(
|
||||||
NOTIFY_CUSTOM_MODULE_MAP[key]['notify']['clihook']['plugin'](),
|
N_MGR._module_map[N_MGR._custom_module_map[key]['name']]['plugin']) \
|
||||||
NotifyBase)
|
== 2
|
||||||
|
for plugin in \
|
||||||
# This is how it ties together in the backend
|
N_MGR._module_map[N_MGR._custom_module_map[key]['name']]['plugin']:
|
||||||
assert NOTIFY_CUSTOM_MODULE_MAP[key]['notify']['clihook']['plugin'] == \
|
assert isinstance(plugin(), NotifyBase)
|
||||||
NOTIFY_SCHEMA_MAP['clihook']
|
|
||||||
|
|
||||||
# Clear our working variables so they don't obstruct the next test
|
# Clear our working variables so they don't obstruct the next test
|
||||||
# This simulates an actual call from the CLI. Unfortunately through
|
# This simulates an actual call from the CLI. Unfortunately through
|
||||||
# testing were occupying the same memory space so our singleton's
|
# testing were occupying the same memory space so our singleton's
|
||||||
# have already been populated
|
# have already been populated
|
||||||
PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
del NOTIFY_SCHEMA_MAP['clihook']
|
del N_MGR['clihook']
|
||||||
|
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
'--plugin-path', str(notify_hook_b),
|
'--plugin-path', str(notify_hook_b),
|
||||||
@ -1046,9 +1056,9 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
# This simulates an actual call from the CLI. Unfortunately through
|
# This simulates an actual call from the CLI. Unfortunately through
|
||||||
# testing were occupying the same memory space so our singleton's
|
# testing were occupying the same memory space so our singleton's
|
||||||
# have already been populated
|
# have already been populated
|
||||||
PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
del NOTIFY_SCHEMA_MAP['clihook']
|
del N_MGR['clihook']
|
||||||
|
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
'--plugin-path', dirname(str(notify_hook_b)),
|
'--plugin-path', dirname(str(notify_hook_b)),
|
||||||
@ -1080,9 +1090,9 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
# This simulates an actual call from the CLI. Unfortunately through
|
# This simulates an actual call from the CLI. Unfortunately through
|
||||||
# testing were occupying the same memory space so our singleton's
|
# testing were occupying the same memory space so our singleton's
|
||||||
# have already been populated
|
# have already been populated
|
||||||
PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
del NOTIFY_SCHEMA_MAP['clihook']
|
del N_MGR['clihook']
|
||||||
|
|
||||||
# Prepare ourselves a file to work with
|
# Prepare ourselves a file to work with
|
||||||
notify_hook_b = tmpdir.mkdir('complex').join('complex.py')
|
notify_hook_b = tmpdir.mkdir('complex').join('complex.py')
|
||||||
@ -1141,32 +1151,32 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
|
|||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
|
|
||||||
# Let's see how things got loaded
|
# Let's see how things got loaded
|
||||||
assert len(PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
# Our path we specified on the CLI...
|
# Our path we specified on the CLI...
|
||||||
assert join(str(tmpdir), 'complex') in PATHS_PREVIOUSLY_SCANNED
|
assert join(str(tmpdir), 'complex') in N_MGR._paths_previously_scanned
|
||||||
|
|
||||||
# absolute path to detected module is also added
|
# absolute path to detected module is also added
|
||||||
assert join(str(tmpdir), 'complex', 'complex.py') \
|
assert join(str(tmpdir), 'complex', 'complex.py') \
|
||||||
in PATHS_PREVIOUSLY_SCANNED
|
in N_MGR._paths_previously_scanned
|
||||||
|
|
||||||
# We loaded our one module successfuly
|
# We loaded our one module successfuly
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# We can find our new hook loaded in our SCHEMA_MAP now...
|
# We can find our new hook loaded in our SCHEMA_MAP now...
|
||||||
assert 'willfail' in NOTIFY_SCHEMA_MAP
|
assert 'willfail' in N_MGR
|
||||||
assert 'clihook1' in NOTIFY_SCHEMA_MAP
|
assert 'clihook1' in N_MGR
|
||||||
assert 'clihook2' in NOTIFY_SCHEMA_MAP
|
assert 'clihook2' in N_MGR
|
||||||
|
|
||||||
# Capture our key for reference
|
# Capture our key for reference
|
||||||
key = [k for k in NOTIFY_CUSTOM_MODULE_MAP.keys()][0]
|
key = [k for k in N_MGR._custom_module_map.keys()][0]
|
||||||
|
|
||||||
assert len(NOTIFY_CUSTOM_MODULE_MAP[key]['notify']) == 3
|
assert len(N_MGR._custom_module_map[key]['notify']) == 3
|
||||||
assert 'willfail' in NOTIFY_CUSTOM_MODULE_MAP[key]['notify']
|
assert 'willfail' in N_MGR._custom_module_map[key]['notify']
|
||||||
assert 'clihook1' in NOTIFY_CUSTOM_MODULE_MAP[key]['notify']
|
assert 'clihook1' in N_MGR._custom_module_map[key]['notify']
|
||||||
# We only load 1 instance of the clihook2, the second will fail
|
# We only load 1 instance of the clihook2, the second will fail
|
||||||
assert 'clihook2' in NOTIFY_CUSTOM_MODULE_MAP[key]['notify']
|
assert 'clihook2' in N_MGR._custom_module_map[key]['notify']
|
||||||
# We can never load previously created notifications
|
# We can never load previously created notifications
|
||||||
assert 'json' not in NOTIFY_CUSTOM_MODULE_MAP[key]['notify']
|
assert 'json' not in N_MGR._custom_module_map[key]['notify']
|
||||||
|
|
||||||
result = runner.invoke(cli.main, [
|
result = runner.invoke(cli.main, [
|
||||||
'--plugin-path', join(str(tmpdir), 'complex'),
|
'--plugin-path', join(str(tmpdir), 'complex'),
|
||||||
|
@ -37,16 +37,21 @@ from apprise import AppriseConfig
|
|||||||
from apprise import AppriseAsset
|
from apprise import AppriseAsset
|
||||||
from apprise.config.ConfigBase import ConfigBase
|
from apprise.config.ConfigBase import ConfigBase
|
||||||
from apprise.plugins.NotifyBase import NotifyBase
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
from apprise.ConfigurationManager import ConfigurationManager
|
||||||
|
|
||||||
from apprise.common import CONFIG_SCHEMA_MAP
|
|
||||||
from apprise.common import NOTIFY_SCHEMA_MAP
|
|
||||||
from apprise.config import __load_matrix
|
|
||||||
from apprise.config.ConfigFile import ConfigFile
|
from apprise.config.ConfigFile import ConfigFile
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
# Grant access to our Configuration Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_config(tmpdir):
|
def test_apprise_config(tmpdir):
|
||||||
"""
|
"""
|
||||||
@ -248,7 +253,7 @@ def test_apprise_multi_config_entries(tmpdir):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR._schema_map['good'] = GoodNotification
|
||||||
|
|
||||||
# Create ourselves a config object
|
# Create ourselves a config object
|
||||||
ac = AppriseConfig()
|
ac = AppriseConfig()
|
||||||
@ -468,7 +473,7 @@ def test_apprise_config_instantiate():
|
|||||||
return ConfigBase.parse_url(url, verify_host=False)
|
return ConfigBase.parse_url(url, verify_host=False)
|
||||||
|
|
||||||
# Store our bad configuration in our schema map
|
# Store our bad configuration in our schema map
|
||||||
CONFIG_SCHEMA_MAP['bad'] = BadConfig
|
C_MGR['bad'] = BadConfig
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
AppriseConfig.instantiate(
|
AppriseConfig.instantiate(
|
||||||
@ -501,7 +506,7 @@ def test_invalid_apprise_config(tmpdir):
|
|||||||
return ConfigBase.parse_url(url, verify_host=False)
|
return ConfigBase.parse_url(url, verify_host=False)
|
||||||
|
|
||||||
# Store our bad configuration in our schema map
|
# Store our bad configuration in our schema map
|
||||||
CONFIG_SCHEMA_MAP['bad'] = BadConfig
|
C_MGR['bad'] = BadConfig
|
||||||
|
|
||||||
# temporary file to work with
|
# temporary file to work with
|
||||||
t = tmpdir.mkdir("apprise-bad-obj").join("invalid")
|
t = tmpdir.mkdir("apprise-bad-obj").join("invalid")
|
||||||
@ -566,7 +571,7 @@ def test_apprise_config_with_apprise_obj(tmpdir):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR._schema_map['good'] = GoodNotification
|
||||||
|
|
||||||
# Create ourselves a config object
|
# Create ourselves a config object
|
||||||
ac = AppriseConfig(cache=False)
|
ac = AppriseConfig(cache=False)
|
||||||
@ -765,9 +770,9 @@ def test_recursive_config_inclusion(tmpdir):
|
|||||||
allow_cross_includes = ContentIncludeMode.NEVER
|
allow_cross_includes = ContentIncludeMode.NEVER
|
||||||
|
|
||||||
# store our entries
|
# store our entries
|
||||||
CONFIG_SCHEMA_MAP['never'] = ConfigCrossPostNever
|
C_MGR['never'] = ConfigCrossPostNever
|
||||||
CONFIG_SCHEMA_MAP['strict'] = ConfigCrossPostStrict
|
C_MGR['strict'] = ConfigCrossPostStrict
|
||||||
CONFIG_SCHEMA_MAP['always'] = ConfigCrossPostAlways
|
C_MGR['always'] = ConfigCrossPostAlways
|
||||||
|
|
||||||
# Make our new path valid
|
# Make our new path valid
|
||||||
suite = tmpdir.mkdir("apprise_config_recursion")
|
suite = tmpdir.mkdir("apprise_config_recursion")
|
||||||
@ -974,11 +979,6 @@ def test_apprise_config_matrix_load():
|
|||||||
apprise.config.ConfigDummy3 = ConfigDummy3
|
apprise.config.ConfigDummy3 = ConfigDummy3
|
||||||
apprise.config.ConfigDummy4 = ConfigDummy4
|
apprise.config.ConfigDummy4 = ConfigDummy4
|
||||||
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
# Call it again so we detect our entries already loaded
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
def test_configmatrix_dynamic_importing(tmpdir):
|
def test_configmatrix_dynamic_importing(tmpdir):
|
||||||
"""
|
"""
|
||||||
@ -1052,8 +1052,6 @@ class ConfigBugger(ConfigBase):
|
|||||||
# always parseable
|
# always parseable
|
||||||
return ConfigBase.parse_url(url, verify_host=False)""")
|
return ConfigBase.parse_url(url, verify_host=False)""")
|
||||||
|
|
||||||
__load_matrix(path=str(base), name=module_name)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('os.path.getsize')
|
@mock.patch('os.path.getsize')
|
||||||
def test_config_base_parse_inaccessible_text_file(mock_getsize, tmpdir):
|
def test_config_base_parse_inaccessible_text_file(mock_getsize, tmpdir):
|
||||||
|
@ -33,7 +33,7 @@ from inspect import cleandoc
|
|||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from apprise import utils
|
from apprise import utils
|
||||||
from apprise import common
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
@ -42,6 +42,9 @@ logging.disable(logging.CRITICAL)
|
|||||||
# Ensure we don't create .pyc files for these tests
|
# Ensure we don't create .pyc files for these tests
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
def test_parse_qsd():
|
def test_parse_qsd():
|
||||||
"utils: parse_qsd() testing """
|
"utils: parse_qsd() testing """
|
||||||
@ -2030,15 +2033,15 @@ def test_module_detection(tmpdir):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Clear our working variables so they don't obstruct with the tests here
|
# Clear our working variables so they don't obstruct with the tests here
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Test case where we load invalid data
|
# Test case where we load invalid data
|
||||||
utils.module_detection(None)
|
N_MGR.module_detection(None)
|
||||||
|
|
||||||
# Invalid data does not load anything
|
# Invalid data does not load anything
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 0
|
assert len(N_MGR._paths_previously_scanned) == 0
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Prepare ourselves a file to work with
|
# Prepare ourselves a file to work with
|
||||||
notify_hook_a_base = tmpdir.mkdir('a')
|
notify_hook_a_base = tmpdir.mkdir('a')
|
||||||
@ -2057,30 +2060,29 @@ def test_module_detection(tmpdir):
|
|||||||
"""))
|
"""))
|
||||||
|
|
||||||
# Not previously loaded
|
# Not previously loaded
|
||||||
assert 'clihook' not in common.NOTIFY_SCHEMA_MAP
|
assert 'clihook' not in N_MGR
|
||||||
|
|
||||||
# load entry by string
|
# load entry by string
|
||||||
utils.module_detection(str(notify_hook_a))
|
N_MGR.module_detection(str(notify_hook_a))
|
||||||
utils.module_detection(str(notify_ignore))
|
N_MGR.module_detection(str(notify_ignore))
|
||||||
utils.module_detection(str(notify_hook_a_base))
|
N_MGR.module_detection(str(notify_hook_a_base))
|
||||||
|
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 3
|
assert len(N_MGR._paths_previously_scanned) == 3
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Now loaded
|
# Now loaded
|
||||||
assert 'clihook' in common.NOTIFY_SCHEMA_MAP
|
assert 'clihook' in N_MGR
|
||||||
|
|
||||||
# load entry by array
|
# load entry by array
|
||||||
utils.module_detection([str(notify_hook_a)])
|
N_MGR.module_detection([str(notify_hook_a)])
|
||||||
|
|
||||||
# No changes to our path
|
# No changes to our path
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 3
|
assert len(N_MGR._paths_previously_scanned) == 3
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Reset our variables for the next test
|
# Reset our variables for the next test
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
del common.NOTIFY_SCHEMA_MAP['clihook']
|
|
||||||
|
|
||||||
# Hidden files are ignored
|
# Hidden files are ignored
|
||||||
notify_hook_b_base = tmpdir.mkdir('b')
|
notify_hook_b_base = tmpdir.mkdir('b')
|
||||||
@ -2094,36 +2096,36 @@ def test_module_detection(tmpdir):
|
|||||||
pass
|
pass
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
assert 'hidden' not in common.NOTIFY_SCHEMA_MAP
|
assert 'hidden' not in N_MGR
|
||||||
|
|
||||||
utils.module_detection([str(notify_hook_b)])
|
N_MGR.module_detection([str(notify_hook_b)])
|
||||||
|
|
||||||
# Verify that it did not load
|
# Verify that it did not load
|
||||||
assert 'hidden' not in common.NOTIFY_SCHEMA_MAP
|
assert 'hidden' not in N_MGR
|
||||||
|
|
||||||
# Path was scanned; nothing loaded
|
# Path was scanned; nothing loaded
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Reset our variables for the next test
|
# Reset our variables for the next test
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# modules with no hooks found are ignored
|
# modules with no hooks found are ignored
|
||||||
notify_hook_c_base = tmpdir.mkdir('c')
|
notify_hook_c_base = tmpdir.mkdir('c')
|
||||||
notify_hook_c = notify_hook_c_base.join('empty.py')
|
notify_hook_c = notify_hook_c_base.join('empty.py')
|
||||||
notify_hook_c.write("")
|
notify_hook_c.write("")
|
||||||
|
|
||||||
utils.module_detection([str(notify_hook_c)])
|
N_MGR.module_detection([str(notify_hook_c)])
|
||||||
|
|
||||||
# File was found, no custom modules
|
# File was found, no custom modules
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# A new path scanned
|
# A new path scanned
|
||||||
utils.module_detection([str(notify_hook_c_base)])
|
N_MGR.module_detection([str(notify_hook_c_base)])
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
def create_hook(tdir, cache=True, on="valid1"):
|
def create_hook(tdir, cache=True, on="valid1"):
|
||||||
"""
|
"""
|
||||||
@ -2139,39 +2141,39 @@ def test_module_detection(tmpdir):
|
|||||||
pass
|
pass
|
||||||
""".format(on)))
|
""".format(on)))
|
||||||
|
|
||||||
utils.module_detection([str(tdir)], cache=cache)
|
N_MGR.module_detection([str(tdir)], cache=cache)
|
||||||
|
|
||||||
create_hook(notify_hook_c, on='valid1')
|
create_hook(notify_hook_c, on='valid1')
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
|
|
||||||
# Even if we correct our empty file; the fact the directory has been
|
# Even if we correct our empty file; the fact the directory has been
|
||||||
# scanned and failed to load (same with file), it won't be loaded
|
# scanned and failed to load (same with file), it won't be loaded
|
||||||
# a second time. This is intentional since module_detection() get's
|
# a second time. This is intentional since module_detection() get's
|
||||||
# called for every AppriseAsset() object creation. This prevents us
|
# called for every AppriseAsset() object creation. This prevents us
|
||||||
# from reloading conent over and over again wasting resources
|
# from reloading conent over and over again wasting resources
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
utils.module_detection([str(notify_hook_c)])
|
N_MGR.module_detection([str(notify_hook_c)])
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Even by absolute path...
|
# Even by absolute path...
|
||||||
utils.module_detection([str(notify_hook_c)])
|
N_MGR.module_detection([str(notify_hook_c)])
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# However we can bypass the cache if we really want to
|
# However we can bypass the cache if we really want to
|
||||||
utils.module_detection([str(notify_hook_c_base)], cache=False)
|
N_MGR.module_detection([str(notify_hook_c_base)], cache=False)
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Bypassing it twice causes the module to load twice (not very efficient)
|
# Bypassing it twice causes the module to load twice (not very efficient)
|
||||||
# However we can bypass the cache if we really want to
|
# However we can bypass the cache if we really want to
|
||||||
utils.module_detection([str(notify_hook_c_base)], cache=False)
|
N_MGR.module_detection([str(notify_hook_c_base)], cache=False)
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# If you update the module (corrupting it in the process and reload)
|
# If you update the module (corrupting it in the process and reload)
|
||||||
notify_hook_c.write(cleandoc("""
|
notify_hook_c.write(cleandoc("""
|
||||||
@ -2179,50 +2181,50 @@ def test_module_detection(tmpdir):
|
|||||||
"""))
|
"""))
|
||||||
|
|
||||||
# Force no cache to cause the file to be replaced
|
# Force no cache to cause the file to be replaced
|
||||||
utils.module_detection([str(notify_hook_c_base)], cache=False)
|
N_MGR.module_detection([str(notify_hook_c_base)], cache=False)
|
||||||
|
|
||||||
# Our valid entry is no longer loaded
|
# Our valid entry is no longer loaded
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
|
|
||||||
# No change to scanned paths
|
# No change to scanned paths
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
# The previously loaded module is now gone
|
# The previously loaded module is now gone
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Reload our valid1 entry
|
# Reload our valid1 entry
|
||||||
create_hook(notify_hook_c, on='valid1', cache=False)
|
create_hook(notify_hook_c, on='valid1', cache=False)
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Prepare an empty file
|
# Prepare an empty file
|
||||||
notify_hook_c.write("")
|
notify_hook_c.write("")
|
||||||
utils.module_detection([str(notify_hook_c_base)], cache=False)
|
N_MGR.module_detection([str(notify_hook_c_base)], cache=False)
|
||||||
|
|
||||||
# Our valid entry is no longer loaded
|
# Our valid entry is no longer loaded
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Now reload our module again (this time rather then an exception, the
|
# Now reload our module again (this time rather then an exception, the
|
||||||
# module is read back and swaps `valid1` for `valid2`
|
# module is read back and swaps `valid1` for `valid2`
|
||||||
create_hook(notify_hook_c, on='valid1', cache=False)
|
create_hook(notify_hook_c, on='valid1', cache=False)
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
assert 'valid2' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' not in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
create_hook(notify_hook_c, on='valid2', cache=False)
|
create_hook(notify_hook_c, on='valid2', cache=False)
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
assert 'valid2' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Reset our variables for the next test
|
# Reset our variables for the next test
|
||||||
create_hook(notify_hook_c, on='valid1', cache=False)
|
create_hook(notify_hook_c, on='valid1', cache=False)
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid1']
|
del N_MGR['valid1']
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
notify_hook_d = notify_hook_c_base.join('.ignore.py')
|
notify_hook_d = notify_hook_c_base.join('.ignore.py')
|
||||||
notify_hook_d.write("")
|
notify_hook_d.write("")
|
||||||
@ -2241,71 +2243,71 @@ def test_module_detection(tmpdir):
|
|||||||
# Try to load our base directory again; this time we search by the
|
# Try to load our base directory again; this time we search by the
|
||||||
# directory; the only edge case we're testing here is it will not
|
# directory; the only edge case we're testing here is it will not
|
||||||
# even look at the .ignore.py file found since it is invalid
|
# even look at the .ignore.py file found since it is invalid
|
||||||
utils.module_detection([str(notify_hook_c_base)])
|
N_MGR.module_detection([str(notify_hook_c_base)])
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Reset our variables for the next test
|
# Reset our variables for the next test
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid1']
|
del N_MGR._schema_map['valid1']
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Try to load our base directory again
|
# Try to load our base directory again
|
||||||
utils.module_detection([str(notify_hook_c)])
|
N_MGR.module_detection([str(notify_hook_c)])
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
|
|
||||||
# Hidden directories are not scanned
|
# Hidden directories are not scanned
|
||||||
assert 'valid2' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' not in N_MGR
|
||||||
|
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert str(notify_hook_c) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_c) in N_MGR._paths_previously_scanned
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# However a direct reference to the hidden directory is okay
|
# However a direct reference to the hidden directory is okay
|
||||||
utils.module_detection([str(notify_hook_e_base)])
|
N_MGR.module_detection([str(notify_hook_e_base)])
|
||||||
|
|
||||||
# We loaded our module
|
# We loaded our module
|
||||||
assert 'valid2' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 3
|
assert len(N_MGR._paths_previously_scanned) == 3
|
||||||
assert str(notify_hook_c) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_c) in N_MGR._paths_previously_scanned
|
||||||
assert str(notify_hook_e) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_e) in N_MGR._paths_previously_scanned
|
||||||
assert str(notify_hook_e_base) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_e_base) in N_MGR._paths_previously_scanned
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 2
|
assert len(N_MGR._custom_module_map) == 2
|
||||||
|
|
||||||
# Reset our variables for the next test
|
# Reset our variables for the next test
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid1']
|
del N_MGR._schema_map['valid1']
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid2']
|
del N_MGR._schema_map['valid2']
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Load our file directly
|
# Load our file directly
|
||||||
assert 'valid2' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' not in N_MGR
|
||||||
utils.module_detection([str(notify_hook_e)])
|
N_MGR.module_detection([str(notify_hook_e)])
|
||||||
|
|
||||||
# Now we have it loaded as expected
|
# Now we have it loaded as expected
|
||||||
assert 'valid2' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert str(notify_hook_e) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_e) in N_MGR._paths_previously_scanned
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# however if we try to load the base directory where the __init__.py
|
# however if we try to load the base directory where the __init__.py
|
||||||
# was already loaded from, it will not change anything
|
# was already loaded from, it will not change anything
|
||||||
utils.module_detection([str(notify_hook_e_base)])
|
N_MGR.module_detection([str(notify_hook_e_base)])
|
||||||
assert 'valid2' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' in N_MGR
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 2
|
assert len(N_MGR._paths_previously_scanned) == 2
|
||||||
assert str(notify_hook_e) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_e) in N_MGR._paths_previously_scanned
|
||||||
assert str(notify_hook_e_base) in utils.PATHS_PREVIOUSLY_SCANNED
|
assert str(notify_hook_e_base) in N_MGR._paths_previously_scanned
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
|
|
||||||
# Tidy up for the next test
|
# Tidy up for the next test
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid2']
|
del N_MGR._schema_map['valid2']
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
assert 'valid1' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' not in N_MGR
|
||||||
assert 'valid2' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' not in N_MGR
|
||||||
assert 'valid3' not in common.NOTIFY_SCHEMA_MAP
|
assert 'valid3' not in N_MGR
|
||||||
notify_hook_f_base = tmpdir.mkdir('f')
|
notify_hook_f_base = tmpdir.mkdir('f')
|
||||||
notify_hook_f = notify_hook_f_base.join('invalid.py')
|
notify_hook_f = notify_hook_f_base.join('invalid.py')
|
||||||
notify_hook_f.write(cleandoc("""
|
notify_hook_f.write(cleandoc("""
|
||||||
@ -2334,20 +2336,20 @@ def test_module_detection(tmpdir):
|
|||||||
|
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
utils.module_detection([str(notify_hook_f)])
|
N_MGR.module_detection([str(notify_hook_f)])
|
||||||
|
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 1
|
assert len(N_MGR._custom_module_map) == 1
|
||||||
assert 'valid1' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid1' in N_MGR
|
||||||
assert 'valid2' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid2' in N_MGR
|
||||||
assert 'valid3' in common.NOTIFY_SCHEMA_MAP
|
assert 'valid3' in N_MGR
|
||||||
|
|
||||||
# Reset our variables for the next test
|
# Reset our variables for the next test
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid1']
|
del N_MGR._schema_map['valid1']
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid2']
|
del N_MGR._schema_map['valid2']
|
||||||
del common.NOTIFY_SCHEMA_MAP['valid3']
|
del N_MGR._schema_map['valid3']
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
# Now test the handling of just bad data entirely
|
# Now test the handling of just bad data entirely
|
||||||
notify_hook_g_base = tmpdir.mkdir('g')
|
notify_hook_g_base = tmpdir.mkdir('g')
|
||||||
@ -2355,13 +2357,13 @@ def test_module_detection(tmpdir):
|
|||||||
with open(str(notify_hook_g), 'wb') as fout:
|
with open(str(notify_hook_g), 'wb') as fout:
|
||||||
fout.write(os.urandom(512))
|
fout.write(os.urandom(512))
|
||||||
|
|
||||||
utils.module_detection([str(notify_hook_g)])
|
N_MGR.module_detection([str(notify_hook_g)])
|
||||||
assert len(utils.PATHS_PREVIOUSLY_SCANNED) == 1
|
assert len(N_MGR._paths_previously_scanned) == 1
|
||||||
assert len(common.NOTIFY_CUSTOM_MODULE_MAP) == 0
|
assert len(N_MGR._custom_module_map) == 0
|
||||||
|
|
||||||
# Reset our variables before we leave
|
# Reset our variables before we leave
|
||||||
utils.PATHS_PREVIOUSLY_SCANNED.clear()
|
N_MGR._paths_previously_scanned.clear()
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP.clear()
|
N_MGR._custom_module_map.clear()
|
||||||
|
|
||||||
|
|
||||||
def test_exclusive_match():
|
def test_exclusive_match():
|
||||||
|
@ -31,13 +31,15 @@ import pytest
|
|||||||
from apprise import Apprise
|
from apprise import Apprise
|
||||||
from apprise import NotifyBase
|
from apprise import NotifyBase
|
||||||
from apprise import NotifyFormat
|
from apprise import NotifyFormat
|
||||||
|
from apprise.NotificationManager import NotificationManager
|
||||||
from apprise.common import NOTIFY_SCHEMA_MAP
|
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info >= (3, 7),
|
@pytest.mark.skipif(sys.version_info >= (3, 7),
|
||||||
reason="Requires Python 3.0 to 3.6")
|
reason="Requires Python 3.0 to 3.6")
|
||||||
@ -65,7 +67,7 @@ def test_apprise_asyncio_runtime_error():
|
|||||||
return NotifyBase.parse_url(url, verify_host=False)
|
return NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
# Create ourselves an Apprise object
|
# Create ourselves an Apprise object
|
||||||
a = Apprise()
|
a = Apprise()
|
||||||
|
@ -36,8 +36,8 @@ from os.path import dirname
|
|||||||
from os.path import getsize
|
from os.path import getsize
|
||||||
from apprise.attachment.AttachHTTP import AttachHTTP
|
from apprise.attachment.AttachHTTP import AttachHTTP
|
||||||
from apprise import AppriseAttachment
|
from apprise import AppriseAttachment
|
||||||
|
from apprise.NotificationManager import NotificationManager
|
||||||
from apprise.plugins.NotifyBase import NotifyBase
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
from apprise.common import NOTIFY_SCHEMA_MAP
|
|
||||||
from apprise.common import ContentLocation
|
from apprise.common import ContentLocation
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
@ -46,6 +46,9 @@ logging.disable(logging.CRITICAL)
|
|||||||
|
|
||||||
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
# Some exception handling we'll use
|
# Some exception handling we'll use
|
||||||
REQUEST_EXCEPTIONS = (
|
REQUEST_EXCEPTIONS = (
|
||||||
requests.ConnectionError(
|
requests.ConnectionError(
|
||||||
@ -131,7 +134,7 @@ def test_attach_http(mock_get):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
# Temporary path
|
# Temporary path
|
||||||
path = join(TEST_VAR_DIR, 'apprise-test.gif')
|
path = join(TEST_VAR_DIR, 'apprise-test.gif')
|
||||||
|
@ -34,12 +34,14 @@ import requests
|
|||||||
from apprise.common import ConfigFormat
|
from apprise.common import ConfigFormat
|
||||||
from apprise.config.ConfigHTTP import ConfigHTTP
|
from apprise.config.ConfigHTTP import ConfigHTTP
|
||||||
from apprise.plugins.NotifyBase import NotifyBase
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
from apprise.common import NOTIFY_SCHEMA_MAP
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
# Some exception handling we'll use
|
# Some exception handling we'll use
|
||||||
REQUEST_EXCEPTIONS = (
|
REQUEST_EXCEPTIONS = (
|
||||||
@ -77,7 +79,7 @@ def test_config_http(mock_post):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Store our good notification in our schema map
|
# Store our good notification in our schema map
|
||||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
# Our default content
|
# Our default content
|
||||||
default_content = """taga,tagb=good://server01"""
|
default_content = """taga,tagb=good://server01"""
|
||||||
|
@ -34,11 +34,15 @@ from apprise import AppriseConfig
|
|||||||
from apprise import AppriseAsset
|
from apprise import AppriseAsset
|
||||||
from apprise import AppriseAttachment
|
from apprise import AppriseAttachment
|
||||||
from apprise import common
|
from apprise import common
|
||||||
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
TEST_VAR_DIR = join(dirname(__file__), 'var')
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +52,7 @@ def test_notify_simple_decoration():
|
|||||||
|
|
||||||
# Verify our schema we're about to declare doesn't already exist
|
# Verify our schema we're about to declare doesn't already exist
|
||||||
# in our schema map:
|
# in our schema map:
|
||||||
assert 'utiltest' not in common.NOTIFY_SCHEMA_MAP
|
assert 'utiltest' not in N_MGR
|
||||||
|
|
||||||
verify_obj = {}
|
verify_obj = {}
|
||||||
|
|
||||||
@ -68,7 +72,7 @@ def test_notify_simple_decoration():
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Now after our hook being inline... it's been loaded
|
# Now after our hook being inline... it's been loaded
|
||||||
assert 'utiltest' in common.NOTIFY_SCHEMA_MAP
|
assert 'utiltest' in N_MGR
|
||||||
|
|
||||||
# Create ourselves an apprise object
|
# Create ourselves an apprise object
|
||||||
aobj = Apprise()
|
aobj = Apprise()
|
||||||
@ -146,7 +150,7 @@ def test_notify_simple_decoration():
|
|||||||
assert verify_obj['kwargs']['meta']['schema'] == 'utiltest'
|
assert verify_obj['kwargs']['meta']['schema'] == 'utiltest'
|
||||||
assert verify_obj['kwargs']['meta']['url'] == 'utiltest://'
|
assert verify_obj['kwargs']['meta']['url'] == 'utiltest://'
|
||||||
|
|
||||||
assert 'notexc' not in common.NOTIFY_SCHEMA_MAP
|
assert 'notexc' not in N_MGR
|
||||||
|
|
||||||
# Define a function here on the spot
|
# Define a function here on the spot
|
||||||
@notify(on="notexc", name="Apprise @notify Exception Handling")
|
@notify(on="notexc", name="Apprise @notify Exception Handling")
|
||||||
@ -154,7 +158,7 @@ def test_notify_simple_decoration():
|
|||||||
body, title, notify_type, attach, *args, **kwargs):
|
body, title, notify_type, attach, *args, **kwargs):
|
||||||
raise ValueError("An exception was thrown!")
|
raise ValueError("An exception was thrown!")
|
||||||
|
|
||||||
assert 'notexc' in common.NOTIFY_SCHEMA_MAP
|
assert 'notexc' in N_MGR
|
||||||
|
|
||||||
# Create ourselves an apprise object
|
# Create ourselves an apprise object
|
||||||
aobj = Apprise()
|
aobj = Apprise()
|
||||||
@ -165,8 +169,7 @@ def test_notify_simple_decoration():
|
|||||||
assert aobj.notify("Exceptions will be thrown!") is False
|
assert aobj.notify("Exceptions will be thrown!") is False
|
||||||
|
|
||||||
# Tidy
|
# Tidy
|
||||||
del common.NOTIFY_SCHEMA_MAP['utiltest']
|
N_MGR.remove('utiltest', 'notexc')
|
||||||
del common.NOTIFY_SCHEMA_MAP['notexc']
|
|
||||||
|
|
||||||
|
|
||||||
def test_notify_complex_decoration():
|
def test_notify_complex_decoration():
|
||||||
@ -175,7 +178,7 @@ def test_notify_complex_decoration():
|
|||||||
|
|
||||||
# Verify our schema we're about to declare doesn't already exist
|
# Verify our schema we're about to declare doesn't already exist
|
||||||
# in our schema map:
|
# in our schema map:
|
||||||
assert 'utiltest' not in common.NOTIFY_SCHEMA_MAP
|
assert 'utiltest' not in N_MGR
|
||||||
|
|
||||||
verify_obj = {}
|
verify_obj = {}
|
||||||
|
|
||||||
@ -196,7 +199,7 @@ def test_notify_complex_decoration():
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Now after our hook being inline... it's been loaded
|
# Now after our hook being inline... it's been loaded
|
||||||
assert 'utiltest' in common.NOTIFY_SCHEMA_MAP
|
assert 'utiltest' in N_MGR
|
||||||
|
|
||||||
# Create ourselves an apprise object
|
# Create ourselves an apprise object
|
||||||
aobj = Apprise()
|
aobj = Apprise()
|
||||||
@ -312,7 +315,7 @@ def test_notify_complex_decoration():
|
|||||||
assert 'key2=another' in verify_obj['kwargs']['meta']['url']
|
assert 'key2=another' in verify_obj['kwargs']['meta']['url']
|
||||||
|
|
||||||
# Tidy
|
# Tidy
|
||||||
del common.NOTIFY_SCHEMA_MAP['utiltest']
|
N_MGR.remove('utiltest')
|
||||||
|
|
||||||
|
|
||||||
def test_notify_multi_instance_decoration(tmpdir):
|
def test_notify_multi_instance_decoration(tmpdir):
|
||||||
@ -321,7 +324,7 @@ def test_notify_multi_instance_decoration(tmpdir):
|
|||||||
|
|
||||||
# Verify our schema we're about to declare doesn't already exist
|
# Verify our schema we're about to declare doesn't already exist
|
||||||
# in our schema map:
|
# in our schema map:
|
||||||
assert 'multi' not in common.NOTIFY_SCHEMA_MAP
|
assert 'multi' not in N_MGR
|
||||||
|
|
||||||
verify_obj = []
|
verify_obj = []
|
||||||
|
|
||||||
@ -342,7 +345,7 @@ def test_notify_multi_instance_decoration(tmpdir):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Now after our hook being inline... it's been loaded
|
# Now after our hook being inline... it's been loaded
|
||||||
assert 'multi' in common.NOTIFY_SCHEMA_MAP
|
assert 'multi' in N_MGR
|
||||||
|
|
||||||
# Prepare our config
|
# Prepare our config
|
||||||
t = tmpdir.mkdir("multi-test").join("apprise.yml")
|
t = tmpdir.mkdir("multi-test").join("apprise.yml")
|
||||||
@ -449,4 +452,4 @@ def test_notify_multi_instance_decoration(tmpdir):
|
|||||||
assert meta['url'] == 'multi://user2:pass2@hostname'
|
assert meta['url'] == 'multi://user2:pass2@hostname'
|
||||||
|
|
||||||
# Tidy
|
# Tidy
|
||||||
del common.NOTIFY_SCHEMA_MAP['multi']
|
N_MGR.remove('multi')
|
||||||
|
389
test/test_notification_manager.py
Normal file
389
test/test_notification_manager.py
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, 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 re
|
||||||
|
import pytest
|
||||||
|
import types
|
||||||
|
from inspect import cleandoc
|
||||||
|
|
||||||
|
from apprise.NotificationManager import NotificationManager
|
||||||
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
|
|
||||||
|
# Disable logging for a cleaner testing output
|
||||||
|
import logging
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification_manager_general():
|
||||||
|
"""
|
||||||
|
N_MGR: Notification Manager General testing
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Clear our set so we can test init calls
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert isinstance(N_MGR.schemas(), list)
|
||||||
|
assert len(N_MGR.schemas()) > 0
|
||||||
|
N_MGR.unload_modules(disable_native=True)
|
||||||
|
assert isinstance(N_MGR.schemas(), list)
|
||||||
|
assert len(N_MGR.schemas()) == 0
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert len(N_MGR) > 0
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
iter(N_MGR)
|
||||||
|
iter(N_MGR)
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert bool(N_MGR) is False
|
||||||
|
assert len([x for x in iter(N_MGR)]) > 0
|
||||||
|
assert bool(N_MGR) is True
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert isinstance(N_MGR.plugins(), types.GeneratorType)
|
||||||
|
assert len([x for x in N_MGR.plugins()]) > 0
|
||||||
|
N_MGR.unload_modules(disable_native=True)
|
||||||
|
assert isinstance(N_MGR.plugins(), types.GeneratorType)
|
||||||
|
assert len([x for x in N_MGR.plugins()]) == 0
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert isinstance(N_MGR['json'](host='localhost'), NotifyBase)
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert 'json' in N_MGR
|
||||||
|
|
||||||
|
# Define our good:// url
|
||||||
|
class DisabledNotification(NotifyBase):
|
||||||
|
# Always disabled
|
||||||
|
enabled = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def notify(self, *args, **kwargs):
|
||||||
|
# Pretend everything is okay
|
||||||
|
return True
|
||||||
|
|
||||||
|
def url(self, **kwargs):
|
||||||
|
# Support url() function
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# Define our good:// url
|
||||||
|
class GoodNotification(NotifyBase):
|
||||||
|
|
||||||
|
secure_protocol = ('good', 'goods')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def notify(self, *args, **kwargs):
|
||||||
|
# Pretend everything is okay
|
||||||
|
return True
|
||||||
|
|
||||||
|
def url(self, **kwargs):
|
||||||
|
# Support url() function
|
||||||
|
return ''
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert N_MGR.add(GoodNotification)
|
||||||
|
assert 'good' in N_MGR
|
||||||
|
assert 'goods' in N_MGR
|
||||||
|
assert 'abcd' not in N_MGR
|
||||||
|
assert 'xyz' not in N_MGR
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert N_MGR.add(GoodNotification, 'abcd')
|
||||||
|
assert 'good' in N_MGR
|
||||||
|
assert 'goods' in N_MGR
|
||||||
|
assert 'abcd' in N_MGR
|
||||||
|
assert 'xyz' not in N_MGR
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert N_MGR.add(GoodNotification, ['abcd', 'xYz'])
|
||||||
|
assert 'good' in N_MGR
|
||||||
|
assert 'goods' in N_MGR
|
||||||
|
assert 'abcd' in N_MGR
|
||||||
|
# Lower case
|
||||||
|
assert 'xyz' in N_MGR
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
# Not going to work; schemas must be a list of string
|
||||||
|
assert N_MGR.add(GoodNotification, object) is False
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
del N_MGR['good']
|
||||||
|
N_MGR['good'] = GoodNotification
|
||||||
|
del N_MGR['good']
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
N_MGR['good'] = GoodNotification
|
||||||
|
assert N_MGR['good'].enabled is True
|
||||||
|
N_MGR.enable_only('json', 'xml')
|
||||||
|
assert N_MGR['good'].enabled is False
|
||||||
|
assert N_MGR['json'].enabled is True
|
||||||
|
assert N_MGR['jsons'].enabled is True
|
||||||
|
assert N_MGR['xml'].enabled is True
|
||||||
|
assert N_MGR['xmls'].enabled is True
|
||||||
|
|
||||||
|
# Only two plugins are enabled
|
||||||
|
assert len([p for p in N_MGR.plugins(include_disabled=False)]) == 2
|
||||||
|
|
||||||
|
N_MGR.enable_only('good')
|
||||||
|
assert N_MGR['good'].enabled is True
|
||||||
|
assert N_MGR['json'].enabled is False
|
||||||
|
assert N_MGR['jsons'].enabled is False
|
||||||
|
assert N_MGR['xml'].enabled is False
|
||||||
|
assert N_MGR['xmls'].enabled is False
|
||||||
|
|
||||||
|
assert len([p for p in N_MGR.plugins(include_disabled=False)]) == 1
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
N_MGR['disabled'] = DisabledNotification
|
||||||
|
assert N_MGR['disabled'].enabled is False
|
||||||
|
N_MGR.enable_only('disabled')
|
||||||
|
# Can't enable items that aren't supposed to be:
|
||||||
|
assert N_MGR['disabled'].enabled is False
|
||||||
|
|
||||||
|
N_MGR['good'] = GoodNotification
|
||||||
|
assert N_MGR['good'].enabled is True
|
||||||
|
|
||||||
|
# You can't disable someething already disabled
|
||||||
|
N_MGR.disable('disabled')
|
||||||
|
assert N_MGR['disabled'].enabled is False
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
N_MGR.enable_only('form', 'xml')
|
||||||
|
for schema in N_MGR.schemas(include_disabled=False):
|
||||||
|
assert re.match(r'^(form|xml)s?$', schema, re.IGNORECASE) is not None
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert N_MGR['form'].enabled is True
|
||||||
|
assert N_MGR['xml'].enabled is True
|
||||||
|
assert N_MGR['json'].enabled is True
|
||||||
|
N_MGR.enable_only('form', 'xml')
|
||||||
|
assert N_MGR['form'].enabled is True
|
||||||
|
assert N_MGR['xml'].enabled is True
|
||||||
|
assert N_MGR['json'].enabled is False
|
||||||
|
|
||||||
|
N_MGR.disable('invalid', 'xml')
|
||||||
|
assert N_MGR['form'].enabled is True
|
||||||
|
assert N_MGR['xml'].enabled is False
|
||||||
|
assert N_MGR['json'].enabled is False
|
||||||
|
|
||||||
|
# Detect that our json object is enabled
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
# The below can not be indexed
|
||||||
|
N_MGR['invalid']
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
N_MGR.disable('invalid', 'xml')
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert N_MGR['json'].enabled is True
|
||||||
|
|
||||||
|
# Work with an empty module tree
|
||||||
|
N_MGR.unload_modules(disable_native=True)
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
# The below can not be indexed
|
||||||
|
N_MGR['good']
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert 'hello' not in N_MGR
|
||||||
|
assert 'good' not in N_MGR
|
||||||
|
assert 'goods' not in N_MGR
|
||||||
|
|
||||||
|
N_MGR['hello'] = GoodNotification
|
||||||
|
assert 'hello' in N_MGR
|
||||||
|
assert 'good' in N_MGR
|
||||||
|
assert 'goods' in N_MGR
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
# Can not assign the value again without getting a Conflict
|
||||||
|
N_MGR['good'] = GoodNotification
|
||||||
|
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
N_MGR.remove('good', 'invalid')
|
||||||
|
assert 'good' not in N_MGR
|
||||||
|
assert 'goods' not in N_MGR
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification_manager_module_loading(tmpdir):
|
||||||
|
"""
|
||||||
|
N_MGR: Notification Manager Module Loading
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Handle loading modules twice (they gracefully handle not loading more in
|
||||||
|
# memory then needed)
|
||||||
|
N_MGR.load_modules()
|
||||||
|
N_MGR.load_modules()
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification_manager_decorators(tmpdir):
|
||||||
|
"""
|
||||||
|
N_MGR: Notification Manager Decorator testing
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare ourselves a file to work with
|
||||||
|
notify_hook = tmpdir.mkdir('goodmodule').join('__init__.py')
|
||||||
|
notify_hook.write(cleandoc("""
|
||||||
|
from apprise.decorators import notify
|
||||||
|
|
||||||
|
# We want to trigger on anyone who configures a call to clihook://
|
||||||
|
@notify(on="clihooka")
|
||||||
|
def mywrapper(body, title, notify_type, *args, **kwargs):
|
||||||
|
# A simple test - print to screen
|
||||||
|
print("A {}: {} - {}".format(notify_type, title, body))
|
||||||
|
|
||||||
|
# No return (so a return of None) get's translated to True
|
||||||
|
|
||||||
|
# Define another in the same file; uppercase goes to lower
|
||||||
|
@notify(on="CLIhookb")
|
||||||
|
def mywrapper(body, title, notify_type, *args, **kwargs):
|
||||||
|
# A simple test - print to screen
|
||||||
|
print("B {}: {} - {}".format(notify_type, title, body))
|
||||||
|
|
||||||
|
# No return (so a return of None) get's translated to True
|
||||||
|
"""))
|
||||||
|
|
||||||
|
N_MGR.module_detection(str(notify_hook))
|
||||||
|
|
||||||
|
assert 'clihooka' in N_MGR
|
||||||
|
assert 'clihookb' in N_MGR
|
||||||
|
N_MGR.unload_modules()
|
||||||
|
assert 'clihooka' not in N_MGR
|
||||||
|
assert 'clihookb' not in N_MGR
|
||||||
|
|
||||||
|
N_MGR.module_detection(str(notify_hook))
|
||||||
|
assert 'clihooka' in N_MGR
|
||||||
|
assert 'clihookb' in N_MGR
|
||||||
|
del N_MGR['clihookb']
|
||||||
|
assert 'clihooka' in N_MGR
|
||||||
|
assert 'clihookb' not in N_MGR
|
||||||
|
del N_MGR['clihooka']
|
||||||
|
assert 'clihooka' not in N_MGR
|
||||||
|
assert 'clihookb' not in N_MGR
|
||||||
|
|
||||||
|
# Prepare ourselves a file to work with
|
||||||
|
notify_base = tmpdir.mkdir('plugins')
|
||||||
|
notify_test = notify_base.join('NotifyTest.py')
|
||||||
|
notify_test.write(cleandoc("""
|
||||||
|
#
|
||||||
|
# Bare Minimum Valid Object
|
||||||
|
#
|
||||||
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
|
from apprise.common import NotifyType
|
||||||
|
|
||||||
|
class NotifyTest(NotifyBase):
|
||||||
|
|
||||||
|
service_name = 'Test'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://github.com/caronc/apprise/'
|
||||||
|
|
||||||
|
# All boxcar notifications are secure
|
||||||
|
secure_protocol = 'mytest'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mytest'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def url(self):
|
||||||
|
return 'mytest://'
|
||||||
|
"""))
|
||||||
|
assert 'mytest' not in N_MGR
|
||||||
|
N_MGR.load_modules(path=str(notify_base))
|
||||||
|
assert 'mytest' in N_MGR
|
||||||
|
del N_MGR['mytest']
|
||||||
|
assert 'mytest' not in N_MGR
|
||||||
|
|
||||||
|
# Prepare ourselves a file to work with
|
||||||
|
notify_test = notify_base.join('NotifyNoAlign.py')
|
||||||
|
notify_test.write(cleandoc("""
|
||||||
|
#
|
||||||
|
# Bare Minimum Valid Object
|
||||||
|
#
|
||||||
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
|
from apprise.common import NotifyType
|
||||||
|
|
||||||
|
class NotifyDifferentName(NotifyBase):
|
||||||
|
|
||||||
|
service_name = 'Unloadable'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://github.com/caronc/apprise/'
|
||||||
|
|
||||||
|
# All boxcar notifications are secure
|
||||||
|
secure_protocol = 'noload'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mytest'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def url(self):
|
||||||
|
return 'mytest://'
|
||||||
|
"""))
|
||||||
|
assert 'mytest' not in N_MGR
|
||||||
|
N_MGR.load_modules(path=str(notify_base))
|
||||||
|
assert 'mytest' in N_MGR
|
||||||
|
|
||||||
|
# Could not be loaded because the filename did not align with the class
|
||||||
|
# name.
|
||||||
|
assert 'noload' not in N_MGR
|
||||||
|
|
||||||
|
# Double load will test section of code that prevents a notification
|
||||||
|
# From reloading if previously already loaded
|
||||||
|
N_MGR.load_modules(path=str(notify_base))
|
||||||
|
# Our item is still loaded as expected
|
||||||
|
assert 'mytest' in N_MGR
|
@ -114,8 +114,7 @@ def setup_glib_environment():
|
|||||||
|
|
||||||
# When patching something which has a side effect on the module-level code
|
# When patching something which has a side effect on the module-level code
|
||||||
# of a plugin, make sure to reload it.
|
# of a plugin, make sure to reload it.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin('NotifyDBus')
|
||||||
reload_plugin('NotifyDBus', replace_in=current_module)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -416,8 +415,7 @@ def test_plugin_dbus_gi_missing(dbus_glib_environment):
|
|||||||
|
|
||||||
# When patching something which has a side effect on the module-level code
|
# When patching something which has a side effect on the module-level code
|
||||||
# of a plugin, make sure to reload it.
|
# of a plugin, make sure to reload it.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin('NotifyDBus')
|
||||||
reload_plugin('NotifyDBus', replace_in=current_module)
|
|
||||||
|
|
||||||
# Create the instance.
|
# Create the instance.
|
||||||
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
|
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
|
||||||
@ -444,8 +442,7 @@ def test_plugin_dbus_gi_require_version_error(dbus_glib_environment):
|
|||||||
|
|
||||||
# When patching something which has a side effect on the module-level code
|
# When patching something which has a side effect on the module-level code
|
||||||
# of a plugin, make sure to reload it.
|
# of a plugin, make sure to reload it.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin('NotifyDBus')
|
||||||
reload_plugin('NotifyDBus', replace_in=current_module)
|
|
||||||
|
|
||||||
# Create instance.
|
# Create instance.
|
||||||
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
|
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
|
||||||
@ -472,8 +469,7 @@ def test_plugin_dbus_module_croaks(mocker, dbus_glib_environment):
|
|||||||
|
|
||||||
# When patching something which has a side effect on the module-level code
|
# When patching something which has a side effect on the module-level code
|
||||||
# of a plugin, make sure to reload it.
|
# of a plugin, make sure to reload it.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin('NotifyDBus')
|
||||||
reload_plugin('NotifyDBus', replace_in=current_module)
|
|
||||||
|
|
||||||
# Verify plugin is not available.
|
# Verify plugin is not available.
|
||||||
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
|
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
|
||||||
|
@ -97,8 +97,7 @@ def setup_glib_environment():
|
|||||||
|
|
||||||
# When patching something which has a side effect on the module-level code
|
# When patching something which has a side effect on the module-level code
|
||||||
# of a plugin, make sure to reload it.
|
# of a plugin, make sure to reload it.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin('NotifyGnome')
|
||||||
reload_plugin('NotifyGnome', replace_in=current_module)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -345,8 +344,7 @@ def test_plugin_gnome_gi_croaks():
|
|||||||
|
|
||||||
# When patching something which has a side effect on the module-level code
|
# When patching something which has a side effect on the module-level code
|
||||||
# of a plugin, make sure to reload it.
|
# of a plugin, make sure to reload it.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin('NotifyGnome')
|
||||||
reload_plugin('NotifyGnome', replace_in=current_module)
|
|
||||||
|
|
||||||
# Create instance.
|
# Create instance.
|
||||||
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False)
|
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False)
|
||||||
|
@ -43,7 +43,7 @@ logging.disable(logging.CRITICAL)
|
|||||||
|
|
||||||
|
|
||||||
if sys.platform not in ["darwin", "linux"]:
|
if sys.platform not in ["darwin", "linux"]:
|
||||||
pytest.skip("Only makes sense on macOS, but also works on Linux",
|
pytest.skip("Only makes sense on macOS, but testable in Linux",
|
||||||
allow_module_level=True)
|
allow_module_level=True)
|
||||||
|
|
||||||
|
|
||||||
@ -56,8 +56,7 @@ def pretend_macos(mocker):
|
|||||||
mocker.patch("platform.mac_ver", return_value=('10.8', ('', '', ''), ''))
|
mocker.patch("platform.mac_ver", return_value=('10.8', ('', '', ''), ''))
|
||||||
|
|
||||||
# Reload plugin module, in order to re-run module-level code.
|
# Reload plugin module, in order to re-run module-level code.
|
||||||
current_module = sys.modules[__name__]
|
reload_plugin("NotifyMacOSX")
|
||||||
reload_plugin("NotifyMacOSX", replace_in=current_module)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -93,6 +92,7 @@ def test_plugin_macosx_general_success(macos_notify_environment):
|
|||||||
NotifyMacOSX() general checks
|
NotifyMacOSX() general checks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Toggle Enable Flag
|
||||||
obj = apprise.Apprise.instantiate(
|
obj = apprise.Apprise.instantiate(
|
||||||
'macosx://_/?image=True', suppress_exceptions=False)
|
'macosx://_/?image=True', suppress_exceptions=False)
|
||||||
assert isinstance(obj, NotifyMacOSX) is True
|
assert isinstance(obj, NotifyMacOSX) is True
|
||||||
|
Loading…
Reference in New Issue
Block a user