1+ from datetime import datetime
2+ import logging
13from platform import python_version
24
35import hashlib
46import hmac
57import jwt
68import os
9+ import pytz
710import requests
811import sys
912import time
1215
1316if sys .version_info [0 ] == 3 :
1417 string_types = (str , bytes )
18+ from urllib .parse import urlparse
1519else :
20+ from urlparse import urlparse
1621 string_types = (unicode , str )
1722
1823__version__ = '2.0.0'
1924
25+ logger = logging .getLogger ('nexmo' )
26+
2027
2128class Error (Exception ):
2229 pass
@@ -133,6 +140,24 @@ def send_ussd_prompt_message(self, params=None, **kwargs):
133140 def send_2fa_message (self , params = None , ** kwargs ):
134141 return self .post (self .host , '/sc/us/2fa/json' , params or kwargs )
135142
143+ def submit_sms_conversion (self , message_id , delivered = True , timestamp = None ):
144+ """
145+ Notify Nexmo that an SMS was successfully received.
146+
147+ :param message_id: The `message-id` str returned by the send_message call.
148+ :param delivered: A `bool` indicating that the message was or was not successfully delivered.
149+ :param timestamp: A `datetime` object containing the time the SMS arrived.
150+ :return: The parsed response from the server. On success, the bytestring b'OK'
151+ """
152+ params = {
153+ 'message-id' : message_id ,
154+ 'delivered' : delivered ,
155+ 'timestamp' : timestamp or datetime .now (pytz .utc ),
156+ }
157+ # Ensure timestamp is a string:
158+ _format_date_param (params , 'timestamp' )
159+ return self .post (self .api_host , '/conversions/sms' , params )
160+
136161 def send_event_alert_message (self , params = None , ** kwargs ):
137162 return self .post (self .host , '/sc/us/alert/json' , params or kwargs )
138163
@@ -226,31 +251,35 @@ def delete_application(self, application_id):
226251 return self .delete (self .api_host , '/v1/applications/' + application_id )
227252
228253 def create_call (self , params = None , ** kwargs ):
229- return self .__post ('/v1/calls' , params or kwargs )
254+ return self ._jwt_signed_post ('/v1/calls' , params or kwargs )
230255
231256 def get_calls (self , params = None , ** kwargs ):
232- return self .__get ('/v1/calls' , params or kwargs )
257+ return self ._jwt_signed_get ('/v1/calls' , params or kwargs )
233258
234259 def get_call (self , uuid ):
235- return self .__get ('/v1/calls/' + uuid )
260+ return self ._jwt_signed_get ('/v1/calls/' + uuid )
236261
237262 def update_call (self , uuid , params = None , ** kwargs ):
238- return self .__put ('/v1/calls/' + uuid , params or kwargs )
263+ return self ._jwt_signed_put ('/v1/calls/' + uuid , params or kwargs )
239264
240265 def send_audio (self , uuid , params = None , ** kwargs ):
241- return self .__put ('/v1/calls/' + uuid + '/stream' , params or kwargs )
266+ return self ._jwt_signed_put ('/v1/calls/' + uuid + '/stream' , params or kwargs )
242267
243268 def stop_audio (self , uuid ):
244- return self .__delete ('/v1/calls/' + uuid + '/stream' )
269+ return self ._jwt_signed_delete ('/v1/calls/' + uuid + '/stream' )
245270
246271 def send_speech (self , uuid , params = None , ** kwargs ):
247- return self .__put ('/v1/calls/' + uuid + '/talk' , params or kwargs )
272+ return self ._jwt_signed_put ('/v1/calls/' + uuid + '/talk' , params or kwargs )
248273
249274 def stop_speech (self , uuid ):
250- return self .__delete ('/v1/calls/' + uuid + '/talk' )
275+ return self ._jwt_signed_delete ('/v1/calls/' + uuid + '/talk' )
251276
252277 def send_dtmf (self , uuid , params = None , ** kwargs ):
253- return self .__put ('/v1/calls/' + uuid + '/dtmf' , params or kwargs )
278+ return self ._jwt_signed_put ('/v1/calls/' + uuid + '/dtmf' , params or kwargs )
279+
280+ def get_recording (self , url ):
281+ hostname = urlparse (url ).hostname
282+ return self .parse (hostname , requests .get (url , headers = self ._headers ()))
254283
255284 def check_signature (self , params ):
256285 params = dict (params )
@@ -286,28 +315,28 @@ def get(self, host, request_uri, params=None):
286315 uri = 'https://' + host + request_uri
287316
288317 params = dict (params or {}, api_key = self .api_key , api_secret = self .api_secret )
289-
318+ logger . debug ( "GET to %r with params %r" , uri , params )
290319 return self .parse (host , requests .get (uri , params = params , headers = self .headers ))
291320
292321 def post (self , host , request_uri , params ):
293322 uri = 'https://' + host + request_uri
294323
295324 params = dict (params , api_key = self .api_key , api_secret = self .api_secret )
296-
325+ logger . debug ( "POST to %r with params %r" , uri , params )
297326 return self .parse (host , requests .post (uri , data = params , headers = self .headers ))
298327
299328 def put (self , host , request_uri , params ):
300329 uri = 'https://' + host + request_uri
301330
302331 params = dict (params , api_key = self .api_key , api_secret = self .api_secret )
303-
332+ logger . debug ( "PUT to %r with params %r" , uri , params )
304333 return self .parse (host , requests .put (uri , json = params , headers = self .headers ))
305334
306335 def delete (self , host , request_uri ):
307336 uri = 'https://' + host + request_uri
308337
309338 params = dict (api_key = self .api_key , api_secret = self .api_secret )
310-
339+ logger . debug ( "DELETE to %r with params %r" , uri , params )
311340 return self .parse (host , requests .delete (uri , params = params , headers = self .headers ))
312341
313342 def parse (self , host , response ):
@@ -316,37 +345,40 @@ def parse(self, host, response):
316345 elif response .status_code == 204 :
317346 return None
318347 elif 200 <= response .status_code < 300 :
319- return response .json ()
348+ if response .headers .get ('content-type' ) == 'application/json' :
349+ return response .json ()
350+ else :
351+ return response .content
320352 elif 400 <= response .status_code < 500 :
353+ logger .warn ("Client error: %s %r" , response .status_code , response .content )
321354 message = "{code} response from {host}" .format (code = response .status_code , host = host )
322-
323355 raise ClientError (message )
324356 elif 500 <= response .status_code < 600 :
357+ logger .warn ("Server error: %s %r" , response .status_code , response .content )
325358 message = "{code} response from {host}" .format (code = response .status_code , host = host )
326-
327359 raise ServerError (message )
328360
329- def __get (self , request_uri , params = None ):
361+ def _jwt_signed_get (self , request_uri , params = None ):
330362 uri = 'https://' + self .api_host + request_uri
331363
332- return self .parse (self .api_host , requests .get (uri , params = params or {}, headers = self .__headers ()))
364+ return self .parse (self .api_host , requests .get (uri , params = params or {}, headers = self ._headers ()))
333365
334- def __post (self , request_uri , params ):
366+ def _jwt_signed_post (self , request_uri , params ):
335367 uri = 'https://' + self .api_host + request_uri
336368
337- return self .parse (self .api_host , requests .post (uri , json = params , headers = self .__headers ()))
369+ return self .parse (self .api_host , requests .post (uri , json = params , headers = self ._headers ()))
338370
339- def __put (self , request_uri , params ):
371+ def _jwt_signed_put (self , request_uri , params ):
340372 uri = 'https://' + self .api_host + request_uri
341373
342- return self .parse (self .api_host , requests .put (uri , json = params , headers = self .__headers ()))
374+ return self .parse (self .api_host , requests .put (uri , json = params , headers = self ._headers ()))
343375
344- def __delete (self , request_uri ):
376+ def _jwt_signed_delete (self , request_uri ):
345377 uri = 'https://' + self .api_host + request_uri
346378
347- return self .parse (self .api_host , requests .delete (uri , headers = self .__headers ()))
379+ return self .parse (self .api_host , requests .delete (uri , headers = self ._headers ()))
348380
349- def __headers (self ):
381+ def _headers (self ):
350382 iat = int (time .time ())
351383
352384 payload = dict (self .auth_params )
@@ -358,3 +390,19 @@ def __headers(self):
358390 token = jwt .encode (payload , self .private_key , algorithm = 'RS256' )
359391
360392 return dict (self .headers , Authorization = b'Bearer ' + token )
393+
394+
395+ def _format_date_param (params , key , format = '%Y-%m-%d %H:%M:%S' ):
396+ """
397+ Utility function to convert datetime values to strings.
398+
399+ If the value is already a str, or is not in the dict, no change is made.
400+
401+ :param params: A `dict` of params that may contain a `datetime` value.
402+ :param key: The datetime value to be converted to a `str`
403+ :param format: The `strftime` format to be used to format the date. The default value is '%Y-%m-%d %H:%M:%S'
404+ """
405+ if key in params :
406+ param = params [key ]
407+ if hasattr (param , 'strftime' ):
408+ params [key ] = param .strftime (format )
0 commit comments