Skip to content

Commit dbac575

Browse files
committed
Code refactoring
1 parent 8095a03 commit dbac575

1 file changed

Lines changed: 116 additions & 158 deletions

File tree

pushover.py

Lines changed: 116 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,15 @@
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, NoOptionError
19-
from argparse import ArgumentParser, RawDescriptionHelpFormatter
20-
import os
21-
2218
import requests
2319

24-
__all__ = ["Pushover", "MessageRequest", "ApiTokenError", "RequestError",
25-
"read_config"]
26-
2720
BASE_URL = "https://api.pushover.net/1/"
2821
MESSAGE_URL = BASE_URL + "messages.json"
2922
USER_URL = BASE_URL + "users/validate.json"
3023
SOUND_URL = BASE_URL + "sounds.json"
3124
RECEIPT_URL = BASE_URL + "receipts/"
3225
GLANCE_URL = BASE_URL + "glances.json"
3326

34-
35-
class ApiTokenError(Exception):
36-
"""Exception which is raised when trying to send a message before
37-
initializing the module.
38-
"""
39-
40-
def __str__(self):
41-
return "No api_token provided."
42-
43-
4427
class RequestError(Exception):
4528
"""Exception which is raised when Pushover's API returns an error code.
4629
@@ -61,8 +44,14 @@ class Request:
6144
a :class:`RequestError` exception when the request is rejected.
6245
"""
6346

64-
def __init__(self, request_type, url, payload, files={}):
65-
request = getattr(requests, request_type)(url, params=payload, files=files)
47+
def __init__(self, method, url, payload):
48+
files = {}
49+
if "attachment" in payload:
50+
files["attachment"] = payload["attachment"]
51+
del payload["attachment"]
52+
self.payload = payload
53+
self.files = files
54+
request = getattr(requests, method)(url, params=payload, files=files)
6655
self.answer = request.json()
6756
if 400 <= request.status_code < 500:
6857
raise RequestError(self.answer["errors"])
@@ -72,131 +61,113 @@ def __str__(self):
7261

7362

7463
class MessageRequest(Request):
75-
"""Class representing a message request to the Pushover API. You do not
76-
need to create them yourself, but the :func:`Client.send_message` function
77-
returns :class:`MessageRequest` objects if you need to inspect the requests
78-
after they have been answered by the Pushover server.
64+
"""This class represents a message request to the Pushover API. You do not
65+
need to create it yourself, but the :func:`Pushover.message` function
66+
returns :class:`MessageRequest` objects.
7967
8068
The :attr:`answer` attribute contains a JSON representation of the answer
81-
made by the Pushover API. In the case where you have sent a message with
82-
a priority of 2, you can poll the status of the notification with the
83-
:func:`poll` function.
69+
made by the Pushover API. When sending a message with a priority of 2, you
70+
can poll the status of the notification with the :func:`poll` function.
8471
"""
8572

86-
def __init__(self, payload, files):
87-
Request.__init__(self, "post", MESSAGE_URL, payload, files)
88-
self.token = payload["token"]
89-
self.receipt = None
73+
params = {"expired": "expires_at",
74+
"called_back": "called_back_at",
75+
"acknowledged": "acknowledged_at"}
76+
77+
def __init__(self, payload):
78+
Request.__init__(self, "post", MESSAGE_URL, payload)
79+
self.status = {"done": True}
9080
if payload.get("priority", 0) == 2:
91-
self.receipt = self.answer["receipt"]
92-
self.parameters = {"expired": "expires_at",
93-
"called_back": "called_back_at",
94-
"acknowledged": "acknowledged_at"}
95-
for param, when in self.parameters.iteritems():
96-
setattr(self, param, False)
97-
setattr(self, when, 0)
81+
self.url = RECEIPT_URL + self.answer["receipt"]
82+
self.status["done"] = False
83+
for param, when in MessageRequest.params.iteritems():
84+
self.status[param] = False
85+
self.status[when] = 0
9886

9987
def poll(self):
100-
"""If the message request has a priority of 2, Pushover will keep
101-
sending the same notification until the client acknowledges it. Calling
102-
the :func:`poll` function will update the status of the
103-
:class:`MessageRequest` object until the notifications either expires,
104-
is acknowledged by the client, or the callback url is reached. The
105-
attributes of interest are: ``expired, called_back, acknowledged`` and
106-
their *_at* variants as explained in the API documentation.
107-
108-
This function returns ``None`` when the request has expired or been
109-
acknowledged, so that a typical handling of a priority-2 notification
110-
can look like this::
111-
112-
request = client.send_message("Urgent notification", priority=2,
113-
expire=120, retry=60)
114-
while request.poll():
88+
"""If the message request has a priority of 2, Pushover keeps sending
89+
the same notification until the client acknowledges it. Calling the
90+
:func:`poll` function fetches the status of the :class:`MessageRequest`
91+
object until the notifications either expires, is acknowledged by the
92+
client, or the callback url is reached. The status is available in the
93+
``status`` dictionary.
94+
95+
Returns ``True`` when the request has expired or been acknowledged and
96+
``False`` otherwise so that a typical handling of a priority-2
97+
notification can look like this::
98+
99+
request = p.message("Urgent!", priority=2, expire=120, retry=60)
100+
while not request.poll():
115101
# do something
116102
time.sleep(5)
117103
118-
print request.acknowledged_at, request.acknowledged_by
104+
print request.status
119105
"""
120-
if (self.receipt and not any(getattr(self, parameter)
121-
for parameter in self.parameters)):
122-
request = Request("get", RECEIPT_URL + self.receipt + ".json", {
123-
"token": self.token
124-
})
125-
for param, when in self.parameters.iteritems():
126-
setattr(self, param, bool(request.answer[param]))
127-
setattr(self, when, request.answer[when])
128-
for param in ["last_delivered_at", "acknowledged_by",
129-
"acknowledged_by_device"]:
130-
setattr(self, param, request.answer[param])
131-
return request
106+
if not self.status["done"]:
107+
r = Request("get", self.url + ".json", {"token": self.payload["token"]})
108+
for param, when in MessageRequest.params.iteritems():
109+
self.status[param] = bool(r.answer[param])
110+
self.status[when] = int(r.answer[when])
111+
for param in ["acknowledged_by", "acknowledged_by_device"]:
112+
self.status[param] = r.answer[param]
113+
self.status["last_delivered_at"] = int(r.answer["last_delivered_at"])
114+
if any(self.status[param] for param in MessageRequest.params):
115+
self.status["done"] = True
116+
return self.status["done"]
132117

