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/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/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: 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/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..592740d95c --- /dev/null +++ b/docs/usage/queues.rst @@ -0,0 +1,99 @@ +.. 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 + + # 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 + + # Dequeue the first call in the queue + print members.dequeue('http://www.twilio.com/welcome/call') 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_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 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_members.py b/tests/test_members.py new file mode 100644 index 0000000000..85bd39425f --- /dev/null +++ b/tests/test_members.py @@ -0,0 +1,46 @@ +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) +TWIML_URL = "example_twiml_url" + +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(TWIML_URL) + + 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): + resp = create_mock_json("tests/resources/members_instance.json") + mock.return_value = resp + + uri = "%s/Members/%s" % (BASE_URI, CALL_SID) + list_resource.dequeue(TWIML_URL, call_sid=CALL_SID) + + mock.assert_called_with("POST", uri, data={"Url": TWIML_URL}, auth=AUTH) 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 d30bb44420..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): @@ -332,6 +350,58 @@ 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 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(".//Enqueue") + + def test_conf_text(self): + self.assertEqual(self.conf.text.strip(), "TestEnqueueAttribute") + + def test_conf_waiturl(self): + 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): def testDial(self): @@ -374,6 +444,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/__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__) 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 2378178bdb..dc89847d47 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: @@ -340,7 +345,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)) @@ -1141,6 +1146,102 @@ def list(self, updated_before=None, updated_after=None, created_after=None, return self.get_instances(kwargs) +class Member(InstanceResource): + id_key = "call_sid" + + +class Members(ListResource): + name = "Members" + instance = Member + key = "queue_members" + + def list(self, **kwargs): + """ + Returns a list of :class:`Member` resources in the given queue + + :param queue_sid: Queue this participant is part of + """ + return self.get_instances(kwargs) + + 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. + + :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. + """ + kwargs['url'] = url + 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_instance(self.name, kwargs) + + def delete(self): + """ + Delete this queue. Can only be run on empty queues. + """ + return self.parent.delete_instance(self.name) + + +class Queues(ListResource): + name = "Queues" + 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): + """ 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) + """ + 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 """ @@ -1227,7 +1328,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) @@ -1250,6 +1351,7 @@ class Account(InstanceResource): PhoneNumbers, Conferences, ConnectApps, + Queues, AuthorizedConnectApps, ] diff --git a/twilio/twiml.py b/twilio/twiml.py index ba7d028541..4042621033 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` """ @@ -367,9 +379,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 +389,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 +408,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 +418,49 @@ 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 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 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 + while the call is on the queue + :param wait_url_method: 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 its queue + """ + GET = 'GET' + POST = 'POST' + + class Record(Verb): """Record audio from caller