mirror of
https://github.com/caronc/apprise.git
synced 2024-11-29 03:24:18 +01:00
Refactored async notification handling (#741)
This commit is contained in:
parent
d395d89a3b
commit
c2fdd47b9d
@ -30,7 +30,9 @@
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from . import common
|
||||
from .conversion import convert_between
|
||||
@ -50,11 +52,6 @@ from .plugins.NotifyBase import NotifyBase
|
||||
from . import plugins
|
||||
from . import __version__
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class Apprise:
|
||||
"""
|
||||
@ -376,91 +373,118 @@ class Apprise:
|
||||
such as turning a \n into an actual new line, etc.
|
||||
"""
|
||||
|
||||
return py3compat.asyncio.tosync(
|
||||
self.async_notify(
|
||||
try:
|
||||
# Process arguments and build synchronous and asynchronous calls
|
||||
# (this step can throw internal errors).
|
||||
sync_partials, async_cors = self._create_notify_calls(
|
||||
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
|
||||
interpret_escapes=interpret_escapes
|
||||
)
|
||||
|
||||
def async_notify(self, *args, **kwargs):
|
||||
except TypeError:
|
||||
# No notifications sent, and there was an internal error.
|
||||
return False
|
||||
|
||||
if not sync_partials and not async_cors:
|
||||
# Nothing to send
|
||||
return None
|
||||
|
||||
sync_result = Apprise._notify_all(*sync_partials)
|
||||
|
||||
if async_cors:
|
||||
# A single coroutine sends all asynchronous notifications in
|
||||
# parallel.
|
||||
all_cor = Apprise._async_notify_all(*async_cors)
|
||||
|
||||
try:
|
||||
# Python <3.7 automatically starts an event loop if there isn't
|
||||
# already one for the main thread.
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
except RuntimeError:
|
||||
# Python >=3.7 raises this exception if there isn't already an
|
||||
# event loop. So, we can spin up our own.
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.set_debug(self.debug)
|
||||
|
||||
# Run the coroutine and wait for the result.
|
||||
async_result = loop.run_until_complete(all_cor)
|
||||
|
||||
# Clean up the loop.
|
||||
loop.close()
|
||||
asyncio.set_event_loop(None)
|
||||
|
||||
else:
|
||||
old_debug = loop.get_debug()
|
||||
loop.set_debug(self.debug)
|
||||
|
||||
# Run the coroutine and wait for the result.
|
||||
async_result = loop.run_until_complete(all_cor)
|
||||
|
||||
loop.set_debug(old_debug)
|
||||
|
||||
else:
|
||||
async_result = True
|
||||
|
||||
return sync_result and async_result
|
||||
|
||||
async def async_notify(self, *args, **kwargs):
|
||||
"""
|
||||
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.)
|
||||
asynchronous callers.
|
||||
|
||||
The arguments are identical to those of Apprise.notify().
|
||||
|
||||
"""
|
||||
try:
|
||||
coroutines = list(
|
||||
self._notifyall(
|
||||
Apprise._notifyhandlerasync, *args, **kwargs))
|
||||
# Process arguments and build synchronous and asynchronous calls
|
||||
# (this step can throw internal errors).
|
||||
sync_partials, async_cors = self._create_notify_calls(
|
||||
*args, **kwargs)
|
||||
|
||||
except TypeError:
|
||||
# No notifications sent, and there was an internal error.
|
||||
return py3compat.asyncio.toasyncwrapvalue(False)
|
||||
|
||||
else:
|
||||
if len(coroutines) > 0:
|
||||
# All notifications sent, return False if any failed.
|
||||
return py3compat.asyncio.notify(coroutines)
|
||||
|
||||
else:
|
||||
# No notifications sent.
|
||||
return py3compat.asyncio.toasyncwrapvalue(None)
|
||||
|
||||
@staticmethod
|
||||
def _notifyhandler(server, **kwargs):
|
||||
"""
|
||||
The synchronous notification sender. Returns True if the notification
|
||||
sent successfully.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Send notification
|
||||
return server.notify(**kwargs)
|
||||
|
||||
except TypeError:
|
||||
# These our our internally thrown notifications
|
||||
return False
|
||||
|
||||
except Exception:
|
||||
# A catch all so we don't have to abort early
|
||||
# just because one of our plugins has a bug in it.
|
||||
logger.exception("Unhandled Notification Exception")
|
||||
return False
|
||||
if not sync_partials and not async_cors:
|
||||
# Nothing to send
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _notifyhandlerasync(server, **kwargs):
|
||||
"""
|
||||
The asynchronous notification sender. Returns a coroutine that yields
|
||||
True if the notification sent successfully.
|
||||
"""
|
||||
sync_result = Apprise._notify_all(*sync_partials)
|
||||
async_result = await Apprise._async_notify_all(*async_cors)
|
||||
return sync_result and async_result
|
||||
|
||||
if server.asset.async_mode:
|
||||
return server.async_notify(**kwargs)
|
||||
|
||||
else:
|
||||
# Send the notification immediately, and wrap the result in a
|
||||
# coroutine.
|
||||
status = Apprise._notifyhandler(server, **kwargs)
|
||||
return py3compat.asyncio.toasyncwrapvalue(status)
|
||||
|
||||
def _notifyall(self, handler, body, title='',
|
||||
notify_type=common.NotifyType.INFO, body_format=None,
|
||||
tag=common.MATCH_ALL_TAG, match_always=True, attach=None,
|
||||
interpret_escapes=None):
|
||||
def _create_notify_calls(self, *args, **kwargs):
|
||||
"""
|
||||
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
|
||||
arguments are exactly as they would be passed to server.notify().
|
||||
Returns a list of synchronous calls (partial functions with no
|
||||
arguments required) for plugins with async disabled and a list of
|
||||
asynchronous calls (coroutines) for plugins with async enabled.
|
||||
"""
|
||||
|
||||
all_calls = list(self._create_notify_gen(*args, **kwargs))
|
||||
|
||||
# Split into synchronous partials and asynchronous coroutines.
|
||||
sync_partials, async_cors = [], []
|
||||
for notify in all_calls:
|
||||
if asyncio.iscoroutine(notify):
|
||||
async_cors.append(notify)
|
||||
else:
|
||||
sync_partials.append(notify)
|
||||
|
||||
return sync_partials, async_cors
|
||||
|
||||
def _create_notify_gen(self, body, title='',
|
||||
notify_type=common.NotifyType.INFO,
|
||||
body_format=None, tag=common.MATCH_ALL_TAG,
|
||||
match_always=True, attach=None,
|
||||
interpret_escapes=None):
|
||||
"""
|
||||
Internal generator function for _create_notify_calls().
|
||||
"""
|
||||
|
||||
if len(self) == 0:
|
||||
@ -553,14 +577,67 @@ class Apprise:
|
||||
logger.error(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
yield handler(
|
||||
server,
|
||||
kwargs = dict(
|
||||
body=conversion_body_map[server.notify_format],
|
||||
title=conversion_title_map[server.notify_format],
|
||||
notify_type=notify_type,
|
||||
attach=attach,
|
||||
body_format=body_format,
|
||||
body_format=body_format
|
||||
)
|
||||
if server.asset.async_mode:
|
||||
yield server.async_notify(**kwargs)
|
||||
else:
|
||||
yield partial(server.notify, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _notify_all(*partials):
|
||||
"""
|
||||
Process a list of synchronous notify() calls.
|
||||
"""
|
||||
|
||||
success = True
|
||||
|
||||
for notify in partials:
|
||||
try:
|
||||
# Send notification
|
||||
result = notify()
|
||||
success = success and result
|
||||
|
||||
except TypeError:
|
||||
# These are our internally thrown notifications.
|
||||
success = False
|
||||
|
||||
except Exception:
|
||||
# A catch all so we don't have to abort early
|
||||
# just because one of our plugins has a bug in it.
|
||||
logger.exception("Unhandled Notification Exception")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
async def _async_notify_all(*cors):
|
||||
"""
|
||||
Process a list of asynchronous async_notify() calls.
|
||||
"""
|
||||
|
||||
# Create log entry
|
||||
logger.info('Notifying %d service(s) asynchronously.', len(cors))
|
||||
|
||||
results = await asyncio.gather(*cors, return_exceptions=True)
|
||||
|
||||
if any(isinstance(status, Exception)
|
||||
and not isinstance(status, TypeError) for status in results):
|
||||
# A catch all so we don't have to abort early just because
|
||||
# one of our plugins has a bug in it.
|
||||
logger.exception("Unhandled Notification Exception")
|
||||
return False
|
||||
|
||||
if any(isinstance(status, TypeError) for status in results):
|
||||
# These are our internally thrown notifications.
|
||||
return False
|
||||
|
||||
return all(results)
|
||||
|
||||
def details(self, lang=None, show_requirements=False, show_disabled=False):
|
||||
"""
|
||||
|
@ -30,7 +30,9 @@
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
from ..URLBase import URLBase
|
||||
from ..common import NotifyType
|
||||
@ -43,12 +45,7 @@ from ..AppriseLocale import gettext_lazy as _
|
||||
from ..AppriseAttachment import AppriseAttachment
|
||||
|
||||
|
||||
# Wrap our base with the asyncio wrapper
|
||||
from ..py3compat.asyncio import AsyncNotifyBase
|
||||
BASE_OBJECT = AsyncNotifyBase
|
||||
|
||||
|
||||
class NotifyBase(BASE_OBJECT):
|
||||
class NotifyBase(URLBase):
|
||||
"""
|
||||
This is the base class for all notification services
|
||||
"""
|
||||
@ -274,19 +271,64 @@ class NotifyBase(BASE_OBJECT):
|
||||
color_type=color_type,
|
||||
)
|
||||
|
||||
def notify(self, body, title=None, notify_type=NotifyType.INFO,
|
||||
overflow=None, attach=None, body_format=None, **kwargs):
|
||||
def notify(self, *args, **kwargs):
|
||||
"""
|
||||
Performs notification
|
||||
"""
|
||||
try:
|
||||
# Build a list of dictionaries that can be used to call send().
|
||||
send_calls = list(self._build_send_calls(*args, **kwargs))
|
||||
|
||||
except TypeError:
|
||||
# Internal error
|
||||
return False
|
||||
|
||||
else:
|
||||
# Loop through each call, one at a time. (Use a list rather than a
|
||||
# generator to call all the partials, even in case of a failure.)
|
||||
the_calls = [self.send(**kwargs2) for kwargs2 in send_calls]
|
||||
return all(the_calls)
|
||||
|
||||
async def async_notify(self, *args, **kwargs):
|
||||
"""
|
||||
Performs notification for asynchronous callers
|
||||
"""
|
||||
try:
|
||||
# Build a list of dictionaries that can be used to call send().
|
||||
send_calls = list(self._build_send_calls(*args, **kwargs))
|
||||
|
||||
except TypeError:
|
||||
# Internal error
|
||||
return False
|
||||
|
||||
else:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Wrap each call in a coroutine that uses the default executor.
|
||||
# TODO: In the future, allow plugins to supply a native
|
||||
# async_send() method.
|
||||
async def do_send(**kwargs2):
|
||||
send = partial(self.send, **kwargs2)
|
||||
result = await loop.run_in_executor(None, send)
|
||||
return result
|
||||
|
||||
# gather() all calls in parallel.
|
||||
the_cors = (do_send(**kwargs2) for kwargs2 in send_calls)
|
||||
return all(await asyncio.gather(*the_cors))
|
||||
|
||||
def _build_send_calls(self, body, title=None,
|
||||
notify_type=NotifyType.INFO, overflow=None,
|
||||
attach=None, body_format=None, **kwargs):
|
||||
"""
|
||||
Get a list of dictionaries that can be used to call send() or
|
||||
(in the future) async_send().
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
# Deny notifications issued to services that are disabled
|
||||
self.logger.warning(
|
||||
"{} is currently disabled on this system.".format(
|
||||
self.service_name))
|
||||
return False
|
||||
msg = f"{self.service_name} is currently disabled on this system."
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prepare attachments if required
|
||||
if attach is not None and not isinstance(attach, AppriseAttachment):
|
||||
@ -295,7 +337,7 @@ class NotifyBase(BASE_OBJECT):
|
||||
|
||||
except TypeError:
|
||||
# bad attachments
|
||||
return False
|
||||
raise
|
||||
|
||||
# Handle situations where the title is None
|
||||
title = '' if not title else title
|
||||
@ -306,14 +348,11 @@ class NotifyBase(BASE_OBJECT):
|
||||
body_format=body_format):
|
||||
|
||||
# Send notification
|
||||
if not self.send(body=chunk['body'], title=chunk['title'],
|
||||
yield dict(
|
||||
body=chunk['body'], title=chunk['title'],
|
||||
notify_type=notify_type, attach=attach,
|
||||
body_format=body_format):
|
||||
|
||||
# Toggle our return status flag
|
||||
return False
|
||||
|
||||
return True
|
||||
body_format=body_format
|
||||
)
|
||||
|
||||
def _apply_overflow(self, body, title=None, overflow=None,
|
||||
body_format=None):
|
||||
|
@ -1,147 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from ..URLBase import URLBase
|
||||
from ..logger import logger
|
||||
|
||||
|
||||
# A global flag that tracks if we are Python v3.7 or higher
|
||||
ASYNCIO_RUN_SUPPORT = \
|
||||
sys.version_info.major > 3 or \
|
||||
(sys.version_info.major == 3 and sys.version_info.minor >= 7)
|
||||
|
||||
|
||||
async def notify(coroutines):
|
||||
"""
|
||||
An async wrapper to the AsyncNotifyBase.async_notify() calls allowing us
|
||||
to call gather() and collect the responses
|
||||
"""
|
||||
|
||||
# Create log entry
|
||||
logger.info(
|
||||
'Notifying {} service(s) asynchronously.'.format(len(coroutines)))
|
||||
|
||||
results = await asyncio.gather(*coroutines, return_exceptions=True)
|
||||
|
||||
# Returns True if all notifications succeeded, otherwise False is
|
||||
# returned.
|
||||
failed = any(not status or isinstance(status, Exception)
|
||||
for status in results)
|
||||
return not failed
|
||||
|
||||
|
||||
def tosync(cor, debug=False):
|
||||
"""
|
||||
Await a coroutine from non-async code.
|
||||
"""
|
||||
|
||||
if ASYNCIO_RUN_SUPPORT:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
except RuntimeError:
|
||||
# There is no existing event loop, so we can start our own.
|
||||
return asyncio.run(cor, debug=debug)
|
||||
|
||||
else:
|
||||
# Enable debug mode
|
||||
loop.set_debug(debug)
|
||||
|
||||
# Run the coroutine and wait for the result.
|
||||
task = loop.create_task(cor)
|
||||
return asyncio.ensure_future(task, loop=loop)
|
||||
|
||||
else:
|
||||
# The Deprecated Way (<= Python v3.6)
|
||||
try:
|
||||
# acquire access to our event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
except RuntimeError:
|
||||
# This happens if we're inside a thread of another application
|
||||
# where there is no running event_loop(). Pythong v3.7 and
|
||||
# higher automatically take care of this case for us. But for
|
||||
# the lower versions we need to do the following:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Enable debug mode
|
||||
loop.set_debug(debug)
|
||||
|
||||
return loop.run_until_complete(cor)
|
||||
|
||||
|
||||
async def toasyncwrapvalue(v):
|
||||
"""
|
||||
Create a coroutine that, when run, returns the provided value.
|
||||
"""
|
||||
|
||||
return v
|
||||
|
||||
|
||||
async def toasyncwrap(fn):
|
||||
"""
|
||||
Create a coroutine that, when run, executes the provided function.
|
||||
"""
|
||||
|
||||
return fn()
|
||||
|
||||
|
||||
class AsyncNotifyBase(URLBase):
|
||||
"""
|
||||
asyncio wrapper for the NotifyBase object
|
||||
"""
|
||||
|
||||
async def async_notify(self, *args, **kwargs):
|
||||
"""
|
||||
Async Notification Wrapper
|
||||
"""
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
return await loop.run_in_executor(
|
||||
None, partial(self.notify, *args, **kwargs))
|
||||
|
||||
except TypeError:
|
||||
# These are our internally thrown notifications
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
# 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")
|
||||
|
||||
return False
|
@ -31,9 +31,11 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from .rest import AppriseURLTester
|
||||
from .asyncio import OuterEventLoop
|
||||
from .module import reload_plugin
|
||||
|
||||
__all__ = [
|
||||
'AppriseURLTester',
|
||||
'OuterEventLoop',
|
||||
'reload_plugin',
|
||||
]
|
||||
|
45
test/helpers/asyncio.py
Normal file
45
test/helpers/asyncio.py
Normal file
@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
import asyncio
|
||||
|
||||
|
||||
class OuterEventLoop():
|
||||
"""
|
||||
An event loop that is easy to put up and tear down from synchronous test
|
||||
code.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._loop = None
|
||||
|
||||
def __enter__(self):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self._loop = loop
|
||||
return loop
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
asyncio.set_event_loop(None)
|
||||
self._loop.close()
|
@ -31,6 +31,7 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import print_function
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
import pytest
|
||||
@ -56,12 +57,9 @@ from apprise import common
|
||||
from apprise.plugins import __load_matrix
|
||||
from apprise.plugins import __reset_matrix
|
||||
from apprise.utils import parse_list
|
||||
from helpers import OuterEventLoop
|
||||
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)
|
||||
@ -86,8 +84,10 @@ def test_apprise_async():
|
||||
API: Apprise() object asynchronous methods
|
||||
|
||||
"""
|
||||
with OuterEventLoop() as loop:
|
||||
def do_notify(server, *args, **kwargs):
|
||||
return py3aio.tosync(server.async_notify(*args, **kwargs))
|
||||
return loop.run_until_complete(
|
||||
server.async_notify(*args, **kwargs))
|
||||
|
||||
apprise_test(do_notify)
|
||||
|
||||
@ -299,6 +299,10 @@ def apprise_test(do_notify):
|
||||
# Pretend everything is okay
|
||||
raise TypeError()
|
||||
|
||||
async def async_notify(self, **kwargs):
|
||||
# Pretend everything is okay (async)
|
||||
raise TypeError()
|
||||
|
||||
def url(self, **kwargs):
|
||||
# Support URL
|
||||
return ''
|
||||
@ -308,6 +312,10 @@ def apprise_test(do_notify):
|
||||
# Pretend everything is okay
|
||||
raise RuntimeError()
|
||||
|
||||
async def async_notify(self, **kwargs):
|
||||
# Pretend everything is okay (async)
|
||||
raise TypeError()
|
||||
|
||||
def url(self, **kwargs):
|
||||
# Support URL
|
||||
return ''
|
||||
@ -318,6 +326,10 @@ def apprise_test(do_notify):
|
||||
# Pretend everything is okay
|
||||
return False
|
||||
|
||||
async def async_notify(self, **kwargs):
|
||||
# Pretend everything is okay (async)
|
||||
raise TypeError()
|
||||
|
||||
def url(self, **kwargs):
|
||||
# Support URL
|
||||
return ''
|
||||
@ -546,8 +558,10 @@ def test_apprise_tagging_async(mock_post, mock_get):
|
||||
API: Apprise() object tagging functionality asynchronous methods
|
||||
|
||||
"""
|
||||
with OuterEventLoop() as loop:
|
||||
def do_notify(server, *args, **kwargs):
|
||||
return py3aio.tosync(server.async_notify(*args, **kwargs))
|
||||
return loop.run_until_complete(
|
||||
server.async_notify(*args, **kwargs))
|
||||
|
||||
apprise_tagging_test(mock_post, mock_get, do_notify)
|
||||
|
||||
@ -733,7 +747,10 @@ def test_apprise_notify_formats(tmpdir):
|
||||
# other if/else parts of the code that aren't otherwise called
|
||||
__load_matrix()
|
||||
|
||||
a = Apprise()
|
||||
# Need to set async_mode=False to call notify() instead of async_notify().
|
||||
asset = AppriseAsset(async_mode=False)
|
||||
|
||||
a = Apprise(asset=asset)
|
||||
|
||||
# no items
|
||||
assert len(a) == 0
|
||||
@ -743,7 +760,7 @@ def test_apprise_notify_formats(tmpdir):
|
||||
notify_format = NotifyFormat.TEXT
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def notify(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
@ -759,7 +776,7 @@ def test_apprise_notify_formats(tmpdir):
|
||||
notify_format = NotifyFormat.HTML
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def notify(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
@ -775,7 +792,7 @@ def test_apprise_notify_formats(tmpdir):
|
||||
notify_format = NotifyFormat.MARKDOWN
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def notify(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
@ -1763,8 +1780,8 @@ def test_apprise_details_plugin_verification():
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
@mock.patch('apprise.py3compat.asyncio.notify', wraps=py3aio.notify)
|
||||
def test_apprise_async_mode(mock_async_notify, mock_post, tmpdir):
|
||||
@mock.patch('asyncio.gather', wraps=asyncio.gather)
|
||||
def test_apprise_async_mode(mock_gather, mock_post, tmpdir):
|
||||
"""
|
||||
API: Apprise() async_mode tests
|
||||
|
||||
@ -1798,8 +1815,8 @@ def test_apprise_async_mode(mock_async_notify, mock_post, tmpdir):
|
||||
assert a.notify("async") is True
|
||||
|
||||
# Verify our async code got executed
|
||||
assert mock_async_notify.call_count == 1
|
||||
mock_async_notify.reset_mock()
|
||||
assert mock_gather.call_count > 0
|
||||
mock_gather.reset_mock()
|
||||
|
||||
# Provide an over-ride now
|
||||
asset = AppriseAsset(async_mode=False)
|
||||
@ -1823,9 +1840,9 @@ def test_apprise_async_mode(mock_async_notify, mock_post, tmpdir):
|
||||
|
||||
# Send Notifications Syncronously
|
||||
assert a.notify("sync") is True
|
||||
# Verify our async code got called
|
||||
assert mock_async_notify.call_count == 1
|
||||
mock_async_notify.reset_mock()
|
||||
# Sequential send doesn't require a gather
|
||||
assert mock_gather.call_count == 0
|
||||
mock_gather.reset_mock()
|
||||
|
||||
# another way of looking a our false set asset configuration
|
||||
assert a[0].asset.async_mode is False
|
||||
@ -1847,8 +1864,8 @@ def test_apprise_async_mode(mock_async_notify, mock_post, tmpdir):
|
||||
assert a.notify("a mixed batch") is True
|
||||
|
||||
# Verify our async code got called
|
||||
assert mock_async_notify.call_count == 1
|
||||
mock_async_notify.reset_mock()
|
||||
assert mock_gather.call_count > 0
|
||||
mock_gather.reset_mock()
|
||||
|
||||
|
||||
def test_notify_matrix_dynamic_importing(tmpdir):
|
||||
|
@ -561,7 +561,7 @@ def test_apprise_config_with_apprise_obj(tmpdir):
|
||||
super().__init__(
|
||||
notify_format=NotifyFormat.HTML, **kwargs)
|
||||
|
||||
def notify(self, **kwargs):
|
||||
async def async_notify(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
return True
|
||||
|
||||
|
@ -39,8 +39,6 @@ from apprise import NotifyFormat
|
||||
|
||||
from apprise.common import NOTIFY_SCHEMA_MAP
|
||||
|
||||
import apprise.py3compat.asyncio as py3aio
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
@ -87,8 +85,12 @@ def test_apprise_asyncio_runtime_error():
|
||||
import asyncio
|
||||
|
||||
# Get our event loop
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
except RuntimeError:
|
||||
loop = None
|
||||
|
||||
# Adjust out event loop to not point at anything
|
||||
asyncio.set_event_loop(None)
|
||||
|
||||
@ -102,67 +104,6 @@ def test_apprise_asyncio_runtime_error():
|
||||
# enough to create a new event loop and continue...
|
||||
assert a.notify(title="title", body="body") is True
|
||||
|
||||
# Verify we have an active event loop
|
||||
new_loop = asyncio.get_event_loop()
|
||||
|
||||
# We didn't throw an exception above; thus we have an event loop at
|
||||
# this point
|
||||
assert new_loop
|
||||
|
||||
# Close off the internal loop created inside a.notify()
|
||||
new_loop.close()
|
||||
|
||||
finally:
|
||||
# Restore our event loop (in the event the above test failed)
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7),
|
||||
reason="Requires Python 3.7+")
|
||||
def test_apprise_works_in_async_loop():
|
||||
"""
|
||||
API: Apprise() can execute synchronously in an existing event loop
|
||||
|
||||
"""
|
||||
class GoodNotification(NotifyBase):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
notify_format=NotifyFormat.HTML, **kwargs)
|
||||
|
||||
def url(self, **kwargs):
|
||||
# Support URL
|
||||
return ''
|
||||
|
||||
def send(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url, *args, **kwargs):
|
||||
# always parseable
|
||||
return NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
# Store our good notification in our schema map
|
||||
NOTIFY_SCHEMA_MAP['good'] = GoodNotification
|
||||
|
||||
# Create ourselves an Apprise object
|
||||
a = Apprise()
|
||||
|
||||
# Add a few entries
|
||||
for _ in range(25):
|
||||
a.add('good://')
|
||||
|
||||
# To ensure backwards compatibility, it should be possible to call
|
||||
# asynchronous Apprise methods from code that already uses an event loop,
|
||||
# even when using the synchronous notify() method.
|
||||
# see https://github.com/caronc/apprise/issues/610
|
||||
import asyncio
|
||||
|
||||
def try_notify():
|
||||
a.notify(title="title", body="body")
|
||||
|
||||
# Convert to a coroutine to run asynchronously.
|
||||
cor = py3aio.toasyncwrap(try_notify)
|
||||
|
||||
# Should execute successfully.
|
||||
asyncio.run(cor)
|
||||
|
@ -68,6 +68,10 @@ def test_apprise_cli_nux_env(tmpdir):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def notify(self, **kwargs):
|
||||
# Pretend everything is okay (when passing --disable-async)
|
||||
return True
|
||||
|
||||
async def async_notify(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
return True
|
||||
|
||||
@ -79,8 +83,8 @@ def test_apprise_cli_nux_env(tmpdir):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def notify(self, **kwargs):
|
||||
# Force a notification failure
|
||||
async def async_notify(self, **kwargs):
|
||||
# Pretend everything is okay
|
||||
return False
|
||||
|
||||
def url(self, *args, **kwargs):
|
||||
|
Loading…
Reference in New Issue
Block a user