From 0af4776a961a5b0fef8052082fb1f4027cff172e Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Thu, 2 Mar 2017 14:22:18 -0800
Subject: [PATCH 1/7] Move TwiML to unique types

---
 twilio/twiml/__init__.py           |  76 ++++++
 twilio/twiml/messaging_response.py |  64 +++++
 twilio/twiml/voice_response.py     | 390 +++++++++++++++++++++++++++++
 3 files changed, 530 insertions(+)
 create mode 100644 twilio/twiml/__init__.py
 create mode 100644 twilio/twiml/messaging_response.py
 create mode 100644 twilio/twiml/voice_response.py

diff --git a/twilio/twiml/__init__.py b/twilio/twiml/__init__.py
new file mode 100644
index 0000000000..e7c5483079
--- /dev/null
+++ b/twilio/twiml/__init__.py
@@ -0,0 +1,76 @@
+import xml.etree.ElementTree as ET
+
+
+def lower_camel(string):
+    result = "".join([x.title() for x in string.split('_')])
+    if not result:
+        return result
+
+    return result[0].lower() + result[1:]
+
+
+class TwiML(object):
+    """
+    Twilio basic verb object.
+    """
+    MAP = {
+        'from_': 'from'
+    }
+
+    def __init__(self, **kwargs):
+        self.name = self.__class__.__name__
+        self.body = None
+        self.verbs = []
+        self.attrs = {}
+
+        for k, v in kwargs.items():
+            if v is not None:
+                self.attrs[lower_camel(self.MAP.get(k, k))] = v
+
+    def __str__(self):
+        return self.to_xml()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        return False
+
+    def to_xml(self, xml_declaration=True):
+        """
+        Return the contents of this verb as an XML string
+
+        :param bool xml_declaration: Include the XML declaration. Defaults to
+                                     True
+        """
+        xml = ET.tostring(self.xml()).decode('utf-8')
+
+        if xml_declaration:
+            return '<?xml version="1.0" encoding="UTF-8"?>' + xml
+        else:
+            return xml
+
+    def xml(self):
+        el = ET.Element(self.name)
+
+        keys = self.attrs.keys()
+        keys = sorted(keys)
+        for a in keys:
+            value = self.attrs[a]
+
+            if isinstance(value, bool):
+                el.set(a, str(value).lower())
+            else:
+                el.set(a, str(value))
+
+        if self.body:
+            el.text = self.body
+
+        for verb in self.verbs:
+            el.append(verb.xml())
+
+        return el
+
+    def append(self, verb):
+        self.verbs.append(verb)
+        return verb
diff --git a/twilio/twiml/messaging_response.py b/twilio/twiml/messaging_response.py
new file mode 100644
index 0000000000..2a99296713
--- /dev/null
+++ b/twilio/twiml/messaging_response.py
@@ -0,0 +1,64 @@
+from twilio.twiml import TwiML
+
+
+class MessagingResponse(TwiML):
+
+    def __init__(self):
+        super(MessagingResponse, self).__init__()
+        self.name = 'Response'
+
+    def message(self,
+                body,
+                to=None,
+                from_=None,
+                method=None,
+                action=None,
+                status_callback=None,
+                **kwargs):
+        return self.append(Message(
+            body=body,
+            to=to,
+            from_=from_,
+            method=method,
+            action=action,
+            status_callback=status_callback,
+            **kwargs
+        ))
+
+    def redirect(self, method=None, url=None, **kwargs):
+        return self.append(Redirect(
+            method=method,
+            url=url,
+            **kwargs
+        ))
+
+
+class Message(TwiML):
+
+    def __init__(self, body=None, **kwargs):
+        super(Message, self).__init__(**kwargs)
+        if body:
+            self.body = body
+
+    def body(self, body):
+        return self.append(Body(body))
+
+    def media(self, url):
+        return self.append(Media(url))
+
+
+class Body(TwiML):
+    def __init__(self, body):
+        super(Body, self).__init__()
+        self.body = body
+
+
+class Media(TwiML):
+    def __init__(self, url):
+        super(Media, self).__init__()
+        self.body = url
+
+
+class Redirect(TwiML):
+    pass
+
diff --git a/twilio/twiml/voice_response.py b/twilio/twiml/voice_response.py
new file mode 100644
index 0000000000..6c82dcf0a3
--- /dev/null
+++ b/twilio/twiml/voice_response.py
@@ -0,0 +1,390 @@
+from twilio.twiml import TwiML
+
+
+class VoiceResponse(TwiML):
+
+    def __init__(self):
+        super(VoiceResponse, self).__init__()
+        self.name = 'Response'
+
+    def dial(self,
+             number,
+             action=None,
+             method=None,
+             timeout=None,
+             hangup_on_star=None,
+             time_limit=None,
+             caller_id=None,
+             record=None,
+             trim=None,
+             recording_status_callback=None,
+             recording_status_callback_method=None,
+             **kwargs):
+        return self.append(Dial(
+            number=number,
+            action=action,
+            method=method,
+            timeout=timeout,
+            hangup_on_star=hangup_on_star,
+            time_limit=time_limit,
+            caller_id=caller_id,
+            record=record,
+            trim=trim,
+            recording_status_callback=recording_status_callback,
+            recording_status_callback_method=recording_status_callback_method,
+            **kwargs
+        ))
+
+    def enqueue(self,
+                name,
+                action=None,
+                method=None,
+                wait_url=None,
+                wait_url_method=None,
+                workflow_sid=None,
+                **kwargs):
+        return self.append(Enqueue(
+            name,
+            action=action,
+            method=method,
+            wait_url=wait_url,
+            wait_url_method=wait_url_method,
+            workflow_sid=workflow_sid,
+            **kwargs
+        ))
+
+    def gather(self,
+               action=None,
+               method=None,
+               timeout=None,
+               finish_on_key=None,
+               num_digits=None,
+               **kwargs):
+        return self.append(Gather(
+            action=action,
+            method=method,
+            timeout=timeout,
+            finish_on_key=finish_on_key,
+            num_digits=num_digits,
+        ))
+
+    def hangup(self):
+        return self.append(Hangup())
+
+    def leave(self):
+        return self.append(Leave())
+
+    def pause(self, length=None):
+        return self.append(Pause(length=length))
+
+    def play(self,
+             url,
+             loop=None,
+             digits=None,
+             **kwargs):
+        return self.append(Play(
+            url,
+            loop=loop,
+            digits=digits,
+            **kwargs
+        ))
+
+    def record(self,
+               action=None,
+               method=None,
+               timeout=None,
+               finish_on_key=None,
+               max_length=None,
+               play_beep=None,
+               trim=None,
+               recording_status_callback=None,
+               recording_status_callback_method=None,
+               transcribe=None,
+               transcribe_callback=None,
+               **kwargs):
+        return self.append(Record(
+            action=action,
+            method=method,
+            timeout=timeout,
+            finish_on_key=finish_on_key,
+            max_length=max_length,
+            play_beep=play_beep,
+            trim=trim,
+            recording_status_callback=recording_status_callback,
+            recording_status_callback_method=recording_status_callback_method,
+            transcribe=transcribe,
+            transcribe_callback=transcribe_callback,
+            **kwargs
+        ))
+
+    def redirect(self, url, method=None, **kwargs):
+        return self.append(Redirect(url, method=method, **kwargs))
+
+    def reject(self, reason=None, **kwargs):
+        return self.append(Reject(reason=reason, **kwargs))
+
+    def say(self,
+            body,
+            loop=None,
+            language=None,
+            voice=None,
+            **kwargs):
+        return self.append(Say(
+            body,
+            loop=loop,
+            language=language,
+            voice=voice,
+            **kwargs
+        ))
+
+    def sms(self,
+            body,
+            to=None,
+            from_=None,
+            method=None,
+            action=None,
+            status_callback=None,
+            **kwargs):
+        return self.append(Sms(
+            body,
+            to=to,
+            from_=from_,
+            method=method,
+            action=action,
+            status_callback=status_callback,
+            **kwargs
+        ))
+
+
+class Dial(TwiML):
+    def __init__(self, number=None, **kwargs):
+        super(Dial, self).__init__(**kwargs)
+        if number:
+            self.body = number
+
+    def client(self,
+               name,
+               method=None,
+               url=None,
+               status_callback_event=None,
+               status_callback_method=None,
+               status_callback=None,
+               **kwargs):
+        return self.append(Client(
+            name,
+            method=method,
+            url=url,
+            status_callback_event=status_callback_event,
+            status_callback_method=status_callback_method,
+            status_callback=status_callback,
+            **kwargs
+        ))
+
+    def conference(self,
+                   name,
+                   muted=None,
+                   start_conference_on_enter=None,
+                   end_conference_on_exit=None,
+                   max_participants=None,
+                   beep=None,
+                   record=None,
+                   trim=None,
+                   wait_method=None,
+                   wait_url=None,
+                   event_callback_url=None,
+                   status_callback_event=None,
+                   status_callback=None,
+                   status_callback_method=None,
+                   recording_status_callback=None,
+                   recording_status_callback_method=None,
+                   **kwargs):
+        return self.append(Conference(
+            name,
+            start_conference_on_enter=start_conference_on_enter,
+            end_conference_on_exit=end_conference_on_exit,
+            max_participants=max_participants,
+            beep=beep,
+            record=record,
+            trim=trim,
+            wait_method=wait_method,
+            wait_url=wait_url,
+            event_callback_url=event_callback_url,
+            status_callback_event=status_callback_event,
+            status_callback=status_callback,
+            status_callback_method=status_callback_method,
+            recording_status_callback=recording_status_callback,
+            recording_status_callback_method=recording_status_callback_method,
+            **kwargs
+        ))
+
+    def number(self,
+               number,
+               send_digits=None,
+               url=None,
+               method=None,
+               status_callback_event=None,
+               status_callback=None,
+               status_callback_method=None,
+               **kwargs):
+        return self.append(Number(
+            number,
+            send_digits=send_digits,
+            url=url,
+            method=method,
+            status_callback_event=status_callback_event,
+            status_callback=status_callback,
+            status_callback_method=status_callback_method,
+            **kwargs
+        ))
+
+    def queue(self,
+              queue_name,
+              url=None,
+              method=None,
+              reservation_sid=None,
+              post_work_activity_sid=None,
+              **kwargs):
+        return self.append(Queue(
+            queue_name,
+            url=url,
+            method=method,
+            reservation_sid=reservation_sid,
+            post_work_activity_sid=post_work_activity_sid,
+            **kwargs
+        ))
+
+    def sip(self,
+            uri,
+            username=None,
+            password=None,
+            url=None,
+            method=None,
+            status_callback_event=None,
+            status_callback=None,
+            status_callback_method=None,
+            **kwargs):
+        return self.append(Sip(
+            uri,
+            username=username,
+            password=password,
+            url=url,
+            method=method,
+            status_callback_event=status_callback_event,
+            status_callback=status_callback,
+            status_callback_method=status_callback_method,
+            **kwargs
+        ))
+
+
+class Client(TwiML):
+    def __init__(self, name, **kwargs):
+        super(Client, self).__init__(**kwargs)
+        self.body = name
+
+
+class Conference(TwiML):
+    def __init__(self, name, **kwargs):
+        super(Conference, self).__init__(**kwargs)
+        self.body = name
+
+
+class Number(TwiML):
+    def __init__(self, number, **kwargs):
+        super(Number, self).__init__(**kwargs)
+        self.body = number
+
+
+class Queue(TwiML):
+    def __init__(self, queue_name, **kwargs):
+        super(Queue, self).__init__(**kwargs)
+        self.body = queue_name
+
+
+class Sip(TwiML):
+    def __init__(self, uri, **kwargs):
+        super(Sip, self).__init__(**kwargs)
+        self.body = uri
+
+
+class Enqueue(TwiML):
+    def __init__(self, name, **kwargs):
+        super(Enqueue, self).__init__(**kwargs)
+        self.body = name
+
+
+class Gather(TwiML):
+    def __init__(self, **kwargs):
+        super(Gather, self).__init__(**kwargs)
+
+    def say(self,
+            body,
+            loop=None,
+            language=None,
+            voice=None,
+            **kwargs):
+        return self.append(Say(
+            body,
+            loop=loop,
+            language=language,
+            voice=voice,
+            **kwargs
+        ))
+
+    def play(self,
+             url,
+             loop=None,
+             digits=None,
+             **kwargs):
+        return self.append(Play(
+            url,
+            loop=loop,
+            digits=digits,
+            **kwargs
+        ))
+
+    def pause(self, length=None):
+        return self.append(Pause(length=length))
+
+
+class Pause(TwiML):
+    pass
+
+
+class Play(TwiML):
+    def __init__(self, url, **kwargs):
+        super(Play, self).__init__(**kwargs)
+        self.body = url
+
+
+class Say(TwiML):
+    def __init__(self, body, **kwargs):
+        super(Say, self).__init__(**kwargs)
+        self.body = body
+
+
+class Hangup(TwiML):
+    pass
+
+
+class Leave(TwiML):
+    pass
+
+
+class Record(TwiML):
+    pass
+
+
+class Redirect(TwiML):
+    def __init__(self, url, **kwargs):
+        super(Redirect, self).__init__(**kwargs)
+        self.body = url
+
+
+class Reject(TwiML):
+    pass
+
+
+class Sms(TwiML):
+    def __init__(self, body, **kwargs):
+        super(Sms, self).__init__(**kwargs)
+        self.body = body

