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}