Commits

Jeffrey Goettsch  committed fdba3c1 Merge

version 0.3

  • Participants
  • Parent commits 4625c87, 6ac8644
  • Branches release-0
  • Tags 0.3

Comments (0)

Files changed (17)

 syntax: regexp
 (^|/)\..*
-^tests/nmakeys.py
-^tests/pushoverkeys.py
+^MANIFEST
+^pushnotify/tests/nmakeys.py
+^pushnotify/tests/prowlkeys.py
+^pushnotify/tests/pushoverkeys.py
 
 syntax: glob
 *.egg-info
+The primary author of py-pushnotify is Jeffrey Goettsch <jgoettsch@gmail.com>
+who may be found online at https://bitbucket.org/jgoettsch/.

File CHANGELOG.txt

+version 0.3
+
+* added support for Prowl: http://www.prowlapp.com/
+
+version 0.2.1
+
+* fixed an issue where developer keys were not being sent with Notify My Android verifications
+
+version 0.2
+
+* added support for Pushover: https://pushover.net/
+
+version 0.1
+
+* added support for Notify My Android: https://www.notifymyandroid.com/
+If you have already downloaded py-pushnotify, simply run the following
+command inside this directory:
+
+    python setup.py install
+
+If you have not yet downloaded py-pushnotify, it would be easier to do
+the following:
+
+    pip -e install py-pushnotify

File LICENSE

-Copyright (c) Jeffrey Goettsch and other contributors.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-    * Redistributions of source code must retain the above copyright
-      notice, this list of conditions and the following disclaimer.
-    * 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.
-    * Neither the name of Jeffrey Goettsch nor the
-      names of the 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 JEFFREY GOETTSCH OR THE 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.
+Copyright (c) Jeffrey Goettsch and other contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * 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.
+    * Neither the name of Jeffrey Goettsch nor the
+      names of the 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 JEFFREY GOETTSCH OR THE 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.
+include AUTHORS.txt
+include CHANGELOG.txt
+include INSTALL.txt
+include LICENSE.txt
+include README.txt
+include TODO.txt
+exclude pushnotify/tests/nmakeys.py
+exclude pushnotify/tests/prowlkeys.py
+exclude pushnotify/tests/pushoverkeys.py
+py-pushnotify
+-------------
+
+py-pushnotify is a package for sending push notifications. It currently
+supports Android devices running Notify My Android and Pushover, and iOS
+devices running Pushover and Prowl.
+
+See
+
+    https://www.notifymyandroid.com/
+    https://pushover.net/ and
+    http://www.prowlapp.com/
+
+for more details about these applications.
+
+All development for py-pushnotify takes place on bitbucket:
+
+    https://bitbucket.org/jgoettsch/py-pushnotify/
+
+If you discover a bug, please go to the above page and create an issue
+ticket. If you have any questions, please contact the author.
+See AUTHORS.txt.
+For all tasks and issues, see the issues list on bitbucket:
+
+    https://bitbucket.org/jgoettsch/py-pushnotify/issues

File pushnotify/exceptions.py

     pass
 
 
