Skip to content

Commit d630f0f

Browse files
authored
Merge pull request #2850 from finos/python-schema-relax
Relax `Table` schema constructor
2 parents e9b6cfa + 370548d commit d630f0f

File tree

10 files changed

+265
-139
lines changed

10 files changed

+265
-139
lines changed

rust/perspective-python/perspective/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"Server",
1717
"Client",
1818
"PerspectiveError",
19-
"PerspectiveWidget",
2019
"ProxySession",
2120
]
2221

rust/perspective-python/perspective/tests/table/test_table.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,26 @@ def test_table_symmetric_string_schema(self):
348348

349349
assert tbl2.schema() == schema
350350

351+
def test_table_python_schema(self):
352+
data = {
353+
"a": int,
354+
"b": float,
355+
"c": str,
356+
"d": bool,
357+
"e": date,
358+
"f": datetime,
359+
}
360+
361+
tbl = Table(data)
362+
assert tbl.schema() == {
363+
"a": "integer",
364+
"b": "float",
365+
"c": "string",
366+
"d": "boolean",
367+
"e": "date",
368+
"f": "datetime",
369+
}
370+
351371
# is_valid_filter
352372

353373
# def test_table_is_valid_filter_str(self):

rust/perspective-python/perspective/tests/test_dependencies.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,10 @@ def test_lazy_modules():
4444

4545
for k, v in cache.items():
4646
sys.modules[k] = v
47+
48+
49+
def test_all():
50+
import perspective
51+
52+
for key in perspective.__all__:
53+
assert hasattr(perspective, key)

rust/perspective-python/src/client/client_sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use pyo3::prelude::*;
2323
use pyo3::types::*;
2424

2525
use super::python::*;
26+
use crate::py_err::ResultTClientErrorExt;
2627
use crate::server::PySyncServer;
2728

2829
#[pyclass]

rust/perspective-python/src/client/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ pub mod client_sync;
1414
mod pandas;
1515
mod pyarrow;
1616
pub mod python;
17+
pub mod table_data;
18+
pub mod update_data;

rust/perspective-python/src/client/python.rs

Lines changed: 6 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,27 @@
1010
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
use std::any::Any;
1413
use std::collections::HashMap;
1514
use std::str::FromStr;
1615
use std::sync::Arc;
1716

1817
use async_lock::RwLock;
1918
use futures::FutureExt;
2019
use perspective_client::{
21-
assert_table_api, assert_view_api, clone, Client, ClientError, OnUpdateMode, OnUpdateOptions,
22-
Table, TableData, TableInitOptions, TableReadFormat, UpdateData, UpdateOptions, View,
20+
assert_table_api, assert_view_api, clone, Client, OnUpdateMode, OnUpdateOptions, Table,
21+
TableData, TableInitOptions, TableReadFormat, UpdateData, UpdateOptions, View,
2322
ViewOnUpdateResp, ViewWindow,
2423
};
25-
use pyo3::create_exception;
2624
use pyo3::exceptions::PyValueError;
2725
use pyo3::prelude::*;
28-
use pyo3::types::{PyAny, PyBytes, PyDict, PyList, PyString};
26+
use pyo3::types::{PyAny, PyBytes, PyDict, PyString};
2927
use pythonize::depythonize_bound;
3028

3129
use super::pandas::arrow_to_pandas;
30+
use super::table_data::TableDataExt;
31+
use super::update_data::UpdateDataExt;
3232
use super::{pandas, pyarrow};
33+
use crate::py_err::{PyPerspectiveError, ResultTClientErrorExt};
3334

