-
Notifications
You must be signed in to change notification settings - Fork 9
VAPI-3161: Add <Refer> BXML verb support #295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9a134a9
VAPI-3161: Add <Refer> BXML verb
mramasubramanian-bw cf272b1
VAPI-3161 Add ReferCompleteCallback model and scenario tests
stampercasey 882735a
VAPI-3163 Replace Transfer SipUri with ReferSipUri in <Refer>
stampercasey 47a9d68
Merge branch 'main' into VAPI-3161-add-refer-verb
stampercasey e0d827a
VAPI-3435 Simplify Refer to reuse Transfer's SipUri
stampercasey a248043
Merge branch 'main' into VAPI-3161-add-refer-verb
stampercasey e76dc6b
Address review feedback: optional sip_uri, set_sip_uri setter
stampercasey 2052a03
Merge remote-tracking branch 'origin/VAPI-3161-add-refer-verb' into V…
stampercasey bb89c32
Merge branch 'main' into VAPI-3161-add-refer-verb
ckoegel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| """ | ||
| refer.py | ||
|
|
||
| Bandwidth's Refer BXML verb | ||
|
|
||
| @copyright Bandwidth INC | ||
| """ | ||
| from ..nestable_verb import NestableVerb | ||
| from .refer_sip_uri import ReferSipUri | ||
|
|
||
|
|
||
| class Refer(NestableVerb): | ||
|
|
||
| def __init__( | ||
| self, sip_uri: ReferSipUri, | ||
| refer_complete_url: str=None, refer_complete_method: str=None, | ||
| tag: str=None | ||
| ): | ||
| """Initialize a <Refer> verb | ||
|
|
||
| The <Refer> verb sends a SIP REFER to the remote endpoint, asking it | ||
| to redirect the call to a new SIP URI. Unlike <Transfer>, a successful | ||
| REFER terminates the call on Bandwidth's side: the remote endpoint | ||
| redirects away from Bandwidth entirely. This is a SIP protocol | ||
| property, not a Bandwidth design choice. As a result, BXML returned in | ||
| response to the referComplete callback is only meaningful for failure | ||
| handling — there is no live call to act on after success. | ||
|
|
||
| Args: | ||
| sip_uri (ReferSipUri): The SIP URI to refer the call to. Required. | ||
| Exactly one <SipUri> child element is allowed. Use ReferSipUri, | ||
| not SipUri — the Transfer SipUri carries callbacks and auth | ||
| fields that are not valid in a REFER context. | ||
| refer_complete_url (str, optional): URL to send the Refer Complete | ||
| event to when the REFER flow finishes (success or failure). | ||
| May be a relative URL. Defaults to None. | ||
| refer_complete_method (str, optional): The HTTP method to use for | ||
| the request to referCompleteUrl. GET or POST. Default value | ||
| is POST. 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. | ||
| """ | ||
| self.sip_uri = sip_uri | ||
| self.refer_complete_url = refer_complete_url | ||
| self.refer_complete_method = refer_complete_method | ||
| self.tag = tag | ||
| super().__init__( | ||
| tag="Refer", | ||
| nested_verbs=[self.sip_uri] | ||
| ) | ||
|
|
||
| @property | ||
| def _attributes(self): | ||
| return { | ||
| "referCompleteUrl": self.refer_complete_url, | ||
| "referCompleteMethod": self.refer_complete_method, | ||
| "tag": self.tag | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should add something similar to ruby and node to allow the user to set the sip_uri after creation, we can't use |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """ | ||
| refer_sip_uri.py | ||
|
|
||
| Bandwidth's ReferSipUri BXML element | ||
|
|
||
| @copyright Bandwidth INC | ||
| """ | ||
| from ..verb import Verb | ||
|
|
||
|
|
||
| class ReferSipUri(Verb): | ||
|
|
||
| def __init__(self, uri: str): | ||
| """Initialize a <SipUri> child element for use within <Refer>. | ||
|
|
||
| Unlike the SipUri used with <Transfer>, this element carries only the | ||
| destination URI — no transfer callbacks, auth, or UUI headers apply | ||
| to a SIP REFER. | ||
|
|
||
| Args: | ||
| uri (str): The SIP URI to refer the call to (e.g. sip:user@host.example.com). | ||
| """ | ||
| self.uri = uri | ||
| super().__init__( | ||
| tag="SipUri", | ||
| content=self.uri, | ||
| ) | ||
|
|
||
| @property | ||
| def _attributes(self): | ||
| return None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| # coding: utf-8 | ||
|
|
||
| """ | ||
| Bandwidth | ||
|
|
||
| Bandwidth's Communication APIs | ||
|
|
||
| The version of the OpenAPI document: 1.0.0 | ||
| Contact: letstalk@bandwidth.com | ||
| Generated by OpenAPI Generator (https://openapi-generator.tech) | ||
|
|
||
| Do not edit the class manually. | ||
| """ # noqa: E501 | ||
|
|
||
|
|
||
| from __future__ import annotations | ||
| import pprint | ||
| import re # noqa: F401 | ||
| import json | ||
|
|
||
| from datetime import datetime | ||
| from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr | ||
| from typing import Any, ClassVar, Dict, List, Optional | ||
| from bandwidth.models.call_direction_enum import CallDirectionEnum | ||
| from typing import Optional, Set | ||
| from typing_extensions import Self | ||
|
|
||
| class ReferCompleteCallback(BaseModel): | ||
| """ | ||
| The Refer Complete event is fired when the <Refer> verb finishes executing. This is sent to the referCompleteUrl specified on the <Refer> verb, and the BXML returned in it is executed on the call only on failure — the call is torn down on success. | ||
| """ # noqa: E501 | ||
| event_type: Optional[StrictStr] = Field(default=None, description="The event type, value is referComplete.", alias="eventType") | ||
| event_time: Optional[datetime] = Field(default=None, description="The approximate UTC date and time when the event was generated by the Bandwidth server, in ISO 8601 format. This may not be exactly the time of event execution.", alias="eventTime") | ||
| account_id: Optional[StrictStr] = Field(default=None, description="The user account associated with the call.", alias="accountId") | ||
| application_id: Optional[StrictStr] = Field(default=None, description="The id of the application associated with the call.", alias="applicationId") | ||
| var_from: Optional[StrictStr] = Field(default=None, description="The provided identifier of the caller. Must be a phone number in E.164 format (e.g. +15555555555).", alias="from") | ||
| to: Optional[StrictStr] = Field(default=None, description="The phone number that received the call, in E.164 format (e.g. +15555555555).") | ||
| direction: Optional[CallDirectionEnum] = None | ||
| call_id: Optional[StrictStr] = Field(default=None, description="The call id associated with the event.", alias="callId") | ||
| call_url: Optional[StrictStr] = Field(default=None, description="The URL of the call associated with the event.", alias="callUrl") | ||
| start_time: Optional[datetime] = Field(default=None, description="Time the call was started, in ISO 8601 format.", alias="startTime") | ||
| answer_time: Optional[datetime] = Field(default=None, description="Time the call was answered, in ISO 8601 format.", alias="answerTime") | ||
| tag: Optional[StrictStr] = Field(default=None, description="(optional) The tag specified on call creation. If no tag was specified or it was previously cleared, this field will not be present.") | ||
| refer_call_status: Optional[StrictStr] = Field(default=None, description="The outcome of the REFER operation. Either 'success' or 'failure'.", alias="referCallStatus") | ||
| refer_sip_response_code: Optional[StrictInt] = Field(default=None, description="The SIP response code returned for the REFER request itself (e.g. 202, 405, 603). Present when a SIP response was received for the REFER.", alias="referSipResponseCode") | ||
| notify_sip_response_code: Optional[StrictInt] = Field(default=None, description="The final SIP response code reported via NOTIFY. Present only when the caller's endpoint sent a final NOTIFY (e.g. 200, 404, 486). Not present on NOTIFY timeout or when REFER was rejected before a subscription was established.", alias="notifySipResponseCode") | ||
| additional_properties: Dict[str, Any] = {} | ||
| __properties: ClassVar[List[str]] = ["eventType", "eventTime", "accountId", "applicationId", "from", "to", "direction", "callId", "callUrl", "startTime", "answerTime", "tag", "referCallStatus", "referSipResponseCode", "notifySipResponseCode"] | ||
|
|
||
| model_config = ConfigDict( | ||
| populate_by_name=True, | ||
| validate_assignment=True, | ||
| protected_namespaces=(), | ||
| ) | ||
|
|
||
|
|
||
| def to_str(self) -> str: | ||
| """Returns the string representation of the model using alias""" | ||
| return pprint.pformat(self.model_dump(by_alias=True)) | ||
|
|
||
| def to_json(self) -> str: | ||
| """Returns the JSON representation of the model using alias""" | ||
| # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead | ||
| return json.dumps(self.to_dict()) | ||
|
|
||
| @classmethod | ||
| def from_json(cls, json_str: str) -> Optional[Self]: | ||
| """Create an instance of ReferCompleteCallback from a JSON string""" | ||
| return cls.from_dict(json.loads(json_str)) | ||
|
|
||
| def to_dict(self) -> Dict[str, Any]: | ||
| """Return the dictionary representation of the model using alias. | ||
|
|
||
| This has the following differences from calling pydantic's | ||
| `self.model_dump(by_alias=True)`: | ||
|
|
||
| * `None` is only added to the output dict for nullable fields that | ||
| were set at model initialization. Other fields with value `None` | ||
| are ignored. | ||
| * Fields in `self.additional_properties` are added to the output dict. | ||
| """ | ||
| excluded_fields: Set[str] = set([ | ||
| "additional_properties", | ||
| ]) | ||
|
|
||
| _dict = self.model_dump( | ||
| by_alias=True, | ||
| exclude=excluded_fields, | ||
| exclude_none=True, | ||
| ) | ||
| # puts key-value pairs in additional_properties in the top level | ||
| if self.additional_properties is not None: | ||
| for _key, _value in self.additional_properties.items(): | ||
| _dict[_key] = _value | ||
|
|
||
| # set to None if answer_time (nullable) is None | ||
| # and model_fields_set contains the field | ||
| if self.answer_time is None and "answer_time" in self.model_fields_set: | ||
| _dict['answerTime'] = None | ||
|
|
||
| # set to None if tag (nullable) is None | ||
| # and model_fields_set contains the field | ||
| if self.tag is None and "tag" in self.model_fields_set: | ||
| _dict['tag'] = None | ||
|
|
||
| # set to None if refer_sip_response_code (nullable) is None | ||
| # and model_fields_set contains the field | ||
| if self.refer_sip_response_code is None and "refer_sip_response_code" in self.model_fields_set: | ||
| _dict['referSipResponseCode'] = None | ||
|
|
||
| # set to None if notify_sip_response_code (nullable) is None | ||
| # and model_fields_set contains the field | ||
| if self.notify_sip_response_code is None and "notify_sip_response_code" in self.model_fields_set: | ||
| _dict['notifySipResponseCode'] = None | ||
|
|
||
| return _dict | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: | ||
| """Create an instance of ReferCompleteCallback from a dict""" | ||
| if obj is None: | ||
| return None | ||
|
|
||
| if not isinstance(obj, dict): | ||
| return cls.model_validate(obj) | ||
|
|
||
| _obj = cls.model_validate({ | ||
| "eventType": obj.get("eventType"), | ||
| "eventTime": obj.get("eventTime"), | ||
| "accountId": obj.get("accountId"), | ||
| "applicationId": obj.get("applicationId"), | ||
| "from": obj.get("from"), | ||
| "to": obj.get("to"), | ||
| "direction": obj.get("direction"), | ||
| "callId": obj.get("callId"), | ||
| "callUrl": obj.get("callUrl"), | ||
| "startTime": obj.get("startTime"), | ||
| "answerTime": obj.get("answerTime"), | ||
| "tag": obj.get("tag"), | ||
| "referCallStatus": obj.get("referCallStatus"), | ||
| "referSipResponseCode": obj.get("referSipResponseCode"), | ||
| "notifySipResponseCode": obj.get("notifySipResponseCode") | ||
| }) | ||
| # store additional fields in additional_properties | ||
| for _key in obj.keys(): | ||
| if _key not in cls.__properties: | ||
| _obj.additional_properties[_key] = obj.get(_key) | ||
|
|
||
| return _obj |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| """ | ||
| test_refer.py | ||
|
|
||
| Unit tests for the <Refer> BXML verb | ||
|
|
||
| @copyright Bandwidth Inc. | ||
| """ | ||
| import unittest | ||
|
|
||
| from bandwidth.models.bxml import Refer, ReferSipUri, SipUri, Verb, NestableVerb | ||
|
|
||
|
|
||
| class TestRefer(unittest.TestCase): | ||
|
|
||
| def setUp(self): | ||
| self.sip_uri = ReferSipUri(uri="sip:alice@atlanta.example.com") | ||
| self.refer = Refer( | ||
| sip_uri=self.sip_uri, | ||
| refer_complete_url="https://example.com/handleRefer", | ||
| refer_complete_method="POST", | ||
| tag="test" | ||
| ) | ||
|
|
||
| def test_instance(self): | ||
| assert isinstance(self.refer, Refer) | ||
| assert isinstance(self.refer, NestableVerb) | ||
| assert isinstance(self.refer, Verb) | ||
|
|
||
| def test_to_bxml(self): | ||
| expected = '<Refer referCompleteUrl="https://example.com/handleRefer" referCompleteMethod="POST" tag="test"><SipUri>sip:alice@atlanta.example.com</SipUri></Refer>' | ||
| assert expected == self.refer.to_bxml() | ||
|
|
||
| def test_minimal(self): | ||
| minimal_refer = Refer(sip_uri=ReferSipUri(uri="sip:bob@example.com")) | ||
| expected = '<Refer><SipUri>sip:bob@example.com</SipUri></Refer>' | ||
| assert expected == minimal_refer.to_bxml() | ||
|
|
||
| def test_refer_sip_uri_is_not_transfer_sip_uri(self): | ||
| """ReferSipUri is a distinct type from the Transfer SipUri — no Transfer-specific | ||
| attributes (transfer_answer_url, uui, auth, etc.) are accepted.""" | ||
| assert not isinstance(self.sip_uri, SipUri) | ||
| assert isinstance(self.sip_uri, ReferSipUri) | ||
|
|
||
| def test_refer_sip_uri_has_no_transfer_attributes(self): | ||
| """ReferSipUri carries only uri — no Transfer baggage.""" | ||
| assert not hasattr(self.sip_uri, 'transfer_answer_url') | ||
| assert not hasattr(self.sip_uri, 'uui') | ||
| assert not hasattr(self.sip_uri, 'username') | ||
| assert not hasattr(self.sip_uri, 'password') |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to handle the uri being optional