Skip to content

Commit 8095a03

Browse files
committed
Fix global token (breaks backward compatibility) Thibauth#27
1 parent 18b6774 commit 8095a03

2 files changed

Lines changed: 106 additions & 147 deletions

File tree

pushover.py

Lines changed: 105 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# pushover 0.4
1+
# pushover 1.0
22
#
33
# Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
44

@@ -15,14 +15,14 @@
1515
# You should have received a copy of the GNU General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
import time
18-
from ConfigParser import RawConfigParser, NoSectionError
18+
from ConfigParser import RawConfigParser, NoSectionError, NoOptionError
1919
from argparse import ArgumentParser, RawDescriptionHelpFormatter
2020
import os
2121

2222
import requests
2323

24-
__all__ = ["init", "get_sounds", "Client", "MessageRequest",
25-
"InitError", "RequestError", "UserError"]
24+
__all__ = ["Pushover", "MessageRequest", "ApiTokenError", "RequestError",
25+
"read_config"]
2626

2727
BASE_URL = "https://api.pushover.net/1/"
2828
MESSAGE_URL = BASE_URL + "messages.json"
@@ -31,52 +31,14 @@
3131
RECEIPT_URL = BASE_URL + "receipts/"
3232
GLANCE_URL = BASE_URL + "glances.json"
3333

34-
SOUNDS = None
35-
TOKEN = None
3634

37-
38-
def get_sounds():
39-
"""Fetch and return a list of sounds (as a list of strings) recognized by
40-
Pushover and that can be used in a notification message.
41-
42-
The result is cached: a request is made to the Pushover server only
43-
the first time this function is called.
44-
"""
45-
global SOUNDS
46-
if not SOUNDS:
47-
request = Request("get", SOUND_URL, {})
48-
SOUNDS = request.answer["sounds"]
49-
return SOUNDS
50-
51-
52-
def init(token, sound=False):
53-
"""Initialize the module by setting the application token which will be
54-
used to send messages. If ``sound`` is ``True`` also returns the list of
55-
valid sounds by calling the :func:`get_sounds` function.
56-
"""
57-
global TOKEN
58-
TOKEN = token
59-
if sound:
60-
return get_sounds()
61-
62-
63-
class InitError(Exception):
35+
class ApiTokenError(Exception):
6436
"""Exception which is raised when trying to send a message before
6537
initializing the module.
6638
"""
6739

6840
def __str__(self):
69-
return ("No api_token provided. Init the pushover module by "
70-
"calling the init function")
71-
72-
73-
class UserError(Exception):
74-
"""Exception which is raised when initializing a :class:`Client` class
75-
without specifying a :attr:`user_key` attribute.
76-
"""
77-
78-
def __str__(self):
79-
return "No user_key attribute provided."
41+
return "No api_token provided."
8042

8143

8244
class RequestError(Exception):
@@ -95,15 +57,11 @@ def __str__(self):
9557

9658
class Request:
9759
"""Base class to send a request to the Pushover server and check the return
98-
status code. The request is sent on the instance initialization and raises
60+
status code. The request is sent on instantiation and raises
9961
a :class:`RequestError` exception when the request is rejected.
10062
"""
10163

10264
def __init__(self, request_type, url, payload, files={}):
103-
if not TOKEN:
104-
raise InitError
105-
106-
payload["token"] = TOKEN
10765
request = getattr(requests, request_type)(url, params=payload, files=files)
10866
self.answer = request.json()
10967
if 400 <= request.status_code < 500:
@@ -127,6 +85,7 @@ class MessageRequest(Request):
12785

