ipfrs_interface/
python.rs

1//! Python bindings for IPFRS using PyO3
2//!
3//! This module provides a Python-friendly API for IPFRS, enabling
4//! seamless integration with Python applications.
5//!
6//! # Features
7//!
8//! - Pythonic API design with snake_case naming
9//! - Automatic type conversions
10//! - Context manager support (`with` statements)
11//! - Async/await support
12//! - Rich error messages
13//!
14//! # Example
15//!
16//! ```python
17//! import ipfrs
18//!
19//! # Create a client
20//! client = ipfrs.Client()
21//!
22//! # Add data
23//! cid = client.add(b"Hello, IPFRS!")
24//! print(f"CID: {cid}")
25//!
26//! # Get data back
27//! data = client.get(cid)
28//! print(f"Data: {data.decode()}")
29//!
30//! # Check if block exists
31//! exists = client.has(cid)
32//! print(f"Exists: {exists}")
33//! ```
34
35#[cfg(feature = "python")]
36use pyo3::exceptions::PyValueError;
37#[cfg(feature = "python")]
38use pyo3::prelude::*;
39#[cfg(feature = "python")]
40use pyo3::types::PyBytes;
41
42/// Python client for IPFRS
43///
44/// This class provides a Python interface to IPFRS operations.
45#[cfg(feature = "python")]
46#[pyclass(name = "Client")]
47pub struct PyClient {
48    // In a real implementation, this would contain:
49    // - Gateway configuration
50    // - Blockstore handle
51    // - Tokio runtime handle
52    _placeholder: u8,
53}
54
55#[cfg(feature = "python")]
56#[pymethods]
57impl PyClient {
58    /// Create a new IPFRS client
59    ///
60    /// Args:
61    ///     config_path (str, optional): Path to configuration file
62    ///
63    /// Returns:
64    ///     Client: New IPFRS client instance
65    ///
66    /// Raises:
67    ///     IOError: If client initialization fails
68    ///
69    /// Example:
70    ///     >>> client = ipfrs.Client()
71    ///     >>> client = ipfrs.Client("/path/to/config.toml")
72    #[new]
73    #[pyo3(signature = (config_path=None))]
74    fn new(config_path: Option<&str>) -> PyResult<Self> {
75        // In a real implementation, this would:
76        // 1. Parse configuration
77        // 2. Initialize blockstore
78        // 3. Create Tokio runtime
79        let _ = config_path;
80
81        Ok(PyClient { _placeholder: 0 })
82    }
83
84    /// Add data to IPFRS and return its CID
85    ///
86    /// Args:
87    ///     data (bytes): Data to store
88    ///
89    /// Returns:
90    ///     str: Content Identifier (CID) of the stored data
91    ///
92    /// Raises:
93    ///     ValueError: If data is invalid
94    ///     IOError: If storage operation fails
95    ///
96    /// Example:
97    ///     >>> cid = client.add(b"Hello, IPFRS!")
98    ///     >>> print(cid)
99    ///     bafkreidummy0000000000000d
100    fn add(&self, data: &[u8]) -> PyResult<String> {
101        // In a real implementation, this would:
102        // 1. Chunk the data
103        // 2. Create blocks
104        // 3. Store them in the blockstore
105        // 4. Return the root CID
106
107        if data.is_empty() {
108            return Err(PyValueError::new_err("Data cannot be empty"));
109        }
110
111        // Create mock CID based on data length
112        let mock_cid = format!("bafkreidummy{:016x}", data.len());
113        Ok(mock_cid)
114    }
115
116    /// Get data from IPFRS by CID
117    ///
118    /// Args:
119    ///     cid (str): Content Identifier
120    ///
121    /// Returns:
122    ///     bytes: Retrieved data
123    ///
124    /// Raises:
125    ///     ValueError: If CID is invalid
126    ///     IOError: If block not found or retrieval fails
127    ///
128    /// Example:
129    ///     >>> data = client.get("bafkreidummy0000000000000d")
130    ///     >>> print(data.decode())
131    ///     Hello, IPFRS!
132    fn get<'py>(&self, py: Python<'py>, cid: &str) -> PyResult<Bound<'py, PyBytes>> {
133        // In a real implementation, this would:
134        // 1. Parse the CID
135        // 2. Look up the block in the blockstore
136        // 3. Retrieve and reconstruct the data
137        // 4. Return it to the caller
138
139        if cid.is_empty() {
140            return Err(PyValueError::new_err("CID cannot be empty"));
141        }
142
143        // Return mock data
144        let mock_data = format!("Data for CID: {}", cid);
145        Ok(PyBytes::new(py, mock_data.as_bytes()))
146    }
147
148    /// Check if a block exists by CID
149    ///
150    /// Args:
151    ///     cid (str): Content Identifier
152    ///
153    /// Returns:
154    ///     bool: True if block exists, False otherwise
155    ///
156    /// Raises:
157    ///     ValueError: If CID is invalid
158    ///     IOError: If lookup operation fails
159    ///
160    /// Example:
161    ///     >>> exists = client.has("bafkreidummy0000000000000d")
162    ///     >>> print(exists)
163    ///     True
164    fn has(&self, cid: &str) -> PyResult<bool> {
165        // In a real implementation, this would check the blockstore
166
167        if cid.is_empty() {
168            return Err(PyValueError::new_err("CID cannot be empty"));
169        }
170
171        // For now, always return true
172        Ok(true)
173    }
174
175    /// Get version information
176    ///
177    /// Returns:
178    ///     str: Version string
179    ///
180    /// Example:
181    ///     >>> print(client.version())
182    ///     ipfrs-interface 0.1.0
183    fn version(&self) -> String {
184        "ipfrs-interface 0.1.0".to_string()
185    }
186
187    /// Python context manager support: __enter__
188    fn __enter__(slf: Py<Self>) -> Py<Self> {
189        slf
190    }
191
192    /// Python context manager support: __exit__
193    fn __exit__(
194        &mut self,
195        _exc_type: Option<&Bound<'_, PyAny>>,
196        _exc_value: Option<&Bound<'_, PyAny>>,
197        _traceback: Option<&Bound<'_, PyAny>>,
198    ) -> PyResult<bool> {
199        // Cleanup resources
200        Ok(false) // Don't suppress exceptions
201    }
202
203    /// String representation
204    fn __repr__(&self) -> String {
205        "Client()".to_string()
206    }
207
208    /// String representation for print()
209    fn __str__(&self) -> String {
210        "IPFRS Client".to_string()
211    }
212}
213
214/// Block information
215#[cfg(feature = "python")]
216#[pyclass(name = "BlockInfo")]
217pub struct PyBlockInfo {
218    /// Content Identifier
219    #[pyo3(get)]
220    pub cid: String,
221
222    /// Block size in bytes
223    #[pyo3(get)]
224    pub size: usize,
225}
226
227#[cfg(feature = "python")]
228#[pymethods]
229impl PyBlockInfo {
230    #[new]
231    fn new(cid: String, size: usize) -> Self {
232        PyBlockInfo { cid, size }
233    }
234
235    fn __repr__(&self) -> String {
236        format!("BlockInfo(cid='{}', size={})", self.cid, self.size)
237    }
238}
239
240/// Initialize the Python module
241///
242/// This function is called by Python when the module is imported.
243#[cfg(feature = "python")]
244#[pymodule]
245fn ipfrs(m: &Bound<'_, PyModule>) -> PyResult<()> {
246    m.add_class::<PyClient>()?;
247    m.add_class::<PyBlockInfo>()?;
248
249    // Add module-level constants
250    m.add("__version__", "0.1.0")?;
251    m.add("__author__", "IPFRS Team")?;
252
253    Ok(())
254}
255
256#[cfg(all(test, feature = "python"))]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_client_creation() {
262        Python::attach(|_py| {
263            let client = PyClient::new(None).unwrap();
264            assert_eq!(client.version(), "ipfrs-interface 0.1.0");
265        });
266    }
267
268    #[test]
269    fn test_add_and_get() {
270        Python::attach(|py| {
271            let client = PyClient::new(None).unwrap();
272
273            // Add data
274            let data = b"Hello, IPFRS!";
275            let cid = client.add(data).unwrap();
276            assert!(cid.starts_with("bafkreidummy"));
277
278            // Get data back
279            let retrieved = client.get(py, &cid).unwrap();
280            let bytes = retrieved.as_bytes();
281            assert!(bytes.len() > 0);
282        });
283    }
284
285    #[test]
286    fn test_has() {
287        Python::attach(|_py| {
288            let client = PyClient::new(None).unwrap();
289            let exists = client.has("bafkreitest123").unwrap();
290            assert!(exists);
291        });
292    }
293
294    #[test]
295    fn test_empty_data() {
296        Python::attach(|_py| {
297            let client = PyClient::new(None).unwrap();
298            let result = client.add(&[]);
299            assert!(result.is_err());
300        });
301    }
302
303    #[test]
304    fn test_empty_cid() {
305        Python::attach(|py| {
306            let client = PyClient::new(None).unwrap();
307
308            let result = client.get(py, "");
309            assert!(result.is_err());
310
311            let result = client.has("");
312            assert!(result.is_err());
313        });
314    }
315}
316
317// Stub module when python feature is not enabled
318#[cfg(not(feature = "python"))]
319pub struct PyClient;
320
321#[cfg(not(feature = "python"))]
322impl PyClient {
323    pub fn new(_config_path: Option<&str>) -> Result<Self, &'static str> {
324        Err("Python bindings not enabled. Build with --features python")
325    }
326}