Apprise Configuration can be added directly (#199)

This commit is contained in:
Chris Caron 2020-01-24 18:04:28 -05:00 committed by GitHub
parent 8f2485c917
commit fa20b38e76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 449 additions and 10 deletions

View File

@ -102,7 +102,7 @@ class AppriseAttachment(object):
# Initialize our default cache value
cache = cache if cache is not None else self.cache
if isinstance(asset, AppriseAsset):
if asset is None:
# prepare default asset
asset = self.asset

View File

@ -115,7 +115,7 @@ class AppriseConfig(object):
# Initialize our default cache value
cache = cache if cache is not None else self.cache
if isinstance(asset, AppriseAsset):
if asset is None:
# prepare default asset
asset = self.asset
@ -165,6 +165,39 @@ class AppriseConfig(object):
# Return our status
return return_status
def add_config(self, content, asset=None, tag=None, format=None):
"""
Adds one configuration file in it's raw format. Content gets loaded as
a memory based object and only exists for the life of this
AppriseConfig object it was loaded into.
If you know the format ('yaml' or 'text') you can specify
it for slightly less overhead during this call. Otherwise the
configuration is auto-detected.
"""
if asset is None:
# prepare default asset
asset = self.asset
if not isinstance(content, six.string_types):
logger.warning(
"An invalid configuration (type={}) was specified.".format(
type(content)))
return False
logger.debug("Loading raw configuration: {}".format(content))
# Create ourselves a ConfigMemory Object to store our configuration
instance = config.ConfigMemory(
content=content, format=format, asset=asset, tag=tag)
# Add our initialized plugin to our server listings
self.configs.append(instance)
# Return our status
return True
def servers(self, tag=MATCH_ALL_TAG, *args, **kwargs):
"""
Returns all of our servers dynamically build based on parsed

View File

@ -92,7 +92,8 @@ class ConfigBase(URLBase):
# Store the encoding
self.encoding = kwargs.get('encoding')
if 'format' in kwargs:
if 'format' in kwargs \
and isinstance(kwargs['format'], six.string_types):
# Store the enforced config format
self.config_format = kwargs.get('format').lower()
@ -249,6 +250,109 @@ class ConfigBase(URLBase):
return results
@staticmethod
def detect_config_format(content, **kwargs):
"""
Takes the specified content and attempts to detect the format type
The function returns the actual format type if detected, otherwise
it returns None
"""
# Detect Format Logic:
# - A pound/hashtag (#) is alawys a comment character so we skip over
# lines matched here.
# - Detection begins on the first non-comment and non blank line
# matched.
# - If we find a string followed by a colon, we know we're dealing
# with a YAML file.
# - If we find a string that starts with a URL, or our tag
# definitions (accepting commas) followed by an equal sign we know
# we're dealing with a TEXT format.
# Define what a valid line should look like
valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(?P<text>((?P<tag>[ \t,a-z0-9_-]+)=)?[a-z0-9]+://.*)|'
r'((?P<yaml>[a-z0-9]+):.*))?$', re.I)
try:
# split our content up to read line by line
content = re.split(r'\r*\n', content)
except TypeError:
# content was not expected string type
ConfigBase.logger.error('Invalid apprise config specified')
return None
# By default set our return value to None since we don't know
# what the format is yet
config_format = None
# iterate over each line of the file to attempt to detect it
# stop the moment a the type has been determined
for line, entry in enumerate(content, start=1):
result = valid_line_re.match(entry)
if not result:
# Invalid syntax
ConfigBase.logger.error(
'Undetectable apprise configuration found '
'based on line {}.'.format(line))
# Take an early exit
return None
# Attempt to detect configuration
if result.group('yaml'):
config_format = ConfigFormat.YAML
ConfigBase.logger.debug(
'Detected YAML configuration '
'based on line {}.'.format(line))
break
elif result.group('text'):
config_format = ConfigFormat.TEXT
ConfigBase.logger.debug(
'Detected TEXT configuration '
'based on line {}.'.format(line))
break
# If we reach here, we have a comment entry
# Adjust default format to TEXT
config_format = ConfigFormat.TEXT
return config_format
@staticmethod
def config_parse(content, asset=None, config_format=None, **kwargs):
"""
Takes the specified config content and loads it based on the specified
config_format. If a format isn't specified, then it is auto detected.
"""
if config_format is None:
# Detect the format
config_format = ConfigBase.detect_config_format(content)
if not config_format:
# We couldn't detect configuration
ConfigBase.logger.error('Could not detect configuration')
return list()
if config_format not in CONFIG_FORMATS:
# Invalid configuration type specified
ConfigBase.logger.error(
'An invalid configuration format ({}) was specified'.format(
config_format))
return list()
# Dynamically load our parse_ function based on our config format
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
# Execute our config parse function which always returns a list
return fn(content=content, asset=asset)
@staticmethod
def config_parse_text(content, asset=None):
"""
@ -270,9 +374,6 @@ class ConfigBase(URLBase):
<URL>
"""
# For logging, track the line number
line = 0
response = list()
# Define what a valid line should look like
@ -290,10 +391,7 @@ class ConfigBase(URLBase):
ConfigBase.logger.error('Invalid apprise text data specified')
return list()
for entry in content:
# Increment our line count
line += 1
for line, entry in enumerate(content, start=1):
result = valid_line_re.match(entry)
if not result:
# Invalid syntax

View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from .ConfigBase import ConfigBase
from ..AppriseLocale import gettext_lazy as _
class ConfigMemory(ConfigBase):
"""
For information that was loaded from memory and does not
persist anywhere.
"""
# The default descriptive name associated with the service
service_name = _('Memory')
# The default protocol
protocol = 'memory'
def __init__(self, content, **kwargs):
"""
Initialize Memory Object
Memory objects just store the raw configuration in memory. There is
no external reference point. It's always considered cached.
"""
super(ConfigMemory, self).__init__(**kwargs)
# Store our raw config into memory
self.content = content
if self.config_format is None:
# Detect our format if possible
self.config_format = \
ConfigMemory.detect_config_format(self.content)
return
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
return 'memory://'
def read(self, **kwargs):
"""
Simply return content stored into memory
"""
return self.content
@staticmethod
def parse_url(url):
"""
Memory objects have no parseable URL
"""
# These URLs can not be parsed
return None

View File

@ -278,6 +278,64 @@ def test_apprise_multi_config_entries(tmpdir):
ac.server_pop(len(ac.servers()) - 1), NotifyBase) is True
def test_apprise_add_config():
"""
API AppriseConfig.add_config()
"""
content = """
# A comment line over top of a URL
mailto://usera:pass@gmail.com
# A line with mulitiple tag assignments to it
taga,tagb=gnome://
# Event if there is accidental leading spaces, this configuation
# is accepting of htat and will not exclude them
tagc=kde://
# A very poorly structured url
sns://:@/
# Just 1 token provided causes exception
sns://T1JJ3T3L2/
"""
# Create ourselves a config object
ac = AppriseConfig()
assert ac.add_config(content=content)
# One configuration file should have been found
assert len(ac) == 1
# Object can be directly checked as a boolean; response is True
# when there is at least one entry
assert ac
# We should be able to read our 3 servers from that
assert len(ac.servers()) == 3
# Get our URL back
assert isinstance(ac[0].url(), six.string_types)
# Test invalid content
assert ac.add_config(content=object()) is False
assert ac.add_config(content=42) is False
assert ac.add_config(content=None) is False
# Still only one server loaded
assert len(ac) == 1
# Test having a pre-defined asset object and tag created
assert ac.add_config(
content=content, asset=AppriseAsset(), tag='a') is True
# Now there are 2 servers loaded
assert len(ac) == 2
# and 6 urls.. (as we've doubled up)
assert len(ac.servers()) == 6
def test_apprise_config_tagging(tmpdir):
"""
API: AppriseConfig tagging

View File

@ -29,6 +29,7 @@ import pytest
from apprise.AppriseAsset import AppriseAsset
from apprise.config.ConfigBase import ConfigBase
from apprise.config import __load_matrix
from apprise import ConfigFormat
# Disable logging for a cleaner testing output
import logging
@ -95,6 +96,110 @@ def test_config_base():
assert results['qsd'].get('format') == 'invalid'
def test_config_base_detect_config_format():
"""
API: ConfigBase.detect_config_format
"""
# Garbage Handling
assert ConfigBase.detect_config_format(object()) is None
assert ConfigBase.detect_config_format(None) is None
assert ConfigBase.detect_config_format(12) is None
# Empty files are valid
assert ConfigBase.detect_config_format('') is ConfigFormat.TEXT
# Valid Text Configuration
assert ConfigBase.detect_config_format("""
# A comment line over top of a URL
mailto://userb:pass@gmail.com
""") is ConfigFormat.TEXT
# A text file that has semi-colon as comment characters
# is valid too
assert ConfigBase.detect_config_format("""
; A comment line over top of a URL
mailto://userb:pass@gmail.com
""") is ConfigFormat.TEXT
# Valid YAML Configuration
assert ConfigBase.detect_config_format("""
# A comment line over top of a URL
version: 1
""") is ConfigFormat.YAML
# Just a whole lot of blank lines...
assert ConfigBase.detect_config_format('\n\n\n') is ConfigFormat.TEXT
# Invalid Config
assert ConfigBase.detect_config_format("3") is None
def test_config_base_config_parse():
"""
API: ConfigBase.config_parse
"""
# Garbage Handling
assert isinstance(ConfigBase.config_parse(object()), list)
assert isinstance(ConfigBase.config_parse(None), list)
assert isinstance(ConfigBase.config_parse(''), list)
assert isinstance(ConfigBase.config_parse(12), list)
# Valid Text Configuration
result = ConfigBase.config_parse("""
# A comment line over top of a URL
mailto://userb:pass@gmail.com
""", asset=AppriseAsset())
# We expect to parse 1 entry from the above
assert isinstance(result, list)
assert len(result) == 1
assert len(result[0].tags) == 0
# Valid Configuration
result = ConfigBase.config_parse("""
# if no version is specified then version 1 is presumed
version: 1
#
# Define your notification urls:
#
urls:
- pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b
- mailto://test:password@gmail.com
- syslog://:
- tag: devops, admin
""", asset=AppriseAsset())
# We expect to parse 3 entries from the above
assert isinstance(result, list)
assert len(result) == 3
assert len(result[0].tags) == 0
assert len(result[1].tags) == 0
assert len(result[2].tags) == 2
# Test case where we pass in a bad format
result = ConfigBase.config_parse("""
; A comment line over top of a URL
mailto://userb:pass@gmail.com
""", config_format='invalid-format')
# This is not parseable despite the valid text
assert isinstance(result, list)
assert len(result) == 0
result = ConfigBase.config_parse("""
; A comment line over top of a URL
mailto://userb:pass@gmail.com
""", config_format=ConfigFormat.TEXT)
# Parseable
assert isinstance(result, list)
assert len(result) == 1
def test_config_base_config_parse_text():
"""
API: ConfigBase.config_parse_text object

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
from apprise.config.ConfigMemory import ConfigMemory
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
def test_config_memory():
"""
API: ConfigMemory() object
"""
assert ConfigMemory.parse_url('garbage://') is None
# Initialize our object
cm = ConfigMemory(content="syslog://", format='text')
# one entry added
assert len(cm) == 1
# Test general functions
assert isinstance(cm.url(), six.string_types) is True
assert isinstance(cm.read(), six.string_types) is True
# Test situation where an auto-detect is required:
cm = ConfigMemory(content="syslog://")
# one entry added
assert len(cm) == 1
# Test general functions
assert isinstance(cm.url(), six.string_types) is True
assert isinstance(cm.read(), six.string_types) is True
# Test situation where we can not detect the data
assert len(ConfigMemory(content="garbage")) == 0