133118
def cancel(self):
134-
"""If the message request has a priority of 2, Pushover will keep
135-
sending the same notification until it either reaches its ``expire``
136-
value or is aknowledged by the client. Calling the :func:`cancel`
137-
function will cancel the notification early.
119+
"""If the message request has a priority of 2, Pushover keeps sending
120+
the same notification until it either reaches its ``expire`` value or
121+
is aknowledged by the client. Calling the :func:`cancel` function
122+
cancels the notification early.
138123
"""
139-
if (self.receipt and not any(getattr(self, parameter)
140-
for parameter in self.parameters)):
141-
request = Request("post", RECEIPT_URL + self.receipt
142-
+ "/cancel.json", {
143-
"token": self.token
144-
})
145-
return request
146-
147-
148-
class GlanceRequest(Request):
149-
"""Class representing a glance request to the Pushover API. This is
150-
a heavily simplified version of the MessageRequest class, with all
151-
polling-related features removed.
152-
"""
153-
154-
def __init__(self, payload):
155-
Request.__init__(self, "post", GLANCE_URL, payload)
124+
if not self.status["done"]:
125+
return Request("post", self.url + "/cancel.json", {"token": self.payload["token"]})
156126

157127

158128
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.
129+
"""This is the main class of the module. It represents a Pushover app and
130+
is tied to a unique API token.
161131
162132
* ``token``: Pushover API token
163133
"""
164134

165135
_SOUNDS = None
136+
message_keywords = ["title", "priority", "sound", "callback", "timestamp", "url", "url_title", "device", "retry", "expire", "html", "attachment"]
137+
glance_keywords = ["title", "text", "subtext", "count", "percent", "device"]
166138

167139
def __init__(self, token):
168140
self.token = token
169141

170142
@property
171143
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.
144+
"""Return a dictionary of sounds recognized by Pushover and that can be
145+
used in a notification message.
177146
"""
178147
if not Pushover._SOUNDS:
179148
request = Request("get", SOUND_URL, {"token": self.token})
180-
Pushover._SOUNDS = request.answer["sounds"].keys()
149+
Pushover._SOUNDS = request.answer["sounds"]
181150
return Pushover._SOUNDS
182151

183-
def verify(self, user_key, device=None):
184-
"""Verify that the `user_key` and optional `device` exist. Returns
152+
153+
def verify(self, user, device=None):
154+
"""Verify that the `user` and optional `device` exist. Returns
185155
`None` when the user/device does not exist or a list of the user's
186156
devices otherwise.
187157
"""
188-
payload = {"user": self.user_key, "token": self.token}
158+
payload = {"user": user, "token": self.token}
189159
if device:
190160
payload["device"] = device
191161
try:
192162
request = Request("post", USER_URL, payload)
193163
except RequestError:
194164
return None
165+
else:
166+
return request.answer["devices"]
195167

196-
return request.answer["devices"]
197168

