perspective-python 5.6.0__cp311-abi3-pyemscripten_2026_0_wasm32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. perspective/__init__.py +403 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +11 -0
  4. perspective/handlers/aiohttp.py +69 -0
  5. perspective/handlers/starlette.py +63 -0
  6. perspective/handlers/tornado.py +192 -0
  7. perspective/perspective.abi3.so +0 -0
  8. perspective/templates/exported_widget.html.template +39 -0
  9. perspective/tests/__init__.py +11 -0
  10. perspective/tests/async/test_async_client.py +83 -0
  11. perspective/tests/async/test_websocket_client.py +124 -0
  12. perspective/tests/column_paths.py +33 -0
  13. perspective/tests/conftest.py +276 -0
  14. perspective/tests/core/__init__.py +11 -0
  15. perspective/tests/core/test_async.py +351 -0
  16. perspective/tests/multi_threaded/__init__.py +11 -0
  17. perspective/tests/multi_threaded/test_derived_threaded.py +103 -0
  18. perspective/tests/multi_threaded/test_multi_threaded.py +293 -0
  19. perspective/tests/server/__init__.py +11 -0
  20. perspective/tests/server/test_server.py +1016 -0
  21. perspective/tests/server/test_session.py +110 -0
  22. perspective/tests/table/__init__.py +11 -0
  23. perspective/tests/table/arrow/date32.arrow +0 -0
  24. perspective/tests/table/arrow/date64.arrow +0 -0
  25. perspective/tests/table/arrow/dict.arrow +0 -0
  26. perspective/tests/table/arrow/dict_update.arrow +0 -0
  27. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  28. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  29. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  30. perspective/tests/table/object_sequence.py +402 -0
  31. perspective/tests/table/test_column_paths.py +88 -0
  32. perspective/tests/table/test_delete.py +124 -0
  33. perspective/tests/table/test_derived.py +145 -0
  34. perspective/tests/table/test_exception.py +73 -0
  35. perspective/tests/table/test_join.py +115 -0
  36. perspective/tests/table/test_leaks.py +105 -0
  37. perspective/tests/table/test_on_remove.py +140 -0
  38. perspective/tests/table/test_ports.py +178 -0
  39. perspective/tests/table/test_remove.py +102 -0
  40. perspective/tests/table/test_table.py +641 -0
  41. perspective/tests/table/test_table_arrow.py +500 -0
  42. perspective/tests/table/test_table_arrow_nested.py +535 -0
  43. perspective/tests/table/test_table_datetime.py +2544 -0
  44. perspective/tests/table/test_table_infer.py +201 -0
  45. perspective/tests/table/test_table_limit.py +45 -0
  46. perspective/tests/table/test_table_numpy.py +1022 -0
  47. perspective/tests/table/test_table_page_to_disk.py +304 -0
  48. perspective/tests/table/test_table_pandas.py +1018 -0
  49. perspective/tests/table/test_table_polars.py +251 -0
  50. perspective/tests/table/test_table_view_table.py +130 -0
  51. perspective/tests/table/test_to_arrow.py +416 -0
  52. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  53. perspective/tests/table/test_to_arrow_zstd.py +41 -0
  54. perspective/tests/table/test_to_format.py +1030 -0
  55. perspective/tests/table/test_to_polars.py +26 -0
  56. perspective/tests/table/test_update.py +561 -0
  57. perspective/tests/table/test_update_arrow.py +980 -0
  58. perspective/tests/table/test_update_pandas.py +211 -0
  59. perspective/tests/table/test_view.py +2503 -0
  60. perspective/tests/table/test_view_expression.py +2256 -0
  61. perspective/tests/test_dependencies.py +53 -0
  62. perspective/tests/viewer/__init__.py +11 -0
  63. perspective/tests/viewer/test_viewer.py +246 -0
  64. perspective/tests/virtual_servers/__init__.py +12 -0
  65. perspective/tests/virtual_servers/test_coerce_types.py +464 -0
  66. perspective/tests/virtual_servers/test_duckdb.py +1280 -0
  67. perspective/tests/virtual_servers/test_polars.py +1075 -0
  68. perspective/tests/virtual_servers/test_postgres.py +964 -0
  69. perspective/tests/widget/__init__.py +11 -0
  70. perspective/tests/widget/test_anywidget_smoke.py +109 -0
  71. perspective/tests/widget/test_widget.py +278 -0
  72. perspective/tests/widget/test_widget_html_export.py +113 -0
  73. perspective/tests/widget/test_widget_pandas.py +453 -0
  74. perspective/virtual_servers/__init__.py +206 -0
  75. perspective/virtual_servers/clickhouse.py +296 -0
  76. perspective/virtual_servers/duckdb.py +367 -0
  77. perspective/virtual_servers/polars.py +750 -0
  78. perspective/virtual_servers/postgres.py +448 -0
  79. perspective/widget/__init__.py +351 -0
  80. perspective/widget/static/perspective-anywidget.css +1 -0
  81. perspective/widget/static/perspective-anywidget.js +31 -0
  82. perspective/widget/viewer/__init__.py +15 -0
  83. perspective/widget/viewer/viewer.py +355 -0
  84. perspective/widget/viewer/viewer_traitlets.py +98 -0
  85. perspective_python-5.6.0.dist-info/METADATA +127 -0
  86. perspective_python-5.6.0.dist-info/RECORD +90 -0
  87. perspective_python-5.6.0.dist-info/WHEEL +4 -0
  88. perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
  89. perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
  90. perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
