diff --git a/bindings/python/benchmark/async_opendal_benchmark.py b/bindings/python/benchmark/async_opendal_benchmark.py index 368eaadbc24f..6b88b94336aa 100644 --- a/bindings/python/benchmark/async_opendal_benchmark.py +++ b/bindings/python/benchmark/async_opendal_benchmark.py @@ -15,11 +15,13 @@ # specific language governing permissions and limitations # under the License. -from pydantic import BaseSettings import asyncio -import opendal import timeit +from pydantic import BaseSettings + +import opendal + class Config(BaseSettings): aws_endpoint: str diff --git a/bindings/python/python/opendal/__init__.py b/bindings/python/python/opendal/__init__.py index 35bc589a5771..54f3a10cea2d 100644 --- a/bindings/python/python/opendal/__init__.py +++ b/bindings/python/python/opendal/__init__.py @@ -17,6 +17,5 @@ from ._opendal import * - __doc__ = _opendal.__doc__ __all__ = _opendal.__all__ diff --git a/bindings/python/python/opendal/__init__.pyi b/bindings/python/python/opendal/__init__.pyi index 55164c816447..df8134e3e9ab 100644 --- a/bindings/python/python/opendal/__init__.pyi +++ b/bindings/python/python/opendal/__init__.pyi @@ -39,6 +39,7 @@ class Operator: def list(self, path: str) -> Iterable[Entry]: ... def scan(self, path: str) -> Iterable[Entry]: ... def capability(self) -> Capability: ... + def copy(self, source: str, target: str): ... class AsyncOperator: def __init__(self, scheme: str, **kwargs): ... @@ -65,6 +66,7 @@ class AsyncOperator: self, path: str, expire_second: int ) -> PresignedRequest: ... def capability(self) -> Capability: ... + async def copy(self, source: str, target: str): ... class Reader: def read(self, size: Optional[int] = None) -> memoryview: ... diff --git a/bindings/python/src/asyncio.rs b/bindings/python/src/asyncio.rs index 1cedac75ee47..2ebd99dd260a 100644 --- a/bindings/python/src/asyncio.rs +++ b/bindings/python/src/asyncio.rs @@ -139,6 +139,19 @@ impl AsyncOperator { }) } + /// Copy source to target.`` + pub fn copy<'p>( + &'p self, + py: Python<'p>, + source: String, + target: String, + ) -> PyResult<&'p PyAny> { + let this = self.0.clone(); + future_into_py(py, async move { + this.copy(&source, &target).await.map_err(format_pyerr) + }) + } + /// Create a dir at given path. /// /// # Notes diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 1ee0884f9098..8271c483a5cb 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -188,6 +188,11 @@ impl Operator { self.0.stat(path).map_err(format_pyerr).map(Metadata) } + /// Copy source to target. + pub fn copy(&self, source: &str, target: &str) -> PyResult<()> { + self.0.copy(source, target).map_err(format_pyerr) + } + /// Create a dir at given path. /// /// # Notes diff --git a/bindings/python/tests/conftest.py b/bindings/python/tests/conftest.py index 6c1a518dab57..e4a88caa762f 100644 --- a/bindings/python/tests/conftest.py +++ b/bindings/python/tests/conftest.py @@ -17,10 +17,10 @@ import os -from dotenv import load_dotenv import pytest -import opendal +from dotenv import load_dotenv +import opendal load_dotenv() pytest_plugins = ("pytest_asyncio",) diff --git a/bindings/python/tests/test_async_copy.py b/bindings/python/tests/test_async_copy.py new file mode 100644 index 000000000000..b5fea80d1734 --- /dev/null +++ b/bindings/python/tests/test_async_copy.py @@ -0,0 +1,114 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os +from random import randint +from uuid import uuid4 + +import pytest + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}" + await async_operator.copy(source_path, target_path) + read_content = await async_operator.read(target_path) + assert read_content is not None + assert read_content == content + await async_operator.delete(source_path) + await async_operator.delete(target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy_non_exist(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + await async_operator.copy(source_path, target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy_source_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}/" + await async_operator.create_dir(source_path) + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + await async_operator.copy(source_path, target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy_target_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}/" + await async_operator.create_dir(target_path) + with pytest.raises(Exception) as e_info: + await async_operator.copy(source_path, target_path) + await async_operator.delete(source_path) + await async_operator.delete(target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy_self(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + with pytest.raises(Exception) as e_info: + await async_operator.copy(source_path, source_path) + await async_operator.delete(source_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy_nested(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + await async_operator.copy(source_path, target_path) + target_content = await async_operator.read(target_path) + assert target_content is not None + assert target_content == content + await async_operator.delete(source_path) + await async_operator.delete(target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "copy") +async def test_async_copy_overwrite(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + source_content = os.urandom(1024) + target_content = os.urandom(1024) + assert source_content != target_content + await async_operator.write(source_path, source_content) + await async_operator.write(target_path, target_content) + await async_operator.copy(source_path, target_path) + target_content = await async_operator.read(target_path) + assert target_content is not None + assert target_content == source_content + await async_operator.delete(source_path) + await async_operator.delete(target_path) diff --git a/bindings/python/tests/test_read.py b/bindings/python/tests/test_read.py index 7c89ac9fdb38..9c318943c701 100644 --- a/bindings/python/tests/test_read.py +++ b/bindings/python/tests/test_read.py @@ -16,8 +16,8 @@ # under the License. import os -from uuid import uuid4 from random import randint +from uuid import uuid4 import pytest diff --git a/bindings/python/tests/test_sync_copy.py b/bindings/python/tests/test_sync_copy.py new file mode 100644 index 000000000000..3db7bb6f16bf --- /dev/null +++ b/bindings/python/tests/test_sync_copy.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os +from random import randint +from uuid import uuid4 + +import pytest + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}" + operator.copy(source_path, target_path) + read_content = operator.read(target_path) + assert read_content is not None + assert read_content == content + operator.delete(source_path) + operator.delete(target_path) + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy_non_exist(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + operator.copy(source_path, target_path) + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy_source_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}/" + operator.create_dir(source_path) + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + operator.copy(source_path, target_path) + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy_target_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}/" + operator.create_dir(target_path) + with pytest.raises(Exception) as e_info: + operator.copy(source_path, target_path) + operator.delete(source_path) + operator.delete(target_path) + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy_self(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + with pytest.raises(Exception) as e_info: + operator.copy(source_path, source_path) + operator.delete(source_path) + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy_nested(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + operator.copy(source_path, target_path) + target_content = operator.read(target_path) + assert target_content is not None + assert target_content == content + operator.delete(source_path) + operator.delete(target_path) + + +@pytest.mark.need_capability("read", "write", "copy") +def test_sync_copy_overwrite(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + source_content = os.urandom(1024) + target_content = os.urandom(1024) + assert source_content != target_content + operator.write(source_path, source_content) + operator.write(target_path, target_content) + operator.copy(source_path, target_path) + target_content = operator.read(target_path) + assert target_content is not None + assert target_content == source_content + operator.delete(source_path) + operator.delete(target_path) diff --git a/bindings/python/tests/test_write.py b/bindings/python/tests/test_write.py index 68395b3fb20a..987f0ce3c54f 100644 --- a/bindings/python/tests/test_write.py +++ b/bindings/python/tests/test_write.py @@ -16,8 +16,8 @@ # under the License. import os -from uuid import uuid4 from random import randint +from uuid import uuid4 import pytest