From 8095a03a06878e6d48138b58ecc852054f96f0e3 Mon Sep 17 00:00:00 2001 From: Thibaut Horel Date: Wed, 24 Oct 2018 19:42:29 -0400 Subject: Fix global token (breaks backward compatibility) #27 --- pushover.py | 251 +++++++++++++++++++++++++----------------------------------- 1 file changed, 105 insertions(+), 146 deletions(-) (limited to 'pushover.py') 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 @@ -15,14 +15,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . 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 = [] - - 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. + _SOUNDS = None + + def __init__(self, token): + self.token = token + + @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. """ - payload = {"user": self.user_key} - device = device or self.device + 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, "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 License GPLv3+: GNU GPL version 3 or later . 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") - - 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) + 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() + + 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() -- cgit v1.2.3-70-g09d2