12886
def __init__(self, payload, files):
12987
Request.__init__(self, "post", MESSAGE_URL, payload, files)
88+
self.token = payload["token"]
13089
self.receipt = None
13190
if payload.get("priority", 0) == 2:
13291
self.receipt = self.answer["receipt"]
@@ -160,7 +119,9 @@ def poll(self):
160119
"""
161120
if (self.receipt and not any(getattr(self, parameter)
162121
for parameter in self.parameters)):
163-
request = Request("get", RECEIPT_URL + self.receipt + ".json", {})
122+
request = Request("get", RECEIPT_URL + self.receipt + ".json", {
123+
"token": self.token
124+
})
164125
for param, when in self.parameters.iteritems():
165126
setattr(self, param, bool(request.answer[param]))
166127
setattr(self, when, request.answer[when])
@@ -178,7 +139,9 @@ def cancel(self):
178139
if (self.receipt and not any(getattr(self, parameter)
179140
for parameter in self.parameters)):
180141
request = Request("post", RECEIPT_URL + self.receipt
181-
+ "/cancel.json", {})
142+
+ "/cancel.json", {
143+
"token": self.token
144+
})
182145
return request
183146

184147

@@ -192,89 +155,86 @@ def __init__(self, payload):
192155
Request.__init__(self, "post", GLANCE_URL, payload)
193156

194157

195-
class Client:
196-
"""This is the main class of the module. It represents a specific Pushover
197-
user to whom messages will be sent when calling the :func:`send_message`
198-
method.
158+
class Pushover:
159+
"""This is the main class of the module. It represents a Pushover app, i.e.
160+
it is tied to an API token.
199161
200-
* ``user_key``: the Pushover's ID of the user.
201-
* ``device``: if provided further ties the Client object to the specified
202-
device.
203-
* ``api_token``: if provided and the module wasn't previously initialized,
204-
call the :func:`init` function to initialize it.
205-
* ``config_path``: configuration file from which to import unprovided
206-
parameters. See Configuration_.
207-
* ``profile``: section of the configuration file to import parameters from.
162+
* ``token``: Pushover API token
208163
"""
209164

210-
def __init__(self, user_key=None, device=None, api_token=None,
211-
config_path="~/.pushoverrc", profile="Default"):
212-
params = _get_config(profile, config_path, user_key, api_token, device)
213-
self.user_key = params["user_key"]
214-
if not self.user_key:
215-
raise UserError
216-
self.device = params["device"]
217-
self.devices = []
218-
219-
def verify(self, device=None):
220-
"""Verify that the Client object is tied to an existing Pushover user
221-
and fetches a list of this user active devices accessible in the
222-
:attr:`devices` attribute. Returns a boolean depending of the validity
223-
of the user.
165+
_SOUNDS = None
166+
167+
def __init__(self, token):
168+
self.token = token
169+
170+
@property
171+
def sounds(self):
172+
"""Return a list of sounds (as a list of strings) recognized
173+
by Pushover and that can be used in a notification message.
174+
175+
The result is cached: a request is made to the Pushover server only
176+
the first time this function is called.
224177
"""
225-
payload = {"user": self.user_key}
226-
device = device or self.device
178+
if not Pushover._SOUNDS:
179+
request = Request("get", SOUND_URL, {"token": self.token})
180+
Pushover._SOUNDS = request.answer["sounds"].keys()
181+
return Pushover._SOUNDS
182+
183+
def verify(self, user_key, device=None):
184+
"""Verify that the `user_key` and optional `device` exist. Returns
185+
`None` when the user/device does not exist or a list of the user's
186+
devices otherwise.
187+
"""
188+
payload = {"user": self.user_key, "token": self.token}
227189
if device:
228190
payload["device"] = device
229191
try:
230192
request = Request("post", USER_URL, payload)
231193
except RequestError:
232-
return False
194+
return None
233195

234-
self.devices = request.answer["devices"]
235-
return True
196+
return request.answer["devices"]
236197

237-
def send_message(self, message, attachment=None, **kwords):
238-
"""Send a message to the user. It is possible to specify additional
239-
properties of the message by passing keyword arguments. The list of
240-
valid keywords is ``title, priority, sound, callback, timestamp, url,
241-
url_title, device, retry, expire and html`` which are described in the
242-
Pushover API documentation. For convenience, you can simply set
243-
``timestamp=True`` to set the timestamp to the current timestamp.
198+
def send_message(self, user_key, message, **kwords):
199+
"""Send `message` to the user specified by `user_key`. It is possible
200+
to specify additional properties of the message by passing keyword
201+
arguments. The list of valid keywords is ``title, priority, sound,
202+
callback, timestamp, url, url_title, device, retry, expire and html``
203+
which are described in the Pushover API documentation.
204+
205+
For convenience, you can simply set ``timestamp=True`` to set the
206+
timestamp to the current timestamp.
244207
245208
An image can be attached to a message by passing a file-like object
246-
with the `attachment` keyword argument.
209+
to the `attachment` keyword argument.
247210
248211
This method returns a :class:`MessageRequest` object.
249212
"""
250213
valid_keywords = ["title", "priority", "sound", "callback",
251214
"timestamp", "url", "url_title", "device",
252-
"retry", "expire", "html"]
253-
254-
payload = {"message": message, "user": self.user_key}
255-
files = {'attachment': attachment} if attachment else {}
256-
if self.device:
257-
payload["device"] = self.device
215+
"retry", "expire", "html", "attachment"]
258216

