diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e75080a..0534969d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,8 +2,6 @@ name: Test Python SDK
on: [ push ]
env:
- APPLICATION_KEY: ${{ secrets.APPLICATION_KEY }}
- APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }}
AUTH_ORIGIN: ${{ secrets.AUTH_ORIGIN }}
CONVERSATION_ORIGIN: ${{ secrets.CONVERSATION_ORIGIN }}
DISABLE_SSL: ${{ secrets.DISABLE_SSL }}
@@ -24,9 +22,9 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -68,22 +66,17 @@ jobs:
python -m coverage report --skip-empty
- name: Checkout sinch-sdk-mockserver repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: sinch/sinch-sdk-mockserver
token: ${{ secrets.PAT_CI }}
fetch-depth: 0
path: sinch-sdk-mockserver
- - name: Install Docker Compose
- run: |
- sudo apt-get update
- sudo apt-get install -y docker-compose
-
- name: Start mock servers with Docker Compose
run: |
cd sinch-sdk-mockserver
- docker-compose up -d
+ docker compose up -d
- name: Copy feature files
run: |
@@ -110,5 +103,3 @@ jobs:
python -m behave tests/e2e/sms/features
python -m behave tests/e2e/conversation/features
python -m behave tests/e2e/number-lookup/features
-
-
\ No newline at end of file
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
index b2d5dc74..19148549 100644
--- a/MIGRATION_GUIDE.md
+++ b/MIGRATION_GUIDE.md
@@ -8,6 +8,8 @@ This guide lists all removed classes and interfaces from V1 and how to migrate t
> **Note:** Voice and Verification are not yet covered by the new V2 APIs. Support will be added in future releases.
+---
+
## Client Initialization
### Overview
@@ -62,6 +64,8 @@ token_client = SinchClient(
sinch_client.configuration.sms_region = "eu"
```
+---
+
### Conversation Region
**In V1:**
@@ -95,6 +99,8 @@ sinch_client = SinchClient(
sinch_client.configuration.conversation_region = "eu"
```
+---
+
### [`Conversation`](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/conversation)
#### Replacement models
@@ -123,8 +129,8 @@ The Conversation domain API access remains `sinch_client.conversation`; message
| `send()` with `SendConversationMessageRequest` | Use convenience methods: `send_text_message()`, `send_card_message()`, `send_carousel_message()`, `send_choice_message()`, `send_contact_info_message()`, `send_list_message()`, `send_location_message()`, `send_media_message()`, `send_template_message()`
Or `send()` with `app_id`, `message` (dict or `SendMessageRequestBodyDict`), and either `contact_id` or `recipient_identities` |
| `get()` with `GetConversationMessageRequest` | `get()` with `message_id: str` parameter |
| `delete()` with `DeleteConversationMessageRequest` | `delete()` with `message_id: str` parameter |
-| `list()` with `ListConversationMessagesRequest` | In Progress |
-| — | **New in V2:** `update()` with `message_id`, `metadata`, and optional `messages_source`|
+| `list()` with `ListConversationMessagesRequest` | `list()` with the same fields as keyword arguments (see models table above). V2 adds optional `channel_identity`, `start_time`, `end_time`, `channel`, `direction`. Returns **`Paginator[ConversationMessageResponse]`**: use `.content()` for messages on the current page, `.next_page()` to load the next page, or `.iterator()` to walk every message across all pages. |
+| — | **New in V2:** `update()` with `message_id`, `metadata`, and optional `messages_source` |
##### Replacement APIs / attributes
@@ -158,7 +164,7 @@ event = handler.parse_event(request_body)
The Conversation HTTP API still expects the JSON field **`callback_url`**. In V2, use the Python parameter / model field `event_destination_target`; it is serialized as `callback_url` on the wire (same pattern as other domains, e.g. SMS).
-
+---
### [`SMS`](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/sms)
@@ -200,6 +206,7 @@ The Conversation HTTP API still expects the JSON field **`callback_url`**. In V2
#### Replacement APIs
The SMS domain API access remains the same: `sinch.sms.batches` and `sinch.sms.delivery_reports`. However, the underlying models and method signatures have changed.
+
Note that `sinch.sms.groups` and `sinch.sms.inbounds` are not supported yet and will be available in future minor versions.
##### Batches API
@@ -213,17 +220,17 @@ Note that `sinch.sms.groups` and `sinch.sms.inbounds` are not supported yet and
| `update()` with `UpdateBatchRequest` | Use convenience methods: `update_sms()`, `update_binary()`, `update_mms()`
Or `update()` with `UpdateBatchMessageRequest` (Union of `UpdateTextRequestWithBatchId`, `UpdateBinaryRequestWithBatchId`, `UpdateMediaRequestWithBatchId`) |
| `replace()` with `ReplaceBatchRequest` | Use convenience methods: `replace_sms()`, `replace_binary()`, `replace_mms()`
Or `replace()` with `ReplaceBatchRequest` (Union of `ReplaceTextRequest`, `ReplaceBinaryRequest`, `ReplaceMediaRequest`) |
-
+---
##### Delivery Reports API
| Old method | New method in `sms.delivery_reports` |
|------------|-------------------------------------|
-| `list()` with `ListSMSDeliveryReportsRequest` | `list()` the parameters `start_date` and `end_date` now accepts both `str` and `datetime` |
+| `list()` with `ListSMSDeliveryReportsRequest` | `list()` the parameters `start_date` and `end_date` now accepts both `str` and `datetime` |
| `get_for_batch()` with `GetSMSDeliveryReportForBatchRequest` | `get()` with `batch_id: str` and optional parameters: `report_type`, `status`, `code`, `client_reference` |
| `get_for_number()` with `GetSMSDeliveryReportForNumberRequest` | `get_for_number()` with `batch_id: str` and `recipient: str` parameters |
-
+---
### [`Numbers` (Virtual Numbers)](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/numbers)
@@ -271,7 +278,6 @@ sinch_client.numbers.event_destinations.update(hmac_secret="your_hmac_secret")
To obtain a Numbers Sinch Events handler: `sinch_client.numbers.sinch_events(callback_secret)` returns a `SinchEvents` instance; `handler.parse_event(request_body)` returns a `NumberSinchEvent`.
-
```python
# New
from sinch.domains.numbers.sinch_events.v1.events import NumberSinchEvent
@@ -286,7 +292,6 @@ event = handler.parse_event(request_body) # event is a NumberSinchEvent
| **Methods that accept the parameter** | Only `numbers.available.rent_any(..., callback_url=...)` | `numbers.rent(...)`, `numbers.rent_any(...)`, and `numbers.update(...)` accept `event_destination_target` |
| **Parameter name** | `callback_url` | `event_destination_target` |
-
##### Replacement request/response attributes
| Old | New |
diff --git a/README.md b/README.md
index 683d16eb..7d72bfc8 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ For more information on the Sinch APIs on which this SDK is based, refer to the
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Getting started](#getting-started)
-- [Logging]()
+- [Logging](#logging)
## Prerequisites
@@ -79,6 +79,10 @@ sinch_client = SinchClient(
)
```
+### SMS and Conversation regions (V2)
+
+You must set `sms_region` before using the SMS API and `conversation_region` before using the Conversation API—either in the `SinchClient(...)` constructor or on `sinch_client.configuration` before the first call to that product. See [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md) for examples.
+
## Logging
Logging configuration for this SDK utilizes following hierarchy:
@@ -86,34 +90,26 @@ Logging configuration for this SDK utilizes following hierarchy:
2. If `logger_name` configurable was provided, SDK will use logger related to that name. For example: `myapp.sinch` will inherit configuration from the `myapp` logger.
3. If `logger` (logger instance) configurable was provided, SDK will use that particular logger for all its logging operations.
-If all logging returned by this SDK needs to be disabled, usage of `NullHanlder` provided by the standard `logging` module is advised.
+If all logging returned by this SDK needs to be disabled, usage of `NullHandler` provided by the standard `logging` module is advised.
## Sample apps
-Usage example of the `numbers` domain:
+Usage example of the Numbers API via [`VirtualNumbers`](sinch/domains/numbers/virtual_numbers.py) on the client (`sinch_client.numbers`)—`list()` returns your project’s active virtual numbers:
```python
-available_numbers = sinch_client.numbers.available.list(
+paginator = sinch_client.numbers.list(
region_code="US",
- number_type="LOCAL"
+ number_type="LOCAL",
)
+for active_number in paginator.iterator():
+ print(active_number)
```
-Returned values are represented as Python `dataclasses`:
-```python
-ListAvailableNumbersResponse(
- available_numbers=[
- Number(
- phone_number='+17862045855',
- region_code='US',
- type='LOCAL',
- capability=['SMS', 'VOICE'],
- setup_price={'currency_code': 'EUR', 'amount': '0.80'},
- monthly_price={'currency_code': 'EUR', 'amount': '0.80'}
- ...
-```
+Returned values are [Pydantic](https://docs.pydantic.dev/) model instances (for example [`ActiveNumber`](sinch/domains/numbers/models/v1/response/active_number.py)), including fields such as `phone_number`, `region_code`, `type`, and `capabilities`.
+
+More examples live under [examples/snippets](examples/snippets) on the `main` branch.
### Handling exceptions
@@ -125,9 +121,9 @@ Example for Numbers API:
from sinch.domains.numbers.api.v1.exceptions import NumbersException
try:
- nums = sinch_client.numbers.available.list(
+ paginator = sinch_client.numbers.list(
region_code="US",
- number_type="LOCAL"
+ number_type="LOCAL",
)
except NumbersException as err:
pass
@@ -163,4 +159,4 @@ The transport must be a synchronous implementation.
## License
-This project is licensed under the Apache License. See the [LICENSE](license.md) file for the license text.
+This project is licensed under the Apache License. See the [LICENSE](LICENSE) file for the license text.
diff --git a/examples/sinch_events/pyproject.toml b/examples/sinch_events/pyproject.toml
index 206757a7..4fb38639 100644
--- a/examples/sinch_events/pyproject.toml
+++ b/examples/sinch_events/pyproject.toml
@@ -9,8 +9,7 @@ package-mode = false
python = "^3.9"
python-dotenv = "^1.0.0"
flask = "^3.0.0"
-# TODO: Uncomment once v2.0 is released
-# sinch = "^2.0.0"
+sinch = "^2.0.0"
[build-system]
requires = ["poetry-core"]
diff --git a/examples/snippets/README.md b/examples/snippets/README.md
index 35bb6550..b5dd5e2f 100644
--- a/examples/snippets/README.md
+++ b/examples/snippets/README.md
@@ -57,5 +57,5 @@ All available code snippets are located in subdirectories, structured by feature
To execute a specific snippet, navigate to the appropriate subdirectory and run:
```shell
-python run python snippet.py
+python snippet.py
```
\ No newline at end of file
diff --git a/examples/snippets/conversation/messages/delete/snippet.py b/examples/snippets/conversation/messages/delete/snippet.py
index c520eb7c..b67d1a9f 100644
--- a/examples/snippets/conversation/messages/delete/snippet.py
+++ b/examples/snippets/conversation/messages/delete/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/get/snippet.py b/examples/snippets/conversation/messages/get/snippet.py
index 4c3d343c..7e9ff953 100644
--- a/examples/snippets/conversation/messages/get/snippet.py
+++ b/examples/snippets/conversation/messages/get/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/list/snippet.py b/examples/snippets/conversation/messages/list/snippet.py
index ab68afb3..a10dd293 100644
--- a/examples/snippets/conversation/messages/list/snippet.py
+++ b/examples/snippets/conversation/messages/list/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/list_last_messages_by_channel_identity/snippet.py b/examples/snippets/conversation/messages/list_last_messages_by_channel_identity/snippet.py
index 66300d6b..7cde3ee9 100644
--- a/examples/snippets/conversation/messages/list_last_messages_by_channel_identity/snippet.py
+++ b/examples/snippets/conversation/messages/list_last_messages_by_channel_identity/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -18,7 +17,7 @@
conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION"
)
-# Channel identities to fetch the last message
+# The channel identities to fetch the last message for
channel_identities = ["CHANNEL_IDENTITY_1", "CHANNEL_IDENTITY_2"]
messages = sinch_client.conversation.messages.list_last_messages_by_channel_identity(
diff --git a/examples/snippets/conversation/messages/send/snippet.py b/examples/snippets/conversation/messages/send/snippet.py
index 970c9c99..1798eb99 100644
--- a/examples/snippets/conversation/messages/send/snippet.py
+++ b/examples/snippets/conversation/messages/send/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -28,14 +27,17 @@
}
]
+# The conversation message payload to send
+message = {
+ "text_message": {
+ "text": "[Python SDK: Conversation Message] Sample text message",
+ },
+}
+
response = sinch_client.conversation.messages.send(
app_id=app_id,
- message={
- "text_message": {
- "text": "[Python SDK: Conversation Message] Sample text message"
- }
- },
- recipient_identities=recipient_identities
+ message=message,
+ recipient_identities=recipient_identities,
)
print(f"Successfully sent message.\n{response}")
diff --git a/examples/snippets/conversation/messages/send_card_message/snippet.py b/examples/snippets/conversation/messages/send_card_message/snippet.py
index 5300eaf2..03a6496c 100644
--- a/examples/snippets/conversation/messages/send_card_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_card_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_carousel_message/snippet.py b/examples/snippets/conversation/messages/send_carousel_message/snippet.py
index bb8ecb4f..92f5f7ec 100644
--- a/examples/snippets/conversation/messages/send_carousel_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_carousel_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_choice_message/snippet.py b/examples/snippets/conversation/messages/send_choice_message/snippet.py
index 315e13d4..5db53bfe 100644
--- a/examples/snippets/conversation/messages/send_choice_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_choice_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_contact_info_message/snippet.py b/examples/snippets/conversation/messages/send_contact_info_message/snippet.py
index 70f84c57..4f3cffa4 100644
--- a/examples/snippets/conversation/messages/send_contact_info_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_contact_info_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_list_message/snippet.py b/examples/snippets/conversation/messages/send_list_message/snippet.py
index 3b7010ac..8b807aa1 100644
--- a/examples/snippets/conversation/messages/send_list_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_list_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_location_message/snippet.py b/examples/snippets/conversation/messages/send_location_message/snippet.py
index 451d0d83..0b8b3426 100644
--- a/examples/snippets/conversation/messages/send_location_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_location_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_media_message/snippet.py b/examples/snippets/conversation/messages/send_media_message/snippet.py
index df7aa970..ca977797 100644
--- a/examples/snippets/conversation/messages/send_media_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_media_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_template_message/snippet.py b/examples/snippets/conversation/messages/send_template_message/snippet.py
index cb604a74..c6b19843 100644
--- a/examples/snippets/conversation/messages/send_template_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_template_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/send_text_message/snippet.py b/examples/snippets/conversation/messages/send_text_message/snippet.py
index 6a5bebbe..fa6431c5 100644
--- a/examples/snippets/conversation/messages/send_text_message/snippet.py
+++ b/examples/snippets/conversation/messages/send_text_message/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/conversation/messages/update/snippet.py b/examples/snippets/conversation/messages/update/snippet.py
index 72d31d32..b6f89a45 100644
--- a/examples/snippets/conversation/messages/update/snippet.py
+++ b/examples/snippets/conversation/messages/update/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -20,10 +19,12 @@
# The ID of the message to update
message_id = "MESSAGE_ID"
+# The metadata string to set on the message
+metadata = "MESSAGE_METADATA"
response = sinch_client.conversation.messages.update(
message_id=message_id,
- metadata="metadata value set from Python SDK snippet"
+ metadata=metadata,
)
print(f"Updated message:\n{response}")
diff --git a/examples/snippets/number_lookup/lookup/snippet.py b/examples/snippets/number_lookup/lookup/snippet.py
index 35b63e3b..98eec221 100644
--- a/examples/snippets/number_lookup/lookup/snippet.py
+++ b/examples/snippets/number_lookup/lookup/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -17,8 +16,8 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
)
-# The phone number to lookup in E.164 format (e.g., +1234567890)
-phone_number = "PHONE_NUMBER_TO_LOOKUP"
+# The phone number to look up in E.164 format (e.g. +1234567890)
+phone_number = "PHONE_NUMBER"
response = sinch_client.number_lookup.lookup(number=phone_number)
diff --git a/examples/snippets/numbers/active_numbers/get/snippet.py b/examples/snippets/numbers/active_numbers/get/snippet.py
index 37516175..5787fd90 100644
--- a/examples/snippets/numbers/active_numbers/get/snippet.py
+++ b/examples/snippets/numbers/active_numbers/get/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -17,7 +16,9 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
-phone_number = os.environ.get("SINCH_PHONE_NUMBER") or "MY_SINCH_PHONE_NUMBER"
+# The active phone number to retrieve details for in E.164 format
+phone_number = os.environ.get("SINCH_PHONE_NUMBER") or "MY_PHONE_NUMBER"
+
response = sinch_client.numbers.get(phone_number=phone_number)
print(f"Rented number details:\n{response}")
diff --git a/examples/snippets/numbers/active_numbers/list/snippet.py b/examples/snippets/numbers/active_numbers/list/snippet.py
index 6ad2d486..65cfc4f7 100644
--- a/examples/snippets/numbers/active_numbers/list/snippet.py
+++ b/examples/snippets/numbers/active_numbers/list/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/numbers/active_numbers/list_auto/snippet.py b/examples/snippets/numbers/active_numbers/list_auto/snippet.py
index c35b1294..ff180cd6 100644
--- a/examples/snippets/numbers/active_numbers/list_auto/snippet.py
+++ b/examples/snippets/numbers/active_numbers/list_auto/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/numbers/active_numbers/release/snippet.py b/examples/snippets/numbers/active_numbers/release/snippet.py
index c0c56489..fbe191aa 100644
--- a/examples/snippets/numbers/active_numbers/release/snippet.py
+++ b/examples/snippets/numbers/active_numbers/release/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -17,7 +16,9 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
+# The phone number to release in E.164 format
phone_number = os.environ.get("SINCH_PHONE_NUMBER") or "MY_SINCH_PHONE_NUMBER"
+
released_number = sinch_client.numbers.release(
phone_number=phone_number
)
diff --git a/examples/snippets/numbers/active_numbers/update/snippet.py b/examples/snippets/numbers/active_numbers/update/snippet.py
index 253fea5c..f8bec173 100644
--- a/examples/snippets/numbers/active_numbers/update/snippet.py
+++ b/examples/snippets/numbers/active_numbers/update/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -17,12 +16,14 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
-phone_number_to_update = os.environ.get("SINCH_PHONE_NUMBER") or "MY_SINCH_PHONE_NUMBER"
-updated_display_name = "Updated DISPLAY_NAME"
+# The phone number to update in E.164 format
+phone_number = os.environ.get("SINCH_PHONE_NUMBER") or "MY_PHONE_NUMBER"
+# The display name to set for the number
+display_name = "Updated DISPLAY_NAME"
response = sinch_client.numbers.update(
- phone_number=phone_number_to_update,
- display_name=updated_display_name
+ phone_number=phone_number,
+ display_name=display_name,
)
print("Updated Number:\n", response)
diff --git a/examples/snippets/numbers/available_numbers/check_availability/snippet.py b/examples/snippets/numbers/available_numbers/check_availability/snippet.py
index 6ad8ab89..3cce5738 100644
--- a/examples/snippets/numbers/available_numbers/check_availability/snippet.py
+++ b/examples/snippets/numbers/available_numbers/check_availability/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -17,7 +16,9 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
+# The phone number to check in E.164 format
phone_number = "PHONE_NUMBER"
+
response = sinch_client.numbers.check_availability(
phone_number=phone_number
)
diff --git a/examples/snippets/numbers/available_numbers/rent/snippet.py b/examples/snippets/numbers/available_numbers/rent/snippet.py
index 82f0f59f..f369d9fe 100644
--- a/examples/snippets/numbers/available_numbers/rent/snippet.py
+++ b/examples/snippets/numbers/available_numbers/rent/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -18,14 +17,16 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
-phone_number_to_be_rented = "AVAILABLE_PHONE_NUMBER_TO_BE_RENTED"
-service_plan_id_to_associate_with_the_number = os.environ.get("SINCH_SERVICE_PLAN_ID") or "MY_SERVICE_PLAN_ID"
+# The available phone number to rent in E.164 format
+phone_number = "PHONE_NUMBER"
+# The service plan ID to associate with the phone number
+service_plan_id = os.environ.get("SINCH_SERVICE_PLAN_ID") or "MY_SERVICE_PLAN_ID"
sms_configuration: SmsConfigurationDict = {
- "service_plan_id": service_plan_id_to_associate_with_the_number
+ "service_plan_id": service_plan_id,
}
rented_number = sinch_client.numbers.rent(
- phone_number=phone_number_to_be_rented,
+ phone_number=phone_number,
sms_configuration=sms_configuration
)
print("Rented Number:\n", rented_number)
diff --git a/examples/snippets/numbers/available_numbers/rent_any/snippet.py b/examples/snippets/numbers/available_numbers/rent_any/snippet.py
index a7ce7a60..cc55923a 100644
--- a/examples/snippets/numbers/available_numbers/rent_any/snippet.py
+++ b/examples/snippets/numbers/available_numbers/rent_any/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -18,16 +17,20 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
-service_plan_id_to_associate_with_the_number = os.environ.get("SINCH_SERVICE_PLAN_ID") or "MY_SERVICE_PLAN_ID"
+# The service plan ID to associate with the phone number
+service_plan_id = os.environ.get("SINCH_SERVICE_PLAN_ID") or "MY_SERVICE_PLAN_ID"
sms_configuration: SmsConfigurationDict = {
- "service_plan_id": service_plan_id_to_associate_with_the_number
+ "service_plan_id": service_plan_id,
}
+# The URL to receive the notifications about provisioning events
+event_destination_target = "CALLBACK_URL"
response = sinch_client.numbers.rent_any(
region_code="US",
number_type="LOCAL",
capabilities=["SMS", "VOICE"],
- sms_configuration=sms_configuration
+ sms_configuration=sms_configuration,
+ event_destination_target=event_destination_target
)
print("Rented Number:\n", response)
diff --git a/examples/snippets/numbers/available_numbers/search_for_available_numbers/snippet.py b/examples/snippets/numbers/available_numbers/search_for_available_numbers/snippet.py
index ac1a8301..8d174729 100644
--- a/examples/snippets/numbers/available_numbers/search_for_available_numbers/snippet.py
+++ b/examples/snippets/numbers/available_numbers/search_for_available_numbers/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/numbers/available_regions/list/snippet.py b/examples/snippets/numbers/available_regions/list/snippet.py
index 1012de9e..59ff6459 100644
--- a/examples/snippets/numbers/available_regions/list/snippet.py
+++ b/examples/snippets/numbers/available_regions/list/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/numbers/event_destinations/get/snippet.py b/examples/snippets/numbers/event_destinations/get/snippet.py
index 413fd0d0..3f33031e 100644
--- a/examples/snippets/numbers/event_destinations/get/snippet.py
+++ b/examples/snippets/numbers/event_destinations/get/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/numbers/event_destinations/update/snippet.py b/examples/snippets/numbers/event_destinations/update/snippet.py
index 51722f76..abadc2d2 100644
--- a/examples/snippets/numbers/event_destinations/update/snippet.py
+++ b/examples/snippets/numbers/event_destinations/update/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -17,7 +16,9 @@
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET"
)
+# The HMAC secret for signing webhook requests to your event destination
hmac_secret = "NEW_HMAC_SECRET"
+
response = sinch_client.numbers.event_destinations.update(
hmac_secret=hmac_secret
)
diff --git a/examples/snippets/sms/batches/cancel/snippet.py b/examples/snippets/sms/batches/cancel/snippet.py
index 55bb5738..ebc64110 100644
--- a/examples/snippets/sms/batches/cancel/snippet.py
+++ b/examples/snippets/sms/batches/cancel/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -20,6 +19,7 @@
# The ID of the batch to cancel
batch_id = "BATCH_ID"
+
response = sinch_client.sms.batches.cancel(batch_id=batch_id)
print(f"Cancelled batch:\n{response}")
diff --git a/examples/snippets/sms/batches/dry_run_binary/snippet.py b/examples/snippets/sms/batches/dry_run_binary/snippet.py
index 03b6eb9c..689cb02e 100644
--- a/examples/snippets/sms/batches/dry_run_binary/snippet.py
+++ b/examples/snippets/sms/batches/dry_run_binary/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/dry_run_mms/snippet.py b/examples/snippets/sms/batches/dry_run_mms/snippet.py
index 11f54c31..8df83795 100644
--- a/examples/snippets/sms/batches/dry_run_mms/snippet.py
+++ b/examples/snippets/sms/batches/dry_run_mms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/dry_run_sms/snippet.py b/examples/snippets/sms/batches/dry_run_sms/snippet.py
index ae0a7d09..f9eb0ded 100644
--- a/examples/snippets/sms/batches/dry_run_sms/snippet.py
+++ b/examples/snippets/sms/batches/dry_run_sms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/get/snippet.py b/examples/snippets/sms/batches/get/snippet.py
index 3c30df32..369851b8 100644
--- a/examples/snippets/sms/batches/get/snippet.py
+++ b/examples/snippets/sms/batches/get/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -20,6 +19,7 @@
# The ID of the batch to retrieve
batch_id = "BATCH_ID"
+
response = sinch_client.sms.batches.get(batch_id=batch_id)
print(f"Batch details:\n{response}")
diff --git a/examples/snippets/sms/batches/list/snippet.py b/examples/snippets/sms/batches/list/snippet.py
index c6364025..15473da4 100644
--- a/examples/snippets/sms/batches/list/snippet.py
+++ b/examples/snippets/sms/batches/list/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/replace_binary/snippet.py b/examples/snippets/sms/batches/replace_binary/snippet.py
index 305e4f59..17f13be2 100644
--- a/examples/snippets/sms/batches/replace_binary/snippet.py
+++ b/examples/snippets/sms/batches/replace_binary/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/replace_mms/snippet.py b/examples/snippets/sms/batches/replace_mms/snippet.py
index fed4d71e..69bcb708 100644
--- a/examples/snippets/sms/batches/replace_mms/snippet.py
+++ b/examples/snippets/sms/batches/replace_mms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/replace_sms/snippet.py b/examples/snippets/sms/batches/replace_sms/snippet.py
index 88066324..f2e8f373 100644
--- a/examples/snippets/sms/batches/replace_sms/snippet.py
+++ b/examples/snippets/sms/batches/replace_sms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/send_binary/snippet.py b/examples/snippets/sms/batches/send_binary/snippet.py
index ad2366a1..f93dbb10 100644
--- a/examples/snippets/sms/batches/send_binary/snippet.py
+++ b/examples/snippets/sms/batches/send_binary/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/send_delivery_feedback/snippet.py b/examples/snippets/sms/batches/send_delivery_feedback/snippet.py
index f19f83ad..5da87fc0 100644
--- a/examples/snippets/sms/batches/send_delivery_feedback/snippet.py
+++ b/examples/snippets/sms/batches/send_delivery_feedback/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/send_mms/snippet.py b/examples/snippets/sms/batches/send_mms/snippet.py
index fff15360..6627b712 100644
--- a/examples/snippets/sms/batches/send_mms/snippet.py
+++ b/examples/snippets/sms/batches/send_mms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/send_sms/snippet.py b/examples/snippets/sms/batches/send_sms/snippet.py
index 64cedd2d..1073cc31 100644
--- a/examples/snippets/sms/batches/send_sms/snippet.py
+++ b/examples/snippets/sms/batches/send_sms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/update_binary/snippet.py b/examples/snippets/sms/batches/update_binary/snippet.py
index ac67b610..7b1360d0 100644
--- a/examples/snippets/sms/batches/update_binary/snippet.py
+++ b/examples/snippets/sms/batches/update_binary/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/update_mms/snippet.py b/examples/snippets/sms/batches/update_mms/snippet.py
index b8a7ba1a..7c37dbb6 100644
--- a/examples/snippets/sms/batches/update_mms/snippet.py
+++ b/examples/snippets/sms/batches/update_mms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/batches/update_sms/snippet.py b/examples/snippets/sms/batches/update_sms/snippet.py
index d65aa994..1e6fd4e6 100644
--- a/examples/snippets/sms/batches/update_sms/snippet.py
+++ b/examples/snippets/sms/batches/update_sms/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/delivery_reports/get/snippet.py b/examples/snippets/sms/delivery_reports/get/snippet.py
index bcf068db..d1e715e1 100644
--- a/examples/snippets/sms/delivery_reports/get/snippet.py
+++ b/examples/snippets/sms/delivery_reports/get/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
@@ -20,6 +19,7 @@
# The ID of the batch to get delivery report for
batch_id = "BATCH_ID"
+
response = sinch_client.sms.delivery_reports.get(batch_id=batch_id)
print(f"Delivery report for batch:\n{response}")
diff --git a/examples/snippets/sms/delivery_reports/get_for_number/snippet.py b/examples/snippets/sms/delivery_reports/get_for_number/snippet.py
index 8e304697..859a8768 100644
--- a/examples/snippets/sms/delivery_reports/get_for_number/snippet.py
+++ b/examples/snippets/sms/delivery_reports/get_for_number/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/examples/snippets/sms/delivery_reports/list/snippet.py b/examples/snippets/sms/delivery_reports/list/snippet.py
index 56f4af20..d92f88ef 100644
--- a/examples/snippets/sms/delivery_reports/list/snippet.py
+++ b/examples/snippets/sms/delivery_reports/list/snippet.py
@@ -1,8 +1,7 @@
"""
Sinch Python Snippet
-TODO: Update links when v2 is released.
-This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/snippets/
+This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""
import os
diff --git a/sinch/core/adapters/requests_http_transport.py b/sinch/core/adapters/requests_http_transport.py
index 04671812..62c0a3cb 100644
--- a/sinch/core/adapters/requests_http_transport.py
+++ b/sinch/core/adapters/requests_http_transport.py
@@ -9,7 +9,7 @@ def __init__(self, sinch):
super().__init__(sinch)
self.http_session = requests.Session()
- def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
+ def send(self, endpoint: HTTPEndpoint) -> HTTPResponse:
request_data: HttpRequest = self.prepare_request(endpoint)
request_data: HttpRequest = self.authenticate(endpoint, request_data)
@@ -34,11 +34,8 @@ def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
f"and body: {response_body} from URL: {request_data.url}"
)
- return self.handle_response(
- endpoint=endpoint,
- http_response=HTTPResponse(
- status_code=response.status_code,
- body=response_body,
- headers=response.headers
- )
+ return HTTPResponse(
+ status_code=response.status_code,
+ body=response_body,
+ headers=response.headers
)
diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py
index 1ca6af3a..78678397 100644
--- a/sinch/core/clients/sinch_client_configuration.py
+++ b/sinch/core/clients/sinch_client_configuration.py
@@ -3,7 +3,6 @@
from sinch.core.ports.http_transport import HTTPTransport
from sinch.core.token_manager import TokenManager
-from sinch.core.enums import HTTPAuthentication
class Configuration:
@@ -43,15 +42,12 @@ def __init__(
self._sms_region_with_service_plan_id = sms_region
self._sms_domain = "https://zt.{}.sms.api.sinch.com"
self._sms_domain_with_service_plan_id = "https://{}.sms.api.sinch.com"
- self._templates_region = "eu"
- self._templates_domain = ".template.api.sinch.com"
self.token_manager = token_manager
self.transport: HTTPTransport = transport
self._set_conversation_origin()
self._set_sms_origin()
self._set_sms_origin_with_service_plan_id()
- self._set_templates_origin()
if logger_name:
self.logger = logging.getLogger(logger_name)
@@ -158,35 +154,6 @@ def _get_conversation_domain(self):
doc="ConversationAPI Domain"
)
- def _set_templates_origin(self):
- self.templates_origin = self._templates_region + self._templates_domain
-
- def _set_templates_region(self, region):
- self._templates_region = region
- self._set_templates_origin()
-
- def _get_templates_region(self):
- return self._templates_region
-
- templates_region = property(
- _get_templates_region,
- _set_templates_region,
- doc="Conversation API Templates Region"
- )
-
- def _set_templates_domain(self, domain):
- self._templates_domain = domain
- self._set_templates_origin()
-
- def _get_templates_domain(self):
- return self._templates_domain
-
- templates_domain = property(
- _get_templates_domain,
- _set_templates_domain,
- doc="Conversation API Templates Domain"
- )
-
def _determine_authentication_method(self):
"""
Determines the authentication method based on provided parameters.
@@ -256,7 +223,7 @@ def get_sms_origin_for_auth(self):
"SMS region is required. "
"Provide sms_region when initializing SinchClient "
"Example: SinchClient(project_id='...', key_id='...', key_secret='...', sms_region='eu')"
- "or set it via sinch_client.configuration.sms_region. "
+ " or set it via sinch_client.configuration.sms_region. "
)
return origin
@@ -273,7 +240,7 @@ def get_conversation_origin(self):
"Conversation region is required. "
"Provide conversation_region when initializing SinchClient "
"Example: SinchClient(project_id='...', key_id='...', key_secret='...', conversation_region='eu')"
- "or set it via sinch_client.configuration.conversation_region. "
+ " or set it via sinch_client.configuration.conversation_region. "
)
return self.conversation_origin
diff --git a/sinch/core/models/utils.py b/sinch/core/models/utils.py
index aef322e2..566ae3d8 100644
--- a/sinch/core/models/utils.py
+++ b/sinch/core/models/utils.py
@@ -29,22 +29,29 @@ def serialize_datetime_in_dict(value: Optional[Dict[str, Any]]) -> Optional[Dict
return serialized
-def model_dump_for_query_params(model, exclude_none=True, by_alias=True):
+def model_dump_for_query_params(
+ model, exclude_none=True, by_alias=True, exclude=None
+):
"""
Serializes a Pydantic model for use as query parameters.
Converts list values to comma-separated strings for APIs that expect this format.
Filters out empty values (empty strings and empty lists).
-
+
:param model: A Pydantic BaseModel instance
:type model: BaseModel
:param exclude_none: Whether to exclude None values (default: True)
:type exclude_none: bool
:param by_alias: Whether to use field aliases (default: True)
:type by_alias: bool
+ :param exclude: Field names to omit (e.g. URL path params), passed to model_dump
+ :type exclude: set[str] | None
:returns: Serialized model data with lists converted to comma-separated strings
:rtype: dict
"""
- data = model.model_dump(exclude_none=exclude_none, by_alias=by_alias)
+ dump_kwargs = {"exclude_none": exclude_none, "by_alias": by_alias}
+ if exclude is not None:
+ dump_kwargs["exclude"] = exclude
+ data = model.model_dump(**dump_kwargs)
filtered_data = {}
for key, value in data.items():
# Filter out empty strings
diff --git a/sinch/core/ports/http_transport.py b/sinch/core/ports/http_transport.py
index f4e30dcd..ec0edafc 100644
--- a/sinch/core/ports/http_transport.py
+++ b/sinch/core/ports/http_transport.py
@@ -10,12 +10,59 @@
class HTTPTransport(ABC):
+ """Base class for HTTP transports.
+
+ Subclasses implement ``send`` to perform the raw HTTP call.
+ The public ``request`` method adds cross-cutting concerns on top:
+ authentication, logging hooks, and automatic token refresh on 401.
+ """
+
def __init__(self, sinch):
self.sinch = sinch
+ # ------------------------------------------------------------------
+ # Subclass contract
+ # ------------------------------------------------------------------
+
@abstractmethod
+ def send(self, endpoint: HTTPEndpoint) -> HTTPResponse:
+ """Execute a single HTTP round-trip and return the response.
+
+ Implementations must prepare the request, authenticate, perform the
+ HTTP call, deserialize the response, and return an ``HTTPResponse``.
+ They should **not** handle token refresh — that is done by
+ ``request``.
+ """
+
+ # ------------------------------------------------------------------
+ # Public API
+ # ------------------------------------------------------------------
+
def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
- pass
+ """Send a request with automatic OAuth token refresh on 401.
+
+ If the server responds with 401 *and* the token is detected as
+ expired, the token is invalidated and **one** retry is attempted
+ with a fresh token. A second consecutive 401 is handed straight
+ to the endpoint's error handler — no further retries.
+ """
+ http_response = self.send(endpoint)
+
+ if self._should_refresh_token(endpoint, http_response):
+ self.sinch.configuration.token_manager.handle_invalid_token(
+ http_response
+ )
+ if (
+ self.sinch.configuration.token_manager.token_state
+ == TokenState.EXPIRED
+ ):
+ http_response = self.send(endpoint)
+
+ return endpoint.handle_response(http_response)
+
+ # ------------------------------------------------------------------
+ # Internals
+ # ------------------------------------------------------------------
def authenticate(self, endpoint, request_data):
if endpoint.HTTP_AUTHENTICATION in (HTTPAuthentication.BASIC.value, HTTPAuthentication.OAUTH.value):
@@ -83,10 +130,7 @@ def deserialize_json_response(response):
response_body = response.json()
except ValueError as err:
raise SinchException(
- message=(
- "Error while parsing json response.",
- err.msg
- ),
+ message=f"Error while parsing json response. {err}",
is_from_server=True,
response=response
)
@@ -95,10 +139,11 @@ def deserialize_json_response(response):
return response_body
- def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse):
- if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value:
- self.sinch.configuration.token_manager.handle_invalid_token(http_response)
- if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED:
- return self.request(endpoint=endpoint)
-
- return endpoint.handle_response(http_response)
+ @staticmethod
+ def _should_refresh_token(endpoint, http_response):
+ """Return True when a 401 response should trigger a token refresh."""
+ return (
+ http_response.status_code == 401
+ and endpoint.HTTP_AUTHENTICATION
+ == HTTPAuthentication.OAUTH.value
+ )
diff --git a/sinch/domains/conversation/api/v1/internal/base/conversation_endpoint.py b/sinch/domains/conversation/api/v1/internal/base/conversation_endpoint.py
index 2cd6f35f..4dac4692 100644
--- a/sinch/domains/conversation/api/v1/internal/base/conversation_endpoint.py
+++ b/sinch/domains/conversation/api/v1/internal/base/conversation_endpoint.py
@@ -107,8 +107,15 @@ def process_response_model(
def handle_response(self, response: HTTPResponse):
if response.status_code >= 400:
+ error = (response.body or {}).get("error", {})
+ message = error.get("message", "")
+ status = error.get("status", "")
+ error_message = (
+ f"{message} {status}".strip()
+ or f"Error {response.status_code}"
+ )
raise ConversationException(
- message=f"{response.body['error'].get('message')} {response.body['error'].get('status')}",
+ message=error_message,
response=response,
is_from_server=True,
)
diff --git a/sinch/domains/numbers/api/v1/internal/active_numbers_endpoints.py b/sinch/domains/numbers/api/v1/internal/active_numbers_endpoints.py
index 9b31a217..6e309763 100644
--- a/sinch/domains/numbers/api/v1/internal/active_numbers_endpoints.py
+++ b/sinch/domains/numbers/api/v1/internal/active_numbers_endpoints.py
@@ -124,8 +124,9 @@ def __init__(
self.request_data = request_data
def request_body(self):
+ path_params = self._get_path_params_from_url()
request_data = self.request_data.model_dump(
- by_alias=True, exclude_none=True
+ by_alias=True, exclude_none=True, exclude=path_params
)
return json.dumps(request_data)
diff --git a/sinch/domains/numbers/api/v1/internal/available_numbers_endpoints.py b/sinch/domains/numbers/api/v1/internal/available_numbers_endpoints.py
index b9321256..1ad32eaa 100644
--- a/sinch/domains/numbers/api/v1/internal/available_numbers_endpoints.py
+++ b/sinch/domains/numbers/api/v1/internal/available_numbers_endpoints.py
@@ -33,8 +33,9 @@ def __init__(self, project_id: str, request_data: RentNumberRequest):
def request_body(self) -> str:
# Convert the request data to a dictionary and remove None values
+ path_params = self._get_path_params_from_url()
request_data = self.request_data.model_dump(
- by_alias=True, exclude_none=True
+ by_alias=True, exclude_none=True, exclude=path_params
)
return json.dumps(request_data)
diff --git a/sinch/domains/numbers/api/v1/internal/base/numbers_endpoint.py b/sinch/domains/numbers/api/v1/internal/base/numbers_endpoint.py
index 43b24862..5bc463f7 100644
--- a/sinch/domains/numbers/api/v1/internal/base/numbers_endpoint.py
+++ b/sinch/domains/numbers/api/v1/internal/base/numbers_endpoint.py
@@ -1,3 +1,4 @@
+import re
from abc import ABC
from typing import Type
from sinch.core.models.http_response import HTTPResponse
@@ -23,6 +24,23 @@ def build_url(self, sinch) -> str:
**vars(self.request_data),
)
+ def _get_path_params_from_url(self) -> set:
+ """
+ Extracts path parameters from ENDPOINT_URL template.
+
+ Returns:
+ set: Set of path parameter names that should be excluded
+ from request body and query params.
+ """
+ if not self.ENDPOINT_URL:
+ return set()
+
+ path_params = set(re.findall(r"\{(\w+)\}", self.ENDPOINT_URL))
+ path_params.discard("origin")
+ path_params.discard("project_id")
+
+ return path_params
+
def build_query_params(self) -> dict:
"""
Constructs the query parameters for the endpoint.
@@ -60,15 +78,26 @@ def process_response_model(
raise ValueError(f"Invalid response structure: {e}") from e
def handle_response(self, response: HTTPResponse):
+ error_data = (response.body or {}).get("error", {})
+
if response.status_code == 404:
- error = NotFoundError(**response.body["error"])
+ try:
+ error = NotFoundError(**error_data)
+ except TypeError:
+ error = f"Not found: {error_data}"
raise NumbersException(
message=error, response=response, is_from_server=True
)
if response.status_code >= 400:
+ message = error_data.get("message", "")
+ status = error_data.get("status", "")
+ error_message = (
+ f"{message} {status}".strip()
+ or f"Error {response.status_code}"
+ )
raise NumbersException(
- message=f"{response.body['error'].get('message')} {response.body['error'].get('status')}",
+ message=error_message,
response=response,
is_from_server=True,
)
diff --git a/sinch/domains/sms/api/v1/internal/delivery_reports_endpoints.py b/sinch/domains/sms/api/v1/internal/delivery_reports_endpoints.py
index ca4aa6e5..b80dd8a8 100644
--- a/sinch/domains/sms/api/v1/internal/delivery_reports_endpoints.py
+++ b/sinch/domains/sms/api/v1/internal/delivery_reports_endpoints.py
@@ -32,7 +32,10 @@ def __init__(
self.request_data = request_data
def build_query_params(self) -> dict:
- return model_dump_for_query_params(self.request_data)
+ path_params = self._get_path_params_from_url()
+ return model_dump_for_query_params(
+ self.request_data, exclude=path_params
+ )
def handle_response(self, response: HTTPResponse) -> BatchDeliveryReport:
try:
diff --git a/tests/conftest.py b/tests/conftest.py
index 3cae4d3c..d879c2d8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -43,7 +43,6 @@ def configure_origin(
sinch_client,
numbers_origin,
conversation_origin,
- templates_origin,
auth_origin,
sms_origin
):
@@ -56,9 +55,6 @@ def configure_origin(
if conversation_origin:
sinch_client.configuration.conversation_origin = conversation_origin
- if templates_origin:
- sinch_client.configuration.templates_origin = templates_origin
-
if sms_origin:
sinch_client.configuration.sms_origin = sms_origin
sinch_client.configuration.sms_origin_with_service_plan_id = sms_origin
@@ -101,11 +97,6 @@ def sms_origin():
return os.getenv("SMS_ORIGIN")
-@pytest.fixture
-def templates_origin():
- return os.getenv("TEMPLATES_ORIGIN")
-
-
@pytest.fixture
def disable_ssl():
return os.getenv("DISABLE_SSL")
@@ -181,7 +172,6 @@ def sinch_client_sync(
key_secret,
numbers_origin,
conversation_origin,
- templates_origin,
auth_origin,
sms_origin,
project_id
@@ -194,7 +184,6 @@ def sinch_client_sync(
),
numbers_origin,
conversation_origin,
- templates_origin,
auth_origin,
sms_origin
)
diff --git a/tests/unit/domains/numbers/v1/endpoints/active/test_update_active_numbers_endpoint.py b/tests/unit/domains/numbers/v1/endpoints/active/test_update_active_numbers_endpoint.py
index 34af95e3..21efe049 100644
--- a/tests/unit/domains/numbers/v1/endpoints/active/test_update_active_numbers_endpoint.py
+++ b/tests/unit/domains/numbers/v1/endpoints/active/test_update_active_numbers_endpoint.py
@@ -51,7 +51,6 @@ def mock_response():
@pytest.fixture
def mock_response_body():
expected_body = {
- "phoneNumber": "+1234567890",
"displayName": "Display Name",
"smsConfiguration": {
"servicePlanId": "Service Plan Id"
diff --git a/tests/unit/domains/numbers/v1/endpoints/available/test_rent_number_endpoint.py b/tests/unit/domains/numbers/v1/endpoints/available/test_rent_number_endpoint.py
index edd8efb4..bc2c3800 100644
--- a/tests/unit/domains/numbers/v1/endpoints/available/test_rent_number_endpoint.py
+++ b/tests/unit/domains/numbers/v1/endpoints/available/test_rent_number_endpoint.py
@@ -40,7 +40,6 @@ def mock_response():
@pytest.fixture
def mock_response_body():
expected_body = {
- "phoneNumber": "+1234567890",
"smsConfiguration": {
"servicePlanId": "YOUR_SMS_servicePlanId"
},
diff --git a/tests/unit/domains/sms/v1/endpoints/delivery_reports/test_get_batch_delivery_report_endpoint.py b/tests/unit/domains/sms/v1/endpoints/delivery_reports/test_get_batch_delivery_report_endpoint.py
index 255845a7..b7d4dbb3 100644
--- a/tests/unit/domains/sms/v1/endpoints/delivery_reports/test_get_batch_delivery_report_endpoint.py
+++ b/tests/unit/domains/sms/v1/endpoints/delivery_reports/test_get_batch_delivery_report_endpoint.py
@@ -60,7 +60,6 @@ def test_build_url(endpoint, mock_sinch_client_sms):
def test_build_query_params(endpoint):
query_params = endpoint.build_query_params()
expected_params = {
- "batch_id": "01FC66621XXXXX119Z8PMV1QPQ",
"type": "summary",
"status": "DELIVERED",
"code": "400",
@@ -79,7 +78,6 @@ def test_build_query_params_with_multiple_status_and_code():
endpoint = GetBatchDeliveryReportEndpoint("test_project_id", request_data)
query_params = endpoint.build_query_params()
expected_params = {
- "batch_id": "01W4FFL35P4NC4K35SMSBATCH1",
"status": "DELIVERED,FAILED,QUEUED",
"code": "400,401,402",
}
diff --git a/tests/unit/http_transport_tests.py b/tests/unit/http_transport_tests.py
index bee82710..c567dcb9 100644
--- a/tests/unit/http_transport_tests.py
+++ b/tests/unit/http_transport_tests.py
@@ -1,33 +1,48 @@
import pytest
-from unittest.mock import Mock
+from unittest.mock import Mock, call
from sinch.core.enums import HTTPAuthentication
from sinch.core.exceptions import ValidationException
from sinch.core.models.http_request import HttpRequest
from sinch.core.endpoint import HTTPEndpoint
from sinch.core.models.http_response import HTTPResponse
from sinch.core.ports.http_transport import HTTPTransport
+from sinch.core.token_manager import TokenState
# Mock classes and fixtures
-class MockEndpoint(HTTPEndpoint):
- def __init__(self, auth_type):
- self.HTTP_AUTHENTICATION = auth_type
- self.HTTP_METHOD = "GET"
+def _make_mock_endpoint(auth_type, error_on_4xx=False):
+ """Create a MockEndpoint that satisfies the abstract property contract."""
- def build_url(self, sinch):
- return "api.sinch.com/test"
+ class _Endpoint(HTTPEndpoint):
+ HTTP_AUTHENTICATION = auth_type
+ HTTP_METHOD = "GET"
- def get_url_without_origin(self, sinch):
- return "/test"
+ def __init__(self):
+ # Skip super().__init__ — we don't need project_id / request_data
+ pass
- def request_body(self):
- return {}
+ def build_url(self, sinch):
+ return "api.sinch.com/test"
- def build_query_params(self):
- return {}
+ def get_url_without_origin(self, sinch):
+ return "/test"
- def handle_response(self, response: HTTPResponse):
- return response
+ def request_body(self):
+ return {}
+
+ def build_query_params(self):
+ return {}
+
+ def handle_response(self, response: HTTPResponse):
+ if error_on_4xx and response.status_code >= 400:
+ raise ValidationException(
+ message=f"HTTP {response.status_code}",
+ is_from_server=True,
+ response=response,
+ )
+ return response
+
+ return _Endpoint()
@pytest.fixture
@@ -46,7 +61,6 @@ def mock_sinch():
def base_request():
return HttpRequest(
headers={},
- protocol="https://",
url="https://api.sinch.com/test",
http_method="GET",
request_body={},
@@ -56,9 +70,24 @@ def base_request():
class MockHTTPTransport(HTTPTransport):
- def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
- # Simple mock implementation that just returns a dummy response
- return HTTPResponse(status_code=200, body={}, headers={})
+ """Transport whose send() returns from a pre-configured list of responses."""
+
+ def __init__(self, sinch, responses=None):
+ super().__init__(sinch)
+ self._responses = list(responses or [])
+ self._call_count = 0
+
+ def send(self, endpoint: HTTPEndpoint) -> HTTPResponse:
+ if self._call_count < len(self._responses):
+ resp = self._responses[self._call_count]
+ else:
+ resp = HTTPResponse(status_code=200, body={}, headers={})
+ self._call_count += 1
+ return resp
+
+ @property
+ def call_count(self):
+ return self._call_count
# Synchronous Transport Tests
@@ -70,7 +99,7 @@ class TestHTTPTransport:
])
def test_authenticate(self, mock_sinch, base_request, auth_type):
transport = MockHTTPTransport(mock_sinch)
- endpoint = MockEndpoint(auth_type)
+ endpoint = _make_mock_endpoint(auth_type)
if auth_type == HTTPAuthentication.BASIC.value:
result = transport.authenticate(endpoint, base_request)
@@ -94,10 +123,131 @@ def test_authenticate(self, mock_sinch, base_request, auth_type):
])
def test_authenticate_missing_credentials(self, mock_sinch, base_request, auth_type, missing_creds):
transport = MockHTTPTransport(mock_sinch)
- endpoint = MockEndpoint(auth_type)
+ endpoint = _make_mock_endpoint(auth_type)
for cred, value in missing_creds.items():
setattr(mock_sinch.configuration, cred, value)
with pytest.raises(ValidationException):
transport.authenticate(endpoint, base_request)
+
+
+class TestTokenRefreshRetry:
+ """Tests for the automatic token refresh on 401 expired responses."""
+
+ @staticmethod
+ def _expired_401():
+ return HTTPResponse(
+ status_code=401,
+ body={"error": "token expired"},
+ headers={"www-authenticate": "Bearer error=\"expired\""},
+ )
+
+ @staticmethod
+ def _non_expired_401():
+ return HTTPResponse(
+ status_code=401,
+ body={"error": "unauthorized"},
+ headers={"www-authenticate": "Bearer error=\"invalid_token\""},
+ )
+
+ @staticmethod
+ def _ok_200():
+ return HTTPResponse(status_code=200, body={"ok": True}, headers={})
+
+ def test_retry_succeeds_after_expired_token(self, mock_sinch):
+ """A single 401-expired followed by a 200 should retry once and succeed."""
+ from sinch.core.token_manager import TokenManager
+
+ token_manager = Mock(spec=TokenManager)
+ token_manager.token_state = TokenState.VALID
+
+ def mark_expired(http_response):
+ token_manager.token_state = TokenState.EXPIRED
+
+ token_manager.handle_invalid_token.side_effect = mark_expired
+ mock_sinch.configuration.token_manager = token_manager
+
+ transport = MockHTTPTransport(
+ mock_sinch,
+ responses=[self._expired_401(), self._ok_200()],
+ )
+ endpoint = _make_mock_endpoint(HTTPAuthentication.OAUTH.value)
+
+ result = transport.request(endpoint)
+
+ assert result.status_code == 200
+ assert transport.call_count == 2
+ token_manager.handle_invalid_token.assert_called_once()
+
+ def test_no_infinite_loop_on_persistent_401(self, mock_sinch):
+ """Two consecutive 401-expired must NOT cause infinite retries.
+
+ The second 401 should be handed to the endpoint's error handler
+ and send() should be called at most twice.
+ """
+ from sinch.core.token_manager import TokenManager
+
+ token_manager = Mock(spec=TokenManager)
+ token_manager.token_state = TokenState.VALID
+
+ def mark_expired(http_response):
+ token_manager.token_state = TokenState.EXPIRED
+
+ token_manager.handle_invalid_token.side_effect = mark_expired
+ mock_sinch.configuration.token_manager = token_manager
+
+ transport = MockHTTPTransport(
+ mock_sinch,
+ responses=[self._expired_401(), self._expired_401()],
+ )
+ endpoint = _make_mock_endpoint(HTTPAuthentication.OAUTH.value, error_on_4xx=True)
+
+ with pytest.raises(ValidationException, match="401"):
+ transport.request(endpoint)
+
+ # send() must have been called exactly twice: initial + one retry
+ assert transport.call_count == 2
+
+ def test_no_retry_when_401_is_not_expired(self, mock_sinch):
+ """A 401 without 'expired' in WWW-Authenticate should NOT trigger a retry."""
+ from sinch.core.token_manager import TokenManager
+
+ token_manager = Mock(spec=TokenManager)
+ token_manager.token_state = TokenState.VALID
+
+ # handle_invalid_token inspects the header but does NOT set EXPIRED
+ # because the header says "invalid_token", not "expired"
+ token_manager.handle_invalid_token.side_effect = lambda r: None
+ mock_sinch.configuration.token_manager = token_manager
+
+ transport = MockHTTPTransport(
+ mock_sinch,
+ responses=[self._non_expired_401()],
+ )
+ endpoint = _make_mock_endpoint(HTTPAuthentication.OAUTH.value, error_on_4xx=True)
+
+ with pytest.raises(ValidationException, match="401"):
+ transport.request(endpoint)
+
+ # send() called only once — no retry
+ assert transport.call_count == 1
+
+ def test_no_retry_for_non_oauth_endpoint(self, mock_sinch):
+ """A 401 on a BASIC-auth endpoint should NOT trigger token refresh."""
+ from sinch.core.token_manager import TokenManager
+
+ token_manager = Mock(spec=TokenManager)
+ mock_sinch.configuration.token_manager = token_manager
+
+ transport = MockHTTPTransport(
+ mock_sinch,
+ responses=[self._expired_401()],
+ )
+ endpoint = _make_mock_endpoint(HTTPAuthentication.BASIC.value, error_on_4xx=True)
+
+ with pytest.raises(ValidationException, match="401"):
+ transport.request(endpoint)
+
+ assert transport.call_count == 1
+ token_manager.handle_invalid_token.assert_not_called()
diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py
index db790b67..8d1a9632 100644
--- a/tests/unit/test_configuration.py
+++ b/tests/unit/test_configuration.py
@@ -76,18 +76,6 @@ def test_if_logger_name_was_preserved_correctly(sinch_client_sync):
assert client_configuration.logger.name == clever_monty_python_quote
-def test_set_templates_region_property_and_check_that_templates_origin_was_updated(sinch_client_sync):
- sinch_client_sync.configuration.templates_region = "Are_you_suggesting_that_coconuts_migrate?"
- assert "coconuts" in sinch_client_sync.configuration.templates_origin
- assert "migrate" in sinch_client_sync.configuration.templates_origin
-
-
-def test_set_templates_domain_property_and_check_that_templates_origin_was_updated(sinch_client_sync):
- sinch_client_sync.configuration.templates_domain = "Are_you_suggesting_that_coconuts_migrate?"
- assert "coconuts" in sinch_client_sync.configuration.templates_origin
- assert "migrate" in sinch_client_sync.configuration.templates_origin
-
-
def test_configuration_expects_authentication_method_determination_sms_auth_priority(sinch_client_sync):
""" Test that SMS authentication takes priority over project authentication """
client_configuration = Configuration(