Merge pull request #97 from caronc/logging-customization

Added deprecate and trace logging directives
This commit is contained in:
Chris Caron 2019-04-05 22:56:37 -04:00 committed by GitHub
commit 5997f6d6c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 269 additions and 69 deletions

View File

@ -24,8 +24,8 @@
# THE SOFTWARE.
import re
import os
import six
import logging
from markdown import markdown
from itertools import chain
@ -34,6 +34,7 @@ from .common import NotifyFormat
from .utils import is_exclusive_match
from .utils import parse_list
from .utils import GET_SCHEMA_RE
from .logger import logger
from .AppriseAsset import AppriseAsset
from .AppriseConfig import AppriseConfig
@ -43,8 +44,6 @@ from .plugins.NotifyBase import NotifyBase
from . import plugins
from . import __version__
logger = logging.getLogger(__name__)
class Apprise(object):
"""
@ -112,6 +111,10 @@ class Apprise(object):
# Build a list of tags to associate with the newly added notifications
results['tag'] = set(parse_list(tag))
logger.trace('URL {} unpacked as:{}{}'.format(
url, os.linesep, os.linesep.join(
['{}="{}"'.format(k, v) for k, v in results.items()])))
# Prepare our Asset Object
results['asset'] = \
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
@ -122,6 +125,9 @@ class Apprise(object):
# URL information
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
# Create log entry of loaded URL
logger.debug('Loaded URL: {}'.format(plugin.url()))
except Exception:
# the arguments are invalid or can not be used.
logger.error('Could not load URL: %s' % url)
@ -163,7 +169,7 @@ class Apprise(object):
return True
elif not isinstance(servers, (tuple, set, list)):
logging.error(
logger.error(
"An invalid notification (type={}) was specified.".format(
type(servers)))
return False
@ -176,7 +182,7 @@ class Apprise(object):
continue
elif not isinstance(_server, six.string_types):
logging.error(
logger.error(
"An invalid notification (type={}) was specified.".format(
type(_server)))
return_status = False
@ -187,7 +193,7 @@ class Apprise(object):
instance = Apprise.instantiate(_server, asset=asset, tag=tag)
if not isinstance(instance, NotifyBase):
return_status = False
logging.error(
logger.error(
"Failed to load notification url: {}".format(_server),
)
continue
@ -321,7 +327,7 @@ class Apprise(object):
except Exception:
# A catch all so we don't have to abort early
# just because one of our plugins has a bug in it.
logging.exception("Notification Exception")
logger.exception("Notification Exception")
status = False
return status

View File

@ -24,7 +24,6 @@
# THE SOFTWARE.
import six
import logging
from . import config
from . import ConfigBase
@ -34,8 +33,7 @@ from .AppriseAsset import AppriseAsset
from .utils import GET_SCHEMA_RE
from .utils import parse_list
from .utils import is_exclusive_match
logger = logging.getLogger(__name__)
from .logger import logger
class AppriseConfig(object):
@ -100,7 +98,7 @@ class AppriseConfig(object):
configs = (configs, )
elif not isinstance(configs, (tuple, set, list)):
logging.error(
logger.error(
'An invalid configuration path (type={}) was '
'specified.'.format(type(configs)))
return False
@ -114,7 +112,7 @@ class AppriseConfig(object):
continue
elif not isinstance(_config, six.string_types):
logging.warning(
logger.warning(
"An invalid configuration (type={}) was specified.".format(
type(_config)))
return_status = False

View File

@ -35,15 +35,13 @@ from . import AppriseAsset
from . import AppriseConfig
from .utils import parse_list
from .common import NOTIFY_TYPES
from .logger import logger
from . import __title__
from . import __version__
from . import __license__
from . import __copywrite__
# Logging
logger = logging.getLogger('apprise')
# Defines our click context settings adding -h to the additional options that
# can be specified to get the help menu to come up
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@ -121,18 +119,28 @@ def main(body, title, config, urls, notification_type, theme, tag, verbose,
# Logging
ch = logging.StreamHandler(sys.stdout)
if verbose > 2:
if verbose > 3:
# -vvvv: Most Verbose Debug Logging
logger.setLevel(logging.TRACE)
elif verbose > 2:
# -vvv: Debug Logging
logger.setLevel(logging.DEBUG)
elif verbose > 1:
# -vv: INFO Messages
logger.setLevel(logging.INFO)
elif verbose > 0:
# -v: WARNING Messages
logger.setLevel(logging.WARNING)
else:
# No verbosity means we display ERRORS only AND any deprecation
# warnings
logger.setLevel(logging.ERROR)
# Format our logger
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

View File

@ -26,7 +26,6 @@
import os
import re
import six
import logging
import yaml
from .. import plugins
@ -37,8 +36,6 @@ from ..common import CONFIG_FORMATS
from ..utils import GET_SCHEMA_RE
from ..utils import parse_list
logger = logging.getLogger(__name__)
class ConfigBase(URLBase):
"""
@ -61,9 +58,6 @@ class ConfigBase(URLBase):
# anything else. 128KB (131072B)
max_buffer_size = 131072
# Logging
logger = logging.getLogger(__name__)
def __init__(self, **kwargs):
"""
Initialize some general logging and common server arguments that will
@ -128,6 +122,13 @@ class ConfigBase(URLBase):
# Execute our config parse function which always returns a list
self._cached_servers.extend(fn(content=content, asset=asset))
if len(self._cached_servers):
self.logger.info('Loaded {} entries from {}'.format(
len(self._cached_servers), self.url()))
else:
self.logger.warning('Failed to load configuration from {}'.format(
self.url()))
return self._cached_servers
def read(self):
@ -216,7 +217,7 @@ class ConfigBase(URLBase):
except TypeError:
# content was not expected string type
logger.error('Invalid apprise text data specified')
ConfigBase.logger.error('Invalid apprise text data specified')
return list()
for entry in content:
@ -226,7 +227,7 @@ class ConfigBase(URLBase):
result = valid_line_re.match(entry)
if not result:
# Invalid syntax
logger.error(
ConfigBase.logger.error(
'Invalid apprise text format found '
'{} on line {}.'.format(entry, line))
@ -255,7 +256,7 @@ class ConfigBase(URLBase):
# Some basic validation
if schema not in plugins.SCHEMA_MAP:
logger.warning(
ConfigBase.logger.warning(
'Unsupported schema {} on line {}.'.format(
schema, line))
continue
@ -266,7 +267,7 @@ class ConfigBase(URLBase):
if results is None:
# Failed to parse the server URL
logger.warning(
ConfigBase.logger.warning(
'Unparseable URL {} on line {}.'.format(url, line))
continue
@ -274,6 +275,11 @@ class ConfigBase(URLBase):
# notifications if any were set
results['tag'] = set(parse_list(result.group('tags')))
ConfigBase.logger.trace(
'URL {} unpacked as:{}{}'.format(
url, os.linesep, os.linesep.join(
['{}="{}"'.format(k, v) for k, v in results.items()])))
# Prepare our Asset Object
results['asset'] = \
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
@ -283,9 +289,12 @@ class ConfigBase(URLBase):
# parsed URL information
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
# Create log entry of loaded URL
ConfigBase.logger.debug('Loaded URL: {}'.format(plugin.url()))
except Exception:
# the arguments are invalid or can not be used.
logger.warning(
ConfigBase.logger.warning(
'Could not load URL {} on line {}.'.format(
url, line))
continue
@ -314,20 +323,22 @@ class ConfigBase(URLBase):
except (AttributeError, yaml.error.MarkedYAMLError) as e:
# Invalid content
logger.error('Invalid apprise yaml data specified.')
logger.debug('YAML Exception:{}{}'.format(os.linesep, e))
ConfigBase.logger.error(
'Invalid apprise yaml data specified.')
ConfigBase.logger.debug(
'YAML Exception:{}{}'.format(os.linesep, e))
return list()
if not isinstance(result, dict):
# Invalid content
logger.error('Invalid apprise yaml structure specified')
ConfigBase.logger.error('Invalid apprise yaml structure specified')
return list()
# YAML Version
version = result.get('version', 1)
if version != 1:
# Invalid syntax
logger.error(
ConfigBase.logger.error(
'Invalid apprise yaml version specified {}.'.format(version))
return list()
@ -342,13 +353,15 @@ class ConfigBase(URLBase):
if k.startswith('_') or k.endswith('_'):
# Entries are considered reserved if they start or end
# with an underscore
logger.warning('Ignored asset key "{}".'.format(k))
ConfigBase.logger.warning(
'Ignored asset key "{}".'.format(k))
continue
if not (hasattr(asset, k) and
isinstance(getattr(asset, k), six.string_types)):
# We can't set a function or non-string set value
logger.warning('Invalid asset key "{}".'.format(k))
ConfigBase.logger.warning(
'Invalid asset key "{}".'.format(k))
continue
if v is None:
@ -357,7 +370,8 @@ class ConfigBase(URLBase):
if not isinstance(v, six.string_types):
# we must set strings with a string
logger.warning('Invalid asset value to "{}".'.format(k))
ConfigBase.logger.warning(
'Invalid asset value to "{}".'.format(k))
continue
# Set our asset object with the new value
@ -379,7 +393,8 @@ class ConfigBase(URLBase):
urls = result.get('urls', None)
if not isinstance(urls, (list, tuple)):
# Unsupported
logger.error('Missing "urls" directive in apprise yaml.')
ConfigBase.logger.error(
'Missing "urls" directive in apprise yaml.')
return list()
# Iterate over each URL
@ -400,7 +415,7 @@ class ConfigBase(URLBase):
# interpretation of a URL geared for them
schema = GET_SCHEMA_RE.match(_url)
if schema is None:
logger.warning(
ConfigBase.logger.warning(
'Unsupported schema in urls entry #{}'.format(no))
continue
@ -409,7 +424,7 @@ class ConfigBase(URLBase):
# Some basic validation
if schema not in plugins.SCHEMA_MAP:
logger.warning(
ConfigBase.logger.warning(
'Unsupported schema {} in urls entry #{}'.format(
schema, no))
continue
@ -418,7 +433,7 @@ class ConfigBase(URLBase):
# containing all of the information parsed from our URL
_results = plugins.SCHEMA_MAP[schema].parse_url(_url)
if _results is None:
logger.warning(
ConfigBase.logger.warning(
'Unparseable {} based url; entry #{}'.format(
schema, no))
continue
@ -439,7 +454,7 @@ class ConfigBase(URLBase):
# Get our schema
schema = GET_SCHEMA_RE.match(_url)
if schema is None:
logger.warning(
ConfigBase.logger.warning(
'Unsupported schema in urls entry #{}'.format(no))
continue
@ -448,7 +463,7 @@ class ConfigBase(URLBase):
# Some basic validation
if schema not in plugins.SCHEMA_MAP:
logger.warning(
ConfigBase.logger.warning(
'Unsupported schema {} in urls entry #{}'.format(
schema, no))
continue
@ -494,7 +509,7 @@ class ConfigBase(URLBase):
else:
# Unsupported
logger.warning(
ConfigBase.logger.warning(
'Unsupported apprise yaml entry #{}'.format(no))
continue
@ -519,6 +534,12 @@ class ConfigBase(URLBase):
# Just use the global settings
_results['tag'] = global_tags
ConfigBase.logger.trace(
'URL no.{} {} unpacked as:{}{}'
.format(os.linesep, no, url, os.linesep.join(
['{}="{}"'.format(k, a)
for k, a in _results.items()])))
# Prepare our Asset Object
_results['asset'] = asset
@ -527,9 +548,13 @@ class ConfigBase(URLBase):
# parsed URL information
plugin = plugins.SCHEMA_MAP[_results['schema']](**_results)
# Create log entry of loaded URL
ConfigBase.logger.debug(
'Loaded URL: {}'.format(plugin.url()))
except Exception:
# the arguments are invalid or can not be used.
logger.warning(
ConfigBase.logger.warning(
'Could not load apprise yaml entry #{}, item #{}'
.format(no, entry))
continue

View File

@ -138,8 +138,6 @@ class ConfigFile(ConfigBase):
# YAML Filename Detected
self.default_config_format = ConfigFormat.YAML
self.logger.debug('Succesfully read config file: %s' % (path))
# Return our response object
return response

View File

@ -76,11 +76,7 @@ class ConfigHTTP(ConfigBase):
"""
super(ConfigHTTP, self).__init__(**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
self.schema = 'https' if self.secure else 'http'
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
@ -242,9 +238,6 @@ class ConfigHTTP(ConfigBase):
# TEXT data detected based on header content
self.default_config_format = ConfigFormat.TEXT
# else do nothing; fall back to whatever default is
# already set.
except requests.RequestException as e:
self.logger.error(
'A Connection error occured retrieving HTTP '

61
apprise/logger.py Normal file
View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 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 logging
# Define a verbosity level that is a noisier then debug mode
logging.TRACE = logging.DEBUG - 1
# Define a verbosity level that is always used even when no verbosity is set
# from the command line. The idea here is to allow for deprecation notices
logging.DEPRECATE = logging.ERROR + 1
# Assign our Levels into our logging object
logging.addLevelName(logging.DEPRECATE, "DEPRECATION WARNING")
logging.addLevelName(logging.TRACE, "TRACE")
def trace(self, message, *args, **kwargs):
"""
Verbose Debug Logging - Trace
"""
if self.isEnabledFor(logging.TRACE):
self._log(logging.TRACE, message, args, **kwargs)
def deprecate(self, message, *args, **kwargs):
"""
Deprication Warning Logging
"""
if self.isEnabledFor(logging.DEPRECATE):
self._log(logging.DEPRECATE, message, args, **kwargs)
# Assign our Loggers for use in Apprise
logging.Logger.trace = trace
logging.Logger.deprecate = deprecate
# Create ourselve a generic logging reference
logger = logging.getLogger('apprise')

View File

@ -321,9 +321,9 @@ class NotifyDiscord(NotifyBase):
# Use Thumbnail
if 'thumbnail' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyDiscord.logger.warning(
'DEPRICATION NOTICE - The Discord URL contains the parameter '
'"thumbnail=" which will be depricated in an upcoming '
NotifyDiscord.logger.deprecate(
'The Discord URL contains the parameter '
'"thumbnail=" which will be deprecated in an upcoming '
'release. Please use "image=" instead.'
)

View File

@ -18,7 +18,7 @@ __all__ = [
'GrowlNotifier'
]
logger = logging.getLogger(__name__)
logger = logging.getLogger('gntp')
class GrowlNotifier(notifier.GrowlNotifier):

View File

@ -27,7 +27,7 @@ __all__ = [
'GrowlNotifier',
]
logger = logging.getLogger(__name__)
logger = logging.getLogger('gntp')
class GrowlNotifier(object):

View File

@ -928,9 +928,9 @@ class NotifyMatrix(NotifyBase):
# Thumbnail (old way)
if 'thumbnail' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyMatrix.logger.warning(
'DEPRICATION NOTICE - The Matrix URL contains the parameter '
'"thumbnail=" which will be depricated in an upcoming '
NotifyMatrix.logger.deprecate(
'The Matrix URL contains the parameter '
'"thumbnail=" which will be deprecated in an upcoming '
'release. Please use "image=" instead.'
)
@ -943,9 +943,9 @@ class NotifyMatrix(NotifyBase):
# Webhook (old way)
if 'webhook' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyMatrix.logger.warning(
'DEPRICATION NOTICE - The Matrix URL contains the parameter '
'"webhook=" which will be depricated in an upcoming '
NotifyMatrix.logger.deprecate(
'The Matrix URL contains the parameter '
'"webhook=" which will be deprecated in an upcoming '
'release. Please use "mode=" instead.'
)

View File

@ -293,9 +293,9 @@ class NotifyRyver(NotifyBase):
if 'webhook' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyRyver.logger.warning(
'DEPRICATION NOTICE - The Ryver URL contains the parameter '
'"webhook=" which will be depricated in an upcoming '
NotifyRyver.logger.deprecate(
'The Ryver URL contains the parameter '
'"webhook=" which will be deprecated in an upcoming '
'release. Please use "mode=" instead.'
)

View File

@ -39,9 +39,6 @@ except ImportError:
from urllib.parse import quote
from urllib.parse import urlparse
import logging
logger = logging.getLogger(__name__)
# URL Indexing Table for returns via parse_url()
VALID_URL_RE = re.compile(
r'^[\s]*(?P<schema>[^:\s]+):[/\\]*(?P<path>[^?]+)'

View File

@ -143,11 +143,19 @@ def test_apprise():
# We fail whenever we're initialized
raise TypeError()
def url(self):
# Support URL
return ''
class GoodNotification(NotifyBase):
def __init__(self, **kwargs):
super(GoodNotification, self).__init__(
notify_format=NotifyFormat.HTML, **kwargs)
def url(self):
# Support URL
return ''
def notify(self, **kwargs):
# Pretend everything is okay
return True
@ -190,17 +198,29 @@ def test_apprise():
# Pretend everything is okay
raise TypeError()
def url(self):
# Support URL
return ''
class RuntimeNotification(NotifyBase):
def notify(self, **kwargs):
# Pretend everything is okay
raise RuntimeError()
def url(self):
# Support URL
return ''
class FailNotification(NotifyBase):
def notify(self, **kwargs):
# Pretend everything is okay
return False
def url(self):
# Support URL
return ''
# Store our bad notification in our schema map
SCHEMA_MAP['throw'] = ThrowNotification
@ -224,6 +244,11 @@ def test_apprise():
def __init__(self, **kwargs):
# Pretend everything is okay
raise TypeError()
def url(self):
# Support URL
return ''
SCHEMA_MAP['throw'] = ThrowInstantiateNotification
# Reset our object
@ -394,6 +419,10 @@ def test_apprise_notify_formats(tmpdir):
# Pretend everything is okay
return True
def url(self):
# Support URL
return ''
class HtmlNotification(NotifyBase):
# set our default notification format
@ -406,6 +435,10 @@ def test_apprise_notify_formats(tmpdir):
# Pretend everything is okay
return True
def url(self):
# Support URL
return ''
class MarkDownNotification(NotifyBase):
# set our default notification format
@ -418,6 +451,10 @@ def test_apprise_notify_formats(tmpdir):
# Pretend everything is okay
return True
def url(self):
# Support URL
return ''
# Store our notifications into our schema map
SCHEMA_MAP['text'] = TextNotification
SCHEMA_MAP['html'] = HtmlNotification

View File

@ -234,6 +234,10 @@ def test_apprise_multi_config_entries(tmpdir):
# Pretend everything is okay
return True
def url(self):
# support url()
return ''
# Store our good notification in our schema map
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
@ -357,6 +361,10 @@ def test_apprise_config_with_apprise_obj(tmpdir):
# Pretend everything is okay
return True
def url(self):
# support url()
return ''
# Store our good notification in our schema map
NOTIFY_SCHEMA_MAP['good'] = GoodNotification

View File

@ -48,6 +48,10 @@ def test_apprise_cli(tmpdir):
# Pretend everything is okay
return True
def url(self):
# Support url()
return ''
class BadNotification(NotifyBase):
def __init__(self, *args, **kwargs):
super(BadNotification, self).__init__(*args, **kwargs)
@ -56,6 +60,10 @@ def test_apprise_cli(tmpdir):
# Pretend everything is okay
return False
def url(self):
# Support url()
return ''
# Set up our notification types
SCHEMA_MAP['good'] = GoodNotification
SCHEMA_MAP['bad'] = BadNotification

View File

@ -68,6 +68,10 @@ def test_config_http(mock_post, mock_get):
# Pretend everything is okay
return True
def url(self):
# Support url() function
return ''
# Store our good notification in our schema map
SCHEMA_MAP['good'] = GoodNotification

57
test/test_logger.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 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 apprise import URLBase
# Disable logging for a cleaner testing output
import logging
def test_apprise_logger():
"""
API: Apprise() Logger
"""
# Ensure we're not running in a disabled state
logging.disable(logging.NOTSET)
# Set our log level
URLBase.logger.setLevel(logging.DEPRECATE + 1)
# Deprication will definitely not trigger
URLBase.logger.deprecate('test')
# Verbose Debugging is not on at this point
URLBase.logger.trace('test')
# Set both logging entries on
URLBase.logger.setLevel(logging.TRACE)
# Deprication will definitely trigger
URLBase.logger.deprecate('test')
# Verbose Debugging will activate
URLBase.logger.trace('test')