diff --git a/src/api/cursor.rs b/src/api/cursor.rs index a809830..86af5a1 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -6,9 +6,13 @@ use codemp_proto as proto; use uuid::Uuid; +#[cfg(feature = "python")] +use pyo3::prelude::*; + /// user cursor position in a buffer #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "python", pyo3::pyclass)] +#[cfg_attr(feature = "python", pyclass)] +// #[cfg_attr(feature = "python", pyo3(crate = "reexported::pyo3"))] pub struct Cursor { /// range of text change, as char indexes in buffer previous state pub start: (i32, i32), diff --git a/src/ffi/python_monolithic.rs b/src/ffi/python_monolithic.rs index cb50e6d..e69de29 100644 --- a/src/ffi/python_monolithic.rs +++ b/src/ffi/python_monolithic.rs @@ -1,494 +0,0 @@ -use std::{format, sync::Arc}; -use tokio::sync::{mpsc, Mutex, RwLock}; -use tracing; -use tracing_subscriber; - -use crate::prelude::*; - -use pyo3::{ - exceptions::{PyConnectionError, PyRuntimeError}, - prelude::*, - types::{PyList, PyString, PyTuple, PyType}, -}; - -// ERRORS And LOGGING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -impl From for PyErr { - fn from(value: CodempError) -> Self { - match value { - CodempError::Transport { status, message } => { - PyConnectionError::new_err(format!("Transport error: ({}) {}", status, message)) - } - CodempError::Channel { send } => { - PyConnectionError::new_err(format!("Channel error (send:{})", send)) - } - CodempError::InvalidState { msg } => { - PyRuntimeError::new_err(format!("Invalid state: {}", msg)) - } - CodempError::Deadlocked => PyRuntimeError::new_err(format!("Deadlock, retry.")), - } - } -} - -#[derive(Debug, Clone)] -struct LoggerProducer(mpsc::Sender); - -impl std::io::Write for LoggerProducer { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - // TODO this is a LOSSY logger!! - let _ = self.0.try_send(String::from_utf8_lossy(buf).to_string()); // ignore: logger disconnected or with full buffer - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -#[pyclass] -struct PyLogger(Arc>>); - -#[pymethods] -impl PyLogger { - fn message<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { Ok(rc.lock().await.recv().await) }) - } -} -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -// Workflow: -// We first spin up an empty handler, that can connect to a server, and create a client. -// We then use the client, to login into a workspace, and join it, obtaining a workspace object -// We will then use that workspace object to interact with the buffers in that workspace. -// In steps: -// 1. Get Object that can initiate a connection -// 2. Connect to a server -// 3. Login to a workspace -// 4. Join a workspace/get an already joined workspace -// 5. Create a new buffer/attach to an existing one - -#[pyfunction] -fn codemp_init<'a>(py: Python<'a>) -> PyResult> { - Ok(Py::new(py, Client::default())?) -} - -#[pyfunction] -fn init_logger(py: Python<'_>, debug: Option) -> PyResult> { - let (tx, rx) = mpsc::channel(256); - let level = if debug.unwrap_or(false) { - tracing::Level::DEBUG - } else { - tracing::Level::INFO - }; - - let format = tracing_subscriber::fmt::format() - .without_time() - .with_level(true) - .with_target(true) - .with_thread_ids(false) - .with_thread_names(false) - .with_file(false) - .with_line_number(false) - .with_source_location(false) - .compact(); - - let _ = tracing_subscriber::fmt() - .with_ansi(false) - .event_format(format) - .with_max_level(level) - .with_writer(std::sync::Mutex::new(LoggerProducer(tx))) - .try_init(); - - Ok(Py::new(py, PyLogger(Arc::new(Mutex::new(rx))))?) -} - -#[pyclass] -struct Client(Arc>>); - -impl Default for Client { - fn default() -> Self { - Client(Arc::new(RwLock::new(None))) - } -} - -impl From for Client { - fn from(value: CodempClient) -> Self { - Client(RwLock::new(Some(value)).into()) - } -} - -#[pymethods] -impl Client { - fn connect<'a>(&'a self, py: Python<'a>, dest: String) -> PyResult<&'a PyAny> { - let cli = self.0.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let client: CodempClient = CodempClient::new(dest.as_str()).await?; - - let _ = cli.write().await.insert(client); - - Ok(()) - }) - } - - fn login<'a>( - &'a self, - py: Python<'a>, - user: String, - password: String, - workspace_id: Option, - ) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); - pyo3_asyncio::tokio::future_into_py(py, async move { - let cli = rc.read().await; - if cli.is_none() { - return Err(PyConnectionError::new_err("Connect to a server first.")); - }; - - cli.as_ref() - .unwrap() - .login(user, password, workspace_id) - .await?; - - Ok(()) - }) - } - - fn join_workspace<'a>(&'a self, py: Python<'a>, workspace: String) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let mut cli = rc.write().await; - if cli.is_none() { - return Err(PyConnectionError::new_err("Connect to a server first.")); - }; - - let workspace: CodempWorkspace = cli - .as_mut() - .unwrap() - .join_workspace(workspace.as_str()) - .await? - .into(); - - Python::with_gil(|py| Ok(Py::new(py, workspace)?)) - }) - } - - // join a workspace - fn get_workspace<'a>(&'a self, py: Python<'a>, id: String) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let cli = rc.read().await; - if cli.is_none() { - return Err(PyConnectionError::new_err("Connect to a server first.")); - }; - - let Some(ws) = cli.as_ref().unwrap().get_workspace(id.as_str()) else { - return Ok(None); - }; - - Python::with_gil(|py| Ok(Some(Py::new(py, ws)?))) - }) - } - - fn user_id<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); - pyo3_asyncio::tokio::future_into_py(py, async move { - let cli = rc.read().await; - if cli.is_none() { - return Err(PyConnectionError::new_err("Connect to a server first.")); - }; - let id = cli.as_ref().unwrap().user_id().to_string(); - - Python::with_gil(|py| { - let id: Py = PyString::new(py, id.as_str()).into_py(py); - Ok(id) - }) - }) - } -} - -#[pymethods] -impl CodempWorkspace { - // join a workspace - #[pyo3(name = "create")] - fn pycreate<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { - let ws = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - ws.create(path.as_str()).await?; - Ok(()) - }) - } - #[pyo3(name = "attach")] - fn pyattach<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { - let ws = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let buffctl: CodempBufferController = ws.attach(path.as_str()).await?.into(); - Python::with_gil(|py| Ok(Py::new(py, buffctl)?)) - }) - } - - #[pyo3(name = "fetch_buffers")] - fn pyfetch_buffers<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let ws = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - ws.fetch_buffers().await?; - Ok(()) - }) - } - - #[pyo3(name = "fetch_users")] - fn pyfetch_users<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let ws = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - ws.fetch_users().await?; - Ok(()) - }) - } - - #[pyo3(name = "list_buffer_users")] - fn pylist_buffer_users<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { - let ws = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let usrlist: Vec = ws - .list_buffer_users(path.as_str()) - .await? - .into_iter() - .map(|e| e.id) - .collect(); - - Ok(usrlist) - }) - } - - #[pyo3(name = "delete")] - fn pydelete<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { - let ws = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - ws.delete(path.as_str()).await?; - Ok(()) - }) - } - - #[pyo3(name = "id")] - fn pyid(&self, py: Python<'_>) -> Py { - PyString::new(py, self.id().as_str()).into() - } - - #[pyo3(name = "cursor")] - fn pycursor(&self, py: Python<'_>) -> PyResult> { - Ok(Py::new(py, CodempCursorController::from(self.cursor()))?) - } - - #[pyo3(name = "buffer_by_name")] - fn pybuffer_by_name( - &self, - py: Python<'_>, - path: String, - ) -> PyResult>> { - let Some(bufctl) = self.buffer_by_name(path.as_str()) else { - return Ok(None); - }; - - Ok(Some(Py::new(py, CodempBufferController::from(bufctl))?)) - } - - #[pyo3(name = "filetree")] - fn pyfiletree(&self, py: Python<'_>) -> Py { - PyList::new(py, self.filetree()).into_py(py) - } -} - -/* ########################################################################### */ - -#[pymethods] -impl CodempCursorController { - #[pyo3(name = "send")] - fn pysend<'a>(&'a self, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<()> { - let pos = CodempCursor { - start: start.into(), - end: end.into(), - buffer: path, - user: None, - }; - - Ok(self.send(pos)?) - } - - #[pyo3(name = "try_recv")] - fn pytry_recv(&self, py: Python<'_>) -> PyResult { - match self.try_recv()? { - Some(cur_event) => { - let evt = CodempCursor::from(cur_event); - Ok(evt.into_py(py)) - } - None => Ok(py.None()), - } - } - - #[pyo3(name = "recv")] - fn pyrecv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let cur_event: CodempCursor = rc.recv().await?.into(); - Python::with_gil(|py| Ok(Py::new(py, cur_event)?)) - }) - } - - #[pyo3(name = "poll")] - fn pypoll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { Ok(rc.poll().await?) }) - } -} - -#[pymethods] -impl CodempCursor { - #[getter(start)] - fn pystart(&self, py: Python<'_>) -> Py { - self.start.into_py(py) - } - - #[getter(end)] - fn pyend(&self, py: Python<'_>) -> Py { - self.end.into_py(py) - } - - #[getter(buffer)] - fn pybuffer(&self, py: Python<'_>) -> Py { - PyString::new(py, self.buffer.as_str()).into() - } - - #[getter(user)] - fn pyuser(&self, py: Python<'_>) -> Py { - match self.user { - Some(user) => PyString::new(py, user.to_string().as_str()).into(), - None => "".into_py(py), - } - } -} - -#[pymethods] -impl CodempBufferController { - #[pyo3(name = "content")] - fn pycontent<'a>(&self, py: Python<'a>) -> &'a PyString { - PyString::new(py, self.content().as_str()) - } - - #[pyo3(name = "send")] - fn pysend(&self, start: usize, end: usize, txt: String) -> PyResult<()> { - let op = CodempTextChange { - span: start..end, - content: txt.into(), - }; - Ok(self.send(op)?) - } - - #[pyo3(name = "try_recv")] - fn pytry_recv(&self, py: Python<'_>) -> PyResult { - match self.try_recv()? { - Some(txt_change) => { - let evt = CodempTextChange::from(txt_change); - Ok(evt.into_py(py)) - } - None => Ok(py.None()), - } - } - - #[pyo3(name = "recv")] - fn pyrecv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let txt_change: CodempTextChange = rc.recv().await?.into(); - Python::with_gil(|py| Ok(Py::new(py, txt_change)?)) - }) - } - - #[pyo3(name = "poll")] - fn pypoll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { Ok(rc.poll().await?) }) - } -} - -#[pymethods] -impl CodempTextChange { - #[getter] - #[pyo3(name = "start_incl")] - fn pystart_incl(&self) -> PyResult { - Ok(self.span.start) - } - - #[getter] - #[pyo3(name = "end_excl")] - fn pyend_excl(&self) -> PyResult { - Ok(self.span.end) - } - - #[getter] - #[pyo3(name = "content")] - fn pycontent(&self) -> PyResult { - Ok(self.content.clone()) - } - - #[pyo3(name = "is_deletion")] - fn pyis_deletion(&self) -> bool { - self.is_deletion() - } - - #[pyo3(name = "is_addition")] - fn pyis_addition(&self) -> bool { - self.is_addition() - } - - #[pyo3(name = "is_empty")] - fn pyis_empty(&self) -> bool { - self.is_empty() - } - - #[pyo3(name = "apply")] - fn pyapply(&self, txt: &str) -> String { - self.apply(txt) - } - - #[classmethod] - #[pyo3(name = "from_diff")] - fn pyfrom_diff(_cls: &PyType, before: &str, after: &str) -> CodempTextChange { - CodempTextChange::from_diff(before, after) - } - - #[classmethod] - #[pyo3(name = "index_to_rowcol")] - fn pyindex_to_rowcol(_cls: &PyType, txt: &str, index: usize) -> (i32, i32) { - CodempTextChange::index_to_rowcol(txt, index).into() - } -} - -/* ------ Python module --------*/ -#[pymodule] -fn codemp(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(codemp_init, m)?)?; - m.add_function(wrap_pyfunction!(init_logger, m)?)?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - - m.add_class::()?; - m.add_class::()?; - - Ok(()) -}