Skip to content
4 changes: 3 additions & 1 deletion bandwidth/model/bxml/bxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

@copyright Bandwidth INC
"""
from typing import List

from .root import Root
from .verb import Verb


class Bxml(Root):
def __init__(self, nested_verbs: list[Verb] = []):
def __init__(self, nested_verbs: List[Verb] = []):
"""Initialize an instance of the <Bxml> root

Args:
Expand Down
4 changes: 3 additions & 1 deletion bandwidth/model/bxml/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

@copyright Bandwidth INC
"""
from typing import List

from .root import Root
from .verb import Verb


class Response(Root):
def __init__(self, nested_verbs: list[Verb] = []):
def __init__(self, nested_verbs: List[Verb] = []):
"""Initialize an instance of the <Response> root

Args:
Expand Down
3 changes: 2 additions & 1 deletion bandwidth/model/bxml/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

@copyright Bandwidth INC
"""
from typing import List
import xml.etree.ElementTree as ET

from bandwidth.model.bxml.verb import Verb
Expand All @@ -14,7 +15,7 @@ class Root:
"""Base class for BXML roots
"""

def __init__(self, tag: str, nested_verbs: list[Verb] = None):
def __init__(self, tag: str, nested_verbs: List[Verb] = None):
"""Initialize instance of class

Args:
Expand Down
22 changes: 18 additions & 4 deletions bandwidth/model/bxml/verb.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,27 @@ def __getitem__(self, position) -> Verb:
"""
return self._nested_verbs[position]

def _set_attributes(self, root: ET.Element):
"""Set XML attributes on an Element

Args:
root (ET.Element): XML Element to add attributes to
"""
if self._attributes:
for key, value in self._attributes.items():
if value is not None:
root.set(key, value)

def _to_etree_element(self) -> ET.Element:
"""Generate an ET.Element object from a Verb Object

Returns:
ET.Element: ET.Element representation of Verb
"""
root = ET.Element(self._tag)
if self._content:
root.text = self._content
if self._attributes:
for key, value in self._attributes:
if value:
root.set(key, value)
self._set_attributes(root)
if self._nested_verbs:
for verb in self._nested_verbs:
root.append(verb._to_etree_element())
Expand All @@ -70,6 +83,7 @@ def _generate_xml(self) -> ET.ElementTree:
root = ET.Element(self._tag)
if self._content:
root.text = self._content
self._set_attributes(root)
if self._nested_verbs:
for verb in self._nested_verbs:
root.append(verb._to_etree_element())
Expand Down
3 changes: 3 additions & 0 deletions bandwidth/model/bxml/verbs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from .phone_number import PhoneNumber
from .sip_uri import SipUri
from .tag import Tag
from .transfer import Transfer
64 changes: 64 additions & 0 deletions bandwidth/model/bxml/verbs/phone_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
phone_number.py

Bandwidth's PhoneNumber BXML verb

@copyright Bandwidth INC
"""
from ..verb import Verb


class PhoneNumber(Verb):

