mirror of https://github.com/caronc/apprise
				
				
				
			Windows notification support; refs #14
							parent
							
								
									804e27c5fd
								
							
						
					
					
						commit
						68270459d0
					
				| 
						 | 
				
			
			@ -2,6 +2,7 @@ include LICENSE
 | 
			
		|||
include README.md
 | 
			
		||||
include README
 | 
			
		||||
include requirements.txt
 | 
			
		||||
include win-requirements.txt
 | 
			
		||||
include dev-requirements.txt
 | 
			
		||||
recursive-include test *
 | 
			
		||||
global-exclude *.pyc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								README
								
								
								
								
							
							
						
						
									
										4
									
								
								README
								
								
								
								
							| 
						 | 
				
			
			@ -119,6 +119,10 @@ The table below identifies the services this tool supports and some example serv
 | 
			
		|||
	xbmc://user@hostname
 | 
			
		||||
	xbmc://user:password@hostname:port
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
* Windows Notifications
 | 
			
		||||
	windows://
 | 
			
		||||
 | 
			
		||||
Email Support
 | 
			
		||||
-------------
 | 
			
		||||
* mailto://
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,6 +42,8 @@ The table below identifies the services this tool supports and some example serv
 | 
			
		|||
| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram://  | (TCP) 443   | tgram://bottoken/ChatID<br />tgram://bottoken/ChatID1/ChatID2/ChatIDN
 | 
			
		||||
| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | tweet://  | (TCP) 443   | tweet://user@CKey/CSecret/AKey/ASecret
 | 
			
		||||
