voice-runtime 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {voice_runtime-0.2.0/voice_runtime.egg-info → voice_runtime-0.3.0}/PKG-INFO +1 -1
  2. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/pyproject.toml +1 -1
  3. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_twilio_call.py +90 -1
  4. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/twilio_call.py +17 -5
  5. {voice_runtime-0.2.0 → voice_runtime-0.3.0/voice_runtime.egg-info}/PKG-INFO +1 -1
  6. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/LICENSE +0 -0
  7. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/README.md +0 -0
  8. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/setup.cfg +0 -0
  9. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_audio.py +0 -0
  10. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_azure_stt.py +0 -0
  11. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_azure_tts.py +0 -0
  12. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_elevenlabs_tts.py +0 -0
  13. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_factories.py +0 -0
  14. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc165_stt_protocol.py +0 -0
  15. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc166_on_committed.py +0 -0
  16. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc170_resilience.py +0 -0
  17. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc193_twilio_sms.py +0 -0
  18. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc199_on_recognizing.py +0 -0
  19. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc236_unique_mark_names.py +0 -0
  20. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc260_gap_a_tts_on_error.py +0 -0
  21. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc260_gap_b_ws_disconnect.py +0 -0
  22. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc260_gap_c_stt_fatal_errors.py +0 -0
  23. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc260_gap_e_stt_ready.py +0 -0
  24. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc260_gap_f_loop_warning.py +0 -0
  25. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc267_mock_providers.py +0 -0
  26. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc271_mock_bridge.py +0 -0
  27. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_nc340_stt_feed_survives_reconnect.py +0 -0
  28. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_session.py +0 -0
  29. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_stt_lifecycle.py +0 -0
  30. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_stt_tee.py +0 -0
  31. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_twilio_ws.py +0 -0
  32. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr002_twilio_http_timeout.py +0 -0
  33. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr003_rest_first_call_end_31921.py +0 -0
  34. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr004_migration_example.py +0 -0
  35. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr004_region_hangup.py +0 -0
  36. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr005_stt_session_task_ownership.py +0 -0
  37. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr006_migration_example.py +0 -0
  38. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr006_region_cdr.py +0 -0
  39. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr007_media_teardown_regions.py +0 -0
  40. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/tests/test_vr009_azure_stt_language_id.py +0 -0
  41. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/__init__.py +0 -0
  42. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/audio.py +0 -0
  43. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/mock/__init__.py +0 -0
  44. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/mock/stt.py +0 -0
  45. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/mock/tts.py +0 -0
  46. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/providers/__init__.py +0 -0
  47. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/providers/azure_stt.py +0 -0
  48. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/providers/azure_tts.py +0 -0
  49. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/providers/elevenlabs_stt.py +0 -0
  50. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/providers/elevenlabs_tts.py +0 -0
  51. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/session.py +0 -0
  52. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/stt.py +0 -0
  53. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/stt_tee.py +0 -0
  54. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transport.py +0 -0
  55. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/__init__.py +0 -0
  56. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/_twilio_client.py +0 -0
  57. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/_twilio_models.py +0 -0
  58. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/mock_bridge.py +0 -0
  59. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/twilio_sms.py +0 -0
  60. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/transports/twilio_ws.py +0 -0
  61. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime/tts.py +0 -0
  62. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime.egg-info/SOURCES.txt +0 -0
  63. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime.egg-info/dependency_links.txt +0 -0
  64. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime.egg-info/requires.txt +0 -0
  65. {voice_runtime-0.2.0 → voice_runtime-0.3.0}/voice_runtime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-runtime
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Provider-agnostic voice call runtime for telephony projects
5
5
  Author: Kasper Heikkinen, Sami Heikkinen
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voice-runtime"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Provider-agnostic voice call runtime for telephony projects"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -158,6 +158,91 @@ class TestInitiateOutboundCall:
158
158
  mock_client.calls.create.assert_called_once()
159
159
 
160
160
 