def __init__(
self, number: str, transfer_answer_url: str=None, transfer_answer_method: str=None,
transfer_answer_fallback_url: str=None, transfer_answer_fallback_method: str=None,
transfer_disconnect_url: str=None, transfer_disconnect_method: str=None, username: str=None,
password: str=None, fallback_username: str=None, fallback_password: str=None, tag: str=None
):
"""Initialize a <PhoneNumber> verb

Args:
phone_number (str): A phone number to transfer the call to. Value must be in E.164 format (e.g. +15555555555).
transfer_answer_url (str, optional): URL, if any, to send the Transfer Answer event to and request BXML to be executed for the called party before the call is bridged. May be a relative URL. Defaults to None.
transfer_answer_method (str, optional): The HTTP method to use for the request to transferAnswerUrl. GET or POST. Default value is POST. Defaults to None.
transfer_answer_fallback_url (str, optional): A fallback url which, if provided, will be used to retry the Transfer Answer callback delivery in case transferAnswerUrl fails to respond. Defaults to None.
transfer_answer_fallback_method (str, optional): The HTTP method to use to deliver the Transfer Answer callback to transferAnswerFallbackUrl. GET or POST. Default value is POST. Defaults to None.
transfer_disconnect_url (str, optional): URL, if any, to send the Transfer Disconnect event to. This event will be sent regardless of how the transfer ends and may not be responded to with BXML. May be a relative URL. Defaults to None.
transfer_disconnect_method (str, optional): The HTTP method to use for the request to transferDisconnectUrl. GET or POST. Default value is POST. Defaults to Defaults to Defaults to None.
username (str, optional): The username to send in the HTTP request to transferAnswerUrl and transferDisconnectUrl. Defaults to Defaults to None.
password (str, optional): The password to send in the HTTP request to transferAnswerUrl and transferDisconnectUrl. Defaults to Defaults to None.
fallback_username (str, optional): The username to send in the HTTP request to transferAnswerFallbackUrl. Defaults to None.
fallback_password (str, optional): The password to send in the HTTP request to transferAnswerFallbackUrl. Defaults to None.
tag (str, optional): A custom string that will be sent with these and all future callbacks unless overwritten by a future tag attribute or cleared. May be cleared by setting tag="" Max length 256 characters. Defaults to None.
"""
self.attributes = {
"fallbackPassword": fallback_password,
"fallbackUsername": fallback_username,
"password": password,
"tag": tag,
"transferAnswerFallbackMethod": transfer_answer_fallback_method,
"transferAnswerFallbackUrl": transfer_answer_fallback_url,
"transferAnswerMethod": transfer_answer_method,
"transferAnswerUrl": transfer_answer_url,
"transferDisconnectMethod": transfer_disconnect_method,
"transferDisconnectUrl": transfer_disconnect_url,
"username": username
}
super().__init__(
tag="PhoneNumber",
content=number,
attributes=self.attributes,
nested_verbs=None
)

def add_verb(self, verb: Verb):
"""Adding verbs is not allowed for <PhoneNumber>

Args:
verb (Verb): BXML verb

Raises:
AttributeError: This method is not allowed for <PhoneNumber>
"""
raise AttributeError('Adding verbs is not supported by <PhoneNumber>')
66 changes: 66 additions & 0 deletions bandwidth/model/bxml/verbs/sip_uri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
sip_uri.py

Bandwidth's SipUri BXML verb

@copyright Bandwidth INC
"""
from ..verb import Verb


class SipUri(Verb):

def __init__(
self, uri: str, uui: str=None, transfer_answer_url: str=None, transfer_answer_method: str=None,
transfer_answer_fallback_url: str=None, transfer_answer_fallback_method: str=None,
transfer_disconnect_url: str=None, transfer_disconnect_method: str=None, username: str=None,
password: str=None, fallback_username: str=None, fallback_password: str=None, tag: str=None
):
"""Initialize a <SipUri> verb