From bab5f055ac8050361c1072e0e3048646df06dd61 Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Thu, 2 Mar 2017 14:22:34 -0800
Subject: [PATCH 2/7] Add unit tests

---
 tests/unit/twiml/__init__.py                |   8 +
 tests/unit/twiml/test_messaging_response.py |  70 +++
 tests/unit/twiml/test_voice_response.py     | 512 ++++++++++++++++++++
 3 files changed, 590 insertions(+)
 create mode 100644 tests/unit/twiml/__init__.py
 create mode 100644 tests/unit/twiml/test_messaging_response.py
 create mode 100644 tests/unit/twiml/test_voice_response.py

diff --git a/tests/unit/twiml/__init__.py b/tests/unit/twiml/__init__.py
new file mode 100644
index 0000000000..069ececb94
--- /dev/null
+++ b/tests/unit/twiml/__init__.py
@@ -0,0 +1,8 @@
+import unittest
+
+from six import text_type
+
+
+class TwilioTest(unittest.TestCase):
+    def strip(self, xml):
+        return text_type(xml)
diff --git a/tests/unit/twiml/test_messaging_response.py b/tests/unit/twiml/test_messaging_response.py
new file mode 100644
index 0000000000..c0ed683d74
--- /dev/null
+++ b/tests/unit/twiml/test_messaging_response.py
@@ -0,0 +1,70 @@
+from nose.tools import assert_equal
+from tests.unit.twiml import TwilioTest
+from twilio.twiml.messaging_response import MessagingResponse, Body, Media
+
+
+class TestResponse(TwilioTest):
+
+    def test_empty_response(self):
+        r = MessagingResponse()
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response />'
+        )
+
+    def test_response(self):
+        r = MessagingResponse()
+        r.message('Hello')
+        r.redirect(url='example.com')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Message>Hello</Message><Redirect url="example.com" /></Response>'
+        )
+
+
+class TestMessage(TwilioTest):
+
+    def test_body(self):
+        r = MessagingResponse()
+        r.message('Hello')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Message>Hello</Message></Response>'
+        )
+
+    def test_nested_body(self):
+        b = Body('Hello World')
+
+        r = MessagingResponse()
+        r.append(b)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Body>Hello World</Body></Response>'
+        )
+
+    def test_nested_body_media(self):
+        b = Body('Hello World')
+        m = Media('hey.jpg')
+
+        r = MessagingResponse()
+        r.append(b)
+        r.append(m)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Body>Hello World</Body><Media>hey.jpg</Media></Response>'
+        )
+
+
+class TestRedirect(TwilioTest):
+    def test_redirect(self):
+        r = MessagingResponse()
+        r.redirect(url='example.com')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect url="example.com" /></Response>'
+        )
diff --git a/tests/unit/twiml/test_voice_response.py b/tests/unit/twiml/test_voice_response.py
new file mode 100644
index 0000000000..8e5320b528
--- /dev/null
+++ b/tests/unit/twiml/test_voice_response.py
@@ -0,0 +1,512 @@
+# -*- coding: utf-8 -*-
+from nose.tools import assert_equal
+from six import u
+from tests.unit.twiml import TwilioTest
+from twilio.twiml.voice_response import VoiceResponse, Dial, Gather
+
+
+class TestResponse(TwilioTest):
+
+    def test_empty_response(self):
+        r = VoiceResponse()
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response />'
+        )
+
+    def test_response(self):
+        r = VoiceResponse()
+        r.hangup()
+        r.leave()
+        r.sms(
+            'twilio sms',
+            to='+11234567890',
+            from_='+10987654321'
+        )
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup /><Leave /><Sms from="+10987654321" to="+11234567890">twilio sms</Sms></Response>'
+        )
+
+
+class TestSay(TwilioTest):
+
+    def test_empty_say(self):
+        """ should be a say with no text """
+        r = VoiceResponse()
+        r.say('')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say /></Response>'
+        )
+
+    def test_say_hello_world(self):
+        """ should say hello world """
+        r = VoiceResponse()
+        r.say('Hello World')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say>Hello World</Say></Response>'
+        )
+
+    def test_say_french(self):
+        """ should say hello monkey """
+        r = VoiceResponse()
+        r.say(u('n\xe9cessaire et d\'autres'))
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say>n&#233;cessaire et d\'autres</Say></Response>'
+        )
+
+    def test_say_loop(self):
+        """ should say hello monkey and loop 3 times """
+        r = VoiceResponse()
+        r.say('Hello Monkey', loop=3)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say loop="3">Hello Monkey</Say></Response>'
+        )
+
+    def test_say_loop_gb(self):
+        """ should say have a woman say hello monkey and loop 3 times """
+        r = VoiceResponse()
+        r.say('Hello Monkey', language='en-gb')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say language="en-gb">Hello Monkey</Say></Response>'
+        )
+
+    def test_say_loop_woman(self):
+        """ should say have a woman say hello monkey and loop 3 times """
+        r = VoiceResponse()
+        r.say('Hello Monkey', loop=3, voice='woman')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say loop="3" voice="woman">Hello Monkey</Say></Response>'
+        )
+
+    def test_say_all(self):
+        """ convenience method: should say have a woman say hello monkey and loop 3 times and be in french """
+        r = VoiceResponse()
+        r.say('Hello Monkey', loop=3, voice='man', language='fr')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Say language="fr" loop="3" voice="man">Hello Monkey</Say></Response>'
+        )
+
+
+class TestPlay(TwilioTest):
+
+    def test_empty_play(self):
+        """ should play hello monkey """
+        r = VoiceResponse()
+        r.play('')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Play /></Response>'
+        )
+
+    def test_play_hello(self):
+        """ should play hello monkey """
+        r = VoiceResponse()
+        r.play('http://hellomonkey.mp3')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Play>http://hellomonkey.mp3</Play></Response>'
+        )
+
+    def test_play_hello_loop(self):
+        """ should play hello monkey loop """
+        r = VoiceResponse()
+        r.play('http://hellomonkey.mp3', loop=3)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Play loop="3">http://hellomonkey.mp3</Play></Response>'
+        )
+
+    def test_play_digits(self):
+        """ should play digits """
+        r = VoiceResponse()
+        r.play('', digits='w123')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Play digits="w123" /></Response>'
+        )
+
+
+class TestRecord(TwilioTest):
+
+    def test_record_empty(self):
+        """ should record """
+        r = VoiceResponse()
+        r.record()
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Record /></Response>'
+        )
+
+    def test_record_action_method(self):
+        """ should record with an action and a get method """
+        r = VoiceResponse()
+        r.record(action='example.com', method='GET')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Record action="example.com" method="GET" /></Response>'
+        )
+
+    def test_record_max_length_finish_timeout(self):
+        """ should record with an maxLength, finishOnKey, and timeout """
+        r = VoiceResponse()
+        r.record(timeout=4, finish_on_key='#', max_length=30)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Record finishOnKey="#" maxLength="30" timeout="4" /></Response>'
+        )
+
+    def test_record_transcribe(self):
+        """ should record with a transcribe and transcribeCallback """
+        r = VoiceResponse()
+        r.record(transcribe_callback='example.com')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Record transcribeCallback="example.com" /></Response>'
+        )
+
+
+class TestRedirect(TwilioTest):
+
+    def test_redirect_empty(self):
+        r = VoiceResponse()
+        r.redirect('')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect /></Response>'
+        )
+
+    def test_redirect_method(self):
+        r = VoiceResponse()
+        r.redirect('example.com', method='POST')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect method="POST">example.com</Redirect></Response>'
+        )
+
+    def test_redirect_method_params(self):
+        r = VoiceResponse()
+        r.redirect('example.com?id=34&action=hey', method='POST')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect method="POST">example.com?id=34&amp;action=hey</Redirect></Response>'
+        )
+
+
+class TestHangup(TwilioTest):
+
+    def test_hangup(self):
+        """ convenience: should Hangup to a url via POST """
+        r = VoiceResponse()
+        r.hangup()
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup /></Response>'
+        )
+
+
+class TestLeave(TwilioTest):
+
+    def test_leave(self):
+        """ convenience: should Hangup to a url via POST """
+        r = VoiceResponse()
+        r.leave()
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Leave /></Response>'
+        )
+
+
+class TestReject(TwilioTest):
+
+    def test_reject(self):
+        """ should be a Reject with default reason """
+        r = VoiceResponse()
+        r.reject()
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Reject /></Response>'
+        )
+
+
+class TestSms(TwilioTest):
+
+    def test_empty(self):
+        """ Test empty sms verb """
+        r = VoiceResponse()
+        r.sms('')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Sms /></Response>'
+        )
+
+    def test_body(self):
+        """ Test hello world """
+        r = VoiceResponse()
+        r.sms('Hello, World')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Sms>Hello, World</Sms></Response>'
+        )
+
+    def test_to_from_action(self):
+        """ Test the to, from, and status callback """
+        r = VoiceResponse()
+        r.sms('Hello, World', to=1231231234, from_=3453453456, status_callback='example.com?id=34&action=hey')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Sms from="3453453456" statusCallback="example.com?id=34&amp;action=hey" to="1231231234">Hello, World</Sms></Response>'
+        )
+
+    def test_action_method(self):
+        """ Test the action and method parameters on Sms """
+        r = VoiceResponse()
+        r.sms('Hello', method='POST', action='example.com?id=34&action=hey')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Sms action="example.com?id=34&amp;action=hey" method="POST">Hello</Sms></Response>'
+        )
+
+
+class TestConference(TwilioTest):
+
+    def test_conference(self):
+        d = Dial()
+        d.conference(
+            'TestConferenceAttributes',
+            beep=False,
+            wait_url='',
+            start_conference_on_enter=True,
+            end_conference_on_exit=True
+        )
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Conference beep="false" endConferenceOnExit="true" startConferenceOnEnter="true" waitUrl="">TestConferenceAttributes</Conference></Dial></Response>'
+        )
+
+
+class TestQueue(TwilioTest):
+
+    def test_queue(self):
+        d = Dial()
+        d.queue('TestQueueAttribute', url='', method='GET')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Queue method="GET" url="">TestQueueAttribute</Queue></Dial></Response>'
+        )
+
+
+class TestEnqueue(TwilioTest):
+
+    def test_enqueue(self):
+        r = VoiceResponse()
+        r.enqueue(
+            'TestEnqueueAttribute',
+            action='act',
+            method='GET',
+            wait_url='wait',
+            wait_url_method='POST'
+        )
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Enqueue action="act" method="GET" waitUrl="wait" waitUrlMethod="POST">TestEnqueueAttribute</Enqueue></Response>'
+        )
+
+
+class TestDial(TwilioTest):
+
+    def test_dial(self):
+        """ should redirect the call """
+        r = VoiceResponse()
+        r.dial("1231231234")
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial>1231231234</Dial></Response>'
+        )
+
+    def test_sip(self):
+        """ should redirect the call """
+        d = Dial()
+        d.sip('foo@example.com')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip>foo@example.com</Sip></Dial></Response>'
+        )
+
+    def test_sip_username_password(self):
+        """ should redirect the call """
+        d = Dial()
+        d.sip('foo@example.com', username='foo', password='bar')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip password="bar" username="foo">foo@example.com</Sip></Dial></Response>'
+        )
+
+    def test_add_number(self):
+        """ add a number to a dial """
+        d = Dial()
+        d.number('1231231234')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Number>1231231234</Number></Dial></Response>'
+        )
+
+    def test_add_number_status_callback_event(self):
+        """ add a number to a dial with status callback events"""
+        d = Dial()
+        d.number('1231231234', status_callback='http://example.com', status_callback_event='initiated completed')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Number statusCallback="http://example.com" statusCallbackEvent="initiated completed">1231231234</Number></Dial></Response>'
+        )
+
+    def test_add_conference(self):
+        """ add a conference to a dial """
+        d = Dial()
+        d.conference('My Room')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Conference>My Room</Conference></Dial></Response>'
+        )
+
+    def test_add_queue(self):
+        """ add a queue to a dial """
+        d = Dial()
+        d.queue('The Cute Queue')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Queue>The Cute Queue</Queue></Dial></Response>'
+        )
+
+    def test_add_empty_client(self):
+        """ add an empty client to a dial """
+        d = Dial()
+        d.client('')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Client /></Dial></Response>'
+        )
+
+    def test_add_client(self):
+        """ add a client to a dial """
+        d = Dial()
+        d.client('alice')
+
+        r = VoiceResponse()
+        r.append(d)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Client>alice</Client></Dial></Response>'
+        )
+
+
+class TestGather(TwilioTest):
+
+    def test_empty(self):
+        """ a gather with nothing inside """
+        r = VoiceResponse()
+        r.gather()
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Gather /></Response>'
+        )
+
+    def test_gather_say(self):
+        g = Gather()
+        g.say('Hello')
+
+        r = VoiceResponse()
+        r.append(g)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Gather><Say>Hello</Say></Gather></Response>'
+        )
+
+    def test_nested_say_play_pause(self):
+        """ a gather with a say, play, and pause """
+        g = Gather()
+        g.say('Hey')
+        g.play('hey.mp3')
+        g.pause()
+
+        r = VoiceResponse()
+        r.append(g)
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Gather><Say>Hey</Say><Play>hey.mp3</Play><Pause /></Gather></Response>'
+        )

