Skip to content

Commit 7bb55fa

Browse files
authored
add timeout override to create_bitstream
2 parents 68c4c42 + 952a6b1 commit 7bb55fa

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

dspace_rest_client/client.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ def __init__(self, api_endpoint=API_ENDPOINT, username=USERNAME, password=PASSWO
103103
:param api_endpoint: base path to DSpace REST API, eg. http://localhost:8080/server/api
104104
:param username: username with appropriate privileges to perform operations on REST API
105105
:param password: password for the above username
106+
:param timeout: default per-request timeout in seconds, used by every request unless a
107+
method call overrides it (eg. create_bitstream's own timeout argument).
108+
None (default) falls back to DEFAULT_TIMEOUT (60s).
106109
"""
107110
self.session = requests.Session()
108111
self.API_ENDPOINT = api_endpoint
@@ -830,7 +833,7 @@ def get_bitstreams(self, uuid=None, bundle=None, page=0, size=20, sort=None):
830833
bitstreams.append(Bitstream(bitstream_resource))
831834
return bitstreams
832835

833-
def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadata=None, retry=False):
836+
def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadata=None, retry=False, timeout=None):
834837
"""
835838
Upload a file and create a bitstream for a specified parent bundle, from the uploaded file and
836839
the supplied metadata.
@@ -845,6 +848,10 @@ def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadat
845848
@param metadata: Full metadata JSON
846849
@param retry: A 'retried' indicator. If the first attempt fails due to an expired or missing auth
847850
token, the request will retry once, after the token is refreshed. (default: False)
851+
@param timeout: Per-call timeout in seconds for this upload, overriding self.timeout - useful for
852+
large files that need longer than the client's default. None (default) falls back
853+
to self.timeout. Preserved across the CSRF-retry recursion, so it still applies
854+
to the retried request.
848855
@return: constructed Bitstream object from the API response, or None if the operation failed.
849856
"""
850857
# TODO: It is probably wise to allow the bundle UUID to be simply passed as an alternative to having the full
@@ -865,19 +872,21 @@ def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadat
865872
h.update({'Content-Encoding': 'gzip', 'User-Agent': self.USER_AGENT})
866873
req = Request('POST', url, data=payload, headers=h, files=files)
867874
prepared_req = self.session.prepare_request(req)
868-
r = self.session.send(prepared_req, proxies=self.proxies, timeout=self.timeout)
875+
r = self.session.send(prepared_req, proxies=self.proxies,
876+
timeout=timeout if timeout is not None else self.timeout)
869877
if 'DSPACE-XSRF-TOKEN' in r.headers:
870878
t = r.headers['DSPACE-XSRF-TOKEN']
871879
_logger.debug('Updating token to ' + t)
872880
self.session.headers.update({'X-XSRF-Token': t})
873881
self.session.cookies.update({'X-XSRF-Token': t})
874882
if not retry and r.status_code in (401, 403):
875883
r_json = parse_json(r)
876-
if 'message' in r_json and 'CSRF token' in r_json['message']:
884+
if 'message' in (r_json or {}) and 'CSRF token' in r_json['message']:
877885
_logger.debug("Retrying request with updated CSRF token")
878886
else:
879887
self.authenticate()
880-
return self.create_bitstream(bundle, name, path, mime, metadata, True)
888+
return self.create_bitstream(bundle=bundle, name=name, path=path, mime=mime,
889+
metadata=metadata, retry=True, timeout=timeout)
881890

882891
if r.status_code == 201 or r.status_code == 200:
883892
# Success

tests/test_client_write.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,26 @@ def test_server_error_returns_none(self):
192192
bundle=bundle, name="a.pdf", path=self.path,
193193
mime="application/pdf"))
194194

195+
def test_csrf_retry_preserves_custom_timeout(self):
196+
# the retry recursion used to drop a caller-supplied timeout override,
197+
# silently falling back to the client default on the retried request
198+
c = make_client()
199+
bundle = Bundle(bundle_json("bnd"))
200+
with requests_mock.Mocker() as m:
201+
m.post(f"{API}/core/bundles/bnd/bitstreams", [
202+
{"status_code": 403, "json": {"message": "CSRF token invalid"}},
203+
{"status_code": 201, "json": bitstream_json("bsnew", "a.pdf", size=20)},
204+
])
205+
bs = c.create_bitstream(
206+
bundle=bundle, name="a.pdf", path=self.path,
207+
mime="application/pdf", timeout=900)
208+
self.assertIsInstance(bs, Bitstream)
209+
# initial attempt + exactly one CSRF retry, both bounded by the
210+
# caller's override rather than the client's flat default
211+
self.assertEqual(len(m.request_history), 2)
212+
self.assertEqual(m.request_history[0].timeout, 900)
213+
self.assertEqual(m.request_history[1].timeout, 900)
214+
195215

196216
class TestCreateClarinAllowances(unittest.TestCase):
197217

0 commit comments

Comments
 (0)