briefcase-python 2.4.1

Python bindings for Briefcase AI
Documentation
//! Python bindings for storage backends

use crate::models::{PyDecisionSnapshot, PySnapshot, PySnapshotQuery};
use crate::runtime::PythonAsyncExt;
use briefcase_core::storage::{LakeFSBackend, SqliteBackend, StorageBackend};
use pyo3::prelude::*;
use pyo3::types::PyList;

/// Python wrapper for SqliteBackend
#[pyclass(name = "SqliteBackend")]
pub struct PySqliteBackend {
    pub inner: SqliteBackend,
}

#[pymethods]
impl PySqliteBackend {
    /// Create new SQLite backend with file path
    #[new]
    fn new(path: Option<String>) -> PyResult<Self> {
        let backend = if let Some(path) = path {
            SqliteBackend::new(path)
        } else {
            SqliteBackend::in_memory()
        };

        match backend {
            Ok(backend) => Ok(Self { inner: backend }),
            Err(e) => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
                "Failed to create SQLite backend: {}",
                e
            ))),
        }
    }

    /// Create in-memory database
    #[staticmethod]
    fn in_memory() -> PyResult<Self> {
        match SqliteBackend::in_memory() {
            Ok(backend) => Ok(Self { inner: backend }),
            Err(e) => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
                "Failed to create in-memory SQLite backend: {}",
                e
            ))),
        }
    }

    /// Save a snapshot
    fn save(&self, snapshot: PyRef<PySnapshot>) -> PyResult<String> {
        let backend = self.inner.clone();
        let snapshot_inner = snapshot.inner.clone();

        // Using global runtime instead of creating new one

        backend.save(&snapshot_inner).block_on_python()
    }

    /// Save a decision snapshot
    fn save_decision(&self, decision: PyRef<PyDecisionSnapshot>) -> PyResult<String> {
        let backend = self.inner.clone();
        let decision_inner = decision.inner.clone();

        // Using global runtime instead of creating new one

        backend.save_decision(&decision_inner).block_on_python()
    }

    /// Load a snapshot by ID
    fn load(&self, snapshot_id: String) -> PyResult<PySnapshot> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend
            .load(&snapshot_id)
            .block_on_python()
            .map(|snapshot| PySnapshot { inner: snapshot })
    }

    /// Load a decision by ID
    fn load_decision(&self, decision_id: String) -> PyResult<PyDecisionSnapshot> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend
            .load_decision(&decision_id)
            .block_on_python()
            .map(|decision| PyDecisionSnapshot { inner: decision })
    }

    /// Query snapshots
    fn query(&self, query: PyRef<PySnapshotQuery>) -> PyResult<PyObject> {
        let backend = self.inner.clone();
        let query_inner = query.inner.clone();

        // Using global runtime instead of creating new one

        let snapshots = backend.query(query_inner).block_on_python()?;

        Python::with_gil(|py| {
            let list = PyList::empty(py);
            for snapshot in snapshots {
                let py_snapshot = PySnapshot { inner: snapshot };
                list.append(Py::new(py, py_snapshot)?)?;
            }
            Ok(list.into())
        })
    }

    /// Delete a snapshot
    fn delete(&self, snapshot_id: String) -> PyResult<bool> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend.delete(&snapshot_id).block_on_python()
    }

    /// Perform health check
    fn health_check(&self) -> PyResult<bool> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend.health_check().block_on_python()
    }

    /// String representation
    fn __repr__(&self) -> String {
        "SqliteBackend()".to_string()
    }
}

/// Python wrapper for LakeFSBackend
#[pyclass(name = "LakeFSBackend")]
pub struct PyLakeFSBackend {
    pub inner: LakeFSBackend,
}

