Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codegen.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "engineHash": "482939a", "specHash": "77eac4b", "version": "4.4.0" }
{ "engineHash": "bc04b80", "specHash": "77eac4b", "version": "4.4.0" }
13 changes: 13 additions & 0 deletions box_sdk_gen/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@

from box_sdk_gen.networking.base_urls import BaseUrls

from box_sdk_gen.networking.timeout_config import TimeoutConfig

from box_sdk_gen.networking.proxy_config import ProxyConfig


Expand Down Expand Up @@ -545,3 +547,14 @@ def with_proxy(self, config: ProxyConfig) -> 'BoxClient':
return BoxClient(
auth=self.auth, network_session=self.network_session.with_proxy(config)
)

def with_timeouts(self, config: TimeoutConfig) -> 'BoxClient':
"""
Create a new client with custom timeouts that will be used for every API call
:param config: Timeout configuration.
:type config: TimeoutConfig
"""
return BoxClient(
auth=self.auth,
network_session=self.network_session.with_timeout_config(config),
)
2 changes: 2 additions & 0 deletions box_sdk_gen/networking/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from box_sdk_gen.networking.box_network_client import *

from box_sdk_gen.networking.timeout_config import *

from box_sdk_gen.networking.proxy_config import *

from box_sdk_gen.networking.network import *
Expand Down
46 changes: 42 additions & 4 deletions box_sdk_gen/networking/box_network_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time
from collections import OrderedDict
from dataclasses import dataclass
from typing import Optional, Dict, Union
from typing import Optional, Dict, Union, Tuple
from sys import version_info as py_version

import requests
Expand All @@ -17,6 +17,7 @@
from ..box.errors import BoxAPIError, BoxSDKError, RequestInfo, ResponseInfo
from ..internal.utils import ByteStream, ResponseByteStream
from ..networking.network_client import NetworkClient
from ..networking.timeout_config import TimeoutConfig
from ..serialization.json import (
sd_to_json,
sd_to_url_params,
Expand All @@ -40,6 +41,7 @@ class APIRequest:
params: Dict[str, str]
data: Optional[Union[str, ByteStream, MultipartEncoder]]
allow_redirects: bool = True
timeout: Optional[Tuple[Optional[float], Optional[float]]] = None


@dataclass
Expand Down Expand Up @@ -151,6 +153,7 @@ def _prepare_request(
options.content_type, options.file_stream or options.data
)
allow_redirects = options.follow_redirects
timeout = self._get_request_timeout(options)

if options.content_type:
if options.content_type == 'multipart/form-data':
Expand Down Expand Up @@ -178,8 +181,43 @@ def _prepare_request(
params=params,
data=data,
allow_redirects=allow_redirects,
timeout=timeout,
)

@staticmethod
def _get_request_timeout(
options: 'FetchOptions',
) -> Optional[Tuple[Optional[float], Optional[float]]]:
"""
Derive requests timeout tuple (connect, read) in seconds.

Uses `options.network_session.timeout_config` when present.
The timeout config values are expected to be in milliseconds.
"""
network_session = options.network_session
timeout_config = network_session.timeout_config if network_session else None
if timeout_config is None:
return None

connection_timeout_ms, read_timeout_ms = (
timeout_config.connection_timeout_ms,
timeout_config.read_timeout_ms,
)

if connection_timeout_ms is None and read_timeout_ms is None:
return None

connection_timeout_sec = (
connection_timeout_ms / 1000.0
if connection_timeout_ms is not None
else None
)
read_timeout_sec = (
read_timeout_ms / 1000.0 if read_timeout_ms is not None else None
)

return (connection_timeout_sec, read_timeout_sec)

@staticmethod
def _prepare_headers(
options: 'FetchOptions', reauthenticate: bool = False
Expand Down Expand Up @@ -216,12 +254,12 @@ def _prepare_body(
or content_type == 'application/octet-stream'
):
return data
raise
raise ValueError(f'Unsupported content type: {content_type}')

def _make_request(self, request: APIRequest) -> APIResponse:
raised_exception = None
reauthentication_needed = False
default_timeout = (5, 60) # connect, read timeout
timeout = request.timeout
try:
network_response = self.requests_session.request(
method=request.method,
Expand All @@ -231,7 +269,7 @@ def _make_request(self, request: APIRequest) -> APIResponse:
params=request.params,
allow_redirects=request.allow_redirects,
stream=True,
timeout=default_timeout,
timeout=timeout,
)
except RequestException as request_exc:
raised_exception = request_exc
Expand Down
31 changes: 31 additions & 0 deletions box_sdk_gen/networking/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .proxy_config import ProxyConfig
from .base_urls import BaseUrls
from .retries import RetryStrategy, BoxRetryStrategy
from .timeout_config import TimeoutConfig


class NetworkSession:
Expand All @@ -18,6 +19,7 @@ def __init__(
base_urls: BaseUrls = None,
proxy_url: str = None,
data_sanitizer: DataSanitizer = None,
timeout_config: TimeoutConfig = None,
):
if additional_headers is None:
additional_headers = {}
Expand All @@ -38,12 +40,18 @@ def __init__(
}
if data_sanitizer is None:
data_sanitizer = DataSanitizer()
if timeout_config is None:
timeout_config = TimeoutConfig(
connection_timeout_ms=5000,
read_timeout_ms=60000,
)
self.additional_headers = additional_headers
self.base_urls = base_urls
self.proxy_url = proxy_url
self.network_client = network_client
self.retry_strategy = retry_strategy
self.data_sanitizer = data_sanitizer
self.timeout_config = timeout_config

def with_additional_headers(
self, additional_headers: Dict[str, str] = None
Expand All @@ -61,6 +69,7 @@ def with_additional_headers(
proxy_url=self.proxy_url,
retry_strategy=self.retry_strategy,
data_sanitizer=self.data_sanitizer,
timeout_config=self.timeout_config,
)

def with_custom_base_urls(self, base_urls: BaseUrls) -> 'NetworkSession':
Expand All @@ -77,6 +86,7 @@ def with_custom_base_urls(self, base_urls: BaseUrls) -> 'NetworkSession':
proxy_url=self.proxy_url,
retry_strategy=self.retry_strategy,
data_sanitizer=self.data_sanitizer,
timeout_config=self.timeout_config,
)

def with_proxy(self, config: ProxyConfig) -> 'NetworkSession':
Expand All @@ -103,6 +113,7 @@ def with_proxy(self, config: ProxyConfig) -> 'NetworkSession':
proxy_url=proxy_url,
retry_strategy=self.retry_strategy,
data_sanitizer=self.data_sanitizer,
timeout_config=self.timeout_config,
)

def with_network_client(self, network_client: NetworkClient) -> 'NetworkSession':
Expand All @@ -119,6 +130,7 @@ def with_network_client(self, network_client: NetworkClient) -> 'NetworkSession'
proxy_url=self.proxy_url,
retry_strategy=self.retry_strategy,
data_sanitizer=self.data_sanitizer,
timeout_config=self.timeout_config,
)