217+
payload = {"message": message, "user": user_key, "token": self.token}
218+
files = {}
259219
for key, value in kwords.iteritems():
260220
if key not in valid_keywords:
261221
raise ValueError("{0}: invalid message parameter".format(key))
262222

263223
if key == "timestamp" and value is True:
264224
payload[key] = int(time.time())
265225
elif key == "sound":
266-
if not SOUNDS:
267-
get_sounds()
268-
if value not in SOUNDS:
226+
if value not in self.sounds:
269227
raise ValueError("{0}: invalid sound".format(value))
270228
else:
271229
payload[key] = value
230+
elif key == "attachment":
231+
files["attachment"] = value
272232
elif value:
273233
payload[key] = value
274234

275235
return MessageRequest(payload, files)
276236

277-
def send_glance(self, text=None, **kwords):
237+
def send_glance(self, user_key, **kwords):
278238
"""Send a glance to the user. The default property is ``text``,
279239
as this is used on most glances, however a valid glance does not
280240
need to require text and can be constructed using any combination
@@ -284,13 +244,10 @@ def send_glance(self, text=None, **kwords):
284244
285245
This method returns a :class:`GlanceRequest` object.
286246
"""
287-
valid_keywords = ["title", "text", "subtext", "count", "percent"]
247+
valid_keywords = ["title", "text", "subtext", "count", "percent",
248+
"device"]
288249

289-
payload = {"user": self.user_key}
290-
if text:
291-
payload["text"] = text
292-
if self.device:
293-
payload["device"] = self.device
250+
payload = {"user": user_key, "token": self.token}
294251

295252
for key, value in kwords.iteritems():
296253
if key not in valid_keywords:
@@ -300,28 +257,23 @@ def send_glance(self, text=None, **kwords):
300257
return GlanceRequest(payload)
301258

302259

303-
def _get_config(profile='Default', config_path='~/.pushoverrc',
304-
user_key=None, api_token=None, device=None):
260+
def read_config(config_path):
305261
config_path = os.path.expanduser(config_path)
306262
config = RawConfigParser()
307-
config.read(config_path)
308-
params = {"user_key": None, "api_token": None, "device": None}
309-
try:
310-
params.update(dict(config.items(profile)))
311-
except NoSectionError:
312-
pass
313-
if user_key:
314-
params["user_key"] = user_key
315-
if api_token:
316-
params["api_token"] = api_token
317-
if device:
318-
params["device"] = device
319-
320-
if not TOKEN:
321-
init(params["api_token"])
322-
if not TOKEN:
323-
raise InitError
324-
263+
params = {"users": {}}
264+
files = config.read(config_path)
265+
if not files:
266+
return params
267+
params["token"] = config.get("main", "token")
268+
for name in config.sections():
269+
if name != "main":
270+
user = {}
271+
user["user_key"] = config.get(name, "user_key")
272+
try:
273+
user["device"] = config.get(name, "device")
274+
except NoOptionError:
275+
user["device"] = None
276+
params["users"][name] = user
325277
return params
326278

