diff --git a/source/ftrack_api/accessor/server.py b/source/ftrack_api/accessor/server.py index da6b0262..0c9e5f78 100644 --- a/source/ftrack_api/accessor/server.py +++ b/source/ftrack_api/accessor/server.py @@ -4,7 +4,6 @@ import os import hashlib import base64 -import json import requests @@ -22,6 +21,7 @@ def __init__(self, resource_identifier, session, mode="rb"): self.mode = mode self.resource_identifier = resource_identifier self._session = session + self._timeout = session.request_timeout self._has_read = False super(ServerFile, self).__init__() @@ -46,15 +46,21 @@ def _read(self): position = self.tell() self.seek(0) - response = requests.get( - "{0}/component/get".format(self._session.server_url), - params={ - "id": self.resource_identifier, - "username": self._session.api_user, - "apiKey": self._session.api_key, - }, - stream=True, - ) + try: + response = requests.get( + "{0}/component/get".format(self._session.server_url), + params={ + "id": self.resource_identifier, + "username": self._session.api_user, + "apiKey": self._session.api_key, + }, + stream=True, + timeout=self._timeout, + ) + except Exception as error: + raise ftrack_api.exception.AccessorOperationFailedError( + "Failed to read data: {0}.".format(error) + ) try: response.raise_for_status() @@ -105,7 +111,10 @@ def _write(self): # Put the file based on the metadata. response = requests.put( - metadata["url"], data=self.wrapped_file, headers=metadata["headers"] + metadata["url"], + data=self.wrapped_file, + headers=metadata["headers"], + timeout=self._timeout, ) try: @@ -153,6 +162,7 @@ def __init__(self, session, **kw): super(_ServerAccessor, self).__init__(**kw) self._session = session + self._timeout = session.request_timeout def open(self, resource_identifier, mode="rb"): """Return :py:class:`~ftrack_api.Data` for *resource_identifier*.""" @@ -160,17 +170,26 @@ def open(self, resource_identifier, mode="rb"): def remove(self, resourceIdentifier): """Remove *resourceIdentifier*.""" - response = requests.get( - "{0}/component/remove".format(self._session.server_url), - params={ - "id": resourceIdentifier, - "username": self._session.api_user, - "apiKey": self._session.api_key, - }, - ) - if response.status_code != 200: + try: + response = requests.get( + "{0}/component/remove".format(self._session.server_url), + params={ + "id": resourceIdentifier, + "username": self._session.api_user, + "apiKey": self._session.api_key, + }, + timeout=self._timeout, + ) + except Exception as error: + raise ftrack_api.exception.AccessorOperationFailedError( + "Failed to remove file: {0}.".format(error) + ) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as error: raise ftrack_api.exception.AccessorOperationFailedError( - "Failed to remove file." + "Failed to remove file: {0}.".format(error) ) def get_container(self, resource_identifier): diff --git a/test/unit/accessor/test_server.py b/test/unit/accessor/test_server.py index bf366c29..b1789428 100644 --- a/test/unit/accessor/test_server.py +++ b/test/unit/accessor/test_server.py @@ -11,27 +11,28 @@ import ftrack_api.data -def test_read_and_write(new_component, session): - """Read and write data from server accessor.""" - random_data = uuid.uuid1().hex.encode() +@pytest.fixture(scope="module") +def random_binary_data(): + return uuid.uuid1().hex.encode() + +def test_read_and_write(new_component, random_binary_data, session): + """Read and write data from server accessor.""" accessor = ftrack_api.accessor.server._ServerAccessor(session) http_file = accessor.open(new_component["id"], mode="wb") - http_file.write(random_data) + http_file.write(random_binary_data) http_file.close() data = accessor.open(new_component["id"], "r") - assert data.read() == random_data, "Read data is the same as written." + assert data.read() == random_binary_data, "Read data is the same as written." data.close() -def test_remove_data(new_component, session): +def test_remove_data(new_component, random_binary_data, session): """Remove data using server accessor.""" - random_data = uuid.uuid1().hex - accessor = ftrack_api.accessor.server._ServerAccessor(session) http_file = accessor.open(new_component["id"], mode="wb") - http_file.write(random_data) + http_file.write(random_binary_data) http_file.close() accessor.remove(new_component["id"]) @@ -39,3 +40,56 @@ def test_remove_data(new_component, session): data = accessor.open(new_component["id"], "r") with pytest.raises(ftrack_api.exception.AccessorOperationFailedError): data.read() + + +def test_read_timeout(new_component, random_binary_data, session, monkeypatch): + """Test that read operations respect timeout settings.""" + # First, write some data so there's something to read + accessor = ftrack_api.accessor.server._ServerAccessor(session) + http_file = accessor.open(new_component["id"], mode="wb") + http_file.write(random_binary_data) + http_file.close() + + # Set an impossibly short timeout - no server can respond this fast + monkeypatch.setattr(session, "request_timeout", 0.0001) + + # Open a new file handle (this captures the patched timeout) + data = accessor.open(new_component["id"], "r") + with pytest.raises( + ftrack_api.exception.AccessorOperationFailedError, match="timed out" + ): + data.read() + + +def test_write_timeout(new_component, random_binary_data, session, monkeypatch): + """Test that write operations respect timeout settings.""" + # Set an impossibly short timeout + monkeypatch.setattr(session, "request_timeout", 0.0001) + + accessor = ftrack_api.accessor.server._ServerAccessor(session) + http_file = accessor.open(new_component["id"], mode="wb") + http_file.write(random_binary_data) + + # Timeout is caught and wrapped in AccessorOperationFailedError + with pytest.raises( + ftrack_api.exception.AccessorOperationFailedError, match="timed out" + ): + http_file.close() # close() triggers flush() which triggers _write() + + +def test_remove_timeout(new_component, random_binary_data, session, monkeypatch): + """Test that remove operations respect timeout settings.""" + # Write something first + accessor = ftrack_api.accessor.server._ServerAccessor(session) + http_file = accessor.open(new_component["id"], mode="wb") + http_file.write(random_binary_data) + http_file.close() + + # Set timeout and create new accessor to pick up the timeout for remove() + monkeypatch.setattr(session, "request_timeout", 0.0001) + accessor = ftrack_api.accessor.server._ServerAccessor(session) + + with pytest.raises( + ftrack_api.exception.AccessorOperationFailedError, match="timed out" + ): + accessor.remove(new_component["id"])