aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pushover.py247
-rw-r--r--setup.py2
2 files changed, 104 insertions, 145 deletions
diff --git a/pushover.py b/pushover.py
index 06400cd..b476158 100644
--- a/pushover.py
+++ b/pushover.py
@@ -1,4 +1,4 @@
-# pushover 0.4
+# pushover 1.0
#
# Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
@@ -15,14 +15,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import time
-from ConfigParser import RawConfigParser, NoSectionError
+from ConfigParser import RawConfigParser, NoSectionError, NoOptionError
from argparse import ArgumentParser, RawDescriptionHelpFormatter
import os
import requests
-__all__ = ["init", "get_sounds", "Client", "MessageRequest",
- "InitError", "RequestError", "UserError"]
+__all__ = ["Pushover", "MessageRequest", "ApiTokenError", "RequestError",
+ "read_config"]
BASE_URL = "https://api.pushover.net/1/"
MESSAGE_URL = BASE_URL + "messages.json"
@@ -31,52 +31,14 @@ SOUND_URL = BASE_URL + "sounds.json"
RECEIPT_URL = BASE_URL + "receipts/"
GLANCE_URL = BASE_URL + "glances.json"
-SOUNDS = None
-TOKEN = None
-
-def get_sounds():
- """Fetch and return a list of sounds (as a list of strings) recognized by
- Pushover and that can be used in a notification message.
-
- The result is cached: a request is made to the Pushover server only
- the first time this function is called.
- """
- global SOUNDS
- if not SOUNDS:
- request = Request("get", SOUND_URL, {})
- SOUNDS = request.answer["sounds"]
- return SOUNDS
-
-
-def init(token, sound=False):
- """Initialize the module by setting the application token which will be
- used to send messages. If ``sound`` is ``True`` also returns the list of
- valid sounds by calling the :func:`get_sounds` function.
- """
- global TOKEN
- TOKEN = token
- if sound:
- return get_sounds()
-
-
-class InitError(Exception):
+class ApiTokenError(Exception):
"""Exception which is raised when trying to send a message before
initializing the module.
"""
def __str__(self):
- return ("No api_token provided. Init the pushover module by "
- "calling the init function")
-
-
-class UserError(Exception):
- """Exception which is raised when initializing a :class:`Client` class
- without specifying a :attr:`user_key` attribute.
- """
-
- def __str__(self):
- return "No user_key attribute provided."
+ return "No api_token provided."
class RequestError(Exception):
@@ -95,15 +57,11 @@ class RequestError(Exception):
class Request:
"""Base class to send a request to the Pushover server and check the return
- status code. The request is sent on the instance initialization and raises
+ status code. The request is sent on instantiation and raises
a :class:`RequestError` exception when the request is rejected.
"""
def __init__(self, request_type, url, payload, files={}):
- if not TOKEN:
- raise InitError
-
- payload["token"] = TOKEN
request = getattr(requests, request_type)(url, params=payload, files=files)
self.answer = request.json()
if 400 <= request.status_code < 500:
@@ -127,6 +85,7 @@ class MessageRequest(Request):
def __init__(self, payload, files):
Request.__init__(self, "post", MESSAGE_URL, payload, files)
+ self.token = payload["token"]
self.receipt = None
if payload.get("priority", 0) == 2:
self.receipt = self.answer["receipt"]
@@ -160,7 +119,9 @@ class MessageRequest(Request):
"""
if (self.receipt and not any(getattr(self, parameter)
for parameter in self.parameters)):
- request = Request("get", RECEIPT_URL + self.receipt + ".json", {})
+ request = Request("get", RECEIPT_URL + self.receipt + ".json", {
+ "token": self.token
+ })
for param, when in self.parameters.iteritems():
setattr(self, param, bool(request.answer[param]))
setattr(self, when, request.answer[when])
@@ -178,7 +139,9 @@ class MessageRequest(Request):
if (self.receipt and not any(getattr(self, parameter)
for parameter in self.parameters)):
request = Request("post", RECEIPT_URL + self.receipt
- + "/cancel.json", {})
+ + "/cancel.json", {
+ "token": self.token
+ })
return request
@@ -192,70 +155,67 @@ class GlanceRequest(Request):
Request.__init__(self, "post", GLANCE_URL, payload)
-class Client:
- """This is the main class of the module. It represents a specific Pushover
- user to whom messages will be sent when calling the :func:`send_message`
- method.
+class Pushover:
+ """This is the main class of the module. It represents a Pushover app, i.e.
+ it is tied to an API token.
- * ``user_key``: the Pushover's ID of the user.
- * ``device``: if provided further ties the Client object to the specified
- device.
- * ``api_token``: if provided and the module wasn't previously initialized,
- call the :func:`init` function to initialize it.
- * ``config_path``: configuration file from which to import unprovided
- parameters. See Configuration_.
- * ``profile``: section of the configuration file to import parameters from.
+ * ``token``: Pushover API token
"""
- def __init__(self, user_key=None, device=None, api_token=None,
- config_path="~/.pushoverrc", profile="Default"):
- params = _get_config(profile, config_path, user_key, api_token, device)
- self.user_key = params["user_key"]
- if not self.user_key:
- raise UserError
- self.device = params["device"]
- self.devices = []
+ _SOUNDS = None
+
+ def __init__(self, token):
+ self.token = token
- def verify(self, device=None):
- """Verify that the Client object is tied to an existing Pushover user
- and fetches a list of this user active devices accessible in the
- :attr:`devices` attribute. Returns a boolean depending of the validity
- of the user.
+ @property
+ def sounds(self):
+ """Return a list of sounds (as a list of strings) recognized
+ by Pushover and that can be used in a notification message.
+
+ The result is cached: a request is made to the Pushover server only
+ the first time this function is called.
+ """
+ if not Pushover._SOUNDS:
+ request = Request("get", SOUND_URL, {"token": self.token})
+ Pushover._SOUNDS = request.answer["sounds"].keys()
+ return Pushover._SOUNDS
+
+ def verify(self, user_key, device=None):
+ """Verify that the `user_key` and optional `device` exist. Returns
+ `None` when the user/device does not exist or a list of the user's
+ devices otherwise.
"""
- payload = {"user": self.user_key}
- device = device or self.device
+ payload = {"user": self.user_key, "token": self.token}
if device:
payload["device"] = device
try:
request = Request("post", USER_URL, payload)
except RequestError:
- return False
+ return None
- self.devices = request.answer["devices"]
- return True
+ return request.answer["devices"]
- def send_message(self, message, attachment=None, **kwords):
- """Send a message to the user. It is possible to specify additional
- properties of the message by passing keyword arguments. The list of
- valid keywords is ``title, priority, sound, callback, timestamp, url,
- url_title, device, retry, expire and html`` which are described in the
- Pushover API documentation. For convenience, you can simply set
- ``timestamp=True`` to set the timestamp to the current timestamp.
+ def send_message(self, user_key, message, **kwords):
+ """Send `message` to the user specified by `user_key`. It is possible
+ to specify additional properties of the message by passing keyword
+ arguments. The list of valid keywords is ``title, priority, sound,
+ callback, timestamp, url, url_title, device, retry, expire and html``
+ which are described in the Pushover API documentation.
+
+ For convenience, you can simply set ``timestamp=True`` to set the
+ timestamp to the current timestamp.
An image can be attached to a message by passing a file-like object
- with the `attachment` keyword argument.
+ to the `attachment` keyword argument.
This method returns a :class:`MessageRequest` object.
"""
valid_keywords = ["title", "priority", "sound", "callback",
"timestamp", "url", "url_title", "device",
- "retry", "expire", "html"]
-
- payload = {"message": message, "user": self.user_key}
- files = {'attachment': attachment} if attachment else {}
- if self.device:
- payload["device"] = self.device
+ "retry", "expire", "html", "attachment"]
+ payload = {"message": message, "user": user_key, "token": self.token}
+ files = {}
for key, value in kwords.iteritems():
if key not in valid_keywords:
raise ValueError("{0}: invalid message parameter".format(key))
@@ -263,18 +223,18 @@ class Client:
if key == "timestamp" and value is True:
payload[key] = int(time.time())
elif key == "sound":
- if not SOUNDS:
- get_sounds()
- if value not in SOUNDS:
+ if value not in self.sounds:
raise ValueError("{0}: invalid sound".format(value))
else:
payload[key] = value
+ elif key == "attachment":
+ files["attachment"] = value
elif value:
payload[key] = value
return MessageRequest(payload, files)
- def send_glance(self, text=None, **kwords):
+ def send_glance(self, user_key, **kwords):
"""Send a glance to the user. The default property is ``text``,
as this is used on most glances, however a valid glance does not
need to require text and can be constructed using any combination
@@ -284,13 +244,10 @@ class Client:
This method returns a :class:`GlanceRequest` object.
"""
- valid_keywords = ["title", "text", "subtext", "count", "percent"]
+ valid_keywords = ["title", "text", "subtext", "count", "percent",
+ "device"]
- payload = {"user": self.user_key}
- if text:
- payload["text"] = text
- if self.device:
- payload["device"] = self.device
+ payload = {"user": user_key, "token": self.token}
for key, value in kwords.iteritems():
if key not in valid_keywords:
@@ -300,28 +257,23 @@ class Client:
return GlanceRequest(payload)
-def _get_config(profile='Default', config_path='~/.pushoverrc',
- user_key=None, api_token=None, device=None):
+def read_config(config_path):
config_path = os.path.expanduser(config_path)
config = RawConfigParser()
- config.read(config_path)
- params = {"user_key": None, "api_token": None, "device": None}
- try:
- params.update(dict(config.items(profile)))
- except NoSectionError:
- pass
- if user_key:
- params["user_key"] = user_key
- if api_token:
- params["api_token"] = api_token
- if device:
- params["device"] = device
-
- if not TOKEN:
- init(params["api_token"])
- if not TOKEN:
- raise InitError
-
+ params = {"users": {}}
+ files = config.read(config_path)
+ if not files:
+ return params
+ params["token"] = config.get("main", "token")
+ for name in config.sections():
+ if name != "main":
+ user = {}
+ user["user_key"] = config.get(name, "user_key")
+ try:
+ user["device"] = config.get(name, "device")
+ except NoOptionError:
+ user["device"] = None
+ params["users"][name] = user
return params
@@ -330,38 +282,45 @@ def main():
formatter_class=RawDescriptionHelpFormatter,
epilog="""
For more details and bug reports, see: https://github.com/Thibauth/python-pushover""")
- parser.add_argument("--api-token", help="Pushover application token")
- parser.add_argument("--user-key", "-u", help="Pushover user key")
- parser.add_argument("message", help="message to send")
- parser.add_argument("--title", "-t", help="message title")
- parser.add_argument("--priority", "-p", help="message priority (-1, 0, 1 or 2)", type=int)
- parser.add_argument("--retry", "-r", help="how often (in seconds) the Pushover servers will send the same notification to the user", type=int)
- parser.add_argument("--expire", "-e", help="how many seconds your notification will continue to be retried for (every retry seconds).", type=int)
- parser.add_argument("--url", help="additional url")
- parser.add_argument("--url-title", help="additional url title")
+ parser.add_argument("--token", help="API token")
+ parser.add_argument("--user", "-u", help="User key or section name in the configuration", required=True)
parser.add_argument("-c", "--config", help="configuration file\
(default: ~/.pushoverrc)", default="~/.pushoverrc")
- parser.add_argument("--profile", help="profile to read in the\
- configuration file (default: Default)",
- default="Default")
+ parser.add_argument("message", help="message to send")
+ parser.add_argument("--url", help="additional url")
+ parser.add_argument("--url-title", help="url title")
+ parser.add_argument("--title", "-t", help="message title")
+ parser.add_argument("--priority", "-p", help="notification priority (-1, 0, 1 or 2)", type=int)
+ parser.add_argument("--retry", "-r", help="resend interval in seconds (required for priority 2)", type=int)
+ parser.add_argument("--expire", "-e", help="expiration time in seconds (required for priority 2)", type=int)
parser.add_argument("--version", "-v", action="version",
help="output version information and exit",
version="""
-%(prog)s 0.4
+%(prog)s 1.0
Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.""")
args = parser.parse_args()
- if args.priority and args.priority==2 and (args.retry is None or args.expire is None):
+ params = read_config(args.config)
+ if args.priority==2 and (args.retry is None or args.expire is None):
parser.error("priority of 2 requires expire and retry")
+ if args.user in params["users"]:
+ user_key = params["users"][args.user]["user_key"]
+ device = params["users"][args.user]["device"]
+ else:
+ user_key = args.user
+ device = None
+ try:
+ token = args.token or params["token"]
+ except KeyError:
+ raise ApiTokenError()
- Client(args.user_key, None, args.api_token, args.config,
- args.profile).send_message(args.message, title=args.title,
- priority=args.priority, url=args.url,
- url_title=args.url_title, timestamp=True,
- retry=args.retry,expire=args.expire)
+ Pushover(token).send_message(user_key, args.message, device=device,
+ title=args.title, priority=args.priority, url=args.url,
+ url_title=args.url_title, timestamp=True, retry=args.retry,
+ expire=args.expire)
if __name__ == "__main__":
main()
diff --git a/setup.py b/setup.py
index 19ded46..e1b0c18 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from setuptools import setup
setup(name='python-pushover',
- version='0.4',
+ version='1.0',
description="Comprehensive bindings and command line utility for the "
"Pushover notification service",
long_description=open("README.rst").read() + "\n"