@@ -0,0 +1,351 @@
1
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ # ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ # ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import base64
14
+ import json
15
+ import logging
16
+ import os
17
+ import pathlib
18
+ import re
19
+ import importlib.metadata
20
+ import inspect
21
+
22
+ from string import Template
23
+ import anywidget
24
+ from traitlets import observe
25
+ from .viewer import PerspectiveViewer
26
+
27
+ __version__ = re.sub(".dev[0-9]+", "", importlib.metadata.version("perspective-python"))
28
+
29
+ __all__ = ["PerspectiveWidget"]
30
+
31
+ _STATIC = pathlib.Path(__file__).parent / "static"
32
+
33
+ __doc__ = """
34
+ `PerspectiveWidget` is a JupyterLab widget that implements the same API as
35
+ `<perspective-viewer>`, allowing for fast, intuitive
36
+ transformations/visualizations of various data formats within JupyterLab.
37
+
38
+ `PerspectiveWidget` is compatible with Jupyterlab 3 and Jupyter Notebook 6 via a
39
+ [prebuilt extension](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#prebuilt-extensions).
40
+ To use it, simply install `perspective-python` and the extensions should be
41
+ available.
42
+
43
+ `perspective-python`'s JupyterLab extension also provides convenient builtin
44
+ viewers for `csv`, `json`, or `arrow` files. Simply right-click on a file with
45
+ this extension and choose the appropriate `Perpective` option from the context
46
+ menu.
47
+
48
+ ## `PerspectiveWidget`
49
+
50
+ Building on top of the API provided by `perspective.Table`, the
51
+ `PerspectiveWidget` is a JupyterLab plugin that offers the entire functionality
52
+ of Perspective within the Jupyter environment. It supports the same API
53
+ semantics of `<perspective-viewer>`, along with the additional data types
54
+ supported by `perspective.Table`. `PerspectiveWidget` takes keyword arguments
55
+ for the managed `View`:
56
+
57
+ ```python
58
+ from perspective.widget import PerspectiveWidget
59
+ w = perspective.PerspectiveWidget(
60
+ data,
61
+ plugin="X Bar",
62
+ aggregates={"datetime": "any"},
63
+ sort=[["date", "desc"]]
64
+ )
65
+ ```
66
+
67
+ ### Creating a widget
68
+
69
+ A widget is created through the `PerspectiveWidget` constructor, which takes as
70
+ its first, required parameter a `perspective.Table`, a dataset, a schema, or
71
+ `None`, which serves as a special value that tells the Widget to defer loading
72
+ any data until later. In maintaining consistency with the Javascript API,
73
+ Widgets cannot be created with empty dictionaries or lists—`None` should be used
74
+ if the intention is to await data for loading later on. A widget can be
75
+ constructed from a dataset:
76
+
77
+ ```python
78
+ from perspective.widget import PerspectiveWidget
79
+ PerspectiveWidget(data, group_by=["date"])
80
+ ```
81
+
82
+ .. or a schema:
83
+
84
+ ```python
85
+ PerspectiveWidget({"a": int, "b": str})
86
+ ```
87
+
88
+ .. or an instance of a `perspective.Table`:
89
+
90
+ ```python
91
+ table = perspective.table(data)
92
+ PerspectiveWidget(table)
93
+ ```
94
+ """
95
+
96
+
97
+ class PerspectiveWidget(anywidget.AnyWidget, PerspectiveViewer):
98
+ """`PerspectiveWidget` allows for Perspective to be used as a Jupyter
99
+ widget.
100
+
101
+ Using `perspective.Table`, you can create a widget that extends the full
102
+ functionality of `perspective-viewer`. Changes on the viewer can be
103
+ programatically set on the `PerspectiveWidget` instance.
104
+
105
+ # Examples
106
+
107
+ >>> from perspective.widget import PerspectiveWidget
108
+ >>> data = {
109
+ ... "a": [1, 2, 3],
110
+ ... "b": [
111
+ ... "2019/07/11 7:30PM",
112
+ ... "2019/07/11 8:30PM",
113
+ ... "2019/07/11 9:30PM"
114
+ ... ]
115
+ ... }
116
+ >>> widget = PerspectiveWidget(
117
+ ... data,
118
+ ... group_by=["a"],
119
+ ... sort=[["b", "desc"]],
120
+ ... filter=[["a", ">", 1]]
121
+ ... )
122
+ >>> widget.sort
123
+ [["b", "desc"]]
124
+ >>> widget.sort.append(["a", "asc"])
125
+ >>> widget.sort
126
+ [["b", "desc"], ["a", "asc"]]
127
+ >>> widget.table.update({"a": [4, 5]}) # Browser UI updates
128
+ """
129
+
130
+ _esm = _STATIC / "perspective-anywidget.js"
131
+ _css = _STATIC / "perspective-anywidget.css"
132
+
133
+ def __init__(
134
+ self,
135
+ data,
136
+ index=None,
137
+ limit=None,
138
+ binding_mode="server",
139
+ **kwargs,
140
+ ):
141
+ """Initialize an instance of `PerspectiveWidget`
142
+ with the given table/data and viewer configuration.
143
+
144
+ If an `AsyncTable` is passed in, then certain widget methods like
145
+ `update()` and `delete()` return coroutines which must be awaited.
146
+
147
+ # Arguments
148
+
149
+ - `data` (`Table`|`AsyncTable`|`dict`|`list`|`pandas.DataFrame`|`bytes`|`str`): a
150
+ `perspective.Table` instance, a `perspective.AsyncTable` instance, or
151
+ a dataset to be loaded in the widget.
152
+
153
+ # Keyword Arguments
154
+
155
+ - `index` (`str`): A column name to be used as the primary key.
156
+ Ignored if `server` is True.
157
+ - `binding_mode` (`str`): "client-server" or "server"
158
+ - `limit` (`int`): A upper limit on the number of rows in the Table.
159
+ Cannot be set at the same time as `index`, ignored if `server`
160
+ is True.
161
+ - `kwargs` (`dict`): configuration options for the `PerspectiveViewer`,
162
+ and `Table` constructor if `data` is a dataset.
163
+
164
+ # Examples
165
+
166
+ >>> widget = PerspectiveWidget(
167
+ ... {"a": [1, 2, 3]},
168
+ ... aggregates={"a": "avg"},
169
+ ... group_by=["a"],
170
+ ... sort=[["b", "desc"]],
171
+ ... filter=[["a", ">", 1]],
172
+ ... expressions=["\"a\" + 100"])
173
+ """
174
+
175
+ self.binding_mode = binding_mode
176
+
177
+ # Pass table load options to the front-end, unless in server mode
178
+ self._options = {}
179
+
180
+ if index is not None and limit is not None:
181
+ raise TypeError("Index and Limit cannot be set at the same time!")
182
+
183
+ # Parse the dataset we pass in - if it's Pandas, preserve pivots
184
+ # if isinstance(data, pandas.DataFrame) or isinstance(data, pandas.Series):
185
+ # data, config = deconstruct_pandas(data)
186
+
187
+ # if config.get("group_by", None) and "group_by" not in kwargs:
188
+ # kwargs.update({"group_by": config["group_by"]})
189
+
190
+ # if config.get("split_by", None) and "split_by" not in kwargs:
191
+ # kwargs.update({"split_by": config["split_by"]})
192
+
193
+ # if config.get("columns", None) and "columns" not in kwargs:
194
+ # kwargs.update({"columns": config["columns"]})
195
+
196
+ # Initialize the viewer
197
+ super(PerspectiveWidget, self).__init__(**kwargs)
198
+
199
+ # Handle messages from the the front end
200
+ self.on_msg(self.handle_message)
201
+ self._sessions = {}
202
+
203
+ # If an empty dataset is provided, don't call `load()` and wait
204
+ # for the user to call `load()`.
205
+ if data is None:
206
+ if index is not None or limit is not None:
207
+ raise TypeError(
208
+ "Cannot initialize PerspectiveWidget `index` or `limit` without a Table, data, or schema!"
209
+ )
210
+ else:
211
+ if index is not None:
212
+ self._options.update({"index": index})
213
+
214
+ if limit is not None:
215
+ self._options.update({"limit": limit})
216
+
217
+ loading = self.load(data, **self._options)
218
+ if inspect.isawaitable(loading):
219
+ import asyncio
220
+
221
+ asyncio.create_task(loading)
222
+
223
+ def load(self, data, **options):
224
+ """Load the widget with data."""
225
+ # Viewer will ignore **options if `data` is a Table or View.
226
+ return super(PerspectiveWidget, self).load(data, **options)
227
+
228
+ def update(self, data):
229
+ """Update the widget with new data."""
230
+ return super(PerspectiveWidget, self).update(data)
231
+
232
+ def clear(self):
233
+ """Clears the widget's underlying `Table`."""
234
+ return super(PerspectiveWidget, self).clear()
235
+
236
+ def replace(self, data):
237
+ """Replaces the widget's `Table` with new data conforming to the same
238
+ schema. Does not clear user-set state. If in client mode, serializes
239
+ the data and sends it to the browser.
240
+ """
241
+ return super(PerspectiveWidget, self).replace(data)
242
+
243
+ def delete(self, delete_table=True):
244
+ """Delete the Widget's data and clears its internal state.
245
+
246
+ # Arguments
247
+
248
+ - `delete_table` (`bool`): whether the underlying `Table` will be
249
+ deleted. Defaults to True.
250
+ """
251
+ ret = super(PerspectiveWidget, self).delete(delete_table)
252
+
253
+ # Close the underlying comm and remove widget from the front-end
254
+ self.close()
255
+ return ret
256
+
257
+ @observe("value")
258
+ def handle_message(self, widget, content, buffers):
259
+ """Given a message from `PerspectiveJupyterClient.send`, process the
260
+ message and return the result to `self.post`.
261
+
262
+ # Arguments
263
+
264
+ - `widget`: a reference to the `Widget` instance that received the
265
+ message.
266
+ - `content` (dict): - the message from the front-end. Automatically
267
+ de-serialized by ipywidgets.
268
+ - `buffers`: optional arraybuffers from the front-end, if any.
269
+ """
270
+ if content["type"] == "connect":
271
+ client_id = content["client_id"]
272
+ logging.debug("view {} connected", client_id)
273
+
274
+ def send_response(msg):
275
+ self.send({"type": "binary_msg", "client_id": client_id}, [msg])
276
+
277
+ self._sessions[client_id] = self.new_proxy_session(send_response)
278
+ elif content["type"] == "binary_msg":
279
+ [binary_msg] = buffers
280
+ client_id = content["client_id"]
281
+ session = self._sessions[client_id]
282
+ if session is not None:
283
+ import asyncio
284
+
285
+ asyncio.create_task(session.handle_request_async(binary_msg))
286
+ else:
287
+ logging.error("No session for client_id {}".format(client_id))
288
+ elif content["type"] == "hangup":
289
+ # XXX(tom): client won't reliably send this so shouldn't rely on it
290
+ # to clean up; does jupyter notify us when the client on the
291
+ # websocket, i.e. the view, disconnects?
292
+ client_id = content["client_id"]
293
+ logging.debug("view {} hangup", client_id)
294
+ session = self._sessions.pop(client_id, None)
295
+ if session:
296
+ session.close()
297
+
298
+ def _repr_mimebundle_(self, **kwargs):
299
+ super_bundle = super()._repr_mimebundle_(**kwargs)
300
+ if not _jupyter_html_export_enabled():
301
+ return super_bundle
302
+
303
+ # Serialize viewer attrs + view data to be rendered in the template
304
+ viewer_attrs = self.save()
305
+ data = self.table.view().to_arrow()
306
+ b64_data = base64.encodebytes(data)
307
+ template_path = os.path.join(
308
+ os.path.dirname(__file__), "../templates/exported_widget.html.template"
309
+ )
310
+ with open(template_path, "r") as template_data:
311
+ template = Template(template_data.read())
312
+
313
+ def psp_cdn(module, path):
314
+ # perspective developer affordance: works with your local `pnpm run start blocks`
315
+ # return f"http://localhost:8080/node_modules/@perspective-dev/{module}/dist/{path}"
316
+ return f"https://cdn.jsdelivr.net/npm/@perspective-dev/{module}@{__version__}/dist/{path}"
317
+
318
+ html = template.substitute(
319
+ psp_cdn_perspective=psp_cdn("client", "cdn/perspective.js"),
320
+ psp_cdn_perspective_viewer=psp_cdn("viewer", "cdn/perspective-viewer.js"),
321
+ psp_cdn_perspective_viewer_datagrid=psp_cdn(
322
+ "viewer-datagrid", "cdn/perspective-viewer-datagrid.js"
323
+ ),
324
+ psp_cdn_perspective_viewer_charts=psp_cdn(
325
+ "viewer-charts", "cdn/perspective-viewer-charts.js"
326
+ ),
327
+ psp_cdn_perspective_viewer_themes=psp_cdn("viewer", "css/themes.css"),
328
+ viewer_id=self.model_id,
329
+ viewer_attrs=json.dumps(viewer_attrs),
330
+ b64_data=b64_data.decode("utf-8"),
331
+ )
332
+
333
+ # anywidget's `_repr_mimebundle_` returns `tuple[dict, dict] | None`
334
+ # (data, metadata) rather than the plain dict `ipywidgets` returns.
335
+ if isinstance(super_bundle, tuple):
336
+ data, metadata = super_bundle
337
+ return dict(data) | {"text/html": html}, metadata
338
+
339
+ return (super_bundle or {}) | {"text/html": html}
340
+
341
+
342
+ def _jupyter_html_export_enabled():
343
+ return os.environ.get("PSP_JUPYTER_HTML_EXPORT", None) == "1"
344
+
345
+
346
+ def set_jupyter_html_export(val):
347
+ """Enables HTML export for Jupyter widgets, when set to True.
348
+ HTML export can also be enabled by setting the environment variable
349
+ `PSP_JUPYTER_HTML_EXPORT` to the string `1`.
350
+ """
351
+ os.environ["PSP_JUPYTER_HTML_EXPORT"] = "1" if val else "0"