From 159a5adedc277d94cb4ecd44f7baec0d4d405f78 Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Thu, 2 Mar 2017 14:22:44 -0800
Subject: [PATCH 3/7] Delete old TwiML files

---
 tests/unit/test_twiml.py | 593 ---------------------------------------
 twilio/twiml.py          | 574 -------------------------------------
 2 files changed, 1167 deletions(-)
 delete mode 100644 tests/unit/test_twiml.py
 delete mode 100644 twilio/twiml.py

diff --git a/tests/unit/test_twiml.py b/tests/unit/test_twiml.py
deleted file mode 100644
index 7bab2ad1e9..0000000000
--- a/tests/unit/test_twiml.py
+++ /dev/null
@@ -1,593 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import with_statement
-import unittest
-import xml.etree.ElementTree as ET
-
-from nose.tools import assert_equal
-from six import u, text_type
-
-from twilio import twiml
-from twilio.twiml import TwimlException
-from twilio.twiml import Response
-
-
-class TwilioTest(unittest.TestCase):
-    def strip(self, xml):
-        return text_type(xml)
-
-    def improperAppend(self, verb):
-        self.assertRaises(TwimlException, verb.append, twiml.Say(""))
-        self.assertRaises(TwimlException, verb.append, twiml.Gather())
-        self.assertRaises(TwimlException, verb.append, twiml.Play(""))
-        self.assertRaises(TwimlException, verb.append, twiml.Record())
-        self.assertRaises(TwimlException, verb.append, twiml.Hangup())
-        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())
-
-
-class TestResponse(TwilioTest):
-
-    def testEmptyResponse(self):
-        r = Response()
-        assert_equal(self.strip(r), '<?xml version="1.0" encoding="UTF-8"?><Response />')
-
-    def testResponseAddAttribute(self):
-        r = Response(foo="bar")
-        assert_equal(self.strip(r), '<?xml version="1.0" encoding="UTF-8"?><Response foo="bar" />')
-
-
-class TestSay(TwilioTest):
-
-    def testEmptySay(self):
-        """ should be a say with no text """
-        r = Response()
-        r.append(twiml.Say(""))
-        assert_equal(self.strip(r), '<?xml version="1.0" encoding="UTF-8"?><Response><Say /></Response>')
-
-    def testSayHelloWorld(self):
-        """ should say hello monkey """
-        r = Response()
-        r.append(twiml.Say("Hello World"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Say>Hello World</Say></Response>')
-
-    def testSayFrench(self):
-        """ should say hello monkey """
-        r = Response()
-        r.append(twiml.Say(u("n\xe9cessaire et d'autres")))  # it works on python 2.6 with the from __future__ import unicode_literal
-        assert_equal(text_type(r),
-                     '<?xml version="1.0" encoding="UTF-8"?><Response><Say>n&#233;cessaire et d\'autres</Say></Response>')
-
-    def testSayLoop(self):
-        """ should say hello monkey and loop 3 times """
-        r = Response()
-        r.append(twiml.Say("Hello Monkey", loop=3))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Say loop="3">Hello Monkey</Say></Response>')
-
-    def testSayLoopGreatBritian(self):
-        """ should say have a woman say hello monkey and loop 3 times """
-        r = Response()
-        r.append(twiml.Say("Hello Monkey", language="en-gb"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Say language="en-gb">Hello Monkey</Say></Response>')
-
-    def testSayLoopWoman(self):
-        """ should say have a woman say hello monkey and loop 3 times """
-        r = Response()
-        r.append(twiml.Say("Hello Monkey", loop=3, voice=twiml.Say.WOMAN))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Say loop="3" voice="woman">Hello Monkey</Say></Response>')
-
-    def testSayConvienceMethod(self):
-        """ convenience method: should say have a woman say hello monkey and loop 3 times and be in french """
-        r = Response()
-        r.addSay("Hello Monkey", loop=3, voice=twiml.Say.MAN, language=twiml.Say.FRENCH)
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Say language="fr" loop="3" voice="man">Hello Monkey</Say></Response>')
-
-    def testSayAddAttribute(self):
-        """ add attribute """
-        r = twiml.Say("", foo="bar")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Say foo="bar" />')
-
-    def testSayBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Say(""))
-
-
-class TestPlay(TwilioTest):
-
-    def testEmptyPlay(self):
-        """ should play hello monkey """
-        r = Response()
-        r.append(twiml.Play(""))
-        r = self.strip(r)
-        self.assertEqual(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Play /></Response>')
-
-    def testPlayHello(self):
-        """ should play hello monkey """
-        r = Response()
-        r.append(twiml.Play("http://hellomonkey.mp3"))
-        r = self.strip(r)
-        self.assertEqual(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Play>http://hellomonkey.mp3</Play></Response>')
-
-    def testPlayHelloLoop(self):
-        """ should play hello monkey loop """
-        r = Response()
-        r.append(twiml.Play("http://hellomonkey.mp3", loop=3))
-        r = self.strip(r)
-        self.assertEqual(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Play loop="3">http://hellomonkey.mp3</Play></Response>')
-
-    def testPlayConvienceMethod(self):
-        """ convenience method: should play hello monkey """
-        r = Response()
-        r.addPlay("http://hellomonkey.mp3", loop=3)
-        r = self.strip(r)
-        self.assertEqual(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Play loop="3">http://hellomonkey.mp3</Play></Response>')
-
-    def testPlayAddAttribute(self):
-        """ add attribute """
-        r = twiml.Play("", foo="bar")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Play foo="bar" />')
-
-    def testPlayBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Play(""))
-
-    def testPlayDigits(self):
-        """ should play digits """
-        r = Response()
-        r.append(twiml.Play(digits='w123'))
-        r = self.strip(r)
-        self.assertEqual(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Play digits="w123" /></Response>')
-
-    def testUrlOrDigitsRequired(self):
-        self.assertRaises(TwimlException, twiml.Play)
-
-
-class TestRecord(TwilioTest):
-
-    def testRecordEmpty(self):
-        """ should record """
-        r = Response()
-        r.append(twiml.Record())
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Record /></Response>')
-
-    def testRecordActionMethod(self):
-        """ should record with an action and a get method """
-        r = Response()
-        r.append(twiml.Record(action="example.com", method="GET"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Record action="example.com" method="GET" /></Response>')
-
-    def testRecordMaxlengthFinishTimeout(self):
-        """ should record with an maxlength, finishonkey, and timeout """
-        r = Response()
-        r.append(twiml.Record(timeout=4, finishOnKey="#", maxLength=30))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Record finishOnKey="#" maxLength="30" timeout="4" /></Response>')
-
-    def testRecordTranscribeCallback(self):
-        """ should record with a transcribe and transcribeCallback """
-        r = Response()
-        r.append(twiml.Record(transcribeCallback="example.com"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Record transcribeCallback="example.com" /></Response>')
-
-    def testRecordAddAttribute(self):
-        """ add attribute """
-        r = twiml.Record(foo="bar")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Record foo="bar" />')
-
-    def testRecordBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Record())
-
-
-class TestRedirect(TwilioTest):
-
-    def testRedirectEmpty(self):
-        r = Response()
-        r.append(twiml.Redirect())
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect /></Response>')
-
-    def testRedirectMethod(self):
-        r = Response()
-        r.append(twiml.Redirect(url="example.com", method="POST"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect method="POST">example.com</Redirect></Response>')
-
-    def testRedirectMethodGetParams(self):
-        r = Response()
-        r.append(twiml.Redirect(url="example.com?id=34&action=hey", method="POST"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Redirect method="POST">example.com?id=34&amp;action=hey</Redirect></Response>')
-
-    def testAddAttribute(self):
-        """ add attribute """
-        r = twiml.Redirect("", foo="bar")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Redirect foo="bar" />')
-
-    def testBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Redirect())
-
-
-class TestHangup(TwilioTest):
-
-    def testHangup(self):
-        """ convenience: should Hangup to a url via POST """
-        r = Response()
-        r.append(twiml.Hangup())
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup /></Response>')
-
-    def testHangupConvience(self):
-        """ should raises exceptions for wrong appending """
-        r = Response()
-        r.addHangup()
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup /></Response>')
-
-    def testBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        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)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Leave /></Response>')
-
-    def testBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Leave())
-
-
-class TestReject(TwilioTest):
-
-    def testReject(self):
-        """ should be a Reject with default reason """
-        r = Response()
-        r.append(twiml.Reject())
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Reject /></Response>')
-
-    def testRejectConvenience(self):
-        """ should be a Reject with reason Busy """
-        r = Response()
-        r.addReject(reason='busy')
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Reject reason="busy" /></Response>')
-
-    def testBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Reject())
-
-
-class TestSms(TwilioTest):
-
-    def testEmpty(self):
-        """ Test empty sms verb """
-        r = Response()
-        r.append(twiml.Sms(""))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Sms /></Response>')
-
-    def testBody(self):
-        """ Test hello world """
-        r = Response()
-        r.append(twiml.Sms("Hello, World"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Sms>Hello, World</Sms></Response>')
-
-    def testToFromAction(self):
-        """ Test the to, from, and status callback """
-        r = Response()
-        r.append(twiml.Sms("Hello, World", to=1231231234, sender=3453453456,
-            statusCallback="example.com?id=34&action=hey"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Sms from="3453453456" statusCallback="example.com?id=34&amp;action=hey" to="1231231234">Hello, World</Sms></Response>')
-
-    def testActionMethod(self):
-        """ Test the action and method parameters on Sms """
-        r = Response()
-        r.append(twiml.Sms("Hello", method="POST", action="example.com?id=34&action=hey"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Sms action="example.com?id=34&amp;action=hey" method="POST">Hello</Sms></Response>')
-
-    def testConvience(self):
-        """ should raises exceptions for wrong appending """
-        r = Response()
-        r.addSms("Hello")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Sms>Hello</Sms></Response>')
-
-    def testBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Sms("Hello"))
-
-
-class TestConference(TwilioTest):
-
-    def setUp(self):
-        r = Response()
-        with r.dial() as dial:
-            dial.conference("TestConferenceAttributes", beep=False, waitUrl="",
-                startConferenceOnEnter=True, endConferenceOnExit=True)
-        xml = r.toxml()
-
-        # parse twiml XML string with Element Tree and inspect structure
-        tree = ET.fromstring(xml)
-        self.conf = tree.find(".//Conference")
-
-    def test_conf_text(self):
-        self.assertEqual(self.conf.text.strip(), "TestConferenceAttributes")
-
-    def test_conf_beep(self):
-        self.assertEqual(self.conf.get('beep'), "false")
-
-    def test_conf_waiturl(self):
-        self.assertEqual(self.conf.get('waitUrl'), "")
-
-    def test_conf_start_conference(self):
-        self.assertEqual(self.conf.get('startConferenceOnEnter'), "true")
-
-    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(".//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',
-                  waitUrl='wait', waitUrlMethod='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('waitUrl'), "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('waitUrlMethod'), "POST")
-
-
-class TestDial(TwilioTest):
-
-    def testDial(self):
-        """ should redirect the call """
-        r = Response()
-        r.append(twiml.Dial("1231231234"))
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial>1231231234</Dial></Response>')
-
-    def testSip(self):
-        """ should redirect the call """
-        r = Response()
-        d = r.dial()
-        d.sip('foo@example.com')
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip>foo@example.com</Sip></Dial></Response>')
-
-    def testSipUsernamePass(self):
-        """ should redirect the call """
-        r = Response()
-        d = r.dial()
-        d.sip('foo@example.com', username='foo', password='bar')
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip password="bar" username="foo">foo@example.com</Sip></Dial></Response>')
-
-    def testSipUri(self):
-        """ should redirect the call """
-        r = Response()
-        d = r.dial()
-        s = d.sip()
-        s.uri('foo@example.com')
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip><Uri>foo@example.com</Uri></Sip></Dial></Response>')
-
-    def testConvienceMethod(self):
-        """ should dial to a url via post """
-        r = Response()
-        r.addDial()
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial /></Response>')
-
-    def testAddNumber(self):
-        """ add a number to a dial """
-        r = Response()
-        d = twiml.Dial()
-        d.append(twiml.Number("1231231234"))
-        r.append(d)
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Number>1231231234</Number></Dial></Response>')
-
-    def testAddNumberStatusCallbackEvent(self):
-        """ add a number to a dial with status callback events"""
-        r = Response()
-        d = twiml.Dial()
-        d.append(twiml.Number("1231231234", statusCallback="http://example.com", statusCallbackEvent="initiated completed"))
-        r.append(d)
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Number statusCallback="http://example.com" statusCallbackEvent="initiated completed">1231231234</Number></Dial></Response>')
-
-    def testAddNumberConvenience(self):
-        """ add a number to a dial, convience method """
-        r = Response()
-        d = r.addDial()
-        d.addNumber("1231231234")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Number>1231231234</Number></Dial></Response>')
-
-    def testAddNumberConvenienceStatusCallbackEvent(self):
-        """ add a number to a dial, convience method """
-        r = Response()
-        d = r.addDial()
-        d.addNumber("1231231234", statusCallback="http://example.com", statusCallbackEvent="initiated completed")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Number statusCallback="http://example.com" statusCallbackEvent="initiated completed">1231231234</Number></Dial></Response>')
-
-    def testAddConference(self):
-        """ add a conference to a dial """
-        r = Response()
-        d = twiml.Dial()
-        d.append(twiml.Conference("My Room"))
-        r.append(d)
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Conference>My Room</Conference></Dial></Response>')
-
-    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)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Queue>The Cute Queue</Queue></Dial></Response>')
-
-    def test_add_empty_client(self):
-        """ add an empty client to a dial """
-        r = Response()
-        d = r.dial()
-        d.client("")
-        assert_equal(str(r), '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Client /></Dial></Response>')
-
-    def test_add_client(self):
-        """ add a client to a dial """
-        r = Response()
-        d = r.dial()
-        d.client("alice")
-        assert_equal(str(r), '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Client>alice</Client></Dial></Response>')
-
-    def testAddConferenceConvenceMethod(self):
-        """ add a conference to a dial, conviently """
-        r = Response()
-        d = r.addDial()
-        d.addConference("My Room")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Conference>My Room</Conference></Dial></Response>')
-
-    def testAddAttribute(self):
-        """ add attribute """
-        r = twiml.Conference("MyRoom", foo="bar")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Conference foo="bar">MyRoom</Conference>')
-
-    def testBadAppend(self):
-        """ should raise exceptions for wrong appending """
-        self.improperAppend(twiml.Conference("Hello"))
-
-
-class TestGather(TwilioTest):
-
-    def testEmpty(self):
-        """ a gather with nothing inside """
-        r = Response()
-        r.append(twiml.Gather())
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Gather /></Response>')
-
-    def test_context_manager(self):
-        with Response() as r:
-            with r.gather() as g:
-                g.say("Hello")
-
-            assert_equal(str(r), '<?xml version="1.0" encoding="UTF-8"?><Response><Gather><Say>Hello</Say></Gather></Response>')
-
-    def testNestedSayPlayPause(self):
-        """ a gather with a say, play, and pause """
-        r = Response()
-        g = twiml.Gather()
-        g.append(twiml.Say("Hey"))
-        g.append(twiml.Play("hey.mp3"))
-        g.append(twiml.Pause())
-        r.append(g)
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Gather><Say>Hey</Say><Play>hey.mp3</Play><Pause /></Gather></Response>')
-
-    def testNestedSayPlayPauseConvience(self):
-        """ a gather with a say, play, and pause """
-        r = Response()
-        g = r.addGather()
-        g.addSay("Hey")
-        g.addPlay("hey.mp3")
-        g.addPause()
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Response><Gather><Say>Hey</Say><Play>hey.mp3</Play><Pause /></Gather></Response>')
-
-    def testAddAttribute(self):
-        """ add attribute """
-        r = twiml.Gather(foo="bar")
-        r = self.strip(r)
-        assert_equal(r, '<?xml version="1.0" encoding="UTF-8"?><Gather foo="bar" />')
-
-    def testNoDeclaration(self):
-        """ add attribute """
-        r = twiml.Gather(foo="bar")
-        assert_equal(r.toxml(xml_declaration=False), '<Gather foo="bar" />')
-
-    def testImproperNesting(self):
-        """ bad nesting """
-        verb = twiml.Gather()
-        self.assertRaises(TwimlException, verb.append, twiml.Gather())
-        self.assertRaises(TwimlException, verb.append, twiml.Record())
-        self.assertRaises(TwimlException, verb.append, twiml.Hangup())
-        self.assertRaises(TwimlException, verb.append, twiml.Redirect())
-        self.assertRaises(TwimlException, verb.append, twiml.Dial())
-        self.assertRaises(TwimlException, verb.append, twiml.Conference(""))
-        self.assertRaises(TwimlException, verb.append, twiml.Sms(""))
diff --git a/twilio/twiml.py b/twilio/twiml.py
deleted file mode 100644
index 61c3894a4d..0000000000
--- a/twilio/twiml.py
+++ /dev/null
@@ -1,574 +0,0 @@
-"""
-Make sure to check out the TwiML overview and tutorial at
-https://www.twilio.com/docs/api/twiml
-"""
-import xml.etree.ElementTree as ET
-
-from twilio.exceptions import TwimlException
-
-
-class Verb(object):
-    """Twilio basic verb object.
-    """
-    GET = "GET"
-    POST = "POST"
-    nestables = None
-
-    def __init__(self, **kwargs):
-        self.name = self.__class__.__name__
-        self.body = None
-        self.verbs = []
-        self.attrs = {}
-
-        if kwargs.get("waitMethod", "GET") not in ["GET", "POST"]:
-            raise TwimlException("Invalid waitMethod parameter, "
-                                 "must be 'GET' or 'POST'")
-
-        if kwargs.get("method", "GET") not in ["GET", "POST"]:
-            raise TwimlException("Invalid method parameter, "
-                                 "must be 'GET' or 'POST'")
-
-        for k, v in kwargs.items():
-            if k == "sender":
-                k = "from"
-            if v is not None:
-                self.attrs[k] = v
-
-    def __str__(self):
-        return self.toxml()
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        return False
-
-    def toxml(self, xml_declaration=True):
-        """
-        Return the contents of this verb as an XML string
-
-        :param bool xml_declaration: Include the XML declaration. Defaults to
-                                     True
-        """
-        xml = ET.tostring(self.xml()).decode('utf-8')
-
-        if xml_declaration:
-            return '<?xml version="1.0" encoding="UTF-8"?>' + xml
-        else:
-            return xml
-
-    def xml(self):
-        el = ET.Element(self.name)
-
-        keys = self.attrs.keys()
-        keys = sorted(keys)
-        for a in keys:
-            value = self.attrs[a]
-
-            if isinstance(value, bool):
-                el.set(a, str(value).lower())
-            else:
-                el.set(a, str(value))
-
-        if self.body:
-            el.text = self.body
-
-        for verb in self.verbs:
-            el.append(verb.xml())
-
-        return el
-
-    def append(self, verb):
-        if not self.nestables or verb.name not in self.nestables:
-            raise TwimlException("%s is not nestable inside %s" %
-                                 (verb.name, self.name))
-        self.verbs.append(verb)
-        return verb
-
-
-class Response(Verb):
-    """Twilio response object."""
-    nestables = [
-        'Say',
-        'Play',
-        'Gather',
-        'Record',
-        'Dial',
-        'Redirect',
-        'Pause',
-        'Hangup',
-        'Reject',
-        'Sms',
-        'Enqueue',
-        'Leave',
-        'Message',
-    ]
-
-    def __init__(self, **kwargs):
-        """Version: Twilio API version e.g. 2008-08-01 """
-        super(Response, self).__init__(**kwargs)
-
-    def say(self, text, **kwargs):
-        """Return a newly created :class:`Say` verb, nested inside this
-        :class:`Response` """
-        return self.append(Say(text, **kwargs))
-
-    def play(self, url=None, digits=None, **kwargs):
-        """Return a newly created :class:`Play` verb, nested inside this
-        :class:`Response` """
-        return self.append(Play(url=url, digits=digits, **kwargs))
-
-    def pause(self, **kwargs):
-        """Return a newly created :class:`Pause` verb, nested inside this
-        :class:`Response` """
-        return self.append(Pause(**kwargs))
-
-    def redirect(self, url=None, **kwargs):
-        """Return a newly created :class:`Redirect` verb, nested inside this
-        :class:`Response` """
-        return self.append(Redirect(url, **kwargs))
-
-    def hangup(self, **kwargs):
-        """Return a newly created :class:`Hangup` verb, nested inside this
-        :class:`Response` """
-        return self.append(Hangup(**kwargs))
-
-    def reject(self, reason=None, **kwargs):
-        """Return a newly created :class:`Hangup` verb, nested inside this
-        :class:`Response` """
-        return self.append(Reject(reason=reason, **kwargs))
-
-    def gather(self, **kwargs):
-        """Return a newly created :class:`Gather` verb, nested inside this
-        :class:`Response` """
-        return self.append(Gather(**kwargs))
-
-    def dial(self, number=None, **kwargs):
-        """Return a newly created :class:`Dial` verb, nested inside this
-        :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` """
-        return self.append(Record(**kwargs))
-
-    def sms(self, msg, **kwargs):
-        """Return a newly created :class:`Sms` verb, nested inside this
-        :class:`Response` """
-        return self.append(Sms(msg, **kwargs))
-
-    def message(self, msg=None, **kwargs):
-        """Return a newly created :class:`Message` verb, nested inside this
-        :class:`Response`"""
-        return self.append(Message(msg, **kwargs))
-
-    # All add* methods are deprecated
-    def addSay(self, *args, **kwargs):
-        return self.say(*args, **kwargs)
-
-    def addPlay(self, *args, **kwargs):
-        return self.play(*args, **kwargs)
-
-    def addPause(self, *args, **kwargs):
-        return self.pause(*args, **kwargs)
-
-    def addRedirect(self, *args, **kwargs):
-        return self.redirect(*args, **kwargs)
-
-    def addHangup(self, *args, **kwargs):
-        return self.hangup(*args, **kwargs)
-
-    def addReject(self, *args, **kwargs):
-        return self.reject(*args, **kwargs)
-
-    def addGather(self, *args, **kwargs):
-        return self.gather(*args, **kwargs)
-
-    def addDial(self, *args, **kwargs):
-        return self.dial(*args, **kwargs)
-
-    def addRecord(self, *args, **kwargs):
-        return self.record(*args, **kwargs)
-
-    def addSms(self, *args, **kwargs):
-        return self.sms(*args, **kwargs)
-
-
-class Say(Verb):
-    """The :class:`Say` verb converts text to speech that is read back to the
-    caller.
-
-    :param voice: allows you to choose a male or female voice to read text
-                  back.
-
-    :param language: allows you pick a voice with a specific language's accent
-                     and pronunciations. Twilio currently supports languages
-                     'en' (English), 'es' (Spanish), 'fr' (French), and 'de'
-                     (German), 'en-gb' (English Great Britain").
-
-    :param loop: specifies how many times you'd like the text repeated.
-                 Specifying '0' will cause the the :class:`Say` verb to loop
-                 until the call is hung up. Defaults to 1.
-    """
-    MAN = 'man'
-    WOMAN = 'woman'
-
-    ENGLISH = 'en'
-    BRITISH = 'en-gb'
-    SPANISH = 'es'
-    FRENCH = 'fr'
-    GERMAN = 'de'
-
-    def __init__(self, text, **kwargs):
-        super(Say, self).__init__(**kwargs)
-        self.body = text
-
-
-class Play(Verb):
-    """Play DTMF digits or audio from a URL.
-
-    :param str url: point to an audio file. The MIME type on the file must be
-                    set correctly. At least one of `url` and `digits` must be
-                    specified. If both are given, the digits will play prior
-                    to the audio from the URL.
-
-    :param str digits: a string of digits to play. To pause before playing
-                   digits, use leading 'w' characters. Each 'w' will cause
-                   Twilio to wait 0.5 seconds instead of playing a digit.
-                   At least one of `url` and `digits` must be specified.
-                   If both are given, the digits will play first.
-
-    :param int loop: specifies how many times you'd like the text repeated.
-                 Specifying '0' will cause the the :class:`Play` verb to loop
-                 until the call is hung up. Defaults to 1.
-    """
-    def __init__(self, url=None, digits=None, **kwargs):
-        if url is None and digits is None:
-            raise TwimlException(
-                "Please specify either a url or digits to play.",
-            )
-
-        if digits is not None:
-            kwargs['digits'] = digits
-        super(Play, self).__init__(**kwargs)
-        if url is not None:
-            self.body = url
-
-
-class Pause(Verb):
-    """Pause the call
-
-    :param length: specifies how many seconds Twilio will wait silently before
-                   continuing on.
-    """
-
-
-class Redirect(Verb):
-    """Redirect call flow to another URL
-
-    :param url: specifies the url which Twilio should query to retrieve new
-                TwiML. The default is the current url
-
-    :param method: specifies the HTTP method to use when retrieving the url
-    """
-    GET = 'GET'
-    POST = 'POST'
-
-    def __init__(self, url="", **kwargs):
-        super(Redirect, self).__init__(**kwargs)
-        self.body = url
-
-
-class Hangup(Verb):
-    """Hangup the call
-    """
-
-
-class Reject(Verb):
-    """Hangup the call
-
-    :param reason: not sure
-    """
-
-
-class Gather(Verb):
-    """Gather digits from the caller's keypad
-
-    :param action: URL to which the digits entered will be sent
-    :param method: submit to 'action' url using GET or POST
-    :param numDigits: how many digits to gather before returning
-    :param timeout: wait for this many seconds before returning
-    :param finishOnKey: key that triggers the end of caller input
-    """
-    GET = 'GET'
-    POST = 'POST'
-    nestables = ['Say', 'Play', 'Pause']
-
-    def __init__(self, **kwargs):
-        super(Gather, self).__init__(**kwargs)
-
-    def say(self, text, **kwargs):
-        return self.append(Say(text, **kwargs))
-
-    def play(self, url, **kwargs):
-        return self.append(Play(url, **kwargs))
-
-    def pause(self, **kwargs):
-        return self.append(Pause(**kwargs))
-
-    def addSay(self, *args, **kwargs):
-        return self.say(*args, **kwargs)
-
-    def addPlay(self, *args, **kwargs):
-        return self.play(*args, **kwargs)
-
-    def addPause(self, *args, **kwargs):
-        return self.pause(*args, **kwargs)
-
-
-class Number(Verb):
-    """Specify phone number in a nested Dial element.
-
-    :param number: phone number to dial
-    :param sendDigits: key to press after connecting to the number
-    """
-    def __init__(self, number, **kwargs):
-        super(Number, self).__init__(**kwargs)
-        self.body = number
-
-
-class Client(Verb):
-    """Specify a client name to call in a nested Dial element.
-
-    :param name: Client name to connect to
-    """
-    def __init__(self, name, **kwargs):
-        super(Client, self).__init__(**kwargs)
-        self.body = name
-
-
-class Sms(Verb):
-    """ Send a Sms Message to a phone number
-
-    :param to: whom to send message to
-    :param sender: whom to send message from.
-    :param action: url to request after the message is queued
-    :param method: submit to 'action' url using GET or POST
-    :param statusCallback: url to hit when the message is actually sent
-    """
-    GET = 'GET'
-    POST = 'POST'
-
-    def __init__(self, msg, **kwargs):
-        super(Sms, self).__init__(**kwargs)
-        self.body = msg
-
-
-class Message(Verb):
-    """ Send an MMS Message to a phone number.
-
-    :param to: whom to send message to
-    :param sender: whom to send message from.
-    :param action: url to request after the message is queued
-    :param method: submit to 'action' url using GET or POST
-    :param statusCallback: url to hit when the message is actually sent
-    """
-
-    GET = 'GET'
-    POST = 'POST'
-
-    nestables = ['Media', 'Body']
-
-    def __init__(self, msg=None, **kwargs):
-        super(Message, self).__init__(**kwargs)
-        if msg is not None:
-            self.append(Body(msg))
-
-    def media(self, media_url, **kwargs):
-        return self.append(Media(media_url, **kwargs))
-
-
-class Body(Verb):
-    """ Specify a text body for a Message.
-
-    :param msg: the text to use in the body.
-    """
-
-    GET = 'GET'
-    POST = 'POST'
-
-    def __init__(self, msg, **kwargs):
-        super(Body, self).__init__(**kwargs)
-        self.body = msg
-
-
-class Media(Verb):
-    """Specify media to include in a Message.
-
-    :param url: The URL of the media to include.
-    """
-
-    GET = 'GET'
-    POST = 'POST'
-
-    def __init__(self, url, **kwargs):
-        super(Media, self).__init__(**kwargs)
-        self.body = url
-
-
-class Conference(Verb):
-    """Specify conference in a nested Dial element.
-
-    :param name: friendly name of conference
-    :param bool muted: keep this participant muted
-    :param bool beep: play a beep when this participant enters/leaves
-    :param bool startConferenceOnEnter: start conf when this participants joins
-    :param bool endConferenceOnExit: end conf when this participants leaves
-    :param waitUrl: TwiML url that executes before conference starts
-    :param waitMethod: HTTP method for waitUrl GET/POST
-    """
-    GET = 'GET'
-    POST = 'POST'
-
-    def __init__(self, name, **kwargs):
-        super(Conference, self).__init__(**kwargs)
-        self.body = name
-
-
-class Dial(Verb):
-    """Dial another phone number and connect it to this call
-
-    :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
-                         party to answer the call
-    :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
-    :param bool record: Record both legs of a call within this <Dial>
-    """
-    GET = 'GET'
-    POST = 'POST'
-    nestables = ['Number', 'Conference', 'Client', 'Queue', 'Sip']
-
-    def __init__(self, number=None, **kwargs):
-        super(Dial, self).__init__(**kwargs)
-        if number and len(number.split(',')) > 1:
-            for n in number.split(','):
-                self.append(Number(n.strip()))
-        else:
-            self.body = number
-
-    def client(self, name, **kwargs):
-        return self.append(Client(name, **kwargs))
-
-    def number(self, number, **kwargs):
-        return self.append(Number(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 sip(self, sip_address=None, **kwargs):
-        return self.append(Sip(sip_address, **kwargs))
-
-    def addNumber(self, *args, **kwargs):
-        return self.number(*args, **kwargs)
-
-    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 <Dial> verb,
-                   this url is executed after the bridged parties disconnect
-    :param method: HTTP method for action GET/POST
-    :param waitUrl: url to a twiml document that executes
-                     while the call is on the queue
-    :param waitUrlMethod: HTTP method for waitUrl 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
-
-    :param action: submit the result of the dial to this URL
-    :param method: submit to 'action' url using GET or POST
-    :param maxLength: maximum number of seconds to record
-    :param timeout: seconds of silence before considering the recording done
-    """
-    GET = 'GET'
-    POST = 'POST'
-
-
-class Sip(Verb):
-    """Dial out to a SIP endpoint
-
-    :param url: call screening URL none
-    :param method: call screening method POST
-    :param username: Username for SIP authentication
-    :param password: Password for SIP authentication
-    """
-    nestables = ['Headers', 'Uri']
-
-    def __init__(self, sip_address=None, **kwargs):
-        super(Sip, self).__init__(**kwargs)
-        if sip_address:
-            self.body = sip_address
-
-    def uri(self, uri, **kwargs):
-        return self.append(Uri(uri, **kwargs))
-
-
-class Uri(Verb):
-    """A uniform resource indentifier"""
-    def __init__(self, uri, **kwargs):
-        super(Uri, self).__init__(**kwargs)
-        self.body = uri

From b84ed80355e48bb348db53ebdd1e38c4f06f1873 Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Thu, 2 Mar 2017 14:57:22 -0800
Subject: [PATCH 4/7] Add docs

---
 twilio/twiml/messaging_response.py |  68 +++++-
 twilio/twiml/voice_response.py     | 344 ++++++++++++++++++++++++++++-
 2 files changed, 409 insertions(+), 3 deletions(-)

diff --git a/twilio/twiml/messaging_response.py b/twilio/twiml/messaging_response.py
index 2a99296713..388476f130 100644
--- a/twilio/twiml/messaging_response.py
+++ b/twilio/twiml/messaging_response.py
@@ -2,8 +2,13 @@
 
 
 class MessagingResponse(TwiML):
-
+    """
+    Messaging TwiML Response
+    """
     def __init__(self):
+        """
+        Create a new <Response>
+        """
         super(MessagingResponse, self).__init__()
         self.name = 'Response'
 
@@ -15,6 +20,18 @@ def message(self,
                 action=None,
                 status_callback=None,
                 **kwargs):
+        """
+        Add a <Message> element
+
+        :param body: body of the message
+        :param to: number to send to
+        :param from_: number to send from
+        :param method: action HTTP method
+        :param action: action URL
+        :param status_callback: callback URL
+        :param kwargs: other attributes
+        :return: <Message> element
+        """
         return self.append(Message(
             body=body,
             to=to,
@@ -26,6 +43,14 @@ def message(self,
         ))
 
     def redirect(self, method=None, url=None, **kwargs):
+        """
+        Add a <Redirect> element
+
+        :param method: HTTP method
+        :param url: URL to redirect to
+        :param kwargs: other attributes
+        :return: <Redirect> element
+        """
         return self.append(Redirect(
             method=method,
             url=url,
@@ -34,31 +59,70 @@ def redirect(self, method=None, url=None, **kwargs):
 
 
 class Message(TwiML):
-
+    """
+    <Message> element
+    """
     def __init__(self, body=None, **kwargs):
+        """
+        Create a new <Message> element
+
+        :param body: body of message
+        :param kwargs: other attributes
+        """
         super(Message, self).__init__(**kwargs)
         if body:
             self.body = body
 
     def body(self, body):
+        """
+        Add a <Body> element
+
+        :param body: body of message
+        :return: <Body> element
+        """
         return self.append(Body(body))
 
     def media(self, url):
+        """
+        Add a <Media> element
+
+        :param url: media URL
+        :return: <Media> element
+        """
         return self.append(Media(url))
 
 
 class Body(TwiML):
+    """
+    <Body> element
+    """
     def __init__(self, body):
+        """
+        Create a new <Body> element
+
+        :param body: message body
+        """
         super(Body, self).__init__()
         self.body = body
 
 
 class Media(TwiML):
+    """
+    <Media> element
+    """
     def __init__(self, url):
+        """
+        Create a new <Media> element
+
+        :param url: media URL location
+        """
         super(Media, self).__init__()
         self.body = url
 
 
 class Redirect(TwiML):
+    """
+    <Redirect> element
+    """
     pass
 
diff --git a/twilio/twiml/voice_response.py b/twilio/twiml/voice_response.py
index 6c82dcf0a3..bc2c62ccdd 100644
--- a/twilio/twiml/voice_response.py
+++ b/twilio/twiml/voice_response.py
@@ -2,8 +2,13 @@
 
 
 class VoiceResponse(TwiML):
-
+    """
+    Voice TwiML Response
+    """
     def __init__(self):
+        """
+        Create a new <Response>
+        """
         super(VoiceResponse, self).__init__()
         self.name = 'Response'
 
@@ -20,6 +25,23 @@ def dial(self,
              recording_status_callback=None,
              recording_status_callback_method=None,
              **kwargs):
+        """
+        Create a <Dial> element
+
+        :param number: phone number to dial
+        :param action: action URL
+        :param method: action HTTP method
+        :param timeout: time to wait for answer
+        :param hangup_on_star: hangup call on * press
+        :param time_limit: max time length
+        :param caller_id: caller ID to display
+        :param record: record the call
+        :param trim: trim the recording
+        :param recording_status_callback: status callback URL
+        :param recording_status_callback_method: status callback URL method
+        :param kwargs: additional attributes
+        :return: <Dial> element
+        """
         return self.append(Dial(
             number=number,
             action=action,
@@ -43,6 +65,18 @@ def enqueue(self,
                 wait_url_method=None,
                 workflow_sid=None,
                 **kwargs):
+        """
+        Add a new <Enqueue> element
+
+        :param name: friendly name
+        :param action: action URL
+        :param method: action URL method
+        :param wait_url: wait URL
+        :param wait_url_method: wait URL method
+        :param workflow_sid: TaskRouter workflow SID
+        :param kwargs: additional attributes
+        :return: <Enqueue> element
+        """
         return self.append(Enqueue(
             name,
             action=action,
@@ -60,6 +94,17 @@ def gather(self,
                finish_on_key=None,
                num_digits=None,
                **kwargs):
+        """
+        Add a new <Gather> element
+
+        :param action: action URL
+        :param method: action URL method
+        :param timeout: time to wait while gathering input
+        :param finish_on_key: finish on key press
+        :param num_digits: digits to collect
+        :param kwargs: additional attributes
+        :return: <Gather> element
+        """
         return self.append(Gather(
             action=action,
             method=method,
@@ -69,12 +114,28 @@ def gather(self,
         ))
 
     def hangup(self):
+        """
+        Add a new <Hangup> element
+
+        :return: <Hangup> element
+        """
         return self.append(Hangup())
 
     def leave(self):
+        """
+        Add a new <Leave> element
+
+        :return: <Leave> element
+        """
         return self.append(Leave())
 
     def pause(self, length=None):
+        """
+        Add a new <Pause> element
+
+        :param length: time in seconds to pause
+        :return: <Pause> element
+        """
         return self.append(Pause(length=length))
 
     def play(self,
@@ -82,6 +143,15 @@ def play(self,
              loop=None,
              digits=None,
              **kwargs):
+        """
+        Add a new <Play> element
+
+        :param url: url to play
+        :param loop: times to loop
+        :param digits: play DTMF tones during a call
+        :param kwargs: additional attributes
+        :return: <Play> element
+        """
         return self.append(Play(
             url,
             loop=loop,
@@ -102,6 +172,23 @@ def record(self,
                transcribe=None,
                transcribe_callback=None,
                **kwargs):
+        """
+        Add a new <Record> element
+
+        :param action: action URL
+        :param method: action URL method
+        :param timeout: timeout for recording
+        :param finish_on_key: finish recording on key
+        :param max_length: max length to record
+        :param play_beep: play beep
+        :param trim: trim the recording
+        :param recording_status_callback: status callback for the recordings
+        :param recording_status_callback_method: status callback method
+        :param transcribe: transcribe the recording
+        :param transcribe_callback: transcribe callback URL
+        :param kwargs: additional attributes
+        :return: <Record> element
+        """
         return self.append(Record(
             action=action,
             method=method,
@@ -118,9 +205,24 @@ def record(self,
         ))
 
     def redirect(self, url, method=None, **kwargs):
+        """
+        Add a <Redirect> element
+
+        :param url: redirect url
+        :param method: redirect method
+        :param kwargs: additional attributes
+        :return: <Redirect> element
+        """
         return self.append(Redirect(url, method=method, **kwargs))
 
     def reject(self, reason=None, **kwargs):
+        """
+        Add a <Reject> element
+
+        :param reason: rejection reason
+        :param kwargs: additional attributes
+        :return: <Reject> element
+        """
         return self.append(Reject(reason=reason, **kwargs))
 
     def say(self,
@@ -129,6 +231,16 @@ def say(self,
             language=None,
             voice=None,
             **kwargs):
+        """
+        Add a <Say> element
+
+        :param body: message body
+        :param loop: times to loop
+        :param language: language of message
+        :param voice: voice to use
+        :param kwargs: additional attributes
+        :return: <Say> element
+        """
         return self.append(Say(
             body,
             loop=loop,
@@ -145,6 +257,18 @@ def sms(self,
             action=None,
             status_callback=None,
             **kwargs):
+        """
+        Add a <Sms> element
+
+        :param body: body of message
+        :param to: to phone number
+        :param from_: from phone number
+        :param method: action URL method
+        :param action: action URL
+        :param status_callback: status callback URL
+        :param kwargs: additional attributes
+        :return: <Sms> element
+        """
         return self.append(Sms(
             body,
             to=to,
@@ -157,7 +281,16 @@ def sms(self,
 
 
 class Dial(TwiML):
+    """
+    <Dial> element
+    """
     def __init__(self, number=None, **kwargs):
+        """
+        Create a new <Dial> element
+
+        :param number: phone number to dial
+        :param kwargs: additional attributes
+        """
         super(Dial, self).__init__(**kwargs)
         if number:
             self.body = number
@@ -170,6 +303,18 @@ def client(self,
                status_callback_method=None,
                status_callback=None,
                **kwargs):
+        """
+        Add a new <Client> element
+
+        :param name: name of client
+        :param method: action URL method
+        :param url: action URL
+        :param status_callback_event: events to call status callback
+        :param status_callback_method: status callback URL method
+        :param status_callback: status callback URL
+        :param kwargs: additional attributes
+        :return: <Client> element
+        """
         return self.append(Client(
             name,
             method=method,
@@ -198,6 +343,28 @@ def conference(self,
                    recording_status_callback=None,
                    recording_status_callback_method=None,
                    **kwargs):
+        """
+        Add a <Conference> element
+
+        :param name: name of conference
+        :param muted: join the conference muted
+        :param start_conference_on_enter: start the conference on enter
+        :param end_conference_on_exit: end the conference on exit
+        :param max_participants: max number of people in conference
+        :param beep: play beep when joining
+        :param record: record the conference
+        :param trim: trim the recording
+        :param wait_method: wait URL method
+        :param wait_url: wait URL to play
+        :param event_callback_url: event callback URL
+        :param status_callback_event: events to call status callback
+        :param status_callback: status callback URL
+        :param status_callback_method: status callback URL method
+        :param recording_status_callback: recording status callback URL
+        :param recording_status_callback_method: recording status callback URL method
+        :param kwargs: additional attributes
+        :return: <Conference> element
+        """
         return self.append(Conference(
             name,
             start_conference_on_enter=start_conference_on_enter,
@@ -226,6 +393,19 @@ def number(self,
                status_callback=None,
                status_callback_method=None,
                **kwargs):
+        """
+        Add a <Number> element
+
+        :param number: phone number to dial
+        :param send_digits: play DTMF tones when the call is answered
+        :param url: TwiML URL
+        :param method: TwiML URL method
+        :param status_callback_event: events to call status callback
+        :param status_callback: status callback URL
+        :param status_callback_method: status callback URL method
+        :param kwargs: additional attributes
+        :return: <Number> element
+        """
         return self.append(Number(
             number,
             send_digits=send_digits,
@@ -244,6 +424,17 @@ def queue(self,
               reservation_sid=None,
               post_work_activity_sid=None,
               **kwargs):
+        """
+        Add a <Queue> element
+
+        :param queue_name: queue name
+        :param url: action URL
+        :param method: action URL method
+        :param reservation_sid: TaskRouter reservation SID
+        :param post_work_activity_sid: TaskRouter activity SID
+        :param kwargs: additional attributes
+        :return: <Queue> element
+        """
         return self.append(Queue(
             queue_name,
             url=url,
@@ -263,6 +454,20 @@ def sip(self,
             status_callback=None,
             status_callback_method=None,
             **kwargs):
+        """
+        Add a <Sip> element
+
+        :param uri: sip url
+        :param username: sip username
+        :param password: sip password
+        :param url: action URL
+        :param method: action URL method
+        :param status_callback_event: events to call status callback
+        :param status_callback: status callback URL
+        :param status_callback_method: status callback URL method
+        :param kwargs: additional attributes
+        :return: <Sip> element
+        """
         return self.append(Sip(
             uri,
             username=username,
@@ -277,43 +482,104 @@ def sip(self,
 
 
 class Client(TwiML):
+    """
+    <Client> element
+    """
     def __init__(self, name, **kwargs):
+        """
+        Create a new <Client> element
+
+        :param name: name of client
+        :param kwargs: attributes
+        """
         super(Client, self).__init__(**kwargs)
         self.body = name
 
 
 class Conference(TwiML):
+    """
+    <Conference> element
+    """
     def __init__(self, name, **kwargs):
+        """
+        Create a new <Conference> element
+
+        :param name: name of conference
+        :param kwargs: attributes
+        """
         super(Conference, self).__init__(**kwargs)
         self.body = name
 
 
 class Number(TwiML):
+    """
+    <Number> element
+    """
     def __init__(self, number, **kwargs):
+        """
+        Create a new <Number> element
+
+        :param number: phone number
+        :param kwargs: attributes
+        """
         super(Number, self).__init__(**kwargs)
         self.body = number
 
 
 class Queue(TwiML):
+    """
+    <Queue> element
+    """
     def __init__(self, queue_name, **kwargs):
+        """
+        Create a new <Queue> element
+
+        :param queue_name: name of queue
+        :param kwargs: attributes
+        """
         super(Queue, self).__init__(**kwargs)
         self.body = queue_name
 
 
 class Sip(TwiML):
+    """
+    <Sip> element
+    """
     def __init__(self, uri, **kwargs):
+        """
+        Create a new <Sip> element
+
+        :param uri: sip url
+        :param kwargs: attributes
+        """
         super(Sip, self).__init__(**kwargs)
         self.body = uri
 
 
 class Enqueue(TwiML):
+    """
+    <Enqueue> element
+    """
     def __init__(self, name, **kwargs):
+        """
+        Create a new <Enqueue> element
+
+        :param name: queue name
+        :param kwargs: attributes
+        """
         super(Enqueue, self).__init__(**kwargs)
         self.body = name
 
 
 class Gather(TwiML):
+    """
+    <Gather> element
+    """
     def __init__(self, **kwargs):
+        """
+        Create a new <Gather> element
+        :param kwargs: attributes
+        """
         super(Gather, self).__init__(**kwargs)
 
     def say(self,
@@ -322,6 +588,16 @@ def say(self,
             language=None,
             voice=None,
             **kwargs):
+        """
+        Add a new <Say> element
+
+        :param body: message body
+        :param loop: times to loop
+        :param language: message language
+        :param voice: voice to use
+        :param kwargs: additional attributes
+        :return: <Say> element
+        """
         return self.append(Say(
             body,
             loop=loop,
@@ -335,6 +611,15 @@ def play(self,
              loop=None,
              digits=None,
              **kwargs):
+        """
+        Add a new <Play> element
+
+        :param url: media URL
+        :param loop: times to loop
+        :param digits: digits to simulate
+        :param kwargs: additional attributes
+        :return: <Play> element
+        """
         return self.append(Play(
             url,
             loop=loop,
@@ -343,48 +628,105 @@ def play(self,
         ))
 
     def pause(self, length=None):
+        """
+        Add a new <Pause> element
+
+        :param length: time to pause
+        :return: <Pause> element
+        """
         return self.append(Pause(length=length))
 
 
 class Pause(TwiML):
+    """
+    <Pause> element
+    """
     pass
 
 
 class Play(TwiML):
+    """
+    <Play> element
+    """
     def __init__(self, url, **kwargs):
+        """
+        Create a new <Play> element
+
+        :param url: media URL
+        :param kwargs: additional attributes
+        """
         super(Play, self).__init__(**kwargs)
         self.body = url
 
 
 class Say(TwiML):
+    """
+    <Say> element
+    """
     def __init__(self, body, **kwargs):
+        """
+        Create a new <Say> element
+
+        :param body: message body
+        :param kwargs: attributes
+        """
         super(Say, self).__init__(**kwargs)
         self.body = body
 
 
 class Hangup(TwiML):
+    """
+    <Hangup> element
+    """
     pass
 
 
 class Leave(TwiML):
+    """
+    <Leave> element
+    """
     pass
 
 
 class Record(TwiML):
+    """
+    <Record> element
+    """
     pass
 
 
 class Redirect(TwiML):
+    """
+    <Redirect> element
+    """
     def __init__(self, url, **kwargs):
+        """
+        Create a new <Redirect> element
+
+        :param url: TwiML URL
+        :param kwargs: attributes
+        """
         super(Redirect, self).__init__(**kwargs)
         self.body = url
 
 
 class Reject(TwiML):
+    """
+    <Reject> element
+    """
     pass
 
 
 class Sms(TwiML):
+    """
+    <Sms> element
+    """
     def __init__(self, body, **kwargs):
+        """
+        Create a new <Sms> element
+
+        :param body: message body
+        :param kwargs: attributes
+        """
         super(Sms, self).__init__(**kwargs)
         self.body = body

From c374d0bbaa0e29d740dee965a1061c444935e31b Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Mon, 6 Mar 2017 11:01:23 -0800
Subject: [PATCH 5/7] Return self on TwiML append

---
 tests/unit/twiml/test_messaging_response.py |  8 ++++++++
 tests/unit/twiml/test_voice_response.py     | 12 ++++++++++++
 twilio/twiml/__init__.py                    |  2 +-
 3 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/tests/unit/twiml/test_messaging_response.py b/tests/unit/twiml/test_messaging_response.py
index c0ed683d74..79dea317a2 100644
--- a/tests/unit/twiml/test_messaging_response.py
+++ b/tests/unit/twiml/test_messaging_response.py
@@ -22,6 +22,14 @@ def test_response(self):
             '<?xml version="1.0" encoding="UTF-8"?><Response><Message>Hello</Message><Redirect url="example.com" /></Response>'
         )
 
+    def test_response_chain(self):
+        r = MessagingResponse().message('Hello').redirect(url='example.com')
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Message>Hello</Message><Redirect url="example.com" /></Response>'
+        )
+
 
 class TestMessage(TwilioTest):
 
diff --git a/tests/unit/twiml/test_voice_response.py b/tests/unit/twiml/test_voice_response.py
index 8e5320b528..0ebd27ae25 100644
--- a/tests/unit/twiml/test_voice_response.py
+++ b/tests/unit/twiml/test_voice_response.py
@@ -29,6 +29,18 @@ def test_response(self):
             '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup /><Leave /><Sms from="+10987654321" to="+11234567890">twilio sms</Sms></Response>'
         )
 
+    def test_response_chain(self):
+        r = VoiceResponse().hangup().leave().sms(
+            'twilio sms',
+            to='+11234567890',
+            from_='+10987654321'
+        )
+
+        assert_equal(
+            self.strip(r),
+            '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup /><Leave /><Sms from="+10987654321" to="+11234567890">twilio sms</Sms></Response>'
+        )
+
 
 class TestSay(TwilioTest):
 
diff --git a/twilio/twiml/__init__.py b/twilio/twiml/__init__.py
index e7c5483079..ce4eb60bd4 100644
--- a/twilio/twiml/__init__.py
+++ b/twilio/twiml/__init__.py
@@ -73,4 +73,4 @@ def xml(self):
 
     def append(self, verb):
         self.verbs.append(verb)
-        return verb
+        return self

From 664c1fbc33cafc147ef56227fa054348e3339a98 Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Tue, 7 Mar 2017 10:21:33 -0800
Subject: [PATCH 6/7] Throw exception on appending non-TwiML

---
 tests/unit/twiml/__init__.py |  8 ++++++++
 twilio/twiml/__init__.py     | 24 ++++++++++++++++++++----
 2 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/tests/unit/twiml/__init__.py b/tests/unit/twiml/__init__.py
index 069ececb94..65873e0927 100644
--- a/tests/unit/twiml/__init__.py
+++ b/tests/unit/twiml/__init__.py
@@ -1,8 +1,16 @@
 import unittest
 
+from nose.tools import raises
 from six import text_type
 
+from twilio.twiml import TwiMLException, TwiML
+
 
 class TwilioTest(unittest.TestCase):
     def strip(self, xml):
         return text_type(xml)
+
+    @raises(TwiMLException)
+    def test_append_fail(self):
+        t = TwiML()
+        t.append('foobar')
diff --git a/twilio/twiml/__init__.py b/twilio/twiml/__init__.py
index ce4eb60bd4..4624dc9427 100644
--- a/twilio/twiml/__init__.py
+++ b/twilio/twiml/__init__.py
@@ -9,6 +9,10 @@ def lower_camel(string):
     return result[0].lower() + result[1:]
 
 
+class TwiMLException(Exception):
+    pass
+
+
 class TwiML(object):
     """
     Twilio basic verb object.
@@ -50,7 +54,23 @@ def to_xml(self, xml_declaration=True):
         else:
             return xml
 
+    def append(self, verb):
+        """
+        Add a TwiML doc
+        :param verb: TwiML Document
+        :return:
+        """
+        if not isinstance(verb, TwiML):
+            raise TwiMLException()
+
+        self.verbs.append(verb)
+        return self
+
     def xml(self):
+        """
+        Convert to XML
+        :return: Generated TwiML
+        """
         el = ET.Element(self.name)
 
         keys = self.attrs.keys()
@@ -70,7 +90,3 @@ def xml(self):
             el.append(verb.xml())
 
         return el
-
-    def append(self, verb):
-        self.verbs.append(verb)
-        return self

From ed160fda8af2841b07569862bb5d3c5f79d2e806 Mon Sep 17 00:00:00 2001
From: Jingming Niu <niu@jingming.ca>
Date: Tue, 7 Mar 2017 10:31:46 -0800
Subject: [PATCH 7/7] Add an exception message

---
 twilio/twiml/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/twilio/twiml/__init__.py b/twilio/twiml/__init__.py
index 4624dc9427..68b3892f00 100644
--- a/twilio/twiml/__init__.py
+++ b/twilio/twiml/__init__.py
@@ -61,7 +61,7 @@ def append(self, verb):
         :return:
         """
         if not isinstance(verb, TwiML):
-            raise TwiMLException()
+            raise TwiMLException('Only appending of TwiML is allowed')
 
         self.verbs.append(verb)
         return self