def with_retry_strategy(self, retry_strategy: RetryStrategy) -> 'NetworkSession':
Expand All @@ -135,6 +147,7 @@ def with_retry_strategy(self, retry_strategy: RetryStrategy) -> 'NetworkSession'
proxy_url=self.proxy_url,
retry_strategy=retry_strategy,
data_sanitizer=self.data_sanitizer,
timeout_config=self.timeout_config,
)

def with_data_sanitizer(self, data_sanitizer: DataSanitizer) -> 'NetworkSession':
Expand All @@ -151,4 +164,22 @@ def with_data_sanitizer(self, data_sanitizer: DataSanitizer) -> 'NetworkSession'
proxy_url=self.proxy_url,
retry_strategy=self.retry_strategy,
data_sanitizer=data_sanitizer,
timeout_config=self.timeout_config,
)

def with_timeout_config(self, timeout_config: TimeoutConfig) -> 'NetworkSession':
"""
Generate a fresh network session by duplicating the existing configuration and network parameters,
while also including timeout config to be used for every API call.
:param timeout_config: TimeoutConfig object, which contains the timeout config
:return: a new instance of NetworkSession
"""
return NetworkSession(
network_client=self.network_client,
additional_headers=self.additional_headers,
base_urls=self.base_urls,
proxy_url=self.proxy_url,
retry_strategy=self.retry_strategy,
data_sanitizer=self.data_sanitizer,
timeout_config=timeout_config,
)
12 changes: 12 additions & 0 deletions box_sdk_gen/networking/timeout_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Optional


