diff --git a/novem/api_ref.py b/novem/api_ref.py index 66998a2..839573d 100644 --- a/novem/api_ref.py +++ b/novem/api_ref.py @@ -1,3 +1,4 @@ +import functools import os import sys import urllib.request @@ -132,6 +133,9 @@ def __init__( self._session = requests.Session() self._session.headers.update(get_ua(is_cli)) self._session.proxies = urllib.request.getproxies() + self._session.request = functools.partial( # type: ignore[method-assign] + self._session.request, timeout=(10, 120) + ) if cfg.ignore_ssl: # supress ssl warnings diff --git a/novem/cli/gql.py b/novem/cli/gql.py index 5cbe6f0..b0a4f40 100644 --- a/novem/cli/gql.py +++ b/novem/cli/gql.py @@ -6,6 +6,7 @@ """ import datetime +import functools import json import re import shutil @@ -35,6 +36,9 @@ def __init__(self, *, debug: bool = False, gql: Any = False, **connection: Any) _, config = get_current_config(**connection) self._config = config self._session = requests.Session() + self._session.request = functools.partial( # type: ignore[method-assign] + self._session.request, timeout=(10, 120) + ) self._debug = debug # Support both old gql_debug and new gql parameter for debug mode self._gql_debug = gql is True # True when --gql with no argument diff --git a/novem/job/__init__.py b/novem/job/__init__.py index ad94e50..15be271 100644 --- a/novem/job/__init__.py +++ b/novem/job/__init__.py @@ -415,7 +415,7 @@ def run( ] if self._debug: print(f" files in: {len(upload)} ({list(upload.keys())})") - r = self._session.post(path, files=multipart, stream=bool(output)) + r = self._session.post(path, files=multipart, stream=bool(output), timeout=(30, 1800)) else: if self._debug: print(" files in: 0") @@ -424,6 +424,7 @@ def run( headers={"Content-type": "application/json; charset=utf-8"}, data="{}", stream=bool(output), + timeout=(30, 1800), ) if not r.ok: diff --git a/tests/test_job.py b/tests/test_job.py index d72b866..7819e1e 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest +from requests import Response from novem import Job from novem.exceptions import Novem403, Novem404 @@ -1015,3 +1016,48 @@ def test_dedup_path_with_conflicts(tmp_path): open(tmp_path / "file (1).txt", "w").close() assert _Job._dedup_path(str(tmp_path), "file.txt") == os.path.join(str(tmp_path), "file (2).txt") + + +# --------------------------------------------------------------------------- +# timeout tests +# --------------------------------------------------------------------------- + + +def _ok_response(content=b""): + r = Response() + r.status_code = 200 + r._content = content + return r + + +def test_session_default_timeout(requests_mock): + """Default (10, 120) timeout reaches session.send for ordinary API calls.""" + j, _ = _make_job(requests_mock) + + with patch.object(j._session, "send", return_value=_ok_response(b"ok")) as mock_send: + j.api_read("/log") + + assert mock_send.call_args.kwargs.get("timeout") == (10, 120) + + +def test_job_run_no_files_uses_job_timeout(requests_mock): + """run() without files passes timeout=(30, 1800) through to session.send.""" + j, _ = _make_job(requests_mock) + + with patch.object(j._session, "send", return_value=_ok_response()) as mock_send: + j.run() + + assert mock_send.call_args.kwargs.get("timeout") == (30, 1800) + + +def test_job_run_with_files_uses_job_timeout(requests_mock, tmp_path): + """run() with files passes timeout=(30, 1800) through to session.send.""" + j, _ = _make_job(requests_mock) + + f1 = tmp_path / "data.csv" + f1.write_text("a,b\n1,2\n") + + with patch.object(j._session, "send", return_value=_ok_response()) as mock_send: + j.run(files=[f"@{f1}"]) + + assert mock_send.call_args.kwargs.get("timeout") == (30, 1800)