Skip to content

Commit de3ec44

Browse files
committed
feat(toggl): dynamic timeout based on report range
1 parent b876c19 commit de3ec44

File tree

2 files changed

+52
-25
lines changed

2 files changed

+52
-25
lines changed

compiler_admin/services/toggl.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ def download_time_entries(
221221
"""
222222
start = start_date.strftime("%Y-%m-%d")
223223
end = end_date.strftime("%Y-%m-%d")
224+
range_days = (end_date - start_date).days
225+
# calculate a timeout based on the size of the reporting period in days
226+
# approximately 5 seconds per month of query size, with a minimum of 5 seconds
227+
timeout = int((max(30, range_days) / 30.0) * 5)
224228

225229
if ("client_ids" not in kwargs or not kwargs["client_ids"]) and isinstance(_toggl_client_id(), int):
226230
kwargs["client_ids"] = [_toggl_client_id()]
@@ -237,7 +241,7 @@ def download_time_entries(
237241
headers = _toggl_api_headers()
238242
url = _toggl_api_report_url("search/time_entries.csv")
239243

240-
response = requests.post(url, json=params, headers=headers, timeout=5)
244+
response = requests.post(url, json=params, headers=headers, timeout=timeout)
241245
response.raise_for_status()
242246

243247
# the raw response has these initial 3 bytes:

tests/services/test_toggl.py

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,31 @@ def mock_google_user_info(mocker):
5959
return mocker.patch(f"{MODULE}.google_user_info")
6060

6161

62+
@pytest.fixture
63+
def mock_api_env(monkeypatch):
64+
monkeypatch.setenv("TOGGL_API_TOKEN", "token")
65+
monkeypatch.setenv("TOGGL_CLIENT_ID", "1234")
66+
monkeypatch.setenv("TOGGL_WORKSPACE_ID", "workspace")
67+
68+
6269
@pytest.fixture
6370
def mock_requests(mocker):
6471
return mocker.patch(f"{MODULE}.requests")
6572

6673

74+
@pytest.fixture
75+
def mock_api_post(mocker, mock_requests, toggl_file):
76+
# setup a mock response to a requests.post call
77+
mock_csv_bytes = Path(toggl_file).read_bytes()
78+
mock_post_response = mocker.Mock()
79+
mock_post_response.raise_for_status.return_value = None
80+
# prepend the BOM to the mock content
81+
mock_post_response.content = b"\xef\xbb\xbf" + mock_csv_bytes
82+
# override the requests.post call to return the mock response
83+
mock_requests.post.return_value = mock_post_response
84+
return mock_requests
85+
86+
6787
def test_harvest_client_name(monkeypatch):
6888
assert _harvest_client_name() == "Test_Client"
6989

@@ -221,35 +241,26 @@ def test_convert_to_harvest_sample(toggl_file, harvest_file, mock_google_user_in
221241
assert output_df["Client"].eq("Test Client 123").all()
222242

223243

224-
def test_download_time_entries(monkeypatch, toggl_file, mock_requests, mocker):
225-
monkeypatch.setenv("TOGGL_API_TOKEN", "token")
226-
monkeypatch.setenv("TOGGL_CLIENT_ID", "1234")
227-
monkeypatch.setenv("TOGGL_WORKSPACE_ID", "workspace")
228-
229-
# setup a mock response to a requests.post call
230-
mock_csv_bytes = Path(toggl_file).read_bytes()
231-
mock_post_response = mocker.Mock()
232-
mock_post_response.raise_for_status.return_value = None
233-
# prepend the BOM to the mock content
234-
mock_post_response.content = b"\xef\xbb\xbf" + mock_csv_bytes
235-
# override the requests.post call to return the mock response
236-
mock_requests.post.return_value = mock_post_response
237-
244+
@pytest.mark.usefixtures("mock_api_env")
245+
def test_download_time_entries(toggl_file, mock_api_post):
238246
dt = datetime.now()
247+
mock_csv_bytes = Path(toggl_file).read_bytes()
239248

240249
with NamedTemporaryFile("w") as temp:
241250
download_time_entries(dt, dt, temp.name, extra_1=1, extra_2="two")
242251

243-
called_params = mock_requests.post.call_args.kwargs["json"]
244-
assert isinstance(called_params, dict)
245-
assert called_params["billable"] is True
246-
assert called_params["client_ids"] == [1234]
247-
assert called_params["end_date"] == dt.strftime("%Y-%m-%d")
248-
assert called_params["extra_1"] == 1
249-
assert called_params["extra_2"] == "two"
250-
assert called_params["rounding"] == 1
251-
assert called_params["rounding_minutes"] == 15
252-
assert called_params["start_date"] == dt.strftime("%Y-%m-%d")
252+
json_params = mock_api_post.post.call_args.kwargs["json"]
253+
assert isinstance(json_params, dict)
254+
assert json_params["billable"] is True
255+
assert json_params["client_ids"] == [1234]
256+
assert json_params["end_date"] == dt.strftime("%Y-%m-%d")
257+
assert json_params["extra_1"] == 1
258+
assert json_params["extra_2"] == "two"
259+
assert json_params["rounding"] == 1
260+
assert json_params["rounding_minutes"] == 15
261+
assert json_params["start_date"] == dt.strftime("%Y-%m-%d")
262+
263+
assert mock_api_post.post.call_args.kwargs["timeout"] == 5
253264

254265
temp.flush()
255266
response_csv_bytes = Path(temp.name).read_bytes()
@@ -265,3 +276,15 @@ def test_download_time_entries(monkeypatch, toggl_file, mock_requests, mocker):
265276
# as corresponding column values from the mock DataFrame
266277
for col in response_df.columns:
267278
assert response_df[col].equals(mock_df[col])
279+
280+
281+
@pytest.mark.usefixtures("mock_api_env")
282+
def test_download_time_entries_dynamic_timeout(mock_api_post):
283+
# range of 6 months
284+
# timeout should be 6 * 5 = 30
285+
start = datetime(2024, 1, 1)
286+
end = datetime(2024, 6, 30)
287+
288+
download_time_entries(start, end)
289+
290+
assert mock_api_post.post.call_args.kwargs["timeout"] == 30

0 commit comments

Comments
 (0)