1- # pushover 0.4
1+ # pushover 1.0
22#
33# Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
44
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/>.
1717import time
18- from ConfigParser import RawConfigParser , NoSectionError
18+ from ConfigParser import RawConfigParser , NoSectionError , NoOptionError
1919from argparse import ArgumentParser , RawDescriptionHelpFormatter
2020import os
2121
2222import requests
2323
24- __all__ = ["init " , "get_sounds " , "Client " , "MessageRequest " ,
25- "InitError" , "RequestError" , "UserError " ]
24+ __all__ = ["Pushover " , "MessageRequest " , "ApiTokenError " , "RequestError " ,
25+ "read_config " ]
2626
2727BASE_URL = "https://api.pushover.net/1/"
2828MESSAGE_URL = BASE_URL + "messages.json"
3131RECEIPT_URL = BASE_URL + "receipts/"
3232GLANCE_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
8244class RequestError (Exception ):
@@ -95,15 +57,11 @@ def __str__(self):
9557
9658class 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 = """
332284For 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
351300Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
352301License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
353302This is free software: you are free to change and redistribute it.
354303There 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
366325if __name__ == "__main__" :
367326 main ()
0 commit comments