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 , NoOptionError
19- from argparse import ArgumentParser , RawDescriptionHelpFormatter
20- import os
21-
2218import requests
2319
24- __all__ = ["Pushover" , "MessageRequest" , "ApiTokenError" , "RequestError" ,
25- "read_config" ]
26-
2720BASE_URL = "https://api.pushover.net/1/"
2821MESSAGE_URL = BASE_URL + "messages.json"
2922USER_URL = BASE_URL + "users/validate.json"
3023SOUND_URL = BASE_URL + "sounds.json"
3124RECEIPT_URL = BASE_URL + "receipts/"
3225GLANCE_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-
4427class 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
7463class 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
158128class 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