Args:
uri (str): A SIP URI to transfer the call to (e.g. sip:user@server.com)
uui (str, optional): he value of the User-To-User header to send within the initial INVITE. Must include the encoding parameter as specified in RFC 7433. Only base64 and jwt encoding are currently allowed. This value, including the encoding specifier, may not exceed 256 characters. Defaults to None.
transfer_answer_url (str, optional): URL, if any, to send the Transfer Answer event to and request BXML to be executed for the called party before the call is bridged. May be a relative URL. Defaults to None.
transfer_answer_method (str, optional): The HTTP method to use for the request to transferAnswerUrl. GET or POST. Default value is POST. Defaults to None.
transfer_answer_fallback_url (str, optional): A fallback url which, if provided, will be used to retry the Transfer Answer callback delivery in case transferAnswerUrl fails to respond. Defaults to None.
transfer_answer_fallback_method (str, optional): The HTTP method to use to deliver the Transfer Answer callback to transferAnswerFallbackUrl. GET or POST. Default value is POST. Defaults to None.
transfer_disconnect_url (str, optional): URL, if any, to send the Transfer Disconnect event to. This event will be sent regardless of how the transfer ends and may not be responded to with BXML. May be a relative URL. Defaults to None.
transfer_disconnect_method (str, optional): The HTTP method to use for the request to transferDisconnectUrl. GET or POST. Default value is POST. Defaults to Defaults to Defaults to None.
username (str, optional): The username to send in the HTTP request to transferAnswerUrl and transferDisconnectUrl. Defaults to Defaults to None.
password (str, optional): The password to send in the HTTP request to transferAnswerUrl and transferDisconnectUrl. Defaults to Defaults to None.
fallback_username (str, optional): The username to send in the HTTP request to transferAnswerFallbackUrl. Defaults to None.
fallback_password (str, optional): The password to send in the HTTP request to transferAnswerFallbackUrl. Defaults to None.
tag (str, optional): A custom string that will be sent with these and all future callbacks unless overwritten by a future tag attribute or cleared. May be cleared by setting tag="" Max length 256 characters. Defaults to None.
"""
self.attributes = {
"fallbackPassword": fallback_password,
"fallbackUsername": fallback_username,
"password": password,
"tag": tag,
"transferAnswerFallbackMethod": transfer_answer_fallback_method,
"transferAnswerFallbackUrl": transfer_answer_fallback_url,
"transferAnswerMethod": transfer_answer_method,
"transferAnswerUrl": transfer_answer_url,
"transferDisconnectMethod": transfer_disconnect_method,
"transferDisconnectUrl": transfer_disconnect_url,
"username": username,
"uui": uui
}
super().__init__(
tag="SipUri",
content=uri,
attributes=self.attributes,
nested_verbs=None
)

def add_verb(self, verb: Verb):
"""Adding verbs is not allowed for <SipUri>

Args:
verb (Verb): BXML verb

Raises:
AttributeError: This method is not allowed for <SipUri>
"""
raise AttributeError('Adding verbs is not supported by <SipUri>')
2 changes: 1 addition & 1 deletion bandwidth/model/bxml/verbs/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self, content=""):
Args:
content (str, optional): Custom tag value. Defaults to "".
"""
super().__init__(tag="Tag", attributes=None, content=content, nested_verbs=None)
super().__init__(tag="Tag", content=content, attributes=None, nested_verbs=None)

def add_verb(self, verb: Verb):
"""Adding verbs is not allowed for <Tag>
Expand Down
87 changes: 87 additions & 0 deletions bandwidth/model/bxml/verbs/transfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
transfer.py

Bandwidth's Transfer BXML verb

@copyright Bandwidth INC
"""
from typing import Union, List

from ..verb import Verb
from ..verbs.phone_number import PhoneNumber
from ..verbs.sip_uri import SipUri


class Transfer(Verb):