198-
def send_message(self, user_key, message, **kwords):
199-
"""Send `message` to the user specified by `user_key`. It is possible
169+
def message(self, user, message, **kwargs):
170+
"""Send `message` to the user specified by `user`. It is possible
200171
to specify additional properties of the message by passing keyword
201172
arguments. The list of valid keywords is ``title, priority, sound,
202173
callback, timestamp, url, url_title, device, retry, expire and html``
@@ -210,74 +181,64 @@ def send_message(self, user_key, message, **kwords):
210181
211182
This method returns a :class:`MessageRequest` object.
212183
"""
213-
valid_keywords = ["title", "priority", "sound", "callback",
214-
"timestamp", "url", "url_title", "device",
215-
"retry", "expire", "html", "attachment"]
216184

217-
payload = {"message": message, "user": user_key, "token": self.token}
218-
files = {}
219-
for key, value in kwords.iteritems():
220-
if key not in valid_keywords:
185+
payload = {"message": message, "user": user, "token": self.token}
186+
for key, value in kwargs.iteritems():
187+
if key not in Pushover.message_keywords:
221188
raise ValueError("{0}: invalid message parameter".format(key))
222-
223-
if key == "timestamp" and value is True:
224-
payload[key] = int(time.time())
225-
elif key == "sound":
226-
if value not in self.sounds:
189+
elif key == "timestamp" and value is True:
190+
payload[key] = int(time.time())
191+
elif key == "sound" and value not in self.sounds:
227192
raise ValueError("{0}: invalid sound".format(value))
228-
else:
229-
payload[key] = value
230-
elif key == "attachment":
231-
files["attachment"] = value
232-
elif value:
193+
else:
233194
payload[key] = value
234195

235-
return MessageRequest(payload, files)
196+
return MessageRequest(payload)
236197

237-
def send_glance(self, user_key, **kwords):
238-
"""Send a glance to the user. The default property is ``text``,
239-
as this is used on most glances, however a valid glance does not
240-
need to require text and can be constructed using any combination
241-
of valid keyword properties. The list of valid keywords is ``title,
242-
text, subtext, count and percent`` which are described in the
198+
def glance(self, user, **kwargs):
199+
"""Send a glance to the user. The default property is ``text``, as this
200+
is used on most glances, however a valid glance does not need to
201+
require text and can be constructed using any combination of valid
202+
keyword properties. The list of valid keywords is ``title, text,
203+
subtext, count, percent and device`` which are described in the
243204
Pushover Glance API documentation.
244205
245206
This method returns a :class:`GlanceRequest` object.
246207
"""
247-
valid_keywords = ["title", "text", "subtext", "count", "percent",
248-
"device"]
249-
250-
payload = {"user": user_key, "token": self.token}
208+
payload = {"user": user, "token": self.token}
251209

252-
for key, value in kwords.iteritems():
253-
if key not in valid_keywords:
254-
raise ValueError("{0}: invalid message parameter".format(key))
255-
payload[key] = value
256-
257-
return GlanceRequest(payload)
210+
for key, value in kwargs.iteritems():
211+
if key not in Pushover.glance_keywords:
212+
raise ValueError("{0}: invalid glance parameter".format(key))
213+
else:
214+
payload[key] = value
258215

216+
return Request("post", GLANCE_URL, payload)
259217

260-
def read_config(config_path):
261-
config_path = os.path.expanduser(config_path)
262-
config = RawConfigParser()
263-
params = {"users": {}}
264-
files = config.read(config_path)
265-
if not files:
218+
def main():
219+
from ConfigParser import RawConfigParser, NoSectionError, NoOptionError
220+
from argparse import ArgumentParser, RawDescriptionHelpFormatter
221+
import os
222+
223+
def read_config(config_path):
224+
config_path = os.path.expanduser(config_path)
225+
config = RawConfigParser()
226+
params = {"users": {}}
227+
files = config.read(config_path)
228+
if not files:
229+
return params
230+
params["token"] = config.get("main", "token")
231+
for name in config.sections():
232+
if name != "main":
233+
user = {}
234+
user["user_key"] = config.get(name, "user_key")
235+
try:
236+
user["device"] = config.get(name, "device")
237+
except NoOptionError:
238+
user["device"] = None
239+
params["users"][name] = user
266240
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
277-
return params
278-
279241

280-
def main():
281242
parser = ArgumentParser(description="Send a message to pushover.",
282243
formatter_class=RawDescriptionHelpFormatter,
283244
epilog="""
@@ -312,12 +273,9 @@ def main():
312273
else:
313274
user_key = args.user
314275
device = None
315-
try:
316-
token = args.token or params["token"]
317-
except KeyError:
318-
raise ApiTokenError()
276+
token = args.token or params["token"]
319277

320-
Pushover(token).send_message(user_key, args.message, device=device,
278+
Pushover(token).message(user_key, args.message, device=device,
321279
title=args.title, priority=args.priority, url=args.url,
322280
url_title=args.url_title, timestamp=True, retry=args.retry,
323281
expire=args.expire)

0 commit comments

Comments
 (0)