327279

@@ -330,38 +282,45 @@ def main():
330282
formatter_class=RawDescriptionHelpFormatter,
331283
epilog="""
332284
For more details and bug reports, see: https://github.com/Thibauth/python-pushover""")
333-
parser.add_argument("--api-token", help="Pushover application token")
334-
parser.add_argument("--user-key", "-u", help="Pushover user key")
335-
parser.add_argument("message", help="message to send")
336-
parser.add_argument("--title", "-t", help="message title")
337-
parser.add_argument("--priority", "-p", help="message priority (-1, 0, 1 or 2)", type=int)
338-
parser.add_argument("--retry", "-r", help="how often (in seconds) the Pushover servers will send the same notification to the user", type=int)
339-
parser.add_argument("--expire", "-e", help="how many seconds your notification will continue to be retried for (every retry seconds).", type=int)
340-
parser.add_argument("--url", help="additional url")
341-
parser.add_argument("--url-title", help="additional url title")
285+
parser.add_argument("--token", help="API token")
286+
parser.add_argument("--user", "-u", help="User key or section name in the configuration", required=True)
342287
parser.add_argument("-c", "--config", help="configuration file\
343288
(default: ~/.pushoverrc)", default="~/.pushoverrc")
344-
parser.add_argument("--profile", help="profile to read in the\
345-
configuration file (default: Default)",
346-
default="Default")
289+
parser.add_argument("message", help="message to send")
290+
parser.add_argument("--url", help="additional url")
291+
parser.add_argument("--url-title", help="url title")
292+
parser.add_argument("--title", "-t", help="message title")
293+
parser.add_argument("--priority", "-p", help="notification priority (-1, 0, 1 or 2)", type=int)
294+
parser.add_argument("--retry", "-r", help="resend interval in seconds (required for priority 2)", type=int)
295+
parser.add_argument("--expire", "-e", help="expiration time in seconds (required for priority 2)", type=int)
347296
parser.add_argument("--version", "-v", action="version",
348297
help="output version information and exit",
349298
version="""
350-
%(prog)s 0.4
299+
%(prog)s 1.0
351300
Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
352301
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
353302
This is free software: you are free to change and redistribute it.
354303
There is NO WARRANTY, to the extent permitted by law.""")
355304

356305
args = parser.parse_args()
357-
if args.priority and args.priority==2 and (args.retry is None or args.expire is None):
306+
params = read_config(args.config)
307+
if args.priority==2 and (args.retry is None or args.expire is None):
358308
parser.error("priority of 2 requires expire and retry")
359-
360-
Client(args.user_key, None, args.api_token, args.config,
361-
args.profile).send_message(args.message, title=args.title,
362-
priority=args.priority, url=args.url,
363-
url_title=args.url_title, timestamp=True,
364-
retry=args.retry,expire=args.expire)
309+
if args.user in params["users"]:
310+
user_key = params["users"][args.user]["user_key"]
311+
device = params["users"][args.user]["device"]
312+
else:
313+
user_key = args.user
314+
device = None
315+
try:
316+
token = args.token or params["token"]
317+
except KeyError:
318+
raise ApiTokenError()
319+
320+
Pushover(token).send_message(user_key, args.message, device=device,
321+
title=args.title, priority=args.priority, url=args.url,
322+
url_title=args.url_title, timestamp=True, retry=args.retry,
323+
expire=args.expire)
365324

366325
if __name__ == "__main__":
367326
main()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from setuptools import setup
44

55
setup(name='python-pushover',
6-
version='0.4',
6+
version='1.0',
77
description="Comprehensive bindings and command line utility for the "
88
"Pushover notification service",
99
long_description=open("README.rst").read() + "\n"

0 commit comments

Comments
 (0)