def __init__(
self, transfer_to: List[Union[PhoneNumber, SipUri]] = [],
transfer_caller_id: str=None, call_timeout: str=None,
transfer_complete_url: str=None, transfer_complete_method: str=None,
transfer_complete_fallback_url: str=None,
transfer_complete_fallback_method: str=None, username: str=None,
password: str=None, fallback_username: str=None,
fallback_password: str=None, tag: str=None,
diversion_treatment: str=None, diversion_reason: str=None
):
"""Initialize a <Transfer> verb

Args:
transfer_to (list[PhoneNumber, SipUri], optional): List of recipients to transfer a call to. Defaults to [].
transfer_caller_id (str, optional): The caller ID to use when the call is transferred, if different. Must be in E.164 format (e.g. +15555555555) or be one of the following strings Restricted, Anonymous, Private, or Unavailable. Leave as default to pass along the number of the remote party. Defaults to None.
call_timeout (str, optional):The timeout (in seconds) for the callee to answer the call after it starts ringing. If the call does not start ringing within 30s, the call will be cancelled regardless of this value. Range: decimal values between 1 - 300. Default value is 30 seconds. Defaults to None.
transfer_complete_url (str, optional): URL to send the Transfer Complete event to and request new BXML. Optional but recommended. See below for further details. May be a relative URL. Defaults to None.
transfer_complete_method (str, optional): The HTTP method to use for the request to transferCompleteUrl. GET or POST. Default value is POST. Defaults to None.
transfer_complete_fallback_url (str, optional): A fallback url which, if provided, will be used to retry the Transfer Complete callback delivery in case transferCompleteUrl fails to respond. Defaults to None.
transfer_complete_fallback_method (str, optional): The HTTP method to use to deliver the Transfer Complete callback to transferCompleteFallbackUrl. GET or POST. Default value is POST. Defaults to None.
username (str, optional): The username to send in the HTTP request to transferCompleteUrl. Defaults to None.
password (str, optional): The password to send in the HTTP request to transferCompleteUrl. Defaults to None.
fallback_username (str, optional): The username to send in the HTTP request to transferCompleteFallbackUrl. Defaults to None.
fallback_password (str, optional): The password to send in the HTTP request to transferCompleteFallbackUrl. Defaults to None.
tag (str, optional): A custom string that will be sent with this and all future callbacks unless overwritten by a future tag attribute or cleared. May be cleared by setting tag="" Max length 256 characters. Defaults to None.
diversion_treatment (str, optional): Can be any of the following:
none: No diversion headers are sent on the outbound leg of the transferred call.
propagate: Copy the Diversion header from the inbound leg to the outbound leg. Ignored if there is no Diversion header present on the inbound leg.
stack: After propagating any Diversion header from the inbound leg to the outbound leg, stack on top another Diversion header based on the Request-URI of the inbound call.

Defaults to none. If diversionTreatment is not specified, no diversion header will be included for the transfer even if one came with the inbound call. Defaults to None.
diversion_reason (str, optional): Can be any of the following values:
unknown
user-busy
no-answer
unavailable
unconditional
time-of-day
do-not-disturb
deflection
follow-me
out-of-service
away

This parameter is considered only when diversionTreatment is set to stack. Defaults is unknown.
Defaults to None.
"""
self.attributes = {
"callTimeout": call_timeout,
"diversionReason": diversion_reason,
"diversionTreatment": diversion_treatment,
"fallbackPassword": fallback_password,
"fallbackUsername": fallback_username,
"password": password,
"tag": tag,
"transferCallerId": transfer_caller_id,
"transferCompleteFallbackMethod": transfer_complete_fallback_method,
"transferCompleteFallbackUrl": transfer_complete_fallback_url,
"transferCompleteMethod": transfer_complete_method,
"transferCompleteUrl": transfer_complete_url,
"username": username
}
super().__init__(
tag="Transfer",
content=None,
attributes=self.attributes,
nested_verbs=transfer_to
)

def add_transfer_recipient(self, recipient: Union[PhoneNumber, SipUri]):
super().add_verb(recipient)
6 changes: 3 additions & 3 deletions bandwidth/utilities/web_rtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def _generate_transfer_model(device_token: str, voice_call_id: str, sip_uri: str
"""
uui = "".join(voice_call_id.split("-")[1::])
sip_uri = SipUri(
uui=f"{uui};encoding=base64,{device_token};encoding=jwt",
uri=sip_uri
uri=sip_uri,
uui=f"{uui};encoding=base64,{device_token};encoding=jwt"
)
transfer = Transfer(
sip_uris=[sip_uri]
transfer_to=[sip_uri]
)
return transfer

Expand Down
32 changes: 32 additions & 0 deletions test/unit/bxml/test_phone_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
test_phone_number.py

Unit tests for the <PhoneNumber> BXML verb

@copyright Bandwidth Inc.
"""
import pytest
import unittest

from bandwidth.model.bxml.verb import Verb
from bandwidth.model.bxml.verbs.phone_number import PhoneNumber


class TestPhoneNumber(unittest.TestCase):

def setUp(self):
self.phone_number = PhoneNumber(
number="+19195551234",
tag="",
transfer_answer_method="POST",
transfer_answer_url="https://example.com/webhooks/transfer_answer"
)
self.test_verb = Verb(tag="test")

def test_to_bxml(self):
expected = '<PhoneNumber tag="" transferAnswerMethod="POST" transferAnswerUrl="https://example.com/webhooks/transfer_answer">+19195551234</PhoneNumber>'
assert(expected == self.phone_number.to_bxml())

def test_add_verb(self):
with pytest.raises(AttributeError):
self.phone_number.add_verb(self.test_verb)
Loading