| [XBMC](https://github.com/caronc/apprise/wiki/Notify_xbmc) | xbmc:// or xbmcs://    | (TCP) 8080 or 443   | xbmc://hostname<br />xbmc://user@hostname<br />xbmc://user:password@hostname:port
 | 
			
		||||
| [Windows Notification](https://github.com/caronc/apprise/wiki/Notify_windows) | windows://    |        n/a          | windows://
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Email Support
 | 
			
		||||
| Service ID | Default Port | Example Syntax |
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -204,6 +204,8 @@ class Apprise(object):
 | 
			
		|||
            instance = Apprise.instantiate(_server, asset=asset)
 | 
			
		||||
            if not instance:
 | 
			
		||||
                return_status = False
 | 
			
		||||
                logging.error(
 | 
			
		||||
                        "Failed to load notification url: {}".format(_server))
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # Add our initialized plugin to our server listings
 | 
			
		||||
| 
						 | 
				
			
			@ -261,10 +263,15 @@ class Apprise(object):
 | 
			
		|||
                    # Toggle our return status flag
 | 
			
		||||
                    status = False
 | 
			
		||||
 | 
			
		||||
            except TypeError:
 | 
			
		||||
                # These our our internally thrown notifications
 | 
			
		||||
                # TODO: Change this to a custom one such as AppriseNotifyError
 | 
			
		||||
                status = 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.
 | 
			
		||||
                logging.exception("notification exception")
 | 
			
		||||
                logging.exception("Notification Exception")
 | 
			
		||||
                status = False
 | 
			
		||||
 | 
			
		||||
        return status
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,12 +54,16 @@ class AppriseAsset(object):
 | 
			
		|||
    # The default color to return if a mapping isn't found in our table above
 | 
			
		||||
    default_html_color = '#888888'
 | 
			
		||||
 | 
			
		||||
    # The default image extension to use
 | 
			
		||||
    default_extension = '.png'
 | 
			
		||||
 | 
			
		||||
    # The default theme
 | 
			
		||||
    theme = 'default'
 | 
			
		||||
 | 
			
		||||
    # Image URL Mask
 | 
			
		||||
    image_url_mask = \
 | 
			
		||||
        'http://nuxref.com/apprise/themes/{THEME}/apprise-{TYPE}-{XY}.png'
 | 
			
		||||
        'http://nuxref.com/apprise/themes/{THEME}/' \
 | 
			
		||||
        'apprise-{TYPE}-{XY}{EXTENSION}'
 | 
			
		||||
 | 
			
		||||
    # Application Logo
 | 
			
		||||
    image_url_logo = \
 | 
			
		||||
| 
						 | 
				
			
			@ -71,11 +75,11 @@ class AppriseAsset(object):
 | 
			
		|||
        'assets',
 | 
			
		||||
        'themes',
 | 
			
		||||
        '{THEME}',
 | 
			
		||||
        'apprise-{TYPE}-{XY}.png',
 | 
			
		||||
        'apprise-{TYPE}-{XY}{EXTENSION}',
 | 
			
		||||
    ))
 | 
			
		||||
 | 
			
		||||
    def __init__(self, theme='default', image_path_mask=None,
 | 
			
		||||
                 image_url_mask=None):
 | 
			
		||||
                 image_url_mask=None, default_extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Asset Initialization
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +93,9 @@ class AppriseAsset(object):
 | 
			
		|||
        if image_url_mask is not None:
 | 
			
		||||
            self.image_url_mask = image_url_mask
 | 
			
		||||
 | 
			
		||||
        if default_extension is not None:
 | 
			
		||||
            self.default_extension = default_extension
 | 
			
		||||
 | 
			
		||||
    def color(self, notify_type, color_type=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns an HTML mapped color based on passed in notify type
 | 
			
		||||
| 
						 | 
				
			
			@ -121,7 +128,7 @@ class AppriseAsset(object):
 | 
			
		|||
        raise ValueError(
 | 
			
		||||
            'AppriseAsset html_color(): An invalid color_type was specified.')
 | 
			
		||||
 | 
			
		||||
    def image_url(self, notify_type, image_size, logo=False):
 | 
			
		||||
    def image_url(self, notify_type, image_size, logo=False, extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Apply our mask to our image URL
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -134,10 +141,14 @@ class AppriseAsset(object):
 | 
			
		|||
            # No image to return
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        if extension is None:
 | 
			
		||||
            extension = self.default_extension
 | 
			
		||||
 | 
			
		||||
        re_map = {
 | 
			
		||||
            '{THEME}': self.theme if self.theme else '',
 | 
			
		||||
            '{TYPE}': notify_type,
 | 
			
		||||
            '{XY}': image_size,
 | 
			
		||||
            '{EXTENSION}': extension,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # Iterate over above list and store content accordingly
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +159,8 @@ class AppriseAsset(object):
 | 
			
		|||
 | 
			
		||||
        return re_table.sub(lambda x: re_map[x.group()], url_mask)
 | 
			
		||||
 | 
			
		||||
    def image_path(self, notify_type, image_size, must_exist=True):
 | 
			
		||||
    def image_path(self, notify_type, image_size, must_exist=True,
 | 
			
		||||
                   extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Apply our mask to our image file path
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -158,10 +170,14 @@ class AppriseAsset(object):
 | 
			
		|||
            # No image to return
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        if extension is None:
 | 
			
		||||
            extension = self.default_extension
 | 
			
		||||
 | 
			
		||||
        re_map = {
 | 
			
		||||
            '{THEME}': self.theme if self.theme else '',
 | 
			
		||||
            '{TYPE}': notify_type,
 | 
			
		||||
            '{XY}': image_size,
 | 
			
		||||
            '{EXTENSION}': extension,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # Iterate over above list and store content accordingly
 | 
			
		||||
| 
						 | 
				
			
			@ -178,13 +194,17 @@ class AppriseAsset(object):
 | 
			
		|||
        # Return what we parsed
 | 
			
		||||
        return path
 | 
			
		||||
 | 
			
		||||
    def image_raw(self, notify_type, image_size):
 | 
			
		||||
    def image_raw(self, notify_type, image_size, extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns the raw image if it can (otherwise the function returns None)
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        path = self.image_path(notify_type=notify_type, image_size=image_size)
 | 
			
		||||
        path = self.image_path(
 | 
			
		||||
            notify_type=notify_type,
 | 
			
		||||
            image_size=image_size,
 | 
			
		||||
            extension=extension,
 | 
			
		||||
        )
 | 
			
		||||
        if path:
 | 
			
		||||
            try:
 | 
			
		||||
                with open(path, 'rb') as fd:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 88 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 88 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 88 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 88 KiB  | 
| 
						 | 
				
			
			@ -171,7 +171,7 @@ class NotifyBase(object):
 | 
			
		|||
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def image_url(self, notify_type, logo=False):
 | 
			
		||||
    def image_url(self, notify_type, logo=False, extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns Image URL if possible
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -186,9 +186,10 @@ class NotifyBase(object):
 | 
			
		|||
            notify_type=notify_type,
 | 
			
		||||
            image_size=self.image_size,
 | 
			
		||||
            logo=logo,
 | 
			
		||||
            extension=extension,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def image_path(self, notify_type):
 | 
			
		||||
    def image_path(self, notify_type, extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns the path of the image if it can
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -201,9 +202,10 @@ class NotifyBase(object):
 | 
			
		|||
        return self.asset.image_path(
 | 
			
		||||
            notify_type=notify_type,
 | 
			
		||||
            image_size=self.image_size,
 | 
			
		||||
            extension=extension,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def image_raw(self, notify_type):
 | 
			
		||||
    def image_raw(self, notify_type, extension=None):
 | 
			
		||||
        """
 | 
			
		||||
        Returns the raw image if it can
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -216,6 +218,7 @@ class NotifyBase(object):
 | 
			
		|||
        return self.asset.image_raw(
 | 
			
		||||
            notify_type=notify_type,
 | 
			
		||||
            image_size=self.image_size,
 | 
			
		||||
            extension=extension,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def color(self, notify_type, color_type=None):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,185 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Windows Notify Wrapper
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2017-2018 Chris Caron <lead2gold@gmail.com>
 | 
			
		||||
#
 | 
			
		||||
# This file is part of apprise.
 | 
			
		||||
#
 | 
			
		||||
# This program is free software; you can redistribute it and/or modify it
 | 
			
		||||
# under the terms of the GNU Lesser General Public License as published by
 | 
			
		||||
# the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Lesser General Public License for more details.
 | 
			
		||||
from __future__ import absolute_import
 | 
			
		||||
from __future__ import print_function
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from time import sleep
 | 
			
		||||
 | 
			
		||||
from .NotifyBase import NotifyBase
 | 
			
		||||
from ..common import NotifyImageSize
 | 
			
		||||
 | 
			
		||||
# Default our global support flag
 | 
			
		||||
NOTIFY_WINDOWS_SUPPORT_ENABLED = False
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    # 3rd party modules (Windows Only)
 | 
			
		||||
    import win32api
 | 
			
		||||
    import win32con
 | 
			
		||||
    import win32gui
 | 
			
		||||
 | 
			
		||||
    # We're good to go!
 | 
			
		||||
    NOTIFY_WINDOWS_SUPPORT_ENABLED = True
 | 
			
		||||
 | 
			
		||||
except ImportError:
 | 
			
		||||
    # No problem; we just simply can't support this plugin because we're
 | 
			
		||||
    # either using Linux, or simply do not have pypiwin32 installed.
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotifyWindows(NotifyBase):
 | 
			
		||||
    """
 | 
			
		||||
    A wrapper for local Windows Notifications
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # The default protocol
 | 
			
		||||
    protocol = 'windows'
 | 
			
		||||
 | 
			
		||||
    # Allows the user to specify the NotifyImageSize object
 | 
			
		||||
    image_size = NotifyImageSize.XY_128
 | 
			
		||||
 | 
			
		||||
    # This entry is a bit hacky, but it allows us to unit-test this library
 | 
			
		||||
    # in an environment that simply doesn't have the windows packages
 | 
			
		||||
    # available to us.  It also allows us to handle situations where the
 | 
			
		||||
    # packages actually are present but we need to test that they aren't.
 | 
			
		||||
    # If anyone is seeing this had knows a better way of testing this
 | 
			
		||||
    # outside of what is defined in test/test_windows_plugin.py, please
 | 
			
		||||
    # let me know! :)
 | 
			
		||||
    _enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Initialize Windows Object
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # Number of seconds to display notification for
 | 
			
		||||
        self.duration = 12
 | 
			
		||||
 | 
			
		||||
        # Define our handler
 | 
			
		||||
        self.hwnd = None
 | 
			
		||||
 | 
			
		||||
        super(NotifyWindows, self).__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def _on_destroy(self, hwnd, msg, wparam, lparam):
 | 
			
		||||
        """
 | 
			
		||||
        Destroy callback function
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        nid = (self.hwnd, 0)
 | 
			
		||||
        win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid)
 | 
			
		||||
        win32api.PostQuitMessage(0)
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def notify(self, title, body, notify_type, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Perform Windows Notification
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if not self._enabled:
 | 
			
		||||
            self.logger.warning(
 | 
			
		||||
                "Windows Notifications are not supported by this system.")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # Limit results to just the first 2 line otherwise
 | 
			
		||||
        # there is just to much content to display
 | 
			
		||||
        body = re.split('[\r\n]+', body)
 | 
			
		||||
        body[0] = body[0].strip('#').strip()
 | 
			
		||||
        body = '\r\n'.join(body[0:2])
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # Register destruction callback
 | 
			
		||||
            message_map = {win32con.WM_DESTROY: self._on_destroy, }
 | 
			
		||||
 | 
			
		||||
            # Register the window class.
 | 
			
		||||
            self.wc = win32gui.WNDCLASS()
 | 
			
		||||
            self.hinst = self.wc.hInstance = win32api.GetModuleHandle(None)
 | 
			
		||||
            self.wc.lpszClassName = str("PythonTaskbar")
 | 
			
		||||
            self.wc.lpfnWndProc = message_map
 | 
			
		||||
            self.classAtom = win32gui.RegisterClass(self.wc)
 | 
			
		||||
 | 
			
		||||
            # Styling and window type
 | 
			
		||||
            style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
 | 
			
		||||
            self.hwnd = win32gui.CreateWindow(
 | 
			
		||||
                self.classAtom, "Taskbar", style, 0, 0,
 | 
			
		||||
                win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, 0, 0,
 | 
			
		||||
                self.hinst, None)
 | 
			
		||||
            win32gui.UpdateWindow(self.hwnd)
 | 
			
		||||
 | 
			
		||||
            # image path
 | 
			
		||||
            icon_path = self.image_path(notify_type, extension='.ico')
 | 
			
		||||
            icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                hicon = win32gui.LoadImage(
 | 
			
		||||
                    self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0,
 | 
			
		||||
                    icon_flags)
 | 
			
		||||
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                self.logger.warning(
 | 
			
		||||
                    "Could not load windows notification icon ({}): {}"
 | 
			
		||||
                    .format(icon_path, e))
 | 
			
		||||
 | 
			
		||||
                # disable icon
 | 
			
		||||
                hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
 | 
			
		||||
 | 
			
		||||
            # Taskbar icon
 | 
			
		||||
            flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP
 | 
			
		||||
            nid = (self.hwnd, 0, flags, win32con.WM_USER + 20, hicon,
 | 
			
		||||
                   "Tooltip")
 | 
			
		||||
            win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, nid)
 | 
			
		||||
            win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, (
 | 
			
		||||
                self.hwnd, 0, win32gui.NIF_INFO, win32con.WM_USER + 20, hicon,
 | 
			
		||||
                "Balloon Tooltip", body, 200, title))
 | 
			
		||||
 | 
			
		||||
            # take a rest then destroy
 | 
			
		||||
            sleep(self.duration)
 | 
			
		||||
            win32gui.DestroyWindow(self.hwnd)
 | 
			
		||||
            win32gui.UnregisterClass(self.wc.lpszClassName, None)
 | 
			
		||||
 | 
			
		||||
            self.logger.info('Sent Windows notification.')
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            self.logger.warning('Failed to send Windows notification.')
 | 
			
		||||
            self.logger.exception('Windows Exception')
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def parse_url(url):
 | 
			
		||||
        """
 | 
			
		||||
        There are no parameters nessisary for this protocol; simply having
 | 
			
		||||
        windows:// is all you need.  This function just makes sure that
 | 
			
		||||
        is in place.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # return a very basic set of requirements
 | 
			
		||||
        return {
 | 
			
		||||
            'schema': NotifyWindows.protocol,
 | 
			
		||||
            'user': None,
 | 
			
		||||
            'password': None,
 | 
			
		||||
            'port': None,
 | 
			
		||||
            'host': 'localhost',
 | 
			
		||||
            'fullpath': None,
 | 
			
		||||
            'path': None,
 | 
			
		||||
            'url': url,
 | 
			
		||||
            'qsd': {},
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +55,9 @@ class NotifyXBMC(NotifyBase):
 | 
			
		|||
        """
 | 
			
		||||
        super(NotifyXBMC, self).__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
        # Number of micro-seconds to display notification for
 | 
			
		||||
        self.duration = 12000
 | 
			
		||||
 | 
			
		||||
        if self.secure:
 | 
			
		||||
            self.schema = 'https'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +88,7 @@ class NotifyXBMC(NotifyBase):
 | 
			
		|||
                'title': title,
 | 
			
		||||
                'message': body,
 | 
			
		||||
                # displaytime is defined in microseconds
 | 
			
		||||
                'displaytime': 12000,
 | 
			
		||||
                'displaytime': self.duration,
 | 
			
		||||
            },
 | 
			
		||||
            'id': 1,
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +122,7 @@ class NotifyXBMC(NotifyBase):
 | 
			
		|||
                'title': title,
 | 
			
		||||
                'message': body,
 | 
			
		||||
                # displaytime is defined in microseconds
 | 
			
		||||
                'displaytime': 12000,
 | 
			
		||||
                'displaytime': self.duration,
 | 
			
		||||
            },
 | 
			
		||||
            'id': 1,
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,7 @@ from .NotifyToasty import NotifyToasty
 | 
			
		|||
from .NotifyTwitter.NotifyTwitter import NotifyTwitter
 | 
			
		||||
from .NotifyXBMC import NotifyXBMC
 | 
			
		||||
from .NotifyXML import NotifyXML
 | 
			
		||||
from .NotifyWindows import NotifyWindows
 | 
			
		||||
 | 
			
		||||
from .NotifyPushjet import pushjet
 | 
			
		||||
from .NotifyGrowl import gntp
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +61,7 @@ __all__ = [
 | 
			
		|||
    'NotifyMatterMost', 'NotifyProwl', 'NotifyPushalot',
 | 
			
		||||
    'NotifyPushBullet', 'NotifyPushjet', 'NotifyPushover', 'NotifyRocketChat',
 | 
			
		||||
    'NotifySlack', 'NotifyStride', 'NotifyToasty', 'NotifyTwitter',
 | 
			
		||||
    'NotifyTelegram', 'NotifyXBMC', 'NotifyXML',
 | 
			
		||||
    'NotifyTelegram', 'NotifyXBMC', 'NotifyXML', 'NotifyWindows',
 | 
			
		||||
 | 
			
		||||
    # Reference
 | 
			
		||||
    'NotifyImageSize', 'NOTIFY_IMAGE_SIZES', 'NotifyType', 'NOTIFY_TYPES',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										9
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										9
									
								
								setup.py
								
								
								
								
							| 
						 | 
				
			
			@ -17,6 +17,7 @@
 | 
			
		|||
#
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import platform
 | 
			
		||||
try:
 | 
			
		||||
    from setuptools import setup
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +27,11 @@ except ImportError:
 | 
			
		|||
from setuptools import find_packages
 | 
			
		||||
 | 
			
		||||
install_options = os.environ.get("APPRISE_INSTALL", "").split(",")
 | 
			
		||||
install_requires = open('requirements.txt').readlines()
 | 
			
		||||
if platform.system().lower().startswith('win'):
 | 
			
		||||
    # Windows Notification Support
 | 
			
		||||
    install_requires += open('win-requirements.txt').readlines()
 | 
			
		||||
 | 
			
		||||
libonly_flags = set(["lib-only", "libonly", "no-cli", "without-cli"])
 | 
			
		||||
if libonly_flags.intersection(install_options):
 | 
			
		||||
    console_scripts = []
 | 
			
		||||
| 
						 | 
				
			
			@ -52,9 +58,10 @@ setup(
 | 
			
		|||
        'apprise': [
 | 
			
		||||
            'assets/NotifyXML-1.0.xsd',
 | 
			
		||||
            'assets/themes/default/*.png',
 | 
			
		||||
            'assets/themes/default/*.ico',
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    install_requires=open('requirements.txt').readlines(),
 | 
			
		||||
    install_requires=install_requires,
 | 
			
		||||
    classifiers=(
 | 
			
		||||
        'Development Status :: 5 - Production/Stable',
 | 
			
		||||
        'Intended Audience :: Developers',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -292,8 +292,8 @@ def test_apprise_asset(tmpdir):
 | 
			
		|||
 | 
			
		||||
    a = AppriseAsset(
 | 
			
		||||
        theme='dark',
 | 
			
		||||
        image_path_mask='/{THEME}/{TYPE}-{XY}.png',
 | 
			
		||||
        image_url_mask='http://localhost/{THEME}/{TYPE}-{XY}.png',
 | 
			
		||||
        image_path_mask='/{THEME}/{TYPE}-{XY}{EXTENSION}',
 | 
			
		||||
        image_url_mask='http://localhost/{THEME}/{TYPE}-{XY}{EXTENSION}',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    a.default_html_color = '#abcabc'
 | 
			
		||||
| 
						 | 
				
			
			@ -430,3 +430,32 @@ def test_apprise_asset(tmpdir):
 | 
			
		|||
           must_exist=False) is None)
 | 
			
		||||
    assert(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256,
 | 
			
		||||
           must_exist=True) is None)
 | 
			
		||||
 | 
			
		||||
    # Test our default extension out
 | 
			
		||||
    a = AppriseAsset(
 | 
			
		||||
        image_path_mask='/{THEME}/{TYPE}-{XY}{EXTENSION}',
 | 
			
		||||
        image_url_mask='http://localhost/{THEME}/{TYPE}-{XY}{EXTENSION}',
 | 
			
		||||
        default_extension='.jpeg',
 | 
			
		||||
    )
 | 
			
		||||
    assert(a.image_path(
 | 
			
		||||
        NotifyType.INFO,
 | 
			
		||||
        NotifyImageSize.XY_256,
 | 
			
		||||
        must_exist=False) == '/default/info-256x256.jpeg')
 | 
			
		||||
 | 
			
		||||
    assert(a.image_url(
 | 
			
		||||
        NotifyType.INFO,
 | 
			
		||||
        NotifyImageSize.XY_256) == 'http://localhost/'
 | 
			
		||||
                                   'default/info-256x256.jpeg')
 | 
			
		||||
 | 
			
		||||
    # extension support
 | 
			
		||||
    assert(a.image_path(
 | 
			
		||||
        NotifyType.INFO,
 | 
			
		||||
        NotifyImageSize.XY_128,
 | 
			
		||||
        must_exist=False,
 | 
			
		||||
        extension='.ico') == '/default/info-128x128.ico')
 | 
			
		||||
 | 
			
		||||
    assert(a.image_url(
 | 
			
		||||
        NotifyType.INFO,
 | 
			
		||||
        NotifyImageSize.XY_256,
 | 
			
		||||
        extension='.test') == 'http://localhost/'
 | 
			
		||||
                              'default/info-256x256.test')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,132 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# NotifyWindows - Unit Tests
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2018 Chris Caron <lead2gold@gmail.com>
 | 
			
		||||
#
 | 
			
		||||
# This file is part of apprise.
 | 
			
		||||
#
 | 
			
		||||
# This program is free software; you can redistribute it and/or modify it
 | 
			
		||||
# under the terms of the GNU Lesser General Public License as published by
 | 
			
		||||
# the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Lesser General Public License for more details.
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
import sys
 | 
			
		||||
import types
 | 
			
		||||
 | 
			
		||||
# Rebuild our Apprise environment
 | 
			
		||||
import apprise
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_windows_plugin():
 | 
			
		||||
    """
 | 
			
		||||
    API: NotifyWindows Plugin()
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # We need to fake our windows environment for testing purposes
 | 
			
		||||
    win32api_name = 'win32api'
 | 
			
		||||
    win32api = types.ModuleType(win32api_name)
 | 
			
		||||
    sys.modules[win32api_name] = win32api
 | 
			
		||||
    win32api.GetModuleHandle = mock.Mock(
 | 
			
		||||
        name=win32api_name + '.GetModuleHandle')
 | 
			
		||||
    win32api.PostQuitMessage = mock.Mock(
 | 
			
		||||
        name=win32api_name + '.PostQuitMessage')
 | 
			
		||||
 | 
			
		||||
    win32con_name = 'win32con'
 | 
			
		||||
    win32con = types.ModuleType(win32con_name)
 | 
			
		||||
    sys.modules[win32con_name] = win32con
 | 
			
		||||
    win32con.CW_USEDEFAULT = mock.Mock(name=win32con_name + '.CW_USEDEFAULT')
 | 
			
		||||
    win32con.IDI_APPLICATION = mock.Mock(
 | 
			
		||||
        name=win32con_name + '.IDI_APPLICATION')
 | 
			
		||||
    win32con.IMAGE_ICON = mock.Mock(name=win32con_name + '.IMAGE_ICON')
 | 
			
		||||
    win32con.LR_DEFAULTSIZE = 1
 | 
			
		||||
    win32con.LR_LOADFROMFILE = 2
 | 
			
		||||
    win32con.WM_DESTROY = mock.Mock(name=win32con_name + '.WM_DESTROY')
 | 
			
		||||
    win32con.WM_USER = 0
 | 
			
		||||
    win32con.WS_OVERLAPPED = 1
 | 
			
		||||
    win32con.WS_SYSMENU = 2
 | 
			
		||||
 | 
			
		||||
    win32gui_name = 'win32gui'
 | 
			
		||||
    win32gui = types.ModuleType(win32gui_name)
 | 
			
		||||
    sys.modules[win32gui_name] = win32gui
 | 
			
		||||
    win32gui.CreateWindow = mock.Mock(name=win32gui_name + '.CreateWindow')
 | 
			
		||||
    win32gui.DestroyWindow = mock.Mock(name=win32gui_name + '.DestroyWindow')
 | 
			
		||||
    win32gui.LoadIcon = mock.Mock(name=win32gui_name + '.LoadIcon')
 | 
			
		||||
    win32gui.LoadImage = mock.Mock(name=win32gui_name + '.LoadImage')
 | 
			
		||||
    win32gui.NIF_ICON = 1
 | 
			
		||||
    win32gui.NIF_INFO = mock.Mock(name=win32gui_name + '.NIF_INFO')
 | 
			
		||||
    win32gui.NIF_MESSAGE = 2
 | 
			
		||||
    win32gui.NIF_TIP = 4
 | 
			
		||||
    win32gui.NIM_ADD = mock.Mock(name=win32gui_name + '.NIM_ADD')
 | 
			
		||||
    win32gui.NIM_DELETE = mock.Mock(name=win32gui_name + '.NIM_DELETE')
 | 
			
		||||
    win32gui.NIM_MODIFY = mock.Mock(name=win32gui_name + '.NIM_MODIFY')
 | 
			
		||||
    win32gui.RegisterClass = mock.Mock(name=win32gui_name + '.RegisterClass')
 | 
			
		||||
    win32gui.UnregisterClass = mock.Mock(
 | 
			
		||||
        name=win32gui_name + '.UnregisterClass')
 | 
			
		||||
    win32gui.Shell_NotifyIcon = mock.Mock(
 | 
			
		||||
        name=win32gui_name + '.Shell_NotifyIcon')
 | 
			
		||||
    win32gui.UpdateWindow = mock.Mock(name=win32gui_name + '.UpdateWindow')
 | 
			
		||||
    win32gui.WNDCLASS = mock.Mock(name=win32gui_name + '.WNDCLASS')
 | 
			
		||||
 | 
			
		||||
    # The following allows our mocked content to kick in. In python 3.x keys()
 | 
			
		||||
    # returns an iterator, therefore we need to convert the keys() back into
 | 
			
		||||
    # a list object to prevent from getting the error:
 | 
			
		||||
    #    "RuntimeError: dictionary changed size during iteration"
 | 
			
		||||
    #
 | 
			
		||||
    for mod in list(sys.modules.keys()):
 | 
			
		||||
        if mod.startswith('apprise.'):
 | 
			
		||||
            del(sys.modules[mod])
 | 
			
		||||
    reload(apprise)
 | 
			
		||||
 | 
			
		||||
    # Create our instance
 | 
			
		||||
    obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False)
 | 
			
		||||
    obj.duration = 0
 | 
			
		||||
 | 
			
		||||
    # Check that it found our mocked environments
 | 
			
		||||
    assert(obj._enabled is True)
 | 
			
		||||
 | 
			
		||||
    # _on_destroy check
 | 
			
		||||
    obj._on_destroy(0, '', 0, 0)
 | 
			
		||||
 | 
			
		||||
    # test notifications
 | 
			
		||||
    assert(obj.notify(title='title', body='body',
 | 
			
		||||
           notify_type=apprise.NotifyType.INFO) is True)
 | 
			
		||||
 | 
			
		||||
    # Test our loading of our icon exception; it will still allow the
 | 
			
		||||
    # notification to be sent
 | 
			
		||||
    win32gui.LoadImage.side_effect = AttributeError
 | 
			
		||||
    assert(obj.notify(title='title', body='body',
 | 
			
		||||
           notify_type=apprise.NotifyType.INFO) is True)
 | 
			
		||||
    # Undo our change
 | 
			
		||||
    win32gui.LoadImage.side_effect = None
 | 
			
		||||
 | 
			
		||||
    # Test our global exception handling
 | 
			
		||||
    win32gui.UpdateWindow.side_effect = AttributeError
 | 
			
		||||
    assert(obj.notify(title='title', body='body',
 | 
			
		||||
           notify_type=apprise.NotifyType.INFO) is False)
 | 
			
		||||
    # Undo our change
 | 
			
		||||
    win32gui.UpdateWindow.side_effect = None
 | 
			
		||||
 | 
			
		||||
    # Toggle our testing for when we can't send notifications because the
 | 
			
		||||
    # package has been made unavailable to us
 | 
			
		||||
    obj._enabled = False
 | 
			
		||||
    assert(obj.notify(title='title', body='body',
 | 
			
		||||
           notify_type=apprise.NotifyType.INFO) is False)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
pypiwin32
 | 
			
		||||
		Loading…
	
		Reference in New Issue