Browse Source

Added support for recent CPython and PyPy versions; Droped Python v2.7 Support (#680)

pull/686/head
Andreas Motl 2 years ago committed by GitHub
parent
commit
00afe4e5b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .gitignore
  2. 41
      .travis.yml
  3. 14
      Dockerfile.py27
  4. 144
      apprise/Apprise.py
  5. 1
      apprise/Apprise.pyi
  6. 6
      apprise/AppriseAsset.py
  7. 19
      apprise/AppriseAttachment.py
  8. 1
      apprise/AppriseAttachment.pyi
  9. 21
      apprise/AppriseConfig.py
  10. 1
      apprise/AppriseConfig.pyi
  11. 18
      apprise/AppriseLocale.py
  12. 51
      apprise/URLBase.py
  13. 11
      apprise/attachment/AttachBase.py
  14. 1
      apprise/attachment/AttachBase.pyi
  15. 3
      apprise/attachment/AttachHTTP.py
  16. 5
      apprise/attachment/__init__.py
  17. 13
      apprise/cli.py
  18. 14
      apprise/common.py
  19. 46
      apprise/config/ConfigBase.py
  20. 5
      apprise/config/ConfigFile.py
  21. 3
      apprise/config/ConfigHTTP.py
  22. 23
      apprise/config/__init__.py
  23. 26
      apprise/conversion.py
  24. 5
      apprise/decorators/CustomNotifyPlugin.py
  25. 2
      apprise/logger.py
  26. 3
      apprise/plugins/NotifyAppriseAPI.py
  27. 7
      apprise/plugins/NotifyBark.py
  28. 12
      apprise/plugins/NotifyBase.py
  29. 3
      apprise/plugins/NotifyBoxcar.py
  30. 13
      apprise/plugins/NotifyD7Networks.py
  31. 7
      apprise/plugins/NotifyDBus.py
  32. 2
      apprise/plugins/NotifyDapnet.py
  33. 76
      apprise/plugins/NotifyEmail.py
  34. 12
      apprise/plugins/NotifyEmby.py
  35. 5
      apprise/plugins/NotifyEnigma2.py
  36. 5
      apprise/plugins/NotifyFCM/__init__.py
  37. 19
      apprise/plugins/NotifyFCM/color.py
  38. 2
      apprise/plugins/NotifyFCM/common.py
  39. 24
      apprise/plugins/NotifyFCM/oauth.py
  40. 17
      apprise/plugins/NotifyFCM/priority.py
  41. 5
      apprise/plugins/NotifyForm.py
  42. 2
      apprise/plugins/NotifyGnome.py
  43. 2
      apprise/plugins/NotifyGotify.py
  44. 2
      apprise/plugins/NotifyGrowl.py
  45. 5
      apprise/plugins/NotifyJSON.py
  46. 2
      apprise/plugins/NotifyJoin.py
  47. 17
      apprise/plugins/NotifyLametric.py
  48. 6
      apprise/plugins/NotifyMQTT.py
  49. 4
      apprise/plugins/NotifyMSG91.py
  50. 8
      apprise/plugins/NotifyMSTeams.py
  51. 44
      apprise/plugins/NotifyMailgun.py
  52. 27
      apprise/plugins/NotifyMatrix.py
  53. 3
      apprise/plugins/NotifyMattermost.py
  54. 5
      apprise/plugins/NotifyNotica.py
  55. 12
      apprise/plugins/NotifyNotifico.py
  56. 7
      apprise/plugins/NotifyNtfy.py
  57. 4
      apprise/plugins/NotifyOpsgenie.py
  58. 4
      apprise/plugins/NotifyPagerDuty.py
  59. 5
      apprise/plugins/NotifyParsePlatform.py
  60. 2
      apprise/plugins/NotifyProwl.py
  61. 10
      apprise/plugins/NotifyPushSafer.py
  62. 7
      apprise/plugins/NotifyPushover.py
  63. 5
      apprise/plugins/NotifyReddit.py
  64. 5
      apprise/plugins/NotifyRocketChat.py
  65. 5
      apprise/plugins/NotifyRyver.py
  66. 58
      apprise/plugins/NotifySES.py
  67. 2
      apprise/plugins/NotifySMSEagle.py
  68. 42
      apprise/plugins/NotifySMTP2Go.py
  69. 5
      apprise/plugins/NotifySinch.py
  70. 2
      apprise/plugins/NotifySlack.py
  71. 13
      apprise/plugins/NotifySparkPost.py
  72. 4
      apprise/plugins/NotifyStreamlabs.py
  73. 5
      apprise/plugins/NotifySyslog.py
  74. 10
      apprise/plugins/NotifyTwist.py
  75. 5
      apprise/plugins/NotifyTwitter.py
  76. 5
      apprise/plugins/NotifyXML.py
  77. 29
      apprise/plugins/__init__.py
  78. 14
      apprise/py3compat/asyncio.py
  79. 149
      apprise/utils.py
  80. 10
      bin/apprise
  81. 1
      dev-requirements.txt
  82. 21
      packaging/README.md
  83. 48
      packaging/redhat/apprise-rhel7-support.patch
  84. 130
      packaging/redhat/python-apprise.spec
  85. 1
      requirements.txt
  86. 1
      setup.cfg
  87. 18
      setup.py
  88. 11
      test/helpers/module.py
  89. 21
      test/helpers/rest.py
  90. 96
      test/test_api.py
  91. 4
      test/test_apprise_attachments.py
  92. 27
      test/test_apprise_config.py
  93. 25
      test/test_apprise_utils.py
  94. 8
      test/test_asyncio.py
  95. 9
      test/test_attach_base.py
  96. 8
      test/test_attach_file.py
  97. 33
      test/test_attach_http.py
  98. 25
      test/test_cli.py
  99. 5
      test/test_config_base.py
  100. 17
      test/test_config_file.py
  101. Some files were not shown because too many files have changed in this diff Show More

9
.gitignore vendored

@ -26,15 +26,6 @@ sdist/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Allow RPM SPEC files despite pyInstaller ignore
!packaging/redhat/*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt

41
.travis.yml

@ -1,6 +1,6 @@
language: python
dist: xenial
dist: focal
addons:
apt:
@ -11,19 +11,23 @@ matrix:
include:
- python: "3.6"
env: TOXENV=py36
- python: "3.7.7"
- python: "3.7"
env: TOXENV=py37
- python: "3.8"
env: TOXENV=py38
- python: "3.9"
env: TOXENV=py39
- python: "3.9-dev"
env: TOXENV=py39-dev
- python: "3.10"
env: TOXENV=py310
# PyPy Environments
- python: "pypy2.7-6.0"
env: TOXENV=pypy
- python: "pypy3.5-7.0"
env: TOXENV=pypy3
- python: "pypy3.6-7.3.3"
env: TOXENV=pypy36
- python: "pypy3.7-7.3.9"
env: TOXENV=pypy37
- python: "pypy3.8-7.3.9"
env: TOXENV=pypy38
- python: "pypy3.9-7.3.9"
env: TOXENV=pypy39
# An extra environment where additional packages are not installed
- python: "3.9"
env:
@ -31,20 +35,27 @@ matrix:
install:
- pip install babel
# upgrade tox, pip, and virtualenv so Python 3.6 will build crytography:
# https://travis-ci.community/t/pip-install-cryptography-fails-on-py36/11233
- pip install -U tox pip virtualenv
# Use up-to-date versions of tox, pip, virtualenv, and wheel.
- pip install --upgrade tox pip virtualenv wheel
# cryptography 3.3 is the last one not needing a Rust toolchain. Let's use it for PyPy.
- if [[ $TOXENV == 'pypy'* ]]; then pip install "cryptography<3.4"; fi
# Install project dependencies.
- pip install codecov
- pip install -r dev-requirements.txt
- pip install -r requirements.txt
# bare installs do not include extra package dependencies
- if [[ $TOXENV != 'bare' ]]; then pip install -r all-plugin-requirements.txt; fi
# pypy and bare installs do not include dbus-python
- if [[ $TOXENV != 'bare' ]] && [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then travis_retry pip install dbus-python; fi
# Python 3.7 importlib-metadata becomes incompatible with flake8 unless we use
# a version that still supports EntryPoints.get(); tox.ini updated to not call flake; this was
# the only way around the Travis CI Builder issues
- if [[ $TOXENV == 'py37' ]]; then pip uninstall --yes flake8; fi
# Fix/workaround: Python 3.7 importlib-metadata becomes incompatible with flake8,
# unless we use a version that still supports EntryPoints.get().
# `tox.ini` has been updated to not call flake8 on Python 3.7.
- if [[ $TOXENV == 'py37' || $TOXENV == 'pypy37' ]]; then pip uninstall --yes flake8; fi
# run tests
script:

14
Dockerfile.py27

@ -1,14 +0,0 @@
# Base
FROM python:2.7-buster
RUN apt-get update && \
apt-get install -y libdbus-1-dev build-essential musl-dev bash
RUN pip install dbus-python
# Apprise Setup
COPY . /apprise
ENV PYTHONPATH /apprise
WORKDIR /apprise
RUN pip install -r requirements.txt -r dev-requirements.txt
# Catalog Construction and Wheel Building
RUN python setup.py compile_catalog bdist_wheel

144
apprise/Apprise.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import os
import six
from itertools import chain
from . import common
from .conversion import convert_between
@ -44,13 +43,13 @@ from .plugins.NotifyBase import NotifyBase
from . import plugins
from . import __version__
# Python v3+ support code made importable so it can remain backwards
# Python v3+ support code made importable, so it can remain backwards
# compatible with Python v2
# TODO: Review after dropping support for Python 2.
from . import py3compat
ASYNCIO_SUPPORT = not six.PY2
class Apprise(object):
class Apprise:
"""
Our Notification Manager
@ -124,7 +123,7 @@ class Apprise(object):
# Prepare our Asset Object
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
if isinstance(url, six.string_types):
if isinstance(url, str):
# Acquire our url tokens
results = plugins.url_to_dict(
url, secure_logging=asset.secure_logging)
@ -247,7 +246,7 @@ class Apprise(object):
# prepare default asset
asset = self.asset
if isinstance(servers, six.string_types):
if isinstance(servers, str):
# build our server list
servers = parse_urls(servers)
if len(servers) == 0:
@ -275,7 +274,7 @@ class Apprise(object):
self.servers.append(_server)
continue
elif not isinstance(_server, (six.string_types, dict)):
elif not isinstance(_server, (str, dict)):
logger.error(
"An invalid notification (type={}) was specified.".format(
type(_server)))
@ -306,7 +305,7 @@ class Apprise(object):
def find(self, tag=common.MATCH_ALL_TAG, match_always=True):
"""
Returns an list of all servers matching against the tag specified.
Returns a list of all servers matching against the tag specified.
"""
@ -347,14 +346,14 @@ class Apprise(object):
body_format=None, tag=common.MATCH_ALL_TAG, match_always=True,
attach=None, interpret_escapes=None):
"""
Send a notification to all of the plugins previously loaded.
Send a notification to all the plugins previously loaded.
If the body_format specified is NotifyFormat.MARKDOWN, it will
be converted to HTML if the Notification type expects this.
if the tag is specified (either a string or a set/list/tuple
of strings), then only the notifications flagged with that
tagged value are notified. By default all added services
tagged value are notified. By default, all added services
are notified (tag=MATCH_ALL_TAG)
This function returns True if all notifications were successfully
@ -363,60 +362,33 @@ class Apprise(object):
simply having empty configuration files that were read.
Attach can contain a list of attachment URLs. attach can also be
represented by a an AttachBase() (or list of) object(s). This
represented by an AttachBase() (or list of) object(s). This
identifies the products you wish to notify
Set interpret_escapes to True if you want to pre-escape a string
such as turning a \n into an actual new line, etc.
"""
if ASYNCIO_SUPPORT:
return py3compat.asyncio.tosync(
self.async_notify(
body, title,
notify_type=notify_type, body_format=body_format,
tag=tag, match_always=match_always, attach=attach,
interpret_escapes=interpret_escapes,
),
debug=self.debug
)
else:
try:
results = list(
self._notifyall(
Apprise._notifyhandler,
body, title,
notify_type=notify_type, body_format=body_format,
tag=tag, attach=attach,
interpret_escapes=interpret_escapes,
)
)
except TypeError:
# No notifications sent, and there was an internal error.
return False
else:
if len(results) > 0:
# All notifications sent, return False if any failed.
return all(results)
else:
# No notifications sent.
return None
return py3compat.asyncio.tosync(
self.async_notify(
body, title,
notify_type=notify_type, body_format=body_format,
tag=tag, match_always=match_always, attach=attach,
interpret_escapes=interpret_escapes,
),
debug=self.debug
)
def async_notify(self, *args, **kwargs):
"""
Send a notification to all of the plugins previously loaded, for
Send a notification to all the plugins previously loaded, for
asynchronous callers. This method is an async method that should be
awaited on, even if it is missing the async keyword in its signature.
(This is omitted to preserve syntax compatibility with Python 2.)
The arguments are identical to those of Apprise.notify(). This method
is not available in Python 2.
"""
The arguments are identical to those of Apprise.notify().
"""
try:
coroutines = list(
self._notifyall(
@ -477,7 +449,7 @@ class Apprise(object):
tag=common.MATCH_ALL_TAG, match_always=True, attach=None,
interpret_escapes=None):
"""
Creates notifications for all of the plugins loaded.
Creates notifications for all the plugins loaded.
Returns a generator that calls handler for each notification. The first
and only argument supplied to handler is the server, and the keyword
@ -496,23 +468,11 @@ class Apprise(object):
raise TypeError(msg)
try:
if six.PY2:
# Python 2.7 encoding support isn't the greatest, so we try
# to ensure that we're ALWAYS dealing with unicode characters
# prior to entrying the next part. This is especially required
# for Markdown support
if title and isinstance(title, str): # noqa: F821
title = title.decode(self.asset.encoding)
if body and isinstance(body, str): # noqa: F821
body = body.decode(self.asset.encoding)
if title and isinstance(title, bytes):
title = title.decode(self.asset.encoding)
else: # Python 3+
if title and isinstance(title, bytes): # noqa: F821
title = title.decode(self.asset.encoding)
if body and isinstance(body, bytes): # noqa: F821
body = body.decode(self.asset.encoding)
if body and isinstance(body, bytes):
body = body.decode(self.asset.encoding)
except UnicodeDecodeError:
msg = 'The content passed into Apprise was not of encoding ' \
@ -580,43 +540,12 @@ class Apprise(object):
.encode('ascii', 'backslashreplace')\
.decode('unicode-escape')
except UnicodeDecodeError: # pragma: no cover
# This occurs using a very old verion of Python 2.7
# such as the one that ships with CentOS/RedHat 7.x
# (v2.7.5).
conversion_body_map[server.notify_format] = \
conversion_body_map[server.notify_format] \
.decode('string_escape')
conversion_title_map[server.notify_format] = \
conversion_title_map[server.notify_format] \
.decode('string_escape')
except AttributeError:
# Must be of string type
msg = 'Failed to escape message body'
logger.error(msg)
raise TypeError(msg)
if six.PY2:
# Python 2.7 strings must be encoded as utf-8 for
# consistency across all platforms
if conversion_body_map[server.notify_format] and \
isinstance(
conversion_body_map[server.notify_format],
unicode): # noqa: F821
conversion_body_map[server.notify_format] = \
conversion_body_map[server.notify_format]\
.encode('utf-8')
if conversion_title_map[server.notify_format] and \
isinstance(
conversion_title_map[server.notify_format],
unicode): # noqa: F821
conversion_title_map[server.notify_format] = \
conversion_title_map[server.notify_format]\
.encode('utf-8')
yield handler(
server,
body=conversion_body_map[server.notify_format],
@ -669,12 +598,12 @@ class Apprise(object):
# Standard protocol(s) should be None or a tuple
protocols = getattr(plugin, 'protocol', None)
if isinstance(protocols, six.string_types):
if isinstance(protocols, str):
protocols = (protocols, )
# Secure protocol(s) should be None or a tuple
secure_protocols = getattr(plugin, 'secure_protocol', None)
if isinstance(secure_protocols, six.string_types):
if isinstance(secure_protocols, str):
secure_protocols = (secure_protocols, )
# Add our protocol details to our content
@ -779,15 +708,8 @@ class Apprise(object):
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an Python 3.x based 'if
statement'. True is returned if at least one service has been loaded.
"""
return len(self) > 0
def __nonzero__(self):
"""
Allows the Apprise object to be wrapped in an Python 2.x based 'if
statement'. True is returned if at least one service has been loaded.
Allows the Apprise object to be wrapped in an 'if statement'.
True is returned if at least one service has been loaded.
"""
return len(self) > 0
@ -807,7 +729,3 @@ class Apprise(object):
"""
return sum([1 if not isinstance(s, (ConfigBase, AppriseConfig))
else len(s.servers()) for s in self.servers])
if six.PY2:
del Apprise.async_notify

1
apprise/Apprise.pyi

@ -58,6 +58,5 @@ class Apprise:
def pop(self, index: int) -> ConfigBase: ...
def __getitem__(self, index: int) -> ConfigBase: ...
def __bool__(self) -> bool: ...
def __nonzero__(self) -> bool: ...
def __iter__(self) -> Iterator[ConfigBase]: ...
def __len__(self) -> int: ...

6
apprise/AppriseAsset.py

@ -33,7 +33,7 @@ from .common import NotifyType
from .utils import module_detection
class AppriseAsset(object):
class AppriseAsset:
"""
Provides a supplimentary class that can be used to provide extra
information and details that can be used by Apprise such as providing
@ -107,8 +107,8 @@ class AppriseAsset(object):
# - NotifyFormat.HTML
# - None
#
# If no format is specified (hence None), then no special pre-formating
# actions will take place during a notificaton. This has been and always
# If no format is specified (hence None), then no special pre-formatting
# actions will take place during a notification. This has been and always
# will be the default.
body_format = None

19
apprise/AppriseAttachment.py

@ -23,8 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
from . import attachment
from . import URLBase
from .AppriseAsset import AppriseAsset
@ -35,7 +33,7 @@ from .common import ATTACHMENT_SCHEMA_MAP
from .utils import GET_SCHEMA_RE
class AppriseAttachment(object):
class AppriseAttachment:
"""
Our Apprise Attachment File Manager
@ -143,7 +141,7 @@ class AppriseAttachment(object):
self.attachments.append(attachments)
return True
elif isinstance(attachments, six.string_types):
elif isinstance(attachments, str):
# Save our path
attachments = (attachments, )
@ -162,7 +160,7 @@ class AppriseAttachment(object):
return_status = False
continue
if isinstance(_attachment, six.string_types):
if isinstance(_attachment, str):
logger.debug("Loading attachment: {}".format(_attachment))
# Instantiate ourselves an object, this function throws or
# returns None if it fails
@ -296,15 +294,8 @@ class AppriseAttachment(object):
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an Python 3.x based 'if
statement'. True is returned if at least one service has been loaded.
"""
return True if self.attachments else False
def __nonzero__(self):
"""
Allows the Apprise object to be wrapped in an Python 2.x based 'if
statement'. True is returned if at least one service has been loaded.
Allows the Apprise object to be wrapped in an 'if statement'.
True is returned if at least one service has been loaded.
"""
return True if self.attachments else False

1
apprise/AppriseAttachment.pyi

@ -33,6 +33,5 @@ class AppriseAttachment:
def pop(self, index: int = ...) -> AttachBase: ...
def __getitem__(self, index: int) -> AttachBase: ...
def __bool__(self) -> bool: ...
def __nonzero__(self) -> bool: ...
def __iter__(self) -> Iterator[AttachBase]: ...
def __len__(self) -> int: ...

21
apprise/AppriseConfig.py

@ -23,8 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
from . import config
from . import ConfigBase
from . import CONFIG_FORMATS
@ -37,7 +35,7 @@ from .utils import is_exclusive_match
from .logger import logger
class AppriseConfig(object):
class AppriseConfig:
"""
Our Apprise Configuration File Manager
@ -169,7 +167,7 @@ class AppriseConfig(object):
self.configs.append(configs)
return True
elif isinstance(configs, six.string_types):
elif isinstance(configs, str):
# Save our path
configs = (configs, )
@ -187,7 +185,7 @@ class AppriseConfig(object):
self.configs.append(_config)
continue
elif not isinstance(_config, six.string_types):
elif not isinstance(_config, str):
logger.warning(
"An invalid configuration (type={}) was specified.".format(
type(_config)))
@ -241,7 +239,7 @@ class AppriseConfig(object):
# prepare default asset
asset = self.asset
if not isinstance(content, six.string_types):
if not isinstance(content, str):
logger.warning(
"An invalid configuration (type={}) was specified.".format(
type(content)))
@ -432,15 +430,8 @@ class AppriseConfig(object):
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an Python 3.x based 'if
statement'. True is returned if at least one service has been loaded.
"""
return True if self.configs else False
def __nonzero__(self):
"""
Allows the Apprise object to be wrapped in an Python 2.x based 'if
statement'. True is returned if at least one service has been loaded.
Allows the Apprise object to be wrapped in an 'if statement'.
True is returned if at least one service has been loaded.
"""
return True if self.configs else False

1
apprise/AppriseConfig.pyi

@ -44,6 +44,5 @@ class AppriseConfig:
def pop(self, index: int = ...) -> ConfigBase: ...
def __getitem__(self, index: int) -> ConfigBase: ...
def __bool__(self) -> bool: ...
def __nonzero__(self) -> bool: ...
def __iter__(self) -> Iterator[ConfigBase]: ...
def __len__(self) -> int: ...

18
apprise/AppriseLocale.py

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import ctypes
import locale
import contextlib
@ -52,18 +51,11 @@ try:
except ImportError:
# gettext isn't available; no problem, just fall back to using
# the library features without multi-language support.
try:
# Python v2.7
import __builtin__
__builtin__.__dict__['_'] = lambda x: x # pragma: no branch
import builtins
builtins.__dict__['_'] = lambda x: x # pragma: no branch
except ImportError:
# Python v3.4+
import builtins
builtins.__dict__['_'] = lambda x: x # pragma: no branch
class LazyTranslation(object):
class LazyTranslation:
"""
Doesn't translate anything until str() or unicode() references
are made.
@ -89,7 +81,7 @@ def gettext_lazy(text):
return LazyTranslation(text=text)
class AppriseLocale(object):
class AppriseLocale:
"""
A wrapper class to gettext so that we can manipulate multiple lanaguages
on the fly if required.
@ -186,7 +178,7 @@ class AppriseLocale(object):
"""
# We want to only use the 2 character version of this language
# hence en_CA becomes en, en_US becomes en.
if not isinstance(lang, six.string_types):
if not isinstance(lang, str):
if detect_fallback is False:
# no detection enabled; we're done
return None

51
apprise/URLBase.py

@ -24,21 +24,13 @@
# THE SOFTWARE.
import re
import six
from .logger import logger
from time import sleep
from datetime import datetime
from xml.sax.saxutils import escape as sax_escape
try:
# Python 2.7
from urllib import unquote as _unquote
from urllib import quote as _quote
except ImportError:
# Python 3.x
from urllib.parse import unquote as _unquote
from urllib.parse import quote as _quote
from urllib.parse import unquote as _unquote
from urllib.parse import quote as _quote
from .AppriseLocale import gettext_lazy as _
from .AppriseAsset import AppriseAsset
@ -52,7 +44,7 @@ from .utils import parse_phone_no
PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
class PrivacyMode(object):
class PrivacyMode:
# Defines different privacy modes strings can be printed as
# Astrisk sets 4 of them: e.g. ****
# This is used for passwords
@ -77,7 +69,7 @@ HTML_LOOKUP = {
}
class URLBase(object):
class URLBase:
"""
This is the base class for all URL Manipulation
"""
@ -345,7 +337,7 @@ class URLBase(object):
Returns:
str: The escaped html
"""
if not isinstance(html, six.string_types) or not html:
if not isinstance(html, str) or not html:
return ''
# Escape HTML
@ -369,7 +361,7 @@ class URLBase(object):
encoding and errors parameters specify how to decode percent-encoded
sequences.
Wrapper to Python's unquote while remaining compatible with both
Wrapper to Python's `unquote` while remaining compatible with both
Python 2 & 3 since the reference to this function changed between
versions.
@ -388,20 +380,14 @@ class URLBase(object):
if not content:
return ''
try:
# Python v3.x
return _unquote(content, encoding=encoding, errors=errors)
except TypeError:
# Python v2.7
return _unquote(content)
return _unquote(content, encoding=encoding, errors=errors)
@staticmethod
def quote(content, safe='/', encoding=None, errors=None):
""" Replaces single character non-ascii characters and URI specific
ones by their %xx code.
Wrapper to Python's unquote while remaining compatible with both
Wrapper to Python's `quote` while remaining compatible with both
Python 2 & 3 since the reference to this function changed between
versions.
@ -421,13 +407,7 @@ class URLBase(object):
if not content:
return ''
try:
# Python v3.x
return _quote(content, safe=safe, encoding=encoding, errors=errors)
except TypeError:
# Python v2.7
return _quote(content, safe=safe)
return _quote(content, safe=safe, encoding=encoding, errors=errors)
@staticmethod
def pprint(content, privacy=True, mode=PrivacyMode.Outer,
@ -456,7 +436,7 @@ class URLBase(object):
# Return 4 Asterisks
return '****'
if not isinstance(content, six.string_types) or not content:
if not isinstance(content, str) or not content:
# Nothing more to do
return ''
@ -471,7 +451,7 @@ class URLBase(object):
def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
"""Convert a mapping object or a sequence of two-element tuples
Wrapper to Python's unquote while remaining compatible with both
Wrapper to Python's `urlencode` while remaining compatible with both
Python 2 & 3 since the reference to this function changed between
versions.
@ -575,11 +555,6 @@ class URLBase(object):
# Nothing further to do
return []
except AttributeError:
# This exception ONLY gets thrown under Python v2.7 if an
# object() is passed in place of the content
return []
content = parse_phone_no(content)
return content
@ -714,13 +689,13 @@ class URLBase(object):
for key in ('protocol', 'secure_protocol'):
schema = getattr(self, key, None)
if isinstance(schema, six.string_types):
if isinstance(schema, str):
schemas.add(schema)
elif isinstance(schema, (set, list, tuple)):
# Support iterables list types
for s in schema:
if isinstance(s, six.string_types):
if isinstance(s, str):
schemas.add(s)
return schemas

11
apprise/attachment/AttachBase.py

@ -367,14 +367,7 @@ class AttachBase(URLBase):
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an Python 3.x based 'if
statement'. True is returned if our content was downloaded correctly.
"""
return True if self.path else False
def __nonzero__(self):
"""
Allows the Apprise object to be wrapped in an Python 2.x based 'if
statement'. True is returned if our content was downloaded correctly.
Allows the Apprise object to be wrapped in an based 'if statement'.
True is returned if our content was downloaded correctly.
"""
return True if self.path else False

1
apprise/attachment/AttachBase.pyi

@ -34,4 +34,3 @@ class AttachBase:
) -> Dict[str, Any]: ...
def __len__(self) -> int: ...
def __bool__(self) -> bool: ...
def __nonzero__(self) -> bool: ...

3
apprise/attachment/AttachHTTP.py

@ -25,7 +25,6 @@
import re
import os
import six
import requests
from tempfile import NamedTemporaryFile
from .AttachBase import AttachBase
@ -67,7 +66,7 @@ class AttachHTTP(AttachBase):
self.schema = 'https' if self.secure else 'http'
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.headers = {}

5
apprise/attachment/__init__.py

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import re
from os import listdir
@ -88,7 +87,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
# Load protocol(s) if defined
proto = getattr(plugin, 'protocol', None)
if isinstance(proto, six.string_types):
if isinstance(proto, str):
if proto not in ATTACHMENT_SCHEMA_MAP:
ATTACHMENT_SCHEMA_MAP[proto] = plugin
@ -100,7 +99,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
# Load secure protocol(s) if defined
protos = getattr(plugin, 'secure_protocol', None)
if isinstance(protos, six.string_types):
if isinstance(protos, str):
if protos not in ATTACHMENT_SCHEMA_MAP:
ATTACHMENT_SCHEMA_MAP[protos] = plugin

13
apprise/cli.py

@ -26,7 +26,6 @@
import click
import logging
import platform
import six
import sys
import os
import re
@ -273,10 +272,10 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# Set the theme
theme=theme,
# Async mode is only used for Python v3+ and allows a user to send
# all of their notifications asyncronously. This was made an option
# incase there are problems in the future where it's better that
# everything run sequentially/syncronously instead.
# Async mode allows a user to send all of their notifications
# asynchronously. This was made an option incase there are problems
# in the future where it is better that everything runs sequentially/
# synchronously instead.
async_mode=disable_async is not True,
# Load our plugins
@ -296,11 +295,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
for entry in plugins:
protocols = [] if not entry['protocols'] else \
[p for p in entry['protocols']
if isinstance(p, six.string_types)]
if isinstance(p, str)]
protocols.extend(
[] if not entry['secure_protocols'] else
[p for p in entry['secure_protocols']
if isinstance(p, six.string_types)])
if isinstance(p, str)])
if len(protocols) == 1:
# Simplify view by swapping {schema} with the single

14
apprise/common.py

@ -69,7 +69,7 @@ CONFIG_SCHEMA_MAP = {}
ATTACHMENT_SCHEMA_MAP = {}
class NotifyType(object):
class NotifyType:
"""
A simple mapping of notification types most commonly used with
all types of logging and notification services.
@ -88,7 +88,7 @@ NOTIFY_TYPES = (
)
class NotifyImageSize(object):
class NotifyImageSize:
"""
A list of pre-defined image sizes to make it easier to work with defined
plugins.
@ -107,7 +107,7 @@ NOTIFY_IMAGE_SIZES = (
)
class NotifyFormat(object):
class NotifyFormat:
"""
A list of pre-defined text message formats that can be passed via the
apprise library.
@ -124,7 +124,7 @@ NOTIFY_FORMATS = (
)
class OverflowMode(object):
class OverflowMode:
"""
A list of pre-defined modes of how to handle the text when it exceeds the
defined maximum message size.
@ -152,7 +152,7 @@ OVERFLOW_MODES = (
)
class ConfigFormat(object):
class ConfigFormat:
"""
A list of pre-defined config formats that can be passed via the
apprise library.
@ -175,7 +175,7 @@ CONFIG_FORMATS = (
)
class ContentIncludeMode(object):
class ContentIncludeMode:
"""
The different Content inclusion modes. All content based plugins will
have one of these associated with it.
@ -200,7 +200,7 @@ CONTENT_INCLUDE_MODES = (
)
class ContentLocation(object):
class ContentLocation:
"""
This is primarily used for handling file attachments. The idea is
to track the source of the attachment itself. We don't want

46
apprise/config/ConfigBase.py

@ -25,7 +25,6 @@
import os
import re
import six
import yaml
import time
@ -135,7 +134,7 @@ class ConfigBase(URLBase):
self.encoding = kwargs.get('encoding')
if 'format' in kwargs \
and isinstance(kwargs['format'], six.string_types):
and isinstance(kwargs['format'], str):
# Store the enforced config format
self.config_format = kwargs.get('format').lower()
@ -180,7 +179,7 @@ class ConfigBase(URLBase):
# config plugin to load the data source and return unparsed content
# None is returned if there was an error or simply no data
content = self.read(**kwargs)
if not isinstance(content, six.string_types):
if not isinstance(content, str):
# Set the time our content was cached at
self._cached_time = time.time()
@ -704,7 +703,7 @@ class ConfigBase(URLBase):
if not (hasattr(asset, k) and
isinstance(getattr(asset, k),
(bool, six.string_types))):
(bool, str))):
# We can't set a function or non-string set value
ConfigBase.logger.warning(
@ -715,7 +714,7 @@ class ConfigBase(URLBase):
# Convert to an empty string
v = ''
if (isinstance(v, (bool, six.string_types))
if (isinstance(v, (bool, str))
and isinstance(getattr(asset, k), bool)):
# If the object in the Asset is a boolean, then
@ -723,7 +722,7 @@ class ConfigBase(URLBase):
# match that.
setattr(asset, k, parse_bool(v))
elif isinstance(v, six.string_types):
elif isinstance(v, str):
# Set our asset object with the new value
setattr(asset, k, v.strip())
@ -738,7 +737,7 @@ class ConfigBase(URLBase):
global_tags = set()
tags = result.get('tag', None)
if tags and isinstance(tags, (list, tuple, six.string_types)):
if tags and isinstance(tags, (list, tuple, str)):
# Store any preset tags
global_tags = set(parse_list(tags))
@ -746,7 +745,7 @@ class ConfigBase(URLBase):
# include root directive
#
includes = result.get('include', None)
if isinstance(includes, six.string_types):
if isinstance(includes, str):
# Support a single inline string or multiple ones separated by a
# comma and/or space
includes = parse_urls(includes)
@ -758,7 +757,7 @@ class ConfigBase(URLBase):
# Iterate over each config URL
for no, url in enumerate(includes):
if isinstance(url, six.string_types):
if isinstance(url, str):
# Support a single inline string or multiple ones separated by
# a comma and/or space
configs.extend(parse_urls(url))
@ -786,7 +785,7 @@ class ConfigBase(URLBase):
loggable_url = url if not asset.secure_logging \
else cwe312_url(url)
if isinstance(url, six.string_types):
if isinstance(url, str):
# We're just a simple URL string...
schema = GET_SCHEMA_RE.match(url)
if schema is None:
@ -817,10 +816,7 @@ class ConfigBase(URLBase):
# can at least tell the end user what entries were ignored
# due to errors
if six.PY2:
it = url.iteritems()
else: # six.PY3
it = iter(url.items())
it = iter(url.items())
# Track the URL to-load
_url = None
@ -870,10 +866,7 @@ class ConfigBase(URLBase):
# We are a url string with additional unescaped options
if isinstance(entries, dict):
if six.PY2:
_url, tokens = next(url.iteritems())
else: # six.PY3
_url, tokens = next(iter(url.items()))
_url, tokens = next(iter(url.items()))
# Tags you just can't over-ride
if 'schema' in entries:
@ -1114,7 +1107,7 @@ class ConfigBase(URLBase):
r'^(choice:)?string',
meta.get('type'),
re.IGNORECASE) \
and not isinstance(value, six.string_types):
and not isinstance(value, str):
# Ensure our format is as expected
value = str(value)
@ -1167,19 +1160,8 @@ class ConfigBase(URLBase):
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an Python 3.x based 'if
statement'. True is returned if our content was downloaded correctly.
"""
if not isinstance(self._cached_servers, list):
# Generate ourselves a list of content we can pull from
self.servers()
return True if self._cached_servers else False
def __nonzero__(self):
"""
Allows the Apprise object to be wrapped in an Python 2.x based 'if
statement'. True is returned if our content was downloaded correctly.
Allows the Apprise object to be wrapped in an 'if statement'.
True is returned if our content was downloaded correctly.
"""
if not isinstance(self._cached_servers, list):
# Generate ourselves a list of content we can pull from

5
apprise/config/ConfigFile.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import io
import os
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
@ -119,9 +118,7 @@ class ConfigFile(ConfigBase):
self.throttle()
try:
# Python 3 just supports open(), however to remain compatible with
# Python 2, we use the io module
with io.open(self.path, "rt", encoding=self.encoding) as f:
with open(self.path, "rt", encoding=self.encoding) as f:
# Store our content for parsing
response = f.read()

3
apprise/config/ConfigHTTP.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import requests
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
@ -81,7 +80,7 @@ class ConfigHTTP(ConfigBase):
self.schema = 'https' if self.secure else 'http'
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.headers = {}

23
apprise/config/__init__.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
from os import listdir
from os.path import dirname
from os.path import abspath
@ -87,27 +86,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
globals()[plugin_name] = plugin
fn = getattr(plugin, 'schemas', None)
try:
schemas = set([]) if not callable(fn) else fn(plugin)
except TypeError:
# Python v2.x support where functions associated with classes
# were considered bound to them and could not be called prior
# to the classes initialization. This code can be dropped
# once Python v2.x support is dropped. The below code introduces
# replication as it already exists and is tested in
# URLBase.schemas()
schemas = set([])
for key in ('protocol', 'secure_protocol'):
schema = getattr(plugin, key, None)
if isinstance(schema, six.string_types):
schemas.add(schema)
elif isinstance(schema, (set, list, tuple)):
# Support iterables list types
for s in schema:
if isinstance(s, six.string_types):
schemas.add(s)
schemas = set([]) if not callable(fn) else fn(plugin)
# map our schema to our plugin
for schema in schemas:

26
apprise/conversion.py

@ -23,18 +23,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import six
from markdown import markdown
from .common import NotifyFormat
from .URLBase import URLBase
if six.PY2:
from HTMLParser import HTMLParser
else:
from html.parser import HTMLParser
from html.parser import HTMLParser
def convert_between(from_format, to_format, content):
@ -80,10 +74,6 @@ def html_to_text(content):
"""
parser = HTMLConverter()
if six.PY2:
# Python 2.7 requires an additional parsing to un-escape characters
content = parser.unescape(content)
parser.feed(content)
parser.close()
return parser.converted
@ -125,20 +115,6 @@ class HTMLConverter(HTMLParser, object):
string = ''.join(self._finalize(self._result))
self.converted = string.strip()
if six.PY2:
# See https://stackoverflow.com/questions/10993612/\
# how-to-remove-xa0-from-string-in-python
#
# This is required since the unescape() nbsp; with \xa0 when
# using Python 2.7
try:
self.converted = self.converted.replace(u'\xa0', u' ')
except UnicodeDecodeError:
# Python v2.7 isn't the greatest for handling unicode
self.converted = \
self.converted.decode('utf-8').replace(u'\xa0', u' ')
def _finalize(self, result):
"""
Combines and strips consecutive strings, then converts consecutive

5
apprise/decorators/CustomNotifyPlugin.py

@ -22,7 +22,6 @@
# 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 ..plugins.NotifyBase import NotifyBase
from ..utils import URL_DETAILS_RE
from ..utils import parse_url
@ -73,7 +72,7 @@ class CustomNotifyPlugin(NotifyBase):
parsed from the provided URL into our supported matrix structure.
"""
if not isinstance(url, six.string_types):
if not isinstance(url, str):
msg = 'An invalid custom notify url/schema ({}) provided in ' \
'function {}.'.format(url, send_func.__name__)
logger.warning(msg)
@ -112,7 +111,7 @@ class CustomNotifyPlugin(NotifyBase):
class CustomNotifyPluginWrapper(CustomNotifyPlugin):
# Our Service Name
service_name = name if isinstance(name, six.string_types) \
service_name = name if isinstance(name, str) \
and name else 'Custom - {}'.format(plugin_name)
# Store our matched schema

2
apprise/logger.py

@ -66,7 +66,7 @@ logging.Logger.deprecate = deprecate
logger = logging.getLogger(LOGGER_NAME)
class LogCapture(object):
class LogCapture:
"""
A class used to allow one to instantiate loggers that write to
memory for temporary purposes. e.g.:

3
apprise/plugins/NotifyAppriseAPI.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import requests
from json import dumps
@ -137,7 +136,7 @@ class NotifyAppriseAPI(NotifyBase):
super(NotifyAppriseAPI, self).__init__(**kwargs)
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.token = validate_regex(

7
apprise/plugins/NotifyBark.py

@ -25,7 +25,6 @@
#
# API: https://github.com/Finb/bark-server/blob/master/docs/API_V2.md#python
#
import six
import requests
import json
@ -76,7 +75,7 @@ BARK_SOUNDS = (
# Supported Level Entries
class NotifyBarkLevel(object):
class NotifyBarkLevel:
"""
Defines the Bark Level options
"""
@ -217,10 +216,10 @@ class NotifyBark(NotifyBase):
# Assign our category
self.category = \
category if isinstance(category, six.string_types) else None
category if isinstance(category, str) else None
# Assign our group
self.group = group if isinstance(group, six.string_types) else None
self.group = group if isinstance(group, str) else None
# Initialize device list
self.targets = parse_list(targets)

12
apprise/plugins/NotifyBase.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
from ..URLBase import URLBase
from ..common import NotifyType
@ -37,14 +36,9 @@ from ..AppriseLocale import gettext_lazy as _
from ..AppriseAttachment import AppriseAttachment
if six.PY3:
# Wrap our base with the asyncio wrapper
from ..py3compat.asyncio import AsyncNotifyBase
BASE_OBJECT = AsyncNotifyBase
else:
# Python v2.7 (backwards compatibility)
BASE_OBJECT = URLBase
# Wrap our base with the asyncio wrapper
from ..py3compat.asyncio import AsyncNotifyBase
BASE_OBJECT = AsyncNotifyBase
class NotifyBase(BASE_OBJECT):

3
apprise/plugins/NotifyBoxcar.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import requests
import hmac
from json import dumps
@ -181,7 +180,7 @@ class NotifyBoxcar(NotifyBase):
self.tags.append(DEFAULT_TAG)
targets = []
elif isinstance(targets, six.string_types):
elif isinstance(targets, str):
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
targets,
))]

13
apprise/plugins/NotifyD7Networks.py

@ -30,7 +30,6 @@
# (both user and password) from the API Details section from within your
# account profile area: https://d7networks.com/accounts/profile/
import six
import requests
import base64
from json import dumps
@ -54,7 +53,7 @@ D7NETWORKS_HTTP_ERROR_MAP = {
# Priorities
class D7SMSPriority(object):
class D7SMSPriority:
"""
D7 Networks SMS Message Priority
"""
@ -192,7 +191,7 @@ class NotifyD7Networks(NotifyBase):
# Setup our source address (if defined)
self.source = None \
if not isinstance(source, six.string_types) else source.strip()
if not isinstance(source, str) else source.strip()
if not (self.user and self.password):
msg = 'A D7 Networks user/pass was not provided.'
@ -232,10 +231,10 @@ class NotifyD7Networks(NotifyBase):
auth = '{user}:{password}'.format(
user=self.user, password=self.password)
if six.PY3:
# Python 3's versio of b64encode() expects a byte array and not
# a string. To accomodate this, we encode the content here
auth = auth.encode('utf-8')
# Python 3's versio of b64encode() expects a byte array and not
# a string. To accommodate this, we encode the content here
auth = auth.encode('utf-8')
# Prepare our headers
headers = {

7
apprise/plugins/NotifyDBus.py

@ -60,7 +60,7 @@ try:
from dbus.mainloop.glib import DBusGMainLoop
LOOP_GLIB = DBusGMainLoop()
except ImportError:
except ImportError: # pragma: no cover
# No problem
pass
@ -109,7 +109,7 @@ MAINLOOP_MAP = {
# Urgencies
class DBusUrgency(object):
class DBusUrgency:
LOW = 0
NORMAL = 1
HIGH = 2
@ -161,10 +161,11 @@ class NotifyDBus(NotifyBase):
service_url = 'http://www.freedesktop.org/Software/dbus/'
# The default protocols
# Python 3 keys() does not return a list object, it's it's own dict_keys()
# Python 3 keys() does not return a list object, it is its own dict_keys()
# object if we were to reference, we wouldn't be backwards compatible with
# Python v2. So converting the result set back into a list makes us
# compatible
# TODO: Review after dropping support for Python 2.
protocol = list(MAINLOOP_MAP.keys())
# A URL that takes you to the setup/help of the specific protocol

2
apprise/plugins/NotifyDapnet.py

@ -58,7 +58,7 @@ from ..utils import parse_list
from ..utils import parse_bool
class DapnetPriority(object):
class DapnetPriority:
NORMAL = 0
EMERGENCY = 1

76
apprise/plugins/NotifyEmail.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
@ -47,7 +46,7 @@ from ..AppriseLocale import gettext_lazy as _
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
class WebBaseLogin(object):
class WebBaseLogin:
"""
This class is just used in conjunction of the default emailers
to best formulate a login to it using the data detected
@ -60,7 +59,7 @@ class WebBaseLogin(object):
# Secure Email Modes
class SecureMailMode(object):
class SecureMailMode:
SSL = "ssl"
STARTTLS = "starttls"
@ -480,11 +479,11 @@ class NotifyEmail(NotifyBase):
# Now detect the SMTP Server
self.smtp_host = \
smtp_host if isinstance(smtp_host, six.string_types) else ''
smtp_host if isinstance(smtp_host, str) else ''
# Now detect secure mode
self.secure_mode = self.default_secure_mode \
if not isinstance(secure_mode, six.string_types) \
if not isinstance(secure_mode, str) \
else secure_mode.lower()
if self.secure_mode not in SECURE_MODES:
msg = 'The secure mode specified ({}) is invalid.'\
@ -684,38 +683,21 @@ class NotifyEmail(NotifyBase):
# Strip target out of reply_to list if in To
reply_to = (self.reply_to - set([to_addr]))
try:
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
# Format our bcc addresses to support the Name field
bcc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in bcc]
# Format our bcc addresses to support the Name field
bcc = [formataddr(
if reply_to:
# Format our reply-to addresses to support the Name field
reply_to = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in bcc]
if reply_to:
# Format our reply-to addresses to support the Name field
reply_to = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in reply_to]
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
cc = [formataddr( # pragma: no branch
(self.names.get(addr, False), addr)) for addr in cc]
# Format our bcc addresses to support the Name field
bcc = [formataddr( # pragma: no branch
(self.names.get(addr, False), addr)) for addr in bcc]
if reply_to:
# Format our reply-to addresses to support the Name field
reply_to = [formataddr( # pragma: no branch
(self.names.get(addr, False), addr))
for addr in reply_to]
for addr in reply_to]
self.logger.debug(
'Email From: {} <{}>'.format(from_name, self.from_addr))
@ -781,25 +763,11 @@ class NotifyEmail(NotifyBase):
base[k] = Header(v, self._get_charset(v))
base['Subject'] = Header(title, self._get_charset(title))
try:
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr))
base['To'] = formataddr((to_name, to_addr))
try:
base['Message-ID'] = make_msgid(domain=self.smtp_host)
except TypeError:
# Python v2.x Support (no domain keyword)
base['Message-ID'] = make_msgid()
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
base['Message-ID'] = make_msgid(domain=self.smtp_host)
base['Date'] = \
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
base['X-Application'] = self.app_id

12
apprise/plugins/NotifyEmby.py

@ -677,7 +677,7 @@ class NotifyEmby(NotifyBase):
def __del__(self):
"""
Deconstructor
Destructor
"""
try:
self.logout()
@ -694,20 +694,20 @@ class NotifyEmby(NotifyBase):
# - https://bugs.python.org/issue29288
#
# A ~similar~ issue can be identified here in the requests
# ticket system as unresolved and has provided work-arounds
# ticket system as unresolved and has provided workarounds
# - https://github.com/kennethreitz/requests/issues/3578
pass
except ImportError: # pragma: no cover
# The actual exception is `ModuleNotFoundError` however ImportError
# grants us backwards compatiblity with versions of Python older
# grants us backwards compatibility with versions of Python older
# than v3.6
# Python code that makes early calls to sys.exit() can cause
# the __del__() code to run. However in some newer versions of
# the __del__() code to run. However, in some newer versions of
# Python, this causes the `sys` library to no longer be
# available. The stack overflow also goes on to suggest that
# it's not wise to use the __del__() as a deconstructor
# it's not wise to use the __del__() as a destructor
# which is the case here.
# https://stackoverflow.com/questions/67218341/\
@ -719,6 +719,6 @@ class NotifyEmby(NotifyBase):
# /1481488/what-is-the-del-method-and-how-do-i-call-it
# At this time it seems clean to try to log out (if we can)
# but not throw any unessisary exceptions (like this one) to
# but not throw any unnecessary exceptions (like this one) to
# the end user if we don't have to.
pass

5
apprise/plugins/NotifyEnigma2.py

@ -31,7 +31,6 @@
# - https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/\
# OpenWebif-API-documentation#message
import six
import requests
from json import loads
@ -41,7 +40,7 @@ from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _
class Enigma2MessageType(object):
class Enigma2MessageType:
# Defines the Enigma2 notification types Apprise can map to
INFO = 1
WARNING = 2
@ -169,7 +168,7 @@ class NotifyEnigma2(NotifyBase):
self.timeout = self.template_args['timeout']['default']
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.headers = {}

5
apprise/plugins/NotifyFCM/__init__.py

@ -45,7 +45,6 @@
#
# If you Generate a new private key, it will provide a .json file
# You will need this in order to send an apprise messag
import six
import requests
from json import dumps
from ..NotifyBase import NotifyBase
@ -74,7 +73,7 @@ except ImportError:
# cryptography is the dependency of the .oauth library
# Create a dummy object for init() call to work
class GoogleOAuth(object):
class GoogleOAuth:
pass
@ -228,7 +227,7 @@ class NotifyFCM(NotifyBase):
else:
# Setup our mode
self.mode = NotifyFCM.template_tokens['mode']['default'] \
if not isinstance(mode, six.string_types) else mode.lower()
if not isinstance(mode, str) else mode.lower()
if self.mode and self.mode not in FCM_MODES:
msg = 'The FCM mode specified ({}) is invalid.'.format(mode)
self.logger.warning(msg)

19
apprise/plugins/NotifyFCM/color.py

@ -31,13 +31,12 @@
# https://firebase.google.com/docs/reference/fcm/rest/v1/\
# projects.messages#androidnotification
import re
import six
from ...utils import parse_bool
from ...common import NotifyType
from ...AppriseAsset import AppriseAsset
class FCMColorManager(object):
class FCMColorManager:
"""
A Simple object to accept either a boolean value
- True: Use colors provided by Apprise
@ -63,7 +62,7 @@ class FCMColorManager(object):
# Prepare our color
self.color = color
if isinstance(color, six.string_types):
if isinstance(color, str):
self.color = self.__color_rgb.match(color)
if self.color:
# Store our RGB value as #rrggbb
@ -112,16 +111,8 @@ class FCMColorManager(object):
def __bool__(self):
"""
Allows this object to be wrapped in an Python 3.x based 'if
statement'. True is returned if a color was loaded
Allows this object to be wrapped in an 'if statement'.
True is returned if a color was loaded
"""
return True if self.color is True or \
isinstance(self.color, six.string_types) else False
def __nonzero__(self):
"""
Allows this object to be wrapped in an Python 2.x based 'if
statement'. True is returned if a color was loaded
"""
return True if self.color is True or \
isinstance(self.color, six.string_types) else False
isinstance(self.color, str) else False

2
apprise/plugins/NotifyFCM/common.py

@ -22,7 +22,7 @@
# 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.
class FCMMode(object):
class FCMMode:
"""
Define the Firebase Cloud Messaging Modes
"""

24
apprise/plugins/NotifyFCM/oauth.py

@ -29,7 +29,6 @@
# 2. Click Generate New Private Key, then confirm by clicking Generate Key.
# 3. Securely store the JSON file containing the key.
import io
import requests
import base64
import json
@ -41,26 +40,13 @@ from cryptography.hazmat.primitives import asymmetric
from cryptography.exceptions import UnsupportedAlgorithm
from datetime import datetime
from datetime import timedelta
from ...logger import logger
try:
# Python 2.7
from urllib import urlencode as _urlencode
except ImportError:
# Python 3.x
from urllib.parse import urlencode as _urlencode
from json.decoder import JSONDecodeError
from urllib.parse import urlencode as _urlencode
try:
# Python 3.x
from json.decoder import JSONDecodeError
except ImportError:
# Python v2.7 Backwards Compatibility support
JSONDecodeError = ValueError
from ...logger import logger
class GoogleOAuth(object):
class GoogleOAuth:
"""
A OAuth simplified implimentation to Google's Firebase Cloud Messaging
@ -127,7 +113,7 @@ class GoogleOAuth(object):
self.__access_token_expiry = datetime.utcnow()
try:
with io.open(path, mode="r", encoding=self.encoding) as fp:
with open(path, mode="r", encoding=self.encoding) as fp:
self.content = json.loads(fp.read())
except (OSError, IOError):

17
apprise/plugins/NotifyFCM/priority.py

@ -33,7 +33,7 @@ from .common import (FCMMode, FCM_MODES)
from ...logger import logger
class NotificationPriority(object):
class NotificationPriority:
"""
Defines the Notification Priorities as described on:
https://firebase.google.com/docs/reference/fcm/rest/v1/\
@ -63,7 +63,7 @@ class NotificationPriority(object):
HIGH = 'HIGH'
class FCMPriority(object):
class FCMPriority:
"""
Defines our accepted priorites
"""
@ -87,7 +87,7 @@ FCM_PRIORITIES = (
)
class FCMPriorityManager(object):
class FCMPriorityManager:
"""
A Simple object to make it easier to work with FCM set priorities
"""
@ -242,14 +242,7 @@ class FCMPriorityManager(object):
def __bool__(self):
"""
Allows this object to be wrapped in an Python 3.x based 'if
statement'. True is returned if a priority was loaded
"""
return True if self.priority else False
def __nonzero__(self):
"""
Allows this object to be wrapped in an Python 2.x based 'if
statement'. True is returned if a priority was loaded
Allows this object to be wrapped in an 'if statement'.
True is returned if a priority was loaded
"""
return True if self.priority else False

5
apprise/plugins/NotifyForm.py

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import requests
from .NotifyBase import NotifyBase
@ -137,11 +136,11 @@ class NotifyForm(NotifyBase):
super(NotifyForm, self).__init__(**kwargs)
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = ''
self.method = self.template_args['method']['default'] \
if not isinstance(method, six.string_types) else method.upper()
if not isinstance(method, str) else method.upper()
if self.method not in METHODS:
msg = 'The method specified ({}) is invalid.'.format(method)

2
apprise/plugins/NotifyGnome.py

@ -60,7 +60,7 @@ except (ImportError, ValueError, AttributeError):
# Urgencies
class GnomeUrgency(object):
class GnomeUrgency:
LOW = 0
NORMAL = 1
HIGH = 2

2
apprise/plugins/NotifyGotify.py

@ -41,7 +41,7 @@ from ..AppriseLocale import gettext_lazy as _
# Priorities
class GotifyPriority(object):
class GotifyPriority:
LOW = 0
MODERATE = 3
NORMAL = 5

2
apprise/plugins/NotifyGrowl.py

@ -46,7 +46,7 @@ except ImportError:
# Priorities
class GrowlPriority(object):
class GrowlPriority:
LOW = -2
MODERATE = -1
NORMAL = 0

5
apprise/plugins/NotifyJSON.py

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import requests
import base64
from json import dumps
@ -139,11 +138,11 @@ class NotifyJSON(NotifyBase):
super(NotifyJSON, self).__init__(**kwargs)
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = ''
self.method = self.template_args['method']['default'] \
if not isinstance(method, six.string_types) else method.upper()
if not isinstance(method, str) else method.upper()
if self.method not in METHODS:
msg = 'The method specified ({}) is invalid.'.format(method)

2
apprise/plugins/NotifyJoin.py

@ -63,7 +63,7 @@ JOIN_IMAGE_XY = NotifyImageSize.XY_72
# Priorities
class JoinPriority(object):
class JoinPriority:
LOW = -2
MODERATE = -1
NORMAL = 0

17
apprise/plugins/NotifyLametric.py

@ -85,7 +85,6 @@
import re
import six
import requests
from json import dumps
from .NotifyBase import NotifyBase
@ -104,7 +103,7 @@ LAMETRIC_APP_ID_DETECTOR_RE = re.compile(
LAMETRIC_IS_APP_TOKEN = re.compile(r'^[a-z0-9]{80,}==$', re.I)
class LametricMode(object):
class LametricMode:
"""
Define Lametric Notification Modes
"""
@ -121,7 +120,7 @@ LAMETRIC_MODES = (
)
class LametricPriority(object):
class LametricPriority:
"""
Priority of the message
"""
@ -158,7 +157,7 @@ LAMETRIC_PRIORITIES = (
)
class LametricIconType(object):
class LametricIconType:
"""
Represents the nature of notification.
"""
@ -184,7 +183,7 @@ LAMETRIC_ICON_TYPES = (
)
class LametricSoundCategory(object):
class LametricSoundCategory:
"""
Define Sound Categories
"""
@ -192,7 +191,7 @@ class LametricSoundCategory(object):
ALARMS = "alarms"
class LametricSound(object):
class LametricSound:
"""
There are 2 categories of sounds, to make things simple we just lump them
all togther in one class object.
@ -471,7 +470,7 @@ class NotifyLametric(NotifyBase):
super(NotifyLametric, self).__init__(**kwargs)
self.mode = mode.strip().lower() \
if isinstance(mode, six.string_types) \
if isinstance(mode, str) \
else self.template_args['mode']['default']
# Default Cloud Argument
@ -543,7 +542,7 @@ class NotifyLametric(NotifyBase):
# assign our icon (if it was defined); we also eliminate
# any hashtag (#) entries that might be present
self.icon = re.search(r'[#\s]*(?P<value>.+?)\s*$', icon) \
.group('value') if isinstance(icon, six.string_types) else None
.group('value') if isinstance(icon, str) else None
if icon_type not in LAMETRIC_ICON_TYPES:
self.icon_type = self.template_args['icon_type']['default']
@ -557,7 +556,7 @@ class NotifyLametric(NotifyBase):
cycles > self.template_args['cycles']['min']) else cycles
self.sound = None
if isinstance(sound, six.string_types):
if isinstance(sound, str):
# If sound is set, get it's match
self.sound = self.sound_lookup(sound.strip().lower())
if self.sound is None:

6
apprise/plugins/NotifyMQTT.py

@ -32,7 +32,6 @@
# /blob/master/src/paho/mqtt/client.py
import ssl
import re
import six
from time import sleep
from datetime import datetime
from os.path import isfile
@ -46,11 +45,6 @@ from ..AppriseLocale import gettext_lazy as _
# Default our global support flag
NOTIFY_MQTT_SUPPORT_ENABLED = False
if six.PY2:
# handle Python v2.7 suport
class ConnectionError(Exception):
pass
try:
# 3rd party modules
import paho.mqtt.client as mqtt

4
apprise/plugins/NotifyMSG91.py

@ -41,7 +41,7 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class MSG91Route(object):
class MSG91Route:
"""
Transactional SMS Routes
route=1 for promotional, route=4 for transactional SMS.
@ -57,7 +57,7 @@ MSG91_ROUTES = (
)
class MSG91Country(object):
class MSG91Country:
"""
Optional value that can be specified on the MSG91 api
"""

8
apprise/plugins/NotifyMSTeams.py

@ -76,6 +76,7 @@
import re
import requests
import json
from json.decoder import JSONDecodeError
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
@ -88,13 +89,6 @@ from ..utils import TemplateType
from ..AppriseAttachment import AppriseAttachment
from ..AppriseLocale import gettext_lazy as _
try:
from json.decoder import JSONDecodeError
except ImportError:
# Python v2.7 Backwards Compatibility support
JSONDecodeError = ValueError
class NotifyMSTeams(NotifyBase):
"""

44
apprise/plugins/NotifyMailgun.py

@ -74,7 +74,7 @@ MAILGUN_HTTP_ERROR_MAP = {
# Priorities
class MailgunRegion(object):
class MailgunRegion:
US = 'us'
EU = 'eu'
@ -383,17 +383,9 @@ class NotifyMailgun(NotifyBase):
return False
try:
reply_to = formataddr(
(self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
reply_to = formataddr(
(self.from_name if self.from_name else False,
self.from_addr))
reply_to = formataddr(
(self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')
# Prepare our payload
payload = {
@ -461,33 +453,17 @@ class NotifyMailgun(NotifyBase):
# Strip target out of bcc list if in To
bcc = (bcc - set([to_addr[1]]))
try:
# Prepare our to
to.append(formataddr(to_addr, charset='utf-8'))
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
# Prepare our to
to.append(formataddr(to_addr))
# Prepare our `to`
to.append(formataddr(to_addr, charset='utf-8'))
# Prepare our To
payload['to'] = ','.join(to)
if cc:
try:
# Format our cc addresses to support the Name field
payload['cc'] = ','.join([formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc])
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
payload['cc'] = ','.join([formataddr( # pragma: no branch
(self.names.get(addr, False), addr))
for addr in cc])
# Format our cc addresses to support the Name field
payload['cc'] = ','.join([formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc])
# Format our bcc addresses to support the Name field
if bcc:

27
apprise/plugins/NotifyMatrix.py

@ -28,7 +28,6 @@
# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst
#
import re
import six
import requests
from markdown import markdown
from json import dumps
@ -67,7 +66,7 @@ IS_ROOM_ID = re.compile(
r'(?P<home_server>[a-z0-9.-]+))?\s*$', re.I)
class MatrixMessageType(object):
class MatrixMessageType:
"""
The Matrix Message types
"""
@ -82,7 +81,7 @@ MATRIX_MESSAGE_TYPES = (
)
class MatrixWebhookMode(object):
class MatrixWebhookMode:
# Webhook Mode is disabled
DISABLED = "off"
@ -263,7 +262,7 @@ class NotifyMatrix(NotifyBase):
# Setup our mode
self.mode = self.template_args['mode']['default'] \
if not isinstance(mode, six.string_types) else mode.lower()
if not isinstance(mode, str) else mode.lower()
if self.mode and self.mode not in MATRIX_WEBHOOK_MODES:
msg = 'The mode specified ({}) is invalid.'.format(mode)
self.logger.warning(msg)
@ -271,7 +270,7 @@ class NotifyMatrix(NotifyBase):
# Setup our message type
self.msgtype = self.template_args['msgtype']['default'] \
if not isinstance(msgtype, six.string_types) else msgtype.lower()
if not isinstance(msgtype, str) else msgtype.lower()
if self.msgtype and self.msgtype not in MATRIX_MESSAGE_TYPES:
msg = 'The msgtype specified ({}) is invalid.'.format(msgtype)
self.logger.warning(msg)
@ -411,7 +410,7 @@ class NotifyMatrix(NotifyBase):
"""
if not hasattr(self, '_re_slack_formatting_rules'):
# Prepare some one-time slack formating variables
# Prepare some one-time slack formatting variables
self._re_slack_formatting_map = {
# New lines must become the string version
@ -762,7 +761,7 @@ class NotifyMatrix(NotifyBase):
# We can't join a room if we're not logged in
return None
if not isinstance(room, six.string_types):
if not isinstance(room, str):
# Not a supported string
return None
@ -850,7 +849,7 @@ class NotifyMatrix(NotifyBase):
# We can't create a room if we're not logged in
return None
if not isinstance(room, six.string_types):
if not isinstance(room, str):
# Not a supported string
return None
@ -930,7 +929,7 @@ class NotifyMatrix(NotifyBase):
# We can't get a room id if we're not logged in
return None
if not isinstance(room, six.string_types):
if not isinstance(room, str):
# Not a supported string
return None
@ -1109,20 +1108,20 @@ class NotifyMatrix(NotifyBase):
# - https://bugs.python.org/issue29288
#
# A ~similar~ issue can be identified here in the requests
# ticket system as unresolved and has provided work-arounds
# ticket system as unresolved and has provided workarounds
# - https://github.com/kennethreitz/requests/issues/3578
pass
except ImportError: # pragma: no cover
# The actual exception is `ModuleNotFoundError` however ImportError
# grants us backwards compatiblity with versions of Python older
# grants us backwards compatibility with versions of Python older
# than v3.6
# Python code that makes early calls to sys.exit() can cause
# the __del__() code to run. However in some newer versions of
# the __del__() code to run. However, in some newer versions of
# Python, this causes the `sys` library to no longer be
# available. The stack overflow also goes on to suggest that
# it's not wise to use the __del__() as a deconstructor
# it's not wise to use the __del__() as a destructor
# which is the case here.
# https://stackoverflow.com/questions/67218341/\
@ -1134,7 +1133,7 @@ class NotifyMatrix(NotifyBase):
# /1481488/what-is-the-del-method-and-how-do-i-call-it
# At this time it seems clean to try to log out (if we can)
# but not throw any unessisary exceptions (like this one) to
# but not throw any unnecessary exceptions (like this one) to
# the end user if we don't have to.
pass

3
apprise/plugins/NotifyMattermost.py

@ -33,7 +33,6 @@
# - swap http with mmost
# - drop /hooks/ reference
import six
import requests
from json import dumps
@ -156,7 +155,7 @@ class NotifyMattermost(NotifyBase):
# our full path
self.fullpath = '' if not isinstance(
fullpath, six.string_types) else fullpath.strip()
fullpath, str) else fullpath.strip()
# Authorization Token (associated with project)
self.token = validate_regex(token)

5
apprise/plugins/NotifyNotica.py

@ -37,7 +37,6 @@
# notica://abc123
#
import re
import six
import requests
from .NotifyBase import NotifyBase
@ -47,7 +46,7 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class NoticaMode(object):
class NoticaMode:
"""
Tracks if we're accessing the notica upstream server or a locally hosted
one.
@ -176,7 +175,7 @@ class NotifyNotica(NotifyBase):
# prepare our fullpath
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.headers = {}

12
apprise/plugins/NotifyNotifico.py

@ -48,8 +48,8 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class NotificoFormat(object):
# Resets all formating
class NotificoFormat:
# Resets all formatting
Reset = '\x0F'
# Formatting
@ -59,7 +59,7 @@ class NotificoFormat(object):
BGSwap = '\x16'
class NotificoColor(object):
class NotificoColor:
# Resets Color
Reset = '\x03'
@ -248,13 +248,13 @@ class NotifyNotifico(NotifyBase):
if self.color:
# Colors were specified, make sure we capture and correctly
# allow them to exist inline in the message
# \g<1> is less ambigious than \1
body = re.sub(r'\\x03(\d{0,2})', '\x03\g<1>', body)
# \g<1> is less ambiguous than \1
body = re.sub(r'\\x03(\d{0,2})', r'\\x03\g<1>', body)
else:
# no colors specified, make sure we strip out any colors found
# to make the string read-able
body = re.sub(r'\\x03(\d{1,2}(,[0-9]{1,2})?)?', '', body)
body = re.sub(r'\\x03(\d{1,2}(,[0-9]{1,2})?)?', r'', body)
# Prepare our payload
payload = {

7
apprise/plugins/NotifyNtfy.py

@ -34,7 +34,6 @@
# ntfy://ntfy.local.domain/?priority=max
import re
import requests
import six
from json import loads
from json import dumps
from os.path import basename
@ -50,7 +49,7 @@ from ..URLBase import PrivacyMode
from ..attachment.AttachBase import AttachBase
class NtfyMode(object):
class NtfyMode:
"""
Define ntfy Notification Modes
"""
@ -67,7 +66,7 @@ NTFY_MODES = (
)
class NtfyPriority(object):
class NtfyPriority:
"""
Ntfy Priority Definitions
"""
@ -247,7 +246,7 @@ class NotifyNtfy(NotifyBase):
# Prepare our mode
self.mode = mode.strip().lower() \
if isinstance(mode, six.string_types) \
if isinstance(mode, str) \
else self.template_args['mode']['default']
if self.mode not in NTFY_MODES:

4
apprise/plugins/NotifyOpsgenie.py

@ -74,7 +74,7 @@ OPSGENIE_CATEGORIES = (
# Regions
class OpsgenieRegion(object):
class OpsgenieRegion:
US = 'us'
EU = 'eu'
@ -93,7 +93,7 @@ OPSGENIE_REGIONS = (
# Priorities
class OpsgeniePriority(object):
class OpsgeniePriority:
LOW = 1
MODERATE = 2
NORMAL = 3

4
apprise/plugins/NotifyPagerDuty.py

@ -40,7 +40,7 @@ from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
class PagerDutySeverity(object):
class PagerDutySeverity:
"""
Defines the Pager Duty Severity Levels
"""
@ -63,7 +63,7 @@ PAGERDUTY_SEVERITY_MAP = {
# Priorities
class PagerDutyRegion(object):
class PagerDutyRegion:
US = 'us'
EU = 'eu'

5
apprise/plugins/NotifyParsePlatform.py

@ -26,7 +26,6 @@
# Official API reference: https://developer.gitter.im/docs/user-resource
import re
import six
import requests
from json import dumps
@ -40,7 +39,7 @@ TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
# Priorities
class ParsePlatformDevice(object):
class ParsePlatformDevice:
# All Devices
ALL = 'all'
@ -134,7 +133,7 @@ class NotifyParsePlatform(NotifyBase):
super(NotifyParsePlatform, self).__init__(**kwargs)
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = '/'
# Application ID

2
apprise/plugins/NotifyProwl.py

@ -32,7 +32,7 @@ from ..AppriseLocale import gettext_lazy as _
# Priorities
class ProwlPriority(object):
class ProwlPriority:
LOW = -2
MODERATE = -1
NORMAL = 0

10
apprise/plugins/NotifyPushSafer.py

@ -23,8 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# We use io because it allows us to test the open() call
import io
import base64
import requests
from json import loads
@ -36,7 +34,7 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class PushSaferSound(object):
class PushSaferSound:
"""
Defines all of the supported PushSafe sounds
"""
@ -248,7 +246,7 @@ PUSHSAFER_SOUND_MAP = {
# Priorities
class PushSaferPriority(object):
class PushSaferPriority:
LOW = -2
MODERATE = -1
NORMAL = 0
@ -282,7 +280,7 @@ DEFAULT_PRIORITY = "normal"
# Vibrations
class PushSaferVibration(object):
class PushSaferVibration:
"""
Defines the acceptable vibration settings for notification
"""
@ -565,7 +563,7 @@ class NotifyPushSafer(NotifyBase):
attachment.url(privacy=True)))
try:
with io.open(attachment.path, 'rb') as f:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachment = (

7
apprise/plugins/NotifyPushover.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import requests
from .NotifyBase import NotifyBase
@ -44,7 +43,7 @@ VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
# Priorities
class PushoverPriority(object):
class PushoverPriority:
LOW = -2
MODERATE = -1
NORMAL = 0
@ -53,7 +52,7 @@ class PushoverPriority(object):
# Sounds
class PushoverSound(object):
class PushoverSound:
PUSHOVER = 'pushover'
BIKE = 'bike'
BUGLE = 'bugle'
@ -280,7 +279,7 @@ class NotifyPushover(NotifyBase):
# Setup our sound
self.sound = NotifyPushover.default_pushover_sound \
if not isinstance(sound, six.string_types) else sound.lower()
if not isinstance(sound, str) else sound.lower()
if self.sound and self.sound not in PUSHOVER_SOUNDS:
msg = 'The sound specified ({}) is invalid.'.format(sound)
self.logger.warning(msg)

5
apprise/plugins/NotifyReddit.py

@ -44,7 +44,6 @@
# - https://www.reddit.com/dev/api/
# - https://www.reddit.com/dev/api/#POST_api_submit
# - https://github.com/reddit-archive/reddit/wiki/API
import six
import requests
from json import loads
from datetime import timedelta
@ -66,7 +65,7 @@ REDDIT_HTTP_ERROR_MAP = {
}
class RedditMessageKind(object):
class RedditMessageKind:
"""
Define the kinds of messages supported
"""
@ -271,7 +270,7 @@ class NotifyReddit(NotifyBase):
self.__access_token_expiry = datetime.utcnow()
self.kind = kind.strip().lower() \
if isinstance(kind, six.string_types) \
if isinstance(kind, str) \
else self.template_args['kind']['default']
if self.kind not in REDDIT_MESSAGE_KINDS:

5
apprise/plugins/NotifyRocketChat.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import requests
from json import loads
from json import dumps
@ -54,7 +53,7 @@ RC_HTTP_ERROR_MAP = {
LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
class RocketChatAuthMode(object):
class RocketChatAuthMode:
"""
The Chat Authentication mode is detected
"""
@ -218,7 +217,7 @@ class NotifyRocketChat(NotifyBase):
# Authentication mode
self.mode = None \
if not isinstance(mode, six.string_types) \
if not isinstance(mode, str) \
else mode.lower()
if self.mode and self.mode not in ROCKETCHAT_AUTH_MODES:

5
apprise/plugins/NotifyRyver.py

@ -32,7 +32,6 @@
# These are important <---^----------------------------------------^
#
import re
import six
import requests
from json import dumps
@ -44,7 +43,7 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class RyverWebhookMode(object):
class RyverWebhookMode:
"""
Ryver supports to webhook modes
"""
@ -152,7 +151,7 @@ class NotifyRyver(NotifyBase):
# Store our webhook mode
self.mode = None \
if not isinstance(mode, six.string_types) else mode.lower()
if not isinstance(mode, str) else mode.lower()
if self.mode not in RYVER_WEBHOOK_MODES:
msg = 'The Ryver webhook mode specified ({}) is invalid.' \

58
apprise/plugins/NotifySES.py

@ -89,13 +89,7 @@ from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr
from email.header import Header
try:
# Python v3.x
from urllib.parse import quote
except ImportError:
# Python v2.x
from urllib import quote
from urllib.parse import quote
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
@ -395,26 +389,15 @@ class NotifySES(NotifyBase):
# Strip target out of bcc list if in To
bcc = (self.bcc - set([to_addr]))
try:
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
# Format our bcc addresses to support the Name field
bcc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in bcc]
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
cc = [formataddr( # pragma: no branch
(self.names.get(addr, False), addr)) for addr in cc]
# Format our bcc addresses to support the Name field
bcc = [formataddr( # pragma: no branch
(self.names.get(addr, False), addr)) for addr in bcc]
# Format our bcc addresses to support the Name field
bcc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in bcc]
self.logger.debug('Email From: {} <{}>'.format(
quote(reply_to[0], ' '),
@ -436,23 +419,14 @@ class NotifySES(NotifyBase):
# Create a Multipart container if there is an attachment
base = MIMEMultipart() if attach else content
# TODO: Deduplicate with `NotifyEmail`?
base['Subject'] = Header(title, 'utf-8')
try:
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
if reply_to[1] != self.from_addr:
base['Reply-To'] = formataddr(reply_to, charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr))
base['To'] = formataddr((to_name, to_addr))
if reply_to[1] != self.from_addr:
base['Reply-To'] = formataddr(reply_to)
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
if reply_to[1] != self.from_addr:
base['Reply-To'] = formataddr(reply_to, charset='utf-8')
base['Cc'] = ','.join(cc)
base['Date'] = \
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")

2
apprise/plugins/NotifySMSEagle.py

@ -47,7 +47,7 @@ CONTACT_REGEX = re.compile(
# Priorities
class SMSEaglePriority(object):
class SMSEaglePriority:
NORMAL = 0
HIGH = 1

42
apprise/plugins/NotifySMTP2Go.py

@ -315,17 +315,9 @@ class NotifySMTP2Go(NotifyBase):
self.logger.debug('I/O Exception: %s' % str(e))
return False
try:
sender = formataddr(
(self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
sender = formataddr(
(self.from_name if self.from_name else False,
self.from_addr))
sender = formataddr(
(self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')
# Prepare our payload
payload = {
@ -369,33 +361,17 @@ class NotifySMTP2Go(NotifyBase):
# Strip target out of bcc list if in To
bcc = (bcc - set([to_addr[1]]))
try:
# Prepare our to
to.append(formataddr(to_addr, charset='utf-8'))
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
# Prepare our to
to.append(formataddr(to_addr))
# Prepare our `to`
to.append(formataddr(to_addr, charset='utf-8'))
# Prepare our To
payload['to'] = to
if cc:
try:
# Format our cc addresses to support the Name field
payload['cc'] = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
payload['cc'] = [formataddr( # pragma: no branch
(self.names.get(addr, False), addr))
for addr in cc]
# Format our cc addresses to support the Name field
payload['cc'] = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
# Format our bcc addresses to support the Name field
if bcc:

5
apprise/plugins/NotifySinch.py

@ -33,7 +33,6 @@
# from). Activated phone numbers can be found on your dashboard here:
# - https://dashboard.sinch.com/numbers/your-numbers/numbers
#
import six
import requests
import json
@ -46,7 +45,7 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class SinchRegion(object):
class SinchRegion:
"""
Defines the Sinch Server Regions
"""
@ -192,7 +191,7 @@ class NotifySinch(NotifyBase):
# Setup our region
self.region = self.template_args['region']['default'] \
if not isinstance(region, six.string_types) else region.lower()
if not isinstance(region, str) else region.lower()
if self.region and self.region not in SINCH_REGIONS:
msg = 'The region specified ({}) is invalid.'.format(region)
self.logger.warning(msg)

2
apprise/plugins/NotifySlack.py

@ -94,7 +94,7 @@ SLACK_HTTP_ERROR_MAP = {
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
class SlackMode(object):
class SlackMode:
"""
Tracks the mode of which we're using Slack
"""

13
apprise/plugins/NotifySparkPost.py

@ -80,7 +80,7 @@ SPARKPOST_HTTP_ERROR_MAP = {
# Priorities
class SparkPostRegion(object):
class SparkPostRegion:
US = 'us'
EU = 'eu'
@ -503,14 +503,9 @@ class NotifySparkPost(NotifyBase):
# Send in batches if identified to do so
batch_size = 1 if not self.batch else self.default_batch_size
try:
reply_to = formataddr((self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
reply_to = formataddr((self.from_name if self.from_name else False,
self.from_addr))
reply_to = formataddr((self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')
payload = {
"options": {
# When set to True, an image is included with the email which

4
apprise/plugins/NotifyStreamlabs.py

@ -42,7 +42,7 @@ from ..AppriseLocale import gettext_lazy as _
# calls
class StrmlabsCall(object):
class StrmlabsCall:
ALERT = 'ALERTS'
DONATION = 'DONATIONS'
@ -55,7 +55,7 @@ STRMLABS_CALLS = (
# alerts
class StrmlabsAlert(object):
class StrmlabsAlert:
FOLLOW = 'follow'
SUBSCRIPTION = 'subscription'
DONATION = 'donation'

5
apprise/plugins/NotifySyslog.py

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import six
import syslog
import socket
@ -101,7 +100,7 @@ SYSLOG_FACILITY_RMAP = {
}
class SyslogMode(object):
class SyslogMode:
# A local query
LOCAL = "local"
@ -217,7 +216,7 @@ class NotifySyslog(NotifyBase):
self.template_tokens['facility']['default']]
self.mode = self.template_args['mode']['default'] \
if not isinstance(mode, six.string_types) else mode.lower()
if not isinstance(mode, str) else mode.lower()
if self.mode not in SYSLOG_MODES:
msg = 'The mode specified ({}) is invalid.'.format(mode)

10
apprise/plugins/NotifyTwist.py

@ -785,7 +785,7 @@ class NotifyTwist(NotifyBase):
def __del__(self):
"""
Deconstructor
Destructor
"""
try:
self.logout()
@ -808,14 +808,14 @@ class NotifyTwist(NotifyBase):
except ImportError: # pragma: no cover
# The actual exception is `ModuleNotFoundError` however ImportError
# grants us backwards compatiblity with versions of Python older
# grants us backwards compatibility with versions of Python older
# than v3.6
# Python code that makes early calls to sys.exit() can cause
# the __del__() code to run. However in some newer versions of
# the __del__() code to run. However, in some newer versions of
# Python, this causes the `sys` library to no longer be
# available. The stack overflow also goes on to suggest that
# it's not wise to use the __del__() as a deconstructor
# it's not wise to use the __del__() as a destructor
# which is the case here.
# https://stackoverflow.com/questions/67218341/\
@ -827,6 +827,6 @@ class NotifyTwist(NotifyBase):
# /1481488/what-is-the-del-method-and-how-do-i-call-it
# At this time it seems clean to try to log out (if we can)
# but not throw any unessisary exceptions (like this one) to
# but not throw any unnecessary exceptions (like this one) to
# the end user if we don't have to.
pass

5
apprise/plugins/NotifyTwitter.py

@ -26,7 +26,6 @@
# See https://developer.twitter.com/en/docs/direct-messages/\
# sending-and-receiving/api-reference/new-event.html
import re
import six
import requests
from copy import deepcopy
from datetime import datetime
@ -45,7 +44,7 @@ from ..attachment.AttachBase import AttachBase
IS_USER = re.compile(r'^\s*@?(?P<user>[A-Z0-9_]+)$', re.I)
class TwitterMessageMode(object):
class TwitterMessageMode:
"""
Twitter Message Mode
"""
@ -223,7 +222,7 @@ class NotifyTwitter(NotifyBase):
# Store our webhook mode
self.mode = None \
if not isinstance(mode, six.string_types) else mode.lower()
if not isinstance(mode, str) else mode.lower()
# Set Cache Flag
self.cache = cache

5
apprise/plugins/NotifyXML.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import requests
import base64
@ -157,11 +156,11 @@ class NotifyXML(NotifyBase):
</soapenv:Envelope>"""
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types):
if not isinstance(self.fullpath, str):
self.fullpath = ''
self.method = self.template_args['method']['default'] \
if not isinstance(method, six.string_types) else method.upper()
if not isinstance(method, str) else method.upper()
if self.method not in METHODS:
msg = 'The method specified ({}) is invalid.'.format(method)

29
apprise/plugins/__init__.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import os
import six
import re
import copy
@ -120,27 +119,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
globals()[plugin_name] = plugin
fn = getattr(plugin, 'schemas', None)
try:
schemas = set([]) if not callable(fn) else fn(plugin)
except TypeError:
# Python v2.x support where functions associated with classes
# were considered bound to them and could not be called prior
# to the classes initialization. This code can be dropped
# once Python v2.x support is dropped. The below code introduces
# replication as it already exists and is tested in
# URLBase.schemas()
schemas = set([])
for key in ('protocol', 'secure_protocol'):
schema = getattr(plugin, key, None)
if isinstance(schema, six.string_types):
schemas.add(schema)
elif isinstance(schema, (set, list, tuple)):
# Support iterables list types
for s in schema:
if isinstance(s, six.string_types):
schemas.add(s)
schemas = set([]) if not callable(fn) else fn(plugin)
# map our schema to our plugin
for schema in schemas:
@ -232,7 +211,7 @@ def _sanitize_token(tokens, default_delimiter):
if 'regex' in tokens[key]:
# Verify that we are a tuple; convert strings to tuples
if isinstance(tokens[key]['regex'], six.string_types):
if isinstance(tokens[key]['regex'], str):
# Default tuple setup
tokens[key]['regex'] = \
(tokens[key]['regex'], None)
@ -473,7 +452,7 @@ def requirements(plugin):
# Get our required packages
_req_packages = plugin.requirements.get('packages_required')
if isinstance(_req_packages, six.string_types):
if isinstance(_req_packages, str):
# Convert to list
_req_packages = [_req_packages]
@ -485,7 +464,7 @@ def requirements(plugin):
# Get our recommended packages
_opt_packages = plugin.requirements.get('packages_recommended')
if isinstance(_opt_packages, six.string_types):
if isinstance(_opt_packages, str):
# Convert to list
_opt_packages = [_opt_packages]

14
apprise/py3compat/asyncio.py

@ -36,9 +36,7 @@ ASYNCIO_RUN_SUPPORT = \
(sys.version_info.major == 3 and sys.version_info.minor >= 7)
# async reference produces a SyntaxError (E999) in Python v2.7
# For this reason we turn on the noqa flag
async def notify(coroutines): # noqa: E999
async def notify(coroutines):
"""
An async wrapper to the AsyncNotifyBase.async_notify() calls allowing us
to call gather() and collect the responses
@ -98,7 +96,7 @@ def tosync(cor, debug=False):
return loop.run_until_complete(cor)
async def toasyncwrapvalue(v): # noqa: E999
async def toasyncwrapvalue(v):
"""
Create a coroutine that, when run, returns the provided value.
"""
@ -106,7 +104,7 @@ async def toasyncwrapvalue(v): # noqa: E999
return v
async def toasyncwrap(fn): # noqa: E999
async def toasyncwrap(fn):
"""
Create a coroutine that, when run, executes the provided function.
"""
@ -119,7 +117,7 @@ class AsyncNotifyBase(URLBase):
asyncio wrapper for the NotifyBase object
"""
async def async_notify(self, *args, **kwargs): # noqa: E999
async def async_notify(self, *args, **kwargs):
"""
Async Notification Wrapper
"""
@ -131,11 +129,11 @@ class AsyncNotifyBase(URLBase):
None, partial(self.notify, *args, **kwargs))
except TypeError:
# These our our internally thrown notifications
# These are our internally thrown notifications
pass
except Exception:
# A catch all so we don't have to abort early
# A catch-all so we don't have to abort early
# just because one of our plugins has a bug in it.
logger.exception("Notification Exception")

149
apprise/utils.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import sys
import json
import contextlib
@ -36,63 +35,40 @@ from functools import reduce
from . import common
from .logger import logger
try:
# Python 2.7
from urllib import unquote
from urllib import quote
from urlparse import urlparse
from urllib import urlencode as _urlencode
from urllib.parse import unquote
from urllib.parse import quote
from urllib.parse import urlparse
from urllib.parse import urlencode as _urlencode
import imp
import importlib.util
def import_module(path, name):
"""
Load our module based on path
"""
try:
return imp.load_source(name, path)
except Exception as e:
logger.debug(
'Custom module exception raised from %s (name=%s) %s',
path, name, str(e))
return None
except ImportError:
# Python 3.5+
from urllib.parse import unquote
from urllib.parse import quote
from urllib.parse import urlparse
from urllib.parse import urlencode as _urlencode
import importlib.util
def import_module(path, name):
"""
Load our module based on path
"""
# if path.endswith('test_module_detection0/a/hook.py'):
# import pdb
# pdb.set_trace()
def import_module(path, name):
"""
Load our module based on path
"""
# if path.endswith('test_module_detection0/a/hook.py'):
# import pdb
# pdb.set_trace()
spec = importlib.util.spec_from_file_location(name, path)
try:
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec = importlib.util.spec_from_file_location(name, path)
try:
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
spec.loader.exec_module(module)
except Exception as e:
# module isn't loadable
del sys.modules[name]
module = None
except Exception as e:
# module isn't loadable
del sys.modules[name]
module = None
logger.debug(
'Custom module exception raised from %s (name=%s) %s',
path, name, str(e))
logger.debug(
'Custom module exception raised from %s (name=%s) %s',
path, name, str(e))
return module
return module
# Hash of all paths previously scanned so we don't waste effort/overhead doing
# it again
@ -226,7 +202,7 @@ UUID4_RE = re.compile(
REGEX_VALIDATE_LOOKUP = {}
class TemplateType(object):
class TemplateType:
"""
Defines the different template types we can perform parsing on
"""
@ -690,7 +666,7 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False,
"""
if not isinstance(url, six.string_types):
if not isinstance(url, str):
# Simple error checking
return None
@ -862,10 +838,10 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False,
# Re-assemble cleaned up version of the url
result['url'] = '%s://' % result['schema']
if isinstance(result.get('user'), six.string_types):
if isinstance(result.get('user'), str):
result['url'] += result['user']
if isinstance(result.get('password'), six.string_types):
if isinstance(result.get('password'), str):
result['url'] += ':%s@' % result['password']
else:
@ -900,7 +876,7 @@ def parse_bool(arg, default=False):
If the content could not be parsed, then the default is returned.
"""
if isinstance(arg, six.string_types):
if isinstance(arg, str):
# no = no - False
# of = short for off - False
# 0 = int for False
@ -930,20 +906,15 @@ def parse_bool(arg, default=False):
return bool(arg)
def parse_phone_no(*args, **kwargs):
def parse_phone_no(*args, store_unparseable=True, **kwargs):
"""
Takes a string containing phone numbers separated by comma's and/or spaces
and returns a list.
"""
# for Python 2.7 support, store_unparsable is not in the url above
# as just parse_emails(*args, store_unparseable=True) since it is
# an invalid syntax. This is the workaround to be backards compatible:
store_unparseable = kwargs.get('store_unparseable', True)
result = []
for arg in args:
if isinstance(arg, six.string_types) and arg:
if isinstance(arg, str) and arg:
_result = PHONE_NO_DETECTION_RE.findall(arg)
if _result:
result += _result
@ -967,20 +938,15 @@ def parse_phone_no(*args, **kwargs):
return result
def parse_call_sign(*args, **kwargs):
def parse_call_sign(*args, store_unparseable=True, **kwargs):
"""
Takes a string containing ham radio call signs separated by
comma and/or spacesand returns a list.
"""
# for Python 2.7 support, store_unparsable is not in the url above
# as just parse_emails(*args, store_unparseable=True) since it is
# an invalid syntax. This is the workaround to be backards compatible:
store_unparseable = kwargs.get('store_unparseable', True)
result = []
for arg in args:
if isinstance(arg, six.string_types) and arg:
if isinstance(arg, str) and arg:
_result = CALL_SIGN_DETECTION_RE.findall(arg)
if _result:
result += _result
@ -1004,20 +970,15 @@ def parse_call_sign(*args, **kwargs):
return result
def parse_emails(*args, **kwargs):
def parse_emails(*args, store_unparseable=True, **kwargs):
"""
Takes a string containing emails separated by comma's and/or spaces and
returns a list.
"""
# for Python 2.7 support, store_unparsable is not in the url above
# as just parse_emails(*args, store_unparseable=True) since it is
# an invalid syntax. This is the workaround to be backards compatible:
store_unparseable = kwargs.get('store_unparseable', True)
result = []
for arg in args:
if isinstance(arg, six.string_types) and arg:
if isinstance(arg, str) and arg:
_result = EMAIL_DETECTION_RE.findall(arg)
if _result:
result += _result
@ -1040,20 +1001,15 @@ def parse_emails(*args, **kwargs):
return result
def parse_urls(*args, **kwargs):
def parse_urls(*args, store_unparseable=True, **kwargs):
"""
Takes a string containing URLs separated by comma's and/or spaces and
returns a list.
"""
# for Python 2.7 support, store_unparsable is not in the url above
# as just parse_urls(*args, store_unparseable=True) since it is
# an invalid syntax. This is the workaround to be backards compatible:
store_unparseable = kwargs.get('store_unparseable', True)
result = []
for arg in args:
if isinstance(arg, six.string_types) and arg:
if isinstance(arg, str) and arg:
_result = URL_DETECTION_RE.findall(arg)
if _result:
result += _result
@ -1140,15 +1096,9 @@ def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
"""
# Tidy query by eliminating any records set to None
_query = {k: v for (k, v) in query.items() if v is not None}
try:
# Python v3.x
return _urlencode(
_query, doseq=doseq, safe=safe, encoding=encoding,
errors=errors)
except TypeError:
# Python v2.7
return _urlencode(_query)
return _urlencode(
_query, doseq=doseq, safe=safe, encoding=encoding,
errors=errors)
def parse_list(*args):
@ -1174,7 +1124,7 @@ def parse_list(*args):
result = []
for arg in args:
if isinstance(arg, six.string_types):
if isinstance(arg, str):
result += re.split(STRING_DELIMITERS, arg)
elif isinstance(arg, (set, list, tuple)):
@ -1183,9 +1133,10 @@ def parse_list(*args):
#
# filter() eliminates any empty entries
#
# Since Python v3 returns a filter (iterator) where-as Python v2 returned
# Since Python v3 returns a filter (iterator) whereas Python v2 returned
# a list, we need to change it into a list object to remain compatible with
# both distribution types.
# TODO: Review after dropping support for Python 2.
return sorted([x for x in filter(bool, list(set(result)))])
@ -1211,7 +1162,7 @@ def is_exclusive_match(logic, data, match_all=common.MATCH_ALL_TAG,
to all specified logic searches.
"""
if isinstance(logic, six.string_types):
if isinstance(logic, str):
# Update our logic to support our delimiters
logic = set(parse_list(logic))
@ -1234,7 +1185,7 @@ def is_exclusive_match(logic, data, match_all=common.MATCH_ALL_TAG,
# Every entry here will be or'ed with the next
for entry in logic:
if not isinstance(entry, (six.string_types, list, tuple, set)):
if not isinstance(entry, (str, list, tuple, set)):
# Garbage entry in our logic found
return False
@ -1300,7 +1251,7 @@ def validate_regex(value, regex=r'[^\s]+', flags=re.I, strip=True, fmt=None):
'x': re.X,
}
if isinstance(flags, six.string_types):
if isinstance(flags, str):
# Convert a string of regular expression flags into their
# respected integer (expected) Python values and perform
# a bit-wise or on each match found:
@ -1355,7 +1306,7 @@ def cwe312_word(word, force=False, advanced=True, threshold=5):
reached, then content is considered secret
"""
class Variance(object):
class Variance:
"""
A Simple List of Possible Character Variances
"""
@ -1368,7 +1319,7 @@ def cwe312_word(word, force=False, advanced=True, threshold=5):
# A Numerical Character (1234... etc)
NUMERIC = 'n'
if not (isinstance(word, six.string_types) and word.strip()):
if not (isinstance(word, str) and word.strip()):
# not a password if it's not something we even support
return word
@ -1594,7 +1545,7 @@ def module_detection(paths, cache=True):
module_re = re.compile(
r'^(?P<name>[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I)
if isinstance(paths, six.string_types):
if isinstance(paths, str):
paths = [paths, ]
if not paths or not isinstance(paths, (tuple, list)):

10
bin/apprise

@ -41,20 +41,20 @@ from os.path import dirname
# First assume we might be in the ./bin directory
sys.path.insert(
0, join(dirname(dirname(abspath(__file__))))) # noqa
0, join(dirname(dirname(abspath(__file__)))))
# The user might have copied the apprise script back one directory
# so support this too..
sys.path.insert(
0, join(dirname(abspath(__file__)))) # noqa
0, join(dirname(abspath(__file__))))
# We can also use the current directory we're standing in as a last
# resort
sys.path.insert(0, join(getcwd())) # noqa
sys.path.insert(0, join(getcwd()))
# Apprise tool now importable
from apprise.cli import main
import logging
from apprise.cli import main # noqa E402
import logging # noqa E402
if __name__ == "__main__":

1
dev-requirements.txt

@ -1,6 +1,5 @@
coverage
flake8
mock; python_version=='2.7'
pytest
pytest-cov
tox

21
packaging/README.md

@ -1,22 +1,17 @@
## Packaging
This directory contains any supporting files to grant usage of apprise in various distributions.
This directory contains any supporting files to grant usage of Apprise in various distributions.
### RPM Based Packages
* [EPEL](https://fedoraproject.org/wiki/EPEL) based distributions are only supported if they are of v7 or higher. This includes:
* Red Hat 7.x (or higher)
* CentOS 7.x (or higher)
* Scientific OS 7.x (or higher)
* Oracle Linux 7.x (or higher)
* [EPEL](https://fedoraproject.org/wiki/EPEL) based distributions are only supported if they are of v8 or higher. This includes:
* Red Hat 8.x (or higher)
* Scientific OS 8.x (or higher)
* Oracle Linux 8.x (or higher)
* Rocky Linux 8.x (or higher)
* Alma Linux 8.x (or higher)
* Fedora 29 (or higher)
Provided you are connected to the [EPEL repositories](https://fedoraproject.org/wiki/EPEL), the following will just work for you:
```bash
# python2-apprise: contains all you need to develop with apprise
# apprise: provides the 'apprise' administrative tool
yum install python2-apprise apprise
```
**Fedora** packaging is available right out of the box; the following will get you going on any distribution (v29 or higher):
```bash
# python3-apprise: contains all you need to develop with apprise
# apprise: provides the 'apprise' administrative tool
dnf install python3-apprise apprise

48
packaging/redhat/apprise-rhel7-support.patch

@ -1,48 +0,0 @@
diff -Naur apprise-1.0.0/test/helpers/rest.py apprise-1.0.0.patched/test/helpers/rest.py
--- apprise-1.0.0/test/helpers/rest.py 2022-07-01 11:37:34.000000000 -0400
+++ apprise-1.0.0.patched/test/helpers/rest.py 2022-08-06 13:30:29.187325564 -0400
@@ -54,8 +54,6 @@
0, 'requests.RequestException() not handled'),
requests.HTTPError(
0, 'requests.HTTPError() not handled'),
- requests.ReadTimeout(
- 0, 'requests.ReadTimeout() not handled'),
requests.TooManyRedirects(
0, 'requests.TooManyRedirects() not handled'),
)
diff -Naur apprise-1.0.0/test/test_attach_http.py apprise-1.0.0.patched/test/test_attach_http.py
--- apprise-1.0.0/test/test_attach_http.py 2022-07-15 14:52:13.000000000 -0400
+++ apprise-1.0.0.patched/test/test_attach_http.py 2022-08-06 13:30:29.188325562 -0400
@@ -51,8 +51,6 @@
0, 'requests.RequestException() not handled'),
requests.HTTPError(
0, 'requests.HTTPError() not handled'),
- requests.ReadTimeout(
- 0, 'requests.ReadTimeout() not handled'),
requests.TooManyRedirects(
0, 'requests.TooManyRedirects() not handled'),
diff -Naur apprise-1.0.0/test/test_config_http.py apprise-1.0.0.patched/test/test_config_http.py
--- apprise-1.0.0/test/test_config_http.py 2022-07-15 14:52:13.000000000 -0400
+++ apprise-1.0.0.patched/test/test_config_http.py 2022-08-06 13:30:29.188325562 -0400
@@ -46,8 +46,6 @@
0, 'requests.RequestException() not handled'),
requests.HTTPError(
0, 'requests.HTTPError() not handled'),
- requests.ReadTimeout(
- 0, 'requests.ReadTimeout() not handled'),
requests.TooManyRedirects(
0, 'requests.TooManyRedirects() not handled'),
)
diff -Naur apprise-1.0.0/test/test_plugin_glib.py apprise-1.0.0.patched/test/test_plugin_glib.py
--- apprise-1.0.0/test/test_plugin_glib.py 2022-07-15 14:52:13.000000000 -0400
+++ apprise-1.0.0.patched/test/test_plugin_glib.py 2022-08-06 13:30:29.189325559 -0400
@@ -49,7 +49,7 @@
if 'dbus' not in sys.modules:
# Environment doesn't allow for dbus
- pytest.skip("Skipping dbus-python based tests", allow_module_level=True)
+ pytest.skip("Skipping dbus-python based tests")
from dbus import DBusException # noqa E402
from apprise.plugins.NotifyDBus import DBusUrgency # noqa E402

130
packaging/redhat/python-apprise.spec

@ -21,14 +21,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###############################################################################
%global with_python2 1
%global with_python3 1
%if 0%{?fedora} || 0%{?rhel} >= 8
# Python v2 Support dropped
%global with_python2 0
%endif
%if 0%{?_module_build}
%bcond_with tests
%else
@ -36,10 +28,6 @@
%bcond_without tests
%endif
%if 0%{?rhel} && 0%{?rhel} <= 7
%global with_python3 0
%endif
%global pypi_name apprise
%global common_description %{expand: \
@ -61,85 +49,30 @@ Teams}
Name: python-%{pypi_name}
Version: 1.0.0
Release: 2%{?dist}
Release: 3%{?dist}
Summary: A simple wrapper to many popular notification services used today
License: MIT
URL: https://github.com/caronc/%{pypi_name}
Source0: %{url}/archive/v%{version}/%{pypi_name}-%{version}.tar.gz
# this patch allows version of requests that ships with RHEL v7 to
# correctly handle test coverage. It also removes reference to a
# extra check not supported in py.test in EPEL7 builds
Patch0: %{pypi_name}-rhel7-support.patch
# CentOS/Rocky 7 and 8 ship with Click v6.7 which does not support the .stdout
# RHEL/Rocky 8 ship with Click v6.7 which does not support the .stdout
# directive used in the unit testing. This patch just makes it so our package
# continues to be compatible with these linux distributions
Patch1: %{pypi_name}-click67-support.patch
Patch0: %{pypi_name}-click67-support.patch
BuildArch: noarch
%description %{common_description}
%if 0%{?with_python2}
%package -n python2-%{pypi_name}
Summary: A simple wrapper to many popular notification services used today
%{?python_provide:%python_provide python2-%{pypi_name}}
BuildRequires: python2-devel
BuildRequires: python-requests
BuildRequires: python2-requests-oauthlib
BuildRequires: python-six
BuildRequires: python2-click >= 5.0
BuildRequires: python-markdown
%if 0%{?rhel} && 0%{?rhel} <= 7
BuildRequires: python-cryptography
BuildRequires: python-babel
BuildRequires: python-yaml
%else
BuildRequires: python2-cryptography
BuildRequires: python2-babel
BuildRequires: python2-yaml
%endif
Requires: python-requests
Requires: python2-requests-oauthlib
Requires: python-six
Requires: python-markdown
%if 0%{?rhel} && 0%{?rhel} <= 7
Requires: python-cryptography
Requires: python-yaml
%else
Requires: python2-cryptography
Requires: python2-yaml
%endif
%if %{with tests}
BuildRequires: python-mock
BuildRequires: python2-pytest-runner
BuildRequires: python2-pytest
%endif
%description -n python2-%{pypi_name} %{common_description}
%endif
%package -n %{pypi_name}
Summary: Apprise CLI Tool
%if 0%{?with_python3}
Requires: python%{python3_pkgversion}-click >= 5.0
Requires: python%{python3_pkgversion}-%{pypi_name} = %{version}-%{release}
%endif
%if 0%{?with_python2}
Requires: python2-click >= 5.0
Requires: python2-%{pypi_name} = %{version}-%{release}
%endif
%description -n %{pypi_name}
An accompanied CLI tool that can be used as part of Apprise
to issue notifications from the command line to you favorite
services.
%if 0%{?with_python3}
%package -n python%{python3_pkgversion}-%{pypi_name}
Summary: A simple wrapper to many popular notification services used today
%{?python_provide:%python_provide python%{python3_pkgversion}-%{pypi_name}}
@ -148,7 +81,6 @@ BuildRequires: python%{python3_pkgversion}-devel
BuildRequires: python%{python3_pkgversion}-setuptools
BuildRequires: python%{python3_pkgversion}-requests
BuildRequires: python%{python3_pkgversion}-requests-oauthlib
BuildRequires: python%{python3_pkgversion}-six
BuildRequires: python%{python3_pkgversion}-click >= 5.0
BuildRequires: python%{python3_pkgversion}-markdown
BuildRequires: python%{python3_pkgversion}-yaml
@ -156,7 +88,6 @@ BuildRequires: python%{python3_pkgversion}-babel
BuildRequires: python%{python3_pkgversion}-cryptography
Requires: python%{python3_pkgversion}-requests
Requires: python%{python3_pkgversion}-requests-oauthlib
Requires: python%{python3_pkgversion}-six
Requires: python%{python3_pkgversion}-markdown
Requires: python%{python3_pkgversion}-cryptography
Requires: python%{python3_pkgversion}-yaml
@ -173,93 +104,52 @@ BuildRequires: python%{python3_pkgversion}-pytest-runner
%endif
%description -n python%{python3_pkgversion}-%{pypi_name} %{common_description}
%endif
%prep
%setup -q -n %{pypi_name}-%{version}
%if 0%{?rhel} && 0%{?rhel} <= 7
# rhel7 older package work-arounds
%patch0 -p1
# rhel7 doesn't like the new asyncio syntax
rm -f apprise/py3compat/asyncio.py
%endif
%if 0%{?rhel} && 0%{?rhel} <= 8
# click v6.7 unit testing support
%patch1 -p1
# Rocky/RHEL 8 click v6.7 unit testing support
%patch0 -p1
%endif
%if 0%{?rhel} >= 9
# Nothing to do under normal circumstances; this line here allows legacy
# copies of Apprise to still build against this one
find test -type f -name '*.py' -exec \
sed -i -e 's|^import mock|from unittest import mock|g' {} \;
# Do nothing
%else
# support python-mock (remain backwards compatible with older distributions)
# CentOS 8.x requires python-mock (cororlates with import ab)ve
find test -type f -name '*.py' -exec \
sed -i -e 's|^from unittest import mock|import mock|g' {} \;
%endif
%build
%if 0%{?with_python2}
%py2_build
%endif
%if 0%{?with_python3}
%py3_build
%endif
%install
%if 0%{?with_python2}
%py2_install
%endif
%if 0%{?with_python3}
%py3_install
%endif
install -p -D -T -m 0644 packaging/man/%{pypi_name}.1 \
%{buildroot}%{_mandir}/man1/%{pypi_name}.1
%if %{with tests}
%check
%if 0%{?with_python2}
LANG=C.UTF-8 PYTHONPATH=%{buildroot}%{python2_sitelib} py.test
%endif
%if 0%{?with_python3}
LANG=C.UTF-8 PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version}
%endif
%endif
%if 0%{?with_python2}
%files -n python2-%{pypi_name}
%license LICENSE
%doc README.md
%{python2_sitelib}/%{pypi_name}
%exclude %{python2_sitelib}/%{pypi_name}/cli.*
%{python2_sitelib}/*.egg-info
%endif
%if 0%{?with_python3}
%files -n python%{python3_pkgversion}-%{pypi_name}
%license LICENSE
%doc README.md
%{python3_sitelib}/%{pypi_name}
%exclude %{python3_sitelib}/%{pypi_name}/cli.*
%{python3_sitelib}/*.egg-info
%endif
%files -n %{pypi_name}
%{_bindir}/%{pypi_name}
%{_mandir}/man1/%{pypi_name}.1*
%if 0%{?with_python3}
%{python3_sitelib}/%{pypi_name}/cli.*
%endif
%if 0%{?with_python2}
%{python2_sitelib}/%{pypi_name}/cli.*
%endif
%changelog
* Fri Oct 7 2022 Chris Caron <lead2gold@gmail.com> - 1.0.0-3
- Python 2 Support dropped
* Wed Aug 31 2022 Chris Caron <lead2gold@gmail.com> - 1.0.0-2
- Rebuilt for RHEL9 Support

1
requirements.txt

@ -1,6 +1,5 @@
requests
requests-oauthlib
six
click >= 5.0
markdown
PyYAML

1
setup.cfg

@ -21,7 +21,6 @@ python_files = test/test_*.py
norecursedirs=test/helpers
filterwarnings =
once::Warning
strict = true
[extract_messages]
output-file = apprise/i18n/apprise.pot

18
setup.py

@ -27,13 +27,7 @@
import re
import os
import platform
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
from setuptools import find_packages
from setuptools import find_packages, setup
cmdclass = {}
try:
@ -94,13 +88,19 @@ setup(
'Operating System :: OS Independent',
'Natural Language :: English',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'License :: OSI Approved :: MIT License',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Libraries :: Application Frameworks',
),
entry_points={'console_scripts': console_scripts},
python_requires='>=2.7',
python_requires='>=3.6',
setup_requires=['babel', ],
)

11
test/helpers/module.py

@ -27,16 +27,7 @@ import re
import os
import sys
try:
# Python v3.4+
from importlib import reload
except ImportError:
try:
# Python v3.0-v3.3
from imp import reload
except ImportError:
# Python v2.7
pass
from importlib import reload
def module_reload(filename):

21
test/helpers/rest.py

@ -24,15 +24,8 @@
# THE SOFTWARE.
import re
import os
import six
import requests
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
from unittest import mock
from json import dumps
from random import choice
@ -51,7 +44,7 @@ import logging
logging.disable(logging.CRITICAL)
class AppriseURLTester(object):
class AppriseURLTester:
# Some exception handling we'll use
req_exceptions = (
@ -151,7 +144,7 @@ class AppriseURLTester(object):
# Allow us to force the server response text to be something other then
# the defaults
requests_response_text = meta.get('requests_response_text')
if not isinstance(requests_response_text, six.string_types):
if not isinstance(requests_response_text, str):
# Convert to string
requests_response_text = dumps(requests_response_text)
@ -242,11 +235,11 @@ class AppriseURLTester(object):
# We loaded okay; now lets make sure we can reverse
# this url
assert isinstance(obj.url(), six.string_types) is True
assert isinstance(obj.url(), str) is True
# Test url() with privacy=True
assert isinstance(
obj.url(privacy=True), six.string_types) is True
obj.url(privacy=True), str) is True
# Some Simple Invalid Instance Testing
assert instance.parse_url(None) is None
@ -299,7 +292,7 @@ class AppriseURLTester(object):
print('%s AssertionError' % url)
raise
# Tidy our object and allow any possible defined deconstructors to
# Tidy our object and allow any possible defined destructors to
# be executed.
del obj
@ -346,7 +339,7 @@ class AppriseURLTester(object):
# Allow us to force the server response text to be something other then
# the defaults
requests_response_text = meta.get('requests_response_text')
if not isinstance(requests_response_text, six.string_types):
if not isinstance(requests_response_text, str):
# Convert to string
requests_response_text = dumps(requests_response_text)

96
test/test_api.py

@ -26,16 +26,9 @@
from __future__ import print_function
import re
import sys
import six
import pytest
import requests
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
from unittest import mock
from os.path import dirname
from os.path import join
@ -58,19 +51,14 @@ from apprise.plugins import __reset_matrix
from apprise.utils import parse_list
import inspect
# Sending notifications requires the coroutines to be awaited, so we need to
# wrap the original function when mocking it.
import apprise.py3compat.asyncio as py3aio
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Sending notifications requires the coroutines to be awaited, so we need to
# wrap the original function when mocking it. But don't import for Python 2.
if not six.PY2:
import apprise.py3compat.asyncio as py3aio
else:
class py3aio:
def notify():
pass
# Attachment Directory
TEST_VAR_DIR = join(dirname(__file__), 'var')
@ -86,7 +74,6 @@ def test_apprise():
apprise_test(do_notify)
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
def test_apprise_async():
"""
API: Apprise() object asynchronous methods
@ -154,12 +141,12 @@ def apprise_test(do_notify):
assert len(a) == 2
# We can retrieve elements from our list too by reference:
assert isinstance(a[0].url(), six.string_types) is True
assert isinstance(a[0].url(), str) is True
# We can iterate over our list too:
count = 0
for o in a:
assert isinstance(o.url(), six.string_types) is True
assert isinstance(o.url(), str) is True
count += 1
# verify that we did indeed iterate over each element
assert len(a) == count
@ -547,7 +534,6 @@ def test_apprise_tagging(mock_post, mock_get):
@mock.patch('requests.get')
@mock.patch('requests.post')
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
def test_apprise_tagging_async(mock_post, mock_get):
"""
API: Apprise() object tagging functionality asynchronous methods
@ -669,7 +655,6 @@ def apprise_tagging_test(mock_post, mock_get, do_notify):
tag=[(object, ), ]) is None
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
def test_apprise_schemas(tmpdir):
"""
API: Apprise().schema() tests
@ -918,20 +903,11 @@ def test_apprise_asset(tmpdir):
must_exist=True) is not None
# Test case where we can't access the image file
if sys.version_info.major <= 2:
# Python v2.x
with mock.patch('__builtin__.open', side_effect=OSError()):
assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None
# Our content is retrivable again
assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None
else:
# Python >= v3.x
with mock.patch('builtins.open', side_effect=OSError()):
assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None
with mock.patch('builtins.open', side_effect=OSError()):
assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None
# Our content is retrivable again
assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None
# Our content is retrivable again
assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None
# Disable all image references
a = AppriseAsset(image_path_mask=False, image_url_mask=False)
@ -1376,7 +1352,7 @@ def test_apprise_details():
assert 'details' in entry['requirements']
assert 'packages_required' in entry['requirements']
assert 'packages_recommended' in entry['requirements']
assert isinstance(entry['requirements']['details'], six.string_types)
assert isinstance(entry['requirements']['details'], str)
assert isinstance(entry['requirements']['packages_required'], list)
assert isinstance(entry['requirements']['packages_recommended'], list)
@ -1403,7 +1379,7 @@ def test_apprise_details():
assert 'details' in entry['requirements']
assert 'packages_required' in entry['requirements']
assert 'packages_recommended' in entry['requirements']
assert isinstance(entry['requirements']['details'], six.string_types)
assert isinstance(entry['requirements']['details'], str)
assert isinstance(entry['requirements']['packages_required'], list)
assert isinstance(entry['requirements']['packages_recommended'], list)
@ -1498,7 +1474,7 @@ def test_apprise_details_plugin_verification():
# A Service Name MUST be defined
assert 'service_name' in entry
assert isinstance(
entry['service_name'], (six.string_types, LazyTranslation))
entry['service_name'], (str, LazyTranslation))
# Acquire our protocols
protocols = parse_list(
@ -1527,10 +1503,10 @@ def test_apprise_details_plugin_verification():
if 'alias_of' not in arg:
# Minimum requirement of an argument
assert 'name' in arg
assert isinstance(arg['name'], six.string_types)
assert isinstance(arg['name'], str)
assert 'type' in arg
assert isinstance(arg['type'], six.string_types)
assert isinstance(arg['type'], str)
assert is_valid_type_re.match(arg['type']) is not None
if 'min' in arg:
@ -1555,7 +1531,7 @@ def test_apprise_details_plugin_verification():
assert isinstance(arg['required'], bool)
if 'prefix' in arg:
assert isinstance(arg['prefix'], six.string_types)
assert isinstance(arg['prefix'], str)
if section == 'kwargs':
# The only acceptable prefix types for kwargs
assert arg['prefix'] in (':', '+', '-')
@ -1566,7 +1542,7 @@ def test_apprise_details_plugin_verification():
if 'map_to' in arg:
# must be a string
assert isinstance(arg['map_to'], six.string_types)
assert isinstance(arg['map_to'], str)
# Track our map_to object
map_to_entries.add(arg['map_to'])
@ -1601,9 +1577,9 @@ def test_apprise_details_plugin_verification():
# Regex must ALWAYS be in the format (regex, option)
assert isinstance(arg['regex'], (tuple, list))
assert len(arg['regex']) == 2
assert isinstance(arg['regex'][0], six.string_types)
assert isinstance(arg['regex'][0], str)
assert arg['regex'][1] is None or isinstance(
arg['regex'][1], six.string_types)
arg['regex'][1], str)
# Compile the regular expression to verify that it is
# valid
@ -1632,10 +1608,10 @@ def test_apprise_details_plugin_verification():
# must be a string
assert isinstance(
arg['alias_of'], (six.string_types, list, tuple, set))
arg['alias_of'], (str, list, tuple, set))
aliases = [arg['alias_of']] \
if isinstance(arg['alias_of'], six.string_types) \
if isinstance(arg['alias_of'], str) \
else arg['alias_of']
for alias_of in aliases:
@ -1687,7 +1663,7 @@ def test_apprise_details_plugin_verification():
# 'alias_of': ('apitoken', 'webtoken'),
# },
# }
if isinstance(arg['alias_of'], six.string_types):
if isinstance(arg['alias_of'], str):
assert len(entry['details'][section][key]) == 1
else: # is tuple,list, or set
assert len(entry['details'][section][key]) == 2
@ -1711,23 +1687,12 @@ def test_apprise_details_plugin_verification():
(tuple, set, list),
)
if six.PY2:
# inspect our object
# getargspec() is deprecated in Python v3
spec = inspect.getargspec(
common.NOTIFY_SCHEMA_MAP[protocols[0]].__init__)
function_args = \
(set(parse_list(spec.keywords)) - set(['kwargs'])) \
| (set(spec.args) - set(['self'])) | valid_kwargs
else:
# Python v3+ uses getfullargspec()
spec = inspect.getfullargspec(
common.NOTIFY_SCHEMA_MAP[protocols[0]].__init__)
spec = inspect.getfullargspec(
common.NOTIFY_SCHEMA_MAP[protocols[0]].__init__)
function_args = \
(set(parse_list(spec.varkw)) - set(['kwargs'])) \
| (set(spec.args) - set(['self'])) | valid_kwargs
function_args = \
(set(parse_list(spec.varkw)) - set(['kwargs'])) \
| (set(spec.args) - set(['self'])) | valid_kwargs
# Iterate over our map_to_entries and make sure that everything
# maps to a function argument
@ -1790,7 +1755,6 @@ def test_apprise_details_plugin_verification():
assert arg in defined_tokens
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
@mock.patch('requests.post')
@mock.patch('apprise.py3compat.asyncio.notify', wraps=py3aio.notify)
def test_apprise_async_mode(mock_async_notify, mock_post, tmpdir):
@ -1902,13 +1866,13 @@ def test_notify_matrix_dynamic_importing(tmpdir):
# Test no app_id
base.join('NotifyBadFile1.py').write(
"""
class NotifyBadFile1(object):
class NotifyBadFile1:
pass""")
# No class of the same name
base.join('NotifyBadFile2.py').write(
"""
class BadClassName(object):
class BadClassName:
pass""")
# Exception thrown

4
test/test_apprise_attachments.py

@ -379,13 +379,13 @@ def test_attachment_matrix_dynamic_importing(tmpdir):
# Test no app_id
base.join('AttachBadFile1.py').write(
"""
class AttachBadFile1(object):
class AttachBadFile1:
pass""")
# No class of the same name
base.join('AttachBadFile2.py').write(
"""
class BadClassName(object):
class BadClassName:
pass""")
# Exception thrown

27
test/test_apprise_config.py

@ -24,17 +24,8 @@
# THE SOFTWARE.
import sys
import six
import io
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
import pytest
from unittest import mock
from apprise import NotifyFormat
from apprise import ConfigFormat
from apprise import ContentIncludeMode
@ -109,7 +100,7 @@ def test_apprise_config(tmpdir):
assert len(ac.servers()) == 4
# Get our URL back
assert isinstance(ac[0].url(), six.string_types)
assert isinstance(ac[0].url(), str)
# Test cases where our URL is invalid
t = tmpdir.mkdir("strange-lines").join("apprise")
@ -156,13 +147,9 @@ def test_apprise_config(tmpdir):
# Iñtërnâtiônàlization Testing
windows://"""
if six.PY2:
# decode string into unicode
istr = istr.decode('utf-8')
# Write our content to our file
t = tmpdir.mkdir("internationalization").join("apprise")
with io.open(str(t), 'wb') as f:
with open(str(t), 'wb') as f:
f.write(istr.encode('latin-1'))
# Create ourselves a config object
@ -191,7 +178,7 @@ def test_apprise_config(tmpdir):
assert len(ac.servers()) == 1
# Get our URL back
assert isinstance(ac[0].url(), six.string_types)
assert isinstance(ac[0].url(), str)
# pop an entry from our list
assert isinstance(ac.pop(0), ConfigBase) is True
@ -329,7 +316,7 @@ def test_apprise_add_config():
assert len(ac.servers()) == 3
# Get our URL back
assert isinstance(ac[0].url(), six.string_types)
assert isinstance(ac[0].url(), str)
# Test invalid content
assert ac.add_config(content=object()) is False
@ -1012,13 +999,13 @@ def test_configmatrix_dynamic_importing(tmpdir):
# Test no app_id
base.join('ConfigBadFile1.py').write(
"""
class ConfigBadFile1(object):
class ConfigBadFile1:
pass""")
# No class of the same name
base.join('ConfigBadFile2.py').write(
"""
class BadClassName(object):
class BadClassName:
pass""")
# Exception thrown

25
test/test_apprise_utils.py

@ -27,15 +27,8 @@ from __future__ import print_function
import re
import os
import sys
import six
from inspect import cleandoc
try:
# Python 2.7
from urllib import unquote
except ImportError:
# Python 3.x
from urllib.parse import unquote
from urllib.parse import unquote
from apprise import utils
from apprise import common
@ -46,8 +39,6 @@ logging.disable(logging.CRITICAL)
# Ensure we don't create .pyc files for these tests
sys.dont_write_bytecode = True
# Python v2.x support requires an environment variable
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
def test_parse_qsd():
@ -1937,7 +1928,7 @@ def test_parse_list():
'.xvid', '.wmv', '.mp4',
])
class StrangeObject(object):
class StrangeObject:
def __str__(self):
return '.avi'
@ -2541,39 +2532,39 @@ def test_apply_templating():
result = utils.apply_template(
template, **{'fname': 'Chris', 'whence': 'this morning'})
assert isinstance(result, six.string_types) is True
assert isinstance(result, str) is True
assert result == "Hello Chris, How are you this morning?"
# In this example 'whence' isn't provided, so it isn't swapped
result = utils.apply_template(
template, **{'fname': 'Chris'})
assert isinstance(result, six.string_types) is True
assert isinstance(result, str) is True
assert result == "Hello Chris, How are you {{whence}}?"
# white space won't cause any ill affects:
template = "Hello {{ fname }}, How are you {{ whence}}?"
result = utils.apply_template(
template, **{'fname': 'Chris', 'whence': 'this morning'})
assert isinstance(result, six.string_types) is True
assert isinstance(result, str) is True
assert result == "Hello Chris, How are you this morning?"
# No arguments won't cause any problems
template = "Hello {{fname}}, How are you {{whence}}?"
result = utils.apply_template(template)
assert isinstance(result, six.string_types) is True
assert isinstance(result, str) is True
assert result == template
# Wrong elements are simply ignored
result = utils.apply_template(
template,
**{'fname': 'l2g', 'whence': 'this evening', 'ignore': 'me'})
assert isinstance(result, six.string_types) is True
assert isinstance(result, str) is True
assert result == "Hello l2g, How are you this evening?"
# Empty template makes things easy
result = utils.apply_template(
"", **{'fname': 'l2g', 'whence': 'this evening'})
assert isinstance(result, six.string_types) is True
assert isinstance(result, str) is True
assert result == ""
# Regular expressions are safely escapped and act as normal

8
test/test_asyncio.py

@ -24,7 +24,6 @@
# THE SOFTWARE.
from __future__ import print_function
import six
import sys
import pytest
from apprise import Apprise
@ -33,15 +32,14 @@ from apprise import NotifyFormat
from apprise.common import NOTIFY_SCHEMA_MAP
if not six.PY2:
import apprise.py3compat.asyncio as py3aio
import apprise.py3compat.asyncio as py3aio
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@pytest.mark.skipif(sys.version_info.major <= 2 or sys.version_info >= (3, 7),
@pytest.mark.skipif(sys.version_info >= (3, 7),
reason="Requires Python 3.0 to 3.6")
def test_apprise_asyncio_runtime_error():
"""
@ -112,7 +110,7 @@ def test_apprise_asyncio_runtime_error():
asyncio.set_event_loop(loop)
@pytest.mark.skipif(sys.version_info.major <= 2 or sys.version_info < (3, 7),
@pytest.mark.skipif(sys.version_info < (3, 7),
reason="Requires Python 3.7+")
def test_apprise_works_in_async_loop():
"""

9
test/test_attach_base.py

@ -23,15 +23,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
import pytest
from unittest import mock
from apprise.attachment.AttachBase import AttachBase
# Disable logging for a cleaner testing output

8
test/test_attach_file.py

@ -25,13 +25,7 @@
import re
import time
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
from unittest import mock
from os.path import dirname
from os.path import join

33
test/test_attach_http.py

@ -24,14 +24,7 @@
# THE SOFTWARE.
import re
import six
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
from unittest import mock
import requests
import mimetypes
@ -140,7 +133,7 @@ def test_attach_http(mock_get):
# Temporary path
path = join(TEST_VAR_DIR, 'apprise-test.gif')
class DummyResponse(object):
class DummyResponse:
"""
A dummy response used to manage our object
"""
@ -193,7 +186,7 @@ def test_attach_http(mock_get):
'http://user:pass@localhost/apprise.gif?dl=1&cache=300')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# Test that our extended variables are passed along
assert mock_get.call_count == 0
@ -210,7 +203,7 @@ def test_attach_http(mock_get):
'http://user:pass@localhost/apprise.gif?+key=value&cache=True')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -223,7 +216,7 @@ def test_attach_http(mock_get):
'http://localhost:3000/noname.gif?name=usethis.jpg&mime=image/jpeg')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# both mime and name over-ridden
assert re.search(r'[?&]mime=image%2Fjpeg', attachment.url())
assert re.search(r'[?&]name=usethis.jpg', attachment.url())
@ -257,7 +250,7 @@ def test_attach_http(mock_get):
results = AttachHTTP.parse_url('http://localhost')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -279,7 +272,7 @@ def test_attach_http(mock_get):
attachment = AttachHTTP(**results)
# we can not download this attachment
assert not attachment
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -298,7 +291,7 @@ def test_attach_http(mock_get):
results = AttachHTTP.parse_url('http://localhost/no-length.gif')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -326,7 +319,7 @@ def test_attach_http(mock_get):
results = AttachHTTP.parse_url('http://user@localhost/ignore-filename.gif')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -347,7 +340,7 @@ def test_attach_http(mock_get):
attachment = AttachHTTP(**results)
# we can not download this attachment
assert not attachment
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -361,7 +354,7 @@ def test_attach_http(mock_get):
results = AttachHTTP.parse_url('http://user@localhost')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -381,7 +374,7 @@ def test_attach_http(mock_get):
results = AttachHTTP.parse_url('http://localhost/invalid-length.gif')
assert isinstance(results, dict)
attachment = AttachHTTP(**results)
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None
@ -399,7 +392,7 @@ def test_attach_http(mock_get):
attachment = AttachHTTP(**results)
# we can not download this attachment
assert attachment
assert isinstance(attachment.url(), six.string_types) is True
assert isinstance(attachment.url(), str) is True
# No mime-type and/or filename over-ride was specified, so therefore it
# won't show up in the generated URL
assert re.search(r'[?&]mime=', attachment.url()) is None

25
test/test_cli.py

@ -24,13 +24,7 @@
# THE SOFTWARE.
from __future__ import print_function
import re
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
from unittest import mock
import requests
import json
@ -47,17 +41,8 @@ from apprise.utils import environ
from apprise.plugins import __load_matrix
from apprise.plugins import __reset_matrix
from importlib import reload
try:
# Python v3.4+
from importlib import reload
except ImportError:
try:
# Python v3.0-v3.3
from imp import reload
except ImportError:
# Python v2.7
pass
# Disable logging for a cleaner testing output
import logging
@ -984,8 +969,7 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
# We can find our new hook loaded in our NOTIFY_SCHEMA_MAP now...
assert 'clihook' in NOTIFY_SCHEMA_MAP
# Store our key after parsing it as a list (this makes this test backwards
# compatible with Python 2.x
# Capture our key for reference
key = [k for k in NOTIFY_CUSTOM_MODULE_MAP.keys()][0]
assert len(NOTIFY_CUSTOM_MODULE_MAP[key]['notify']) == 1
@ -1147,8 +1131,7 @@ def test_apprise_cli_plugin_loading(mock_post, tmpdir):
assert 'clihook1' in NOTIFY_SCHEMA_MAP
assert 'clihook2' in NOTIFY_SCHEMA_MAP
# Store our key after parsing it as a list (this makes this test backwards
# compatible with Python 2.x
# Capture our key for reference
key = [k for k in NOTIFY_CUSTOM_MODULE_MAP.keys()][0]
assert len(NOTIFY_CUSTOM_MODULE_MAP[key]['notify']) == 3

5
test/test_config_base.py

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import pytest
from apprise.AppriseAsset import AppriseAsset
from apprise.config.ConfigBase import ConfigBase
@ -754,9 +753,9 @@ urls:
assert asset.theme == AppriseAsset().theme
# Empty string assignment
assert isinstance(asset.image_url_mask, six.string_types) is True
assert isinstance(asset.image_url_mask, str) is True
assert asset.image_url_mask == ""
assert isinstance(asset.image_url_logo, six.string_types) is True
assert isinstance(asset.image_url_logo, str) is True
assert asset.image_url_logo == ""
# For on-lookers looking through this file; here is a perfectly formatted

17
test/test_config_file.py

@ -23,14 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
try:
# Python 3.x
from unittest import mock
except ImportError:
# Python 2.7
import mock
from unittest import mock
from apprise.config.ConfigFile import ConfigFile
from apprise.plugins.NotifyBase import NotifyBase
@ -64,7 +57,7 @@ def test_config_file(tmpdir):
# one entry added
assert len(cf) == 1
assert isinstance(cf.url(), six.string_types) is True
assert isinstance(cf.url(), str) is True
# Verify that we're using the same asset
assert cf[0].asset is asset
@ -102,8 +95,8 @@ def test_config_file(tmpdir):
'file://{}?cache=30'.format(str(t)))
assert isinstance(results, dict)
cf = ConfigFile(**results)
assert isinstance(cf.url(), six.string_types) is True
assert isinstance(cf.read(), six.string_types) is True
assert isinstance(cf.url(), str) is True
assert isinstance(cf.read(), str) is True
def test_config_file_exceptions(tmpdir):
@ -120,7 +113,7 @@ def test_config_file_exceptions(tmpdir):
cf = ConfigFile(path=str(t), format='text')
# Internal Exception would have been thrown and this would fail
with mock.patch('io.open', side_effect=OSError):
with mock.patch('builtins.open', side_effect=OSError):
assert cf.read() is None
# handle case where the file is to large for what was expected:

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save