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;
15
16#[cfg(doc)]
17use perspective_client::{Schema, TableInitOptions, UpdateOptions, config::ViewConfigUpdate};
18use perspective_client::{assert_table_api, assert_view_api};
19use pyo3::exceptions::PyTypeError;
20use pyo3::marker::Ungil;
21use pyo3::prelude::*;
22use pyo3::types::*;
23
24use super::client_async::*;
25use crate::server::Server;
26
27pub(crate) trait PyFutureExt: Future {
28    fn py_block_on(self, py: Python<'_>) -> Self::Output
29    where
30        Self: Sized + Send,
31        Self::Output: Ungil,
32    {
33        use pollster::FutureExt;
34        py.allow_threads(move || self.block_on())
35    }
36}
37
38impl<F: Future> PyFutureExt for F {}
39
40/// An instance of a [`Client`] is a connection to a single [`Server`], whether
41/// locally in-memory or remote over some transport like a WebSocket.
42///
43/// [`Client`] and Perspective objects derived from it have _synchronous_ APIs,
44/// suitable for use in a repl or script context where this is the _only_
45/// [`Client`] connected to its [`Server`]. If you want to
46/// integrate with a Web framework or otherwise connect multiple clients,
47/// use [`AsyncClient`].
48#[pyclass(subclass, module = "perspective")]
49pub struct Client(pub(crate) AsyncClient);
50
51#[pymethods]
52impl Client {
53    #[new]
54    #[pyo3(signature = (handle_request, close_cb=None, name=None))]
55    pub fn new(
56        handle_request: Py<PyAny>,
57        close_cb: Option<Py<PyAny>>,
58        name: Option<String>,
59    ) -> PyResult<Self> {
60        let client = AsyncClient::new(handle_request, close_cb, name)?;
61        Ok(Client(client))
62    }
63
64    /// Create a new [`Client`] instance bound to a specific in-process
65    /// [`Server`] (e.g. generally _not_ the global [`Server`]).
66    #[staticmethod]
67    pub fn from_server(py: Python<'_>, server: Py<Server>) -> PyResult<Self> {
68        server.borrow(py).new_local_client()
69    }
70
71    /// Handle a message from the external message queue.
72    /// [`Client::handle_response`] is part of the low-level message-handling
73    /// API necessary to implement new transports for a [`Client`]
74    /// connection to a local-or-remote [`Server`], and
75    /// doesn't generally need to be called directly by "users" of a
76    /// [`Client`] once connected.
77    pub fn handle_response(&self, py: Python<'_>, response: Py<PyBytes>) -> PyResult<bool> {
78        self.0.handle_response(response).py_block_on(py)
79    }
80
81    /// Creates a new [`Table`] from either a _schema_ or _data_.
82    ///
83    /// The [`Client::table`] factory function can be initialized with either a
84    /// _schema_ (see [`Table::schema`]), or data in one of these formats:
85    ///
86    /// - Apache Arrow
87    /// - CSV
88    /// - JSON row-oriented
89    /// - JSON column-oriented
90    /// - NDJSON
91    ///
92    /// When instantiated with _data_, the schema is inferred from this data.
93    /// While this is convenient, inferrence is sometimes imperfect e.g.
94    /// when the input is empty, null or ambiguous. For these cases,
95    /// [`Client::table`] can first be instantiated with a explicit schema.
96    ///
97    /// When instantiated with a _schema_, the resulting [`Table`] is empty but
98    /// with known column names and column types. When subsqeuently
99    /// populated with [`Table::update`], these columns will be _coerced_ to
100    /// the schema's type. This behavior can be useful when
101    /// [`Client::table`]'s column type inferences doesn't work.
102    ///
103    /// The resulting [`Table`] is _virtual_, and invoking its methods
104    /// dispatches events to the `perspective_server::Server` this
105    /// [`Client`] connects to, where the data is stored and all calculation
106    /// occurs.
107    ///
108    /// # Arguments
109    ///
110    /// - `arg` - Either _schema_ or initialization _data_.
111    /// - `options` - Optional configuration which provides one of:
112    ///     - `limit` - The max number of rows the resulting [`Table`] can
113    ///       store.
114    ///     - `index` - The column name to use as an _index_ column. If this
115    ///       `Table` is being instantiated by _data_, this column name must be
116    ///       present in the data.
117    ///     - `name` - The name of the table. This will be generated if it is
118    ///       not provided.
119    ///     - `format` - The explicit format of the input data, can be one of
120    ///       `"json"`, `"columns"`, `"csv"` or `"arrow"`. This overrides
121    ///       language-specific type dispatch behavior, which allows stringified
122    ///       and byte array alternative inputs.
123    ///
124    /// # Python Examples
125    ///
126    /// Load a CSV from a `str`:
127    ///
128    /// ```python
129    /// table = client.table("x,y\n1,2\n3,4")
130    /// ```
131    #[pyo3(signature = (input, limit=None, index=None, name=None, format=None))]
132    pub fn table(
133        &self,
134        py: Python<'_>,
135        input: Py<PyAny>,
136        limit: Option<u32>,
137        index: Option<Py<PyString>>,
138        name: Option<Py<PyString>>,
139        format: Option<Py<PyString>>,
140    ) -> PyResult<Table> {
141        Ok(Table(
142            self.0
143                .table(input, limit, index, name, format)
144                .py_block_on(py)?,
145        ))
146    }
147
148    /// Opens a [`Table`] that is hosted on the `perspective_server::Server`
149    /// that is connected to this [`Client`].
150    ///
151    /// The `name` property of [`TableInitOptions`] is used to identify each
152    /// [`Table`]. [`Table`] `name`s can be looked up for each [`Client`]
153    /// via [`Client::get_hosted_table_names`].
154    ///
155    /// # Python Examples
156    ///
157    /// ```python
158    /// table =  client.open_table("table_one");
159    /// ```
160    pub fn open_table(&self, py: Python<'_>, name: String) -> PyResult<Table> {
161        let client = self.0.clone();
162        let table = client.open_table(name).py_block_on(py)?;
163        Ok(Table(table))
164    }
165
166    /// Retrieves the names of all tables that this client has access to.
167    ///
168    /// `name` is a string identifier unique to the [`Table`] (per [`Client`]),
169    /// which can be used in conjunction with [`Client::open_table`] to get
170    /// a [`Table`] instance without the use of [`Client::table`]
171    /// constructor directly (e.g., one created by another [`Client`]).
172    ///
173    /// # Python Examples
174    ///
175    /// ```python
176    /// tables = client.get_hosted_table_names();
177    /// ```
178    pub fn get_hosted_table_names(&self, py: Python<'_>) -> PyResult<Vec<String>> {
179        self.0.get_hosted_table_names().py_block_on(py)
180    }
181
182    /// Register a callback which is invoked whenever [`Client::table`] (on this
183    /// [`Client`]) or [`Table::delete`] (on a [`Table`] belinging to this
184    /// [`Client`]) are called.
185    pub fn on_hosted_tables_update(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
186        self.0.on_hosted_tables_update(callback).py_block_on(py)
187    }
188
189    /// Remove a callback previously registered via
190    /// [`Client::on_hosted_tables_update`].
191    pub fn remove_hosted_tables_update(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
192        self.0
193            .remove_hosted_tables_update(callback_id)
194            .py_block_on(py)
195    }
196
197    /// Terminates this [`Client`], cleaning up any [`View`] handles the
198    /// [`Client`] has open as well as its callbacks.
199    pub fn terminate(&self, py: Python<'_>) -> PyResult<()> {
200        self.0.terminate(py)
201    }
202}
203
204#[pyclass(subclass, name = "Table", module = "perspective")]
205pub struct Table(AsyncTable);
206
207assert_table_api!(Table);
208
209#[pymethods]
210impl Table {
211    #[new]
212    fn new() -> PyResult<Self> {
213        Err(PyTypeError::new_err(
214            "Do not call Table's constructor directly, construct from a Client instance.",
215        ))
216    }
217
218    /// Returns the name of the index column for the table.
219    ///
220    /// # Python Examples
221    ///
222    /// ```python
223    /// table = perspective.table("x,y\n1,2\n3,4", index="x");
224    /// index = client.get_index()
225    /// ```
226    pub fn get_index(&self) -> Option<String> {
227        self.0.get_index()
228    }
229
230    /// Get a copy of the [`Client`] this [`Table`] came from.
231    pub fn get_client(&self, py: Python<'_>) -> Client {
232        Client(self.0.get_client().py_block_on(py))
233    }
234
235    /// Returns the user-specified row limit for this table.
236    pub fn get_limit(&self) -> Option<u32> {
237        self.0.get_limit()
238    }
239
240    /// Returns the user-specified name for this table, or the auto-generated
241    /// name if a name was not specified when the table was created.
242    pub fn get_name(&self) -> String {
243        self.0.get_name()
244    }
245
246    /// Removes all the rows in the [`Table`], but preserves everything else
247    /// including the schema, index, and any callbacks or registered
248    /// [`View`] instances.
249    ///
250    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
251    /// will trigger an update event to any registered listeners via
252    /// [`View::on_update`].
253    pub fn clear(&self, py: Python<'_>) -> PyResult<()> {
254        self.0.clear().py_block_on(py)
255    }
256
257    /// Returns the column names of this [`Table`] in "natural" order (the
258    /// ordering implied by the input format).
259    ///  
260    ///  # Python Examples
261    ///
262    /// ```python
263    /// columns = table.columns()
264    /// ```
265    pub fn columns(&self, py: Python<'_>) -> PyResult<Vec<String>> {
266        self.0.columns().py_block_on(py)
267    }
268
269    /// Delete this [`Table`] and cleans up associated resources.
270    ///
271    /// [`Table`]s do not stop consuming resources or processing updates when
272    /// they are garbage collected in their host language - you must call
273    /// this method to reclaim these.
274    ///
275    /// # Arguments
276    ///
277    /// - `options` An options dictionary.
278    ///     - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
279    ///       default), the delete will occur immediately, assuming it has no
280    ///       [`View`] instances registered to it (which must be deleted first,
281    ///       otherwise this method will throw an error). When true, the
282    ///       [`Table`] will only be marked for deltion once its [`View`]
283    ///       dependency count reaches 0.
284    ///
285    /// # Python Examples
286    ///
287    /// ```python
288    /// table = client.table("x,y\n1,2\n3,4")
289    ///
290    /// # ...
291    ///
292    /// table.delete(lazy=True)
293    /// ```
294    #[pyo3(signature=(lazy=false))]
295    pub fn delete(&self, py: Python<'_>, lazy: bool) -> PyResult<()> {
296        self.0.delete(lazy).py_block_on(py)
297    }
298
299    /// Create a unique channel ID on this [`Table`], which allows
300    /// `View::on_update` callback calls to be associated with the
301    /// `Table::update` which caused them.
302    pub fn make_port(&self, py: Python<'_>) -> PyResult<i32> {
303        let table = self.0.clone();
304        table.make_port().py_block_on(py)
305    }
306
307    /// Register a callback which is called exactly once, when this [`Table`] is
308    /// deleted with the [`Table::delete`] method.
309    ///
310    /// [`Table::on_delete`] resolves when the subscription message is sent, not
311    /// when the _delete_ event occurs.
312    pub fn on_delete(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
313        let table = self.0.clone();
314        table.on_delete(callback).py_block_on(py)
315    }
316
317    #[pyo3(signature = (input, format=None))]
318    pub fn remove(&self, py: Python<'_>, input: Py<PyAny>, format: Option<String>) -> PyResult<()> {
319        let table = self.0.clone();
320        table.remove(input, format).py_block_on(py)
321    }
322
323    /// Removes a listener with a given ID, as returned by a previous call to
324    /// [`Table::on_delete`].
325    pub fn remove_delete(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
326        let table = self.0.clone();
327        table.remove_delete(callback_id).py_block_on(py)
328    }
329
330    /// Returns a table's [`Schema`], a mapping of column names to column types.
331    ///
332    /// The mapping of a [`Table`]'s column names to data types is referred to
333    /// as a [`Schema`]. Each column has a unique name and a data type, one
334    /// of:
335    ///
336    /// - `"boolean"` - A boolean type
337    /// - `"date"` - A timesonze-agnostic date type (month/day/year)
338    /// - `"datetime"` - A millisecond-precision datetime type in the UTC
339    ///   timezone
340    /// - `"float"` - A 64 bit float
341    /// - `"integer"` - A signed 32 bit integer (the integer type supported by
342    ///   JavaScript)
343    /// - `"string"` - A `String` data type (encoded internally as a
344    ///   _dictionary_)
345    ///
346    /// Note that all [`Table`] columns are _nullable_, regardless of the data
347    /// type.
348    pub fn schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
349        let table = self.0.clone();
350        table.schema().py_block_on(py)
351    }
352
353    /// Validates the given expressions.
354    pub fn validate_expressions(
355        &self,
356        py: Python<'_>,
357        expression: Py<PyAny>,
358    ) -> PyResult<Py<PyAny>> {
359        let table = self.0.clone();
360        table.validate_expressions(expression).py_block_on(py)
361    }
362
363    /// Create a new [`View`] from this table with a specified
364    /// [`ViewConfigUpdate`].
365    ///
366    /// See [`View`] struct.
367    ///
368    /// # Examples
369    ///
370    /// ```python
371    /// view view = table.view(
372    ///     columns=["Sales"],
373    ///     aggregates={"Sales": "sum"},
374    ///     group_by=["Region", "State"],
375    /// )
376    /// ```
377    #[pyo3(signature = (**config))]
378    pub fn view(&self, py: Python<'_>, config: Option<Py<PyDict>>) -> PyResult<View> {
379        Ok(View(self.0.view(config).py_block_on(py)?))
380    }
381
382    /// Returns the number of rows in a [`Table`].
383    pub fn size(&self, py: Python<'_>) -> PyResult<usize> {
384        self.0.size().py_block_on(py)
385    }
386
387    /// Removes all the rows in the [`Table`], but preserves everything else
388    /// including the schema, index, and any callbacks or registered
389    /// [`View`] instances.
390    ///
391    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
392    /// will trigger an update event to any registered listeners via
393    /// [`View::on_update`].
394    #[pyo3(signature = (input, format=None))]
395    pub fn replace(
396        &self,
397        py: Python<'_>,
398        input: Py<PyAny>,
399        format: Option<String>,
400    ) -> PyResult<()> {
401        self.0.replace(input, format).py_block_on(py)
402    }
403
404    /// Updates the rows of this table and any derived [`View`] instances.
405    ///
406    /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
407    /// register to derived [`View`], and the call itself will not resolve until
408    /// _all_ derived [`View`]'s are notified.
409    ///
410    /// When updating a [`Table`] with an `index`, [`Table::update`] supports
411    /// partial updates, by omitting columns from the update data.
412    ///
413    /// # Arguments
414    ///
415    /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
416    ///   is immutable after creation, so this method cannot be called with a
417    ///   schema.
418    /// - `options` - Options for this update step - see
419    ///   [`perspective_client::UpdateOptions`].
420    /// ```  
421    #[pyo3(signature = (input, port_id=None, format=None))]
422    pub fn update(
423        &self,
424        py: Python<'_>,
425        input: Py<PyAny>,
426        port_id: Option<u32>,
427        format: Option<String>,
428    ) -> PyResult<()> {
429        self.0.update(input, port_id, format).py_block_on(py)
430    }
431}
432
433/// The [`View`] struct is Perspective's query and serialization interface. It
434/// represents a query on the `Table`'s dataset and is always created from an
435/// existing `Table` instance via the [`Table::view`] method.
436///
437/// [`View`]s are immutable with respect to the arguments provided to the
438/// [`Table::view`] method; to change these parameters, you must create a new
439/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
440/// respect to the [`Table`]'s data, and will (within a conflation window)
441/// update with the latest state as its parent [`Table`] updates, including
442/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
443/// query parameters are composable, in that each parameter works independently
444/// _and_ in conjunction with each other, and there is no limit to the number of
445/// pivots, filters, etc. which can be applied.
446///
447/// To construct a [`View`], call the [`Table::view`] factory method. A
448/// [`Table`] can have as many [`View`]s associated with it as you need -
449/// Perspective conserves memory by relying on a single [`Table`] to power
450/// multiple [`View`]s concurrently.
451#[pyclass(subclass, name = "View", module = "perspective")]
452pub struct View(AsyncView);
453
454assert_view_api!(View);
455
456#[pymethods]
457impl View {
458    #[new]
459    fn new() -> PyResult<Self> {
460        Err(PyTypeError::new_err(
461            "Do not call View's constructor directly, construct from a Table instance.",
462        ))
463    }
464
465    /// Returns an array of strings containing the column paths of the [`View`]
466    /// without any of the source columns.
467    ///
468    /// A column path shows the columns that a given cell belongs to after
469    /// pivots are applied.
470    pub fn column_paths(&self, py: Python<'_>) -> PyResult<Vec<String>> {
471        self.0.column_paths().py_block_on(py)
472    }
473
474    /// Renders this [`View`] as a column-oriented JSON string. Useful if you
475    /// want to save additional round trip serialize/deserialize cycles.  
476    #[pyo3(signature = (**window))]
477    pub fn to_columns_string(
478        &self,
479        py: Python<'_>,
480        window: Option<Py<PyDict>>,
481    ) -> PyResult<String> {
482        self.0.to_columns_string(window).py_block_on(py)
483    }
484
485    /// Renders this `View` as a row-oriented JSON string.
486    #[pyo3(signature = (**window))]
487    pub fn to_json_string(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
488        self.0.to_json_string(window).py_block_on(py)
489    }
490
491    /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
492    /// formatted `String`.
493    #[pyo3(signature = (**window))]
494    pub fn to_ndjson(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
495        self.0.to_ndjson(window).py_block_on(py)
496    }
497
498    /// Renders this [`View`] as a row-oriented Python `list`.
499    #[pyo3(signature = (**window))]
500    pub fn to_records<'a>(
501        &self,
502        py: Python<'a>,
503        window: Option<Py<PyDict>>,
504    ) -> PyResult<Bound<'a, PyAny>> {
505        let json = self.0.to_json_string(window).py_block_on(py)?;
506        let json_module = PyModule::import(py, "json")?;
507        json_module.call_method1("loads", (json,))
508    }
509
510    /// Renders this [`View`] as a row-oriented Python `list`.
511    #[pyo3(signature = (**window))]
512    pub fn to_json<'a>(
513        &self,
514        py: Python<'a>,
515        window: Option<Py<PyDict>>,
516    ) -> PyResult<Bound<'a, PyAny>> {
517        self.to_records(py, window)
518    }
519
520    /// Renders this [`View`] as a column-oriented Python `dict`.
521    #[pyo3(signature = (**window))]
522    pub fn to_columns<'a>(
523        &self,
524        py: Python<'a>,
525        window: Option<Py<PyDict>>,
526    ) -> PyResult<Bound<'a, PyAny>> {
527        let json = self.0.to_columns_string(window).py_block_on(py)?;
528        let json_module = PyModule::import(py, "json")?;
529        json_module.call_method1("loads", (json,))
530    }
531
532    /// Renders this [`View`] as a CSV `String` in a standard format.
533    #[pyo3(signature = (**window))]
534    pub fn to_csv(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
535        self.0.to_csv(window).py_block_on(py)
536    }
537
538    /// Renders this [`View`] as a `pandas.DataFrame`.
539    #[pyo3(signature = (**window))]
540    // #[deprecated(since="3.2.0", note="Please use `View::to_pandas`")]
541    pub fn to_dataframe(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
542        self.0.to_dataframe(window).py_block_on(py)
543    }
544
545    /// Renders this [`View`] as a `pandas.DataFrame`.
546    #[pyo3(signature = (**window))]
547    pub fn to_pandas(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
548        self.0.to_dataframe(window).py_block_on(py)
549    }
550
551    /// Renders this [`View`] as a `polars.DataFrame`.
552    #[pyo3(signature = (**window))]
553    pub fn to_polars(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
554        self.0.to_polars(window).py_block_on(py)
555    }
556
557    /// Renders this [`View`] as the Apache Arrow data format.
558    ///
559    /// # Arguments
560    ///
561    /// - `window` - a [`ViewWindow`]
562    #[pyo3(signature = (**window))]
563    pub fn to_arrow(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyBytes>> {
564        self.0.to_arrow(window).py_block_on(py)
565    }
566
567    /// Delete this [`View`] and clean up all resources associated with it.
568    /// [`View`] objects do not stop consuming resources or processing
569    /// updates when they are garbage collected - you must call this method
570    /// to reclaim these.
571    pub fn delete(&self, py: Python<'_>) -> PyResult<()> {
572        self.0.delete().py_block_on(py)
573    }
574
575    pub fn expand(&self, py: Python<'_>, index: u32) -> PyResult<u32> {
576        self.0.expand(index).py_block_on(py)
577    }
578
579    pub fn collapse(&self, py: Python<'_>, index: u32) -> PyResult<u32> {
580        self.0.collapse(index).py_block_on(py)
581    }
582
583    /// Returns this [`View`]'s _dimensions_, row and column count, as well as
584    /// those of the [`crate::Table`] from which it was derived.
585    ///
586    /// - `num_table_rows` - The number of rows in the underlying
587    ///   [`crate::Table`].
588    /// - `num_table_columns` - The number of columns in the underlying
589    ///   [`crate::Table`] (including the `index` column if this
590    ///   [`crate::Table`] was constructed with one).
591    /// - `num_view_rows` - The number of rows in this [`View`]. If this
592    ///   [`View`] has a `group_by` clause, `num_view_rows` will also include
593    ///   aggregated rows.
594    /// - `num_view_columns` - The number of columns in this [`View`]. If this
595    ///   [`View`] has a `split_by` clause, `num_view_columns` will include all
596    ///   _column paths_, e.g. the number of `columns` clause times the number
597    ///   of `split_by` groups.
598    pub fn dimensions(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
599        self.0.dimensions().py_block_on(py)
600    }
601
602    /// The expression schema of this [`View`], which contains only the
603    /// expressions created on this [`View`]. See [`View::schema`] for
604    /// details.
605    pub fn expression_schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
606        self.0.expression_schema().py_block_on(py)
607    }
608
609    /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
610    /// which created this [`View`].
611    pub fn get_config(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
612        self.0.get_config().py_block_on(py)
613    }
614
615    /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
616    ///
617    /// # Returns
618    ///
619    /// A tuple of [min, max], whose types are column and aggregate dependent.
620    pub fn get_min_max(&self, py: Python<'_>, column_name: String) -> PyResult<(String, String)> {
621        self.0.get_min_max(column_name).py_block_on(py)
622    }
623
624    /// The number of aggregated rows in this [`View`]. This is affected by the
625    /// "group_by" configuration parameter supplied to this view's contructor.
626    ///
627    /// # Returns
628    ///
629    /// The number of aggregated rows.
630    pub fn num_rows(&self, py: Python<'_>) -> PyResult<u32> {
631        self.0.num_rows().py_block_on(py)
632    }
633
634    /// The schema of this [`View`].
635    ///
636    /// The [`View`] schema differs from the `schema` returned by
637    /// [`Table::schema`]; it may have different column names due to
638    /// `expressions` or `columns` configs, or it maye have _different
639    /// column types_ due to the application og `group_by` and `aggregates`
640    /// config. You can think of [`Table::schema`] as the _input_ schema and
641    /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
642    pub fn schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
643        self.0.schema().py_block_on(py)
644    }
645
646    /// Register a callback with this [`View`]. Whenever the [`View`] is
647    /// deleted, this callback will be invoked.
648    pub fn on_delete(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
649        self.0.on_delete(callback).py_block_on(py)
650    }
651
652    /// Unregister a previously registered [`View::on_delete`] callback.
653    pub fn remove_delete(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
654        self.0.remove_delete(callback_id).py_block_on(py)
655    }
656
657    /// Register a callback with this [`View`]. Whenever the view's underlying
658    /// table emits an update, this callback will be invoked with an object
659    /// containing `port_id`, indicating which port the update fired on, and
660    /// optionally `delta`, which is the new data that was updated for each
661    /// cell or each row.
662    ///
663    /// # Arguments
664    ///
665    /// - `on_update` - A callback function invoked on update, which receives an
666    ///   object with two keys: `port_id`, indicating which port the update was
667    ///   triggered on, and `delta`, whose value is dependent on the mode
668    ///   parameter.
669    /// - `options` - If this is provided as `OnUpdateOptions { mode:
670    ///   Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
671    ///   rows. Otherwise `delta` will be [`Option::None`].
672    #[pyo3(signature = (callback, mode=None))]
673    pub fn on_update(
674        &self,
675        py: Python<'_>,
676        callback: Py<PyAny>,
677        mode: Option<String>,
678    ) -> PyResult<u32> {
679        self.0.on_update(callback, mode).py_block_on(py)
680    }
681
682    /// Unregister a previously registered update callback with this [`View`].
683    ///
684    /// # Arguments
685    ///
686    /// - `id` - A callback `id` as returned by a recipricol call to
687    ///   [`View::on_update`].
688    ///
689    /// # Examples
690    ///
691    /// ```rust
692    /// let callback = |_| async { print!("Updated!") };
693    /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
694    /// view.remove_update(cid).await?;
695    /// ```
696    pub fn remove_update(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
697        self.0.remove_update(callback_id).py_block_on(py)
698    }
699}