161
+ _REGION_ENV = {
162
+ "TWILIO_ACCOUNT_SID": "",
163
+ "TWILIO_AUTH_TOKEN": "",
164
+ "TWILIO_PHONE_NUMBER": "+15551234567",
165
+ "VOICE_STREAM_URL": "https://example.ngrok.io",
166
+ }
167
+
168
+
169
+ def _ie1_region():
170
+ from voice_runtime.transports.twilio_call import TwilioRegion
171
+
172
+ return TwilioRegion(
173
+ api_host="api.dublin.ie1.twilio.com",
174
+ account_sid="AC1",
175
+ auth=("SK1", "secret"),
176
+ )
177
+
178
+
179
+ class TestInitiateOutboundCallRegion:
180
+ """An explicit region places the call with its own identity, no Auth Token."""
181
+
182
+ def test_region_uses_explicit_client_without_auth_token(self):
183
+ from voice_runtime.transports.twilio_call import initiate_outbound_call
184
+
185
+ mock_client = MagicMock()
186
+ mock_client.calls.create.return_value = MagicMock(sid="CA9")
187
+ region = _ie1_region()
188
+
189
+ with (
190
+ patch.dict("os.environ", _REGION_ENV, clear=False),
191
+ patch(
192
+ "voice_runtime.transports.twilio_call.build_explicit_twilio_client",
193
+ return_value=mock_client,
194
+ ) as build_explicit,
195
+ patch(
196
+ "voice_runtime.transports.twilio_call.build_twilio_client"
197
+ ) as build_ambient,
198
+ ):
199
+ sid = initiate_outbound_call("+358401234567", region=region)
200
+
201
+ assert sid == "CA9"
202
+ build_explicit.assert_called_once_with(region)
203
+ build_ambient.assert_not_called()
204
+ _, kwargs = mock_client.calls.create.call_args
205
+ assert kwargs["to"] == "+358401234567"
206
+ assert kwargs["from_"] == "+15551234567"
207
+ assert 'url="wss://example.ngrok.io/voice"' in kwargs["twiml"]
208
+
209
+ def test_region_still_requires_stream_url(self):
210
+ from voice_runtime.transports.twilio_call import initiate_outbound_call
211
+
212
+ env = {**_REGION_ENV, "VOICE_STREAM_URL": ""}
213
+ with (
214
+ patch.dict("os.environ", env, clear=False),
215
+ pytest.raises(RuntimeError, match="VOICE_STREAM_URL"),
216
+ ):
217
+ initiate_outbound_call("+358401234567", region=_ie1_region())
218
+
219
+ def test_region_still_requires_phone_number(self):
220
+ from voice_runtime.transports.twilio_call import initiate_outbound_call
221
+
222
+ env = {**_REGION_ENV, "TWILIO_PHONE_NUMBER": ""}
223
+ with (
224
+ patch.dict("os.environ", env, clear=False),
225
+ pytest.raises(RuntimeError, match="TWILIO_PHONE_NUMBER"),
226
+ ):
227
+ initiate_outbound_call("+358401234567", region=_ie1_region())
228
+
229
+ def test_region_is_revalidated(self):
230
+ from voice_runtime.transports.twilio_call import initiate_outbound_call
231
+
232
+ with (
233
+ patch.dict("os.environ", _REGION_ENV, clear=False),
234
+ pytest.raises(ValueError, match="Invalid Twilio API host"),
235
+ ):
236
+ initiate_outbound_call(
237
+ "+358401234567",
238
+ region={
239
+ "api_host": "evil.example.com",
240
+ "account_sid": "AC1",
241
+ "auth": ("a", "b"),
242
+ },
243
+ )
244
+
245
+
161
246
  class TestHangupCall:
162
247
  """ninchat_voice NC-362: supervisor-side reap teardown rides this seam."""
163
248
 
@@ -184,7 +269,11 @@ class TestHangupCall:
184
269
  from voice_runtime.transports.twilio_call import hangup_call
185
270
 
186
271
  with (
187
- patch.dict("os.environ", {"TWILIO_ACCOUNT_SID": "", "TWILIO_AUTH_TOKEN": ""}, clear=False),
272
+ patch.dict(
273
+ "os.environ",
274
+ {"TWILIO_ACCOUNT_SID": "", "TWILIO_AUTH_TOKEN": ""},
275
+ clear=False,
276
+ ),
188
277
  pytest.raises(RuntimeError, match="TWILIO_ACCOUNT_SID"),
189
278
  ):
190
279
  hangup_call("CA-x")
@@ -84,17 +84,23 @@ def build_route_stream_xml(stream_url: str, route_token: str) -> str:
84
84
  </Response>""".strip()
85
85
 
86
86
 
87
- def initiate_outbound_call(phone: str) -> str:
87
+ def initiate_outbound_call(phone: str, *, region: TwilioRegion | None = None) -> str:
88
88
  """Initiate an outbound Twilio call with Media Streams.
89
89
 
90
90
  Args:
91
91
  phone: Phone number to call (E.164 format).
92
+ region: Explicit routing and identity (e.g. an IE1 API key pair).
93
+ When given, the call is placed through that region and
94
+ TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN are not read. Without it,
95
+ the ambient Account Auth Token is used.
92
96
 
93
97
  Returns:
94
98
  call_sid from Twilio.
95
99
 
96
100
  Raises:
97
- RuntimeError: If VOICE_STREAM_URL or Twilio credentials are missing.
101
+ RuntimeError: If VOICE_STREAM_URL, TWILIO_PHONE_NUMBER or, without
102
+ a region, the Twilio credentials are missing.
103
+ ValueError: If region is not a valid TwilioRegion.
98
104
  """
99
105
  account_sid, auth_token, phone_number, stream_url = _get_twilio_env()
100
106
 
@@ -102,15 +108,21 @@ def initiate_outbound_call(phone: str) -> str:
102
108
  raise RuntimeError(
103
109
  "Set VOICE_STREAM_URL to a public WebSocket URL (use ngrok for local dev)"
104
110
  )
105
- if not account_sid or not auth_token:
106
- raise RuntimeError("TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN required")
111
+ if region is None:
112
+ if not account_sid or not auth_token:
113
+ raise RuntimeError("TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN required")
114
+ else:
115
+ region = TwilioRegion.model_validate(region)
107
116
  if not phone_number:
108
117
  raise RuntimeError("TWILIO_PHONE_NUMBER required")
109
118
 
110
119
  twiml = build_stream_twiml(stream_url)
111
120
  logger.info("Initiating outbound call to %s", phone)
112
121
 
113
- client = build_twilio_client(account_sid, auth_token)
122
+ if region is None:
123
+ client = build_twilio_client(account_sid, auth_token)
124
+ else:
125
+ client = build_explicit_twilio_client(region)
114
126
  call = client.calls.create(
115
127
  to=phone,
116
128
  from_=phone_number,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-runtime
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Provider-agnostic voice call runtime for telephony projects
5
5
  Author: Kasper Heikkinen, Sami Heikkinen
6
6
  License: MIT License
File without changes
File without changes
File without changes