#[pymethods]
impl PyLakeFSBackend {
    /// Create new LakeFS backend
    #[new]
    fn new(
        endpoint: String,
        repository: String,
        branch: String,
        access_key: String,
        secret_key: String,
    ) -> PyResult<Self> {
        let config = briefcase_core::storage::LakeFSConfig::new(
            endpoint, repository, branch, access_key, secret_key,
        );
        match LakeFSBackend::new(config) {
            Ok(backend) => Ok(Self { inner: backend }),
            Err(e) => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
                "Failed to create LakeFS backend: {}",
                e
            ))),
        }
    }

    /// Save a snapshot
    fn save(&self, snapshot: PyRef<PySnapshot>) -> PyResult<String> {
        let backend = self.inner.clone();
        let snapshot_inner = snapshot.inner.clone();

        // Using global runtime instead of creating new one

        backend.save(&snapshot_inner).block_on_python()
    }

    /// Save a decision snapshot
    fn save_decision(&self, decision: PyRef<PyDecisionSnapshot>) -> PyResult<String> {
        let backend = self.inner.clone();
        let decision_inner = decision.inner.clone();

        // Using global runtime instead of creating new one

        backend.save_decision(&decision_inner).block_on_python()
    }

    /// Load a snapshot by ID
    fn load(&self, snapshot_id: String) -> PyResult<PySnapshot> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend
            .load(&snapshot_id)
            .block_on_python()
            .map(|snapshot| PySnapshot { inner: snapshot })
    }

    /// Load a decision by ID
    fn load_decision(&self, decision_id: String) -> PyResult<PyDecisionSnapshot> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend
            .load_decision(&decision_id)
            .block_on_python()
            .map(|decision| PyDecisionSnapshot { inner: decision })
    }

    /// Query snapshots
    fn query(&self, query: PyRef<PySnapshotQuery>) -> PyResult<PyObject> {
        let backend = self.inner.clone();
        let query_inner = query.inner.clone();

        // Using global runtime instead of creating new one

        let snapshots = backend.query(query_inner).block_on_python()?;

        Python::with_gil(|py| {
            let list = PyList::empty(py);
            for snapshot in snapshots {
                let py_snapshot = PySnapshot { inner: snapshot };
                list.append(Py::new(py, py_snapshot)?)?;
            }
            Ok(list.into())
        })
    }

    /// Delete a snapshot
    fn delete(&self, snapshot_id: String) -> PyResult<bool> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend.delete(&snapshot_id).block_on_python()
    }

    /// Perform health check
    fn health_check(&self) -> PyResult<bool> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        backend.health_check().block_on_python()
    }

    /// Flush pending writes
    fn flush(&self) -> PyResult<String> {
        let backend = self.inner.clone();

        // Using global runtime instead of creating new one

        let result = backend.flush().block_on_python()?;
        Ok(format!(
            "Flushed {} snapshots, {} bytes",
            result.snapshots_written, result.bytes_written
        ))
    }

    /// String representation
    fn __repr__(&self) -> String {
        "LakeFSBackend()".to_string()
    }
}

/// Unified storage backend enum for Python bindings
#[derive(Clone)]
pub enum PyStorageBackend {
    Sqlite(SqliteBackend),
    LakeFS(LakeFSBackend),
}

#[async_trait::async_trait]
impl StorageBackend for PyStorageBackend {
    async fn save(
        &self,
        snapshot: &briefcase_core::models::Snapshot,
    ) -> Result<String, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.save(snapshot).await,
            PyStorageBackend::LakeFS(backend) => backend.save(snapshot).await,
        }
    }

    async fn save_decision(
        &self,
        decision: &briefcase_core::models::DecisionSnapshot,
    ) -> Result<String, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.save_decision(decision).await,
            PyStorageBackend::LakeFS(backend) => backend.save_decision(decision).await,
        }
    }

    async fn load(
        &self,
        snapshot_id: &str,
    ) -> Result<briefcase_core::models::Snapshot, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.load(snapshot_id).await,
            PyStorageBackend::LakeFS(backend) => backend.load(snapshot_id).await,
        }
    }

    async fn load_decision(
        &self,
        snapshot_id: &str,
    ) -> Result<briefcase_core::models::DecisionSnapshot, briefcase_core::storage::StorageError>
    {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.load_decision(snapshot_id).await,
            PyStorageBackend::LakeFS(backend) => backend.load_decision(snapshot_id).await,
        }
    }

    async fn query(
        &self,
        query: briefcase_core::storage::SnapshotQuery,
    ) -> Result<Vec<briefcase_core::models::Snapshot>, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.query(query).await,
            PyStorageBackend::LakeFS(backend) => backend.query(query).await,
        }
    }

    async fn delete(
        &self,
        snapshot_id: &str,
    ) -> Result<bool, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.delete(snapshot_id).await,
            PyStorageBackend::LakeFS(backend) => backend.delete(snapshot_id).await,
        }
    }

    async fn health_check(&self) -> Result<bool, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.health_check().await,
            PyStorageBackend::LakeFS(backend) => backend.health_check().await,
        }
    }

    async fn flush(
        &self,
    ) -> Result<briefcase_core::storage::FlushResult, briefcase_core::storage::StorageError> {
        match self {
            PyStorageBackend::Sqlite(backend) => backend.flush().await,
            PyStorageBackend::LakeFS(backend) => backend.flush().await,
        }
    }
}