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::{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 /// Provides the [`SystemInfo`] struct, implementation-specific metadata
198 /// about the [`perspective_server::Server`] runtime such as Memory and
199 /// CPU usage.
200 pub fn system_info(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
201 self.0.system_info().py_block_on(py)
202 }
203
204 /// Terminates this [`Client`], cleaning up any [`View`] handles the
205 /// [`Client`] has open as well as its callbacks.
206 pub fn terminate(&self, py: Python<'_>) -> PyResult<()> {
207 self.0.terminate(py)
208 }
209}
210
211/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
212/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
213/// by index, and update notifications.
214///
215/// A [`Table`] contains columns, each of which have a unique name, are strongly
216/// and consistently typed, and contains rows of data conforming to the column's
217/// type. Each column in a [`Table`] must have the same number of rows, though
218/// not every row must contain data; null-values are used to indicate missing
219/// values in the dataset. The schema of a [`Table`] is _immutable after
220/// creation_, which means the column names and data types cannot be changed
221/// after the [`Table`] has been created. Columns cannot be added or deleted
222/// after creation either, but a [`View`] can be used to select an arbitrary set
223/// of columns from the [`Table`].
224#[pyclass(subclass, name = "Table", module = "perspective")]
225pub struct Table(AsyncTable);
226
227assert_table_api!(Table);
228
229#[pymethods]
230impl Table {
231 #[new]
232 fn new() -> PyResult<Self> {
233 Err(PyTypeError::new_err(
234 "Do not call Table's constructor directly, construct from a Client instance.",
235 ))
236 }
237
238 /// Returns the name of the index column for the table.
239 ///
240 /// # Python Examples
241 ///
242 /// ```python
243 /// table = perspective.table("x,y\n1,2\n3,4", index="x");
244 /// index = client.get_index()
245 /// ```
246 pub fn get_index(&self) -> Option<String> {
247 self.0.get_index()
248 }
249
250 /// Get a copy of the [`Client`] this [`Table`] came from.
251 pub fn get_client(&self, py: Python<'_>) -> Client {
252 Client(self.0.get_client().py_block_on(py))
253 }
254
255 /// Returns the user-specified row limit for this table.
256 pub fn get_limit(&self) -> Option<u32> {
257 self.0.get_limit()
258 }
259
260 /// Returns the user-specified name for this table, or the auto-generated
261 /// name if a name was not specified when the table was created.
262 pub fn get_name(&self) -> String {
263 self.0.get_name()
264 }
265
266 /// Removes all the rows in the [`Table`], but preserves everything else
267 /// including the schema, index, and any callbacks or registered
268 /// [`View`] instances.
269 ///
270 /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
271 /// will trigger an update event to any registered listeners via
272 /// [`View::on_update`].
273 pub fn clear(&self, py: Python<'_>) -> PyResult<()> {
274 self.0.clear().py_block_on(py)
275 }
276
277 /// Returns the column names of this [`Table`] in "natural" order (the
278 /// ordering implied by the input format).
279 ///
280 /// # Python Examples
281 ///
282 /// ```python
283 /// columns = table.columns()
284 /// ```
285 pub fn columns(&self, py: Python<'_>) -> PyResult<Vec<String>> {
286 self.0.columns().py_block_on(py)
287 }
288
289 /// Delete this [`Table`] and cleans up associated resources.
290 ///
291 /// [`Table`]s do not stop consuming resources or processing updates when
292 /// they are garbage collected in their host language - you must call
293 /// this method to reclaim these.
294 ///
295 /// # Arguments
296 ///
297 /// - `options` An options dictionary.
298 /// - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
299 /// default), the delete will occur immediately, assuming it has no
300 /// [`View`] instances registered to it (which must be deleted first,
301 /// otherwise this method will throw an error). When true, the
302 /// [`Table`] will only be marked for deltion once its [`View`]
303 /// dependency count reaches 0.
304 ///
305 /// # Python Examples
306 ///
307 /// ```python
308 /// table = client.table("x,y\n1,2\n3,4")
309 ///
310 /// # ...
311 ///
312 /// table.delete(lazy=True)
313 /// ```
314 #[pyo3(signature=(lazy=false))]
315 pub fn delete(&self, py: Python<'_>, lazy: bool) -> PyResult<()> {
316 self.0.delete(lazy).py_block_on(py)
317 }
318
319 /// Create a unique channel ID on this [`Table`], which allows
320 /// `View::on_update` callback calls to be associated with the
321 /// `Table::update` which caused them.
322 pub fn make_port(&self, py: Python<'_>) -> PyResult<i32> {
323 let table = self.0.clone();
324 table.make_port().py_block_on(py)
325 }
326
327 /// Register a callback which is called exactly once, when this [`Table`] is
328 /// deleted with the [`Table::delete`] method.
329 ///
330 /// [`Table::on_delete`] resolves when the subscription message is sent, not
331 /// when the _delete_ event occurs.
332 pub fn on_delete(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
333 let table = self.0.clone();
334 table.on_delete(callback).py_block_on(py)
335 }
336
337 #[pyo3(signature = (input, format=None))]
338 pub fn remove(&self, py: Python<'_>, input: Py<PyAny>, format: Option<String>) -> PyResult<()> {
339 let table = self.0.clone();
340 table.remove(input, format).py_block_on(py)
341 }
342
343 /// Removes a listener with a given ID, as returned by a previous call to
344 /// [`Table::on_delete`].
345 pub fn remove_delete(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
346 let table = self.0.clone();
347 table.remove_delete(callback_id).py_block_on(py)
348 }
349
350 /// Returns a table's [`Schema`], a mapping of column names to column types.
351 ///
352 /// The mapping of a [`Table`]'s column names to data types is referred to
353 /// as a [`Schema`]. Each column has a unique name and a data type, one
354 /// of:
355 ///
356 /// - `"boolean"` - A boolean type
357 /// - `"date"` - A timesonze-agnostic date type (month/day/year)
358 /// - `"datetime"` - A millisecond-precision datetime type in the UTC
359 /// timezone
360 /// - `"float"` - A 64 bit float
361 /// - `"integer"` - A signed 32 bit integer (the integer type supported by
362 /// JavaScript)
363 /// - `"string"` - A `String` data type (encoded internally as a
364 /// _dictionary_)
365 ///
366 /// Note that all [`Table`] columns are _nullable_, regardless of the data
367 /// type.
368 pub fn schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
369 let table = self.0.clone();
370 table.schema().py_block_on(py)
371 }
372
373 /// Validates the given expressions.
374 pub fn validate_expressions(
375 &self,
376 py: Python<'_>,
377 expression: Py<PyAny>,
378 ) -> PyResult<Py<PyAny>> {
379 let table = self.0.clone();
380 table.validate_expressions(expression).py_block_on(py)
381 }
382
383 /// Create a new [`View`] from this table with a specified
384 /// [`ViewConfigUpdate`].
385 ///
386 /// See [`View`] struct.
387 ///
388 /// # Examples
389 ///
390 /// ```python
391 /// view view = table.view(
392 /// columns=["Sales"],
393 /// aggregates={"Sales": "sum"},
394 /// group_by=["Region", "State"],
395 /// )
396 /// ```
397 #[pyo3(signature = (**config))]
398 pub fn view(&self, py: Python<'_>, config: Option<Py<PyDict>>) -> PyResult<View> {
399 Ok(View(self.0.view(config).py_block_on(py)?))
400 }
401
402 /// Returns the number of rows in a [`Table`].
403 pub fn size(&self, py: Python<'_>) -> PyResult<usize> {
404 self.0.size().py_block_on(py)
405 }
406
407 /// Removes all the rows in the [`Table`], but preserves everything else
408 /// including the schema, index, and any callbacks or registered
409 /// [`View`] instances.
410 ///
411 /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
412 /// will trigger an update event to any registered listeners via
413 /// [`View::on_update`].
414 #[pyo3(signature = (input, format=None))]
415 pub fn replace(
416 &self,
417 py: Python<'_>,
418 input: Py<PyAny>,
419 format: Option<String>,
420 ) -> PyResult<()> {
421 self.0.replace(input, format).py_block_on(py)
422 }
423
424 /// Updates the rows of this table and any derived [`View`] instances.
425 ///
426 /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
427 /// register to derived [`View`], and the call itself will not resolve until
428 /// _all_ derived [`View`]'s are notified.
429 ///
430 /// When updating a [`Table`] with an `index`, [`Table::update`] supports
431 /// partial updates, by omitting columns from the update data.
432 ///
433 /// # Arguments
434 ///
435 /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
436 /// is immutable after creation, so this method cannot be called with a
437 /// schema.
438 /// - `options` - Options for this update step - see
439 /// [`perspective_client::UpdateOptions`].
440 /// ```
441 #[pyo3(signature = (input, port_id=None, format=None))]
442 pub fn update(
443 &self,
444 py: Python<'_>,
445 input: Py<PyAny>,
446 port_id: Option<u32>,
447 format: Option<String>,
448 ) -> PyResult<()> {
449 self.0.update(input, port_id, format).py_block_on(py)
450 }
451}
452
453/// The [`View`] struct is Perspective's query and serialization interface. It
454/// represents a query on the `Table`'s dataset and is always created from an
455/// existing `Table` instance via the [`Table::view`] method.
456///
457/// [`View`]s are immutable with respect to the arguments provided to the
458/// [`Table::view`] method; to change these parameters, you must create a new
459/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
460/// respect to the [`Table`]'s data, and will (within a conflation window)
461/// update with the latest state as its parent [`Table`] updates, including
462/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
463/// query parameters are composable, in that each parameter works independently
464/// _and_ in conjunction with each other, and there is no limit to the number of
465/// pivots, filters, etc. which can be applied.
466///
467/// To construct a [`View`], call the [`Table::view`] factory method. A
468/// [`Table`] can have as many [`View`]s associated with it as you need -
469/// Perspective conserves memory by relying on a single [`Table`] to power
470/// multiple [`View`]s concurrently.
471#[pyclass(subclass, name = "View", module = "perspective")]
472pub struct View(AsyncView);
473
474assert_view_api!(View);
475
476#[pymethods]
477impl View {
478 #[new]
479 fn new() -> PyResult<Self> {
480 Err(PyTypeError::new_err(
481 "Do not call View's constructor directly, construct from a Table instance.",
482 ))
483 }
484
485 /// Returns an array of strings containing the column paths of the [`View`]
486 /// without any of the source columns.
487 ///
488 /// A column path shows the columns that a given cell belongs to after
489 /// pivots are applied.
490 #[pyo3(signature = (**window))]
491 pub fn column_paths(
492 &self,
493 py: Python<'_>,
494 window: Option<Py<PyDict>>,
495 ) -> PyResult<Vec<String>> {
496 self.0.column_paths(window).py_block_on(py)
497 }
498
499 /// Renders this [`View`] as a column-oriented JSON string. Useful if you
500 /// want to save additional round trip serialize/deserialize cycles.
501 #[pyo3(signature = (**window))]
502 pub fn to_columns_string(
503 &self,
504 py: Python<'_>,
505 window: Option<Py<PyDict>>,
506 ) -> PyResult<String> {
507 self.0.to_columns_string(window).py_block_on(py)
508 }
509
510 /// Renders this `View` as a row-oriented JSON string.
511 #[pyo3(signature = (**window))]
512 pub fn to_json_string(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
513 self.0.to_json_string(window).py_block_on(py)
514 }
515
516 /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
517 /// formatted `String`.
518 #[pyo3(signature = (**window))]
519 pub fn to_ndjson(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
520 self.0.to_ndjson(window).py_block_on(py)
521 }
522
523 /// Renders this [`View`] as a row-oriented Python `list`.
524 #[pyo3(signature = (**window))]
525 pub fn to_records<'a>(
526 &self,
527 py: Python<'a>,
528 window: Option<Py<PyDict>>,
529 ) -> PyResult<Bound<'a, PyAny>> {
530 let json = self.0.to_json_string(window).py_block_on(py)?;
531 let json_module = PyModule::import(py, "json")?;
532 json_module.call_method1("loads", (json,))
533 }
534
535 /// Renders this [`View`] as a row-oriented Python `list`.
536 #[pyo3(signature = (**window))]
537 pub fn to_json<'a>(
538 &self,
539 py: Python<'a>,
540 window: Option<Py<PyDict>>,
541 ) -> PyResult<Bound<'a, PyAny>> {
542 self.to_records(py, window)
543 }
544
545 /// Renders this [`View`] as a column-oriented Python `dict`.
546 #[pyo3(signature = (**window))]
547 pub fn to_columns<'a>(
548 &self,
549 py: Python<'a>,
550 window: Option<Py<PyDict>>,
551 ) -> PyResult<Bound<'a, PyAny>> {
552 let json = self.0.to_columns_string(window).py_block_on(py)?;
553 let json_module = PyModule::import(py, "json")?;
554 json_module.call_method1("loads", (json,))
555 }
556
557 /// Renders this [`View`] as a CSV `String` in a standard format.
558 #[pyo3(signature = (**window))]
559 pub fn to_csv(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<String> {
560 self.0.to_csv(window).py_block_on(py)
561 }
562
563 /// Renders this [`View`] as a `pandas.DataFrame`.
564 #[pyo3(signature = (**window))]
565 // #[deprecated(since="3.2.0", note="Please use `View::to_pandas`")]
566 pub fn to_dataframe(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
567 self.0.to_dataframe(window).py_block_on(py)
568 }
569
570 /// Renders this [`View`] as a `pandas.DataFrame`.
571 #[pyo3(signature = (**window))]
572 pub fn to_pandas(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
573 self.0.to_dataframe(window).py_block_on(py)
574 }
575
576 /// Renders this [`View`] as a `polars.DataFrame`.
577 #[pyo3(signature = (**window))]
578 pub fn to_polars(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyAny>> {
579 self.0.to_polars(window).py_block_on(py)
580 }
581
582 /// Renders this [`View`] as the Apache Arrow data format.
583 ///
584 /// # Arguments
585 ///
586 /// - `window` - a [`ViewWindow`]
587 #[pyo3(signature = (**window))]
588 pub fn to_arrow(&self, py: Python<'_>, window: Option<Py<PyDict>>) -> PyResult<Py<PyBytes>> {
589 self.0.to_arrow(window).py_block_on(py)
590 }
591
592 /// Delete this [`View`] and clean up all resources associated with it.
593 /// [`View`] objects do not stop consuming resources or processing
594 /// updates when they are garbage collected - you must call this method
595 /// to reclaim these.
596 pub fn delete(&self, py: Python<'_>) -> PyResult<()> {
597 self.0.delete().py_block_on(py)
598 }
599
600 pub fn expand(&self, py: Python<'_>, index: u32) -> PyResult<u32> {
601 self.0.expand(index).py_block_on(py)
602 }
603
604 pub fn collapse(&self, py: Python<'_>, index: u32) -> PyResult<u32> {
605 self.0.collapse(index).py_block_on(py)
606 }
607
608 /// Returns this [`View`]'s _dimensions_, row and column count, as well as
609 /// those of the [`crate::Table`] from which it was derived.
610 ///
611 /// - `num_table_rows` - The number of rows in the underlying
612 /// [`crate::Table`].
613 /// - `num_table_columns` - The number of columns in the underlying
614 /// [`crate::Table`] (including the `index` column if this
615 /// [`crate::Table`] was constructed with one).
616 /// - `num_view_rows` - The number of rows in this [`View`]. If this
617 /// [`View`] has a `group_by` clause, `num_view_rows` will also include
618 /// aggregated rows.
619 /// - `num_view_columns` - The number of columns in this [`View`]. If this
620 /// [`View`] has a `split_by` clause, `num_view_columns` will include all
621 /// _column paths_, e.g. the number of `columns` clause times the number
622 /// of `split_by` groups.
623 pub fn dimensions(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
624 self.0.dimensions().py_block_on(py)
625 }
626
627 /// The expression schema of this [`View`], which contains only the
628 /// expressions created on this [`View`]. See [`View::schema`] for
629 /// details.
630 pub fn expression_schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
631 self.0.expression_schema().py_block_on(py)
632 }
633
634 /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
635 /// which created this [`View`].
636 pub fn get_config(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
637 self.0.get_config().py_block_on(py)
638 }
639
640 /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
641 ///
642 /// # Returns
643 ///
644 /// A tuple of [min, max], whose types are column and aggregate dependent.
645 pub fn get_min_max(&self, py: Python<'_>, column_name: String) -> PyResult<(String, String)> {
646 self.0.get_min_max(column_name).py_block_on(py)
647 }
648
649 /// The number of aggregated rows in this [`View`]. This is affected by the
650 /// "group_by" configuration parameter supplied to this view's contructor.
651 ///
652 /// # Returns
653 ///
654 /// The number of aggregated rows.
655 pub fn num_rows(&self, py: Python<'_>) -> PyResult<u32> {
656 self.0.num_rows().py_block_on(py)
657 }
658
659 /// The schema of this [`View`].
660 ///
661 /// The [`View`] schema differs from the `schema` returned by
662 /// [`Table::schema`]; it may have different column names due to
663 /// `expressions` or `columns` configs, or it maye have _different
664 /// column types_ due to the application og `group_by` and `aggregates`
665 /// config. You can think of [`Table::schema`] as the _input_ schema and
666 /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
667 pub fn schema(&self, py: Python<'_>) -> PyResult<HashMap<String, String>> {
668 self.0.schema().py_block_on(py)
669 }
670
671 /// Register a callback with this [`View`]. Whenever the [`View`] is
672 /// deleted, this callback will be invoked.
673 pub fn on_delete(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<u32> {
674 self.0.on_delete(callback).py_block_on(py)
675 }
676
677 /// Unregister a previously registered [`View::on_delete`] callback.
678 pub fn remove_delete(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
679 self.0.remove_delete(callback_id).py_block_on(py)
680 }
681
682 /// Register a callback with this [`View`]. Whenever the view's underlying
683 /// table emits an update, this callback will be invoked with an object
684 /// containing `port_id`, indicating which port the update fired on, and
685 /// optionally `delta`, which is the new data that was updated for each
686 /// cell or each row.
687 ///
688 /// # Arguments
689 ///
690 /// - `on_update` - A callback function invoked on update, which receives an
691 /// object with two keys: `port_id`, indicating which port the update was
692 /// triggered on, and `delta`, whose value is dependent on the mode
693 /// parameter.
694 /// - `options` - If this is provided as `OnUpdateOptions { mode:
695 /// Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
696 /// rows. Otherwise `delta` will be [`Option::None`].
697 #[pyo3(signature = (callback, mode=None))]
698 pub fn on_update(
699 &self,
700 py: Python<'_>,
701 callback: Py<PyAny>,
702 mode: Option<String>,
703 ) -> PyResult<u32> {
704 self.0.on_update(callback, mode).py_block_on(py)
705 }
706
707 /// Unregister a previously registered update callback with this [`View`].
708 ///
709 /// # Arguments
710 ///
711 /// - `id` - A callback `id` as returned by a recipricol call to
712 /// [`View::on_update`].
713 ///
714 /// # Examples
715 ///
716 /// ```rust
717 /// let callback = |_| async { print!("Updated!") };
718 /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
719 /// view.remove_update(cid).await?;
720 /// ```
721 pub fn remove_update(&self, py: Python<'_>, callback_id: u32) -> PyResult<()> {
722 self.0.remove_update(callback_id).py_block_on(py)
723 }
724}