mirror of
https://github.com/caronc/apprise.git
synced 2024-12-29 10:09:10 +01:00
Apprise Configuration can be added directly (#199)
This commit is contained in:
parent
8f2485c917
commit
fa20b38e76
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
82
apprise/config/ConfigMemory.py
Normal file
82
apprise/config/ConfigMemory.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
63
test/test_config_memory.py
Normal file
63
test/test_config_memory.py
Normal 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
|
Loading…
Reference in New Issue
Block a user