3435
#[derive(Clone)]
3536
pub struct PyClient {
@@ -38,138 +39,6 @@ pub struct PyClient {
3839
close_cb: Option<Py<PyAny>>,
3940
}
4041

41-
#[extend::ext]
42-
pub impl<T> Result<T, ClientError> {
43-
fn into_pyerr(self) -> PyResult<T> {
44-
match self {
45-
Ok(x) => Ok(x),
46-
Err(x) => Err(PyPerspectiveError::new_err(format!("{}", x))),
47-
}
48-
}
49-
}
50-
51-
create_exception!(
52-
perspective,
53-
PyPerspectiveError,
54-
pyo3::exceptions::PyException
55-
);
56-
57-
#[extend::ext]
58-
impl UpdateData {
59-
fn from_py_partial(
60-
py: Python<'_>,
61-
input: &Py<PyAny>,
62-
format: Option<TableReadFormat>,
63-
) -> Result<Option<UpdateData>, PyErr> {
64-
if let Ok(pybytes) = input.downcast_bound::<PyBytes>(py) {
65-
// TODO need to explicitly qualify this b/c bug in
66-
// rust-analyzer - should be: just `pybytes.as_bytes()`.
67-
let vec = pyo3::prelude::PyBytesMethods::as_bytes(pybytes).to_vec();
68-
69-
match format {
70-
Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(String::from_utf8(vec)?))),
71-
Some(TableReadFormat::JsonString) => {
72-
Ok(Some(UpdateData::JsonRows(String::from_utf8(vec)?)))
73-
},
74-
Some(TableReadFormat::ColumnsString) => {
75-
Ok(Some(UpdateData::JsonColumns(String::from_utf8(vec)?)))
76-
},
77-
None | Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(vec.into()))),
78-
}
79-
} else if let Ok(pystring) = input.downcast_bound::<PyString>(py) {
80-
let string = pystring.extract::<String>()?;
81-
match format {
82-
None | Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(string))),
83-
Some(TableReadFormat::JsonString) => Ok(Some(UpdateData::JsonRows(string))),
84-
Some(TableReadFormat::ColumnsString) => Ok(Some(UpdateData::JsonColumns(string))),
85-
Some(TableReadFormat::Arrow) => {
86-
Ok(Some(UpdateData::Arrow(string.into_bytes().into())))
87-
},
88-
}
89-
} else if let Ok(pylist) = input.downcast_bound::<PyList>(py) {
90-
let json_module = PyModule::import_bound(py, "json")?;
91-
let string = json_module.call_method("dumps", (pylist,), None)?;
92-
Ok(Some(UpdateData::JsonRows(string.extract::<String>()?)))
93-
} else if let Ok(pydict) = input.downcast_bound::<PyDict>(py) {
94-
if pydict.keys().is_empty() {
95-
return Err(PyValueError::new_err("Cannot infer type of empty dict"));
96-
}
97-
98-
let first_key = pydict.keys().get_item(0)?;
99-
let first_item = pydict
100-
.get_item(first_key)?
101-
.ok_or_else(|| PyValueError::new_err("Bad Input"))?;
102-
103-
if first_item.downcast::<PyList>().is_ok() {
104-
let json_module = PyModule::import_bound(py, "json")?;
105-
let string = json_module.call_method("dumps", (pydict,), None)?;
106-
Ok(Some(UpdateData::JsonColumns(string.extract::<String>()?)))
107-
} else {
108-
Ok(None)
109-
}
110-
} else {
111-
Ok(None)
112-
}
113-
}
114-
115-
fn from_py(
116-
py: Python<'_>,
117-
input: &Py<PyAny>,
118-
format: Option<TableReadFormat>,
119-
) -> Result<UpdateData, PyErr> {
120-
if let Some(x) = Self::from_py_partial(py, input, format)? {
121-
Ok(x)
122-
} else {
123-
Err(PyValueError::new_err(format!(
124-
"Unknown input type {:?}",
125-
input.type_id()
126-
)))
127-
}
128-
}
129-
}
130-
131-
#[extend::ext]
132-
impl TableData {
133-
fn from_py(
134-
py: Python<'_>,
135-
input: Py<PyAny>,
136-
format: Option<TableReadFormat>,
137-
) -> Result<TableData, PyErr> {
138-
if let Some(update) = UpdateData::from_py_partial(py, &input, format)? {
139-
Ok(TableData::Update(update))
140-
} else if let Ok(pylist) = input.downcast_bound::<PyList>(py) {
141-
let json_module = PyModule::import_bound(py, "json")?;
142-
let string = json_module.call_method("dumps", (pylist,), None)?;
143-
Ok(UpdateData::JsonRows(string.extract::<String>()?).into())
144-
} else if let Ok(pydict) = input.downcast_bound::<PyDict>(py) {
145-
let first_key = pydict.keys().get_item(0)?;
146-
let first_item = pydict
147-
.get_item(first_key)?
148-
.ok_or_else(|| PyValueError::new_err("Bad Input"))?;
149-
if first_item.downcast::<PyList>().is_ok() {
150-
let json_module = PyModule::import_bound(py, "json")?;
151-
let string = json_module.call_method("dumps", (pydict,), None)?;
152-
Ok(UpdateData::JsonColumns(string.extract::<String>()?).into())
153-
} else {
154-
let mut schema = vec![];
155-
for (key, val) in pydict.into_iter() {
156-
schema.push((
157-
key.extract::<String>()?,
158-
val.extract::<String>()?.as_str().try_into().into_pyerr()?,
159-
));
160-
}
161-
162-
Ok(TableData::Schema(schema))
163-
}
164-
} else {
165-
Err(PyValueError::new_err(format!(
166-
"Unknown input type {:?}",
167-
input.type_id()
168-
)))
169-
}
170-
}
171-
}
172-
17342
impl PyClient {
17443
pub fn new(handle_request: Py<PyAny>, handle_close: Option<Py<PyAny>>) -> Self {
17544
let client = Client::new_with_callback({
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
use perspective_client::{ColumnType, TableData, TableReadFormat, UpdateData};
14+
use pyo3::exceptions::{PyTypeError, PyValueError};
15+
use pyo3::prelude::*;
16+
use pyo3::types::{PyAny, PyAnyMethods, PyDict, PyList, PyString, PyType};
17+
18+
use super::update_data::UpdateDataExt;
19+
use crate::py_err::ResultTClientErrorExt;
20+
21+
fn psp_type_from_py_type(_py: Python<'_>, val: Bound<'_, PyAny>) -> PyResult<ColumnType> {
22+
if val.is_instance_of::<PyString>() {
23+
val.extract::<String>()?.as_str().try_into().into_pyerr()
24+
} else if let Ok(val) = val.downcast::<PyType>() {
25+
match val.name()?.as_ref() {
26+
"builtins.int" | "int" => Ok(ColumnType::Integer),
27+
"builtins.float" | "float" => Ok(ColumnType::Float),
28+
"builtins.str" | "str" => Ok(ColumnType::String),
29+
"builtins.bool" | "bool" => Ok(ColumnType::Boolean),
30+
"datetime.date" => Ok(ColumnType::Date),
31+
"datetime.datetime" => Ok(ColumnType::Datetime),
32+
type_name => Err(PyTypeError::new_err(type_name.to_string())),
33+
}
34+
} else {
35+
Err(PyTypeError::new_err(format!(
36+
"Unknown schema type {:?}",
37+
val.get_type().name()?
38+
)))
39+
}
40+
}
41+
42+
fn from_dict(py: Python<'_>, pydict: &Bound<'_, PyDict>) -> Result<TableData, PyErr> {
43+
let first_key = pydict.keys().get_item(0)?;
44+
let first_item = pydict
45+
.get_item(first_key)?
46+
.ok_or_else(|| PyValueError::new_err("Schema has no columns"))?;
47+
48+
if first_item.downcast::<PyList>().is_ok() {
49+
let json_module = PyModule::import_bound(py, "json")?;
50+
let string = json_module.call_method("dumps", (pydict,), None)?;
51+
Ok(UpdateData::JsonColumns(string.extract::<String>()?).into())
52+
} else {
53+
let mut schema = vec![];
54+
for (key, val) in pydict.into_iter() {
55+
schema.push((key.extract::<String>()?, psp_type_from_py_type(py, val)?));
56+
}
57+
58+
Ok(TableData::Schema(schema))
59+
}
60+
}
61+
62+
#[extend::ext]
63+
pub impl TableData {
64+
fn from_py(
65+
py: Python<'_>,
66+
input: Py<PyAny>,
67+
format: Option<TableReadFormat>,
68+
) -> Result<TableData, PyErr> {
69+
if let Some(update) = UpdateData::from_py_partial(py, &input, format)? {
70+
Ok(TableData::Update(update))
71+
} else if let Ok(pylist) = input.downcast_bound::<PyList>(py) {
72+
let json_module = PyModule::import_bound(py, "json")?;
73+
let string = json_module.call_method("dumps", (pylist,), None)?;
74+
Ok(UpdateData::JsonRows(string.extract::<String>()?).into())
75+
} else if let Ok(pydict) = input.downcast_bound::<PyDict>(py) {
76+
from_dict(py, pydict)
77+
} else {
78+
Err(PyTypeError::new_err(format!(
79+
"Unknown input type {:?}",
80+
input.bind(py).get_type().name()?
81+
)))
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)