diff --git a/faculty/clients/object.py b/faculty/clients/object.py index 2b220bf6..099c0ff5 100644 --- a/faculty/clients/object.py +++ b/faculty/clients/object.py @@ -239,6 +239,37 @@ def copy(self, project_id, source, destination, recursive=False): else: raise + def move(self, project_id, source, destination): + """Move objects in the store + + Parameters + ---------- + project_id : uuid.UUID + The project to move objects in. + source : str + Move object from this source path. + destination : str + Move object to this destination path. + + + Raises + ------ + PathNotFound + When the source path does not exist or is not found. + """ + url_encoded_destination = urllib.parse.quote(destination.lstrip("/")) + + endpoint = "/project/{}/object-move/{}".format( + project_id, url_encoded_destination + ) + params = {"sourcePath": source} + + try: + self._put_raw(endpoint, params=params) + except NotFound as err: + if err.error_code == "source_path_not_found": + raise PathNotFound(source) + def delete(self, project_id, path, recursive=False): """Delete objects in the store. diff --git a/faculty/datasets/__init__.py b/faculty/datasets/__init__.py index 26ba5e3d..ebe52d25 100644 --- a/faculty/datasets/__init__.py +++ b/faculty/datasets/__init__.py @@ -334,19 +334,7 @@ def mv(source_path, destination_path, project_id=None, object_client=None): project_id = project_id or get_context().project_id object_client = object_client or ObjectClient(get_session()) - cp( - source_path, - destination_path, - project_id=project_id, - recursive=True, - object_client=object_client, - ) - rm( - source_path, - project_id=project_id, - recursive=True, - object_client=object_client, - ) + object_client.move(project_id, source_path, destination_path) def cp( diff --git a/tests/clients/test_object.py b/tests/clients/test_object.py index 7afbd0a6..ca6251ac 100644 --- a/tests/clients/test_object.py +++ b/tests/clients/test_object.py @@ -378,6 +378,28 @@ def test_object_client_copy_source_is_a_directory(mocker): client.copy(PROJECT_ID, "source", "destination") +def test_object_client_move_url_encoding(mocker): + mocker.patch.object(ObjectClient, "_put_raw") + + client = ObjectClient(mocker.Mock()) + client.move(PROJECT_ID, "source", "/[1]/") + + ObjectClient._put_raw.assert_called_once_with( + "/project/{}/object-move/%5B1%5D/".format(PROJECT_ID), + params={"sourcePath": "source"}, + ) + + +def test_object_client_move_source_not_found(mocker): + error_code = "source_path_not_found" + exception = NotFound(mocker.Mock(), mocker.Mock(), error_code) + mocker.patch.object(ObjectClient, "_put_raw", side_effect=exception) + + client = ObjectClient(mocker.Mock()) + with pytest.raises(PathNotFound, match="'source' cannot be found"): + client.move(PROJECT_ID, "source", "destination") + + def test_object_client_delete_default(mocker): path = "test-path" mocker.patch.object(ObjectClient, "_delete_raw") diff --git a/tests/datasets/test_init.py b/tests/datasets/test_init.py index 181895c0..0368fe47 100644 --- a/tests/datasets/test_init.py +++ b/tests/datasets/test_init.py @@ -419,23 +419,9 @@ def test_rmdir_nonempty_directory(mocker, prefix): def test_mv(mocker, mock_client): - cp_mock = mocker.patch("faculty.datasets.cp") - rm_mock = mocker.patch("faculty.datasets.rm") - datasets.mv("source-path", "destination-path", project_id=PROJECT_ID) - - cp_mock.assert_called_once_with( - "source-path", - "destination-path", - project_id=PROJECT_ID, - recursive=True, - object_client=mock_client, - ) - rm_mock.assert_called_once_with( - "source-path", - project_id=PROJECT_ID, - recursive=True, - object_client=mock_client, + mock_client.move.assert_called_once_with( + PROJECT_ID, "source-path", "destination-path" )