perspective_python/client/client_sync.rs
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
13use std::collections::HashMap;
14use std::future::Future;
15use std::sync::Arc;
16
17use perspective_client::config::Scalar;
18use perspective_client::{JoinType, TableRef, assert_table_api, assert_view_api};
19#[cfg(doc)]
20use perspective_client::{TableInitOptions, UpdateOptions, config::ViewConfigUpdate};
21use pyo3::exceptions::PyTypeError;
22use pyo3::marker::Ungil;
23use pyo3::prelude::*;
24use pyo3::types::*;
25
26use super::client_async::*;
27use crate::py_err::ResultTClientErrorExt;
28use crate::server::Server;
29
30pub(crate) fn py_to_table_ref(val: &Bound<'_, PyAny>) -> PyResult<TableRef> {
31 if let Ok(t) = val.cast::<Table>() {
32 let table_ref = t.borrow();
33 Ok(TableRef::from(&*table_ref.0.table))
34 } else if let Ok(name) = val.extract::<String>() {
35 Ok(TableRef::from(name))
36 } else {
37 Err(PyTypeError::new_err(
38 "Expected a Table or string table name",
39 ))
40 }
41}
42
43pub(crate) fn parse_join_type(join_type: Option<&str>) -> PyResult<JoinType> {
44 match join_type {
45 Some("left") => Ok(JoinType::Left),
46 Some("outer") => Ok(JoinType::Outer),
47 None | Some("inner") => Ok(JoinType::Inner),
48 Some(other) => Err(pyo3::exceptions::PyValueError::new_err(format!(
49 "Unknown join type: \"{}\"",
50 other
51 ))),
52 }
53}
54
55pub(crate) fn scalar_to_py(py: Python<'_>, scalar: &Scalar) -> Py<PyAny> {
56 match scalar {
57 Scalar::Float(x) => x.into_pyobject(py).unwrap().into_any().unbind(),
58 Scalar::String(x) => x.into_pyobject(py).unwrap().into_any().unbind(),
59 Scalar::Bool(x) => x.into_pyobject(py).unwrap().to_owned().into_any().unbind(),
60 Scalar::Null => py.None(),
61 }
62}
63
64pub(crate) trait PyFutureExt: Future {
65 fn py_block_on(self, py: Python<'_>) -> Self::Output
66 where
67 Self: Sized + Send,
68 Self::Output: Ungil,
69 {
70 use pollster::FutureExt;
71 py.detach(move || self.block_on())
72 }
73}
74
75impl<F: Future> PyFutureExt for F {}
76
77/// An instance of a [`Client`] is a connection to a single [`Server`], whether
78/// locally in-memory or remote over some transport like a WebSocket.
79///
80/// [`Client`] and Perspective objects derived from it have _synchronous_ APIs,
81/// suitable for use in a repl or script context where this is the _only_
82/// [`Client`] connected to its [`Server`]. If you want to
83/// integrate with a Web framework or otherwise connect multiple clients,
84/// use [`AsyncClient`].
85#[pyclass(subclass, module = "perspective")]
86pub struct Client(pub(crate) AsyncClient);
87
88#[pymethods]
89impl Client {
90 #[new]
91 #[pyo3(signature = (handle_request, close_cb=None, name=None))]
92 pub fn new(
93 handle_request: Py<PyAny>,
94 close_cb: Option<Py<PyAny>>,
95 name: Option<String>,
96 ) -> PyResult<Self> {
97 let client = AsyncClient::new(handle_request, close_cb, name)?;
98 Ok(Client(client))
99 }
100
101 /// Create a new [`Client`] instance bound to a specific in-process
102 /// [`Server`] (e.g. generally _not_ the global [`Server`]).
103 #[staticmethod]
104 pub fn from_server(py: Python<'_>, server: Py<Server>) -> PyResult<Self> {
105 server.borrow(py).new_local_client()
106 }
107
108 /// Handle a message from the external message queue.
109 /// [`Client::handle_response`] is part of the low-level message-handling
110 /// API necessary to implement new transports for a [`Client`]
111 /// connection to a local-or-remote [`Server`], and
112 /// doesn't generally need to be called directly by "users" of a
113 /// [`Client`] once connected.
114 pub fn handle_response(&self, py: Python<'_>, response: Py<PyBytes>) -> PyResult<bool> {
115 self.0.handle_response(response).py_block_on(py)
116 }
117
118 /// Creates a new [`Table`] from either a _schema_ or _data_.
119 ///
120 /// The [`Client::table`] factory function can be initialized with either a
121 /// _schema_ (see [`Table::schema`]), or data in one of these formats:
122 ///
123 /// - Apache Arrow
124 /// - CSV
125 /// - JSON row-oriented
126 /// - JSON column-oriented
127 /// - NDJSON
128 ///
129 /// When instantiated with _data_, the schema is inferred from this data.
130 /// While this is convenient, inferrence is sometimes imperfect e.g.
131 /// when the input is empty, null or ambiguous. For these cases,
132 /// [`Client::table`] can first be instantiated with a explicit schema.
133 ///
134 /// When instantiated with a _schema_, the resulting [`Table`] is empty but
135 /// with known column names and column types. When subsqeuently
136 /// populated with [`Table::update`], these columns will be _coerced_ to
137 /// the schema's type. This behavior can be useful when
138 /// [`Client::table`]'s column type inferences doesn't work.
139 ///
140 /// The resulting [`Table`] is _virtual_, and invoking its methods
141 /// dispatches events to the `perspective_server::Server` this
142 /// [`Client`] connects to, where the data is stored and all calculation
143 /// occurs.
144 ///
145 /// # Arguments
146 ///
147 /// - `arg` - Either _schema_ or initialization _data_.
148 /// - `options` - Optional configuration which provides one of:
149 /// - `limit` - The max number of rows the resulting [`Table`] can
150 /// store.
151 /// - `index` - The column name to use as an _index_ column. If this
152 /// `Table` is being instantiated by _data_, this column name must be
153 /// present in the data.
154 /// - `name` - The name of the table. This will be generated if it is
155 /// not provided.
156 /// - `format` - The explicit format of the input data, can be one of
157 /// `"json"`, `"columns"`, `"csv"` or `"arrow"`. This overrides
158 /// language-specific type dispatch behavior, which allows stringified
159 /// and byte array alternative inputs.
160 /// - `schema` - The columns of a [`Table`] derived from a `View`, in
161 /// place of the ones inferred from it.
162 ///
163 /// # Python Examples
164 ///
165 /// Load a CSV from a `str`:
166 ///
167 /// ```python
168 /// table = client.table("x,y\n1,2\n3,4")
169 /// ```
170 #[allow(clippy::too_many_arguments)]
171 #[pyo3(signature = (input, limit=None, index=None, name=None, format=None, page_to_disk=None, list_flatten=None, schema=None))]
172 pub fn table(
173 &self,
174 py: Python<'_>,
175 input: Py<PyAny>,
176 limit: Option<u32>,
177 index: Option<Py<PyString>>,
178 name: Option<Py<PyString>>,
179 format: Option<Py<PyString>>,
180 page_to_disk: Option<bool>,
181 list_flatten: Option<Py<PyString>>,
182 schema: Option<Py<PyDict>>,
183 ) -> PyResult<Table> {
184 Ok(Table(
185 self.0
186 .table(
187 input,
188 limit,
189 index,
190 name,
191 format,
192 page_to_disk,
193 list_flatten,
194 schema,
195 )
196 .py_block_on(py)?,
197 ))
198 }
199
200 /// Opens a [`Table`] that is hosted on the `perspective_server::Server`
201 /// that is connected to this [`Client`].
202 ///
203 /// The `name` property of [`TableInitOptions`] is used to identify each
204 /// [`Table`]. [`Table`] `name`s can be looked up for each [`Client`]
205 /// via [`Client::get_hosted_table_names`].
206 ///
207 /// # Python Examples
208 ///
209 /// ```python
210 /// table = client.open_table("table_one");
211 /// ```
212 pub fn open_table(&self, py: Python<'_>, name: String) -> PyResult<Table> {
213 let client = self.0.clone();
214 let table = client.open_table(name).py_block_on(py)?;
215 Ok(Table(table))
216 }
217
218 /// Creates a new read-only [`Table`] by performing a JOIN on two
219 /// source tables. The resulting table is reactive: when either source
220 /// table is updated, the join is automatically recomputed.
221 ///
222 /// # Python Examples
223 ///
224 /// ```python
225 /// joined = client.join(orders_table, products_table, "Product ID", "left")
226 /// ```
227 #[pyo3(signature = (left, right, on, join_type=None, name=None, right_on=None))]
228 #[allow(clippy::too_many_arguments, reason = "This is a Python API")]
229 pub fn join(
230 &self,
231 py: Python<'_>,
232 left: &Bound<'_, PyAny>,
233 right: &Bound<'_, PyAny>,
234 on: String,
235 join_type: Option<String>,
236 name: Option<String>,
237 right_on: Option<String>,
238 ) -> PyResult<Table> {
239 let left_ref = py_to_table_ref(left)?;
240 let right_ref = py_to_table_ref(right)?;
241 let jt = parse_join_type(join_type.as_deref())?;
242 let options = perspective_client::JoinOptions {
243 join_type: Some(jt),
244 name,
245 right_on,
246 };
247 let table = self
248 .0
249 .client
250 .join(left_ref, right_ref, &on, options)
251 .py_block_on(py)
252 .into_pyerr()?;
253 Ok(Table(AsyncTable {
254 table: Arc::new(table),
255 client: self.0.clone(),
256 }))
257 }
258
259 /// Retrieves the names of all tables that this client has access to.
260 ///
261 /// `name` is a string identifier unique to the [`Table`] (per [`Client`]),
262 /// which can be used in conjunction with [`Client::open_table`] to get
263 /// a [`Table`] instance without the use of [`Client::table`]
264 /// constructor directly (e.g., one created by another [`Client`]).
265 ///
266 /// # Python Examples
267 ///
268 /// ```python
269 /// tables = client.get_hosted_table_names();
270 /// ```
271 pub fn get_hosted_table_names(&self, py: Python<'_>) -> PyResult<Vec<String>> {
272 self.0.get_hosted_table_names().py_block_on(py)
273 }
274
275 /// Register a callback which is invoked whenever [`Client::table`] (on this
276 /// [`Client`]) or [`Table::delete`] (on a [`Table`] belinging to this
277 /// [`Client`]) are called.
278 pub fn on_hosted_tables_update(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
279 self.0.on_hosted_tables_update(callback).py_block_on(py)
280 }
281
282 /// Remove a callback previously registered via
283 /// [`Client::on_hosted_tables_update`].
284 pub fn remove_hosted_tables_update(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
285 self.0
286 .remove_hosted_tables_update(callback_id)
287 .py_block_on(py)
288 }
289
290 /// Provides the [`SystemInfo`] struct, implementation-specific metadata
291 /// about the [`perspective_server::Server`] runtime such as Memory and
292 /// CPU usage.
293 pub fn system_info(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
294 self.0.system_info().py_block_on(py)
295 }
296
297 /// Terminates this [`Client`], cleaning up any [`View`] handles the
298 /// [`Client`] has open as well as its callbacks.
299 pub fn terminate(&self, py: Python<'_>) -> PyResult<()> {
300 self.0.terminate(py)
301 }
302}
303
304/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
305/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
306/// by index, and update notifications.
307///
308/// A [`Table`] contains columns, each of which have a unique name, are strongly
309/// and consistently typed, and contains rows of data conforming to the column's
310/// type. Each column in a [`Table`] must have the same number of rows, though
311/// not every row must contain data; null-values are used to indicate missing
312/// values in the dataset. The schema of a [`Table`] is _immutable after
313/// creation_, which means the column names and data types cannot be changed
314/// after the [`Table`] has been created. Columns cannot be added or deleted
315/// after creation either, but a [`View`] can be used to select an arbitrary set
316/// of columns from the [`Table`].
317#[pyclass(subclass, name = "Table", module = "perspective")]
318pub struct Table(AsyncTable);
319
320assert_table_api!(Table);
321
322#[pymethods]
323impl Table {
324 #[new]
325 fn new() -> PyResult<Self> {
326 Err(PyTypeError::new_err(
327 "Do not call Table's constructor directly, construct from a Client instance.",
328 ))
329 }
330
331 /// Returns the name of the index column for the table.
332 ///
333 /// # Python Examples
334 ///
335 /// ```python
336 /// table = perspective.table("x,y\n1,2\n3,4", index="x");
337 /// index = client.get_index()
338 /// ```
339 pub fn get_index(&self) -> Option<String> {
340 self.0.get_index()
341 }
342
343 /// Get a copy of the [`Client`] this [`Table`] came from.
344 pub fn get_client(&self, py: Python<'_>) -> Client {
345 Client(self.0.get_client().py_block_on(py))
346 }
347
348 /// Returns the user-specified row limit for this table.
349 pub fn get_limit(&self) -> Option<u32> {
350 self.0.get_limit()
351 }
352
353 /// Returns the user-specified name for this table, or the auto-generated
354 /// name if a name was not specified when the table was created.
355 pub fn get_name(&self) -> String {
356 self.0.get_name()
357 }
358
359 /// Removes all the rows in the [`Table`], but preserves everything else
360 /// including the schema, index, and any callbacks or registered
361 /// [`View`] instances.
362 ///
363 /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
364 /// will trigger an update event to any registered listeners via
365 /// [`View::on_update`].
366 pub fn clear(&self, py: Python<'_>) -> PyResult<()> {
367 self.0.clear().py_block_on(py)
368 }
369
370 /// Returns the column names of this [`Table`] in "natural" order (the
371 /// ordering implied by the input format).
372 ///
373 /// # Python Examples
374 ///
375 /// ```python
376 /// columns = table.columns()
377 /// ```
378 pub fn columns(&self, py: Python<'_>) -> PyResult<Vec<String>> {
379 self.0.columns().py_block_on(py)
380 }
381
382 /// Delete this [`Table`] and cleans up associated resources.
383 ///
384 /// [`Table`]s do not stop consuming resources or processing updates when
385 /// they are garbage collected in their host language - you must call
386 /// this method to reclaim these.
387 ///
388 /// # Arguments
389 ///
390 /// - `options` An options dictionary.
391 /// - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
392 /// default), the delete will occur immediately, assuming it has no
393 /// [`View`] instances registered to it (which must be deleted first,
394 /// otherwise this method will throw an error). When true, the
395 /// [`Table`] will only be marked for deltion once its [`View`]
396 /// dependency count reaches 0.
397 ///
398 /// # Python Examples
399 ///
400 /// ```python
401 /// table = client.table("x,y\n1,2\n3,4")
402 ///
403 /// # ...
404 ///
405 /// table.delete(lazy=True)
406 /// ```
407 #[pyo3(signature=(lazy=false))]
408 pub fn delete(&self, py: Python<'_>, lazy: bool) -> PyResult<()> {
409 self.0.delete(lazy).py_block_on(py)
410 }
411
412 /// Create a unique channel ID on this [`Table`], which allows
413 /// `View::on_update` callback calls to be associated with the
414 /// `Table::update` which caused them.
415 pub fn make_port(&self, py: Python<'_>) -> PyResult<i32> {
416 let table = self.0.clone();
417 table.make_port().py_block_on(py)
418 }
419
420 /// Register a callback which is called exactly once, when this [`Table`] is
421 /// deleted with the [`Table::delete`] method.
422 ///
423 /// [`Table::on_delete`] resolves when the subscription message is sent, not
424 /// when the _delete_ event occurs.
425 pub fn on_delete(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
426 let table = self.0.clone();
427 table.on_delete(callback).py_block_on(py)
428 }
429
430 #[pyo3(signature = (input, format=None))]
431 pub fn remove(&self, py: Python<'_>, input: Py<PyAny>, format: Option<String>) -> PyResult<()> {
432 let table = self.0.clone();
433 table.remove(input, format).py_block_on(py)
434 }
435
436 /// Removes a listener with a given ID, as returned by a previous call to
437 /// [`Table::on_delete`].
438 pub fn remove_delete(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
439 let table = self.0.clone();
440 table.remove_delete(callback_id).py_block_on(py)
441 }
442
443 /// Returns a table's [`Schema`], a mapping of column names to column types.
444 ///
445 /// The mapping of a [`Table`]'s column names to data types is referred to
446 /// as a [`Schema`]. Each column has a unique name and a data type, one
447 /// of:
448 ///
449 /// - `"boolean"` - A boolean type
450 /// - `"date"` - A timesonze-agnostic date type (month/day/year)
451 /// - `"datetime"` - A millisecond-precision datetime type in the UTC
452 /// timezone
453 /// - `"float"` - A 64 bit float
454 /// - `"integer"` - A signed 32 bit integer (the integer type supported by
455 /// JavaScript)
456 /// - `"string"` - A `String` data type (encoded internally as a
457 /// _dictionary_)
458 ///
459 /// Note that all [`Table`] columns are _nullable_, regardless of the data
460 /// type.
461 pub fn schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
462 let table = self.0.clone();
463 table.schema().py_block_on(py)
464 }
465
466 /// Validate a complete view config against this table and report the schema
467 /// a [`View`] built from it would have, WITHOUT creating one.
468 #[pyo3(signature = (**config))]
469 pub fn describe(&self, py: Python<'_>, config: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
470 let table = self.0.clone();
471 table.describe(config).py_block_on(py)
472 }
473
474 /// Validates the given expressions.
475 pub fn validate_expressions(
476 &self,
477 py: Python<'_>,
478 expression: Py<PyAny>,
479 ) -> PyResult<Py<PyAny>> {
480 let table = self.0.clone();
481 table.validate_expressions(expression).py_block_on(py)
482 }
483
484 /// Create a new [`View`] from this table with a specified
485 /// [`ViewConfigUpdate`].
486 ///
487 /// See [`View`] struct.
488 ///
489 /// # Examples
490 ///
491 /// ```python
492 /// view view = table.view(
493 /// columns=["Sales"],
494 /// aggregates={"Sales": "sum"},
495 /// group_by=["Region", "State"],
496 /// )
497 /// ```
498 #[pyo3(signature = (**config))]
499 pub fn view(&self, py: Python<'_>, config: Option<Py<PyDict>>) -> PyResult<View> {
500 Ok(View(self.0.view(config).py_block_on(py)?))
501 }
502
503 /// Returns the number of rows in a [`Table`].
504 pub fn size(&self, py: Python<'_>) -> PyResult<usize> {
505 self.0.size().py_block_on(py)
506 }
507
508 /// Removes all the rows in the [`Table`], but preserves everything else
509 /// including the schema, index, and any callbacks or registered
510 /// [`View`] instances.
511 ///
512 /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
513 /// will trigger an update event to any registered listeners via
514 /// [`View::on_update`].
515 #[pyo3(signature = (input, format=None))]
516 pub fn replace(
517 &self,
518 py: Python<'_>,
519 input: Py<PyAny>,
520 format: Option<String>,
521 ) -> PyResult<()> {
522 self.0.replace(input, format).py_block_on(py)
523 }
524
525 /// Updates the rows of this table and any derived [`View`] instances.
526 ///
527 /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
528 /// register to derived [`View`], and the call itself will not resolve until
529 /// _all_ derived [`View`]'s are notified.
530 ///
531 /// When updating a [`Table`] with an `index`, [`Table::update`] supports
532 /// partial updates, by omitting columns from the update data.
533 ///
534 /// # Arguments
535 ///
536 /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
537 /// is immutable after creation, so this method cannot be called with a
538 /// schema.
539 /// - `options` - Options for this update step - see
540 /// [`perspective_client::UpdateOptions`].
541 /// ```
542 #[pyo3(signature = (input, port_id=None, format=None))]
543 pub fn update(
544 &self,
545 py: Python<'_>,
546 input: Py<PyAny>,
547 port_id: Option<u32>,
548 format: Option<String>,
549 ) -> PyResult<()> {
550 self.0.update(input, port_id, format).py_block_on(py)
551 }
552}
553
554/// The [`View`] struct is Perspective's query and serialization interface. It
555/// represents a query on the `Table`'s dataset and is always created from an
556/// existing `Table` instance via the [`Table::view`] method.
557///
558/// [`View`]s are immutable with respect to the arguments provided to the
559/// [`Table::view`] method; to change these parameters, you must create a new
560/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
561/// respect to the [`Table`]'s data, and will (within a conflation window)
562/// update with the latest state as its parent [`Table`] updates, including
563/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
564/// query parameters are composable, in that each parameter works independently
565/// _and_ in conjunction with each other, and there is no limit to the number of
566/// pivots, filters, etc. which can be applied.
567///
568/// To construct a [`View`], call the [`Table::view`] factory method. A
569/// [`Table`] can have as many [`View`]s associated with it as you need -
570/// Perspective conserves memory by relying on a single [`Table`] to power
571/// multiple [`View`]s concurrently.
572#[pyclass(subclass, name = "View", module = "perspective")]
573pub struct View(pub(crate) AsyncView);
574
575assert_view_api!(View);
576
577#[pymethods]
578impl View {
579 #[new]
580 fn new() -> PyResult<Self> {
581 Err(PyTypeError::new_err(
582 "Do not call View's constructor directly, construct from a Table instance.",
583 ))
584 }
585
586 /// Returns this [`View`]'s column header area for `window`, transposed as
587 /// `area[level][column]`.
588 ///
589 /// A column path shows the columns that a given cell belongs to after
590 /// pivots are applied. The area is rectangular with one level per
591 /// `split_by` plus one: a column's split values occupy the leading levels
592 /// and its name always occupies the last, so a subtotal or grand total
593 /// under `split_rollup_mode: "rollup"` reads `None` at the levels it does
594 /// not pivot on. Values keep their column's type - a `datetime` split value
595 /// is epoch milliseconds, not text - so formatting them is the caller's
596 /// choice.
597 ///
598 /// `window` slices the *column* axis; the number of levels does not depend
599 /// on it.
600 #[pyo3(signature = (**window))]
601 pub fn column_paths(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
602 self.0.column_paths(window).py_block_on(py)
603 }
604
605 /// Renders this [`View`] as a column-oriented JSON string. Useful if you
606 /// want to save additional round trip serialize/deserialize cycles.
607 #[pyo3(signature = (**window))]
608 pub fn to_columns_string(
609 &self,
610 py: Python<'_>,
611 window: Option<Py<PyDict>>,
612 ) -> PyResult<String> {
613 self.0.to_columns_string(window).py_block_on(py)
614 }
615
616 /// Renders this `View` as a row-oriented JSON string.
617 #[pyo3(signature = (**window))]
618 pub fn to_json_string(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
619 self.0.to_json_string(window).py_block_on(py)
620 }
621
622 /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
623 /// formatted `String`.
624 #[pyo3(signature = (**window))]
625 pub fn to_ndjson(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
626 self.0.to_ndjson(window).py_block_on(py)
627 }
628
629 /// Renders this [`View`] as a row-oriented Python `list`.
630 #[pyo3(signature = (**window))]
631 pub fn to_records<'a>(
632 &self,
633 py: Python<'a>,
634 window: Option<Py<PyDict>>,
635 ) -> PyResult<Bound<'a, PyAny>> {
636 let json = self.0.to_json_string(window).py_block_on(py)?;
637 let json_module = PyModule::import(py, "json")?;
638 json_module.call_method1("loads", (json,))
639 }
640
641 /// Renders this [`View`] as a row-oriented Python `list`.
642 #[pyo3(signature = (**window))]
643 pub fn to_json<'a>(
644 &self,
645 py: Python<'a>,
646 window: Option<Py<PyDict>>,
647 ) -> PyResult<Bound<'a, PyAny>> {
648 self.to_records(py, window)
649 }
650
651 /// Renders this [`View`] as a column-oriented Python `dict`.
652 #[pyo3(signature = (**window))]
653 pub fn to_columns<'a>(
654 &self,
655 py: Python<'a>,
656 window: Option<Py<PyDict>>,
657 ) -> PyResult<Bound<'a, PyAny>> {
658 let json = self.0.to_columns_string(window).py_block_on(py)?;
659 let json_module = PyModule::import(py, "json")?;
660 json_module.call_method1("loads", (json,))
661 }
662
663 /// Renders this [`View`] as a CSV `String` in a standard format.
664 #[pyo3(signature = (**window))]
665 pub fn to_csv(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
666 self.0.to_csv(window).py_block_on(py)
667 }
668
669 /// Renders this [`View`] as a `pandas.DataFrame`.
670 #[pyo3(signature = (**window))]
671 // #[deprecated(since="3.2.0", note="Please use `View::to_pandas`")]
672 pub fn to_dataframe(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
673 self.0.to_dataframe(window).py_block_on(py)
674 }
675
676 /// Renders this [`View`] as a `pandas.DataFrame`.
677 #[pyo3(signature = (**window))]
678 pub fn to_pandas(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
679 self.0.to_dataframe(window).py_block_on(py)
680 }
681
682 /// Renders this [`View`] as a `polars.DataFrame`.
683 #[pyo3(signature = (**window))]
684 pub fn to_polars(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
685 self.0.to_polars(window).py_block_on(py)
686 }
687
688 /// Renders this [`View`] as the Apache Arrow data format.
689 ///
690 /// # Arguments
691 ///
692 /// - `window` - a [`ViewWindow`]; its `compression` key selects Arrow IPC
693 /// body compression, `"lz4"` or `"zstd"` (uncompressed when omitted).
694 ///
695 /// # Examples
696 ///
697 /// ```python
698 /// arrow = view.to_arrow(compression="zstd")
699 /// ```
700 #[pyo3(signature = (**window))]
701 pub fn to_arrow(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyBytes>> {
702 self.0.to_arrow(window).py_block_on(py)
703 }
704
705 /// Delete this [`View`] and clean up all resources associated with it.
706 /// [`View`] objects do not stop consuming resources or processing
707 /// updates when they are garbage collected - you must call this method
708 /// to reclaim these.
709 pub fn delete(&self, py: Python<'_>) -> PyResult<()> {
710 self.0.delete().py_block_on(py)
711 }
712
713 pub fn expand(&self, py: Python<'_>, index: u32) -> PyResult<u32> {
714 self.0.expand(index).py_block_on(py)
715 }
716
717 pub fn collapse(&self, py: Python<'_>, index: u32) -> PyResult<u32> {
718 self.0.collapse(index).py_block_on(py)
719 }
720
721 /// Returns this [`View`]'s _dimensions_, row and column count, as well as
722 /// those of the [`crate::Table`] from which it was derived.
723 ///
724 /// - `num_table_rows` - The number of rows in the underlying
725 /// [`crate::Table`].
726 /// - `num_table_columns` - The number of columns in the underlying
727 /// [`crate::Table`] (including the `index` column if this
728 /// [`crate::Table`] was constructed with one).
729 /// - `num_view_rows` - The number of rows in this [`View`]. If this
730 /// [`View`] has a `group_by` clause, `num_view_rows` will also include
731 /// aggregated rows.
732 /// - `num_view_columns` - The number of columns in this [`View`]. If this
733 /// [`View`] has a `split_by` clause, `num_view_columns` will include all
734 /// _column paths_, e.g. the number of `columns` clause times the number
735 /// of `split_by` groups.
736 pub fn dimensions(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
737 self.0.dimensions().py_block_on(py)
738 }
739
740 /// The expression schema of this [`View`], which contains only the
741 /// expressions created on this [`View`]. See [`View::schema`] for
742 /// details.
743 pub fn expression_schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
744 self.0.expression_schema().py_block_on(py)
745 }
746
747 /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
748 /// which created this [`View`].
749 pub fn get_config(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
750 self.0.get_config().py_block_on(py)
751 }
752
753 /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
754 ///
755 /// # Returns
756 ///
757 /// A tuple of [min, max], whose types are column and aggregate dependent.
758 pub fn get_min_max(
759 &self,
760 py: Python<'_>,
761 column_name: String,
762 ) -> PyResult<(Py<PyAny>, Py<PyAny>)> {
763 self.0.get_min_max(column_name).py_block_on(py)
764 }
765
766 /// The number of aggregated rows in this [`View`]. This is affected by the
767 /// "group_by" configuration parameter supplied to this view's contructor.
768 ///
769 /// # Returns
770 ///
771 /// The number of aggregated rows.
772 pub fn num_rows(&self, py: Python<'_>) -> PyResult<u32> {
773 self.0.num_rows().py_block_on(py)
774 }
775
776 /// The number of aggregated columns in this [`View`]. This is affected by
777 /// the "split_by" configuration parameter supplied to this view's
778 /// contructor.
779 ///
780 /// # Returns
781 ///
782 /// The number of aggregated columns.
783 pub fn num_columns(&self, py: Python<'_>) -> PyResult<u32> {
784 self.0.num_columns().py_block_on(py)
785 }
786
787 /// The schema of this [`View`].
788 ///
789 /// The [`View`] schema differs from the `schema` returned by
790 /// [`Table::schema`]; it may have different column names due to
791 /// `expressions` or `columns` configs, or it maye have _different
792 /// column types_ due to the application og `group_by` and `aggregates`
793 /// config. You can think of [`Table::schema`] as the _input_ schema and
794 /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
795 pub fn schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
796 self.0.schema().py_block_on(py)
797 }
798
799 /// Register a callback with this [`View`]. Whenever the [`View`] is
800 /// deleted, this callback will be invoked.
801 pub fn on_delete(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
802 self.0.on_delete(callback).py_block_on(py)
803 }
804
805 /// Unregister a previously registered [`View::on_delete`] callback.
806 pub fn remove_delete(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
807 self.0.remove_delete(callback_id).py_block_on(py)
808 }
809
810 /// Register a callback with this [`View`]. Whenever the view's underlying
811 /// table emits an update, this callback will be invoked with an object
812 /// containing `port_id`, indicating which port the update fired on, and
813 /// optionally `delta`, which is the new data that was updated for each
814 /// cell or each row.
815 ///
816 /// # Arguments
817 ///
818 /// - `on_update` - A callback function invoked on update, which receives an
819 /// object with two keys: `port_id`, indicating which port the update was
820 /// triggered on, and `delta`, whose value is dependent on the mode
821 /// parameter.
822 /// - `options` - If this is provided as `OnUpdateOptions { mode:
823 /// Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
824 /// rows. Otherwise `delta` will be [`Option::None`].
825 #[pyo3(signature = (callback, mode=None))]
826 pub fn on_update(
827 &self,
828 py: Python<'_>,
829 callback: Py<PyAny>,
830 mode: Option<String>,
831 ) -> PyResult<u32> {
832 self.0.on_update(callback, mode).py_block_on(py)
833 }
834
835 /// Unregister a previously registered update callback with this [`View`].
836 ///
837 /// # Arguments
838 ///
839 /// - `id` - A callback `id` as returned by a recipricol call to
840 /// [`View::on_update`].
841 ///
842 /// # Examples
843 ///
844 /// ```rust
845 /// let callback = |_| async { print!("Updated!") };
846 /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
847 /// view.remove_update(cid).await?;
848 /// ```
849 pub fn remove_update(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
850 self.0.remove_update(callback_id).py_block_on(py)
851 }
852
853 /// Register a callback which is invoked whenever rows are removed from
854 /// this [`View`]'s [`Table`] by [`Table::remove`], with two arguments:
855 /// `port_id`,
856 /// and `indices`, the removed `index` column values as an Apache Arrow
857 /// (`bytes`) of one column named after the index.
858 ///
859 /// [`Table::replace`] reports the keys it does not re-supply and
860 /// [`Table::clear`] reports every key. It never fires for a
861 /// [`Table`] without an `index`.
862 ///
863 /// # Python Examples
864 ///
865 /// ```python
866 /// def on_remove(port_id, indices):
867 /// replica.remove(indices)
868 ///
869 /// callback_id = view.on_remove(on_remove)
870 /// ```
871 pub fn on_remove(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
872 self.0.on_remove(callback).py_block_on(py)
873 }
874
875 /// Unregister a previously registered [`View::on_remove`] callback.
876 ///
877 /// # Arguments
878 ///
879 /// - `id` - A callback `id` as returned by a reciprocal call to
880 /// [`View::on_remove`].
881 pub fn remove_remove(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
882 self.0.remove_remove(callback_id).py_block_on(py)
883 }
884}