From 2e5f51cf5a751d03eaab6355f9c8e5acaa654267 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Fri, 3 Aug 2012 15:17:39 -0700 Subject: [PATCH 01/16] some initial work on queue twiml --- tests/test_twiml.py | 31 +++++++++++++++++++++++++++++++ twilio/twiml.py | 27 +++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/tests/test_twiml.py b/tests/test_twiml.py index d30bb44420..70d01ba95a 100644 --- a/tests/test_twiml.py +++ b/tests/test_twiml.py @@ -332,6 +332,29 @@ def test_conf_end_conference(self): self.assertEqual(self.conf.get('endConferenceOnExit'), "true") +class TestQueue(TwilioTest): + + def setUp(self): + r = Response() + with r.dial() as dial: + dial.queue("TestQueueAttribute", url="", method='GET') + xml = r.toxml() + + #parse twiml XML string with Element Tree and inspect + #structure + tree = ET.fromstring(xml) + self.conf = tree.find(".//Dial/Queue") + + def test_conf_text(self): + self.assertEqual(self.conf.text.strip(), "TestQueueAttribute") + + def test_conf_waiturl(self): + self.assertEqual(self.conf.get('url'), "") + + def test_conf_method(self): + self.assertEqual(self.conf.get('method'), "GET") + + class TestDial(TwilioTest): def testDial(self): @@ -374,6 +397,14 @@ def testAddConference(self): r = self.strip(r) self.assertEquals(r, 'My Room') + def test_add_queue(self): + """ add a queue to a dial""" + r = Response() + d = r.dial() + d.append(twiml.Queue("The Cute Queue")) + r = self.strip(r) + self.assertEquals(r, 'The Cute Queue') + def test_add_empty_client(self): """ add an empty client to a dial""" r = Response() diff --git a/twilio/twiml.py b/twilio/twiml.py index ba7d028541..907ab68a2d 100644 --- a/twilio/twiml.py +++ b/twilio/twiml.py @@ -367,9 +367,9 @@ class Dial(Verb): :param action: submit the result of the dial to this URL :param method: submit to 'action' url using GET or POST - :param int timeout: The number of seconds to waits for the called + :param int timeout: The number of seconds to waits for the called party to answer the call - :param bool hangupOnStar: Allow the calling party to hang up on the + :param bool hangupOnStar: Allow the calling party to hang up on the called party by pressing the '*' key :param int timeLimit: The maximum duration of the Call in seconds :param callerId: The caller ID that will appear to the called party @@ -377,10 +377,10 @@ class Dial(Verb): """ GET = 'GET' POST = 'POST' - nestables = ['Number', 'Conference', 'Client'] + nestables = ['Number', 'Conference', 'Client', 'Queue'] def __init__(self, number=None, **kwargs): - super(Dial, self).__init__(**kwargs) + super(Dial, self).__init__(**kwargs) if number and len(number.split(',')) > 1: for n in number.split(','): self.append(Number(n.strip())) @@ -396,6 +396,9 @@ def number(self, number, **kwargs): def conference(self, name, **kwargs): return self.append(Conference(name, **kwargs)) + def queue(self, name, **kwargs): + return self.append(Queue(name, **kwargs)) + def addNumber(self, *args, **kwargs): return self.number(*args, **kwargs) @@ -403,6 +406,22 @@ def addConference(self, *args, **kwargs): return self.conference(*args, **kwargs) +class Queue(Verb): + """Specify queue in a nested Dial element. + + :param name: friendly name for the queue + :param url: url to a twiml document that executes after a call is dequeued + and before the call is connected + :param method: HTTP method for url GET/POST + """ + GET = 'GET' + POST = 'POST' + + def __init__(self, name, **kwargs): + super(Queue, self).__init__(**kwargs) + self.body = name + + class Record(Verb): """Record audio from caller From 58051813885b078dcedab1333e698391dc95329c Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Mon, 6 Aug 2012 13:18:37 -0700 Subject: [PATCH 02/16] restapi support for queues, untested --- twilio/rest/resources.py | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 2378178bdb..57201b4587 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1141,6 +1141,73 @@ def list(self, updated_before=None, updated_after=None, created_after=None, return self.get_instances(kwargs) +class Member(InstanceResource): + pass + + +class Members(ListResource): + name = "Members" + instance = Member + + def list(self, **kwargs): + """ + Returns a list of :class:`Member` resources in the given queue + + :param queue_sid: Conference this participant is part of + """ + return self.get_instances(kwargs) + + def dequeue(self, call_sid='Front', **kwargs): + """ + Dequeues a member from the queue and have the member's call + begin executing the TwiML document at the url. + + :param call_sid: Call sid specifying the member, if not given, + the member at the front of the queue will be used + :param url: url of the TwiML document to be executed. + """ + return self.update_instance(call_sid, kwargs) + + +class Queue(InstanceResource): + + subresources = [ + Members + ] + + def update(self, **kwargs): + """ + Update this queue + :param friendly_name: A new friendly name for this queue + :param max_size: A new max size. Changing a max size to less than the + current size results in the queue rejecting incoming + requests until it shrinks below the new max size + """ + return self.parent.update(self.sid, kwargs) + + def delete(self): + """ + Delete this queue. Can only be run on empty queues. + """ + return self.parent.delete(self.sid) + + +class Queues(ListResource): + name = "Queues" + instance = Queue + + def list(self, **kwargs): + return self.get_instances(kwargs) + + def create(self, **kwargs): + """ Create an :class:`Queue` with any of these optional parameters. + :param friendly_name: A human readable description of the application, + with maximum length 64 characters. + :param max_size: The upper limit of calls allowed into the queue + """ + return self.create_instance(kwargs) + + class Application(InstanceResource): """ An application resource """ From 66a919d20533678ca348e925dce84d812236edcd Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Tue, 7 Aug 2012 17:25:59 -0700 Subject: [PATCH 03/16] finish testing queues --- tests/resources/members_instance.json | 1 + tests/resources/members_list.json | 1 + tests/resources/queues_instance.json | 1 + tests/resources/queues_list.json | 1 + tests/test_calls.py | 11 ----- tests/test_queues.py | 70 +++++++++++++++++++++++++++ tests/test_twiml.py | 53 ++++++++++++++++++-- twilio/rest/__init__.py | 5 +- twilio/rest/resources.py | 14 +++--- twilio/twiml.py | 59 +++++++++++++++++++++- 10 files changed, 192 insertions(+), 24 deletions(-) create mode 100644 tests/resources/members_instance.json create mode 100644 tests/resources/members_list.json create mode 100644 tests/resources/queues_instance.json create mode 100644 tests/resources/queues_list.json create mode 100644 tests/test_queues.py diff --git a/tests/resources/members_instance.json b/tests/resources/members_instance.json new file mode 100644 index 0000000000..d98b38e0c4 --- /dev/null +++ b/tests/resources/members_instance.json @@ -0,0 +1 @@ +{"call_sid": "CAaaf2e9ded94aba3e57c42a3d55be6ff2", "date_enqueued": "Tue, 07 Aug 2012 22:57:41 +0000", "wait_time": 143, "current_position": 1, "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QUe675e1913214346f48a9d2296a8d3788/Members/CAaaf2e9ded94aba3e57c42a3d55be6ff2.json"} diff --git a/tests/resources/members_list.json b/tests/resources/members_list.json new file mode 100644 index 0000000000..2e6e249f48 --- /dev/null +++ b/tests/resources/members_list.json @@ -0,0 +1 @@ +{"first_page_uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QUe675e1913214346f48a9d2296a8d3788/Members.json?Page=0&PageSize=50", "num_pages": 1, "previous_page_uri": null, "last_page_uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QUe675e1913214346f48a9d2296a8d3788/Members.json?Page=0&PageSize=50", "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QUe675e1913214346f48a9d2296a8d3788/Members.json", "page_size": 50, "start": 0, "next_page_uri": null, "end": 0, "total": 1, "queue_members": [{"call_sid": "CAaaf2e9ded94aba3e57c42a3d55be6ff2", "date_enqueued": "Tue, 07 Aug 2012 22:57:41 +0000", "wait_time": 124, "current_position": 1, "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QUe675e1913214346f48a9d2296a8d3788/Members/CAaaf2e9ded94aba3e57c42a3d55be6ff2.json"}], "page": 0} diff --git a/tests/resources/queues_instance.json b/tests/resources/queues_instance.json new file mode 100644 index 0000000000..2eeff479ba --- /dev/null +++ b/tests/resources/queues_instance.json @@ -0,0 +1 @@ +{"sid": "QU6e2cd5a7c8074edc8b383a3b198b2f8b", "friendly_name": "CutieQueue", "current_size": null, "average_wait_time": null, "max_size": 100, "date_created": "Tue, 07 Aug 2012 21:08:51 +0000", "date_updated": "Tue, 07 Aug 2012 21:08:51 +0000", "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QU6e2cd5a7c8074edc8b383a3b198b2f8b.json"} diff --git a/tests/resources/queues_list.json b/tests/resources/queues_list.json new file mode 100644 index 0000000000..ae389ef7f1 --- /dev/null +++ b/tests/resources/queues_list.json @@ -0,0 +1 @@ +{"first_page_uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues.json?Page=0&PageSize=50", "num_pages": 1, "previous_page_uri": null, "queues": [{"sid": "QU1b9faddec3d54ec18488f86c83019bf0", "friendly_name": "Support", "current_size": 0, "average_wait_time": 0, "max_size": 100, "date_created": "Tue, 07 Aug 2012 21:09:04 +0000", "date_updated": "Tue, 07 Aug 2012 21:09:04 +0000", "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QU1b9faddec3d54ec18488f86c83019bf0.json"}, {"sid": "QU6e2cd5a7c8074edc8b383a3b198b2f8b", "friendly_name": "CutieQueue", "current_size": 0, "average_wait_time": 0, "max_size": 100, "date_created": "Tue, 07 Aug 2012 21:08:51 +0000", "date_updated": "Tue, 07 Aug 2012 21:08:51 +0000", "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QU6e2cd5a7c8074edc8b383a3b198b2f8b.json"}, {"sid": "QUa8ce40f68d3f41958e2519646d55b989", "friendly_name": "Test1", "current_size": 0, "average_wait_time": 0, "max_size": 64, "date_created": "Tue, 07 Aug 2012 19:09:17 +0000", "date_updated": "Tue, 07 Aug 2012 19:09:17 +0000", "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues/QUa8ce40f68d3f41958e2519646d55b989.json"}], "uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues.json", "page_size": 50, "start": 0, "next_page_uri": null, "end": 2, "total": 3, "last_page_uri": "/2010-04-01/Accounts/ACf5af0ea0c9955b67b39c52fb11ee6e69/Queues.json?Page=0&PageSize=50", "page": 0} diff --git a/tests/test_calls.py b/tests/test_calls.py index dcade1f092..1c762f38a7 100644 --- a/tests/test_calls.py +++ b/tests/test_calls.py @@ -11,17 +11,6 @@ list_resource = Calls(BASE_URI, AUTH) -@patch("twilio.rest.resources.make_twilio_request") -def test_paging(mock): - resp = create_mock_json("tests/resources/calls_list.json") - mock.return_value = resp - - uri = "%s/Calls" % (BASE_URI) - list_resource.list(started=date(2010,12,5)) - exp_params = {'StartTime': '2010-12-05'} - - mock.assert_called_with("GET", uri, params=exp_params, auth=AUTH) - @patch("twilio.rest.resources.make_twilio_request") def test_create_call(mock): resp = create_mock_json("tests/resources/calls_instance.json") diff --git a/tests/test_queues.py b/tests/test_queues.py new file mode 100644 index 0000000000..bba1f28aa1 --- /dev/null +++ b/tests/test_queues.py @@ -0,0 +1,70 @@ +from mock import patch, Mock +from datetime import date +from mock import patch, Mock +from nose.tools import raises, assert_equals, assert_true +from tools import create_mock_json +from twilio.rest.resources import Queues, Queue + +BASE_URI = "https://api.twilio.com/2010-04-01/Accounts/AC123" +ACCOUNT_SID = "AC123" +AUTH = (ACCOUNT_SID, "token") +QUEUE_SID = "QU1b9faddec3d54ec18488f86c83019bf0" +CALL_SID = "CAaaf2e9ded94aba3e57c42a3d55be6ff2" + +list_resource = Queues(BASE_URI, AUTH) +instance_resource = Queue(list_resource, QUEUE_SID) + +@patch("twilio.rest.resources.make_twilio_request") +def test_queues_list(mock): + resp = create_mock_json("tests/resources/queues_list.json") + mock.return_value = resp + + uri = "%s/Queues" % (BASE_URI) + list_resource.list() + + mock.assert_called_with("GET", uri, params={}, auth=AUTH) + +@patch("twilio.rest.resources.make_twilio_request") +def test_queues_create(mock): + resp = create_mock_json("tests/resources/queues_instance.json") + resp.status_code = 201 + mock.return_value = resp + + uri = "%s/Queues" % (BASE_URI) + list_resource.create('test', max_size=9001) + + mock.assert_called_with("POST", uri, + data={'FriendlyName': 'test', 'MaxSize': 9001}, + auth=AUTH) + +@patch("twilio.rest.resources.make_twilio_request") +def test_queues_get(mock): + resp = create_mock_json("tests/resources/queues_instance.json") + mock.return_value = resp + + uri = "%s/Queues/%s" % (BASE_URI, QUEUE_SID) + r = list_resource.get(QUEUE_SID) + + mock.assert_called_with("GET", uri, auth=AUTH) + + +@patch("twilio.rest.resources.make_twilio_request") +def test_queue_update(mock): + resp = create_mock_json("tests/resources/queues_instance.json") + mock.return_value = resp + + uri = "%s/Queues/%s" % (BASE_URI, QUEUE_SID) + r = instance_resource.update(friendly_name='QQ') + + mock.assert_called_with("POST", uri, data={'FriendlyName':'QQ'}, auth=AUTH) + + +@patch("twilio.rest.resources.make_twilio_request") +def test_queue_delete(mock): + resp = create_mock_json("tests/resources/queues_instance.json") + mock.return_value = resp + + uri = "%s/Queues/%s" % (BASE_URI, QUEUE_SID) + r = instance_resource.delete() + + mock.assert_called_with("DELETE", uri, auth=AUTH) diff --git a/tests/test_twiml.py b/tests/test_twiml.py index 70d01ba95a..8b54d084a9 100644 --- a/tests/test_twiml.py +++ b/tests/test_twiml.py @@ -26,7 +26,11 @@ def improperAppend(self, verb): self.assertRaises(TwimlException, verb.append, twiml.Reject()) self.assertRaises(TwimlException, verb.append, twiml.Redirect()) self.assertRaises(TwimlException, verb.append, twiml.Dial()) + self.assertRaises(TwimlException, verb.append, twiml.Enqueue("")) + self.assertRaises(TwimlException, verb.append, twiml.Queue("")) + self.assertRaises(TwimlException, verb.append, twiml.Leave()) self.assertRaises(TwimlException, verb.append, twiml.Conference("")) + self.assertRaises(TwimlException, verb.append, twiml.Client("")) self.assertRaises(TwimlException, verb.append, twiml.Sms("")) self.assertRaises(TwimlException, verb.append, twiml.Pause()) @@ -240,6 +244,20 @@ def testBadAppend(self): self.improperAppend(twiml.Hangup()) +class TestLeave(TwilioTest): + + def testLeave(self): + """convenience: should Hangup to a url via POST""" + r = Response() + r.append(twiml.Leave()) + r = self.strip(r) + self.assertEquals(r, '') + + def testBadAppend(self): + """ should raise exceptions for wrong appending""" + self.improperAppend(twiml.Leave()) + + class TestReject(TwilioTest): def testReject(self): @@ -338,22 +356,51 @@ def setUp(self): r = Response() with r.dial() as dial: dial.queue("TestQueueAttribute", url="", method='GET') + xml = r.toxml() + + #parse twiml XML string with Element Tree and inspect + #structure + tree = ET.fromstring(xml) + self.conf = tree.find(".//Dial/Queue") + + def test_conf_text(self): + self.assertEqual(self.conf.text.strip(), "TestQueueAttribute") + + def test_conf_waiturl(self): + self.assertEqual(self.conf.get('url'), "") + + def test_conf_method(self): + self.assertEqual(self.conf.get('method'), "GET") + + +class TestEnqueue(TwilioTest): + + def setUp(self): + r = Response() + r.enqueue("TestEnqueueAttribute", action="act", method='GET', + wait_url='wait', wait_url_method='POST') xml = r.toxml() #parse twiml XML string with Element Tree and inspect #structure tree = ET.fromstring(xml) - self.conf = tree.find(".//Dial/Queue") + self.conf = tree.find(".//Enqueue") def test_conf_text(self): - self.assertEqual(self.conf.text.strip(), "TestQueueAttribute") + self.assertEqual(self.conf.text.strip(), "TestEnqueueAttribute") def test_conf_waiturl(self): - self.assertEqual(self.conf.get('url'), "") + self.assertEqual(self.conf.get('wait_url'), "wait") def test_conf_method(self): self.assertEqual(self.conf.get('method'), "GET") + def test_conf_action(self): + self.assertEqual(self.conf.get('action'), "act") + + def test_conf_waitmethod(self): + self.assertEqual(self.conf.get('wait_url_method'), "POST") + class TestDial(TwilioTest): diff --git a/twilio/rest/__init__.py b/twilio/rest/__init__.py index dd58de2a94..1b52c7d222 100644 --- a/twilio/rest/__init__.py +++ b/twilio/rest/__init__.py @@ -7,6 +7,7 @@ from twilio.rest.resources import AuthorizedConnectApps from twilio.rest.resources import Calls from twilio.rest.resources import CallerIds +from twilio.rest.resources import Queues from twilio.rest.resources import ConnectApps from twilio.rest.resources import Notifications from twilio.rest.resources import Recordings @@ -108,7 +109,7 @@ def __init__(self, account=None, token=None, base="https://api.twilio.com", and be sure to replace the values for the Account SID and auth token with the values from your Twilio Account at https://www.twilio.com/user/account. """) - + self.base = base auth = (account, token) version_uri = "%s/%s" % (base, version) @@ -126,6 +127,7 @@ def __init__(self, account=None, token=None, base="https://api.twilio.com", self.sms = Sms(account_uri, auth) self.phone_numbers = PhoneNumbers(account_uri, auth) self.conferences = Conferences(account_uri, auth) + self.queues = Queues(account_uri, auth) self.sandboxes = Sandboxes(account_uri, auth) self.auth = auth @@ -138,4 +140,3 @@ def participants(self, conference_sid): """ base_uri = "%s/Conferences/%s" % (self.account_uri, conference_sid) return Participants(base_uri, self.auth) - diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 57201b4587..05d9496587 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -340,7 +340,7 @@ def update_instance(self, sid, body): Update an InstanceResource via a POST sid: string -- String identifier for the list resource - body: string -- Dict of items to POST + body: dictionary -- Dict of items to POST """ uri = "%s/%s" % (self.uri, sid) resp, entry = self.request("POST", uri, data=transform_params(body)) @@ -1183,13 +1183,13 @@ def update(self, **kwargs): current size results in the queue rejecting incoming requests until it shrinks below the new max size """ - return self.parent.update(self.sid, kwargs) + return self.parent.update_instance(self.name, kwargs) def delete(self): """ Delete this queue. Can only be run on empty queues. """ - return self.parent.delete(self.sid) + return self.parent.delete_instance(self.name) class Queues(ListResource): @@ -1199,12 +1199,13 @@ class Queues(ListResource): def list(self, **kwargs): return self.get_instances(kwargs) - def create(self, **kwargs): + def create(self, name, **kwargs): """ Create an :class:`Queue` with any of these optional parameters. - :param friendly_name: A human readable description of the application, + :param name: A human readable description of the application, with maximum length 64 characters. - :param max_size: The upper limit of calls allowed into the queue + :param max_size: The limit on calls allowed into the queue (optional) """ + kwargs['friendly_name'] = name return self.create_instance(kwargs) @@ -1317,6 +1318,7 @@ class Account(InstanceResource): PhoneNumbers, Conferences, ConnectApps, + Queues, AuthorizedConnectApps, ] diff --git a/twilio/twiml.py b/twilio/twiml.py index 907ab68a2d..62f7fdcf34 100644 --- a/twilio/twiml.py +++ b/twilio/twiml.py @@ -82,7 +82,7 @@ def xml(self): def append(self, verb): if not self.nestables or verb.name not in self.nestables: - raise TwimlException("%s is not nestable inside %s" % \ + raise TwimlException("%s is not nestable inside %s" % (verb.name, self.name)) self.verbs.append(verb) return verb @@ -101,6 +101,8 @@ class Response(Verb): 'Hangup', 'Reject', 'Sms', + 'Enqueue', + 'Leave' ] def __init__(self, **kwargs): @@ -147,6 +149,16 @@ def dial(self, number=None, **kwargs): :class:`Response` """ return self.append(Dial(number, **kwargs)) + def enqueue(self, name, **kwargs): + """Return a newly created :class:`Enqueue` verb, nested inside this + :class:`Response` """ + return self.append(Enqueue(name, **kwargs)) + + def leave(self, **kwargs): + """Return a newly created :class:`Leave` verb, nested inside this + :class:`Response` """ + return self.append(Leave(**kwargs)) + def record(self, **kwargs): """Return a newly created :class:`Record` verb, nested inside this :class:`Response` """ @@ -411,7 +423,23 @@ class Queue(Verb): :param name: friendly name for the queue :param url: url to a twiml document that executes after a call is dequeued - and before the call is connected + and before the call is connected + :param method: HTTP method for url GET/POST + """ + GET = 'GET' + POST = 'POST' + + def __init__(self, name, **kwargs): + super(Queue, self).__init__(**kwargs) + self.body = name + + +class Queue(Verb): + """Specify queue in a nested Dial element. + + :param name: friendly name for the queue + :param url: url to a twiml document that executes after a call is dequeued + and before the call is connected :param method: HTTP method for url GET/POST """ GET = 'GET' @@ -422,6 +450,33 @@ def __init__(self, name, **kwargs): self.body = name +class Enqueue(Verb): + """Enqueue the call into a specific queue. + + :param name: friendly name for the queue + :param action: url to a twiml document that executes when the call + leaves the queue. When dequeued vial a verb, + this url is executed after the bridged parties disconnect + :param method: HTTP method for action GET/POST + :param wait_url: url to a twiml document that executes + while the call is on the queue + :param wait_url_methid: HTTP method for wait_url GET/POST + """ + GET = 'GET' + POST = 'POST' + + def __init__(self, name, **kwargs): + super(Enqueue, self).__init__(**kwargs) + self.body = name + + +class Leave(Verb): + """Signals the call to leave it's queue + """ + GET = 'GET' + POST = 'POST' + + class Record(Verb): """Record audio from caller From 8e70e360dd6533ba304fe4081f0a957e08f864f6 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 00:03:17 -0700 Subject: [PATCH 04/16] Finished testing members --- tests/test_members.py | 45 ++++++++++++++++++++++++++++++++++++++++ twilio/rest/resources.py | 3 ++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/test_members.py diff --git a/tests/test_members.py b/tests/test_members.py new file mode 100644 index 0000000000..dc24013ab9 --- /dev/null +++ b/tests/test_members.py @@ -0,0 +1,45 @@ +from mock import patch, Mock +from datetime import date +from mock import patch, Mock +from nose.tools import raises, assert_equals, assert_true +from tools import create_mock_json +from twilio.rest.resources import Member, Members + +QUEUE_SID = "QU1b9faddec3d54ec18488f86c83019bf0" +ACCOUNT_SID = "AC123" +AUTH = (ACCOUNT_SID, "token") +CALL_SID = "CAaaf2e9ded94aba3e57c42a3d55be6ff2" +BASE_URI = "https://api.twilio.com/2010-04-01/Accounts/AC123/Queues/%s" % ( + QUEUE_SID) + +list_resource = Members(BASE_URI, AUTH) + +@patch("twilio.rest.resources.make_twilio_request") +def test_members_list(mock): + resp = create_mock_json("tests/resources/members_list.json") + mock.return_value = resp + + uri = "%s/Members" % (BASE_URI) + list_resource.list() + + mock.assert_called_with("GET", uri, params={}, auth=AUTH) + +@patch("twilio.rest.resources.make_twilio_request") +def test_members_dequeue_front(mock): + resp = create_mock_json("tests/resources/members_instance.json") + mock.return_value = resp + + uri = "%s/Members/Front" % (BASE_URI) + list_resource.dequeue() + + mock.assert_called_with("POST", uri, data={}, auth=AUTH) + +@patch("twilio.rest.resources.make_twilio_request") +def test_members_dequeue_call(mock): + resp = create_mock_json("tests/resources/members_instance.json") + mock.return_value = resp + + uri = "%s/Members/%s" % (BASE_URI, CALL_SID) + list_resource.dequeue(call_sid=CALL_SID) + + mock.assert_called_with("POST", uri, data={}, auth=AUTH) diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 05d9496587..1a152dd374 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1142,12 +1142,13 @@ def list(self, updated_before=None, updated_after=None, created_after=None, class Member(InstanceResource): - pass + id_key = "call_sid" class Members(ListResource): name = "Members" instance = Member + key = "queue_members" def list(self, **kwargs): """ From 2f73bd2d8e8301660c2919ba0db1ef6d0d863719 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 02:13:25 -0700 Subject: [PATCH 05/16] danyang can't spell nor can he copy and paste correctly --- twilio/rest/resources.py | 2 +- twilio/twiml.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 1a152dd374..c910ddbc5f 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1154,7 +1154,7 @@ def list(self, **kwargs): """ Returns a list of :class:`Member` resources in the given queue - :param queue_sid: Conference this participant is part of + :param queue_sid: Queue this participant is part of """ return self.get_instances(kwargs) diff --git a/twilio/twiml.py b/twilio/twiml.py index 62f7fdcf34..ec2fbdb168 100644 --- a/twilio/twiml.py +++ b/twilio/twiml.py @@ -471,7 +471,7 @@ def __init__(self, name, **kwargs): class Leave(Verb): - """Signals the call to leave it's queue + """Signals the call to leave its queue """ GET = 'GET' POST = 'POST' From 866eba569e338f10be96729d55636e5131af62db Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 02:18:07 -0700 Subject: [PATCH 06/16] remove duplicates and fix typos --- twilio/twiml.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/twilio/twiml.py b/twilio/twiml.py index ec2fbdb168..e8931262b4 100644 --- a/twilio/twiml.py +++ b/twilio/twiml.py @@ -434,28 +434,12 @@ def __init__(self, name, **kwargs): self.body = name -class Queue(Verb): - """Specify queue in a nested Dial element. - - :param name: friendly name for the queue - :param url: url to a twiml document that executes after a call is dequeued - and before the call is connected - :param method: HTTP method for url GET/POST - """ - GET = 'GET' - POST = 'POST' - - def __init__(self, name, **kwargs): - super(Queue, self).__init__(**kwargs) - self.body = name - - class Enqueue(Verb): """Enqueue the call into a specific queue. :param name: friendly name for the queue :param action: url to a twiml document that executes when the call - leaves the queue. When dequeued vial a verb, + leaves the queue. When dequeued via a verb, this url is executed after the bridged parties disconnect :param method: HTTP method for action GET/POST :param wait_url: url to a twiml document that executes From 867a286a061474b1c0e2891b06e860a822ea84fe Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 10:53:20 -0700 Subject: [PATCH 07/16] made url mandatory in dequeue --- tests/test_members.py | 9 +++++---- twilio/rest/resources.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_members.py b/tests/test_members.py index dc24013ab9..85bd39425f 100644 --- a/tests/test_members.py +++ b/tests/test_members.py @@ -11,6 +11,7 @@ CALL_SID = "CAaaf2e9ded94aba3e57c42a3d55be6ff2" BASE_URI = "https://api.twilio.com/2010-04-01/Accounts/AC123/Queues/%s" % ( QUEUE_SID) +TWIML_URL = "example_twiml_url" list_resource = Members(BASE_URI, AUTH) @@ -30,9 +31,9 @@ def test_members_dequeue_front(mock): mock.return_value = resp uri = "%s/Members/Front" % (BASE_URI) - list_resource.dequeue() + list_resource.dequeue(TWIML_URL) - mock.assert_called_with("POST", uri, data={}, auth=AUTH) + mock.assert_called_with("POST", uri, data={"Url": TWIML_URL}, auth=AUTH) @patch("twilio.rest.resources.make_twilio_request") def test_members_dequeue_call(mock): @@ -40,6 +41,6 @@ def test_members_dequeue_call(mock): mock.return_value = resp uri = "%s/Members/%s" % (BASE_URI, CALL_SID) - list_resource.dequeue(call_sid=CALL_SID) + list_resource.dequeue(TWIML_URL, call_sid=CALL_SID) - mock.assert_called_with("POST", uri, data={}, auth=AUTH) + mock.assert_called_with("POST", uri, data={"Url": TWIML_URL}, auth=AUTH) diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index c910ddbc5f..f81ae8d96d 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1158,7 +1158,7 @@ def list(self, **kwargs): """ return self.get_instances(kwargs) - def dequeue(self, call_sid='Front', **kwargs): + def dequeue(self, url, call_sid='Front'): """ Dequeues a member from the queue and have the member's call begin executing the TwiML document at the url. @@ -1167,7 +1167,7 @@ def dequeue(self, call_sid='Front', **kwargs): the member at the front of the queue will be used :param url: url of the TwiML document to be executed. """ - return self.update_instance(call_sid, kwargs) + return self.update_instance(call_sid, {"url":url}) class Queue(InstanceResource): From bbb814f01d12d11e7e8c9f8cd11b29e328d710fc Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 14:35:41 -0700 Subject: [PATCH 08/16] removed too much --- twilio/rest/resources.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index f81ae8d96d..6fab37fd73 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1158,7 +1158,7 @@ def list(self, **kwargs): """ return self.get_instances(kwargs) - def dequeue(self, url, call_sid='Front'): + def dequeue(self, url, call_sid='Front', **kwargs): """ Dequeues a member from the queue and have the member's call begin executing the TwiML document at the url. @@ -1167,7 +1167,8 @@ def dequeue(self, url, call_sid='Front'): the member at the front of the queue will be used :param url: url of the TwiML document to be executed. """ - return self.update_instance(call_sid, {"url":url}) + kwargs['url'] = url + return self.update_instance(call_sid, kwargs) class Queue(InstanceResource): From 6a3be2c8c574c438b78930225b99a47631ebd085 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 8 Aug 2012 16:50:07 -0700 Subject: [PATCH 09/16] Import parse_qs from cgi if needed Python 2.5 doesn't have a parse_qs method in urlparse. --- twilio/rest/resources.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 6fab37fd73..4fe6c44c84 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -5,7 +5,12 @@ from twilio import TwilioException from twilio import TwilioRestException from urllib import urlencode -from urlparse import urlparse, parse_qs +from urlparse import urlparse + +try: + from urlparse import parse_qs +except ImportError: + from cgi import parse_qs # import json try: From d8be3e1530a35ca0350c858a7f6a687ee11f87ca Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 8 Aug 2012 16:55:17 -0700 Subject: [PATCH 10/16] Import with statement from future --- tests/test_base_resource.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_base_resource.py b/tests/test_base_resource.py index b1b1d3b33d..e899e3a3a8 100644 --- a/tests/test_base_resource.py +++ b/tests/test_base_resource.py @@ -1,11 +1,12 @@ -""" -Test the base Resource class -""" +# -*- coding: utf-8 -*- +from __future__ import with_statement import sys + if sys.version_info < (2, 7): import unittest2 as unittest else: import unittest + from mock import Mock, patch from nose.tools import assert_equals from nose.tools import raises From aeefacc9ae57b169c0ab9528b32baa7451a4c4df Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 8 Aug 2012 17:28:01 -0700 Subject: [PATCH 11/16] Add Queue and Queue Member API reference --- docs/api/rest/resources.rst | 68 +++++++++++++++++++++++++++++++++++++ twilio/rest/resources.py | 24 ++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/docs/api/rest/resources.rst b/docs/api/rest/resources.rst index bf3a61aceb..f58e12a345 100644 --- a/docs/api/rest/resources.rst +++ b/docs/api/rest/resources.rst @@ -603,6 +603,74 @@ Phone Numbers .. attribute:: iso_country +Queues +>>>>>>>>>>> + +.. autoclass:: twilio.rest.resources.Queues + :members: + :exclude-members: instance + +.. autoclass:: twilio.rest.resources.Queue + :members: + + .. attribute:: sid + + A 34 character string that uniquely identifies this queue. + + .. attribute:: friendly_name + + A user-provided string that identifies this queue. + + .. attribute:: current_size + + The count of calls currently in the queue. + + .. attribute:: max_size + + The upper limit of calls allowed to be in the queue. + `unlimited` is an option. The default is 100. + + .. attribute:: average_wait_time + + The average wait time of the members of this queue in seconds. + This is calculated at the time of the request. + + .. attribute:: uri + + The URI for this resource, relative to https://api.twilio.com. + + +Queue Members +>>>>>>>>>>>>>> + +.. autoclass:: twilio.rest.resources.Members + :members: + :exclude-members: instance + +.. autoclass:: twilio.rest.resources.Member + :members: + + .. attribute:: call_sid + + A 34 character string that uniquely identifies the call that is enqueued. + + .. attribute:: date_enqueued + + The date that the member was enqueued, given in RFC 2822 format. + + .. attribute:: wait_time + + The number of seconds the member has been in the queue. + + .. attribute:: position + + This member’s current position in the queue. + + .. attribute:: uri + + The URI for this resource, relative to https://api.twilio.com. + + Recordings >>>>>>>>>>> diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 4fe6c44c84..8ddc55b9b4 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1185,6 +1185,7 @@ class Queue(InstanceResource): def update(self, **kwargs): """ Update this queue + :param friendly_name: A new friendly name for this queue :param max_size: A new max size. Changing a max size to less than the current size results in the queue rejecting incoming @@ -1208,6 +1209,7 @@ def list(self, **kwargs): def create(self, name, **kwargs): """ Create an :class:`Queue` with any of these optional parameters. + :param name: A human readable description of the application, with maximum length 64 characters. :param max_size: The limit on calls allowed into the queue (optional) @@ -1215,6 +1217,26 @@ def create(self, name, **kwargs): kwargs['friendly_name'] = name return self.create_instance(kwargs) + def update(self, sid, **kwargs): + """ + Update a :class:`Queue` + + :param sid: String identifier for a Queue resource + :param friendly_name: A new friendly name for this queue + :param max_size: A new max size. Changing a max size to less than the + current size results in the queue rejecting incoming + requests until it shrinks below the new max size + """ + return self.update_instance(sid, kwargs) + + def delete(self, sid): + """ + Delete a :class:`Queue`. Can only be run on empty queues. + + :param sid: String identifier for a Queue resource + """ + return self.delete_instance(sid) + class Application(InstanceResource): """ An application resource """ @@ -1302,7 +1324,7 @@ def update(self, sid, **kwargs): def delete(self, sid): """ - Update an :class:`Application` with the given parameters. + Delete an :class:`Application` """ return self.delete_instance(sid) From 1287dfac0fbd0dd0d995029f486973b238e0faa3 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 17:43:52 -0700 Subject: [PATCH 12/16] queues usage documentation --- docs/index.rst | 3 +- docs/usage/conferences.rst | 3 +- docs/usage/queues.rst | 105 +++++++++++++++++++++++++++++++++++++ twilio/rest/resources.py | 4 ++ 4 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 docs/usage/queues.rst diff --git a/docs/index.rst b/docs/index.rst index d6f1e4a849..216db080bd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,7 +60,7 @@ Query the Twilio REST API to create phone calls, send SMS messages and so much m usage/recordings usage/transcriptions usage/caller-ids - + usage/queues TwiML --------- @@ -125,4 +125,3 @@ Report bugs using the Github `issue tracker `_ See the :doc:`/changelog` for version history. - diff --git a/docs/usage/conferences.rst b/docs/usage/conferences.rst index f2e5520fae..54f6821512 100644 --- a/docs/usage/conferences.rst +++ b/docs/usage/conferences.rst @@ -4,7 +4,7 @@ Conferences and Participants ============================== -For more information, see the `Conference REST Resource `_ and `Participant REST Resource `_ documentation. +For more information, see the `Conference REST Resource `_ and `Participant REST Resource `_ documentation. Listing Conferences ----------------------- @@ -93,4 +93,3 @@ code kicks out the first participant and mutes the rest. # And mute the rest for participant in participants: participant.mute() - diff --git a/docs/usage/queues.rst b/docs/usage/queues.rst new file mode 100644 index 0000000000..852e3ac074 --- /dev/null +++ b/docs/usage/queues.rst @@ -0,0 +1,105 @@ +.. module:: twilio.rest.resources + +============================== +Queues and Members +============================== + +For more information, see the `Queue REST Resource `_ and `Member REST Resource `_ documentation. + +Listing Queues +----------------------- + +.. code-block:: python + + from twilio.rest import TwilioRestClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) + conferences = client.queues.list() + + for queue in queues: + print queue.sid + +Listing Queue Members +---------------------- + +Each :class:`Queue` has a :attr:`queue_members` instance which +represents all current calls in the queue. + +.. code-block:: python + + from twilio.rest import TwilioRestClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) + queue = client.queues.get("QU123") + + for member in queue.queue_members.list(): + print member.call_sid + +Getting a specific Queue Member +------------------------------- + +To retrieve information about a specific member in the queue, each +:class:`Members` has a :attr:`get` method. :attr:`get` accepts one +argument. The argument can either be a call_sid thats in the queue, +in which case :attr:`get` will return a :class:`Member` instance +representing that call, or the argument can be 'Front', in which case +:attr:`Get` will return a :class:`Member` instance representing the +first call in the queue. + +.. code-block:: python + + from twilio.rest import TwilioRestClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + QUEUE_SID = "QUaaaaaaaaaaaaa" + CALL_SID = "CAxxxxxxxxxxxxxx" + client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) + members = client.queues.get(QUEUE_SID).queue_members.list() + + if len(members) == 0: + return + + # Get the first call in the queue + print members.get('Front').date_enqueued + + # Get the call with the given call sid in the queue + print members.get(CALL_SID).current_position + + +Dequeueing Queue Members +------------------------ + +To dequeue a specific member from the queue, each +:class:`Members` has a :attr:`dequeue` method. :attr:`dequeue` accepts an +argument and two optional keyword arguments. The first argument is the +url of the twiml document to be executed when the member is +dequeued. The other two are :attr:`call_sid` and :attr:`method`, their +default values are 'Front' and 'GET' + +.. code-block:: python + + from twilio.rest import TwilioRestClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + QUEUE_SID = "QUaaaaaaaaaaaaa" + + client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) + members = client.queues.get(QUEUE_SID).queue_members.list() + + if len(members) == 0: + return + + # Dequeue the first call in the queue + print members.dequeue('http://www.twilio.com/welcome/call') diff --git a/twilio/rest/resources.py b/twilio/rest/resources.py index 6fab37fd73..7d0dfac34e 100644 --- a/twilio/rest/resources.py +++ b/twilio/rest/resources.py @@ -1199,6 +1199,10 @@ class Queues(ListResource): instance = Queue def list(self, **kwargs): + """ + Returns a page of :class:`Queue` resources as a list sorted by DateUpdated. + For paging informtion see :class:`ListResource` + """ return self.get_instances(kwargs) def create(self, name, **kwargs): From 92f51e8f925e7d424dd2c44eb99bad353f14b9f3 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 17:48:59 -0700 Subject: [PATCH 13/16] documentation fixes --- docs/usage/queues.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/usage/queues.rst b/docs/usage/queues.rst index 852e3ac074..592740d95c 100644 --- a/docs/usage/queues.rst +++ b/docs/usage/queues.rst @@ -48,7 +48,7 @@ Getting a specific Queue Member To retrieve information about a specific member in the queue, each :class:`Members` has a :attr:`get` method. :attr:`get` accepts one -argument. The argument can either be a call_sid thats in the queue, +argument. The argument can either be a `call_sid` thats in the queue, in which case :attr:`get` will return a :class:`Member` instance representing that call, or the argument can be 'Front', in which case :attr:`Get` will return a :class:`Member` instance representing the @@ -64,10 +64,7 @@ first call in the queue. QUEUE_SID = "QUaaaaaaaaaaaaa" CALL_SID = "CAxxxxxxxxxxxxxx" client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) - members = client.queues.get(QUEUE_SID).queue_members.list() - - if len(members) == 0: - return + members = client.queues.get(QUEUE_SID).queue_members # Get the first call in the queue print members.get('Front').date_enqueued @@ -96,10 +93,7 @@ default values are 'Front' and 'GET' QUEUE_SID = "QUaaaaaaaaaaaaa" client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) - members = client.queues.get(QUEUE_SID).queue_members.list() - - if len(members) == 0: - return + members = client.queues.get(QUEUE_SID).queue_members # Dequeue the first call in the queue print members.dequeue('http://www.twilio.com/welcome/call') From 94325139fb48857fd0258a79376f26bb0559f3a0 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 17:52:23 -0700 Subject: [PATCH 14/16] twiml docs --- docs/api/twiml.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/api/twiml.rst b/docs/api/twiml.rst index 210a76f459..c1eada91e0 100644 --- a/docs/api/twiml.rst +++ b/docs/api/twiml.rst @@ -43,6 +43,12 @@ Secondary Verbs .. autoclass:: twilio.twiml.Sms :members: +.. autoclass:: twilio.twiml.Enqueue + :members: + +.. autoclass:: twilio.twiml.Leave + :members: + Nouns ~~~~~~ @@ -54,3 +60,6 @@ Nouns .. autoclass:: twilio.twiml.Client :members: + +.. autoclass:: twilio.twiml.Queue + :members: From 522832dd521e27f5d2028b808245c743da6b4745 Mon Sep 17 00:00:00 2001 From: Dan Yang Date: Wed, 8 Aug 2012 17:54:34 -0700 Subject: [PATCH 15/16] documentation fixes --- twilio/twiml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twilio/twiml.py b/twilio/twiml.py index e8931262b4..4042621033 100644 --- a/twilio/twiml.py +++ b/twilio/twiml.py @@ -423,7 +423,7 @@ class Queue(Verb): :param name: friendly name for the queue :param url: url to a twiml document that executes after a call is dequeued - and before the call is connected + and before the call is connected :param method: HTTP method for url GET/POST """ GET = 'GET' @@ -444,7 +444,7 @@ class Enqueue(Verb): :param method: HTTP method for action GET/POST :param wait_url: url to a twiml document that executes while the call is on the queue - :param wait_url_methid: HTTP method for wait_url GET/POST + :param wait_url_method: HTTP method for wait_url GET/POST """ GET = 'GET' POST = 'POST' From 8d793dd02e8a27b66a76899705568788d0ff91e2 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 8 Aug 2012 17:56:56 -0700 Subject: [PATCH 16/16] Bump to version 3.3.10 --- CHANGES | 6 ++++++ docs/conf.py | 2 +- twilio/__init__.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 57b9510595..fa2f2df2c7 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,12 @@ twilio-python Changelog Here you can see the full list of changes between each twilio-python release. +Version 3.3.10 +------------- + +- Add support for Queue. Fix a bug where the library wouldn't work in Python 2.5 + + Version 3.3.9 ------------- diff --git a/docs/conf.py b/docs/conf.py index 8399e5eb7e..fd20c9600e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,7 +57,7 @@ # The short X.Y version. version = '3.3' # The full version, including alpha/beta/rc tags. -release = '3.3.9' +release = '3.3.10' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/twilio/__init__.py b/twilio/__init__.py index 555e6229f6..5984528eb1 100644 --- a/twilio/__init__.py +++ b/twilio/__init__.py @@ -1,4 +1,4 @@ -__version_info__ = ('3', '3', '9') +__version_info__ = ('3', '3', '10') __version__ = '.'.join(__version_info__)