diff --git a/.github/actions/validate/Dockerfile b/.github/actions/validate/Dockerfile
index 6977b083..191a052e 100644
--- a/.github/actions/validate/Dockerfile
+++ b/.github/actions/validate/Dockerfile
@@ -3,6 +3,7 @@ FROM python:3
# Copies your code file from your action repository to the filesystem path `/` of the container
COPY entrypoint.sh /entrypoint.sh
+COPY bxml_tests.py /bxml_tests.py
#Make entrypoint.sh exacutable
RUN chmod +x /entrypoint.sh
diff --git a/.github/actions/validate/bxml_tests.py b/.github/actions/validate/bxml_tests.py
new file mode 100644
index 00000000..5ad6ad4b
--- /dev/null
+++ b/.github/actions/validate/bxml_tests.py
@@ -0,0 +1,366 @@
+"""
+bxml_tests.py
+
+Unit tests for BXML
+
+@copyright Bandwidth INC
+"""
+from bandwidth.voice.bxml.response import Response
+from bandwidth.voice.bxml.verbs import *
+
+import unittest
+
+
+class BxmlTests(unittest.TestCase):
+ """
+ Class for the BXML tests
+ """
+ def test_forward_xml_with_optional_fields(self):
+ response = Response()
+ forward = Forward(
+ to="+10987654321",
+ from_="+11234567890",
+ call_timeout=100,
+ diversion_treatment="propagate",
+ diversion_reason="away"
+ )
+ response.add_verb(forward)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_gather_no_nested(self):
+ response = Response()
+ gather = Gather(
+ gather_url="https://gather.url/nextBXML",
+ gather_method="POST",
+ terminating_digits="#",
+ tag="tag",
+ max_digits=20,
+ inter_digit_timeout=50,
+ username="user",
+ password="password",
+ first_digit_timeout=10,
+ repeat_count=3,
+ gather_fallback_url="https://test.com",
+ gather_fallback_method="GET",
+ fallback_username="fuser",
+ fallback_password="fpass"
+ )
+ response.add_verb(gather)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_gather_with_speak_sentence(self):
+ response = Response()
+ speak_sentence = SpeakSentence(
+ sentence="Phrase.",
+ voice="kate",
+ locale="en_US",
+ gender="female"
+ )
+ gather = Gather(
+ gather_url="https://gather.url/nextBXML",
+ gather_method="POST",
+ terminating_digits="#",
+ tag="tag",
+ max_digits=20,
+ inter_digit_timeout=50,
+ username="user",
+ password="password",
+ first_digit_timeout=10,
+ repeat_count=3,
+ speak_sentence=speak_sentence
+ )
+ expected_bxml = expected_bxml = 'Phrase.'
+ response.add_verb(gather)
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_gather_play_audio(self):
+ response = Response()
+ play_audio_1 = PlayAudio(url="https://audio.url/audio1.wav")
+ gather = Gather(
+ gather_url="https://gather.url/nextBXML",
+ gather_method="POST",
+ terminating_digits="#",
+ tag="tag",
+ max_digits=20,
+ inter_digit_timeout=50,
+ username="user",
+ password="password",
+ first_digit_timeout=10,
+ repeat_count=3,
+ play_audio=play_audio_1
+ )
+ expected_bxml = 'https://audio.url/audio1.wav'
+ response.add_verb(gather)
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_hangup(self):
+ response = Response()
+ hang_up = Hangup()
+ response.add_verb(hang_up)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_pause(self):
+ response = Response()
+ pause = Pause(duration=400)
+ response.add_verb(pause)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_pause_recording(self):
+ response = Response()
+ pause_recording = PauseRecording()
+ response.add_verb(pause_recording)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_play_audio(self):
+ response = Response()
+ play_audio_1 = PlayAudio(
+ url="https://audio.url/audio1.wav",
+ username="user",
+ password="pass"
+ )
+ response.add_verb(play_audio_1)
+ expected_bxml = 'https://audio.url/audio1.wav'
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_record(self):
+ response = Response()
+ record = Record(
+ tag = "tag",
+ username = "user",
+ password = "pass",
+ record_complete_url="https://record.url.server/record",
+ record_complete_method="POST",
+ recording_available_url="https://record.url.server/available",
+ recording_available_method="GET",
+ terminating_digits="#",
+ max_duration=90,
+ file_format="mp3",
+ transcribe=False,
+ transcription_available_url="https://transcribe.url.server/available",
+ transcription_available_method="POST",
+ silence_timeout=90,
+ record_complete_fallback_url="https://test.com",
+ record_complete_fallback_method="GET",
+ fallback_username="fuser",
+ fallback_password="fpass"
+ )
+ response.add_verb(record)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_redirect(self):
+ response = Response()
+ redirect = Redirect(
+ redirect_url="http://flow.url/newFlow",
+ redirect_method="POST",
+ tag="tag",
+ username="user",
+ password="pass",
+ redirect_fallback_url="https://test.com",
+ redirect_fallback_method="GET",
+ fallback_username="fuser",
+ fallback_password="fpass"
+ )
+ response.add_verb(redirect)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_resume_recording(self):
+ response = Response()
+ resume_recording = ResumeRecording()
+ response.add_verb(resume_recording)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_dtmf(self):
+ response = Response()
+ send_dtmf = SendDtmf(
+ dtmf = "1234",
+ tone_duration = 200,
+ tone_interval = 450
+ )
+ expected_bxml = '1234'
+ response.add_verb(send_dtmf)
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_speak_sentence(self):
+ response = Response()
+ speak_sentence = SpeakSentence(
+ sentence="Phrase.",
+ voice="kate",
+ locale="en_US",
+ gender="female"
+ )
+ expected_bxml = 'Phrase.'
+ response.add_verb(speak_sentence)
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_speak_sentence_SSML(self):
+ response = Response()
+ speak_sentence = SpeakSentence(
+ sentence='Hydrogen is the most abundant element in the universe.',
+ voice="kate",
+ locale="en_US",
+ gender="female"
+ )
+ expected_bxml = 'Hydrogen is the most abundant element in the universe.'
+ response.add_verb(speak_sentence)
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_start_recording(self):
+ response = Response()
+ record = StartRecording(
+ tag = "tag",
+ username = "user",
+ password = "pass",
+ recording_available_url="https://record.url.server/available",
+ recording_available_method="GET",
+ file_format="mp3",
+ multi_channel=True,
+ transcribe=False,
+ transcription_available_url="https://transcribe.url.server/available",
+ transcription_available_method="POST"
+ )
+ response.add_verb(record)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_stop_recording(self):
+ response = Response()
+ stop_recording = StopRecording()
+ response.add_verb(stop_recording)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_transfer(self):
+ response = Response()
+ phone1 = PhoneNumber(
+ number="+11234567891",
+ transfer_answer_url="https://transfer.com/answer",
+ transfer_answer_method="POST",
+ username="user",
+ password="pass",
+ tag="tag",
+ transfer_disconnect_method="POST",
+ transfer_disconnect_url="https://transfer.com/disconnect",
+ transfer_answer_fallback_url="https://test.com",
+ transfer_answer_fallback_method="GET",
+ fallback_username="fuser",
+ fallback_password="fpass"
+ )
+ phone1_bxml = '+11234567891'
+ phone2 = PhoneNumber(number="+11234567892")
+ phone2_bxml = '+11234567892'
+ transfer = Transfer(
+ transfer_caller_id="+15555555555",
+ call_timeout=50,
+ tag="tag",
+ transfer_complete_url="https://transcribe.url.server/complete",
+ transfer_complete_method="POST",
+ username="user",
+ password="pass",
+ diversion_treatment="propagate",
+ diversion_reason="away",
+ phone_numbers=[phone1, phone2],
+ transfer_complete_fallback_url="https://test.com",
+ transfer_complete_fallback_method="GET",
+ fallback_username="fusern",
+ fallback_password="fpassw"
+ )
+ response.add_verb(transfer)
+ expected_bxml = f'{phone1_bxml}{phone2_bxml}'
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_conference(self):
+ conference = Conference("my-conference", mute=False, hold=True, call_ids_to_coach="c-123,c-345",
+ conference_event_url="https://test.com", conference_event_method="GET", username="user",
+ password="pass", tag="tag", conference_event_fallback_url="https://test2.com",
+ conference_event_fallback_method="POST", fallback_username="fuser", fallback_password="fpass")
+
+ response = Response()
+ response.add_verb(conference)
+ expected_bxml = 'my-conference'
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_conference_coach_array(self):
+ conference = Conference("my-conference", call_ids_to_coach=["c-123", "c-456"])
+
+ response = Response()
+ response.add_verb(conference)
+ expected_bxml = 'my-conference'
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_bridge(self):
+ bridge = Bridge("c-95ac8d6e-1a31c52e-b38f-4198-93c1-51633ec68f8d",
+ bridge_complete_url="https://test.com",
+ bridge_complete_method="GET",
+ bridge_target_complete_url="https://test2.com",
+ bridge_target_complete_method="POST",
+ username="user",
+ password="pass",
+ tag="custom tag",
+ bridge_complete_fallback_url="https://test3.com",
+ bridge_complete_fallback_method="GET",
+ bridge_target_complete_fallback_url="https://test4.com",
+ bridge_target_complete_fallback_method="POST",
+ fallback_username="fuser",
+ fallback_password="fpass"
+ )
+
+ response = Response()
+ response.add_verb(bridge)
+ expected_bxml = 'c-95ac8d6e-1a31c52e-b38f-4198-93c1-51633ec68f8d'
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_ring(self):
+ ring = Ring(
+ duration=5
+ )
+
+ response = Response()
+ response.add_verb(ring)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_start_gather(self):
+ startGather = StartGather(
+ dtmfUrl= "https://test.com",
+ dtmfMethod = "POST",
+ username = "user",
+ password = "pass",
+ tag = "custom tag"
+ )
+
+ response = Response()
+ response.add_verb(startGather)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_stop_gather(self):
+ stopGather = StopGather()
+
+ response = Response()
+ response.add_verb(stopGather)
+ expected_bxml = ''
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+ def test_gather_speak_sentence_ssml(self):
+ response = Response()
+ speak_sentence = SpeakSentence(
+ sentence='Hello. Your number is asdf, lets play a game. What is 10 + 3. Press the pound key when finished.'
+ )
+ gather = Gather(
+ speak_sentence=speak_sentence
+ )
+ expected_bxml = 'Hello. Your number is asdf, lets play a game. What is 10 + 3. Press the pound key when finished.'
+ response.add_verb(gather)
+ self.assertEqual(response.to_bxml(), expected_bxml)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/.github/actions/validate/entrypoint.sh b/.github/actions/validate/entrypoint.sh
index bb4e395c..0bb481d5 100644
--- a/.github/actions/validate/entrypoint.sh
+++ b/.github/actions/validate/entrypoint.sh
@@ -1,7 +1,5 @@
#!/bin/sh
-pip install twine
-pip install wheel
-python setup.py sdist bdist_wheel
-
-twine check dist/*
+pip install -e .
+cp /bxml_tests.py .
+python bxml_tests.py
diff --git a/bandwidth/voice/bxml/verbs/gather.py b/bandwidth/voice/bxml/verbs/gather.py
index b6aecde8..1a094a99 100644
--- a/bandwidth/voice/bxml/verbs/gather.py
+++ b/bandwidth/voice/bxml/verbs/gather.py
@@ -9,6 +9,9 @@
from lxml import etree
from .base_verb import AbstractBxmlVerb
+from .speak_sentence import SSML_REGEX
+
+import re
GATHER_TAG = "Gather"
@@ -97,4 +100,4 @@ def to_bxml(self):
if self.nested_verbs is not None:
for verb in self.nested_verbs:
root.append(verb.to_etree_element())
- return etree.tostring(root).decode()
+ return re.sub(SSML_REGEX, r"<\1>", etree.tostring(root).decode())
diff --git a/setup.py b/setup.py
index a518e388..0cff86ed 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@
setup(
name='bandwidth-sdk',
- version='6.14.0',
+ version='6.14.1',
description='Bandwidth\'s set of APIs',
long_description=long_description,
long_description_content_type="text/markdown",