Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax Table schema constructor #2850

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion rust/perspective-python/perspective/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"Server",
"Client",
"PerspectiveError",
"PerspectiveWidget",
"ProxySession",
]

Expand Down
20 changes: 20 additions & 0 deletions rust/perspective-python/perspective/tests/table/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,26 @@ def test_table_symmetric_string_schema(self):

assert tbl2.schema() == schema

def test_table_python_schema(self):
data = {
"a": int,
"b": float,
"c": str,
"d": bool,
"e": date,
"f": datetime,
}

tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}

# is_valid_filter

# def test_table_is_valid_filter_str(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ def test_lazy_modules():

for k, v in cache.items():
sys.modules[k] = v


def test_all():
import perspective

for key in perspective.__all__:
assert hasattr(perspective, key)
1 change: 1 addition & 0 deletions rust/perspective-python/src/client/client_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use pyo3::prelude::*;
use pyo3::types::*;

use super::python::*;
use crate::py_err::ResultTClientErrorExt;
use crate::server::PySyncServer;

#[pyclass]
Expand Down
2 changes: 2 additions & 0 deletions rust/perspective-python/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ pub mod client_sync;
mod pandas;
mod pyarrow;
pub mod python;
pub mod table_data;
pub mod update_data;
143 changes: 6 additions & 137 deletions rust/perspective-python/src/client/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,27 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

use std::any::Any;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;

use async_lock::RwLock;
use futures::FutureExt;
use perspective_client::{
assert_table_api, assert_view_api, clone, Client, ClientError, OnUpdateMode, OnUpdateOptions,
Table, TableData, TableInitOptions, TableReadFormat, UpdateData, UpdateOptions, View,
assert_table_api, assert_view_api, clone, Client, OnUpdateMode, OnUpdateOptions, Table,
TableData, TableInitOptions, TableReadFormat, UpdateData, UpdateOptions, View,
ViewOnUpdateResp, ViewWindow,
};
use pyo3::create_exception;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyBytes, PyDict, PyList, PyString};
use pyo3::types::{PyAny, PyBytes, PyDict, PyString};
use pythonize::depythonize_bound;

use super::pandas::arrow_to_pandas;
use super::table_data::TableDataExt;
use super::update_data::UpdateDataExt;
use super::{pandas, pyarrow};
use crate::py_err::{PyPerspectiveError, ResultTClientErrorExt};

