Skip to content

Commit 6f2ad28

Browse files
committed
:Setting timeout for HTTP: requests does not hanging forever if the download speed is zero (addressing an issue when Apple does not response to HTTP GET)
1 parent c4a6322 commit 6f2ad28

File tree

3 files changed

+69
-9
lines changed

3 files changed

+69
-9
lines changed

src/icloudpd/download.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import socket
77
import time
88

9-
from requests import Response
9+
from requests import Response, Timeout
1010
from requests.exceptions import ConnectionError
1111
from tzlocal import get_localzone
1212

@@ -125,7 +125,7 @@ def download_media(
125125
)
126126
break
127127

128-
except (ConnectionError, socket.timeout, PyiCloudAPIResponseException) as ex:
128+
except (ConnectionError, socket.timeout, PyiCloudAPIResponseException, Timeout) as ex:
129129
if "Invalid global session" in str(ex):
130130
logger.error("Session error, re-authenticating...")
131131
if retries > 0:

src/pyicloud_ipd/session.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def filter(self, record: logging.LogRecord) -> bool:
3535
message = record.getMessage()
3636
if self.name in message:
3737
record.msg = message.replace(self.name, "********")
38-
record.args = [] # type: ignore[assignment]
38+
record.args = [] # type: ignore[assignment]
3939

4040
return True
4141

@@ -45,11 +45,12 @@ class PyiCloudSession(Session):
4545

4646
def __init__(self, service: Any):
4747
self.service = service
48+
self._timeout = 30.0
4849
super().__init__()
4950

5051
@override
51-
# type: ignore
52-
def request(self, method: str, url, **kwargs):
52+
# type: ignore
53+
def request(self, method: str, url, **kwargs):
5354

5455
# Charge logging to the right service endpoint
5556
callee = inspect.stack()[2]
@@ -62,6 +63,8 @@ def request(self, method: str, url, **kwargs):
6263

6364
has_retried = kwargs.get("retried")
6465
kwargs.pop("retried", None)
66+
if "timeout" not in kwargs and self._timeout is not None:
67+
kwargs["timeout"] = self._timeout
6568
response = super().request(method, url, **kwargs)
6669

6770
content_type = response.headers.get("Content-Type", "").split(";")[0]
@@ -131,7 +134,7 @@ def request(self, method: str, url, **kwargs):
131134

132135
try:
133136
data = response.json() if response.status_code != 204 else {}
134-
except:
137+
except:
135138
request_logger.warning("Failed to parse response with JSON mimetype")
136139
return response
137140

tests/test_download_photos_id.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import pytest
1414
from click.testing import CliRunner
1515
from piexif._exceptions import InvalidImageDataError
16-
from requests import Response
16+
from requests import Response, Timeout
1717
from requests.exceptions import ConnectionError
1818
from vcr import VCR
1919

@@ -543,11 +543,11 @@ def mocked_authenticate(self: PyiCloudService) -> None:
543543

544544
assert result.exit_code == 1
545545

546-
def test_handle_connection_error_name_id7(self) -> None:
546+
def test_timeout_error(self) -> None:
547547
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
548548

549549
def mock_raise_response_error(_arg: Any) -> NoReturn:
550-
raise ConnectionError("Connection Error")
550+
raise Timeout("Connection Error")
551551

552552
with mock.patch.object(PhotoAsset, "download") as pa_download:
553553
pa_download.side_effect = mock_raise_response_error
@@ -599,6 +599,63 @@ def mocked_authenticate(self: PyiCloudService) -> None:
599599
)
600600
assert result.exit_code == 0
601601

602+
def test_handle_connection_error_name_id7(self) -> None:
603+
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
604+
605+
def mock_raise_response_error(_arg: Any) -> NoReturn:
606+
raise ConnectionError("Connection Error")
607+
608+
with mock.patch.object(PhotoAsset, "download") as pa_download:
609+
pa_download.side_effect = mock_raise_response_error
610+
611+
# Let the initial authenticate() call succeed,
612+
# but do nothing on the second try.
613+
orig_authenticate = PyiCloudService.authenticate
614+
615+
def mocked_authenticate(self: PyiCloudService) -> None:
616+
if not hasattr(self, "already_authenticated"):
617+
orig_authenticate(self)
618+
setattr(self, "already_authenticated", True) # noqa: B010
619+
620+
with mock.patch("icloudpd.constants.WAIT_SECONDS", 0): # noqa: SIM117
621+
with mock.patch.object(PyiCloudService, "authenticate", new=mocked_authenticate):
622+
_, result = run_icloudpd_test(
623+
self.assertEqual,
624+
self.root_path,
625+
base_dir,
626+
"listing_photos.yml",
627+
[],
628+
[],
629+
[
630+
"--username",
631+
"jdoe@gmail.com",
632+
"--password",
633+
"password1",
634+
"--recent",
635+
"1",
636+
"--skip-videos",
637+
"--skip-live-photos",
638+
"--no-progress-bar",
639+
"--file-match-policy",
640+
"name-id7",
641+
],
642+
)
643+
644+
# Error msg should be repeated 5 times
645+
assert (
646+
self._caplog.text.count(
647+
"Error downloading IMG_7409_QVk2Yyt.JPG, retrying after 0 seconds..."
648+
)
649+
== 5
650+
)
651+
652+
self.assertIn(
653+
"ERROR Could not download IMG_7409_QVk2Yyt.JPG. Please try again later.",
654+
self._caplog.text,
655+
)
656+
assert result.exit_code == 0
657+
658+
602659
def test_handle_albums_error_name_id7(self) -> None:
603660
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
604661

0 commit comments

Comments
 (0)