Repository: apache/opendal (language: rust)

Issue:
new feature: open exists api in operator for python binding
### Feature Description

https://opendal.apache.org/docs/rust/opendal/struct.Operator.html#method.exists
seems it is not in python binding and some repo need to use it

### Problem and Solution

add it

### Additional Context

_No response_

### Are you willing to contribute to the development of this feature?

- [X] Yes, I am willing to contribute to the development of this feature.


Discussion / hints:
@Xuanwo @yihong0618 Yes, I am willing to contribute to the development of this feature. Can you please explain it more how can i do that ? 


> @Xuanwo @yihong0618 Yes, I am willing to contribute to the development of this feature. Can you please explain it more how can i do that ?

Hi, we need to implement an `exists(&self, path: &str) -> PyResult<bool>` like the following:

https://github.com/apache/opendal/blob/a9d0ec658449cdcb60e2c59e5ac7c0163e00ca31/bindings/python/src/operator.rs#L182-L184
@Xuanwo , I am very much confused and unable to understand some features of GitHub . Actually I changed in the file for resolving issue #5408 , but I don't know why it is not seen here . Sorry,  for this inconvenience. From the next time , I will make sure before doing any PR .
Can you please now tell me , What is the issue name of 5442 ? 
> Can you please now tell me , What is the issue name of 5442 ?

Hi, perhaps you could take a look at our contributing guide first: https://github.com/apache/opendal/blob/main/CONTRIBUTING.md#github-pull-requests

Fix patch:
diff --git a/bindings/python/python/opendal/__init__.pyi b/bindings/python/python/opendal/__init__.pyi
index 557f71dc9d7..442a094fa39 100644
--- a/bindings/python/python/opendal/__init__.pyi
+++ b/bindings/python/python/opendal/__init__.pyi
@@ -45,6 +45,7 @@ class Operator(_Base):
     def stat(self, path: PathBuf) -> Metadata: ...
     def create_dir(self, path: PathBuf) -> None: ...
     def delete(self, path: PathBuf) -> None: ...
+    def exists(self, path: PathBuf) -> bool: ...
     def list(self, path: PathBuf) -> Iterable[Entry]: ...
     def scan(self, path: PathBuf) -> Iterable[Entry]: ...
     def capability(self) -> Capability: ...
@@ -72,6 +73,7 @@ class AsyncOperator(_Base):
     async def stat(self, path: PathBuf) -> Metadata: ...
     async def create_dir(self, path: PathBuf) -> None: ...
     async def delete(self, path: PathBuf) -> None: ...
+    async def exists(self, path: PathBuf) -> bool: ...
     async def list(self, path: PathBuf) -> AsyncIterable[Entry]: ...
     async def scan(self, path: PathBuf) -> AsyncIterable[Entry]: ...
     async def presign_stat(
diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs
index 73e0fb95b1b..910a3e7ceac 100644
--- a/bindings/python/src/operator.rs
+++ b/bindings/python/src/operator.rs
@@ -198,6 +198,16 @@ impl Operator {
         self.core.delete(&path).map_err(format_pyerr)
     }
 
+    /// Check given path is exists.
+    ///
+    /// # Notes
+    ///
+    /// - Check not existing path won't return errors.
+    pub fn exists(&self, path: PathBuf) -> PyResult<bool> {
+        let path = path.to_string_lossy().to_string();
+        self.core.exists(&path).map_err(format_pyerr)
+    }
+
     /// List current dir path.
     pub fn list(&self, path: PathBuf) -> PyResult<BlockingLister> {
         let path = path.to_string_lossy().to_string();
@@ -460,6 +470,20 @@ impl AsyncOperator {
         )
     }
 
+    /// Check given path is exists.
+    ///
+    /// # Notes
+    ///
+    /// - Check not existing path won't return errors.
+    pub fn exists<'p>(&'p self, py: Python<'p>, path: PathBuf) -> PyResult<Bound<'p, PyAny>> {
+        let this = self.core.clone();
+        let path = path.to_string_lossy().to_string();
+        future_into_py(
+            py,
+            async move { this.exists(&path).await.map_err(format_pyerr) },
+        )
+    }
+
     /// List current dir path.
     pub fn list<'p>(&'p self, py: Python<'p>, path: PathBuf) -> PyResult<Bound<'p, PyAny>> {
         let this = self.core.clone();


Test patch:
diff --git a/bindings/python/tests/test_async_exists.py b/bindings/python/tests/test_async_exists.py
new file mode 100644
index 00000000000..4899efc5193
--- /dev/null
+++ b/bindings/python/tests/test_async_exists.py
@@ -0,0 +1,31 @@
+# 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 uuid import uuid4
+
+import pytest
+
+
+@pytest.mark.asyncio
+@pytest.mark.need_capability("read", "write", "delete", "list", "create_dir")
+async def test_async_remove_all(service_name, operator, async_operator):
+    content = os.urandom(1024)
+    target = f"random_{str(uuid4())}"
+    await async_operator.write(target, content)
+    assert await async_operator.exists(target)
+    assert not await async_operator.exists(target + "1")
diff --git a/bindings/python/tests/test_sync_exists.py b/bindings/python/tests/test_sync_exists.py
new file mode 100644
index 00000000000..6e4b3913bd1
--- /dev/null
+++ b/bindings/python/tests/test_sync_exists.py
@@ -0,0 +1,32 @@
+# 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 uuid import uuid4
+
+import pytest
+
+
+@pytest.mark.need_capability(
+    "read", "write", "delete", "list", "blocking", "create_dir"
+)
+def test_sync_exists(service_name, operator, async_operator):
+    content = os.urandom(1024)
+    target = f"random_{str(uuid4())}"
+    operator.write(target, content)
+    assert operator.exists(target)
+    assert not operator.exists(target + "1")