From 48d409af3298a2045b1af64bf28e23e4613a66dd Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 14 Feb 2024 16:29:04 +0100 Subject: [PATCH 01/53] feat(Voice): WiP --- sinch/core/clients/sinch_client_async.py | 4 +- sinch/core/clients/sinch_client_base.py | 2 + .../clients/sinch_client_configuration.py | 2 + sinch/core/clients/sinch_client_sync.py | 2 + sinch/domains/voice/__init__.py | 180 ++++++++++++++++++ sinch/domains/voice/endpoints/__init__.py | 0 .../voice/endpoints/callouts/__init__.py | 0 .../voice/endpoints/callouts/callout.py | 41 ++++ .../domains/voice/endpoints/calls/__init__.py | 0 .../domains/voice/endpoints/calls/get_call.py | 37 ++++ .../voice/endpoints/calls/manage_call.py | 0 .../voice/endpoints/calls/update_call.py | 28 +++ .../domains/voice/endpoints/voice_endpoint.py | 13 ++ sinch/domains/voice/enums.py | 7 + sinch/domains/voice/exceptions.py | 5 + sinch/domains/voice/models/__init__.py | 0 .../domains/voice/models/callouts/__init__.py | 0 .../domains/voice/models/callouts/requests.py | 48 +++++ .../voice/models/callouts/responses.py | 7 + sinch/domains/voice/models/calls/__init__.py | 0 sinch/domains/voice/models/calls/requests.py | 14 ++ sinch/domains/voice/models/calls/responses.py | 23 +++ tests/conftest.py | 7 +- tests/e2e/voice/callouts/test_callout.py | 52 +++++ tests/e2e/voice/calls/test_get_call.py | 11 ++ tests/e2e/voice/calls/test_manage_call.py | 0 tests/e2e/voice/calls/test_update_call.py | 23 +++ 27 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 sinch/domains/voice/__init__.py create mode 100644 sinch/domains/voice/endpoints/__init__.py create mode 100644 sinch/domains/voice/endpoints/callouts/__init__.py create mode 100644 sinch/domains/voice/endpoints/callouts/callout.py create mode 100644 sinch/domains/voice/endpoints/calls/__init__.py create mode 100644 sinch/domains/voice/endpoints/calls/get_call.py create mode 100644 sinch/domains/voice/endpoints/calls/manage_call.py create mode 100644 sinch/domains/voice/endpoints/calls/update_call.py create mode 100644 sinch/domains/voice/endpoints/voice_endpoint.py create mode 100644 sinch/domains/voice/enums.py create mode 100644 sinch/domains/voice/exceptions.py create mode 100644 sinch/domains/voice/models/__init__.py create mode 100644 sinch/domains/voice/models/callouts/__init__.py create mode 100644 sinch/domains/voice/models/callouts/requests.py create mode 100644 sinch/domains/voice/models/callouts/responses.py create mode 100644 sinch/domains/voice/models/calls/__init__.py create mode 100644 sinch/domains/voice/models/calls/requests.py create mode 100644 sinch/domains/voice/models/calls/responses.py create mode 100644 tests/e2e/voice/callouts/test_callout.py create mode 100644 tests/e2e/voice/calls/test_get_call.py create mode 100644 tests/e2e/voice/calls/test_manage_call.py create mode 100644 tests/e2e/voice/calls/test_update_call.py diff --git a/sinch/core/clients/sinch_client_async.py b/sinch/core/clients/sinch_client_async.py index a32a223..c78cf71 100644 --- a/sinch/core/clients/sinch_client_async.py +++ b/sinch/core/clients/sinch_client_async.py @@ -6,7 +6,8 @@ from sinch.domains.numbers import NumbersAsync from sinch.domains.conversation import ConversationAsync from sinch.domains.sms import SMSAsync -from sinch.domains.verification import Verification as VerificationAsync +from sinch.domains.verification import VerificationAsync +from sinch.domains.voice import VoiceAsync class ClientAsync(ClientBase): @@ -42,3 +43,4 @@ def __init__( self.conversation = ConversationAsync(self) self.sms = SMSAsync(self) self.verification = VerificationAsync(self) + self.voice = VoiceAsync(self) diff --git a/sinch/core/clients/sinch_client_base.py b/sinch/core/clients/sinch_client_base.py index 7f53fd0..d3277c1 100644 --- a/sinch/core/clients/sinch_client_base.py +++ b/sinch/core/clients/sinch_client_base.py @@ -4,6 +4,7 @@ from sinch.domains.numbers import NumbersBase from sinch.domains.conversation import ConversationBase from sinch.domains.sms import SMSBase +from sinch.domains.voice import VoiceBase class ClientBase(ABC): @@ -17,6 +18,7 @@ class ClientBase(ABC): numbers = NumbersBase conversation = ConversationBase sms = SMSBase + voice = VoiceBase @abstractmethod def __init__( diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index 74f0931..11e8a77 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -32,6 +32,8 @@ def __init__( self.auth_origin = "auth.sinch.com" self.numbers_origin = "numbers.api.sinch.com" self.verification_origin = "verification.api.sinch.com" + self.voice_origin = "calling.api.sinch.com" + self.voice_region = "" # TODO: add region handling self._conversation_region = "eu" self._conversation_domain = ".conversation.api.sinch.com" self._sms_region = "us" diff --git a/sinch/core/clients/sinch_client_sync.py b/sinch/core/clients/sinch_client_sync.py index 0ec939c..a3746bc 100644 --- a/sinch/core/clients/sinch_client_sync.py +++ b/sinch/core/clients/sinch_client_sync.py @@ -7,6 +7,7 @@ from sinch.domains.conversation import Conversation from sinch.domains.sms import SMS from sinch.domains.verification import Verification +from sinch.domains.voice import Voice class Client(ClientBase): @@ -42,3 +43,4 @@ def __init__( self.conversation = Conversation(self) self.sms = SMS(self) self.verification = Verification(self) + self.voice = Voice(self) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py new file mode 100644 index 0000000..8a999e7 --- /dev/null +++ b/sinch/domains/voice/__init__.py @@ -0,0 +1,180 @@ +from sinch.domains.voice.endpoints.callouts.callout import CalloutEndpoint +from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint +from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint +from sinch.domains.voice.enums import CalloutMethod +from sinch.domains.voice.models.callouts.responses import CalloutResponse +from sinch.domains.voice.models.callouts.requests import ( + ConferenceCalloutRequest, + TextToSpeechCalloutRequest, + CustomCalloutRequest +) +from sinch.domains.voice.models.calls.requests import GetVoiceCallRequest, UpdateVoiceCallRequest +from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse + + +class Callouts: + def __init__(self, sinch): + self._sinch = sinch + + def text_to_speech( + self, + destination: dict, + cli: str = None, + dtmf: str = None, + domain: str = None, + custom: str = None, + locale: str = None, + text: str = None, + prompts: str = None, + enable_ace: bool = None, + enable_dice: bool = None, + enable_pie: bool = None + ) -> CalloutResponse: + return self._sinch.configuration.transport.request( + CalloutEndpoint( + callout_method=CalloutMethod.TTS.value, + request_data=TextToSpeechCalloutRequest( + destination=destination, + cli=cli, + dtmf=dtmf, + domain=domain, + custom=custom, + locale=locale, + text=text, + prompts=prompts, + enableAce=enable_ace, + enableDice=enable_dice, + enablePie=enable_pie + ) + ) + ) + + def conference( + self, + destination: dict, + conference_id: str, + cli: str = None, + conference_dtmf_options: dict = None, + dtmf: str = None, + conference: str = None, + max_duration: int = None, + enable_ace: bool = None, + enable_dice: bool = None, + enable_pie: bool = None, + locale: str = None, + greeting: str = None, + moh_class: str = None, + custom: str = None, + domain: str = None + ) -> CalloutResponse: + return self._sinch.configuration.transport.request( + CalloutEndpoint( + callout_method=CalloutMethod.CONFERENCE.value, + request_data=ConferenceCalloutRequest( + destination=destination, + conference_id=conference_id, + cli=cli, + conferenceDtmfOptions=conference_dtmf_options, + dtmf=dtmf, + conference=conference, + maxDuration=max_duration, + enableAce=enable_ace, + enableDice=enable_dice, + enablePie=enable_pie, + locale=locale, + greeting=greeting, + mohClass=moh_class, + custom=custom, + domain=domain + ) + ) + ) + + def custom( + self, + cli: str = None, + destination: dict = None, + dtmf: str = None, + custom: str = None, + max_duration: int = None, + ice: str = None, + ace: str = None, + pie: str = None + ) -> CalloutResponse: + return self._sinch.configuration.transport.request( + CalloutEndpoint( + callout_method=CalloutMethod.CUSTOM.value, + request_data=CustomCalloutRequest( + cli=cli, + destination=destination, + dtmf=dtmf, + custom=custom, + maxDuration=max_duration, + ice=ice, + ace=ace, + pie=pie + ) + ) + ) + + +class Calls: + def __init__(self, sinch): + self._sinch = sinch + + def get(self, call_id) -> GetVoiceCallResponse: + return self._sinch.configuration.transport.request( + GetCallEndpoint( + request_data=GetVoiceCallRequest( + callId=call_id + ) + ) + ) + + def update( + self, + call_id, + instructions: list, + action: dict + ): + return self._sinch.configuration.transport.request( + UpdateCallEndpoint( + request_data=UpdateVoiceCallRequest( + callId=call_id, + instructions=instructions, + action=action + ) + ) + ) + + +class VoiceBase: + """ + Documentation for the Voice API: https://developers.sinch.com/docs/voice/ + """ + def __init__(self, sinch): + self._sinch = sinch + + +class Voice(VoiceBase): + """ + Synchronous version of the Voice Domain + """ + __doc__ += VoiceBase.__doc__ + + def __init__(self, sinch): + super().__init__(sinch) + self.callouts = Callouts(self._sinch) + self.calls = Calls(self._sinch) + + +class VoiceAsync(VoiceBase): + """ + Asynchronous version of the Voice Domain + """ + __doc__ += VoiceBase.__doc__ + + def __init__(self, sinch): + super().__init__(sinch) + self.callouts = Callouts(self._sinch) + self.calls = Calls(self._sinch) diff --git a/sinch/domains/voice/endpoints/__init__.py b/sinch/domains/voice/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/callouts/__init__.py b/sinch/domains/voice/endpoints/callouts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/callouts/callout.py b/sinch/domains/voice/endpoints/callouts/callout.py new file mode 100644 index 0000000..495b39e --- /dev/null +++ b/sinch/domains/voice/endpoints/callouts/callout.py @@ -0,0 +1,41 @@ +import json +from sinch.domains.voice.enums import CalloutMethod +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.callouts.responses import CalloutResponse + + +class CalloutEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/callouts" + HTTP_METHOD = HTTPMethods.POST.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data, callout_method): + self.request_data = request_data + self.callout_method = callout_method + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin + ) + + def request_body(self): + request_data = {} + if self.callout_method == CalloutMethod.TTS.value: + request_data["method"] = CalloutMethod.TTS.value + request_data[CalloutMethod.TTS.value] = self.request_data.as_dict() + + elif self.callout_method == CalloutMethod.CUSTOM.value: + request_data["method"] = CalloutMethod.CUSTOM.value + request_data[CalloutMethod.CUSTOM.value] = self.request_data.as_dict() + + elif self.callout_method == CalloutMethod.CONFERENCE.value: + request_data["method"] = CalloutMethod.CONFERENCE.value + request_data[CalloutMethod.CONFERENCE.value] = self.request_data.as_dict() + + return json.dumps(request_data) + + def handle_response(self, response: HTTPResponse): + super().handle_response(response) + return CalloutResponse(call_id=response.body["callId"]) diff --git a/sinch/domains/voice/endpoints/calls/__init__.py b/sinch/domains/voice/endpoints/calls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py new file mode 100644 index 0000000..750757a --- /dev/null +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -0,0 +1,37 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse +from sinch.domains.voice.models.calls.requests import GetVoiceCallRequest + + +class GetCallEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/calls/id/{call_id}" + HTTP_METHOD = HTTPMethods.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: GetVoiceCallRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + call_id=self.request_data.callId + ) + + def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: + super().handle_response(response) + return GetVoiceCallResponse( + from_=response.body.get("from"), + to=response.body.get("to"), + domain=response.body.get("domain"), + call_id=response.body.get("callId"), + duration=response.body.get("duration"), + status=response.body.get("status"), + result=response.body.get("result"), + reason=response.body.get("reason"), + timestamp=response.body.get("timestamp"), + custom=response.body.get("custom"), + user_rate=response.body.get("userRate"), + debit=response.body.get("debit") + ) diff --git a/sinch/domains/voice/endpoints/calls/manage_call.py b/sinch/domains/voice/endpoints/calls/manage_call.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/calls/update_call.py b/sinch/domains/voice/endpoints/calls/update_call.py new file mode 100644 index 0000000..afd4850 --- /dev/null +++ b/sinch/domains/voice/endpoints/calls/update_call.py @@ -0,0 +1,28 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.calls.responses import UpdateVoiceCallResponse +from sinch.domains.voice.models.calls.requests import UpdateVoiceCallRequest + + +class UpdateCallEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/calls/id/{call_id}" + HTTP_METHOD = HTTPMethods.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: UpdateVoiceCallRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + call_id=self.request_data.callId + ) + + def request_body(self): + self.request_data.callId = None + return self.request_data.as_json() + + def handle_response(self, response: HTTPResponse) -> UpdateVoiceCallResponse: + super().handle_response(response) + return UpdateVoiceCallResponse() diff --git a/sinch/domains/voice/endpoints/voice_endpoint.py b/sinch/domains/voice/endpoints/voice_endpoint.py new file mode 100644 index 0000000..0595072 --- /dev/null +++ b/sinch/domains/voice/endpoints/voice_endpoint.py @@ -0,0 +1,13 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.core.endpoint import HTTPEndpoint +from sinch.domains.voice.exceptions import VoiceException + + +class VoiceEndpoint(HTTPEndpoint): + def handle_response(self, response: HTTPResponse): + if response.status_code >= 400: + raise VoiceException( + message=response.body["message"], + response=response, + is_from_server=True + ) diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py new file mode 100644 index 0000000..1c3091e --- /dev/null +++ b/sinch/domains/voice/enums.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class CalloutMethod(Enum): + TTS = "ttsCallout" + CUSTOM = "customCallout" + CONFERENCE = "conferenceCallout" diff --git a/sinch/domains/voice/exceptions.py b/sinch/domains/voice/exceptions.py new file mode 100644 index 0000000..630a780 --- /dev/null +++ b/sinch/domains/voice/exceptions.py @@ -0,0 +1,5 @@ +from sinch.core.exceptions import SinchException + + +class VoiceException(SinchException): + pass diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/models/callouts/__init__.py b/sinch/domains/voice/models/callouts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py new file mode 100644 index 0000000..500b193 --- /dev/null +++ b/sinch/domains/voice/models/callouts/requests.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchRequestBaseModel + + +@dataclass +class TextToSpeechCalloutRequest(SinchRequestBaseModel): + destination: dict + cli: str + dtmf: str + domain: str + custom: str + locale: str + text: str + prompts: str + enableAce: bool + enableDice: bool + enablePie: bool + + +@dataclass +class ConferenceCalloutRequest(SinchRequestBaseModel): + destination: dict + conference_id: str + cli: str + conferenceDtmfOptions: dict + dtmf: str + conference: str + maxDuration: int + enableAce: bool + enableDice: bool + enablePie: bool + locale: str + greeting: str + mohClass: str + custom: str + domain: str + + +@dataclass +class CustomCalloutRequest(SinchRequestBaseModel): + cli: str + destination: dict + dtmf: str + custom: str + maxDuration: int + ice: str + ace: str + pie: str diff --git a/sinch/domains/voice/models/callouts/responses.py b/sinch/domains/voice/models/callouts/responses.py new file mode 100644 index 0000000..805aa5d --- /dev/null +++ b/sinch/domains/voice/models/callouts/responses.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchBaseModel + + +@dataclass +class CalloutResponse(SinchBaseModel): + call_id: str diff --git a/sinch/domains/voice/models/calls/__init__.py b/sinch/domains/voice/models/calls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py new file mode 100644 index 0000000..31e45bb --- /dev/null +++ b/sinch/domains/voice/models/calls/requests.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchRequestBaseModel + + +@dataclass +class GetVoiceCallRequest(SinchRequestBaseModel): + callId: str + + +@dataclass +class UpdateVoiceCallRequest(SinchRequestBaseModel): + callId: str + instructions: list + action: dict diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py new file mode 100644 index 0000000..983d820 --- /dev/null +++ b/sinch/domains/voice/models/calls/responses.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchBaseModel + + +@dataclass +class GetVoiceCallResponse(SinchBaseModel): + from_: str + to: dict + domain: str + call_id: str + duration: int + status: str + result: str + reason: str + timestamp: str + custom: dict + user_rate: dict + debit: dict + + +@dataclass +class UpdateVoiceCallResponse(SinchBaseModel): + pass diff --git a/tests/conftest.py b/tests/conftest.py index a7cb74f..bd17049 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -116,7 +116,7 @@ def templates_origin(): @pytest.fixture def disable_ssl(): - return os.getenv("DISABLE_SSL") + return @pytest.fixture @@ -144,6 +144,11 @@ def verification_id(): return os.getenv("VERIFICATION_ID") +@pytest.fixture() +def call_id(): + return os.getenv("VOICE_CALL_ID") + + @pytest.fixture def app_id(): return os.getenv("APP_ID") diff --git a/tests/e2e/voice/callouts/test_callout.py b/tests/e2e/voice/callouts/test_callout.py new file mode 100644 index 0000000..bacaee4 --- /dev/null +++ b/tests/e2e/voice/callouts/test_callout.py @@ -0,0 +1,52 @@ +from sinch.domains.voice.models.callouts.responses import CalloutResponse + + +def test_tts_callout( + sinch_client_sync, + phone_number, + origin_phone_number +): + tts_callout_response = sinch_client_sync.voice.callouts.text_to_speech( + destination={ + "type": "number", + "endpoint": phone_number + }, + text="test message", + locale="en-US", + cli=origin_phone_number + ) + assert isinstance(tts_callout_response, CalloutResponse) + + +def test_custom_callout( + sinch_client_sync, + phone_number, + origin_phone_number +): + custom_callout_response = sinch_client_sync.voice.callouts.conference( + destination={ + "type": "number", + "endpoint": phone_number + }, + text="test message", + locale="en-US", + cli=origin_phone_number + ) + assert isinstance(custom_callout_response, CalloutResponse) + + +def test_conference_callout( + sinch_client_sync, + phone_number, + origin_phone_number +): + conference_callout_response = sinch_client_sync.callouts.custom( + destination={ + "type": "number", + "endpoint": phone_number + }, + text="test message", + locale="en-US", + cli=origin_phone_number + ) + assert isinstance(conference_callout_response, CalloutResponse) diff --git a/tests/e2e/voice/calls/test_get_call.py b/tests/e2e/voice/calls/test_get_call.py new file mode 100644 index 0000000..4c71d78 --- /dev/null +++ b/tests/e2e/voice/calls/test_get_call.py @@ -0,0 +1,11 @@ +from sinch.domains.voice.models.calls.responses import GetCallResponse + + +def test_get_call( + sinch_client_sync, + # call_id +): + get_call_response = sinch_client_sync.voice.calls.get( + call_id="7c5160ce-f62c-495b-8012-b0b1379a618c" + ) + assert isinstance(get_call_response, GetCallResponse) diff --git a/tests/e2e/voice/calls/test_manage_call.py b/tests/e2e/voice/calls/test_manage_call.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/voice/calls/test_update_call.py b/tests/e2e/voice/calls/test_update_call.py new file mode 100644 index 0000000..a929154 --- /dev/null +++ b/tests/e2e/voice/calls/test_update_call.py @@ -0,0 +1,23 @@ +from sinch.domains.voice.models.calls.responses import UpdateVoiceCallResponse + + +def test_update_call( + sinch_client_sync, + call_id +): + update_call_response = sinch_client_sync.voice.calls.update( + call_id=call_id, + instructions={}, + action={} + ) + assert isinstance(update_call_response, UpdateVoiceCallResponse) + + +async def test_update_call_async( + sinch_client_async, + call_id +): + update_call_response = sinch_client_async.voice.calls.update( + call_id=call_id + ) + assert isinstance(update_call_response, UpdateVoiceCallResponse) From b5a4a198cd95c0479286b50b67c3ab8eab17a7f9 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 15 Feb 2024 11:28:15 +0100 Subject: [PATCH 02/53] feat(Voice): region handling --- .../clients/sinch_client_configuration.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index 11e8a77..a06a200 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -32,8 +32,8 @@ def __init__( self.auth_origin = "auth.sinch.com" self.numbers_origin = "numbers.api.sinch.com" self.verification_origin = "verification.api.sinch.com" - self.voice_origin = "calling.api.sinch.com" - self.voice_region = "" # TODO: add region handling + self._voice_domain = "{}.api.sinch.com" + self._voice_region = None self._conversation_region = "eu" self._conversation_domain = ".conversation.api.sinch.com" self._sms_region = "us" @@ -55,6 +55,25 @@ def __init__( else: self.logger = logging.getLogger("Sinch") + def _set_voice_origin(self): + if not self._voice_region: + self.voice_origin = self._voice_domain.format("calling") + else: + self.voice_origin = self._voice_domain.format("calling-" + self._voice_region) + + def _set_voice_region(self, region): + self._voice_region = region + self._set_voice_origin() + + def _get_voice_region(self): + return self._voice_region + + voice_region = property( + _get_voice_region, + _set_voice_region, + doc="Voice Region" + ) + def _set_sms_origin(self): self.sms_origin = self._sms_domain.format(self._sms_region) From 4ba5eea04ebd6639ef15a188af350199f99238d0 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 15 Feb 2024 15:01:24 +0100 Subject: [PATCH 03/53] feat(Voice): WiP --- .../clients/sinch_client_configuration.py | 1 + sinch/domains/voice/__init__.py | 4 +- .../domains/voice/endpoints/calls/get_call.py | 2 +- .../voice/endpoints/calls/update_call.py | 6 +-- sinch/domains/voice/models/calls/requests.py | 4 +- tests/conftest.py | 20 +++++++++- tests/e2e/voice/callouts/test_callout.py | 40 +++++++++++++------ tests/e2e/voice/calls/test_get_call.py | 18 +++++++-- tests/e2e/voice/calls/test_manage_call.py | 0 tests/e2e/voice/calls/test_update_call.py | 24 +++++++++-- 10 files changed, 90 insertions(+), 29 deletions(-) delete mode 100644 tests/e2e/voice/calls/test_manage_call.py diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index a06a200..7b1e8d6 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -47,6 +47,7 @@ def __init__( self._set_conversation_origin() self._set_sms_origin() self._set_templates_origin() + self._set_voice_origin() if logger_name: self.logger = logging.getLogger(logger_name) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 8a999e7..361c27c 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -126,7 +126,7 @@ def get(self, call_id) -> GetVoiceCallResponse: return self._sinch.configuration.transport.request( GetCallEndpoint( request_data=GetVoiceCallRequest( - callId=call_id + call_id=call_id ) ) ) @@ -140,7 +140,7 @@ def update( return self._sinch.configuration.transport.request( UpdateCallEndpoint( request_data=UpdateVoiceCallRequest( - callId=call_id, + call_id=call_id, instructions=instructions, action=action ) diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py index 750757a..3edca92 100644 --- a/sinch/domains/voice/endpoints/calls/get_call.py +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -16,7 +16,7 @@ def __init__(self, request_data: GetVoiceCallRequest): def build_url(self, sinch) -> str: return self.ENDPOINT_URL.format( origin=sinch.configuration.voice_origin, - call_id=self.request_data.callId + call_id=self.request_data.call_id ) def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: diff --git a/sinch/domains/voice/endpoints/calls/update_call.py b/sinch/domains/voice/endpoints/calls/update_call.py index afd4850..e9f6c74 100644 --- a/sinch/domains/voice/endpoints/calls/update_call.py +++ b/sinch/domains/voice/endpoints/calls/update_call.py @@ -7,7 +7,7 @@ class UpdateCallEndpoint(VoiceEndpoint): ENDPOINT_URL = "{origin}/calling/v1/calls/id/{call_id}" - HTTP_METHOD = HTTPMethods.GET.value + HTTP_METHOD = HTTPMethods.PATCH.value HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value def __init__(self, request_data: UpdateVoiceCallRequest): @@ -16,11 +16,11 @@ def __init__(self, request_data: UpdateVoiceCallRequest): def build_url(self, sinch) -> str: return self.ENDPOINT_URL.format( origin=sinch.configuration.voice_origin, - call_id=self.request_data.callId + call_id=self.request_data.call_id ) def request_body(self): - self.request_data.callId = None + self.request_data.call_id = None return self.request_data.as_json() def handle_response(self, response: HTTPResponse) -> UpdateVoiceCallResponse: diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py index 31e45bb..3928b88 100644 --- a/sinch/domains/voice/models/calls/requests.py +++ b/sinch/domains/voice/models/calls/requests.py @@ -4,11 +4,11 @@ @dataclass class GetVoiceCallRequest(SinchRequestBaseModel): - callId: str + call_id: str @dataclass class UpdateVoiceCallRequest(SinchRequestBaseModel): - callId: str + call_id: str instructions: list action: dict diff --git a/tests/conftest.py b/tests/conftest.py index bd17049..10d48c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,6 +43,7 @@ def configure_origin( auth_origin, sms_origin, verification_origin, + voice_origin, disable_ssl ): if auth_origin: @@ -63,6 +64,9 @@ def configure_origin( if verification_origin: sinch_client.configuration.verification_origin = verification_origin + if voice_origin: + sinch_client.configuration.voice_origin = voice_origin + if disable_ssl: sinch_client.configuration.disable_https = True @@ -109,6 +113,11 @@ def verification_origin(): return os.getenv("VERIFICATION_ORIGIN") +@pytest.fixture +def voice_origin(): + return os.getenv("VOICE_ORIGIN") + + @pytest.fixture def templates_origin(): return os.getenv("TEMPLATES_ORIGIN") @@ -116,7 +125,7 @@ def templates_origin(): @pytest.fixture def disable_ssl(): - return + return os.getenv("DISABLE_SSL") @pytest.fixture @@ -129,6 +138,11 @@ def origin_phone_number(): return os.getenv("ORIGIN_PHONE_NUMBER") +@pytest.fixture +def voice_origin_phone_number(): + return os.getenv("VOICE_ORIGIN_PHONE_NUMBER") + + @pytest.fixture def application_key(): return os.getenv("APPLICATION_KEY") @@ -311,6 +325,7 @@ def sinch_client_sync( auth_origin, sms_origin, verification_origin, + voice_origin, disable_ssl, project_id ): @@ -328,6 +343,7 @@ def sinch_client_sync( auth_origin, sms_origin, verification_origin, + voice_origin, disable_ssl ) @@ -344,6 +360,7 @@ def sinch_client_async( auth_origin, sms_origin, verification_origin, + voice_origin, disable_ssl, project_id ): @@ -361,5 +378,6 @@ def sinch_client_async( auth_origin, sms_origin, verification_origin, + voice_origin, disable_ssl ) diff --git a/tests/e2e/voice/callouts/test_callout.py b/tests/e2e/voice/callouts/test_callout.py index bacaee4..ea1343d 100644 --- a/tests/e2e/voice/callouts/test_callout.py +++ b/tests/e2e/voice/callouts/test_callout.py @@ -1,10 +1,11 @@ +import pytest from sinch.domains.voice.models.callouts.responses import CalloutResponse def test_tts_callout( sinch_client_sync, phone_number, - origin_phone_number + voice_origin_phone_number ): tts_callout_response = sinch_client_sync.voice.callouts.text_to_speech( destination={ @@ -13,40 +14,55 @@ def test_tts_callout( }, text="test message", locale="en-US", - cli=origin_phone_number + cli=voice_origin_phone_number ) assert isinstance(tts_callout_response, CalloutResponse) -def test_custom_callout( - sinch_client_sync, +async def test_tts_callout_async( + sinch_client_async, phone_number, - origin_phone_number + voice_origin_phone_number ): - custom_callout_response = sinch_client_sync.voice.callouts.conference( + tts_callout_response = await sinch_client_async.voice.callouts.text_to_speech( destination={ "type": "number", "endpoint": phone_number }, text="test message", locale="en-US", - cli=origin_phone_number + cli=voice_origin_phone_number ) - assert isinstance(custom_callout_response, CalloutResponse) + assert isinstance(tts_callout_response, CalloutResponse) +@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") def test_conference_callout( sinch_client_sync, phone_number, - origin_phone_number + voice_origin_phone_number ): - conference_callout_response = sinch_client_sync.callouts.custom( + conference_callout_response = sinch_client_sync.voice.callouts.conference( destination={ "type": "number", "endpoint": phone_number }, - text="test message", locale="en-US", - cli=origin_phone_number + cli=voice_origin_phone_number ) assert isinstance(conference_callout_response, CalloutResponse) + + +def test_custom_callout( + sinch_client_sync, + phone_number, + voice_origin_phone_number +): + custom_callout_response = sinch_client_sync.voice.callouts.custom( + destination={ + "type": "number", + "endpoint": phone_number + }, + cli=voice_origin_phone_number + ) + assert isinstance(custom_callout_response, CalloutResponse) diff --git a/tests/e2e/voice/calls/test_get_call.py b/tests/e2e/voice/calls/test_get_call.py index 4c71d78..01a7a6f 100644 --- a/tests/e2e/voice/calls/test_get_call.py +++ b/tests/e2e/voice/calls/test_get_call.py @@ -1,11 +1,21 @@ -from sinch.domains.voice.models.calls.responses import GetCallResponse +from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse def test_get_call( sinch_client_sync, - # call_id + call_id ): get_call_response = sinch_client_sync.voice.calls.get( - call_id="7c5160ce-f62c-495b-8012-b0b1379a618c" + call_id=call_id ) - assert isinstance(get_call_response, GetCallResponse) + assert isinstance(get_call_response, GetVoiceCallResponse) + + +async def test_get_call_async( + sinch_client_async, + call_id +): + get_call_response = await sinch_client_async.voice.calls.get( + call_id=call_id + ) + assert isinstance(get_call_response, GetVoiceCallResponse) diff --git a/tests/e2e/voice/calls/test_manage_call.py b/tests/e2e/voice/calls/test_manage_call.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/e2e/voice/calls/test_update_call.py b/tests/e2e/voice/calls/test_update_call.py index a929154..84992e9 100644 --- a/tests/e2e/voice/calls/test_update_call.py +++ b/tests/e2e/voice/calls/test_update_call.py @@ -7,8 +7,15 @@ def test_update_call( ): update_call_response = sinch_client_sync.voice.calls.update( call_id=call_id, - instructions={}, - action={} + instructions=[ + { + "name": "sendDtmf", + "value": "1234#" + } + ], + action={ + "name": "hangup" + } ) assert isinstance(update_call_response, UpdateVoiceCallResponse) @@ -17,7 +24,16 @@ async def test_update_call_async( sinch_client_async, call_id ): - update_call_response = sinch_client_async.voice.calls.update( - call_id=call_id + update_call_response = await sinch_client_async.voice.calls.update( + call_id=call_id, + instructions=[ + { + "name": "sendDtmf", + "value": "1234#" + } + ], + action={ + "name": "hangup" + } ) assert isinstance(update_call_response, UpdateVoiceCallResponse) From d9d8ed23b6a8c7f50f8475a95123cc08f1985e5b Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 15 Feb 2024 15:25:21 +0100 Subject: [PATCH 04/53] test(Voice): e2e for updating call --- sinch/domains/voice/endpoints/calls/update_call.py | 2 +- tests/e2e/voice/calls/test_update_call.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sinch/domains/voice/endpoints/calls/update_call.py b/sinch/domains/voice/endpoints/calls/update_call.py index e9f6c74..dbd629a 100644 --- a/sinch/domains/voice/endpoints/calls/update_call.py +++ b/sinch/domains/voice/endpoints/calls/update_call.py @@ -20,7 +20,7 @@ def build_url(self, sinch) -> str: ) def request_body(self): - self.request_data.call_id = None + # self.request_data.call_id = None return self.request_data.as_json() def handle_response(self, response: HTTPResponse) -> UpdateVoiceCallResponse: diff --git a/tests/e2e/voice/calls/test_update_call.py b/tests/e2e/voice/calls/test_update_call.py index 84992e9..b9c4a25 100644 --- a/tests/e2e/voice/calls/test_update_call.py +++ b/tests/e2e/voice/calls/test_update_call.py @@ -1,6 +1,8 @@ +import pytest from sinch.domains.voice.models.calls.responses import UpdateVoiceCallResponse +@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") def test_update_call( sinch_client_sync, call_id @@ -20,6 +22,7 @@ def test_update_call( assert isinstance(update_call_response, UpdateVoiceCallResponse) +@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") async def test_update_call_async( sinch_client_async, call_id From f54117517aa6150ea8a19bc29e5fbff25147ef95 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 15 Feb 2024 17:19:25 +0100 Subject: [PATCH 05/53] feat(Voice): naming aligned --- sinch/domains/voice/__init__.py | 20 +++++++++---------- .../voice/endpoints/callouts/callout.py | 4 ++-- .../domains/voice/models/callouts/requests.py | 6 +++--- .../voice/models/callouts/responses.py | 2 +- tests/e2e/voice/callouts/test_callout.py | 10 +++++----- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 361c27c..c5e4f9f 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -2,11 +2,11 @@ from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint from sinch.domains.voice.enums import CalloutMethod -from sinch.domains.voice.models.callouts.responses import CalloutResponse +from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse from sinch.domains.voice.models.callouts.requests import ( - ConferenceCalloutRequest, - TextToSpeechCalloutRequest, - CustomCalloutRequest + ConferenceVoiceCalloutRequest, + TextToSpeechVoiceCalloutRequest, + CustomVoiceCalloutRequest ) from sinch.domains.voice.models.calls.requests import GetVoiceCallRequest, UpdateVoiceCallRequest from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse @@ -29,11 +29,11 @@ def text_to_speech( enable_ace: bool = None, enable_dice: bool = None, enable_pie: bool = None - ) -> CalloutResponse: + ) -> VoiceCalloutResponse: return self._sinch.configuration.transport.request( CalloutEndpoint( callout_method=CalloutMethod.TTS.value, - request_data=TextToSpeechCalloutRequest( + request_data=TextToSpeechVoiceCalloutRequest( destination=destination, cli=cli, dtmf=dtmf, @@ -66,11 +66,11 @@ def conference( moh_class: str = None, custom: str = None, domain: str = None - ) -> CalloutResponse: + ) -> VoiceCalloutResponse: return self._sinch.configuration.transport.request( CalloutEndpoint( callout_method=CalloutMethod.CONFERENCE.value, - request_data=ConferenceCalloutRequest( + request_data=ConferenceVoiceCalloutRequest( destination=destination, conference_id=conference_id, cli=cli, @@ -100,11 +100,11 @@ def custom( ice: str = None, ace: str = None, pie: str = None - ) -> CalloutResponse: + ) -> VoiceCalloutResponse: return self._sinch.configuration.transport.request( CalloutEndpoint( callout_method=CalloutMethod.CUSTOM.value, - request_data=CustomCalloutRequest( + request_data=CustomVoiceCalloutRequest( cli=cli, destination=destination, dtmf=dtmf, diff --git a/sinch/domains/voice/endpoints/callouts/callout.py b/sinch/domains/voice/endpoints/callouts/callout.py index 495b39e..0e5dec2 100644 --- a/sinch/domains/voice/endpoints/callouts/callout.py +++ b/sinch/domains/voice/endpoints/callouts/callout.py @@ -3,7 +3,7 @@ from sinch.core.models.http_response import HTTPResponse from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint from sinch.core.enums import HTTPAuthentication, HTTPMethods -from sinch.domains.voice.models.callouts.responses import CalloutResponse +from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse class CalloutEndpoint(VoiceEndpoint): @@ -38,4 +38,4 @@ def request_body(self): def handle_response(self, response: HTTPResponse): super().handle_response(response) - return CalloutResponse(call_id=response.body["callId"]) + return VoiceCalloutResponse(call_id=response.body["callId"]) diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py index 500b193..66f4f6f 100644 --- a/sinch/domains/voice/models/callouts/requests.py +++ b/sinch/domains/voice/models/callouts/requests.py @@ -3,7 +3,7 @@ @dataclass -class TextToSpeechCalloutRequest(SinchRequestBaseModel): +class TextToSpeechVoiceCalloutRequest(SinchRequestBaseModel): destination: dict cli: str dtmf: str @@ -18,7 +18,7 @@ class TextToSpeechCalloutRequest(SinchRequestBaseModel): @dataclass -class ConferenceCalloutRequest(SinchRequestBaseModel): +class ConferenceVoiceCalloutRequest(SinchRequestBaseModel): destination: dict conference_id: str cli: str @@ -37,7 +37,7 @@ class ConferenceCalloutRequest(SinchRequestBaseModel): @dataclass -class CustomCalloutRequest(SinchRequestBaseModel): +class CustomVoiceCalloutRequest(SinchRequestBaseModel): cli: str destination: dict dtmf: str diff --git a/sinch/domains/voice/models/callouts/responses.py b/sinch/domains/voice/models/callouts/responses.py index 805aa5d..1269107 100644 --- a/sinch/domains/voice/models/callouts/responses.py +++ b/sinch/domains/voice/models/callouts/responses.py @@ -3,5 +3,5 @@ @dataclass -class CalloutResponse(SinchBaseModel): +class VoiceCalloutResponse(SinchBaseModel): call_id: str diff --git a/tests/e2e/voice/callouts/test_callout.py b/tests/e2e/voice/callouts/test_callout.py index ea1343d..4f0a983 100644 --- a/tests/e2e/voice/callouts/test_callout.py +++ b/tests/e2e/voice/callouts/test_callout.py @@ -1,5 +1,5 @@ import pytest -from sinch.domains.voice.models.callouts.responses import CalloutResponse +from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse def test_tts_callout( @@ -16,7 +16,7 @@ def test_tts_callout( locale="en-US", cli=voice_origin_phone_number ) - assert isinstance(tts_callout_response, CalloutResponse) + assert isinstance(tts_callout_response, VoiceCalloutResponse) async def test_tts_callout_async( @@ -33,7 +33,7 @@ async def test_tts_callout_async( locale="en-US", cli=voice_origin_phone_number ) - assert isinstance(tts_callout_response, CalloutResponse) + assert isinstance(tts_callout_response, VoiceCalloutResponse) @pytest.mark.skip(reason="Conference endpoints have to be implemented first.") @@ -50,7 +50,7 @@ def test_conference_callout( locale="en-US", cli=voice_origin_phone_number ) - assert isinstance(conference_callout_response, CalloutResponse) + assert isinstance(conference_callout_response, VoiceCalloutResponse) def test_custom_callout( @@ -65,4 +65,4 @@ def test_custom_callout( }, cli=voice_origin_phone_number ) - assert isinstance(custom_callout_response, CalloutResponse) + assert isinstance(custom_callout_response, VoiceCalloutResponse) From df5865886a46e54f179dc5e217ebbfb2e5750bed Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 15 Feb 2024 17:36:06 +0100 Subject: [PATCH 06/53] feat(Voice): Price DTO added --- sinch/domains/voice/endpoints/calls/get_call.py | 11 +++++++++-- sinch/domains/voice/models/__init__.py | 7 +++++++ sinch/domains/voice/models/calls/responses.py | 5 +++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py index 3edca92..f416739 100644 --- a/sinch/domains/voice/endpoints/calls/get_call.py +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -3,6 +3,7 @@ from sinch.core.enums import HTTPAuthentication, HTTPMethods from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse from sinch.domains.voice.models.calls.requests import GetVoiceCallRequest +from sinch.domains.voice.models import Price class GetCallEndpoint(VoiceEndpoint): @@ -32,6 +33,12 @@ def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: reason=response.body.get("reason"), timestamp=response.body.get("timestamp"), custom=response.body.get("custom"), - user_rate=response.body.get("userRate"), - debit=response.body.get("debit") + user_rate=Price( + currency_id=response.body["userRate"]["currencyId"], + amount=response.body["userRate"]["amount"] + ), + debit=Price( + currency_id=response.body["userRate"]["currencyId"], + amount=response.body["userRate"]["amount"] + ) ) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index e69de29..e945acd 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class Price: + currency_id: str + amount: float diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py index 983d820..99e48ad 100644 --- a/sinch/domains/voice/models/calls/responses.py +++ b/sinch/domains/voice/models/calls/responses.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from sinch.core.models.base_model import SinchBaseModel +from sinch.domains.voice.models import Price @dataclass @@ -14,8 +15,8 @@ class GetVoiceCallResponse(SinchBaseModel): reason: str timestamp: str custom: dict - user_rate: dict - debit: dict + user_rate: Price + debit: Price @dataclass From c6d93dbb9e025906d57252c4202fea0e6d3259d6 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 16 Feb 2024 15:45:24 +0100 Subject: [PATCH 07/53] feat(Voice): better naming --- sinch/domains/voice/__init__.py | 2 +- sinch/domains/voice/endpoints/callouts/callout.py | 6 +++--- sinch/domains/voice/enums.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index c5e4f9f..97eb4bd 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -32,7 +32,7 @@ def text_to_speech( ) -> VoiceCalloutResponse: return self._sinch.configuration.transport.request( CalloutEndpoint( - callout_method=CalloutMethod.TTS.value, + callout_method=CalloutMethod.TEXT_TO_SPEECH.value, request_data=TextToSpeechVoiceCalloutRequest( destination=destination, cli=cli, diff --git a/sinch/domains/voice/endpoints/callouts/callout.py b/sinch/domains/voice/endpoints/callouts/callout.py index 0e5dec2..c85224e 100644 --- a/sinch/domains/voice/endpoints/callouts/callout.py +++ b/sinch/domains/voice/endpoints/callouts/callout.py @@ -22,9 +22,9 @@ def build_url(self, sinch) -> str: def request_body(self): request_data = {} - if self.callout_method == CalloutMethod.TTS.value: - request_data["method"] = CalloutMethod.TTS.value - request_data[CalloutMethod.TTS.value] = self.request_data.as_dict() + if self.callout_method == CalloutMethod.TEXT_TO_SPEECH.value: + request_data["method"] = CalloutMethod.TEXT_TO_SPEECH.value + request_data[CalloutMethod.TEXT_TO_SPEECH.value] = self.request_data.as_dict() elif self.callout_method == CalloutMethod.CUSTOM.value: request_data["method"] = CalloutMethod.CUSTOM.value diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py index 1c3091e..c16b8a6 100644 --- a/sinch/domains/voice/enums.py +++ b/sinch/domains/voice/enums.py @@ -2,6 +2,6 @@ class CalloutMethod(Enum): - TTS = "ttsCallout" + TEXT_TO_SPEECH = "ttsCallout" CUSTOM = "customCallout" CONFERENCE = "conferenceCallout" From efbcd6c61f2c5820aa8f912e03465db836b7a331 Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 19 Feb 2024 11:40:15 +0100 Subject: [PATCH 08/53] feat(Voice): Manage call endpoint added --- sinch/domains/voice/__init__.py | 35 ++++++++++++++++--- .../voice/endpoints/calls/manage_call.py | 30 ++++++++++++++++ .../voice/endpoints/calls/update_call.py | 2 +- sinch/domains/voice/models/calls/requests.py | 8 +++++ sinch/domains/voice/models/calls/responses.py | 4 +++ tests/e2e/voice/calls/magage_call.py | 23 ++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/voice/calls/magage_call.py diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 97eb4bd..590b3db 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -1,6 +1,7 @@ from sinch.domains.voice.endpoints.callouts.callout import CalloutEndpoint from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint +from sinch.domains.voice.endpoints.calls.manage_call import ManageCallEndpoint from sinch.domains.voice.enums import CalloutMethod from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse from sinch.domains.voice.models.callouts.requests import ( @@ -8,8 +9,16 @@ TextToSpeechVoiceCalloutRequest, CustomVoiceCalloutRequest ) -from sinch.domains.voice.models.calls.requests import GetVoiceCallRequest, UpdateVoiceCallRequest -from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse +from sinch.domains.voice.models.calls.requests import ( + GetVoiceCallRequest, + UpdateVoiceCallRequest, + ManageVoiceCallRequest +) +from sinch.domains.voice.models.calls.responses import ( + GetVoiceCallResponse, + UpdateVoiceCallResponse, + ManageVoiceCallResponse +) class Callouts: @@ -133,10 +142,10 @@ def get(self, call_id) -> GetVoiceCallResponse: def update( self, - call_id, + call_id: str, instructions: list, action: dict - ): + ) -> UpdateVoiceCallResponse: return self._sinch.configuration.transport.request( UpdateCallEndpoint( request_data=UpdateVoiceCallRequest( @@ -147,6 +156,24 @@ def update( ) ) + def manage( + self, + call_id: str, + call_leg: str, + instructions: list, + action: dict + ) -> ManageVoiceCallResponse: + return self._sinch.configuration.transport.request( + ManageCallEndpoint( + request_data=ManageVoiceCallRequest( + call_id=call_id, + call_leg=call_leg, + instructions=instructions, + action=action + ) + ) + ) + class VoiceBase: """ diff --git a/sinch/domains/voice/endpoints/calls/manage_call.py b/sinch/domains/voice/endpoints/calls/manage_call.py index e69de29..07630a0 100644 --- a/sinch/domains/voice/endpoints/calls/manage_call.py +++ b/sinch/domains/voice/endpoints/calls/manage_call.py @@ -0,0 +1,30 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.calls.responses import ManageVoiceCallResponse +from sinch.domains.voice.models.calls.requests import ManageVoiceCallRequest + + +class ManageCallEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/calls/id/{call_id}/leg/{call_leg}" + HTTP_METHOD = HTTPMethods.PATCH.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: ManageVoiceCallRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + call_id=self.request_data.call_id, + call_leg=self.request_data.call_leg + ) + + def request_body(self): + self.request_data.call_id = None + self.request_data.call_leg = None + return self.request_data.as_json() + + def handle_response(self, response: HTTPResponse) -> ManageVoiceCallResponse: + super().handle_response(response) + return ManageVoiceCallResponse() diff --git a/sinch/domains/voice/endpoints/calls/update_call.py b/sinch/domains/voice/endpoints/calls/update_call.py index dbd629a..e9f6c74 100644 --- a/sinch/domains/voice/endpoints/calls/update_call.py +++ b/sinch/domains/voice/endpoints/calls/update_call.py @@ -20,7 +20,7 @@ def build_url(self, sinch) -> str: ) def request_body(self): - # self.request_data.call_id = None + self.request_data.call_id = None return self.request_data.as_json() def handle_response(self, response: HTTPResponse) -> UpdateVoiceCallResponse: diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py index 3928b88..2dd79ea 100644 --- a/sinch/domains/voice/models/calls/requests.py +++ b/sinch/domains/voice/models/calls/requests.py @@ -12,3 +12,11 @@ class UpdateVoiceCallRequest(SinchRequestBaseModel): call_id: str instructions: list action: dict + + +@dataclass +class ManageVoiceCallRequest(SinchRequestBaseModel): + call_id: str + call_leg: str + instructions: list + action: dict diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py index 99e48ad..bcb3d69 100644 --- a/sinch/domains/voice/models/calls/responses.py +++ b/sinch/domains/voice/models/calls/responses.py @@ -22,3 +22,7 @@ class GetVoiceCallResponse(SinchBaseModel): @dataclass class UpdateVoiceCallResponse(SinchBaseModel): pass + + +class ManageVoiceCallResponse(SinchBaseModel): + pass diff --git a/tests/e2e/voice/calls/magage_call.py b/tests/e2e/voice/calls/magage_call.py new file mode 100644 index 0000000..1d03ca2 --- /dev/null +++ b/tests/e2e/voice/calls/magage_call.py @@ -0,0 +1,23 @@ +import pytest +from sinch.domains.voice.models.calls.responses import ManageVoiceCallResponse + + +@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") +def test_manage_call( + sinch_client_sync, + call_id +): + update_call_response = sinch_client_sync.voice.calls.manage( + call_id=call_id, + call_leg="caller", + instructions=[ + { + "name": "sendDtmf", + "value": "1234#" + } + ], + action={ + "name": "hangup" + } + ) + assert isinstance(update_call_response, ManageVoiceCallResponse) From 9e057629bc58a1579d191383dd3a1e3ee80745b3 Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 19 Feb 2024 12:31:16 +0100 Subject: [PATCH 09/53] feat(Voice): bring back regions enum --- sinch/domains/voice/enums.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py index c16b8a6..ac4c66b 100644 --- a/sinch/domains/voice/enums.py +++ b/sinch/domains/voice/enums.py @@ -5,3 +5,11 @@ class CalloutMethod(Enum): TEXT_TO_SPEECH = "ttsCallout" CUSTOM = "customCallout" CONFERENCE = "conferenceCallout" + + +class VoiceRegion(Enum): + EUROPE = "euc1" + NORTH_AMERICA = "use1" + SOUTH_AMERICA = "sae1" + SOUTH_EAST_ASIA_1 = "apse1" + SOUTH_EAST_ASIA_2 = "apse2" From be0b34d52c1f853f5ba426097792380366047088 Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 19 Feb 2024 13:27:19 +0100 Subject: [PATCH 10/53] feat(Voice): Destination dataclass added --- sinch/domains/voice/endpoints/calls/get_call.py | 7 +++++-- sinch/domains/voice/models/__init__.py | 6 ++++++ sinch/domains/voice/models/calls/responses.py | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py index f416739..1c4f0cf 100644 --- a/sinch/domains/voice/endpoints/calls/get_call.py +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -3,7 +3,7 @@ from sinch.core.enums import HTTPAuthentication, HTTPMethods from sinch.domains.voice.models.calls.responses import GetVoiceCallResponse from sinch.domains.voice.models.calls.requests import GetVoiceCallRequest -from sinch.domains.voice.models import Price +from sinch.domains.voice.models import Price, Destination class GetCallEndpoint(VoiceEndpoint): @@ -24,7 +24,10 @@ def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: super().handle_response(response) return GetVoiceCallResponse( from_=response.body.get("from"), - to=response.body.get("to"), + to=Destination( + type=response.body["to"]["type"], + endpoint=response.body["to"]["endpoint"] + ), domain=response.body.get("domain"), call_id=response.body.get("callId"), duration=response.body.get("duration"), diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index e945acd..d6a58aa 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -5,3 +5,9 @@ class Price: currency_id: str amount: float + + +@dataclass +class Destination: + type: str + endpoint: str diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py index bcb3d69..bd128c9 100644 --- a/sinch/domains/voice/models/calls/responses.py +++ b/sinch/domains/voice/models/calls/responses.py @@ -1,12 +1,12 @@ from dataclasses import dataclass from sinch.core.models.base_model import SinchBaseModel -from sinch.domains.voice.models import Price +from sinch.domains.voice.models import Price, Destination @dataclass class GetVoiceCallResponse(SinchBaseModel): from_: str - to: dict + to: Destination domain: str call_id: str duration: int From 4a12fd5cea964d81a40e07baf660944ad76bd2e2 Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 19 Feb 2024 14:08:44 +0100 Subject: [PATCH 11/53] feat(CI): Voice related env vars added --- .github/workflows/run-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0d4b61e..07c8900 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -28,6 +28,9 @@ env: VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP}} VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE}} VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE}} + VOICE_ORIGIN: ${{ secrets.VOICE_ORIGIN}} + VOICE_ORIGIN_PHONE_NUMBER: ${{ secrets.VOICE_ORIGIN_PHONE_NUMBER}} + VOICE_CALL_ID: ${{ secrets.VOICE_CALL_ID}} jobs: build: From 7ede865301f02273cab7326e931dae9e722122a6 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 29 Feb 2024 16:36:58 +0100 Subject: [PATCH 12/53] feat(Voice): apps and conferences WiP --- .github/workflows/run-tests.yml | 3 +- .../clients/sinch_client_configuration.py | 1 + sinch/domains/voice/__init__.py | 149 +++++++++++++++++- .../voice/endpoints/applications/__init__.py | 0 .../applications/get_callback_urls.py | 26 +++ .../endpoints/applications/get_numbers.py | 30 ++++ .../endpoints/applications/query_number.py | 30 ++++ .../endpoints/applications/unassign_number.py | 24 +++ .../applications/update_callbacks.py | 0 .../endpoints/applications/update_numbers.py | 0 .../voice/endpoints/calls/manage_call.py | 8 +- .../voice/endpoints/conferences/__init__.py | 0 .../endpoints/conferences/get_conference.py | 29 ++++ .../conferences/kick_all_participants.py | 24 +++ .../endpoints/conferences/kick_participant.py | 25 +++ .../conferences/manage_participant.py | 32 ++++ sinch/domains/voice/enums.py | 16 +- sinch/domains/voice/models/__init__.py | 15 ++ .../voice/models/applications/__init__.py | 0 .../voice/models/applications/requests.py | 24 +++ .../voice/models/applications/responses.py | 40 +++++ .../domains/voice/models/callouts/requests.py | 2 +- .../voice/models/conferences/__init__.py | 0 .../voice/models/conferences/requests.py | 26 +++ .../voice/models/conferences/responses.py | 24 +++ tests/conftest.py | 10 ++ .../voice/applications/test_assign_numbers.py | 0 .../applications/test_get_callback_urls.py | 8 + .../voice/applications/test_get_numbers.py | 15 ++ .../voice/applications/test_query_number.py | 8 + .../applications/test_unassign_number.py | 8 + .../applications/test_update_callback_urls.py | 0 tests/e2e/voice/callouts/test_callout.py | 6 +- .../{magage_call.py => test_magage_call.py} | 13 +- .../voice/conferences/test_get_conference.py | 17 ++ tests/e2e/voice/conferences/test_kick_all.py | 17 ++ .../conferences/test_kick_participant.py | 25 +++ .../conferences/test_manage_participant.py | 29 ++++ 38 files changed, 670 insertions(+), 14 deletions(-) create mode 100644 sinch/domains/voice/endpoints/applications/__init__.py create mode 100644 sinch/domains/voice/endpoints/applications/get_callback_urls.py create mode 100644 sinch/domains/voice/endpoints/applications/get_numbers.py create mode 100644 sinch/domains/voice/endpoints/applications/query_number.py create mode 100644 sinch/domains/voice/endpoints/applications/unassign_number.py create mode 100644 sinch/domains/voice/endpoints/applications/update_callbacks.py create mode 100644 sinch/domains/voice/endpoints/applications/update_numbers.py create mode 100644 sinch/domains/voice/endpoints/conferences/__init__.py create mode 100644 sinch/domains/voice/endpoints/conferences/get_conference.py create mode 100644 sinch/domains/voice/endpoints/conferences/kick_all_participants.py create mode 100644 sinch/domains/voice/endpoints/conferences/kick_participant.py create mode 100644 sinch/domains/voice/endpoints/conferences/manage_participant.py create mode 100644 sinch/domains/voice/models/applications/__init__.py create mode 100644 sinch/domains/voice/models/applications/requests.py create mode 100644 sinch/domains/voice/models/applications/responses.py create mode 100644 sinch/domains/voice/models/conferences/__init__.py create mode 100644 sinch/domains/voice/models/conferences/requests.py create mode 100644 sinch/domains/voice/models/conferences/responses.py create mode 100644 tests/e2e/voice/applications/test_assign_numbers.py create mode 100644 tests/e2e/voice/applications/test_get_callback_urls.py create mode 100644 tests/e2e/voice/applications/test_get_numbers.py create mode 100644 tests/e2e/voice/applications/test_query_number.py create mode 100644 tests/e2e/voice/applications/test_unassign_number.py create mode 100644 tests/e2e/voice/applications/test_update_callback_urls.py rename tests/e2e/voice/calls/{magage_call.py => test_magage_call.py} (79%) create mode 100644 tests/e2e/voice/conferences/test_get_conference.py create mode 100644 tests/e2e/voice/conferences/test_kick_all.py create mode 100644 tests/e2e/voice/conferences/test_kick_participant.py create mode 100644 tests/e2e/voice/conferences/test_manage_participant.py diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 07c8900..1f69034 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -30,7 +30,8 @@ env: VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE}} VOICE_ORIGIN: ${{ secrets.VOICE_ORIGIN}} VOICE_ORIGIN_PHONE_NUMBER: ${{ secrets.VOICE_ORIGIN_PHONE_NUMBER}} - VOICE_CALL_ID: ${{ secrets.VOICE_CALL_ID}} + CONFERENCE_ID: ${{ secrets.CONFERENCE_ID} + CONFERENCE_CALL_ID: ${{ secrets.CONFERENCE_CALL_ID} jobs: build: diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index 7b1e8d6..9459b25 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -32,6 +32,7 @@ def __init__( self.auth_origin = "auth.sinch.com" self.numbers_origin = "numbers.api.sinch.com" self.verification_origin = "verification.api.sinch.com" + self.voice_applications_origin = "callingapi.sinch.com" self._voice_domain = "{}.api.sinch.com" self._voice_region = None self._conversation_region = "eu" diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 590b3db..c3116bb 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -2,6 +2,17 @@ from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint from sinch.domains.voice.endpoints.calls.manage_call import ManageCallEndpoint + +from sinch.domains.voice.endpoints.applications.get_numbers import GetVoiceNumbersEndpoint +from sinch.domains.voice.endpoints.applications.query_number import QueryVoiceNumberEndpoint +from sinch.domains.voice.endpoints.applications.get_callback_urls import GetVoiceCallbacksEndpoint +from sinch.domains.voice.endpoints.applications.unassign_number import UnAssignVoiceNumberEndpoint + +from sinch.domains.voice.endpoints.conferences.kick_participant import KickParticipantConferenceEndpoint +from sinch.domains.voice.endpoints.conferences.kick_all_participants import KickAllConferenceEndpoint +from sinch.domains.voice.endpoints.conferences.manage_participant import ManageParticipantConferenceEndpoint +from sinch.domains.voice.endpoints.conferences.get_conference import GetConferenceEndpoint + from sinch.domains.voice.enums import CalloutMethod from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse from sinch.domains.voice.models.callouts.requests import ( @@ -19,6 +30,33 @@ UpdateVoiceCallResponse, ManageVoiceCallResponse ) +from sinch.domains.voice.models.conferences.requests import ( + GetVoiceConferenceRequest, + KickAllVoiceConferenceRequest, + KickParticipantVoiceConferenceRequest, + ManageParticipantVoiceConferenceRequest +) +from sinch.domains.voice.models.conferences.responses import ( + GetVoiceConferenceResponse, + KickAllVoiceConferenceResponse, + ManageParticipantVoiceConferenceResponse, + KickParticipantVoiceConferenceResponse +) +from sinch.domains.voice.models.applications.requests import ( + GetNumbersVoiceApplicationRequest, + AssignNumbersVoiceApplicationRequest, + UnassignNumbersVoiceApplicationRequest, + GetCallbackUrlsVoiceApplicationRequest, + QueryNumberVoiceApplicationRequest +) +from sinch.domains.voice.models.applications.responses import ( + GetNumbersVoiceApplicationResponse, + AssignNumbersVoiceApplicationResponse, + UnassignNumbersVoiceApplicationResponse, + KickParticipantVoiceConferenceResponse, + GetCallbackUrlsVoiceApplicationResponse, + QueryNumberVoiceApplicationResponse +) class Callouts: @@ -81,7 +119,7 @@ def conference( callout_method=CalloutMethod.CONFERENCE.value, request_data=ConferenceVoiceCalloutRequest( destination=destination, - conference_id=conference_id, + conferenceId=conference_id, cli=cli, conferenceDtmfOptions=conference_dtmf_options, dtmf=dtmf, @@ -175,6 +213,111 @@ def manage( ) +class Conferences: + def __init__(self, sinch): + self._sinch = sinch + + def get(self, conference_id: str) -> GetVoiceConferenceResponse: + return self._sinch.configuration.transport.request( + GetConferenceEndpoint( + request_data=GetVoiceConferenceRequest( + conference_id=conference_id + ) + ) + ) + + def kick_all(self, conference_id: str) -> KickAllVoiceConferenceResponse: + return self._sinch.configuration.transport.request( + KickAllConferenceEndpoint( + request_data=KickAllVoiceConferenceRequest( + conference_id=conference_id + ) + ) + ) + + def kick_participant( + self, + call_id: str, + conference_id: str, + ) -> KickParticipantVoiceConferenceResponse: + return self._sinch.configuration.transport.request( + KickParticipantConferenceEndpoint( + request_data=KickParticipantVoiceConferenceRequest( + call_id=call_id, + conference_id=conference_id + ) + ) + ) + + def manage_participant( + self, + call_id: str, + conference_id: str, + command: str, + moh: str = None + ) -> ManageParticipantVoiceConferenceResponse: + return self._sinch.configuration.transport.request( + ManageParticipantConferenceEndpoint( + request_data=ManageParticipantVoiceConferenceRequest( + call_id=call_id, + conference_id=conference_id, + command=command, + moh=moh + ) + ) + ) + + +class Applications: + def __init__(self, sinch): + self._sinch = sinch + + def get_numbers(self) -> GetNumbersVoiceApplicationResponse: + return self._sinch.configuration.transport.request( + GetVoiceNumbersEndpoint() + ) + + def assign_numbers(self, call_id) -> AssignNumbersVoiceApplicationResponse: + return self._sinch.configuration.transport.request( + AssignVoiceNumbersEndxpoint( + request_data=AssignNumbersVoiceApplicationRequest( + call_id=call_id + ) + ) + ) + + def unassign_number( + self, + number: str, + application_key: str =None, + capability: str = None + + ) -> UnassignNumbersVoiceApplicationResponse: + return self._sinch.configuration.transport.request( + UnAssignVoiceNumberEndpoint( + request_data=UnassignNumbersVoiceApplicationRequest( + number=number, + application_key=application_key, + capability=capability + ) + ) + ) + + def get_callback_urls(self) -> GetCallbackUrlsVoiceApplicationResponse: + return self._sinch.configuration.transport.request( + GetVoiceCallbacksEndpoint() + ) + + def query_number(self, number) -> QueryNumberVoiceApplicationResponse: + return self._sinch.configuration.transport.request( + QueryVoiceNumberEndpoint( + request_data=QueryNumberVoiceApplicationRequest( + number=number + ) + ) + ) + + class VoiceBase: """ Documentation for the Voice API: https://developers.sinch.com/docs/voice/ @@ -193,6 +336,8 @@ def __init__(self, sinch): super().__init__(sinch) self.callouts = Callouts(self._sinch) self.calls = Calls(self._sinch) + self.conferences = Conferences(self._sinch) + self.applications = Applications(self._sinch) class VoiceAsync(VoiceBase): @@ -205,3 +350,5 @@ def __init__(self, sinch): super().__init__(sinch) self.callouts = Callouts(self._sinch) self.calls = Calls(self._sinch) + self.conferences = Conferences(self._sinch) + self.applications = Applications(self._sinch) diff --git a/sinch/domains/voice/endpoints/applications/__init__.py b/sinch/domains/voice/endpoints/applications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/applications/get_callback_urls.py b/sinch/domains/voice/endpoints/applications/get_callback_urls.py new file mode 100644 index 0000000..7a491a5 --- /dev/null +++ b/sinch/domains/voice/endpoints/applications/get_callback_urls.py @@ -0,0 +1,26 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.applications.responses import GetCallbackUrlsVoiceApplicationResponse + + +class GetVoiceCallbacksEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/v1/configuration/callbacks/applications/{application_key}" + HTTP_METHOD = HTTPMethods.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self): + pass + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_applications_origin, + application_key=sinch.configuration.application_key + ) + + def handle_response(self, response: HTTPResponse) -> GetCallbackUrlsVoiceApplicationResponse: + super().handle_response(response) + return GetCallbackUrlsVoiceApplicationResponse( + primary=response.body["url"]["primary"], + fallback=response.body["url"]["fallback"] + ) diff --git a/sinch/domains/voice/endpoints/applications/get_numbers.py b/sinch/domains/voice/endpoints/applications/get_numbers.py new file mode 100644 index 0000000..78e557f --- /dev/null +++ b/sinch/domains/voice/endpoints/applications/get_numbers.py @@ -0,0 +1,30 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.domains.voice.models import ApplicationNumber +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.applications.responses import GetNumbersVoiceApplicationResponse + + +class GetVoiceNumbersEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/v1/configuration/numbers" + HTTP_METHOD = HTTPMethods.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self): + pass + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_applications_origin + ) + + def handle_response(self, response: HTTPResponse) -> GetNumbersVoiceApplicationResponse: + super().handle_response(response) + return GetNumbersVoiceApplicationResponse( + numbers=[ + ApplicationNumber( + number=number["number"], + capability=number["capability"] + ) for number in response.body["numbers"] + ] + ) diff --git a/sinch/domains/voice/endpoints/applications/query_number.py b/sinch/domains/voice/endpoints/applications/query_number.py new file mode 100644 index 0000000..d8c6f3a --- /dev/null +++ b/sinch/domains/voice/endpoints/applications/query_number.py @@ -0,0 +1,30 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.applications.requests import QueryNumberVoiceApplicationRequest +from sinch.domains.voice.models.applications.responses import QueryNumberVoiceApplicationResponse + + +class QueryVoiceNumberEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/v1/calling/query/number/{number}" + HTTP_METHOD = HTTPMethods.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: QueryNumberVoiceApplicationRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_applications_origin, + number=self.request_data.number + ) + + def handle_response(self, response: HTTPResponse) -> QueryNumberVoiceApplicationResponse: + super().handle_response(response) + return QueryNumberVoiceApplicationResponse( + country_id=response.body["countryId"], + number_type=response.body["numberType"], + normalized_number=response.body["normalizedNumber"], + restricted=response.body["restricted"], + rate=response.body["rate"] + ) diff --git a/sinch/domains/voice/endpoints/applications/unassign_number.py b/sinch/domains/voice/endpoints/applications/unassign_number.py new file mode 100644 index 0000000..f62958f --- /dev/null +++ b/sinch/domains/voice/endpoints/applications/unassign_number.py @@ -0,0 +1,24 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.applications.requests import UnassignNumbersVoiceApplicationRequest +from sinch.domains.voice.models.applications.responses import UnassignNumbersVoiceApplicationResponse + + +class UnAssignVoiceNumberEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/v1/configuration/numbers" + HTTP_METHOD = HTTPMethods.DELETE.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: UnassignNumbersVoiceApplicationRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_applications_origin, + number=self.request_data.number + ) + + def handle_response(self, response: HTTPResponse) -> UnassignNumbersVoiceApplicationResponse: + super().handle_response(response) + return UnassignNumbersVoiceApplicationResponse() diff --git a/sinch/domains/voice/endpoints/applications/update_callbacks.py b/sinch/domains/voice/endpoints/applications/update_callbacks.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/applications/update_numbers.py b/sinch/domains/voice/endpoints/applications/update_numbers.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/calls/manage_call.py b/sinch/domains/voice/endpoints/calls/manage_call.py index 07630a0..b734f30 100644 --- a/sinch/domains/voice/endpoints/calls/manage_call.py +++ b/sinch/domains/voice/endpoints/calls/manage_call.py @@ -1,3 +1,4 @@ +from copy import deepcopy from sinch.core.models.http_response import HTTPResponse from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint from sinch.core.enums import HTTPAuthentication, HTTPMethods @@ -21,9 +22,10 @@ def build_url(self, sinch) -> str: ) def request_body(self): - self.request_data.call_id = None - self.request_data.call_leg = None - return self.request_data.as_json() + request_data = deepcopy(self.request_data) + request_data.call_leg = None + request_data.call_id = None + return request_data.as_json() def handle_response(self, response: HTTPResponse) -> ManageVoiceCallResponse: super().handle_response(response) diff --git a/sinch/domains/voice/endpoints/conferences/__init__.py b/sinch/domains/voice/endpoints/conferences/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/endpoints/conferences/get_conference.py b/sinch/domains/voice/endpoints/conferences/get_conference.py new file mode 100644 index 0000000..8f2ec10 --- /dev/null +++ b/sinch/domains/voice/endpoints/conferences/get_conference.py @@ -0,0 +1,29 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.conferences.responses import GetVoiceConferenceResponse +from sinch.domains.voice.models.conferences.requests import GetVoiceConferenceRequest +from sinch.domains.voice.models import ConferenceParticipant + + +class GetConferenceEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/conferences/id/{conference_id}" + HTTP_METHOD = HTTPMethods.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: GetVoiceConferenceRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + conference_id=self.request_data.conference_id + ) + + def handle_response(self, response: HTTPResponse) -> GetVoiceConferenceResponse: + super().handle_response(response) + return GetVoiceConferenceResponse( + participants=[ + ConferenceParticipant(**participant) for participant in response.body["participants"] + ] + ) diff --git a/sinch/domains/voice/endpoints/conferences/kick_all_participants.py b/sinch/domains/voice/endpoints/conferences/kick_all_participants.py new file mode 100644 index 0000000..7f3e5dc --- /dev/null +++ b/sinch/domains/voice/endpoints/conferences/kick_all_participants.py @@ -0,0 +1,24 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.conferences.responses import KickAllVoiceConferenceResponse +from sinch.domains.voice.models.conferences.requests import KickAllVoiceConferenceRequest + + +class KickAllConferenceEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/conferences/id/{conference_id}" + HTTP_METHOD = HTTPMethods.DELETE.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: KickAllVoiceConferenceRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + conference_id=self.request_data.conference_id + ) + + def handle_response(self, response: HTTPResponse) -> KickAllVoiceConferenceResponse: + super().handle_response(response) + return KickAllVoiceConferenceResponse() diff --git a/sinch/domains/voice/endpoints/conferences/kick_participant.py b/sinch/domains/voice/endpoints/conferences/kick_participant.py new file mode 100644 index 0000000..2dd0b4e --- /dev/null +++ b/sinch/domains/voice/endpoints/conferences/kick_participant.py @@ -0,0 +1,25 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.conferences.responses import KickParticipantVoiceConferenceResponse +from sinch.domains.voice.models.conferences.requests import KickParticipantVoiceConferenceRequest + + +class KickParticipantConferenceEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/conferences/id/{conference_id}/{call_id}" + HTTP_METHOD = HTTPMethods.DELETE.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: KickParticipantVoiceConferenceRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + conference_id=self.request_data.conference_id, + call_id=self.request_data.call_id + ) + + def handle_response(self, response: HTTPResponse) -> KickParticipantVoiceConferenceResponse: + super().handle_response(response) + return KickParticipantVoiceConferenceResponse() diff --git a/sinch/domains/voice/endpoints/conferences/manage_participant.py b/sinch/domains/voice/endpoints/conferences/manage_participant.py new file mode 100644 index 0000000..593f9e3 --- /dev/null +++ b/sinch/domains/voice/endpoints/conferences/manage_participant.py @@ -0,0 +1,32 @@ +from copy import deepcopy +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.conferences.responses import ManageParticipantVoiceConferenceResponse +from sinch.domains.voice.models.conferences.requests import ManageParticipantVoiceConferenceRequest + + +class ManageParticipantConferenceEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/calling/v1/conferences/id/{conference_id}/{call_id}" + HTTP_METHOD = HTTPMethods.PATCH.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: ManageParticipantVoiceConferenceRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_origin, + conference_id=self.request_data.conference_id, + call_id=self.request_data.call_id + ) + + def request_body(self): + request_data = deepcopy(self.request_data) + request_data.conference_id = None + request_data.call_id = None + return request_data.as_json() + + def handle_response(self, response: HTTPResponse) -> ManageParticipantVoiceConferenceResponse: + super().handle_response(response) + return ManageParticipantVoiceConferenceResponse() diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py index ac4c66b..407a11c 100644 --- a/sinch/domains/voice/enums.py +++ b/sinch/domains/voice/enums.py @@ -7,9 +7,23 @@ class CalloutMethod(Enum): CONFERENCE = "conferenceCallout" -class VoiceRegion(Enum): +class Region(Enum): EUROPE = "euc1" NORTH_AMERICA = "use1" SOUTH_AMERICA = "sae1" SOUTH_EAST_ASIA_1 = "apse1" SOUTH_EAST_ASIA_2 = "apse2" + + +class ConferenceCommand(Enum): + MUTE = "mute" + UNMUTE = "unmute" + ONHOLD = "onhold" + RESUME = "resume" + + +class ConferenceMusicOnHold(Enum): + RING = "ring" + MUSIC_1 = "music1" + MUSIC_2 = "music2" + MUSIC_3 = "music3" diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index d6a58aa..f5758fb 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -11,3 +11,18 @@ class Price: class Destination: type: str endpoint: str + + +@dataclass +class ConferenceParticipant: + cli: str + id: str + duration: int + muted: bool + onhold: bool + + +@dataclass +class ApplicationNumber: + number: str + capability: str diff --git a/sinch/domains/voice/models/applications/__init__.py b/sinch/domains/voice/models/applications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/models/applications/requests.py b/sinch/domains/voice/models/applications/requests.py new file mode 100644 index 0000000..a40ae1e --- /dev/null +++ b/sinch/domains/voice/models/applications/requests.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchRequestBaseModel + + +@dataclass +class GetNumbersVoiceApplicationRequest(SinchRequestBaseModel): + pass + + +@dataclass +class AssignNumbersVoiceApplicationRequest(SinchRequestBaseModel): + pass + + +@dataclass +class UnassignNumbersVoiceApplicationRequest(SinchRequestBaseModel): + number: str + application_key: str + capability: str + + +@dataclass +class QueryNumberVoiceApplicationRequest(SinchRequestBaseModel): + number: str diff --git a/sinch/domains/voice/models/applications/responses.py b/sinch/domains/voice/models/applications/responses.py new file mode 100644 index 0000000..769164a --- /dev/null +++ b/sinch/domains/voice/models/applications/responses.py @@ -0,0 +1,40 @@ +from typing import List + +from dataclasses import dataclass +from sinch.core.models.base_model import SinchBaseModel +from sinch.domains.voice.models import ApplicationNumber, Price + + +@dataclass +class GetNumbersVoiceApplicationResponse(SinchBaseModel): + numbers: List[ApplicationNumber] + + +@dataclass +class AssignNumbersVoiceApplicationResponse(SinchBaseModel): + pass + + +@dataclass +class UnassignNumbersVoiceApplicationResponse(SinchBaseModel): + pass + + +@dataclass +class KickParticipantVoiceConferenceResponse(SinchBaseModel): + pass + + +@dataclass +class GetCallbackUrlsVoiceApplicationResponse(SinchBaseModel): + primary: str + fallback: str + + +@dataclass +class QueryNumberVoiceApplicationResponse(SinchBaseModel): + country_id: str + number_type: str + normalized_number: str + restricted: bool + rate: Price diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py index 66f4f6f..e24934b 100644 --- a/sinch/domains/voice/models/callouts/requests.py +++ b/sinch/domains/voice/models/callouts/requests.py @@ -20,7 +20,7 @@ class TextToSpeechVoiceCalloutRequest(SinchRequestBaseModel): @dataclass class ConferenceVoiceCalloutRequest(SinchRequestBaseModel): destination: dict - conference_id: str + conferenceId: str cli: str conferenceDtmfOptions: dict dtmf: str diff --git a/sinch/domains/voice/models/conferences/__init__.py b/sinch/domains/voice/models/conferences/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/models/conferences/requests.py b/sinch/domains/voice/models/conferences/requests.py new file mode 100644 index 0000000..aca3f0b --- /dev/null +++ b/sinch/domains/voice/models/conferences/requests.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchRequestBaseModel + + +@dataclass +class GetVoiceConferenceRequest(SinchRequestBaseModel): + conference_id: str + + +@dataclass +class KickAllVoiceConferenceRequest(SinchRequestBaseModel): + conference_id: str + + +@dataclass +class ManageParticipantVoiceConferenceRequest(SinchRequestBaseModel): + conference_id: str + call_id: str + command: str + moh: str + + +@dataclass +class KickParticipantVoiceConferenceRequest(SinchRequestBaseModel): + conference_id: str + call_id: str diff --git a/sinch/domains/voice/models/conferences/responses.py b/sinch/domains/voice/models/conferences/responses.py new file mode 100644 index 0000000..ad0c042 --- /dev/null +++ b/sinch/domains/voice/models/conferences/responses.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from typing import List +from sinch.core.models.base_model import SinchBaseModel +from sinch.domains.voice.models import ConferenceParticipant + + +@dataclass +class GetVoiceConferenceResponse(SinchBaseModel): + participants: List[ConferenceParticipant] + + +@dataclass +class KickAllVoiceConferenceResponse(SinchBaseModel): + pass + + +@dataclass +class ManageParticipantVoiceConferenceResponse(SinchBaseModel): + pass + + +@dataclass +class KickParticipantVoiceConferenceResponse(SinchBaseModel): + pass diff --git a/tests/conftest.py b/tests/conftest.py index 10d48c3..2820ce1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,6 +163,16 @@ def call_id(): return os.getenv("VOICE_CALL_ID") +@pytest.fixture() +def conference_id(): + return os.getenv("CONFERENCE_ID") + + +@pytest.fixture() +def conference_call_id(): + return os.getenv("CONFERENCE_CALL_ID") + + @pytest.fixture def app_id(): return os.getenv("APP_ID") diff --git a/tests/e2e/voice/applications/test_assign_numbers.py b/tests/e2e/voice/applications/test_assign_numbers.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/voice/applications/test_get_callback_urls.py b/tests/e2e/voice/applications/test_get_callback_urls.py new file mode 100644 index 0000000..175a147 --- /dev/null +++ b/tests/e2e/voice/applications/test_get_callback_urls.py @@ -0,0 +1,8 @@ +from sinch.domains.voice.models.applications.responses import GetCallbackUrlsVoiceApplicationResponse + + +def test_get_application_callback_urls_call( + sinch_client_sync +): + callback_urls_response = sinch_client_sync.voice.applications.get_callback_urls() + assert isinstance(callback_urls_response, GetCallbackUrlsVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_get_numbers.py b/tests/e2e/voice/applications/test_get_numbers.py new file mode 100644 index 0000000..ab01f80 --- /dev/null +++ b/tests/e2e/voice/applications/test_get_numbers.py @@ -0,0 +1,15 @@ +from sinch.domains.voice.models.applications.responses import GetNumbersVoiceApplicationResponse + + +def test_get_application_numbers_call( + sinch_client_sync +): + get_voice_numbers_response = sinch_client_sync.voice.applications.get_numbers() + assert isinstance(get_voice_numbers_response, GetNumbersVoiceApplicationResponse) + + +async def test_get_application_numbers_call_async( # TODO: fix that + sinch_client_async +): + get_voice_numbers_response = await sinch_client_async.voice.applications.get_numbers() + assert isinstance(get_voice_numbers_response, GetNumbersVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_query_number.py b/tests/e2e/voice/applications/test_query_number.py new file mode 100644 index 0000000..c08aa21 --- /dev/null +++ b/tests/e2e/voice/applications/test_query_number.py @@ -0,0 +1,8 @@ +from sinch.domains.voice.models.applications.responses import QueryNumberVoiceApplicationResponse + + +def test_query_application_numbers_call( + sinch_client_sync +): + query_voice_numbers_response = sinch_client_sync.voice.applications.query_number() + assert isinstance(query_voice_numbers_response, QueryNumberVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_unassign_number.py b/tests/e2e/voice/applications/test_unassign_number.py new file mode 100644 index 0000000..4aaef34 --- /dev/null +++ b/tests/e2e/voice/applications/test_unassign_number.py @@ -0,0 +1,8 @@ +from sinch.domains.voice.models.applications.responses import UnassignNumbersVoiceApplicationResponse + + +def test_unassign_application_number( + sinch_client_sync +): + unassign_number_response = sinch_client_sync.voice.applications.get_callback_urls() + assert isinstance(unassign_number_response, UnassignNumbersVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_update_callback_urls.py b/tests/e2e/voice/applications/test_update_callback_urls.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/voice/callouts/test_callout.py b/tests/e2e/voice/callouts/test_callout.py index 4f0a983..605b482 100644 --- a/tests/e2e/voice/callouts/test_callout.py +++ b/tests/e2e/voice/callouts/test_callout.py @@ -1,4 +1,3 @@ -import pytest from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse @@ -36,13 +35,14 @@ async def test_tts_callout_async( assert isinstance(tts_callout_response, VoiceCalloutResponse) -@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") def test_conference_callout( sinch_client_sync, phone_number, - voice_origin_phone_number + voice_origin_phone_number, + conference_id ): conference_callout_response = sinch_client_sync.voice.callouts.conference( + conference_id=conference_id, destination={ "type": "number", "endpoint": phone_number diff --git a/tests/e2e/voice/calls/magage_call.py b/tests/e2e/voice/calls/test_magage_call.py similarity index 79% rename from tests/e2e/voice/calls/magage_call.py rename to tests/e2e/voice/calls/test_magage_call.py index 1d03ca2..4224a84 100644 --- a/tests/e2e/voice/calls/magage_call.py +++ b/tests/e2e/voice/calls/test_magage_call.py @@ -1,14 +1,12 @@ -import pytest from sinch.domains.voice.models.calls.responses import ManageVoiceCallResponse -@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") def test_manage_call( sinch_client_sync, - call_id + conference_call_id ): update_call_response = sinch_client_sync.voice.calls.manage( - call_id=call_id, + call_id=conference_call_id, call_leg="caller", instructions=[ { @@ -21,3 +19,10 @@ def test_manage_call( } ) assert isinstance(update_call_response, ManageVoiceCallResponse) + + +def test_manage_call_async( + sinch_client_async, + call_id +): + pass diff --git a/tests/e2e/voice/conferences/test_get_conference.py b/tests/e2e/voice/conferences/test_get_conference.py new file mode 100644 index 0000000..95df515 --- /dev/null +++ b/tests/e2e/voice/conferences/test_get_conference.py @@ -0,0 +1,17 @@ +from sinch.domains.voice.models.conferences.responses import GetVoiceConferenceResponse + + +def test_get_conference( + sinch_client_sync, + conference_id +): + get_conference_response = sinch_client_sync.voice.conferences.get(conference_id) + assert isinstance(get_conference_response, GetVoiceConferenceResponse) + + +async def test_get_conference_async( + sinch_client_async, + conference_id +): + get_conference_response = await sinch_client_async.voice.conferences.get(conference_id) + assert isinstance(get_conference_response, GetVoiceConferenceResponse) diff --git a/tests/e2e/voice/conferences/test_kick_all.py b/tests/e2e/voice/conferences/test_kick_all.py new file mode 100644 index 0000000..b063a04 --- /dev/null +++ b/tests/e2e/voice/conferences/test_kick_all.py @@ -0,0 +1,17 @@ +from sinch.domains.voice.models.conferences.responses import KickAllVoiceConferenceResponse + + +def test_kick_all_conference_participants( + sinch_client_sync, + conference_id +): + kick_all_participants_response = sinch_client_sync.voice.conferences.kick_all(conference_id) + assert isinstance(kick_all_participants_response, KickAllVoiceConferenceResponse) + + +async def test_kick_all_conference_participants_async( + sinch_client_async, + conference_id +): + kick_all_participants_response = await sinch_client_async.voice.conferences.kick_all(conference_id) + assert isinstance(kick_all_participants_response, KickAllVoiceConferenceResponse) diff --git a/tests/e2e/voice/conferences/test_kick_participant.py b/tests/e2e/voice/conferences/test_kick_participant.py new file mode 100644 index 0000000..076e364 --- /dev/null +++ b/tests/e2e/voice/conferences/test_kick_participant.py @@ -0,0 +1,25 @@ +from sinch.domains.voice.models.conferences.responses import KickParticipantVoiceConferenceResponse + + +def test_kick_conference_participant( + sinch_client_sync, + conference_id, + conference_call_id +): + kick_participant_response = sinch_client_sync.voice.conferences.kick_participant( + conference_id=conference_id, + call_id=conference_call_id + ) + assert isinstance(kick_participant_response, KickParticipantVoiceConferenceResponse) + + +async def test_kick_conference_participant_async( + sinch_client_async, + conference_id, + conference_call_id +): + kick_participant_response = await sinch_client_async.voice.conferences.kick_participant( + conference_id=conference_id, + call_id=conference_call_id + ) + assert isinstance(kick_participant_response, KickParticipantVoiceConferenceResponse) diff --git a/tests/e2e/voice/conferences/test_manage_participant.py b/tests/e2e/voice/conferences/test_manage_participant.py new file mode 100644 index 0000000..7cbcdf0 --- /dev/null +++ b/tests/e2e/voice/conferences/test_manage_participant.py @@ -0,0 +1,29 @@ +from sinch.domains.voice.models.conferences.responses import ManageParticipantVoiceConferenceResponse +from sinch.domains.voice.enums import ConferenceCommand + + +def test_manage_conference_conference( + sinch_client_sync, + conference_id, + conference_call_id +): + manage_participant_response = sinch_client_sync.voice.conferences.manage_participant( + conference_id=conference_id, + call_id=conference_call_id, + command=ConferenceCommand.MUTE.value + ) + assert isinstance(manage_participant_response, ManageParticipantVoiceConferenceResponse) + + +async def test_manage_conference_conference_async( + sinch_client_async, + conference_id, + conference_call_id +): + manage_participant_response = await sinch_client_async.voice.conferences.manage_participant( + conference_id=conference_id, + call_id=conference_call_id, + command=ConferenceCommand.MUTE.value + ) + assert isinstance(manage_participant_response, ManageParticipantVoiceConferenceResponse) + From 31c65d309a4abef41cf2f1151f87923064c5a56f Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 1 Mar 2024 07:43:25 +0100 Subject: [PATCH 13/53] feat(Voice): WiP --- sinch/domains/voice/__init__.py | 71 ++++++++++++++----- .../endpoints/applications/assign_numbers.py | 27 +++++++ .../applications/get_callback_urls.py | 11 +-- .../endpoints/applications/query_number.py | 10 +-- .../endpoints/applications/unassign_number.py | 18 ++++- .../applications/update_callbacks.py | 38 ++++++++++ .../endpoints/applications/update_numbers.py | 0 .../voice/endpoints/callouts/callout.py | 14 ++++ sinch/domains/voice/models/__init__.py | 1 + .../voice/models/applications/requests.py | 22 ++++-- .../voice/models/applications/responses.py | 2 +- .../domains/voice/models/callouts/requests.py | 20 ++++-- sinch/domains/voice/models/calls/requests.py | 9 ++- tests/conftest.py | 1 + .../voice/applications/test_assign_numbers.py | 21 ++++++ .../applications/test_get_callback_urls.py | 19 ++++- .../voice/applications/test_get_numbers.py | 4 +- .../voice/applications/test_query_number.py | 17 ++++- .../applications/test_unassign_number.py | 15 +++- .../applications/test_update_callback_urls.py | 25 +++++++ 20 files changed, 294 insertions(+), 51 deletions(-) create mode 100644 sinch/domains/voice/endpoints/applications/assign_numbers.py delete mode 100644 sinch/domains/voice/endpoints/applications/update_numbers.py diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index c3116bb..4429c86 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -1,3 +1,4 @@ +from typing import List from sinch.domains.voice.endpoints.callouts.callout import CalloutEndpoint from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint @@ -7,6 +8,8 @@ from sinch.domains.voice.endpoints.applications.query_number import QueryVoiceNumberEndpoint from sinch.domains.voice.endpoints.applications.get_callback_urls import GetVoiceCallbacksEndpoint from sinch.domains.voice.endpoints.applications.unassign_number import UnAssignVoiceNumberEndpoint +from sinch.domains.voice.endpoints.applications.assign_numbers import AssignVoiceNumbersEndpoint +from sinch.domains.voice.endpoints.applications.update_callbacks import UpdateVoiceCallbacksEndpoint from sinch.domains.voice.endpoints.conferences.kick_participant import KickParticipantConferenceEndpoint from sinch.domains.voice.endpoints.conferences.kick_all_participants import KickAllConferenceEndpoint @@ -18,12 +21,15 @@ from sinch.domains.voice.models.callouts.requests import ( ConferenceVoiceCalloutRequest, TextToSpeechVoiceCalloutRequest, - CustomVoiceCalloutRequest + CustomVoiceCalloutRequest, + Destination, + ConferenceDTMFOptions ) from sinch.domains.voice.models.calls.requests import ( GetVoiceCallRequest, UpdateVoiceCallRequest, - ManageVoiceCallRequest + ManageVoiceCallRequest, + Action ) from sinch.domains.voice.models.calls.responses import ( GetVoiceCallResponse, @@ -43,17 +49,16 @@ KickParticipantVoiceConferenceResponse ) from sinch.domains.voice.models.applications.requests import ( - GetNumbersVoiceApplicationRequest, AssignNumbersVoiceApplicationRequest, UnassignNumbersVoiceApplicationRequest, - GetCallbackUrlsVoiceApplicationRequest, - QueryNumberVoiceApplicationRequest + QueryNumberVoiceApplicationRequest, + UpdateCallbackUrlsVoiceApplicationRequest, + GetCallbackUrlsVoiceApplicationRequest ) from sinch.domains.voice.models.applications.responses import ( GetNumbersVoiceApplicationResponse, AssignNumbersVoiceApplicationResponse, UnassignNumbersVoiceApplicationResponse, - KickParticipantVoiceConferenceResponse, GetCallbackUrlsVoiceApplicationResponse, QueryNumberVoiceApplicationResponse ) @@ -65,7 +70,7 @@ def __init__(self, sinch): def text_to_speech( self, - destination: dict, + destination: Destination, cli: str = None, dtmf: str = None, domain: str = None, @@ -98,10 +103,10 @@ def text_to_speech( def conference( self, - destination: dict, + destination: Destination, conference_id: str, cli: str = None, - conference_dtmf_options: dict = None, + conference_dtmf_options: ConferenceDTMFOptions = None, dtmf: str = None, conference: str = None, max_duration: int = None, @@ -140,7 +145,7 @@ def conference( def custom( self, cli: str = None, - destination: dict = None, + destination: Destination = None, dtmf: str = None, custom: str = None, max_duration: int = None, @@ -182,7 +187,7 @@ def update( self, call_id: str, instructions: list, - action: dict + action: Action, ) -> UpdateVoiceCallResponse: return self._sinch.configuration.transport.request( UpdateCallEndpoint( @@ -199,7 +204,7 @@ def manage( call_id: str, call_leg: str, instructions: list, - action: dict + action: Action, ) -> ManageVoiceCallResponse: return self._sinch.configuration.transport.request( ManageCallEndpoint( @@ -277,11 +282,18 @@ def get_numbers(self) -> GetNumbersVoiceApplicationResponse: GetVoiceNumbersEndpoint() ) - def assign_numbers(self, call_id) -> AssignNumbersVoiceApplicationResponse: + def assign_numbers( + self, + numbers: List[str], + application_key: str = None, + capability: str = None + ) -> AssignNumbersVoiceApplicationResponse: return self._sinch.configuration.transport.request( - AssignVoiceNumbersEndxpoint( + AssignVoiceNumbersEndpoint( request_data=AssignNumbersVoiceApplicationRequest( - call_id=call_id + numbers=numbers, + application_key=application_key, + capability=capability ) ) ) @@ -289,7 +301,7 @@ def assign_numbers(self, call_id) -> AssignNumbersVoiceApplicationResponse: def unassign_number( self, number: str, - application_key: str =None, + application_key: str = None, capability: str = None ) -> UnassignNumbersVoiceApplicationResponse: @@ -303,9 +315,32 @@ def unassign_number( ) ) - def get_callback_urls(self) -> GetCallbackUrlsVoiceApplicationResponse: + def get_callback_urls( + self, + application_key: str + ) -> GetCallbackUrlsVoiceApplicationResponse: return self._sinch.configuration.transport.request( - GetVoiceCallbacksEndpoint() + GetVoiceCallbacksEndpoint( + request_data=GetCallbackUrlsVoiceApplicationRequest( + application_key=application_key + ) + ) + ) + + def update_callback_urls( + self, + application_key: str, + primary: str = None, + fallback: str = None + ): + return self._sinch.configuration.transport.request( + UpdateVoiceCallbacksEndpoint( + request_data=UpdateCallbackUrlsVoiceApplicationRequest( + application_key=application_key, + primary=primary, + fallback=fallback + ) + ) ) def query_number(self, number) -> QueryNumberVoiceApplicationResponse: diff --git a/sinch/domains/voice/endpoints/applications/assign_numbers.py b/sinch/domains/voice/endpoints/applications/assign_numbers.py new file mode 100644 index 0000000..77568bc --- /dev/null +++ b/sinch/domains/voice/endpoints/applications/assign_numbers.py @@ -0,0 +1,27 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.applications.requests import AssignNumbersVoiceApplicationRequest +from sinch.domains.voice.models.applications.responses import AssignNumbersVoiceApplicationResponse + + +class AssignVoiceNumbersEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/v1/configuration/numbers" + HTTP_METHOD = HTTPMethods.POST.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: AssignNumbersVoiceApplicationRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_applications_origin + ) + + def request_body(self): + return self.request_data.as_json() + + def handle_response(self, response: HTTPResponse) -> AssignNumbersVoiceApplicationResponse: + super().handle_response(response) + return AssignNumbersVoiceApplicationResponse() + diff --git a/sinch/domains/voice/endpoints/applications/get_callback_urls.py b/sinch/domains/voice/endpoints/applications/get_callback_urls.py index 7a491a5..65c15ea 100644 --- a/sinch/domains/voice/endpoints/applications/get_callback_urls.py +++ b/sinch/domains/voice/endpoints/applications/get_callback_urls.py @@ -2,6 +2,7 @@ from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint from sinch.core.enums import HTTPAuthentication, HTTPMethods from sinch.domains.voice.models.applications.responses import GetCallbackUrlsVoiceApplicationResponse +from sinch.domains.voice.models.applications.requests import GetCallbackUrlsVoiceApplicationRequest class GetVoiceCallbacksEndpoint(VoiceEndpoint): @@ -9,18 +10,18 @@ class GetVoiceCallbacksEndpoint(VoiceEndpoint): HTTP_METHOD = HTTPMethods.GET.value HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value - def __init__(self): - pass + def __init__(self, request_data: GetCallbackUrlsVoiceApplicationRequest): + self.request_data = request_data def build_url(self, sinch) -> str: return self.ENDPOINT_URL.format( origin=sinch.configuration.voice_applications_origin, - application_key=sinch.configuration.application_key + application_key=self.request_data.application_key ) def handle_response(self, response: HTTPResponse) -> GetCallbackUrlsVoiceApplicationResponse: super().handle_response(response) return GetCallbackUrlsVoiceApplicationResponse( - primary=response.body["url"]["primary"], - fallback=response.body["url"]["fallback"] + primary=response.body["url"].get("primary"), + fallback=response.body["url"].get("fallback") ) diff --git a/sinch/domains/voice/endpoints/applications/query_number.py b/sinch/domains/voice/endpoints/applications/query_number.py index d8c6f3a..f5cef05 100644 --- a/sinch/domains/voice/endpoints/applications/query_number.py +++ b/sinch/domains/voice/endpoints/applications/query_number.py @@ -22,9 +22,9 @@ def build_url(self, sinch) -> str: def handle_response(self, response: HTTPResponse) -> QueryNumberVoiceApplicationResponse: super().handle_response(response) return QueryNumberVoiceApplicationResponse( - country_id=response.body["countryId"], - number_type=response.body["numberType"], - normalized_number=response.body["normalizedNumber"], - restricted=response.body["restricted"], - rate=response.body["rate"] + country_id=response.body["number"]["countryId"], + number_type=response.body["number"]["numberType"], + normalized_number=response.body["number"]["normalizedNumber"], + restricted=response.body["number"]["restricted"], + rate=response.body["number"]["rate"] ) diff --git a/sinch/domains/voice/endpoints/applications/unassign_number.py b/sinch/domains/voice/endpoints/applications/unassign_number.py index f62958f..1b5aeaa 100644 --- a/sinch/domains/voice/endpoints/applications/unassign_number.py +++ b/sinch/domains/voice/endpoints/applications/unassign_number.py @@ -1,3 +1,4 @@ +import json from sinch.core.models.http_response import HTTPResponse from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint from sinch.core.enums import HTTPAuthentication, HTTPMethods @@ -15,10 +16,23 @@ def __init__(self, request_data: UnassignNumbersVoiceApplicationRequest): def build_url(self, sinch) -> str: return self.ENDPOINT_URL.format( - origin=sinch.configuration.voice_applications_origin, - number=self.request_data.number + origin=sinch.configuration.voice_applications_origin ) + def request_body(self): + request_data = {} + + if self.request_data.number: + request_data["numbers"] = self.request_data.number + + if self.request_data.application_key: + request_data["applicationKey"] = self.request_data.application_key + + if self.request_data.capability: + request_data["capability"] = self.request_data.capability + + return json.dumps(request_data) + def handle_response(self, response: HTTPResponse) -> UnassignNumbersVoiceApplicationResponse: super().handle_response(response) return UnassignNumbersVoiceApplicationResponse() diff --git a/sinch/domains/voice/endpoints/applications/update_callbacks.py b/sinch/domains/voice/endpoints/applications/update_callbacks.py index e69de29..f95630e 100644 --- a/sinch/domains/voice/endpoints/applications/update_callbacks.py +++ b/sinch/domains/voice/endpoints/applications/update_callbacks.py @@ -0,0 +1,38 @@ +import json +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethods +from sinch.domains.voice.models.applications.requests import UpdateCallbackUrlsVoiceApplicationRequest +from sinch.domains.voice.models.applications.responses import UpdateCallbackUrlsVoiceApplicationResponse + + +class UpdateVoiceCallbacksEndpoint(VoiceEndpoint): + ENDPOINT_URL = "{origin}/v1/configuration/callbacks/applications/{application_key}" + HTTP_METHOD = HTTPMethods.POST.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: UpdateCallbackUrlsVoiceApplicationRequest): + self.request_data = request_data + + def build_url(self, sinch) -> str: + return self.ENDPOINT_URL.format( + origin=sinch.configuration.voice_applications_origin, + application_key=self.request_data.application_key + ) + + def request_body(self): + request_data = { + "url": {} + } + + if self.request_data.primary: + request_data["url"]["primary"] = self.request_data.primary + + if self.request_data.primary: + request_data["url"]["fallback"] = self.request_data.fallback + + return json.dumps(request_data) + + def handle_response(self, response: HTTPResponse) -> UpdateCallbackUrlsVoiceApplicationResponse: + super().handle_response(response) + return UpdateCallbackUrlsVoiceApplicationResponse() diff --git a/sinch/domains/voice/endpoints/applications/update_numbers.py b/sinch/domains/voice/endpoints/applications/update_numbers.py deleted file mode 100644 index e69de29..0000000 diff --git a/sinch/domains/voice/endpoints/callouts/callout.py b/sinch/domains/voice/endpoints/callouts/callout.py index c85224e..324e769 100644 --- a/sinch/domains/voice/endpoints/callouts/callout.py +++ b/sinch/domains/voice/endpoints/callouts/callout.py @@ -32,6 +32,20 @@ def request_body(self): elif self.callout_method == CalloutMethod.CONFERENCE.value: request_data["method"] = CalloutMethod.CONFERENCE.value + if self.request_data.conferenceDtmfOptions: + dtmf_options = {} + + if self.request_data.conferenceDtmfOptions["mode"]: + dtmf_options["mode"] = self.request_data.get["conferenceDtmfOptions"]["mode"] + + if self.request_data.conferenceDtmfOptions["timeout_mills"]: + dtmf_options["timeoutMills"] = self.request_data.get["conferenceDtmfOptions"]["timeout_mills"] + + if self.request_data.conferenceDtmfOptions["max_digits"]: + dtmf_options["maxDigits"] = self.request_data.get["conferenceDtmfOptions"]["max_digits"] + + self.request_data.conferenceDtmfOptions = dtmf_options + request_data[CalloutMethod.CONFERENCE.value] = self.request_data.as_dict() return json.dumps(request_data) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index f5758fb..b355f9b 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -26,3 +26,4 @@ class ConferenceParticipant: class ApplicationNumber: number: str capability: str + diff --git a/sinch/domains/voice/models/applications/requests.py b/sinch/domains/voice/models/applications/requests.py index a40ae1e..3d0883f 100644 --- a/sinch/domains/voice/models/applications/requests.py +++ b/sinch/domains/voice/models/applications/requests.py @@ -1,15 +1,13 @@ +from typing import List from dataclasses import dataclass from sinch.core.models.base_model import SinchRequestBaseModel -@dataclass -class GetNumbersVoiceApplicationRequest(SinchRequestBaseModel): - pass - - @dataclass class AssignNumbersVoiceApplicationRequest(SinchRequestBaseModel): - pass + numbers: List[str] + application_key: str + capability: str @dataclass @@ -22,3 +20,15 @@ class UnassignNumbersVoiceApplicationRequest(SinchRequestBaseModel): @dataclass class QueryNumberVoiceApplicationRequest(SinchRequestBaseModel): number: str + + +@dataclass +class UpdateCallbackUrlsVoiceApplicationRequest(SinchRequestBaseModel): + application_key: str + primary: str + fallback: str + + +@dataclass +class GetCallbackUrlsVoiceApplicationRequest(SinchRequestBaseModel): + application_key: str diff --git a/sinch/domains/voice/models/applications/responses.py b/sinch/domains/voice/models/applications/responses.py index 769164a..a1637d8 100644 --- a/sinch/domains/voice/models/applications/responses.py +++ b/sinch/domains/voice/models/applications/responses.py @@ -21,7 +21,7 @@ class UnassignNumbersVoiceApplicationResponse(SinchBaseModel): @dataclass -class KickParticipantVoiceConferenceResponse(SinchBaseModel): +class UpdateCallbackUrlsVoiceApplicationResponse(SinchBaseModel): pass diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py index e24934b..369c4ef 100644 --- a/sinch/domains/voice/models/callouts/requests.py +++ b/sinch/domains/voice/models/callouts/requests.py @@ -1,10 +1,22 @@ from dataclasses import dataclass +from typing import TypedDict from sinch.core.models.base_model import SinchRequestBaseModel +class Destination(TypedDict): + type: str + endpoint: str + + +class ConferenceDTMFOptions(TypedDict): + mode: str + max_digits: int + timeout_mills: int + + @dataclass class TextToSpeechVoiceCalloutRequest(SinchRequestBaseModel): - destination: dict + destination: Destination cli: str dtmf: str domain: str @@ -19,10 +31,10 @@ class TextToSpeechVoiceCalloutRequest(SinchRequestBaseModel): @dataclass class ConferenceVoiceCalloutRequest(SinchRequestBaseModel): - destination: dict + destination: Destination conferenceId: str cli: str - conferenceDtmfOptions: dict + conferenceDtmfOptions: ConferenceDTMFOptions dtmf: str conference: str maxDuration: int @@ -39,7 +51,7 @@ class ConferenceVoiceCalloutRequest(SinchRequestBaseModel): @dataclass class CustomVoiceCalloutRequest(SinchRequestBaseModel): cli: str - destination: dict + destination: Destination dtmf: str custom: str maxDuration: int diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py index 2dd79ea..28c549a 100644 --- a/sinch/domains/voice/models/calls/requests.py +++ b/sinch/domains/voice/models/calls/requests.py @@ -1,7 +1,12 @@ from dataclasses import dataclass +from typing import TypedDict from sinch.core.models.base_model import SinchRequestBaseModel +class Action(TypedDict): + name: str + + @dataclass class GetVoiceCallRequest(SinchRequestBaseModel): call_id: str @@ -11,7 +16,7 @@ class GetVoiceCallRequest(SinchRequestBaseModel): class UpdateVoiceCallRequest(SinchRequestBaseModel): call_id: str instructions: list - action: dict + action: Action @dataclass @@ -19,4 +24,4 @@ class ManageVoiceCallRequest(SinchRequestBaseModel): call_id: str call_leg: str instructions: list - action: dict + action: Action diff --git a/tests/conftest.py b/tests/conftest.py index 2820ce1..d940ae9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,6 +66,7 @@ def configure_origin( if voice_origin: sinch_client.configuration.voice_origin = voice_origin + sinch_client.configuration.voice_applications_origin = voice_origin if disable_ssl: sinch_client.configuration.disable_https = True diff --git a/tests/e2e/voice/applications/test_assign_numbers.py b/tests/e2e/voice/applications/test_assign_numbers.py index e69de29..4ce0c6e 100644 --- a/tests/e2e/voice/applications/test_assign_numbers.py +++ b/tests/e2e/voice/applications/test_assign_numbers.py @@ -0,0 +1,21 @@ +from sinch.domains.voice.models.applications.responses import AssignNumbersVoiceApplicationResponse + + +def test_assign_application_numbers( + sinch_client_sync +): + get_voice_numbers_response = sinch_client_sync.voice.applications.get_numbers() + assign_voice_numbers_response = sinch_client_sync.voice.applications.assign_numbers( + numbers=[get_voice_numbers_response.numbers[0].number] + ) + assert isinstance(assign_voice_numbers_response, AssignNumbersVoiceApplicationResponse) + + +async def test_assign_application_numbers_async( + sinch_client_async +): + get_voice_numbers_response = await sinch_client_async.voice.applications.get_numbers() + assign_voice_numbers_response = await sinch_client_async.voice.applications.assign_numbers( + numbers=[get_voice_numbers_response.numbers[0].number] + ) + assert isinstance(assign_voice_numbers_response, AssignNumbersVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_get_callback_urls.py b/tests/e2e/voice/applications/test_get_callback_urls.py index 175a147..0f64966 100644 --- a/tests/e2e/voice/applications/test_get_callback_urls.py +++ b/tests/e2e/voice/applications/test_get_callback_urls.py @@ -1,8 +1,21 @@ from sinch.domains.voice.models.applications.responses import GetCallbackUrlsVoiceApplicationResponse -def test_get_application_callback_urls_call( - sinch_client_sync +def test_get_application_callback_urls( + sinch_client_sync, + application_key ): - callback_urls_response = sinch_client_sync.voice.applications.get_callback_urls() + callback_urls_response = sinch_client_sync.voice.applications.get_callback_urls( + application_key=application_key + ) + assert isinstance(callback_urls_response, GetCallbackUrlsVoiceApplicationResponse) + + +async def test_get_application_callback_async( + sinch_client_sync, + application_key +): + callback_urls_response = sinch_client_sync.voice.applications.get_callback_urls( + application_key=application_key + ) assert isinstance(callback_urls_response, GetCallbackUrlsVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_get_numbers.py b/tests/e2e/voice/applications/test_get_numbers.py index ab01f80..b9c1fad 100644 --- a/tests/e2e/voice/applications/test_get_numbers.py +++ b/tests/e2e/voice/applications/test_get_numbers.py @@ -1,14 +1,14 @@ from sinch.domains.voice.models.applications.responses import GetNumbersVoiceApplicationResponse -def test_get_application_numbers_call( +def test_get_application_numbers( sinch_client_sync ): get_voice_numbers_response = sinch_client_sync.voice.applications.get_numbers() assert isinstance(get_voice_numbers_response, GetNumbersVoiceApplicationResponse) -async def test_get_application_numbers_call_async( # TODO: fix that +async def test_get_application_numbers_async( sinch_client_async ): get_voice_numbers_response = await sinch_client_async.voice.applications.get_numbers() diff --git a/tests/e2e/voice/applications/test_query_number.py b/tests/e2e/voice/applications/test_query_number.py index c08aa21..0260c60 100644 --- a/tests/e2e/voice/applications/test_query_number.py +++ b/tests/e2e/voice/applications/test_query_number.py @@ -1,8 +1,21 @@ from sinch.domains.voice.models.applications.responses import QueryNumberVoiceApplicationResponse -def test_query_application_numbers_call( +def test_query_application_numbers( sinch_client_sync ): - query_voice_numbers_response = sinch_client_sync.voice.applications.query_number() + get_voice_numbers_response = sinch_client_sync.voice.applications.get_numbers() + query_voice_numbers_response = sinch_client_sync.voice.applications.query_number( + get_voice_numbers_response.numbers[0].number + ) + assert isinstance(query_voice_numbers_response, QueryNumberVoiceApplicationResponse) + + +async def test_query_application_numbers_async( + sinch_client_async +): + get_voice_numbers_response = await sinch_client_async.voice.applications.get_numbers() + query_voice_numbers_response = await sinch_client_async.voice.applications.query_number( + get_voice_numbers_response.numbers[0].number + ) assert isinstance(query_voice_numbers_response, QueryNumberVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_unassign_number.py b/tests/e2e/voice/applications/test_unassign_number.py index 4aaef34..8189e7b 100644 --- a/tests/e2e/voice/applications/test_unassign_number.py +++ b/tests/e2e/voice/applications/test_unassign_number.py @@ -4,5 +4,18 @@ def test_unassign_application_number( sinch_client_sync ): - unassign_number_response = sinch_client_sync.voice.applications.get_callback_urls() + get_voice_numbers_response = sinch_client_sync.voice.applications.get_numbers() + unassign_number_response = sinch_client_sync.voice.applications.unassign_number( + number=get_voice_numbers_response.numbers[0].number + ) + assert isinstance(unassign_number_response, UnassignNumbersVoiceApplicationResponse) + + +async def test_unassign_application_number_async( + sinch_client_async +): + get_voice_numbers_response = await sinch_client_async.voice.applications.get_numbers() + unassign_number_response = await sinch_client_async.voice.applications.unassign_number( + number=get_voice_numbers_response.numbers[0].number + ) assert isinstance(unassign_number_response, UnassignNumbersVoiceApplicationResponse) diff --git a/tests/e2e/voice/applications/test_update_callback_urls.py b/tests/e2e/voice/applications/test_update_callback_urls.py index e69de29..259c5de 100644 --- a/tests/e2e/voice/applications/test_update_callback_urls.py +++ b/tests/e2e/voice/applications/test_update_callback_urls.py @@ -0,0 +1,25 @@ +from sinch.domains.voice.models.applications.responses import UpdateCallbackUrlsVoiceApplicationResponse + + +def test_update_application_callback_urls( + sinch_client_sync, + application_key +): + callback_urls_response = sinch_client_sync.voice.applications.update_callback_urls( + application_key=application_key, + primary="testprimary.com/123", + fallback="testfallback.com/123" + ) + assert isinstance(callback_urls_response, UpdateCallbackUrlsVoiceApplicationResponse) + + +async def test_update_application_callback_urls_async( + sinch_client_async, + application_key +): + callback_urls_response = await sinch_client_async.voice.applications.update_callback_urls( + application_key=application_key, + primary="testprimary.com/123", + fallback="testfallback.com/123" + ) + assert isinstance(callback_urls_response, UpdateCallbackUrlsVoiceApplicationResponse) From cdfc926387e46b9e27d6977966eae81709c017ee Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 1 Mar 2024 07:58:29 +0100 Subject: [PATCH 14/53] style(pep8): redundant empty line removed --- sinch/domains/voice/models/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index b355f9b..f5758fb 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -26,4 +26,3 @@ class ConferenceParticipant: class ApplicationNumber: number: str capability: str - From 20917c5960e92b93938d7b5ab79aa56ab9479c54 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 1 Mar 2024 08:01:11 +0100 Subject: [PATCH 15/53] fix(CI): typo --- .github/workflows/run-tests.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1f69034..b68638e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,14 +24,14 @@ env: APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} APPLICATION_KEY: ${{ secrets.APPLICATION_KEY }} VERIFICATION_ID: ${{ secrets.VERIFICATION_ID }} - VERIFICATION_ORIGIN: ${{ secrets.VERIFICATION_ORIGIN}} - VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP}} - VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE}} - VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE}} - VOICE_ORIGIN: ${{ secrets.VOICE_ORIGIN}} - VOICE_ORIGIN_PHONE_NUMBER: ${{ secrets.VOICE_ORIGIN_PHONE_NUMBER}} - CONFERENCE_ID: ${{ secrets.CONFERENCE_ID} - CONFERENCE_CALL_ID: ${{ secrets.CONFERENCE_CALL_ID} + VERIFICATION_ORIGIN: ${{ secrets.VERIFICATION_ORIGIN }} + VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP }} + VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE }} + VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE }} + VOICE_ORIGIN: ${{ secrets.VOICE_ORIGIN }} + VOICE_ORIGIN_PHONE_NUMBER: ${{ secrets.VOICE_ORIGIN_PHONE_NUMBER }} + CONFERENCE_ID: ${{ secrets.CONFERENCE_ID }} + CONFERENCE_CALL_ID: ${{ secrets.CONFERENCE_CALL_ID }} jobs: build: From ea22a2bbba3ece33c1cbcd7a455d452a6b05b4cb Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 1 Mar 2024 08:04:03 +0100 Subject: [PATCH 16/53] fix(style): redundant white line removed --- sinch/domains/voice/endpoints/applications/assign_numbers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sinch/domains/voice/endpoints/applications/assign_numbers.py b/sinch/domains/voice/endpoints/applications/assign_numbers.py index 77568bc..d07bcac 100644 --- a/sinch/domains/voice/endpoints/applications/assign_numbers.py +++ b/sinch/domains/voice/endpoints/applications/assign_numbers.py @@ -24,4 +24,3 @@ def request_body(self): def handle_response(self, response: HTTPResponse) -> AssignNumbersVoiceApplicationResponse: super().handle_response(response) return AssignNumbersVoiceApplicationResponse() - From 6aa94e0bc0e34c15d81818080b2f3ff0db759708 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 7 Mar 2024 11:40:16 +0100 Subject: [PATCH 17/53] feat(Voice): ConferenceDTMFOptionsMode added --- sinch/domains/voice/enums.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py index 407a11c..b36b6ba 100644 --- a/sinch/domains/voice/enums.py +++ b/sinch/domains/voice/enums.py @@ -27,3 +27,9 @@ class ConferenceMusicOnHold(Enum): MUSIC_1 = "music1" MUSIC_2 = "music2" MUSIC_3 = "music3" + + +class ConferenceDTMFOptionsMode(Enum): + IGNORE = "ignore" + FORWARD = "forward" + DETECT = "detect" From 9d9cd2a2270ba8e0e9f9b5f32668484642f099db Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 7 Mar 2024 12:38:03 +0100 Subject: [PATCH 18/53] feat(Voice): align naming to other SDKs --- sinch/domains/voice/__init__.py | 2 +- tests/e2e/voice/calls/test_magage_call.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 4429c86..7cac4d7 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -199,7 +199,7 @@ def update( ) ) - def manage( + def manage_with_call_leg( self, call_id: str, call_leg: str, diff --git a/tests/e2e/voice/calls/test_magage_call.py b/tests/e2e/voice/calls/test_magage_call.py index 4224a84..1b3a51b 100644 --- a/tests/e2e/voice/calls/test_magage_call.py +++ b/tests/e2e/voice/calls/test_magage_call.py @@ -5,7 +5,7 @@ def test_manage_call( sinch_client_sync, conference_call_id ): - update_call_response = sinch_client_sync.voice.calls.manage( + update_call_response = sinch_client_sync.voice.calls.manage_with_call_leg( call_id=conference_call_id, call_leg="caller", instructions=[ @@ -21,8 +21,21 @@ def test_manage_call( assert isinstance(update_call_response, ManageVoiceCallResponse) -def test_manage_call_async( +async def test_manage_call_async( sinch_client_async, - call_id + conference_call_id ): - pass + update_call_response = await sinch_client_async.voice.calls.manage_with_call_leg( + call_id=conference_call_id, + call_leg="caller", + instructions=[ + { + "name": "sendDtmf", + "value": "1234#" + } + ], + action={ + "name": "hangup" + } + ) + assert isinstance(update_call_response, ManageVoiceCallResponse) From f9694b284b79a1c687678564d82ef79982336886 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 7 Mar 2024 12:49:48 +0100 Subject: [PATCH 19/53] feat(Voice): missing DTO property added --- sinch/domains/voice/endpoints/applications/get_numbers.py | 5 +++-- sinch/domains/voice/models/__init__.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sinch/domains/voice/endpoints/applications/get_numbers.py b/sinch/domains/voice/endpoints/applications/get_numbers.py index 78e557f..ceed55a 100644 --- a/sinch/domains/voice/endpoints/applications/get_numbers.py +++ b/sinch/domains/voice/endpoints/applications/get_numbers.py @@ -23,8 +23,9 @@ def handle_response(self, response: HTTPResponse) -> GetNumbersVoiceApplicationR return GetNumbersVoiceApplicationResponse( numbers=[ ApplicationNumber( - number=number["number"], - capability=number["capability"] + number=number.get("number"), + capability=number.get("capability"), + applicationkey=number.get("applicationkey") ) for number in response.body["numbers"] ] ) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index f5758fb..653ada4 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -26,3 +26,4 @@ class ConferenceParticipant: class ApplicationNumber: number: str capability: str + applicationkey: str From 16aa654f83673364f2fa2bdc2f90825c048f3cde Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 7 Mar 2024 13:15:59 +0100 Subject: [PATCH 20/53] fix(Verification): typo --- sinch/domains/voice/endpoints/applications/unassign_number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/domains/voice/endpoints/applications/unassign_number.py b/sinch/domains/voice/endpoints/applications/unassign_number.py index 1b5aeaa..a2b6bfd 100644 --- a/sinch/domains/voice/endpoints/applications/unassign_number.py +++ b/sinch/domains/voice/endpoints/applications/unassign_number.py @@ -23,7 +23,7 @@ def request_body(self): request_data = {} if self.request_data.number: - request_data["numbers"] = self.request_data.number + request_data["number"] = self.request_data.number if self.request_data.application_key: request_data["applicationKey"] = self.request_data.application_key From 903047534a7be4464b26858c03496e82808c46e0 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 7 Mar 2024 13:45:49 +0100 Subject: [PATCH 21/53] test(Voice): enable update call e2e tests --- tests/e2e/voice/calls/test_update_call.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/e2e/voice/calls/test_update_call.py b/tests/e2e/voice/calls/test_update_call.py index b9c4a25..84992e9 100644 --- a/tests/e2e/voice/calls/test_update_call.py +++ b/tests/e2e/voice/calls/test_update_call.py @@ -1,8 +1,6 @@ -import pytest from sinch.domains.voice.models.calls.responses import UpdateVoiceCallResponse -@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") def test_update_call( sinch_client_sync, call_id @@ -22,7 +20,6 @@ def test_update_call( assert isinstance(update_call_response, UpdateVoiceCallResponse) -@pytest.mark.skip(reason="Conference endpoints have to be implemented first.") async def test_update_call_async( sinch_client_async, call_id From b78427b165c21d11dc2b4bddfaf0b13682c9bda4 Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 7 Mar 2024 13:56:03 +0100 Subject: [PATCH 22/53] test(Voice): async tests added to callouts --- tests/e2e/voice/callouts/test_callout.py | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/e2e/voice/callouts/test_callout.py b/tests/e2e/voice/callouts/test_callout.py index 605b482..aaa0b6d 100644 --- a/tests/e2e/voice/callouts/test_callout.py +++ b/tests/e2e/voice/callouts/test_callout.py @@ -53,6 +53,24 @@ def test_conference_callout( assert isinstance(conference_callout_response, VoiceCalloutResponse) +async def test_conference_callout_async( + sinch_client_async, + phone_number, + voice_origin_phone_number, + conference_id +): + conference_callout_response = await sinch_client_async.voice.callouts.conference( + conference_id=conference_id, + destination={ + "type": "number", + "endpoint": phone_number + }, + locale="en-US", + cli=voice_origin_phone_number + ) + assert isinstance(conference_callout_response, VoiceCalloutResponse) + + def test_custom_callout( sinch_client_sync, phone_number, @@ -66,3 +84,18 @@ def test_custom_callout( cli=voice_origin_phone_number ) assert isinstance(custom_callout_response, VoiceCalloutResponse) + + +async def test_custom_callout_async( + sinch_client_async, + phone_number, + voice_origin_phone_number +): + custom_callout_response = await sinch_client_async.voice.callouts.custom( + destination={ + "type": "number", + "endpoint": phone_number + }, + cli=voice_origin_phone_number + ) + assert isinstance(custom_callout_response, VoiceCalloutResponse) From 10dc71f8f948a8616fb6e21597a967256a907ca7 Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 11 Mar 2024 14:03:35 +0100 Subject: [PATCH 23/53] fix(Voice): missing env variable definition --- .github/workflows/run-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b68638e..6ef94a8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -30,6 +30,7 @@ env: VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE }} VOICE_ORIGIN: ${{ secrets.VOICE_ORIGIN }} VOICE_ORIGIN_PHONE_NUMBER: ${{ secrets.VOICE_ORIGIN_PHONE_NUMBER }} + VOICE_CALL_ID: ${{ secrets.VOICE_CALL_ID }} CONFERENCE_ID: ${{ secrets.CONFERENCE_ID }} CONFERENCE_CALL_ID: ${{ secrets.CONFERENCE_CALL_ID }} From bf1b61a8f351f4b1b9430721bc95cd4d83329c8e Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 11 Mar 2024 15:18:05 +0100 Subject: [PATCH 24/53] feat(Voice): SVAML support --- .../voice/endpoints/calls/update_call.py | 6 +- sinch/domains/voice/models/__init__.py | 18 ++- .../domains/voice/models/callouts/requests.py | 13 +- sinch/domains/voice/models/svaml/__init__.py | 0 sinch/domains/voice/models/svaml/actions.py | 114 ++++++++++++++++++ .../voice/models/svaml/instructions.py | 74 ++++++++++++ .../applications/test_unassign_number.py | 2 +- tests/e2e/voice/calls/test_update_call.py | 28 ++--- 8 files changed, 218 insertions(+), 37 deletions(-) create mode 100644 sinch/domains/voice/models/svaml/__init__.py create mode 100644 sinch/domains/voice/models/svaml/actions.py create mode 100644 sinch/domains/voice/models/svaml/instructions.py diff --git a/sinch/domains/voice/endpoints/calls/update_call.py b/sinch/domains/voice/endpoints/calls/update_call.py index e9f6c74..7dd982e 100644 --- a/sinch/domains/voice/endpoints/calls/update_call.py +++ b/sinch/domains/voice/endpoints/calls/update_call.py @@ -1,3 +1,4 @@ +from copy import deepcopy from sinch.core.models.http_response import HTTPResponse from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint from sinch.core.enums import HTTPAuthentication, HTTPMethods @@ -20,8 +21,9 @@ def build_url(self, sinch) -> str: ) def request_body(self): - self.request_data.call_id = None - return self.request_data.as_json() + request_data = deepcopy(self.request_data) + request_data.call_id = None + return request_data.as_json() def handle_response(self, response: HTTPResponse) -> UpdateVoiceCallResponse: super().handle_response(response) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index 653ada4..692850d 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import TypedDict @dataclass @@ -7,12 +8,6 @@ class Price: amount: float -@dataclass -class Destination: - type: str - endpoint: str - - @dataclass class ConferenceParticipant: cli: str @@ -27,3 +22,14 @@ class ApplicationNumber: number: str capability: str applicationkey: str + + +class Destination(TypedDict): + type: str + endpoint: str + + +class ConferenceDTMFOptions(TypedDict): + mode: str + max_digits: int + timeout_mills: int diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py index 369c4ef..855a927 100644 --- a/sinch/domains/voice/models/callouts/requests.py +++ b/sinch/domains/voice/models/callouts/requests.py @@ -1,17 +1,6 @@ from dataclasses import dataclass -from typing import TypedDict from sinch.core.models.base_model import SinchRequestBaseModel - - -class Destination(TypedDict): - type: str - endpoint: str - - -class ConferenceDTMFOptions(TypedDict): - mode: str - max_digits: int - timeout_mills: int +from sinch.domains.voice.models import Destination, ConferenceDTMFOptions @dataclass diff --git a/sinch/domains/voice/models/svaml/__init__.py b/sinch/domains/voice/models/svaml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sinch/domains/voice/models/svaml/actions.py b/sinch/domains/voice/models/svaml/actions.py new file mode 100644 index 0000000..643d811 --- /dev/null +++ b/sinch/domains/voice/models/svaml/actions.py @@ -0,0 +1,114 @@ +from dataclasses import dataclass +from typing import Optional, List, TypedDict +from sinch.core.models.base_model import SinchRequestBaseModel +from sinch.domains.voice import Destination, ConferenceDTMFOptions + + +class AnsweringMachineDetection(TypedDict): + enabled: bool + + +class CallHeaders(TypedDict): + key: str + value: str + + +@dataclass +class HangupAction(SinchRequestBaseModel): + name: str = "hangup" + + +@dataclass +class ContinueAction(SinchRequestBaseModel): + name: str = "continue" + + +@dataclass +class ConnectPtsnAction(SinchRequestBaseModel): + name: str = "connectPstn" + number: Optional[str] = None + locale: Optional[str] = None + max_duration: Optional[int] = None + dial_timeout: Optional[int] = None + cli: Optional[str] = None + suppress_callbacks: Optional[bool] = None + dtmf: Optional[str] = None + indications: Optional[str] = None + amd: Optional[AnsweringMachineDetection] = None + + def as_dict(self): + payload = super().as_dict() + if payload.get("max_duration"): + payload["maxDuration"] = payload.pop("max_duration") + + if payload.get("dial_timeout"): + payload["dialTimeout"] = payload.pop("dial_timeout") + + if payload.get("suppress_callbacks"): + payload["suppressCallbacks"] = payload.pop("suppress_callbacks") + + return payload + + +@dataclass +class ConnectMxpAction(SinchRequestBaseModel): + name: str = "connectMxp" + destination: Optional[Destination] = None + call_headers: Optional[List[CallHeaders]] = None + + def as_dict(self): + payload = super().as_dict() + if payload.get("call_headers"): + payload["callHeaders"] = payload.pop("call_headers") + + return payload + + +@dataclass +class ConnectSipAction(SinchRequestBaseModel): + destination: Optional[Destination] + name: str = "connectSip" + max_duration: Optional[int] = None + cli: Optional[str] = None + transport: Optional[str] = None + suppressCallbacks: Optional[bool] = None + call_headers: Optional[List[CallHeaders]] = None + moh: Optional[str] = None + + def as_dict(self): + payload = super().as_dict() + if payload.get("max_duration"): + payload["maxDuration"] = payload.pop("max_duration") + + if payload.get("suppress_callbacks"): + payload["suppressCallbacks"] = payload.pop("suppress_callbacks") + + return payload + + +@dataclass +class ConnectConfAction(SinchRequestBaseModel): + destination: Optional[Destination] + conference_id: str + name: str = "connectConf" + conference_dtmf_options: Optional[ConferenceDTMFOptions] = None + moh: Optional[str] = None + + +@dataclass +class RunMenuAction(SinchRequestBaseModel): + name: str = "runMenu" + barge: Optional[bool] = None + locale: Optional[str] = None + main_menu: Optional[str] = None + enable_voice: Optional[bool] = None + menus: Optional[dict] = None + + +@dataclass +class ParkAction(SinchRequestBaseModel): + name: str = "park" + locale: Optional[str] = None + introPrompt: Optional[str] = None + holdPrompt: Optional[str] = None + maxDuration: Optional[int] = None diff --git a/sinch/domains/voice/models/svaml/instructions.py b/sinch/domains/voice/models/svaml/instructions.py new file mode 100644 index 0000000..c9ac2ad --- /dev/null +++ b/sinch/domains/voice/models/svaml/instructions.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from typing import Optional, List +from sinch.core.models.base_model import SinchRequestBaseModel + + +@dataclass +class TranscriptionOptions(SinchRequestBaseModel): + enabled: str = None + locale: str = None + + +@dataclass +class RecordingOptions(SinchRequestBaseModel): + destination_url: str = None + credentials: str = None + format: str = None + notification_events: str = None + transcription_options: TranscriptionOptions = None + + def as_dict(self): + payload = super().as_dict() + if payload.get("destination_url"): + payload["destinationUrl"] = payload.pop("destination_url") + + if payload.get("notification_events"): + payload["notificationEvents"] = payload.pop("notification_events") + + if payload.get("transcription_options"): + payload["transcriptionOptions"] = payload.pop("transcription_options") + + return payload + + +@dataclass +class PlayFileInstruction(SinchRequestBaseModel): + ids: List[List[str]] + locale: str + name: str = "playFiles" + + +@dataclass +class SayInstruction(SinchRequestBaseModel): + name: str = "say" + text: Optional[str] = None + locale: Optional[str] = None + + +@dataclass +class SendDtmfInstruction(SinchRequestBaseModel): + name: str = "sendDtmf" + value: Optional[str] = None + + +@dataclass +class SetCookieInstruction(SinchRequestBaseModel): + name: str = "setCookie" + key: Optional[str] = None + value: Optional[str] = None + + +@dataclass +class AnswerInstruction(SinchRequestBaseModel): + name: str = "answer" + + +@dataclass +class StartRecordingInstruction(SinchRequestBaseModel): + name: str = "startRecording" + options: Optional[RecordingOptions] = None + + +@dataclass +class StopRecordingInstruction(SinchRequestBaseModel): + name: str = "stopRecording" diff --git a/tests/e2e/voice/applications/test_unassign_number.py b/tests/e2e/voice/applications/test_unassign_number.py index 8189e7b..7672cbe 100644 --- a/tests/e2e/voice/applications/test_unassign_number.py +++ b/tests/e2e/voice/applications/test_unassign_number.py @@ -6,7 +6,7 @@ def test_unassign_application_number( ): get_voice_numbers_response = sinch_client_sync.voice.applications.get_numbers() unassign_number_response = sinch_client_sync.voice.applications.unassign_number( - number=get_voice_numbers_response.numbers[0].number + number=get_voice_numbers_response.numbers[4].number ) assert isinstance(unassign_number_response, UnassignNumbersVoiceApplicationResponse) diff --git a/tests/e2e/voice/calls/test_update_call.py b/tests/e2e/voice/calls/test_update_call.py index 84992e9..9030917 100644 --- a/tests/e2e/voice/calls/test_update_call.py +++ b/tests/e2e/voice/calls/test_update_call.py @@ -1,39 +1,35 @@ from sinch.domains.voice.models.calls.responses import UpdateVoiceCallResponse +from sinch.domains.voice.models.svaml.actions import HangupAction +from sinch.domains.voice.models.svaml.instructions import SendDtmfInstruction -def test_update_call( +def test_update_voice_call( sinch_client_sync, call_id ): update_call_response = sinch_client_sync.voice.calls.update( call_id=call_id, instructions=[ - { - "name": "sendDtmf", - "value": "1234#" - } + SendDtmfInstruction( + value="1234#" + ).as_dict() ], - action={ - "name": "hangup" - } + action=HangupAction().as_dict() ) assert isinstance(update_call_response, UpdateVoiceCallResponse) -async def test_update_call_async( +async def test_update_voice_call_async( sinch_client_async, call_id ): update_call_response = await sinch_client_async.voice.calls.update( call_id=call_id, instructions=[ - { - "name": "sendDtmf", - "value": "1234#" - } + SendDtmfInstruction( + value="1234#" + ).as_dict() ], - action={ - "name": "hangup" - } + action=HangupAction().as_dict() ) assert isinstance(update_call_response, UpdateVoiceCallResponse) From cd91ca405b7a4ee2fde1356ac79325ee3e872f9b Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 11 Mar 2024 15:44:01 +0100 Subject: [PATCH 25/53] test(Voice): use SVAML DTOs --- tests/e2e/voice/calls/test_magage_call.py | 24 ++++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/e2e/voice/calls/test_magage_call.py b/tests/e2e/voice/calls/test_magage_call.py index 1b3a51b..f4ac085 100644 --- a/tests/e2e/voice/calls/test_magage_call.py +++ b/tests/e2e/voice/calls/test_magage_call.py @@ -1,4 +1,6 @@ from sinch.domains.voice.models.calls.responses import ManageVoiceCallResponse +from sinch.domains.voice.models.svaml.actions import HangupAction +from sinch.domains.voice.models.svaml.instructions import SendDtmfInstruction def test_manage_call( @@ -9,14 +11,11 @@ def test_manage_call( call_id=conference_call_id, call_leg="caller", instructions=[ - { - "name": "sendDtmf", - "value": "1234#" - } + SendDtmfInstruction( + value="1234#" + ).as_dict() ], - action={ - "name": "hangup" - } + action=HangupAction().as_dict() ) assert isinstance(update_call_response, ManageVoiceCallResponse) @@ -29,13 +28,10 @@ async def test_manage_call_async( call_id=conference_call_id, call_leg="caller", instructions=[ - { - "name": "sendDtmf", - "value": "1234#" - } + SendDtmfInstruction( + value="1234#" + ).as_dict() ], - action={ - "name": "hangup" - } + action=HangupAction().as_dict() ) assert isinstance(update_call_response, ManageVoiceCallResponse) From 082d2ba6fb2ae7ec852c780b7590b86e3d026392 Mon Sep 17 00:00:00 2001 From: 650elx Date: Tue, 12 Mar 2024 17:34:33 +0100 Subject: [PATCH 26/53] feat(Voice): SVAML WiP --- sinch/domains/voice/models/svaml/actions.py | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/sinch/domains/voice/models/svaml/actions.py b/sinch/domains/voice/models/svaml/actions.py index 643d811..0f163b2 100644 --- a/sinch/domains/voice/models/svaml/actions.py +++ b/sinch/domains/voice/models/svaml/actions.py @@ -64,6 +64,24 @@ def as_dict(self): return payload +@dataclass +class Option(SinchRequestBaseModel): + dtmf: str + action: str + + +@dataclass +class MenuOption(SinchRequestBaseModel): + id: str + main_prompt: Optional[str] = None + repeat_prompt: Optional[str] = None + repeats: Optional[int] = None + max_digits: Optional[int] = None + timeout_mills: Optional[int] = None + max_timeout_mills: Optional[int] = None + options: Optional[List[Option]] = None + + @dataclass class ConnectSipAction(SinchRequestBaseModel): destination: Optional[Destination] @@ -71,7 +89,7 @@ class ConnectSipAction(SinchRequestBaseModel): max_duration: Optional[int] = None cli: Optional[str] = None transport: Optional[str] = None - suppressCallbacks: Optional[bool] = None + suppress_callbacks: Optional[bool] = None call_headers: Optional[List[CallHeaders]] = None moh: Optional[str] = None @@ -102,13 +120,13 @@ class RunMenuAction(SinchRequestBaseModel): locale: Optional[str] = None main_menu: Optional[str] = None enable_voice: Optional[bool] = None - menus: Optional[dict] = None + menus: Optional[List[MenuOption]] = None @dataclass class ParkAction(SinchRequestBaseModel): name: str = "park" locale: Optional[str] = None - introPrompt: Optional[str] = None + intro_prompt: Optional[str] = None holdPrompt: Optional[str] = None maxDuration: Optional[int] = None From 7d8fb986e5b6308ca7627fd7f3b725aacb551a50 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 13 Mar 2024 23:21:45 +0100 Subject: [PATCH 27/53] feat(Voice): additional conference call method --- sinch/domains/voice/__init__.py | 36 ++++++++++++++++++ .../voice/conferences/test_call_conference.py | 37 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/e2e/voice/conferences/test_call_conference.py diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 7cac4d7..5ad3e79 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -222,6 +222,42 @@ class Conferences: def __init__(self, sinch): self._sinch = sinch + def call( + self, + destination: Destination, + conference_id: str, + cli: str = None, + conference_dtmf_options: ConferenceDTMFOptions = None, + dtmf: str = None, + conference: str = None, + max_duration: int = None, + enable_ace: bool = None, + enable_dice: bool = None, + enable_pie: bool = None, + locale: str = None, + greeting: str = None, + moh_class: str = None, + custom: str = None, + domain: str = None + ) -> VoiceCalloutResponse: + return self._sinch.voice.callouts.conference( + destination=destination, + conference_id=conference_id, + cli=cli, + conference_dtmf_options=conference_dtmf_options, + dtmf=dtmf, + conference=conference, + max_duration=max_duration, + enable_ace=enable_ace, + enable_dice=enable_dice, + enable_pie=enable_pie, + locale=locale, + greeting=greeting, + moh_class=moh_class, + custom=custom, + domain=domain + ) + def get(self, conference_id: str) -> GetVoiceConferenceResponse: return self._sinch.configuration.transport.request( GetConferenceEndpoint( diff --git a/tests/e2e/voice/conferences/test_call_conference.py b/tests/e2e/voice/conferences/test_call_conference.py new file mode 100644 index 0000000..3be04f1 --- /dev/null +++ b/tests/e2e/voice/conferences/test_call_conference.py @@ -0,0 +1,37 @@ +from sinch.domains.voice.models.callouts.responses import VoiceCalloutResponse + + +def test_conference_call( + sinch_client_sync, + phone_number, + voice_origin_phone_number, + conference_id +): + conference_callout_response = sinch_client_sync.voice.conferences.call( + conference_id=conference_id, + destination={ + "type": "number", + "endpoint": phone_number + }, + locale="en-US", + cli=voice_origin_phone_number + ) + assert isinstance(conference_callout_response, VoiceCalloutResponse) + + +async def test_conference_call_async( + sinch_client_async, + phone_number, + voice_origin_phone_number, + conference_id +): + conference_callout_response = await sinch_client_async.voice.conferences.call( + conference_id=conference_id, + destination={ + "type": "number", + "endpoint": phone_number + }, + locale="en-US", + cli=voice_origin_phone_number + ) + assert isinstance(conference_callout_response, VoiceCalloutResponse) \ No newline at end of file From 6afa46f5660858367cf3b209f2616581c62b50fb Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 13 Mar 2024 23:29:33 +0100 Subject: [PATCH 28/53] feat(Voice): Literal type for domain arg --- sinch/domains/voice/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 5ad3e79..29c4ce4 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Literal from sinch.domains.voice.endpoints.callouts.callout import CalloutEndpoint from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint @@ -73,7 +73,7 @@ def text_to_speech( destination: Destination, cli: str = None, dtmf: str = None, - domain: str = None, + domain: Literal["pstn", "mxp"] = None, custom: str = None, locale: str = None, text: str = None, @@ -117,7 +117,7 @@ def conference( greeting: str = None, moh_class: str = None, custom: str = None, - domain: str = None + domain: Literal["pstn", "mxp"] = None, ) -> VoiceCalloutResponse: return self._sinch.configuration.transport.request( CalloutEndpoint( @@ -238,7 +238,7 @@ def call( greeting: str = None, moh_class: str = None, custom: str = None, - domain: str = None + domain: Literal["pstn", "mxp"] = None, ) -> VoiceCalloutResponse: return self._sinch.voice.callouts.conference( destination=destination, From 324ddfb3379e93b937669b6fe951c5fca17a9fa7 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 13 Mar 2024 23:58:49 +0100 Subject: [PATCH 29/53] feat(Voice): rename model --- sinch/domains/voice/models/svaml/actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sinch/domains/voice/models/svaml/actions.py b/sinch/domains/voice/models/svaml/actions.py index 0f163b2..e055008 100644 --- a/sinch/domains/voice/models/svaml/actions.py +++ b/sinch/domains/voice/models/svaml/actions.py @@ -8,7 +8,7 @@ class AnsweringMachineDetection(TypedDict): enabled: bool -class CallHeaders(TypedDict): +class CallHeader(TypedDict): key: str value: str @@ -54,7 +54,7 @@ def as_dict(self): class ConnectMxpAction(SinchRequestBaseModel): name: str = "connectMxp" destination: Optional[Destination] = None - call_headers: Optional[List[CallHeaders]] = None + call_headers: Optional[List[CallHeader]] = None def as_dict(self): payload = super().as_dict() @@ -90,7 +90,7 @@ class ConnectSipAction(SinchRequestBaseModel): cli: Optional[str] = None transport: Optional[str] = None suppress_callbacks: Optional[bool] = None - call_headers: Optional[List[CallHeaders]] = None + call_headers: Optional[List[CallHeader]] = None moh: Optional[str] = None def as_dict(self): From 29997e9f9be497c33b15ea83750b2cad4227ef0f Mon Sep 17 00:00:00 2001 From: 650elx Date: Thu, 14 Mar 2024 11:33:30 +0100 Subject: [PATCH 30/53] feat(Voice): DTO adapted to the facade changes --- sinch/domains/voice/models/callouts/requests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py index 855a927..2efe21b 100644 --- a/sinch/domains/voice/models/callouts/requests.py +++ b/sinch/domains/voice/models/callouts/requests.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Literal from sinch.core.models.base_model import SinchRequestBaseModel from sinch.domains.voice.models import Destination, ConferenceDTMFOptions @@ -34,7 +35,7 @@ class ConferenceVoiceCalloutRequest(SinchRequestBaseModel): greeting: str mohClass: str custom: str - domain: str + domain: Literal["pstn", "mxp"] @dataclass From 90a9537b6400fdc5961c99f8f7d265ef9f52438b Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 15 Mar 2024 20:32:54 +0100 Subject: [PATCH 31/53] fix: typo --- sinch/domains/voice/models/svaml/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/svaml/actions.py b/sinch/domains/voice/models/svaml/actions.py index e055008..0de25bb 100644 --- a/sinch/domains/voice/models/svaml/actions.py +++ b/sinch/domains/voice/models/svaml/actions.py @@ -24,7 +24,7 @@ class ContinueAction(SinchRequestBaseModel): @dataclass -class ConnectPtsnAction(SinchRequestBaseModel): +class ConnectPstnAction(SinchRequestBaseModel): name: str = "connectPstn" number: Optional[str] = None locale: Optional[str] = None From 24b03513a3a6d3e3e8f54d7e59aed2d3aefb8ece Mon Sep 17 00:00:00 2001 From: 650elx Date: Mon, 18 Mar 2024 12:18:14 +0100 Subject: [PATCH 32/53] fix: naming convetion --- sinch/domains/voice/models/svaml/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sinch/domains/voice/models/svaml/actions.py b/sinch/domains/voice/models/svaml/actions.py index 0de25bb..d32dee9 100644 --- a/sinch/domains/voice/models/svaml/actions.py +++ b/sinch/domains/voice/models/svaml/actions.py @@ -128,5 +128,5 @@ class ParkAction(SinchRequestBaseModel): name: str = "park" locale: Optional[str] = None intro_prompt: Optional[str] = None - holdPrompt: Optional[str] = None - maxDuration: Optional[int] = None + hold_prompt: Optional[str] = None + max_duration: Optional[int] = None From 3d6b44c5d9db26b9d34142157c48974b253572bc Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 00:09:43 +0100 Subject: [PATCH 33/53] feat(Voice): represent timestamps as datetime --- sinch/core/deserializers.py | 5 +++++ sinch/domains/voice/endpoints/calls/get_call.py | 3 ++- sinch/domains/voice/models/calls/responses.py | 3 ++- tests/unit/test_serdes.py | 8 ++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 sinch/core/deserializers.py diff --git a/sinch/core/deserializers.py b/sinch/core/deserializers.py new file mode 100644 index 0000000..5e38b25 --- /dev/null +++ b/sinch/core/deserializers.py @@ -0,0 +1,5 @@ +from datetime import datetime + + +def timestamp_to_datetime_in_utc_deserializer(timestamp: str): + return datetime.fromisoformat(timestamp + 'Z') diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py index 1c4f0cf..5013886 100644 --- a/sinch/domains/voice/endpoints/calls/get_call.py +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -1,3 +1,4 @@ +from sinch.core.deserializers import timestamp_to_datetime_in_utc_deserializer from sinch.core.models.http_response import HTTPResponse from sinch.domains.voice.endpoints.voice_endpoint import VoiceEndpoint from sinch.core.enums import HTTPAuthentication, HTTPMethods @@ -34,7 +35,7 @@ def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: status=response.body.get("status"), result=response.body.get("result"), reason=response.body.get("reason"), - timestamp=response.body.get("timestamp"), + timestamp=timestamp_to_datetime_in_utc_deserializer(response.body["timestamp"]), custom=response.body.get("custom"), user_rate=Price( currency_id=response.body["userRate"]["currencyId"], diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py index bd128c9..218f666 100644 --- a/sinch/domains/voice/models/calls/responses.py +++ b/sinch/domains/voice/models/calls/responses.py @@ -1,3 +1,4 @@ +from datetime import datetime from dataclasses import dataclass from sinch.core.models.base_model import SinchBaseModel from sinch.domains.voice.models import Price, Destination @@ -13,7 +14,7 @@ class GetVoiceCallResponse(SinchBaseModel): status: str result: str reason: str - timestamp: str + timestamp: datetime custom: dict user_rate: Price debit: Price diff --git a/tests/unit/test_serdes.py b/tests/unit/test_serdes.py index fa0062c..9b15ba8 100644 --- a/tests/unit/test_serdes.py +++ b/tests/unit/test_serdes.py @@ -1,6 +1,8 @@ import json +import datetime from dataclasses import dataclass from sinch.core.models.base_model import SinchBaseModel, SinchRequestBaseModel +from sinch.core.deserializers import timestamp_to_datetime_in_utc_deserializer @dataclass @@ -55,3 +57,9 @@ def test_sinch_request_base_model_empty_field_removal(): test_data_model = construct_request_test_data_model() request_data_model_as_dict = test_data_model.as_dict() assert not request_data_model_as_dict.get("accepted") + + +def test_timestamp_to_datetime_in_utc_deserializer(): + datetime_in_utc = timestamp_to_datetime_in_utc_deserializer("2024-02-15T13:01:29") + assert isinstance(datetime_in_utc, datetime.datetime) + assert datetime_in_utc.tzinfo == datetime.timezone.utc From 05b6ac27a1c1890da96f6f00ac7e6a2bde246193 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 00:42:48 +0100 Subject: [PATCH 34/53] feat(Voice): timestamp format compatible with older versions of Python --- sinch/core/deserializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/core/deserializers.py b/sinch/core/deserializers.py index 5e38b25..8ca137c 100644 --- a/sinch/core/deserializers.py +++ b/sinch/core/deserializers.py @@ -2,4 +2,4 @@ def timestamp_to_datetime_in_utc_deserializer(timestamp: str): - return datetime.fromisoformat(timestamp + 'Z') + return datetime.fromisoformat(timestamp + "+00:00") From c8a47d0aae2873899944df809c9f079b260393b5 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 01:08:23 +0100 Subject: [PATCH 35/53] feat(SVAML): __all__ added --- .../domains/voice/models/svaml/actions/__init__.py | 13 +++++++++++++ .../voice/models/svaml/{ => actions}/actions.py | 0 .../voice/models/svaml/instructions/__init__.py | 11 +++++++++++ .../models/svaml/{ => instructions}/instructions.py | 0 4 files changed, 24 insertions(+) create mode 100644 sinch/domains/voice/models/svaml/actions/__init__.py rename sinch/domains/voice/models/svaml/{ => actions}/actions.py (100%) create mode 100644 sinch/domains/voice/models/svaml/instructions/__init__.py rename sinch/domains/voice/models/svaml/{ => instructions}/instructions.py (100%) diff --git a/sinch/domains/voice/models/svaml/actions/__init__.py b/sinch/domains/voice/models/svaml/actions/__init__.py new file mode 100644 index 0000000..94f6d89 --- /dev/null +++ b/sinch/domains/voice/models/svaml/actions/__init__.py @@ -0,0 +1,13 @@ +from .actions import ( + AnsweringMachineDetection, CallHeader, HangupAction, + ContinueAction, ConnectPstnAction, ConnectMxpAction, + Option, MenuOption, ConnectSipAction, ConnectConfAction, + RunMenuAction, ParkAction +) + +__all__ = [ + "AnsweringMachineDetection", "CallHeader", "HangupAction", + "ContinueAction", "ConnectPstnAction", "ConnectMxpAction", + "Option", "MenuOption", "ConnectSipAction", "ConnectConfAction", + "RunMenuAction", "ParkAction" +] diff --git a/sinch/domains/voice/models/svaml/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py similarity index 100% rename from sinch/domains/voice/models/svaml/actions.py rename to sinch/domains/voice/models/svaml/actions/actions.py diff --git a/sinch/domains/voice/models/svaml/instructions/__init__.py b/sinch/domains/voice/models/svaml/instructions/__init__.py new file mode 100644 index 0000000..b4cc255 --- /dev/null +++ b/sinch/domains/voice/models/svaml/instructions/__init__.py @@ -0,0 +1,11 @@ +from .instructions import ( + TranscriptionOptions, RecordingOptions, PlayFileInstruction, + SayInstruction, SendDtmfInstruction, SetCookieInstruction, + AnswerInstruction, StartRecordingInstruction, StopRecordingInstruction +) + +__all__ = [ + "TranscriptionOptions", "RecordingOptions", "PlayFileInstruction", + "SayInstruction", "SendDtmfInstruction", "SetCookieInstruction", + "AnswerInstruction", "StartRecordingInstruction", "StopRecordingInstruction" +] diff --git a/sinch/domains/voice/models/svaml/instructions.py b/sinch/domains/voice/models/svaml/instructions/instructions.py similarity index 100% rename from sinch/domains/voice/models/svaml/instructions.py rename to sinch/domains/voice/models/svaml/instructions/instructions.py From 69b20962a4a509489a3d805c64de2c76de60e67c Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 01:25:30 +0100 Subject: [PATCH 36/53] feat(Voice): type hint for Destination type added --- sinch/domains/voice/models/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index 692850d..20e461b 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TypedDict +from typing import TypedDict, Literal @dataclass @@ -25,7 +25,7 @@ class ApplicationNumber: class Destination(TypedDict): - type: str + type: Literal["number", "username"] endpoint: str From c344d7a607f4ed78f7562b718cbcba61d958d4f0 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 01:33:43 +0100 Subject: [PATCH 37/53] feat(Voice): mode type hints added --- sinch/domains/voice/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/__init__.py b/sinch/domains/voice/models/__init__.py index 20e461b..dc15940 100644 --- a/sinch/domains/voice/models/__init__.py +++ b/sinch/domains/voice/models/__init__.py @@ -30,6 +30,6 @@ class Destination(TypedDict): class ConferenceDTMFOptions(TypedDict): - mode: str + mode: Literal["ignore", "forward", "detect"] max_digits: int timeout_mills: int From e0d0a98e52aa0d68604a2590bc6670393c08e110 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 12:33:06 +0100 Subject: [PATCH 38/53] feat(SVAML): Indications enum added --- .../voice/models/svaml/actions/actions.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index d32dee9..eaccadb 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -1,9 +1,52 @@ +from enum import Enum from dataclasses import dataclass from typing import Optional, List, TypedDict from sinch.core.models.base_model import SinchRequestBaseModel from sinch.domains.voice import Destination, ConferenceDTMFOptions +class Indications(Enum): + AUSTRIA = "at" + AUSTRALIA = "au" + BULGARIA = "bg" + BRAZIL = "br" + BELGIUM = "be" + SWITZERLAND = "ch" + CHILE = "cl" + CHINA = "cn" + CZECH_REPUBLIC = "cz" + GERMANY = "de" + DENMARK = "dk" + ESTONIA = "ee" + SPAIN = "es" + FINLAND = "fi" + FRANCE = "fr" + GREECE = "gr" + HUNGARY = "hu" + ISRAEL = "il" + INDIA = "in" + ITALY = "it" + LITHUANIA = "lt" + JAPAN = "jp" + MEXICO = "mx" + MALAYSIA = "my" + NETHERLANDS = "nl" + NORWAY = "no" + NEW_ZEALAND = "nz" + PHILIPPINES = "ph" + POLAND = "pl" + PORTUGAL = "pt" + RUSSIA = "ru" + SWEDEN = "se" + SINGAPORE = "sg" + THAILAND = "th" + UNITED_KINGDOM = "uk" + UNITED_STATES = "us" + TAIWAN = "tw" + VENEZUELA = "ve" + SOUTH_AFRICA = "za" + + class AnsweringMachineDetection(TypedDict): enabled: bool From 5a0d073150a84b8a074a9a39cb5557248382c5a2 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 14:13:35 +0100 Subject: [PATCH 39/53] fix(Voice): missing property transformation --- sinch/domains/voice/models/svaml/actions/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index eaccadb..8ab1a50 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -144,6 +144,9 @@ def as_dict(self): if payload.get("suppress_callbacks"): payload["suppressCallbacks"] = payload.pop("suppress_callbacks") + if payload.get("call_headers"): + payload["callHeaders"] = payload.pop("call_headers") + return payload From 96dfa8a809fda965a06b059b95ecd1f61e1dce9a Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 14:31:25 +0100 Subject: [PATCH 40/53] fix(Voice): better Enum naming --- sinch/domains/voice/enums.py | 44 ++++++++++++++++++- .../voice/models/svaml/actions/actions.py | 43 ------------------ 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py index b36b6ba..1e1018a 100644 --- a/sinch/domains/voice/enums.py +++ b/sinch/domains/voice/enums.py @@ -22,7 +22,7 @@ class ConferenceCommand(Enum): RESUME = "resume" -class ConferenceMusicOnHold(Enum): +class MusicOnHold(Enum): RING = "ring" MUSIC_1 = "music1" MUSIC_2 = "music2" @@ -33,3 +33,45 @@ class ConferenceDTMFOptionsMode(Enum): IGNORE = "ignore" FORWARD = "forward" DETECT = "detect" + + +class Indications(Enum): + AUSTRIA = "at" + AUSTRALIA = "au" + BULGARIA = "bg" + BRAZIL = "br" + BELGIUM = "be" + SWITZERLAND = "ch" + CHILE = "cl" + CHINA = "cn" + CZECH_REPUBLIC = "cz" + GERMANY = "de" + DENMARK = "dk" + ESTONIA = "ee" + SPAIN = "es" + FINLAND = "fi" + FRANCE = "fr" + GREECE = "gr" + HUNGARY = "hu" + ISRAEL = "il" + INDIA = "in" + ITALY = "it" + LITHUANIA = "lt" + JAPAN = "jp" + MEXICO = "mx" + MALAYSIA = "my" + NETHERLANDS = "nl" + NORWAY = "no" + NEW_ZEALAND = "nz" + PHILIPPINES = "ph" + POLAND = "pl" + PORTUGAL = "pt" + RUSSIA = "ru" + SWEDEN = "se" + SINGAPORE = "sg" + THAILAND = "th" + UNITED_KINGDOM = "uk" + UNITED_STATES = "us" + TAIWAN = "tw" + VENEZUELA = "ve" + SOUTH_AFRICA = "za" diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index 8ab1a50..91e8ac6 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -1,52 +1,9 @@ -from enum import Enum from dataclasses import dataclass from typing import Optional, List, TypedDict from sinch.core.models.base_model import SinchRequestBaseModel from sinch.domains.voice import Destination, ConferenceDTMFOptions -class Indications(Enum): - AUSTRIA = "at" - AUSTRALIA = "au" - BULGARIA = "bg" - BRAZIL = "br" - BELGIUM = "be" - SWITZERLAND = "ch" - CHILE = "cl" - CHINA = "cn" - CZECH_REPUBLIC = "cz" - GERMANY = "de" - DENMARK = "dk" - ESTONIA = "ee" - SPAIN = "es" - FINLAND = "fi" - FRANCE = "fr" - GREECE = "gr" - HUNGARY = "hu" - ISRAEL = "il" - INDIA = "in" - ITALY = "it" - LITHUANIA = "lt" - JAPAN = "jp" - MEXICO = "mx" - MALAYSIA = "my" - NETHERLANDS = "nl" - NORWAY = "no" - NEW_ZEALAND = "nz" - PHILIPPINES = "ph" - POLAND = "pl" - PORTUGAL = "pt" - RUSSIA = "ru" - SWEDEN = "se" - SINGAPORE = "sg" - THAILAND = "th" - UNITED_KINGDOM = "uk" - UNITED_STATES = "us" - TAIWAN = "tw" - VENEZUELA = "ve" - SOUTH_AFRICA = "za" - - class AnsweringMachineDetection(TypedDict): enabled: bool From e12a149d46968a9611aee7ad82aaa1aac74c4fed Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 14:38:48 +0100 Subject: [PATCH 41/53] fix: remove redundant property --- sinch/domains/voice/models/svaml/actions/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index 91e8ac6..5728ae8 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -109,7 +109,6 @@ def as_dict(self): @dataclass class ConnectConfAction(SinchRequestBaseModel): - destination: Optional[Destination] conference_id: str name: str = "connectConf" conference_dtmf_options: Optional[ConferenceDTMFOptions] = None From 164eb956f21abbf73608226bcc0bf3da782919f8 Mon Sep 17 00:00:00 2001 From: 650elx Date: Wed, 20 Mar 2024 22:16:34 +0100 Subject: [PATCH 42/53] fix(SVAML): missing transformations added --- .../voice/models/svaml/actions/actions.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index 5728ae8..bf50178 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -114,6 +114,13 @@ class ConnectConfAction(SinchRequestBaseModel): conference_dtmf_options: Optional[ConferenceDTMFOptions] = None moh: Optional[str] = None + def as_dict(self): + payload = super().as_dict() + if payload.get("conference_dtmf_options"): + payload["conferenceDtmfOptions"] = payload.pop("conference_dtmf_options") + + return payload + @dataclass class RunMenuAction(SinchRequestBaseModel): @@ -124,6 +131,16 @@ class RunMenuAction(SinchRequestBaseModel): enable_voice: Optional[bool] = None menus: Optional[List[MenuOption]] = None + def as_dict(self): + payload = super().as_dict() + if payload.get("main_menu"): + payload["mainMenu"] = payload.pop("main_menu") + + if payload.get("enable_voice"): + payload["enableVoice"] = payload.pop("enable_voice") + + return payload + @dataclass class ParkAction(SinchRequestBaseModel): @@ -132,3 +149,16 @@ class ParkAction(SinchRequestBaseModel): intro_prompt: Optional[str] = None hold_prompt: Optional[str] = None max_duration: Optional[int] = None + + def as_dict(self): + payload = super().as_dict() + if payload.get("intro_prompt"): + payload["introPrompt"] = payload.pop("intro_prompt") + + if payload.get("hold_prompt"): + payload["holdPrompt"] = payload.pop("hold_prompt") + + if payload.get("max_duration"): + payload["maxDuration"] = payload.pop("max_duration") + + return payload From a0dbff000937c6e00df18a1fb5ba7891f0059948 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 5 Apr 2024 17:25:24 +0200 Subject: [PATCH 43/53] feat(SVAML): missing transformation added --- sinch/domains/voice/models/svaml/actions/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index bf50178..76f0744 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -116,6 +116,9 @@ class ConnectConfAction(SinchRequestBaseModel): def as_dict(self): payload = super().as_dict() + if payload.get("conference_id"): + payload["conferenceId"] = payload.pop("conference_id") + if payload.get("conference_dtmf_options"): payload["conferenceDtmfOptions"] = payload.pop("conference_dtmf_options") From 0cba99cca175cc4b333b560ccc53f5a82e98af82 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 5 Apr 2024 22:23:19 +0200 Subject: [PATCH 44/53] feat(Voice): domain type hint added --- sinch/domains/voice/models/callouts/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/callouts/requests.py b/sinch/domains/voice/models/callouts/requests.py index 2efe21b..753ce2f 100644 --- a/sinch/domains/voice/models/callouts/requests.py +++ b/sinch/domains/voice/models/callouts/requests.py @@ -9,7 +9,7 @@ class TextToSpeechVoiceCalloutRequest(SinchRequestBaseModel): destination: Destination cli: str dtmf: str - domain: str + domain: Literal["pstn", "mxp"] custom: str locale: str text: str From 9def0cc73c8c28a75e31a228bb455af42f0ebc60 Mon Sep 17 00:00:00 2001 From: 650elx Date: Sat, 6 Apr 2024 00:10:20 +0200 Subject: [PATCH 45/53] feat(Voice): missing transformation added --- .../voice/models/svaml/actions/actions.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index 76f0744..a8f8d5c 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -81,6 +81,25 @@ class MenuOption(SinchRequestBaseModel): max_timeout_mills: Optional[int] = None options: Optional[List[Option]] = None + def as_dict(self): + payload = super().as_dict() + if payload.get("main_prompt"): + payload["mainPrompt"] = payload.pop("main_prompt") + + if payload.get("repeat_prompt"): + payload["repeatPrompt"] = payload.pop("repeat_prompt") + + if payload.get("max_digits"): + payload["maxDigits"] = payload.pop("max_digits") + + if payload.get("timeout_mills"): + payload["timeoutMills"] = payload.pop("timeout_mills") + + if payload.get("max_timeout_mills"): + payload["maxTimeoutMills"] = payload.pop("max_timeout_mills") + + return payload + @dataclass class ConnectSipAction(SinchRequestBaseModel): From b41187da18ecc0593abbdbbe99a945f027096573 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 11:04:27 +0200 Subject: [PATCH 46/53] feat(Voice): timestamp validation added --- sinch/core/deserializers.py | 7 +++++++ tests/unit/test_serdes.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/sinch/core/deserializers.py b/sinch/core/deserializers.py index 8ca137c..f81e5fd 100644 --- a/sinch/core/deserializers.py +++ b/sinch/core/deserializers.py @@ -2,4 +2,11 @@ def timestamp_to_datetime_in_utc_deserializer(timestamp: str): + """ + Older Python versions (like 3.9) do not support "Z" as a TZ information. + One needs to use '+00:00' to represent UTC tz. + """ + if timestamp.endswith("Z"): + timestamp = timestamp[:-1] + return datetime.fromisoformat(timestamp + "+00:00") diff --git a/tests/unit/test_serdes.py b/tests/unit/test_serdes.py index 9b15ba8..d8e16f4 100644 --- a/tests/unit/test_serdes.py +++ b/tests/unit/test_serdes.py @@ -63,3 +63,9 @@ def test_timestamp_to_datetime_in_utc_deserializer(): datetime_in_utc = timestamp_to_datetime_in_utc_deserializer("2024-02-15T13:01:29") assert isinstance(datetime_in_utc, datetime.datetime) assert datetime_in_utc.tzinfo == datetime.timezone.utc + + +def test_timestamp_to_datetime_in_utc_deserializer_with_added_tz(): + datetime_in_utc = timestamp_to_datetime_in_utc_deserializer("2024-02-15T13:01:29Z") + assert isinstance(datetime_in_utc, datetime.datetime) + assert datetime_in_utc.tzinfo == datetime.timezone.utc From cac25fe92f7bfa7e9d9576694a8febc8839828b1 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 11:55:08 +0200 Subject: [PATCH 47/53] feat(Voice): type hits for instructions added --- sinch/domains/voice/__init__.py | 15 +++++++------ sinch/domains/voice/models/calls/requests.py | 6 +---- .../voice/models/svaml/actions/actions.py | 22 +++++++++++-------- .../models/svaml/instructions/instructions.py | 19 ++++++++++------ 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/sinch/domains/voice/__init__.py b/sinch/domains/voice/__init__.py index 29c4ce4..8f997af 100644 --- a/sinch/domains/voice/__init__.py +++ b/sinch/domains/voice/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Literal +from typing import List, Literal, Union from sinch.domains.voice.endpoints.callouts.callout import CalloutEndpoint from sinch.domains.voice.endpoints.calls.get_call import GetCallEndpoint from sinch.domains.voice.endpoints.calls.update_call import UpdateCallEndpoint @@ -28,8 +28,7 @@ from sinch.domains.voice.models.calls.requests import ( GetVoiceCallRequest, UpdateVoiceCallRequest, - ManageVoiceCallRequest, - Action + ManageVoiceCallRequest ) from sinch.domains.voice.models.calls.responses import ( GetVoiceCallResponse, @@ -62,6 +61,8 @@ GetCallbackUrlsVoiceApplicationResponse, QueryNumberVoiceApplicationResponse ) +from sinch.domains.voice.models.svaml.actions.actions import Action +from sinch.domains.voice.models.svaml.instructions.instructions import Instruction class Callouts: @@ -186,8 +187,8 @@ def get(self, call_id) -> GetVoiceCallResponse: def update( self, call_id: str, - instructions: list, - action: Action, + instructions: Union[list, List[Instruction]], + action: Union[dict, Action] ) -> UpdateVoiceCallResponse: return self._sinch.configuration.transport.request( UpdateCallEndpoint( @@ -203,8 +204,8 @@ def manage_with_call_leg( self, call_id: str, call_leg: str, - instructions: list, - action: Action, + instructions: Union[list, List[Instruction]], + action: Union[dict, Action] ) -> ManageVoiceCallResponse: return self._sinch.configuration.transport.request( ManageCallEndpoint( diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py index 28c549a..955714a 100644 --- a/sinch/domains/voice/models/calls/requests.py +++ b/sinch/domains/voice/models/calls/requests.py @@ -1,10 +1,6 @@ from dataclasses import dataclass -from typing import TypedDict from sinch.core.models.base_model import SinchRequestBaseModel - - -class Action(TypedDict): - name: str +from sinch.domains.voice.models.svaml.actions.actions import Action @dataclass diff --git a/sinch/domains/voice/models/svaml/actions/actions.py b/sinch/domains/voice/models/svaml/actions/actions.py index a8f8d5c..125d6b8 100644 --- a/sinch/domains/voice/models/svaml/actions/actions.py +++ b/sinch/domains/voice/models/svaml/actions/actions.py @@ -1,7 +1,11 @@ from dataclasses import dataclass from typing import Optional, List, TypedDict from sinch.core.models.base_model import SinchRequestBaseModel -from sinch.domains.voice import Destination, ConferenceDTMFOptions +from sinch.domains.voice.models import Destination, ConferenceDTMFOptions + + +class Action(SinchRequestBaseModel): + name: str class AnsweringMachineDetection(TypedDict): @@ -14,17 +18,17 @@ class CallHeader(TypedDict): @dataclass -class HangupAction(SinchRequestBaseModel): +class HangupAction(Action): name: str = "hangup" @dataclass -class ContinueAction(SinchRequestBaseModel): +class ContinueAction(Action): name: str = "continue" @dataclass -class ConnectPstnAction(SinchRequestBaseModel): +class ConnectPstnAction(Action): name: str = "connectPstn" number: Optional[str] = None locale: Optional[str] = None @@ -51,7 +55,7 @@ def as_dict(self): @dataclass -class ConnectMxpAction(SinchRequestBaseModel): +class ConnectMxpAction(Action): name: str = "connectMxp" destination: Optional[Destination] = None call_headers: Optional[List[CallHeader]] = None @@ -102,7 +106,7 @@ def as_dict(self): @dataclass -class ConnectSipAction(SinchRequestBaseModel): +class ConnectSipAction(Action): destination: Optional[Destination] name: str = "connectSip" max_duration: Optional[int] = None @@ -127,7 +131,7 @@ def as_dict(self): @dataclass -class ConnectConfAction(SinchRequestBaseModel): +class ConnectConfAction(Action): conference_id: str name: str = "connectConf" conference_dtmf_options: Optional[ConferenceDTMFOptions] = None @@ -145,7 +149,7 @@ def as_dict(self): @dataclass -class RunMenuAction(SinchRequestBaseModel): +class RunMenuAction(Action): name: str = "runMenu" barge: Optional[bool] = None locale: Optional[str] = None @@ -165,7 +169,7 @@ def as_dict(self): @dataclass -class ParkAction(SinchRequestBaseModel): +class ParkAction(Action): name: str = "park" locale: Optional[str] = None intro_prompt: Optional[str] = None diff --git a/sinch/domains/voice/models/svaml/instructions/instructions.py b/sinch/domains/voice/models/svaml/instructions/instructions.py index c9ac2ad..8b254a2 100644 --- a/sinch/domains/voice/models/svaml/instructions/instructions.py +++ b/sinch/domains/voice/models/svaml/instructions/instructions.py @@ -3,6 +3,11 @@ from sinch.core.models.base_model import SinchRequestBaseModel +@dataclass +class Instruction(SinchRequestBaseModel): + pass + + @dataclass class TranscriptionOptions(SinchRequestBaseModel): enabled: str = None @@ -32,43 +37,43 @@ def as_dict(self): @dataclass -class PlayFileInstruction(SinchRequestBaseModel): +class PlayFileInstruction(Instruction): ids: List[List[str]] locale: str name: str = "playFiles" @dataclass -class SayInstruction(SinchRequestBaseModel): +class SayInstruction(Instruction): name: str = "say" text: Optional[str] = None locale: Optional[str] = None @dataclass -class SendDtmfInstruction(SinchRequestBaseModel): +class SendDtmfInstruction(Instruction): name: str = "sendDtmf" value: Optional[str] = None @dataclass -class SetCookieInstruction(SinchRequestBaseModel): +class SetCookieInstruction(Instruction): name: str = "setCookie" key: Optional[str] = None value: Optional[str] = None @dataclass -class AnswerInstruction(SinchRequestBaseModel): +class AnswerInstruction(Instruction): name: str = "answer" @dataclass -class StartRecordingInstruction(SinchRequestBaseModel): +class StartRecordingInstruction(Instruction): name: str = "startRecording" options: Optional[RecordingOptions] = None @dataclass -class StopRecordingInstruction(SinchRequestBaseModel): +class StopRecordingInstruction(Instruction): name: str = "stopRecording" From e7d795fa1e6b34881449d6f8a1466d79c9385ee8 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 13:31:54 +0200 Subject: [PATCH 48/53] fix(Voice): missing type added --- sinch/domains/voice/endpoints/calls/get_call.py | 5 ++++- sinch/domains/voice/models/calls/responses.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py index 5013886..87d87a4 100644 --- a/sinch/domains/voice/endpoints/calls/get_call.py +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -24,7 +24,10 @@ def build_url(self, sinch) -> str: def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: super().handle_response(response) return GetVoiceCallResponse( - from_=response.body.get("from"), + from_=Destination( + type=response.body["from"]["type"], + endpoint=response.body["from"]["endpoint"] + ), to=Destination( type=response.body["to"]["type"], endpoint=response.body["to"]["endpoint"] diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py index 218f666..c0b7e13 100644 --- a/sinch/domains/voice/models/calls/responses.py +++ b/sinch/domains/voice/models/calls/responses.py @@ -6,7 +6,7 @@ @dataclass class GetVoiceCallResponse(SinchBaseModel): - from_: str + from_: Destination to: Destination domain: str call_id: str From 3a3e3c616c1ca7cee842f09d84fcb4b831e1dff3 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 13:44:13 +0200 Subject: [PATCH 49/53] fix(Voice): response handling improved --- sinch/domains/voice/endpoints/calls/get_call.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sinch/domains/voice/endpoints/calls/get_call.py b/sinch/domains/voice/endpoints/calls/get_call.py index 87d87a4..5e5b450 100644 --- a/sinch/domains/voice/endpoints/calls/get_call.py +++ b/sinch/domains/voice/endpoints/calls/get_call.py @@ -23,15 +23,17 @@ def build_url(self, sinch) -> str: def handle_response(self, response: HTTPResponse) -> GetVoiceCallResponse: super().handle_response(response) + call_origin = response.body.get("from") + call_destination = response.body.get("to") return GetVoiceCallResponse( from_=Destination( - type=response.body["from"]["type"], - endpoint=response.body["from"]["endpoint"] - ), + type=call_origin["type"], + endpoint=call_origin.get["endpoint"], + ) if call_origin else None, to=Destination( - type=response.body["to"]["type"], - endpoint=response.body["to"]["endpoint"] - ), + type=call_destination.get("type"), + endpoint=call_destination.get("endpoint") + ) if call_destination else None, domain=response.body.get("domain"), call_id=response.body.get("callId"), duration=response.body.get("duration"), From d74934d916f075cc669ef8976030efa0506bd31e Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 15:01:32 +0200 Subject: [PATCH 50/53] fix(Voice): wrong type --- sinch/domains/voice/models/calls/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/calls/responses.py b/sinch/domains/voice/models/calls/responses.py index c0b7e13..f0e87ef 100644 --- a/sinch/domains/voice/models/calls/responses.py +++ b/sinch/domains/voice/models/calls/responses.py @@ -15,7 +15,7 @@ class GetVoiceCallResponse(SinchBaseModel): result: str reason: str timestamp: datetime - custom: dict + custom: str user_rate: Price debit: Price From 01a2d10be567a069d9f739c32fa1189103d308c3 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 15:10:34 +0200 Subject: [PATCH 51/53] feat(Voice): capability enum added --- sinch/domains/voice/enums.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sinch/domains/voice/enums.py b/sinch/domains/voice/enums.py index 1e1018a..40de184 100644 --- a/sinch/domains/voice/enums.py +++ b/sinch/domains/voice/enums.py @@ -75,3 +75,8 @@ class Indications(Enum): TAIWAN = "tw" VENEZUELA = "ve" SOUTH_AFRICA = "za" + + +class Capability(Enum): + VOCE = "voice" + SMS = "sms" From 73d7ea268a974731d90fa441020c38ea8fe02301 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 15:16:12 +0200 Subject: [PATCH 52/53] feat(Voice): more comples type hint added --- sinch/domains/voice/models/calls/requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py index 955714a..06fa1c2 100644 --- a/sinch/domains/voice/models/calls/requests.py +++ b/sinch/domains/voice/models/calls/requests.py @@ -1,6 +1,8 @@ +from typing import Union, List from dataclasses import dataclass from sinch.core.models.base_model import SinchRequestBaseModel from sinch.domains.voice.models.svaml.actions.actions import Action +from sinch.domains.voice.models.svaml.instructions.instructions import Instruction @dataclass @@ -19,5 +21,5 @@ class UpdateVoiceCallRequest(SinchRequestBaseModel): class ManageVoiceCallRequest(SinchRequestBaseModel): call_id: str call_leg: str - instructions: list + instructions: Union[list, List[Instruction]] action: Action From 9acaf659e7a7f0192a0136cca5ab525a785ab997 Mon Sep 17 00:00:00 2001 From: 650elx Date: Fri, 10 May 2024 15:17:30 +0200 Subject: [PATCH 53/53] feat(Voice): better type annotation --- sinch/domains/voice/models/calls/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sinch/domains/voice/models/calls/requests.py b/sinch/domains/voice/models/calls/requests.py index 06fa1c2..a49e29f 100644 --- a/sinch/domains/voice/models/calls/requests.py +++ b/sinch/domains/voice/models/calls/requests.py @@ -13,7 +13,7 @@ class GetVoiceCallRequest(SinchRequestBaseModel): @dataclass class UpdateVoiceCallRequest(SinchRequestBaseModel): call_id: str - instructions: list + instructions: Union[list, List[Instruction]] action: Action