File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -24,12 +24,16 @@ class HttpTransport:
2424
2525 :param base_url: The base URL of the Flow API
2626 (e.g. ``"https://app.flow.bio/api"``).
27+ :param connection_retries: Number of retries on connection failure.
28+ Only retries ``ConnectError``/``ConnectTimeout`` (TCP never
29+ established), so it is safe for all HTTP methods. Defaults to ``3``.
2730 """
2831
29- def __init__ (self , base_url : str ) -> None :
32+ def __init__ (self , base_url : str , connection_retries : int = 3 ) -> None :
3033 self ._base_url = base_url .rstrip ("/" )
3134 self ._client = httpx .Client (
3235 headers = {"User-Agent" : f"flowbio-python/{ _CLIENT_VERSION } " },
36+ transport = httpx .HTTPTransport (retries = connection_retries ),
3337 )
3438
3539 _STATUS_TO_EXCEPTION : dict [int , type [FlowApiError ]] = {
Original file line number Diff line number Diff line change @@ -13,6 +13,11 @@ class ClientConfig:
1313 Defaults to 1 MB.
1414 :param show_progress: Whether to display progress bars during
1515 file uploads. Defaults to ``True``.
16+ :param connection_retries: Number of times to retry on connection
17+ failure (e.g. ``ConnectError``, ``ConnectTimeout``). Only retries
18+ when the TCP connection was never established, so it is safe for
19+ all HTTP methods including POST. Set to ``0`` to disable.
20+ Defaults to ``3``.
1621
1722 Example usage::
1823
@@ -26,6 +31,7 @@ class ClientConfig:
2631
2732 chunk_size : int = 1_000_000
2833 show_progress : bool = True
34+ connection_retries : int = 3
2935
3036
3137class Client :
@@ -58,8 +64,11 @@ def __init__(
5864 base_url : str = "https://app.flow.bio/api" ,
5965 config : ClientConfig | None = None ,
6066 ) -> None :
61- self ._transport = HttpTransport (base_url )
6267 self ._config = config or ClientConfig ()
68+ self ._transport = HttpTransport (
69+ base_url ,
70+ connection_retries = self ._config .connection_retries ,
71+ )
6372 self ._samples = SampleResource (self ._transport , self ._config )
6473
6574 @property
Original file line number Diff line number Diff line change 11import json
2+ from unittest .mock import patch
23
34import httpx
45import pytest
@@ -111,6 +112,12 @@ def test_default_config_values(self) -> None:
111112
112113 assert config .chunk_size == 1_000_000
113114 assert config .show_progress is True
115+ assert config .connection_retries == 3
116+
117+ def test_custom_connection_retries (self ) -> None :
118+ config = ClientConfig (connection_retries = 0 )
119+
120+ assert config .connection_retries == 0
114121
115122 def test_config_is_immutable (self ) -> None :
116123 config = ClientConfig ()
@@ -120,3 +127,14 @@ def test_config_is_immutable(self) -> None:
120127
121128 def test_client_accepts_custom_config (self ) -> None :
122129 Client (config = ClientConfig (chunk_size = 5_000_000 ))
130+
131+ @patch ("flowbio.v2.client.HttpTransport" )
132+ def test_client_passes_connection_retries_to_transport (self , mock_transport ) -> None :
133+ connection_retries = 5
134+
135+ Client (config = ClientConfig (connection_retries = connection_retries ))
136+
137+ mock_transport .assert_called_once_with (
138+ "https://app.flow.bio/api" ,
139+ connection_retries = connection_retries ,
140+ )
Original file line number Diff line number Diff line change @@ -425,6 +425,25 @@ def test_uploads_in_chunks(self, tmp_path: Path) -> None:
425425
426426 assert route .call_count == 3
427427
428+ @respx .mock
429+ def test_connect_error_on_chunk_propagates (self , tmp_path : Path ) -> None :
430+ file_path = tmp_path / "reads.fastq"
431+ file_path .write_bytes (b"A" * 100 )
432+ route = respx .post (f"{ DEFAULT_BASE_URL } /upload/sample" )
433+ route .side_effect = [
434+ httpx .Response (200 , json = {"sample_id" : None , "data_id" : "d1" }),
435+ httpx .ConnectError ("Connection refused" ),
436+ ]
437+
438+ client = Client (config = ClientConfig (chunk_size = 40 , show_progress = False ))
439+
440+ with pytest .raises (httpx .ConnectError ):
441+ client .samples .upload_sample (
442+ name = "My Sample" ,
443+ sample_type = "rna_seq" ,
444+ data = {"reads1" : file_path },
445+ )
446+
428447 def test_rejects_invalid_reads_key (self , tmp_path : Path ) -> None :
429448 file_path = tmp_path / "reads.fastq"
430449 file_path .write_bytes (b"ATCG" )
Original file line number Diff line number Diff line change 11import json
2+ from unittest .mock import patch
23
34import httpx
45import respx
1617from tests .unit .v2 .conftest import DEFAULT_BASE_URL
1718
1819
20+ class TestTransportConnectionRetries :
21+
22+ @patch ("flowbio.v2._transport.httpx.HTTPTransport" )
23+ def test_default_retries (self , mock_http_transport ) -> None :
24+ HttpTransport (DEFAULT_BASE_URL )
25+
26+ mock_http_transport .assert_called_once_with (retries = 3 )
27+
28+ @patch ("flowbio.v2._transport.httpx.HTTPTransport" )
29+ def test_custom_retries (self , mock_http_transport ) -> None :
30+ connection_retries = 5
31+
32+ HttpTransport (DEFAULT_BASE_URL , connection_retries = connection_retries )
33+
34+ mock_http_transport .assert_called_once_with (retries = connection_retries )
35+
36+ @patch ("flowbio.v2._transport.httpx.HTTPTransport" )
37+ def test_retries_disabled (self , mock_http_transport ) -> None :
38+ HttpTransport (DEFAULT_BASE_URL , connection_retries = 0 )
39+
40+ mock_http_transport .assert_called_once_with (retries = 0 )
41+
42+
1943class TestTransportGet :
2044
2145 @respx .mock
You can’t perform that action at this time.
0 commit comments