This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 1dbb7591a feat(bindings/python): Add stubs for some more types (#6703)
1dbb7591a is described below
commit 1dbb7591a5232002a5321f9d25c05be41dc1f421
Author: Chitral Verma <[email protected]>
AuthorDate: Sat Oct 18 17:06:41 2025 +0530
feat(bindings/python): Add stubs for some more types (#6703)
* Stubs for Layers
* Stubs for Metadata, Entry, and EntryMode
* Add to API Reference
---
.../python/docs/api/{metadata.md => capability.md} | 4 +-
bindings/python/docs/api/exceptions.md | 45 +++++++
bindings/python/docs/api/layers.md | 32 +++++
bindings/python/docs/api/types.md | 25 ++++
bindings/python/mkdocs.yml | 13 +-
bindings/python/pyrightconfig.json | 6 +
bindings/python/python/opendal/__init__.pyi | 57 +--------
bindings/python/python/opendal/layers.pyi | 131 ++++++++++++++++---
bindings/python/python/opendal/types.pyi | 140 +++++++++++++++++++++
bindings/python/src/layers.rs | 105 ++++++++++++++--
bindings/python/src/lib.rs | 25 ++--
bindings/python/src/metadata.rs | 120 +++++++++++++-----
12 files changed, 568 insertions(+), 135 deletions(-)
diff --git a/bindings/python/docs/api/metadata.md
b/bindings/python/docs/api/capability.md
similarity index 59%
rename from bindings/python/docs/api/metadata.md
rename to bindings/python/docs/api/capability.md
index f0df34777..14bac70df 100644
--- a/bindings/python/docs/api/metadata.md
+++ b/bindings/python/docs/api/capability.md
@@ -1,6 +1,6 @@
-::: opendal.Metadata
+::: opendal.Capability
options:
- heading: "opendal.Metadata"
+ heading: "opendal.Capability"
heading_level: 2
show_source: false
show_bases: false
diff --git a/bindings/python/docs/api/exceptions.md
b/bindings/python/docs/api/exceptions.md
new file mode 100644
index 000000000..873246ce7
--- /dev/null
+++ b/bindings/python/docs/api/exceptions.md
@@ -0,0 +1,45 @@
+This page documents all exceptions raised by the OpenDAL.
+
+::: opendal.exceptions.Error
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.AlreadyExists
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.ConditionNotMatch
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.ConfigInvalid
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.IsADirectory
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.IsSameFile
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.NotADirectory
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.NotFound
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.PermissionDenied
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.Unexpected
+ options:
+ heading_level: 2
+
+::: opendal.exceptions.Unsupported
+ options:
+ heading_level: 2
diff --git a/bindings/python/docs/api/layers.md
b/bindings/python/docs/api/layers.md
new file mode 100644
index 000000000..9d89e75ff
--- /dev/null
+++ b/bindings/python/docs/api/layers.md
@@ -0,0 +1,32 @@
+# Layers
+
+This page documents all layers in OpenDAL.
+
+## Layer
+::: opendal.layers.Layer
+ options:
+ heading: "opendal.layers.Layer"
+ heading_level: 2
+ show_source: false
+ show_bases: false
+
+## RetryLayer
+::: opendal.layers.RetryLayer
+ options:
+ heading: "opendal.layers.RetryLayer"
+ heading_level: 2
+ show_source: false
+
+## ConcurrentLimitLayer
+::: opendal.layers.ConcurrentLimitLayer
+ options:
+ heading: "opendal.layers.ConcurrentLimitLayer"
+ heading_level: 2
+ show_source: false
+
+## MimeGuessLayer
+::: opendal.layers.MimeGuessLayer
+ options:
+ heading: "opendal.layers.MimeGuessLayer"
+ heading_level: 2
+ show_source: false
diff --git a/bindings/python/docs/api/types.md
b/bindings/python/docs/api/types.md
new file mode 100644
index 000000000..396d9bee4
--- /dev/null
+++ b/bindings/python/docs/api/types.md
@@ -0,0 +1,25 @@
+# Types
+
+This page documents all types in OpenDAL.
+
+## Entry
+::: opendal.Entry
+ options:
+ heading: "opendal.Entry"
+ heading_level: 2
+ show_source: false
+ show_bases: false
+
+## EntryMode
+::: opendal.types.EntryMode
+ options:
+ heading: "opendal.EntryMode"
+ heading_level: 2
+
+## Metadata
+::: opendal.types.Metadata
+ options:
+ heading: "opendal.Metadata"
+ heading_level: 2
+ show_source: false
+ show_bases: false
diff --git a/bindings/python/mkdocs.yml b/bindings/python/mkdocs.yml
index 13db72908..6adeb6474 100644
--- a/bindings/python/mkdocs.yml
+++ b/bindings/python/mkdocs.yml
@@ -43,11 +43,14 @@ nav:
- Pandas: examples/pandas.ipynb
- Polars: examples/polars.ipynb
- API Reference:
- - api/operator.md
- - api/file.md
- - api/async_operator.md
- - api/async_file.md
- - api/metadata.md
+ - AsyncFile: api/async_file.md
+ - AsyncOperator: api/async_operator.md
+ - Capability: api/capability.md
+ - Exceptions: api/exceptions.md
+ - File: api/file.md
+ - Layers: api/layers.md
+ - Operator: api/operator.md
+ - Types: api/types.md
markdown_extensions:
- pymdownx.highlight:
diff --git a/bindings/python/pyrightconfig.json
b/bindings/python/pyrightconfig.json
new file mode 100644
index 000000000..a76866465
--- /dev/null
+++ b/bindings/python/pyrightconfig.json
@@ -0,0 +1,6 @@
+{
+ "include": ["python/opendal/"],
+ "ignore": [
+ "python/opendal/*.pyi"
+ ]
+}
diff --git a/bindings/python/python/opendal/__init__.pyi
b/bindings/python/python/opendal/__init__.pyi
index 825b1f6d6..91ec09466 100644
--- a/bindings/python/python/opendal/__init__.pyi
+++ b/bindings/python/python/opendal/__init__.pyi
@@ -17,7 +17,6 @@
import os
from collections.abc import AsyncIterable, Iterable
-from datetime import datetime
from types import TracebackType
from typing import TypeAlias, final
@@ -30,6 +29,7 @@ from opendal import layers as layers
from opendal.__base import _Base
from opendal.capability import Capability
from opendal.layers import Layer
+from opendal.types import Entry, Metadata
PathBuf: TypeAlias = str | os.PathLike
@@ -703,61 +703,6 @@ class AsyncFile:
async def writable(self) -> bool:
"""Check if the file is writable."""
-@final
-class Entry:
- """An entry in the directory listing."""
-
- @property
- def path(self) -> str:
- """The path of the entry."""
- @property
- def metadata(self) -> Metadata:
- """The metadata of the entry."""
-
-@final
-class Metadata:
- @property
- def content_disposition(self) -> str | None:
- """The content disposition of the object."""
- @property
- def content_length(self) -> int:
- """The content length of the object."""
- @property
- def content_md5(self) -> str | None:
- """The MD5 checksum of the object."""
- @property
- def content_type(self) -> str | None:
- """The mime type of the object."""
- @property
- def content_encoding(self) -> str | None:
- """The content encoding of the object."""
- @property
- def etag(self) -> str | None:
- """The ETag of the object."""
- @property
- def mode(self) -> EntryMode:
- """The mode of the object."""
- @property
- def is_file(self) -> bool:
- """Returns `True` if this metadata is for a file."""
- @property
- def is_dir(self) -> bool:
- """Returns `True` if this metadata is for a directory."""
- @property
- def last_modified(self) -> datetime | None:
- """The last modified time of the object."""
- @property
- def version(self) -> str | None:
- """The version of the object, if available."""
- @property
- def user_metadata(self) -> str | None:
- """The user defined metadata of the object."""
-
-@final
-class EntryMode:
- def is_file(self) -> bool: ...
- def is_dir(self) -> bool: ...
-
@final
class PresignedRequest:
@property
diff --git a/bindings/python/python/opendal/layers.pyi
b/bindings/python/python/opendal/layers.pyi
index c305e9ab0..8dbd071c9 100644
--- a/bindings/python/python/opendal/layers.pyi
+++ b/bindings/python/python/opendal/layers.pyi
@@ -15,25 +15,122 @@
# specific language governing permissions and limitations
# under the License.
-from typing import final
+# This file is automatically generated by pyo3_stub_gen
+# ruff: noqa: E501, F401
-class Layer: ...
+import builtins
+import typing
-@final
-class RetryLayer(Layer):
- def __init__(
- self,
- max_times: int | None = None,
- factor: float | None = None,
- jitter: bool = False,
- max_delay: float | None = None,
- min_delay: float | None = None,
- ) -> None: ...
-
-@final
[email protected]
class ConcurrentLimitLayer(Layer):
- def __init__(self, limit: int) -> None: ...
+ r"""
+ ConcurrentLimitLayer.
+
+ Create a layer that limits the number of concurrent operations.
+
+ Notes
+ -----
+ All operators wrapped by this layer will share a common semaphore. This
+ allows you to reuse the same layer across multiple operators, ensuring
+ that the total number of concurrent requests across the entire
+ application does not exceed the limit.
+ """
+
+ def __new__(cls, limit: builtins.int) -> ConcurrentLimitLayer:
+ r"""
+ Create a new ConcurrentLimitLayer.
+
+ Parameters
+ ----------
+ limit : int
+ Maximum number of concurrent operations allowed.
+
+ Returns
+ -------
+ ConcurrentLimitLayer
+ """
-@final
+class Layer:
+ r"""
+ Layer.
+
+ Layers are used to intercept the operations on the underlying storage.
+ """
+
[email protected]
class MimeGuessLayer(Layer):
- def __init__(self) -> None: ...
+ r"""
+ MimeGuessLayer.
+
+ Create a layer that guesses MIME types for objects based on their
+ paths or content.
+
+ This layer uses the `mime_guess` crate
+ (see https://crates.io/crates/mime_guess) to infer the
+ ``Content-Type``.
+
+ Notes
+ -----
+ This layer will not override a ``Content-Type`` that has already
+ been set, either manually or by the backend service. It is only
+ applied if no content type is present.
+
+ A ``Content-Type`` is not guaranteed. If the file extension is
+ uncommon or unknown, the content type will remain unset.
+ """
+
+ def __new__(cls) -> MimeGuessLayer:
+ r"""
+ Create a new MimeGuessLayer.
+
+ Returns
+ -------
+ MimeGuessLayer
+ """
+
[email protected]
+class RetryLayer(Layer):
+ r"""
+ RetryLayer.
+
+ A layer that retries operations that fail with temporary errors.
+
+ Operations are retried if they fail with an error for which
+ `Error.is_temporary` returns `True`. If all retries are exhausted,
+ the error is marked as persistent and then returned.
+
+ Notes
+ -----
+ After an operation on a `Reader` or `Writer` has failed through
+ all retries, the object is in an undefined state. Reusing it
+ can lead to exceptions.
+ """
+
+ def __new__(
+ cls,
+ max_times: builtins.int | None = None,
+ factor: builtins.float | None = None,
+ jitter: builtins.bool = False,
+ max_delay: builtins.float | None = None,
+ min_delay: builtins.float | None = None,
+ ) -> RetryLayer:
+ r"""
+ Create a new RetryLayer.
+
+ Parameters
+ ----------
+ max_times : Optional[int]
+ Maximum number of retry attempts. Defaults to ``3``.
+ factor : Optional[float]
+ Backoff factor applied between retries. Defaults to ``2.0``.
+ jitter : bool
+ Whether to apply jitter to the backoff. Defaults to ``False``.
+ max_delay : Optional[float]
+ Maximum delay (in seconds) between retries. Defaults to ``60.0``.
+ min_delay : Optional[float]
+ Minimum delay (in seconds) between retries. Defaults to ``1.0``.
+
+ Returns
+ -------
+ RetryLayer
+ """
diff --git a/bindings/python/python/opendal/types.pyi
b/bindings/python/python/opendal/types.pyi
new file mode 100644
index 000000000..48377f206
--- /dev/null
+++ b/bindings/python/python/opendal/types.pyi
@@ -0,0 +1,140 @@
+# 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.
+
+# This file is automatically generated by pyo3_stub_gen
+# ruff: noqa: E501, F401
+
+import builtins
+import datetime
+import enum
+import typing
+
[email protected]
+class Entry:
+ r"""
+ Entry.
+
+ An entry representing a path and its associated metadata.
+
+ Notes
+ -----
+ If this entry is a directory, ``path`` **must** end with ``/``.
+ Otherwise, ``path`` **must not** end with ``/``.
+ """
+
+ @property
+ def path(self) -> builtins.str:
+ r"""The path of entry relative to the operator's root."""
+ @property
+ def name(self) -> builtins.str:
+ r"""The name of entry, representing the last segment of the path."""
+ @property
+ def metadata(self) -> Metadata:
+ r"""The metadata of this entry."""
+
[email protected]
+class Metadata:
+ r"""
+ The metadata of an ``Entry``.
+
+ The metadata is always tied to a specific context and is not a global
+ state. For example, two versions of the same path might have different
+ content lengths.
+
+ Notes
+ -----
+ In systems that support versioning, such as AWS S3, the metadata may
+ represent a specific version of a file. Use :attr:`version` to get
+ the version of a file if it is available.
+ """
+
+ @property
+ def content_disposition(self) -> builtins.str | None:
+ r"""The content disposition of this entry."""
+ @property
+ def content_length(self) -> builtins.int:
+ r"""The content length of this entry."""
+ @property
+ def content_md5(self) -> builtins.str | None:
+ r"""The content MD5 of this entry."""
+ @property
+ def content_type(self) -> builtins.str | None:
+ r"""The content type of this entry."""
+ @property
+ def content_encoding(self) -> builtins.str | None:
+ r"""The content encoding of this entry."""
+ @property
+ def etag(self) -> builtins.str | None:
+ r"""The ETag of this entry."""
+ @property
+ def mode(self) -> EntryMode:
+ r"""The mode of this entry."""
+ @property
+ def is_file(self) -> builtins.bool:
+ r"""Whether this entry is a file."""
+ @property
+ def is_dir(self) -> builtins.bool:
+ r"""Whether this entry is a directory."""
+ @property
+ def last_modified(self) -> datetime.datetime:
+ r"""The last modified timestamp of this entry."""
+ @property
+ def version(self) -> builtins.str | None:
+ r"""The version of this entry."""
+ @property
+ def user_metadata(self) -> builtins.dict[builtins.str, builtins.str] |
None:
+ r"""The user-defined metadata of this entry."""
+
[email protected]
+class EntryMode(enum.Enum):
+ r"""
+ EntryMode.
+
+ The mode of an entry, indicating if it is a file or a directory.
+ """
+
+ File = ...
+ r"""
+ The entry is a file and has data to read.
+ """
+ Dir = ...
+ r"""
+ The entry is a directory and can be listed.
+ """
+ Unknown = ...
+ r"""
+ The mode of the entry is unknown.
+ """
+
+ def is_file(self) -> builtins.bool:
+ r"""
+ Check if the entry mode is `File`.
+
+ Returns
+ -------
+ bool
+ True if the entry is a file.
+ """
+ def is_dir(self) -> builtins.bool:
+ r"""
+ Check if the entry mode is `Dir`.
+
+ Returns
+ -------
+ bool
+ True if the entry is a directory.
+ """
diff --git a/bindings/python/src/layers.rs b/bindings/python/src/layers.rs
index 59b0c3540..3948f30d6 100644
--- a/bindings/python/src/layers.rs
+++ b/bindings/python/src/layers.rs
@@ -17,18 +17,34 @@
use std::time::Duration;
-use opendal::Operator;
-use pyo3::prelude::*;
-
use crate::*;
+use opendal::Operator;
pub trait PythonLayer: Send + Sync {
fn layer(&self, op: Operator) -> Operator;
}
+/// Layer
+///
+/// Layers are used to intercept the operations on the underlying storage.
+#[gen_stub_pyclass]
#[pyclass(module = "opendal.layers", subclass)]
pub struct Layer(pub Box<dyn PythonLayer>);
+/// RetryLayer
+///
+/// A layer that retries operations that fail with temporary errors.
+///
+/// Operations are retried if they fail with an error for which
+/// `Error.is_temporary` returns `True`. If all retries are exhausted,
+/// the error is marked as persistent and then returned.
+///
+/// Notes
+/// -----
+/// After an operation on a `Reader` or `Writer` has failed through
+/// all retries, the object is in an undefined state. Reusing it
+/// can lead to exceptions.
+#[gen_stub_pyclass]
#[pyclass(module = "opendal.layers", extends=Layer)]
#[derive(Clone)]
pub struct RetryLayer(ocore::layers::RetryLayer);
@@ -39,8 +55,28 @@ impl PythonLayer for RetryLayer {
}
}
+#[gen_stub_pymethods]
#[pymethods]
impl RetryLayer {
+ /// Create a new RetryLayer.
+ ///
+ /// Parameters
+ /// ----------
+ /// max_times : Optional[int]
+ /// Maximum number of retry attempts. Defaults to ``3``.
+ /// factor : Optional[float]
+ /// Backoff factor applied between retries. Defaults to ``2.0``.
+ /// jitter : bool
+ /// Whether to apply jitter to the backoff. Defaults to ``False``.
+ /// max_delay : Optional[float]
+ /// Maximum delay (in seconds) between retries. Defaults to ``60.0``.
+ /// min_delay : Optional[float]
+ /// Minimum delay (in seconds) between retries. Defaults to ``1.0``.
+ ///
+ /// Returns
+ /// -------
+ /// RetryLayer
+ #[gen_stub(override_return_type(type_repr = "RetryLayer"))]
#[new]
#[pyo3(signature = (
max_times = None,
@@ -67,10 +103,10 @@ impl RetryLayer {
retry = retry.with_jitter();
}
if let Some(max_delay) = max_delay {
- retry = retry.with_max_delay(Duration::from_micros((max_delay *
1000000.0) as u64));
+ retry = retry.with_max_delay(Duration::from_micros((max_delay *
1_000_000.0) as u64));
}
if let Some(min_delay) = min_delay {
- retry = retry.with_min_delay(Duration::from_micros((min_delay *
1000000.0) as u64));
+ retry = retry.with_min_delay(Duration::from_micros((min_delay *
1_000_000.0) as u64));
}
let retry_layer = Self(retry);
@@ -81,7 +117,18 @@ impl RetryLayer {
}
}
-#[pyclass(module = "opendal.layers", extends=Layer)]
+/// ConcurrentLimitLayer
+///
+/// Create a layer that limits the number of concurrent operations.
+///
+/// Notes
+/// -----
+/// All operators wrapped by this layer will share a common semaphore. This
+/// allows you to reuse the same layer across multiple operators, ensuring
+/// that the total number of concurrent requests across the entire
+/// application does not exceed the limit.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.layers", extends = Layer)]
#[derive(Clone)]
pub struct ConcurrentLimitLayer(ocore::layers::ConcurrentLimitLayer);
@@ -91,8 +138,20 @@ impl PythonLayer for ConcurrentLimitLayer {
}
}
+#[gen_stub_pymethods]
#[pymethods]
impl ConcurrentLimitLayer {
+ /// Create a new ConcurrentLimitLayer.
+ ///
+ /// Parameters
+ /// ----------
+ /// limit : int
+ /// Maximum number of concurrent operations allowed.
+ ///
+ /// Returns
+ /// -------
+ /// ConcurrentLimitLayer
+ #[gen_stub(override_return_type(type_repr = "ConcurrentLimitLayer"))]
#[new]
#[pyo3(signature = (limit))]
fn new(limit: usize) -> PyResult<PyClassInitializer<Self>> {
@@ -104,7 +163,25 @@ impl ConcurrentLimitLayer {
}
}
-#[pyclass(module = "opendal.layers", extends=Layer)]
+/// MimeGuessLayer
+///
+/// Create a layer that guesses MIME types for objects based on their
+/// paths or content.
+///
+/// This layer uses the `mime_guess` crate
+/// (see https://crates.io/crates/mime_guess) to infer the
+/// ``Content-Type``.
+///
+/// Notes
+/// -----
+/// This layer will not override a ``Content-Type`` that has already
+/// been set, either manually or by the backend service. It is only
+/// applied if no content type is present.
+///
+/// A ``Content-Type`` is not guaranteed. If the file extension is
+/// uncommon or unknown, the content type will remain unset.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.layers", extends = Layer)]
#[derive(Clone)]
pub struct MimeGuessLayer(ocore::layers::MimeGuessLayer);
@@ -114,14 +191,20 @@ impl PythonLayer for MimeGuessLayer {
}
}
+#[gen_stub_pymethods]
#[pymethods]
impl MimeGuessLayer {
+ /// Create a new MimeGuessLayer.
+ ///
+ /// Returns
+ /// -------
+ /// MimeGuessLayer
+ #[gen_stub(override_return_type(type_repr = "MimeGuessLayer"))]
#[new]
- #[pyo3(signature = ())]
fn new() -> PyResult<PyClassInitializer<Self>> {
- let mime_guess_layer = Self(ocore::layers::MimeGuessLayer::default());
- let class =
PyClassInitializer::from(Layer(Box::new(mime_guess_layer.clone())))
- .add_subclass(mime_guess_layer);
+ let mime_guess = Self(ocore::layers::MimeGuessLayer::default());
+ let class =
+
PyClassInitializer::from(Layer(Box::new(mime_guess.clone()))).add_subclass(mime_guess);
Ok(class)
}
}
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index ad0b1ae3b..f86b5fa22 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -51,9 +51,17 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) ->
PyResult<()> {
// Capability module
add_pymodule!(py, m, "capability", [Capability])?;
- m.add_class::<Entry>()?;
- m.add_class::<EntryMode>()?;
- m.add_class::<Metadata>()?;
+ // Layers module
+ add_pymodule!(
+ py,
+ m,
+ "layers",
+ [Layer, RetryLayer, ConcurrentLimitLayer, MimeGuessLayer]
+ )?;
+
+ // Types module
+ add_pymodule!(py, m, "types", [Entry, EntryMode, Metadata])?;
+
m.add_class::<PresignedRequest>()?;
m.add_class::<WriteOptions>()?;
@@ -61,17 +69,6 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) ->
PyResult<()> {
m.add_class::<ListOptions>()?;
m.add_class::<StatOptions>()?;
- // Layer module
- let layers_module = PyModule::new(py, "layers")?;
- layers_module.add_class::<Layer>()?;
- layers_module.add_class::<RetryLayer>()?;
- layers_module.add_class::<ConcurrentLimitLayer>()?;
- layers_module.add_class::<MimeGuessLayer>()?;
- m.add_submodule(&layers_module)?;
- py.import("sys")?
- .getattr("modules")?
- .set_item("opendal.layers", layers_module)?;
-
// Exceptions module
add_pyexceptions!(
py,
diff --git a/bindings/python/src/metadata.rs b/bindings/python/src/metadata.rs
index 3c9d33a17..7c249268f 100644
--- a/bindings/python/src/metadata.rs
+++ b/bindings/python/src/metadata.rs
@@ -18,7 +18,16 @@
use crate::*;
use std::collections::HashMap;
-#[pyclass(module = "opendal")]
+/// Entry
+///
+/// An entry representing a path and its associated metadata.
+///
+/// Notes
+/// -----
+/// If this entry is a directory, ``path`` **must** end with ``/``.
+/// Otherwise, ``path`` **must not** end with ``/``.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.types")]
pub struct Entry(ocore::Entry);
impl Entry {
@@ -27,24 +36,33 @@ impl Entry {
}
}
+#[gen_stub_pymethods]
#[pymethods]
impl Entry {
- /// Path of entry. Path is relative to the operator's root.
+ /// The path of entry relative to the operator's root.
#[getter]
pub fn path(&self) -> &str {
self.0.path()
}
- /// Metadata of entry.
+ /// The name of entry, representing the last segment of the path.
+ #[getter]
+ pub fn name(&self) -> &str {
+ self.0.name()
+ }
+
+ /// The metadata of this entry.
#[getter]
pub fn metadata(&self) -> Metadata {
Metadata::new(self.0.metadata().clone())
}
+ #[gen_stub(skip)]
fn __str__(&self) -> &str {
self.0.path()
}
+ #[gen_stub(skip)]
fn __repr__(&self) -> String {
format!(
"Entry(path={:?}, metadata={})",
@@ -54,7 +72,19 @@ impl Entry {
}
}
-#[pyclass(module = "opendal")]
+/// The metadata of an ``Entry``.
+///
+/// The metadata is always tied to a specific context and is not a global
+/// state. For example, two versions of the same path might have different
+/// content lengths.
+///
+/// Notes
+/// -----
+/// In systems that support versioning, such as AWS S3, the metadata may
+/// represent a specific version of a file. Use :attr:`version` to get
+/// the version of a file if it is available.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.types")]
pub struct Metadata(ocore::Metadata);
impl Metadata {
@@ -63,79 +93,83 @@ impl Metadata {
}
}
+#[gen_stub_pymethods]
#[pymethods]
impl Metadata {
+ /// The content disposition of this entry.
#[getter]
pub fn content_disposition(&self) -> Option<&str> {
self.0.content_disposition()
}
- /// Content length of this entry.
+ /// The content length of this entry.
#[getter]
pub fn content_length(&self) -> u64 {
self.0.content_length()
}
- /// Content MD5 of this entry.
+ /// The content MD5 of this entry.
#[getter]
pub fn content_md5(&self) -> Option<&str> {
self.0.content_md5()
}
- /// Content Type of this entry.
+ /// The content type of this entry.
#[getter]
pub fn content_type(&self) -> Option<&str> {
self.0.content_type()
}
- /// Content Type of this entry.
+ /// The content encoding of this entry.
#[getter]
pub fn content_encoding(&self) -> Option<&str> {
self.0.content_encoding()
}
- /// ETag of this entry.
+ /// The ETag of this entry.
#[getter]
pub fn etag(&self) -> Option<&str> {
self.0.etag()
}
- /// mode represents this entry's mode.
+ /// The mode of this entry.
#[getter]
pub fn mode(&self) -> EntryMode {
- EntryMode(self.0.mode())
+ EntryMode::new(self.0.mode())
}
- /// Returns `true` if this metadata is for a file.
+ /// Whether this entry is a file.
#[getter]
pub fn is_file(&self) -> bool {
self.mode().is_file()
}
- /// Returns `true` if this metadata is for a directory.
+ /// Whether this entry is a directory.
#[getter]
pub fn is_dir(&self) -> bool {
self.mode().is_dir()
}
- /// Last modified time
+ /// The last modified timestamp of this entry.
+ #[gen_stub(override_return_type(type_repr = "datetime.datetime",
imports=("datetime")))]
#[getter]
pub fn last_modified(&self) -> Option<jiff::Timestamp> {
self.0.last_modified().map(Into::into)
}
- /// Version of this entry, if available.
+ /// The version of this entry.
#[getter]
pub fn version(&self) -> Option<&str> {
self.0.version()
}
- /// User defined metadata of this entry
+ /// The user-defined metadata of this entry.
#[getter]
pub fn user_metadata(&self) -> Option<&HashMap<String, String>> {
self.0.user_metadata()
}
+ #[gen_stub(skip)]
pub fn __repr__(&self) -> String {
let mut parts = vec![];
@@ -157,26 +191,52 @@ impl Metadata {
}
}
-#[pyclass(module = "opendal")]
-pub struct EntryMode(ocore::EntryMode);
+/// EntryMode
+///
+/// The mode of an entry, indicating if it is a file or a directory.
+#[gen_stub_pyclass_enum]
+#[pyclass(eq, eq_int, hash, frozen, module = "opendal.types")]
+#[pyo3(rename_all = "PascalCase")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum EntryMode {
+ /// The entry is a file and has data to read.
+ FILE,
+ /// The entry is a directory and can be listed.
+ DIR,
+ /// The mode of the entry is unknown.
+ Unknown,
+}
+
+impl EntryMode {
+ pub fn new(mode: ocore::EntryMode) -> Self {
+ match mode {
+ ocore::EntryMode::FILE => Self::FILE,
+ ocore::EntryMode::DIR => Self::DIR,
+ ocore::EntryMode::Unknown => Self::Unknown,
+ }
+ }
+}
+#[gen_stub_pymethods]
#[pymethods]
impl EntryMode {
- /// Returns `True` if this is a file.
+ /// Check if the entry mode is `File`.
+ ///
+ /// Returns
+ /// -------
+ /// bool
+ /// True if the entry is a file.
pub fn is_file(&self) -> bool {
- self.0.is_file()
+ matches!(self, EntryMode::FILE)
}
- /// Returns `True` if this is a directory.
+ /// Check if the entry mode is `Dir`.
+ ///
+ /// Returns
+ /// -------
+ /// bool
+ /// True if the entry is a directory.
pub fn is_dir(&self) -> bool {
- self.0.is_dir()
- }
-
- pub fn __repr__(&self) -> &'static str {
- match self.0 {
- ocore::EntryMode::FILE => "EntryMode.FILE",
- ocore::EntryMode::DIR => "EntryMode.DIR",
- ocore::EntryMode::Unknown => "EntryMode.UNKNOWN",
- }
+ matches!(self, EntryMode::DIR)
}
}