class TimeoutConfig:
def __init__(
self,
*,
connection_timeout_ms: Optional[int] = None,
read_timeout_ms: Optional[int] = None
):
self.connection_timeout_ms = connection_timeout_ms
self.read_timeout_ms = read_timeout_ms
13 changes: 13 additions & 0 deletions docs/box_sdk_gen/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ divided across resource managers.
- [Suppress notifications](#suppress-notifications)
- [Custom headers](#custom-headers)
- [Custom Base URLs](#custom-base-urls)
- [Use Timeouts for API calls](#use-timeouts-for-api-calls)
- [Use Proxy for API calls](#use-proxy-for-api-calls)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -151,6 +152,18 @@ new_client = client.with_custom_base_urls(base_urls=BaseUrls(
))
```

# Use Timeouts for API calls

In order to configure timeout for API calls, calling the `client.with_timeouts(config)` method creates a new client with timeout settings, leaving the original client unmodified.

```python
timeout_config = TimeoutConfig(
connection_timeout_ms=10000,
read_timeout_ms=30000
)
new_client = client.with_timeouts(timeout_config)
```

# Use Proxy for API calls

In order to use a proxy for API calls, calling the `client.with_proxy(proxyConfig)` method creates a new client, leaving the original client unmodified, with the username and password being optional.
Expand Down
28 changes: 28 additions & 0 deletions docs/box_sdk_gen/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Network Exception Handling](#network-exception-handling)
- [Customizing Retry Parameters](#customizing-retry-parameters)
- [Custom Retry Strategy](#custom-retry-strategy)
- [Timeouts](#timeouts)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -178,3 +179,30 @@ auth = BoxDeveloperTokenAuth(token='DEVELOPER_TOKEN_GOES_HERE')
network_session = NetworkSession(retry_strategy=CustomRetryStrategy())
client = BoxClient(auth=auth, network_session=network_session)
```

## Timeouts

You can configure network timeouts with `TimeoutConfig` on `NetworkSession`.
Python SDK supports separate connection and read timeout values in milliseconds.

```python
from box_sdk_gen import BoxClient, BoxDeveloperTokenAuth, NetworkSession, TimeoutConfig

auth = BoxDeveloperTokenAuth(token='DEVELOPER_TOKEN_GOES_HERE')
timeout_config = TimeoutConfig(
connection_timeout_ms=10000,
read_timeout_ms=30000,
)
network_session = NetworkSession(timeout_config=timeout_config)
client = BoxClient(auth=auth, network_session=network_session)
```

How timeout handling works:

- Timeout values are configured in milliseconds and converted to seconds internally for HTTP requests.
- The SDK uses default timeouts when timeout config is not provided: `connection_timeout_ms=5000` and `read_timeout_ms=60000`.
- To disable all SDK timeouts, pass `TimeoutConfig(connection_timeout_ms=None, read_timeout_ms=None)` explicitly to `NetworkSession`.
- You can also disable only one timeout by setting one value to `None` (for example, `connection_timeout_ms=None` or `read_timeout_ms=None`). If you provide only the other value (for example, `read_timeout_ms=30000`) and leave one unspecified, the unspecified field remains `None` and that timeout stays disabled.
- Timeout failures are treated as network exceptions, and retry behavior is controlled by the configured retry strategy.
- Timeout applies to a single HTTP request attempt to the Box API (not the total time across all retries).
- If retries are exhausted, the SDK raises `BoxSDKError` with the underlying request exception.
25 changes: 23 additions & 2 deletions test/box_sdk_gen/test/box_network_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,25 @@ def network_session_mock():
return NetworkSession()


def test_network_session_uses_default_timeout_config_values():
network_session = NetworkSession()

assert network_session.timeout_config.connection_timeout_ms == 5000
assert network_session.timeout_config.read_timeout_ms == 60000


def test_prepare_request_uses_default_network_session_timeouts(network_client):
options = FetchOptions(
url="https://example.com",
method="GET",
network_session=NetworkSession(),
)

api_request = network_client._prepare_request(options=options)

assert api_request.timeout == (5, 60)


@pytest.fixture
def network_client(mock_requests_session):
return BoxNetworkClient(mock_requests_session)
Expand Down Expand Up @@ -295,14 +314,15 @@ def test_prepare_body_invalid_content_type(network_client):
network_client._prepare_body("invalid_content_type", {})


def test_prepare_json_request(network_client):
def test_prepare_json_request(network_client, network_session_mock):
options = FetchOptions(
url="https://example.com",
method="POST",
data={"key": "value"},
headers={"header": "test"},
params={"param": "value"},
content_type="application/json",
network_session=network_session_mock,
)

api_request = network_client._prepare_request(options=options)
Expand All @@ -318,6 +338,7 @@ def test_prepare_json_request(network_client):
},
params={"param": "value"},
data='{"key": "value"}',
timeout=(5, 60),
)


Expand Down Expand Up @@ -379,7 +400,7 @@ def test_make_request(network_client, mock_requests_session, response_200):
)
assert mock_requests_session.request.call_count == 1
mock_requests_session.request.assert_called_once_with(
**request_params, stream=True, timeout=(5, 60)
**request_params, stream=True, timeout=None
)


Expand Down
Loading
Loading