#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::needless_pass_by_value)]
use crate::client::{BlockingWeedClient, WeedClientBuilder};
use crate::domain::{DomainError, FileId};
use crate::infrastructure::MasterSelectionStrategy;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use std::sync::Arc;
fn to_py_err(err: DomainError) -> PyErr {
PyRuntimeError::new_err(err.to_string())
}
#[pyclass(name = "FileId")]
#[derive(Clone)]
pub struct PyFileId {
inner: FileId,
}
#[pymethods]
impl PyFileId {
#[staticmethod]
fn parse(fid: &str) -> PyResult<Self> {
let inner = FileId::parse(fid).map_err(to_py_err)?;
Ok(Self { inner })
}
#[getter]
fn volume_id(&self) -> u32 {
self.inner.volume_id()
}
#[getter]
fn file_key(&self) -> u64 {
self.inner.file_key()
}
#[getter]
fn cookie(&self) -> u32 {
self.inner.cookie()
}
fn render(&self) -> String {
self.inner.render()
}
fn __str__(&self) -> String {
self.inner.render()
}
fn __repr__(&self) -> String {
format!(
"FileId(volume_id={}, file_key={}, cookie={})",
self.inner.volume_id(),
self.inner.file_key(),
self.inner.cookie()
)
}
}
#[pyclass(name = "WeedClient")]
pub struct PyWeedClient {
client: Arc<BlockingWeedClient>,
}
#[pymethods]
impl PyWeedClient {
#[new]
#[pyo3(signature = (master_urls, strategy = "round_robin", max_retries = 3))]
fn new(master_urls: Vec<String>, strategy: &str, max_retries: usize) -> PyResult<Self> {
let strategy = match strategy {
"round_robin" => MasterSelectionStrategy::RoundRobin,
"failover" => MasterSelectionStrategy::Failover,
"random" => MasterSelectionStrategy::Random,
_ => {
return Err(PyRuntimeError::new_err(format!(
"Invalid strategy: {strategy}. Must be 'round_robin', 'failover', or 'random'"
)))
}
};
let client = WeedClientBuilder::new()
.master_urls(master_urls)
.strategy(strategy)
.max_retries(max_retries)
.build_blocking()
.map_err(to_py_err)?;
Ok(Self {
client: Arc::new(client),
})
}
#[pyo3(signature = (data, filename = None))]
fn write(&self, data: &[u8], filename: Option<&str>) -> PyResult<PyFileId> {
let file_id = self
.client
.write(data.to_vec(), filename)
.map_err(to_py_err)?;
Ok(PyFileId { inner: file_id })
}
#[pyo3(signature = (data, filename = None))]
fn upload_bytes(&self, data: &[u8], filename: Option<&str>) -> PyResult<PyFileId> {
self.write(data, filename)
}
fn read<'py>(&self, py: Python<'py>, file_id: PyFileIdOrStr) -> PyResult<Bound<'py, PyBytes>> {
let fid = file_id.into_file_id()?;
let data = self.client.read(&fid).map_err(to_py_err)?;
Ok(PyBytes::new(py, &data))
}
fn delete(&self, file_id: PyFileIdOrStr) -> PyResult<()> {
let fid = file_id.into_file_id()?;
self.client.delete(&fid).map_err(to_py_err)
}
fn public_url(&self, file_id: PyFileIdOrStr) -> PyResult<String> {
let fid = file_id.into_file_id()?;
self.client.public_url(&fid).map_err(to_py_err)
}
fn public_url_resized(
&self,
file_id: PyFileIdOrStr,
width: u32,
height: u32,
) -> PyResult<String> {
let fid = file_id.into_file_id()?;
self.client
.public_url_resized(&fid, width, height)
.map_err(to_py_err)
}
#[staticmethod]
fn parse_file_id(fid: &str) -> PyResult<PyFileId> {
PyFileId::parse(fid)
}
}
#[derive(FromPyObject)]
enum PyFileIdOrStr {
FileId(PyFileId),
Str(String),
}
impl PyFileIdOrStr {
fn into_file_id(self) -> PyResult<FileId> {
match self {
Self::FileId(py_fid) => Ok(py_fid.inner),
Self::Str(s) => FileId::parse(&s).map_err(to_py_err),
}
}
}
#[pymodule]
pub fn weedforge(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyFileId>()?;
m.add_class::<PyWeedClient>()?;
m.add("__doc__", "Rust-first, Python-friendly SDK for `SeaweedFS`")?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}