Skip to main content

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