#[derive(Clone)]
pub struct PyClient {
Expand All @@ -38,138 +39,6 @@ pub struct PyClient {
close_cb: Option<Py<PyAny>>,
}

#[extend::ext]
pub impl<T> Result<T, ClientError> {
fn into_pyerr(self) -> PyResult<T> {
match self {
Ok(x) => Ok(x),
Err(x) => Err(PyPerspectiveError::new_err(format!("{}", x))),
}
}
}

create_exception!(
perspective,
PyPerspectiveError,
pyo3::exceptions::PyException
);

#[extend::ext]
impl UpdateData {
fn from_py_partial(
py: Python<'_>,
input: &Py<PyAny>,
format: Option<TableReadFormat>,
) -> Result<Option<UpdateData>, PyErr> {
if let Ok(pybytes) = input.downcast_bound::<PyBytes>(py) {
// TODO need to explicitly qualify this b/c bug in
// rust-analyzer - should be: just `pybytes.as_bytes()`.
let vec = pyo3::prelude::PyBytesMethods::as_bytes(pybytes).to_vec();

match format {
Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(String::from_utf8(vec)?))),
Some(TableReadFormat::JsonString) => {
Ok(Some(UpdateData::JsonRows(String::from_utf8(vec)?)))
},
Some(TableReadFormat::ColumnsString) => {
Ok(Some(UpdateData::JsonColumns(String::from_utf8(vec)?)))
},
None | Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(vec.into()))),
}
} else if let Ok(pystring) = input.downcast_bound::<PyString>(py) {
let string = pystring.extract::<String>()?;
match format {
None | Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(string))),
Some(TableReadFormat::JsonString) => Ok(Some(UpdateData::JsonRows(string))),
Some(TableReadFormat::ColumnsString) => Ok(Some(UpdateData::JsonColumns(string))),
Some(TableReadFormat::Arrow) => {
Ok(Some(UpdateData::Arrow(string.into_bytes().into())))
},
}
} else if let Ok(pylist) = input.downcast_bound::<PyList>(py) {
let json_module = PyModule::import_bound(py, "json")?;
let string = json_module.call_method("dumps", (pylist,), None)?;
Ok(Some(UpdateData::JsonRows(string.extract::<String>()?)))
} else if let Ok(pydict) = input.downcast_bound::<PyDict>(py) {
if pydict.keys().is_empty() {
return Err(PyValueError::new_err("Cannot infer type of empty dict"));
}

let first_key = pydict.keys().get_item(0)?;
let first_item = pydict
.get_item(first_key)?
.ok_or_else(|| PyValueError::new_err("Bad Input"))?;

if first_item.downcast::<PyList>().is_ok() {
let json_module = PyModule::import_bound(py, "json")?;
let string = json_module.call_method("dumps", (pydict,), None)?;
Ok(Some(UpdateData::JsonColumns(string.extract::<String>()?)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}

fn from_py(
py: Python<'_>,
input: &Py<PyAny>,
format: Option<TableReadFormat>,
) -> Result<UpdateData, PyErr> {
if let Some(x) = Self::from_py_partial(py, input, format)? {
Ok(x)
} else {
Err(PyValueError::new_err(format!(
"Unknown input type {:?}",
input.type_id()
)))
}
}
}

#[extend::ext]
impl TableData {
fn from_py(
py: Python<'_>,
input: Py<PyAny>,
format: Option<TableReadFormat>,
) -> Result<TableData, PyErr> {
if let Some(update) = UpdateData::from_py_partial(py, &input, format)? {
Ok(TableData::Update(update))
} else if let Ok(pylist) = input.downcast_bound::<PyList>(py) {
let json_module = PyModule::import_bound(py, "json")?;
let string = json_module.call_method("dumps", (pylist,), None)?;
Ok(UpdateData::JsonRows(string.extract::<String>()?).into())
} else if let Ok(pydict) = input.downcast_bound::<PyDict>(py) {
let first_key = pydict.keys().get_item(0)?;
let first_item = pydict
.get_item(first_key)?
.ok_or_else(|| PyValueError::new_err("Bad Input"))?;
if first_item.downcast::<PyList>().is_ok() {
let json_module = PyModule::import_bound(py, "json")?;
let string = json_module.call_method("dumps", (pydict,), None)?;
Ok(UpdateData::JsonColumns(string.extract::<String>()?).into())
} else {
let mut schema = vec![];
for (key, val) in pydict.into_iter() {
schema.push((
key.extract::<String>()?,
val.extract::<String>()?.as_str().try_into().into_pyerr()?,
));
}

Ok(TableData::Schema(schema))
}
} else {
Err(PyValueError::new_err(format!(
"Unknown input type {:?}",
input.type_id()
)))
}
}
}

impl PyClient {
pub fn new(handle_request: Py<PyAny>, handle_close: Option<Py<PyAny>>) -> Self {
let client = Client::new_with_callback({
Expand Down
84 changes: 84 additions & 0 deletions rust/perspective-python/src/client/table_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

use perspective_client::{ColumnType, TableData, TableReadFormat, UpdateData};
use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyAnyMethods, PyDict, PyList, PyString, PyType};

use super::update_data::UpdateDataExt;
use crate::py_err::ResultTClientErrorExt;

fn psp_type_from_py_type(_py: Python<'_>, val: Bound<'_, PyAny>) -> PyResult<ColumnType> {
if val.is_instance_of::<PyString>() {
val.extract::<String>()?.as_str().try_into().into_pyerr()
} else if let Ok(val) = val.downcast::<PyType>() {
match val.name()?.as_ref() {
"builtins.int" | "int" => Ok(ColumnType::Integer),
"builtins.float" | "float" => Ok(ColumnType::Float),
"builtins.str" | "str" => Ok(ColumnType::String),
"builtins.bool" | "bool" => Ok(ColumnType::Boolean),
"datetime.date" => Ok(ColumnType::Date),
"datetime.datetime" => Ok(ColumnType::Datetime),
type_name => Err(PyTypeError::new_err(type_name.to_string())),
}
} else {
Err(PyTypeError::new_err(format!(
"Unknown schema type {:?}",
val.get_type().name()?
)))
}
}

fn from_dict(py: Python<'_>, pydict: &Bound<'_, PyDict>) -> Result<TableData, PyErr> {
let first_key = pydict.keys().get_item(0)?;
let first_item = pydict
.get_item(first_key)?
.ok_or_else(|| PyValueError::new_err("Schema has no columns"))?;

if first_item.downcast::<PyList>().is_ok() {
let json_module = PyModule::import_bound(py, "json")?;
let string = json_module.call_method("dumps", (pydict,), None)?;
Ok(UpdateData::JsonColumns(string.extract::<String>()?).into())
} else {
let mut schema = vec![];
for (key, val) in pydict.into_iter() {
schema.push((key.extract::<String>()?, psp_type_from_py_type(py, val)?));
}

Ok(TableData::Schema(schema))
}
}

#[extend::ext]
pub impl TableData {
fn from_py(
py: Python<'_>,
input: Py<PyAny>,
format: Option<TableReadFormat>,
) -> Result<TableData, PyErr> {
if let Some(update) = UpdateData::from_py_partial(py, &input, format)? {
Ok(TableData::Update(update))
} else if let Ok(pylist) = input.downcast_bound::<PyList>(py) {
let json_module = PyModule::import_bound(py, "json")?;
let string = json_module.call_method("dumps", (pylist,), None)?;
Ok(UpdateData::JsonRows(string.extract::<String>()?).into())
} else if let Ok(pydict) = input.downcast_bound::<PyDict>(py) {
from_dict(py, pydict)
} else {
Err(PyTypeError::new_err(format!(
"Unknown input type {:?}",
input.bind(py).get_type().name()?
)))
}
}
}
Loading
Loading