+class PermissionDenied(PushNotifyError):
+    """Raised when a request had not been approved.
+
+    Args:
+        args[0]: A string containing a message from the server.
+        args[1]: An integer containing an error code from the server.
+
+    """
+
+    pass
+
+
 class RateLimitExceeded(PushNotifyError):
     """Raised when too many requests are submitted in too small a time
     frame.

File pushnotify/prowl.py

+#!/usr/bin/env python
+# vim: set fileencoding=utf-8
+
+"""Module for sending push notifications to iOS devices that have
+Prowl installed. See http://www.prowlapp.com/ for more
+information.
+
+copyright: Copyright (c) Jeffrey Goettsch and other contributors.
+license: BSD, see LICENSE for details.
+
+"""
+
+
+import urllib
+import urllib2
+try:
+    from xml.etree import cElementTree
+    ElementTree = cElementTree
+except ImportError:
+    from xml.etree import ElementTree
+
+from pushnotify import exceptions
+
+
+PUBLIC_API_URL = u'https://api.prowlapp.com/publicapi'
+VERIFY_URL = u'/'.join([PUBLIC_API_URL, 'verify'])
+NOTIFY_URL = u'/'.join([PUBLIC_API_URL, 'add'])
+RETRIEVE_TOKEN_URL = u'/'.join([PUBLIC_API_URL, 'retrieve', 'token'])
+RETRIEVE_APIKEY_URL = u'/'.join([PUBLIC_API_URL, 'retrieve', 'apikey'])
+
+
+class Client(object):
+    """Client for sending push notificiations to iOS devices with
+    the Prowl application installed.
+
+    Member Vars:
+        apikeys: A list of strings, each containing a 40 character api
+            key.
+        providerkey: A string containing a 40 character provider key.
+
+    """
+
+    def __init__(self, apikeys=None, providerkey=None):
+        """Initialize the Prowl client.
+
+        Args:
+            apikeys:  A list of strings of 40 characters each, each
+                containing a valid api key.
+            providerkey: A string of 40 characters containing a valid
+                provider key.
+
+        """
+
+        self._browser = urllib2.build_opener(urllib2.HTTPSHandler())
+        self._last_type = None
+        self._last_code = None
+        self._last_message = None
+        self._last_remaining = None
+        self._last_resetdate = None
+        self._last_token = None
+        self._last_token_url = None
+        self._last_apikey = None
+
+        self.apikeys = [] if apikeys is None else apikeys
+        self.providerkey = providerkey
+
+    def _get(self, url):
+
+        request = urllib2.Request(url)
+        try:
+            response_stream = self._browser.open(request)
+        except urllib2.HTTPError, exc:
+            return exc
+        else:
+            return response_stream
+
+    def _parse_response(self, response, verify=False):
+
+        xmlresp = response.read()
+        root = ElementTree.fromstring(xmlresp)
+
+        self._last_type = root[0].tag.lower()
+        self._last_code = root[0].attrib['code']
+
+        if self._last_type == 'success':
+            self._last_message = None
+            self._last_remaining = root[0].attrib['remaining']
+            self._last_resetdate = root[0].attrib['resetdate']
+        elif self._last_type == 'error':
+            self._last_message = root[0].text
+            self._last_remaining = None
+            self._last_resetdate = None
+
+            if (not verify or
+                    (self._last_code != '400' and self._last_code != '401')):
+                self._raise_exception()
+        else:
+            raise exceptions.UnrecognizedResponseError(xmlresp, -1)
+
+        if len(root) > 1:
+            if root[1].tag.lower() == 'retrieve':
+                if 'token' in root[1].attrib:
+                    self._last_token = root[1].attrib['token']
+                    self._last_token_url = root[1].attrib['url']
+                    self._last_apikey = None
+                elif 'apikey' in root[1].attrib:
+                    self._last_token = None
+                    self.last_token_url = None
+                    self._last_apikey = root[1].attrib['apikey']
+                else:
+                    raise exceptions.UnrecognizedResponseError(xmlresp, -1)
+            else:
+                raise exceptions.UnrecognizedResponseError(xmlresp, -1)
+
+        return root
+
+    def _post(self, url, data):
+
+        request = urllib2.Request(url, data)
+        try:
+            response_stream = self._browser.open(request)
+        except urllib2.HTTPError, exc:
+            return exc
+        else:
+            return response_stream
+
+    def _raise_exception(self):
+
+        if self._last_code == '400':
+            raise exceptions.FormatError(self._last_message,
+                                         int(self._last_code))
+        elif self._last_code == '401':
+            raise exceptions.ApiKeyError(self._last_message,
+                                         int(self._last_code))
+        elif self._last_code == '406':
+            raise exceptions.RateLimitExceeded(self._last_message,
+                                               int(self._last_code))
+        elif self._last_code == '409':
+            raise exceptions.PermissionDenied(self._last_message,
+                                              int(self._last_code))
+        elif self._last_code == '500':
+            raise exceptions.ServerError(self._last_message,
+                                         int(self._last_code))
+        else:
+            raise exceptions.UnknownError(self._last_message,
+                                          int(self._last_code))
+
+    def notify(self, app, event, desc, kwargs=None):
+        """Send a notification to each apikey in self.apikeys.
+
+        Args:
+            app: A string of up to 256 characters containing the name
+                of the application sending the notification.
+            event: A string of up to 1024 characters containing the
+                event that is being notified (i.e. subject or brief
+                description.)
+            desc: A string of up to 10000 characters containing the
+                notification text.
+            kwargs: A dictionary with any of the following strings as
+                    keys:
+                priority: An integer between -2 and 2, indicating the
+                    priority of the notification. -2 is the lowest, 2 is
+                    the highest, and 0 is normal.
+                url: A string of up to 512 characters containing a URL
+                    to attach to the notification.
+                (default: None)
+
+        Raises:
+            pushnotify.exceptions.FormatError
+            pushnotify.exceptions.ApiKeyError
+            pushnotify.exceptions.RateLimitExceeded
+            pushnotify.exceptions.ServerError
+            pushnotify.exceptions.UnknownError
+            pushnotify.exceptions.UnrecognizedResponseError
+
+        """
+
+        data = {'apikey': ','.join(self.apikeys),
+                'application': app,
+                'event': event,
+                'description': desc}
+
+        if self.providerkey:
+            data['providerkey'] = self.providerkey
+
+        if kwargs:
+            data.update(kwargs)
+
+        data = urllib.urlencode(data)
+
+        response = self._post(NOTIFY_URL, data)
+        self._parse_response(response)
+
+    def retrieve_apikey(self, token):
+        """Get an API key for a given token.
+
+        Once a user has approved you sending them push notifications,
+        you can supply the returned token here and get an API key.
+
+        Args:
+            token: A string containing a registration token returned
+                from the retrieve_token method.
+
+        Returns:
+            A string containing the API key.
+
+        """
+
+        data = {'providerkey': self.providerkey,
+                'token': token}
+
+        querystring = urllib.urlencode(data)
+        url = '?'.join([RETRIEVE_APIKEY_URL, querystring])
+
+        response = self._get(url)
+        self._parse_response(response)
+
+        return self._last_apikey
+
+    def retrieve_token(self):
+        """Get a registration token and approval URL.
+
+        A user follows the URL and logs in to the Prowl website to
+        approve you sending them push notifications. If you've
+        associated a 'Retrieve success URL' with your provider key, they
+        will be redirected there.
+
+        Returns:
+            A two-item tuple where the first item is a string containing
+            a registration token, and the second item is a string
+            containing the associated URL.
+        """
+
+        data = {'providerkey': self.providerkey}
+
+        querystring = urllib.urlencode(data)
+        url = '?'.join([RETRIEVE_TOKEN_URL, querystring])
+
+        response = self._get(url)
+        self._parse_response(response)
+
+        return self._last_token, self._last_token_url
+
+    def verify_user(self, apikey):
+        """Verify an API key for a user.
+
+        Args:
+            apikey: A string of 40 characters containing an API key.
+
+        Raises:
+            pushnotify.exceptions.RateLimitExceeded
+            pushnotify.exceptions.ServerError
+            pushnotify.exceptions.UnknownError
+            pushnotify.exceptions.UnrecognizedResponseError
+
+        Returns:
+            A boolean containing True if the API key is valid, and False
+            if it is not.
+
+        """
+
+        data = {'apikey': apikey}
+
+        if self.providerkey:
+            data['providerkey'] = self.providerkey
+
+        querystring = urllib.urlencode(data)
+        url = '?'.join([VERIFY_URL, querystring])
+
+        response = self._get(url)
+        self._parse_response(response, True)
+
+        return self._last_code == '200'
+
+if __name__ == '__main__':
+    pass

File pushnotify/tests/__init__.py

+"""copyright: Copyright (c) Jeffrey Goettsch and other contributors.
+license: BSD, see LICENSE for details.
+
+"""

File pushnotify/tests/tests.py

+#!/usr/bin/env python
+# vim: set fileencoding=utf-8
+
+"""Unit tests.
+
+copyright: Copyright (c) Jeffrey Goettsch and other contributors.
+license: BSD, see LICENSE for details.
+
+"""
+
+
+import imp
+import os
+import unittest
+
+from pushnotify import exceptions
+from pushnotify import nma
+from pushnotify import prowl
+from pushnotify import pushover
+
+try:
+    imp.find_module('nmakeys', [os.path.dirname(__file__)])
+except ImportError:
+    NMA_API_KEYS = []
+    NMA_DEVELOPER_KEY = ''
+else:
+    from pushnotify.tests.nmakeys import API_KEYS as NMA_API_KEYS
+    from pushnotify.tests.nmakeys import DEVELOPER_KEY as NMA_DEVELOPER_KEY
+try:
+    imp.find_module('prowlkeys', [os.path.dirname(__file__)])
+except ImportError:
+    PROWL_API_KEYS = []
+    PROWL_PROVIDER_KEY = ''
+    PROWL_REG_TOKEN = ''
+else:
+    from pushnotify.tests.prowlkeys import API_KEYS as PROWL_API_KEYS
+    from pushnotify.tests.prowlkeys import PROVIDER_KEY as PROWL_PROVIDER_KEY
+    from pushnotify.tests.prowlkeys import REG_TOKEN as PROWL_REG_TOKEN
+try:
+    imp.find_module('pushoverkeys', [os.path.dirname(__file__)])
+except ImportError:
+    PUSHOVER_TOKEN = ''
+    PUSHOVER_USER = ''
+    PUSHOVER_DEVICE = ''
+else:
+    from pushnotify.tests.pushoverkeys import TOKEN as PUSHOVER_TOKEN
+    from pushnotify.tests.pushoverkeys import USER as PUSHOVER_USER
+    from pushnotify.tests.pushoverkeys import DEVICE as PUSHOVER_DEVICE
+
+
+class NMATest(unittest.TestCase):
+    """Test the Notify my Android client.
+
+    """
+
+    def setUp(self):
+
+        self.client = nma.Client(NMA_API_KEYS, NMA_DEVELOPER_KEY)
+
+        self.app = 'pushnotify unit tests'
+        self.event = 'unit test: test_notify'
+        self.desc = 'valid notification test for pushnotify'
+
+    def test_notify_valid(self):
+        """Test notify with valid notifications.
+
+        """
+
+        # valid notification
+
+        self.client.notify(self.app, self.event, self.desc)
+
+        # valid notification, extra arguments, html
+
+        html_desc = '<h1>{0}</h1><p>{1}<br>{2}</p>'.format(
+            self.app, self.event, self.desc)
+        priority = 0
+        url = nma.NOTIFY_URL
+
+        self.client.notify(self.app, self.event, html_desc,
+                           kwargs={'priority': priority, 'url': url,
+                                   'content-type': 'text/html'})
+
+    def test_notify_invalid(self):
+        """Test notify with invalid notifications.
+
+        """
+
+        # invalid API key
+
+        char = self.client.apikeys[0][0]
+        apikey = self.client.apikeys[0].replace(char, '_')
+        self.client.apikeys = [apikey, ]
+        self.client.developerkey = ''
+
+        self.assertRaises(exceptions.ApiKeyError,
+                          self.client.notify, self.app, self.event, self.desc)
+
+        self.client.apikeys = NMA_API_KEYS
+        self.client.developerkey = NMA_DEVELOPER_KEY
+
+        # invalid argument lengths
+
+        bad_app = 'a' * 257
+        self.assertRaises(exceptions.FormatError,
+                          self.client.notify, bad_app, self.event, self.desc)
+
+    def test_verify_valid(self):
+        """Test verify with a valid API key.
+
+        """
+
+        self.assertTrue(self.client.verify(self.client.apikeys[0]))
+
+    def test_verify_invalid(self):
+        """Test verify with invalid API keys.
+
+        """
+
+        # invalid API key of incorrect length
+
+        apikey = u'{0}{1}'.format(self.client.apikeys[0], '1')
+
+        self.assertFalse(self.client.verify(apikey))
+
+        # invalid API key of correct length
+
+        char = self.client.apikeys[0][0]
+        apikey = self.client.apikeys[0].replace(char, '_')
+
+        self.assertFalse(self.client.verify(apikey))
+
+
+class ProwlTest(unittest.TestCase):
+    """Test the Prowl client.
+
+    """
+
+    def setUp(self):
+
+        self.client = prowl.Client(PROWL_API_KEYS, PROWL_PROVIDER_KEY)
+
+        self.app = 'pushnotify unit tests'
+        self.event = 'unit test: test_notify'
+        self.desc = 'valid notification test for pushnotify'
+
+    def test_notify_valid(self):
+        """Test notify with a valid notification.
+
+        """
+
+        self.client.notify(self.app, self.event, self.desc,
+                           kwargs={'priority': 0, 'url': 'http://google.com/'})
+
+    def test_notify_invalid(self):
+        """Test notify with invalid notifications.
+
+        """
+
+        # invalid API key
+
+        char = self.client.apikeys[0][0]
+        apikey = self.client.apikeys[0].replace(char, '_')
+        self.client.apikeys = [apikey, ]
+        self.client.developerkey = ''
+
+        self.assertRaises(exceptions.ApiKeyError,
+                          self.client.notify, self.app, self.event, self.desc)
+
+        self.client.apikeys = NMA_API_KEYS
+        self.client.developerkey = NMA_DEVELOPER_KEY
+
+        # invalid argument lengths
+
+        bad_app = 'a' * 257
+        self.assertRaises(exceptions.FormatError,
+                          self.client.notify, bad_app, self.event, self.desc)
+
+    def test_retrieve_apikey_valid(self):
+        """Test retrieve_apikey with a valid token.
+
+        """
+
+        apikey = self.client.retrieve_apikey(PROWL_REG_TOKEN)
+        self.assertTrue(apikey)
+        self.assertIs(type(apikey), str)
+
+    def test_retrieve_apikey_invalid(self):
+        """Test retrieve_apikey with an invalid token and provider key.
+
+        """
+
+        # invalid registration token
+
+        self.assertRaises(exceptions.PermissionDenied,
+                          self.client.retrieve_apikey, PROWL_REG_TOKEN[0:-1])
+
+        # invalid providerkey
+
+        self.client.providerkey = self.client.providerkey[0:-1]
+        self.assertRaises(exceptions.ApiKeyError,
+                          self.client.retrieve_apikey, PROWL_REG_TOKEN)
+
+    def test_retrieve_token_valid(self):
+        """Test retrieve_token with a valid providerkey.
+
+        """
+
+        token = self.client.retrieve_token()
+        self.assertTrue(token)
+        self.assertEqual(len(token), 2)
+        self.assertIs(type(token[0]), str)
+        self.assertIs(type(token[1]), str)
+
+    def test_retrieve_token_invalid(self):
+        """Test retrieve_token with an invalid providerkey.
+
+        """
+
+        self.client.providerkey = self.client.providerkey[0:-1]
+        self.assertRaises(exceptions.ApiKeyError,
+                          self.client.retrieve_token)
+
+    def test_verify_user_valid(self):
+        """Test verify_user with a valid API key.
+
+        """
+
+        self.assertTrue(self.client.verify_user(self.client.apikeys[0]))
+
+    def test_verify_user_invalid(self):
+        """Test verify_user with invalid API keys.
+
+        """
+
+        # invalid API key of incorrect length
+
+        apikey = u'{0}{1}'.format(self.client.apikeys[0], '1')
+
+        self.assertFalse(self.client.verify_user(apikey))
+
+        # invalid API key of correct length
+
+        char = self.client.apikeys[0][0]
+        apikey = self.client.apikeys[0].replace(char, '_')
+
+        self.assertFalse(self.client.verify_user(apikey))
+
+
+class PushoverTest(unittest.TestCase):
+    """Test the Pushover client.
+
+    """
+
+    def setUp(self):
+
+        self.client = pushover.Client(PUSHOVER_TOKEN,
+                                      [(PUSHOVER_USER, PUSHOVER_DEVICE)])
+
+        self.title = 'pushnotify unit tests'
+        self.message = 'valid notification test for pushnotify'
+
+    def test_notify_valid(self):
+        """Test notify with a valid notification.
+
+        """
+
+        self.client.notify(self.title, self.message,
+                           kwargs={'priority': 1, 'url': 'http://google.com/',
+                                   'url_title': 'Google'})
+
+    def test_notify_invalid_token(self):
+        """Test notify with an invalid token.
+
+        """
+
+        char = self.client.token[0]
+        bad_token = self.client.token.replace(char, '_')
+        self.client.token = bad_token
+
+        self.assertRaises(exceptions.ApiKeyError, self.client.notify,
+                          self.title, self.message)
+
+    def test_notify_invalid_user(self):
+        """Test notify with an invalid user.
+
+        """
+
+        char = self.client.users[0][0][0]
+        bad_users = (self.client.users[0][0].replace(char, '_'),
+                     PUSHOVER_DEVICE)
+        self.client.users = bad_users
+
+        self.assertRaises(exceptions.ApiKeyError, self.client.notify,
+                          self.title, self.message)
+
+    def test_notify_invalid_device(self):
+        """Test notify with an invalid device.
+
+        """
+
+        char = self.client.users[0][1][0]
+        bad_users = (PUSHOVER_USER, self.client.users[0][1].replace(char, '_'))
+        self.client.users = bad_users
+
+        self.assertRaises(exceptions.ApiKeyError, self.client.notify,
+                          self.title, self.message)
+
+    def test_notify_invalid_args(self):
+        """Test notify with invalid argument lengths.
+
+        """
+
+        msg = 'a' * 513
+
+        self.assertRaises(exceptions.FormatError, self.client.notify,
+                          self.title, msg)
+
+    def test_verify_user_valid(self):
+        """Test veriy_user with a valid user token.
+
+        """
+
+        self.assertTrue(self.client.verify_user(PUSHOVER_USER))
+
+    def test_verify_user_invalid(self):
+        """Test verify_user with an invalid user token.
+
+        """
+
+        self.assertFalse(self.client.verify_user('foo'))
+
+    def test_verify_device_valid(self):
+        """Test verify_device with a valid device string.
+
+        """
+
+        self.assertTrue(self.client.verify_device(PUSHOVER_USER,
+                                                  PUSHOVER_DEVICE))
+
+    def test_verify_device_invalid(self):
+        """Test verify_device with an invalid device string.
+
+        """
+
+        self.assertFalse(self.client.verify_device(PUSHOVER_USER, 'foo'))
+
+    def test_verify_device_invalid_user(self):
+        """Test verify_device with an invalid user token.
+
+        """
+
+        self.assertRaises(exceptions.ApiKeyError, self.client.verify_device,
+                          'foo', PUSHOVER_DEVICE)
+
+
+if __name__ == '__main__':
+    pass
 #!/usr/bin/env sh
 
 set -x
-nosetests --with-coverage --cover-package=pushnotify tests/tests.py
+nosetests --with-coverage --cover-package=pushnotify pushnotify/tests/tests.py
 from distutils.core import setup
 
 
-version = '0.2.1'
+version = '0.3'
 
 
 setup(
     name='pushnotify',
+    packages=['pushnotify', 'pushnotify.tests'],
     version=version,
-    author='Jeffrey Goettsch and other contributors',
+    author='Jeffrey Goettsch',
     author_email='jgoettsch@gmail.com',
     url='https://bitbucket.org/jgoettsch/py-pushnotify/',
-    description=('A package for sending push notifications to Android '
-                 'and iOS devices.'),
-    long_description=('A package for sending push notifications to '
-                      'Android and iOS devices. It requires Notify My '
-                      'Android or Pushover be installed on each device.'),
+    description=(
+        'py-pushnotify is a package for sending push notifications to '
+        'Android and iOS devices.'),
+    long_description=(
+        'py-pushnotify is a package for sending push notifications. It '
+        'currently supports Android devices running Notify My Android '
+        'and Pushover, and iOS devices running Pushover and Prowl.'),
     download_url=('https://bitbucket.org/jgoettsch/py-pushnotify/get/'
                   '{0}.tar.gz').format(version),
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
         'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
         'Programming Language :: Python',
         'Topic :: Software Development',
         'Topic :: Software Development :: Libraries',
-        'Topic :: Software Development :: Libraries :: Python Modules'])
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: System :: Monitoring',
+        'Topic :: System :: Systems Administration'])

File tests/__init__.py

-"""copyright: Copyright (c) Jeffrey Goettsch and other contributors.
-license: BSD, see LICENSE for details.
-
-"""

File tests/tests.py

-#!/usr/bin/env python
-# vim: set fileencoding=utf-8
-
-"""copyright: Copyright (c) Jeffrey Goettsch and other contributors.
-license: BSD, see LICENSE for details.
-
-"""
-
-
-import imp
-import os
-import unittest
-
-from pushnotify import exceptions
-from pushnotify import nma
-from pushnotify import pushover
-
-try:
-    imp.find_module('nmakeys', [os.path.dirname(__file__)])
-except ImportError:
-    NMA_API_KEYS = {}
-    NMA_DEVELOPER_KEY = ''
-else:
-    from nmakeys import API_KEYS as NMA_API_KEYS
-    from nmakeys import DEVELOPER_KEY as NMA_DEVELOPER_KEY
-
-try:
-    imp.find_module('pushoverkeys', [os.path.dirname(__file__)])
-except ImportError:
-    PUSHOVER_TOKEN = ''
-    PUSHOVER_USER = ''
-    PUSHOVER_DEVICE = ''
-else:
-    from pushoverkeys import TOKEN as PUSHOVER_TOKEN
-    from pushoverkeys import USER as PUSHOVER_USER
-    from pushoverkeys import DEVICE as PUSHOVER_DEVICE
-
-
-class NMATest(unittest.TestCase):
-
-    def setUp(self):
-
-        self.client = nma.Client(NMA_API_KEYS, NMA_DEVELOPER_KEY)
-
-        self.app = 'pushnotify unit tests'
-        self.event = 'unit test: test_notify'
-        self.desc = 'valid notification test for pushnotify'
-
-    def test_notify_valid(self):
-        """Test notify with valid notifications.
-
-        """
-
-        """valid notification"""
-
-        self.client.notify(self.app, self.event, self.desc)
-
-        """valid notification, extra arguments, html"""
-
-        html_desc = '<h1>{0}</h1><p>{1}<br>{2}</p>'.format(
-            self.app, self.event, self.desc)
-        priority = 0
-        url = nma.NOTIFY_URL
-
-        self.client.notify(self.app, self.event, html_desc,
-                           kwargs={'priority': priority, 'url': url,
-                                   'content-type': 'text/html'})
-
-    def test_notify_invalid(self):
-        """Test notify with invalid notifications.
-
-        """
-
-        """invalid API key"""
-
-        char = self.client.apikeys[0][0]
-        apikey = self.client.apikeys[0].replace(char, '_')
-        self.client.apikeys = [apikey, ]
-        self.client.developerkey = ''
-
-        self.assertRaises(exceptions.ApiKeyError,
-                          self.client.notify, self.app, self.event, self.desc)
-
-        self.client.apikeys = NMA_API_KEYS
-        self.client.developerkey = NMA_DEVELOPER_KEY
-
-        """invalid argument lengths"""
-
-        bad_app = 'a' * 257
-        self.assertRaises(exceptions.FormatError,
-                          self.client.notify, bad_app, self.event, self.desc)
-
-    def test_verify_valid(self):
-        """Test verify with a valid API key.
-
-        """
-
-        self.assertTrue(self.client.verify(self.client.apikeys[0]))
-
-    def test_verify_invalid(self):
-        """Test verify with invalid API keys.
-
-        """
-
-        """invalid API key of incorrect length"""
-
-        apikey = u'{0}{1}'.format(self.client.apikeys[0], '1')
-
-        self.assertFalse(self.client.verify(apikey))
-
-        """invalid API key of correct length"""
-
-        char = self.client.apikeys[0][0]
-        apikey = self.client.apikeys[0].replace(char, '_')
-
-        self.assertFalse(self.client.verify(apikey))
-
-
-class PushoverTest(unittest.TestCase):
-
-    def setUp(self):
-
-        self.client = pushover.Client(PUSHOVER_TOKEN,
-                                      [(PUSHOVER_USER, PUSHOVER_DEVICE)])
-
-        self.title = 'pushnotify unit tests'
-        self.message = 'valid notification test for pushnotify'
-
-    def test_notify_valid(self):
-
-        """valid notification"""
-
-        self.client.notify(self.title, self.message,
-                           kwargs={'priority': 1, 'url': 'http://google.com/',
-                                   'url_title': 'Google'})
-
-    def test_notify_invalid_token(self):
-
-        """invalid token"""
-
-        char = self.client.token[0]
-        bad_token = self.client.token.replace(char, '_')
-        self.client.token = bad_token
-
-        self.assertRaises(exceptions.ApiKeyError, self.client.notify,
-                          self.title, self.message)
-
-    def test_notify_invalid_user(self):
-
-        """invalid user"""
-
-        char = self.client.users[0][0][0]
-        bad_users = (self.client.users[0][0].replace(char, '_'),
-                     PUSHOVER_DEVICE)
-        self.client.users = bad_users
-
-        self.assertRaises(exceptions.ApiKeyError, self.client.notify,
-                          self.title, self.message)
-
-    def test_notify_invalid_device(self):
-
-        """invalid device"""
-
-        char = self.client.users[0][1][0]
-        bad_users = (PUSHOVER_USER, self.client.users[0][1].replace(char, '_'))
-        self.client.users = bad_users
-
-        self.assertRaises(exceptions.ApiKeyError, self.client.notify,
-                          self.title, self.message)
-
-    def test_notify_invalid_args(self):
-
-        """invalid argument lengths"""
-
-        msg = 'a' * 513
-
-        self.assertRaises(exceptions.FormatError, self.client.notify,
-                          self.title, msg)
-
-    def test_verify_user_valid(self):
-        """Test veriy_user with a valid user token.
-
-        """
-
-        self.assertTrue(self.client.verify_user(PUSHOVER_USER))
-
-    def test_verify_user_invalid(self):
-        """Test verify_user with an invalid user token.
-
-        """
-
-        self.assertFalse(self.client.verify_user('foo'))
-
-    def test_verify_device_valid(self):
-        """Test verify_device with a valid device string.
-
-        """
-
-        self.assertTrue(self.client.verify_device(PUSHOVER_USER,
-                                                  PUSHOVER_DEVICE))
-
-    def test_verify_device_invalid(self):
-        """Test verify_device with an invalid device string.
-
-        """
-
-        self.assertFalse(self.client.verify_device(PUSHOVER_USER, 'foo'))
-
-    def test_verify_device_invalid_user(self):
-        """Test verify_device with an invalid user token.
-
-        """
-
-        self.assertRaises(exceptions.ApiKeyError, self.client.verify_device,
-                          'foo', PUSHOVER_DEVICE)
-
-
-if __name__ == '__main__':
-    pass