perspective-python 4.5.2__cp311-abi3-pyemscripten_2025_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 (79) 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 +35 -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/conftest.py +263 -0
  13. perspective/tests/core/__init__.py +11 -0
  14. perspective/tests/core/test_async.py +351 -0
  15. perspective/tests/multi_threaded/__init__.py +11 -0
  16. perspective/tests/multi_threaded/test_multi_threaded.py +201 -0
  17. perspective/tests/server/__init__.py +11 -0
  18. perspective/tests/server/test_server.py +1016 -0
  19. perspective/tests/server/test_session.py +110 -0
  20. perspective/tests/table/__init__.py +11 -0
  21. perspective/tests/table/arrow/date32.arrow +0 -0
  22. perspective/tests/table/arrow/date64.arrow +0 -0
  23. perspective/tests/table/arrow/dict.arrow +0 -0
  24. perspective/tests/table/arrow/dict_update.arrow +0 -0
  25. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  26. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  27. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  28. perspective/tests/table/object_sequence.py +402 -0
  29. perspective/tests/table/test_column_paths.py +89 -0
  30. perspective/tests/table/test_delete.py +124 -0
  31. perspective/tests/table/test_exception.py +65 -0
  32. perspective/tests/table/test_join.py +115 -0
  33. perspective/tests/table/test_leaks.py +105 -0
  34. perspective/tests/table/test_ports.py +178 -0
  35. perspective/tests/table/test_remove.py +102 -0
  36. perspective/tests/table/test_table.py +641 -0
  37. perspective/tests/table/test_table_arrow.py +500 -0
  38. perspective/tests/table/test_table_datetime.py +2409 -0
  39. perspective/tests/table/test_table_infer.py +201 -0
  40. perspective/tests/table/test_table_limit.py +45 -0
  41. perspective/tests/table/test_table_numpy.py +1022 -0
  42. perspective/tests/table/test_table_page_to_disk.py +304 -0
  43. perspective/tests/table/test_table_pandas.py +1018 -0
  44. perspective/tests/table/test_table_polars.py +251 -0
  45. perspective/tests/table/test_table_view_table.py +130 -0
  46. perspective/tests/table/test_to_arrow.py +417 -0
  47. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  48. perspective/tests/table/test_to_format.py +1024 -0
  49. perspective/tests/table/test_to_polars.py +26 -0
  50. perspective/tests/table/test_update.py +536 -0
  51. perspective/tests/table/test_update_arrow.py +980 -0
  52. perspective/tests/table/test_update_pandas.py +211 -0
  53. perspective/tests/table/test_view.py +2444 -0
  54. perspective/tests/table/test_view_expression.py +1940 -0
  55. perspective/tests/test_dependencies.py +53 -0
  56. perspective/tests/viewer/__init__.py +11 -0
  57. perspective/tests/viewer/test_viewer.py +246 -0
  58. perspective/tests/virtual_servers/__init__.py +12 -0
  59. perspective/tests/virtual_servers/test_coerce_types.py +209 -0
  60. perspective/tests/virtual_servers/test_duckdb.py +908 -0
  61. perspective/tests/virtual_servers/test_polars.py +1032 -0
  62. perspective/tests/widget/__init__.py +11 -0
  63. perspective/tests/widget/test_widget.py +278 -0
  64. perspective/tests/widget/test_widget_pandas.py +453 -0
  65. perspective/virtual_servers/__init__.py +142 -0
  66. perspective/virtual_servers/clickhouse.py +240 -0
  67. perspective/virtual_servers/duckdb.py +236 -0
  68. perspective/virtual_servers/polars.py +710 -0
  69. perspective/widget/__init__.py +349 -0
  70. perspective/widget/viewer/__init__.py +15 -0
  71. perspective/widget/viewer/validate.py +22 -0
  72. perspective/widget/viewer/viewer.py +343 -0
  73. perspective/widget/viewer/viewer_traitlets.py +101 -0
  74. perspective_python-4.5.2.dist-info/METADATA +29 -0
  75. perspective_python-4.5.2.dist-info/RECORD +79 -0
  76. perspective_python-4.5.2.dist-info/WHEEL +4 -0
  77. perspective_python-4.5.2.dist-info/licenses/LICENSE.md +193 -0
  78. perspective_python-4.5.2.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +19255 -0
  79. perspective_python-4.5.2.dist-info/sboms/perspective-python.cyclonedx.json +5546 -0
@@ -0,0 +1,53 @@
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 pytest
14
+
15
+
16
+ # test that loading perspective and calling a common constructor code path does
17
+ # not load various "expensive" modules. "Expensive" is in quotes because this is
18
+ # utter nonsense.
19
+ @pytest.mark.skip(reason="https://github.com/pyca/bcrypt/issues/694")
20
+ def test_lazy_modules():
21
+ import sys
22
+
23
+ cache = {}
24
+ for key in list(sys.modules.keys()):
25
+ if (
26
+ key.startswith("perspective")
27
+ or key.startswith("test")
28
+ or key.startswith("pandas")
29
+ or key.startswith("pyarrow")
30
+ or key.startswith("tornado")
31
+ ):
32
+ cache[key] = sys.modules[key]
33
+ del sys.modules[key]
34
+
35
+ import perspective
36
+
37
+ t1 = perspective.table("x\n1")
38
+ t1.delete()
39
+
40
+ assert "perspective" in sys.modules
41
+ assert "pandas" not in sys.modules
42
+ assert "pyarrow" not in sys.modules
43
+ assert "tornado" not in sys.modules
44
+
45
+ for k, v in cache.items():
46
+ sys.modules[k] = v
47
+
48
+
49
+ def test_all():
50
+ import perspective
51
+
52
+ for key in perspective.__all__:
53
+ assert hasattr(perspective, key)
@@ -0,0 +1,11 @@
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
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
@@ -0,0 +1,246 @@
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 numpy as np
14
+ import pandas as pd
15
+ import pytest
16
+ from perspective.widget.viewer import PerspectiveViewer
17
+ import perspective as psp
18
+
19
+ client = psp.Server().new_local_client()
20
+ Table = client.table
21
+
22
+
23
+ class TestViewer:
24
+ def test_viewer_get_table(self):
25
+ table = Table({"a": [1, 2, 3]})
26
+ viewer = PerspectiveViewer()
27
+ viewer.load(table)
28
+ assert viewer.table == table
29
+
30
+ # loading
31
+
32
+ def test_viewer_load_table(self):
33
+ table = Table({"a": [1, 2, 3]})
34
+ viewer = PerspectiveViewer()
35
+ viewer.load(table)
36
+ assert viewer.columns == ["a"]
37
+
38
+ def test_viewer_load_named_table(self):
39
+ table = Table({"a": [1, 2, 3]}, name="data_1")
40
+ viewer = PerspectiveViewer()
41
+ viewer.load(table)
42
+ assert viewer.columns == ["a"]
43
+ assert viewer.table_name == "data_1"
44
+ assert viewer.table == table
45
+
46
+ def test_viewer_load_data(self):
47
+ viewer = PerspectiveViewer()
48
+ viewer.load({"a": [1, 2, 3]})
49
+ assert viewer.columns == ["a"]
50
+
51
+ def test_viewer_load_named_data(self):
52
+ viewer = PerspectiveViewer()
53
+ viewer.load({"a": [1, 2, 3]}, name="data_1")
54
+ assert viewer.columns == ["a"]
55
+ assert viewer.table_name == "data_1"
56
+
57
+ def test_viewer_load_schema(self):
58
+ viewer = PerspectiveViewer()
59
+ viewer.load({"a": "string", "b": "integer", "c": "boolean", "d": "string"})
60
+ for col in viewer.columns:
61
+ assert col in ["a", "b", "c", "d"]
62
+
63
+ def test_viewer_load_table_with_options(self):
64
+ table = Table({"a": [1, 2, 3]})
65
+ viewer = PerspectiveViewer()
66
+ # options should be disregarded when loading Table
67
+ viewer.load(table, limit=1)
68
+ assert viewer.columns == ["a"]
69
+ assert viewer.table.size() == 3
70
+
71
+ def test_viewer_load_data_with_options(self):
72
+ viewer = PerspectiveViewer()
73
+ # options should be forwarded to the Table constructor
74
+ viewer.load({"a": [1, 2, 3]}, limit=1)
75
+ assert viewer.columns == ["a"]
76
+ assert viewer.table.size() == 1
77
+
78
+ def test_viewer_load_clears_state(self):
79
+ table = Table({"a": [1, 2, 3]})
80
+ viewer = PerspectiveViewer(theme="Pro Dark", group_by=["a"])
81
+ viewer.load(table)
82
+ assert viewer.group_by == ["a"]
83
+ viewer.load({"b": [1, 2, 3]})
84
+ assert viewer.group_by == []
85
+ assert viewer.theme == "Pro Dark" # should not break UI
86
+
87
+ @pytest.mark.skip
88
+ def test_viewer_load_np(self):
89
+ table = Table({"a": np.arange(1, 100)})
90
+ viewer = PerspectiveViewer()
91
+ viewer.load(table)
92
+ assert viewer.columns == ["a"]
93
+
94
+ @pytest.mark.skip
95
+ def test_viewer_load_np_data(self):
96
+ viewer = PerspectiveViewer()
97
+ viewer.load({"a": np.arange(1, 100)})
98
+ assert viewer.columns == ["a"]
99
+ assert viewer.table.size() == 99
100
+
101
+ @pytest.mark.skip
102
+ def test_viewer_load_df(self):
103
+ table = Table(pd.DataFrame({"a": np.arange(1, 100)}))
104
+ viewer = PerspectiveViewer()
105
+ viewer.load(table)
106
+ for col in viewer.columns:
107
+ assert col in ["index", "a"]
108
+ assert viewer.table.size() == 99
109
+
110
+ def test_viewer_load_df_data(self):
111
+ viewer = PerspectiveViewer()
112
+ viewer.load(pd.DataFrame({"a": np.arange(1, 100)}))
113
+ for col in viewer.columns:
114
+ assert col in ["index", "a"]
115
+
116
+ # update
117
+
118
+ def test_viewer_update_dict(self):
119
+ table = Table({"a": [1, 2, 3]})
120
+ viewer = PerspectiveViewer()
121
+ viewer.load(table)
122
+ viewer.update({"a": [4, 5, 6]})
123
+ assert table.size() == 6
124
+ assert viewer.table.size() == 6
125
+ assert viewer.table.view().to_columns() == {"a": [1, 2, 3, 4, 5, 6]}
126
+
127
+ def test_viewer_update_list(self):
128
+ table = Table({"a": [1, 2, 3]})
129
+ viewer = PerspectiveViewer()
130
+ viewer.load(table)
131
+ viewer.update([{"a": 4}, {"a": 5}, {"a": 6}])
132
+ assert table.size() == 6
133
+ assert viewer.table.size() == 6
134
+ assert viewer.table.view().to_columns() == {"a": [1, 2, 3, 4, 5, 6]}
135
+
136
+ def test_viewer_update_df(self):
137
+ table = Table({"a": [1, 2, 3]})
138
+ viewer = PerspectiveViewer()
139
+ viewer.load(table)
140
+ viewer.update(pd.DataFrame({"a": [4, 5, 6]}))
141
+ assert table.size() == 6
142
+ assert viewer.table.size() == 6
143
+ assert viewer.table.view().to_columns() == {"a": [1, 2, 3, 4, 5, 6]}
144
+
145
+ def test_viewer_update_dict_partial(self):
146
+ table = Table({"a": [1, 2, 3], "b": [5, 6, 7]}, index="a")
147
+ viewer = PerspectiveViewer()
148
+ viewer.load(table)
149
+ viewer.update({"a": [1, 2, 3], "b": [8, 9, 10]})
150
+ assert table.size() == 3
151
+ assert viewer.table.size() == 3
152
+ assert viewer.table.view().to_columns() == {"a": [1, 2, 3], "b": [8, 9, 10]}
153
+
154
+ # clear
155
+
156
+ def test_viewer_clear(self):
157
+ table = Table({"a": [1, 2, 3]})
158
+ viewer = PerspectiveViewer()
159
+ viewer.load(table)
160
+ viewer.clear()
161
+ assert viewer.table.size() == 0
162
+ assert viewer.table.schema() == {"a": "integer"}
163
+
164
+ # replace
165
+
166
+ def test_viewer_replace(self):
167
+ table = Table({"a": [1, 2, 3]})
168
+ viewer = PerspectiveViewer()
169
+ viewer.load(table)
170
+ viewer.replace({"a": [4, 5, 6]})
171
+ assert viewer.table.size() == 3
172
+ assert viewer.table.schema() == {"a": "integer"}
173
+ assert viewer.table.view().to_columns() == {"a": [4, 5, 6]}
174
+
175
+ # reset
176
+
177
+ def test_viewer_reset(self):
178
+ table = Table({"a": [1, 2, 3]})
179
+ viewer = PerspectiveViewer(plugin="X Bar", filter=[["a", "==", 2]])
180
+ viewer.load(table)
181
+ assert viewer.filter == [["a", "==", 2]]
182
+ viewer.reset()
183
+ assert viewer.plugin == "Datagrid"
184
+ assert viewer.filter == []
185
+
186
+ # delete
187
+
188
+ def test_viewer_delete(self):
189
+ table = Table({"a": [1, 2, 3]})
190
+ viewer = PerspectiveViewer(plugin="X Bar", filter=[["a", "==", 2]])
191
+ viewer.load(table)
192
+ assert viewer.filter == [["a", "==", 2]]
193
+ viewer.delete()
194
+ assert viewer.table_name is None
195
+ assert viewer.table is None
196
+
197
+ def test_viewer_delete_without_table(self):
198
+ table = Table({"a": [1, 2, 3]})
199
+ viewer = PerspectiveViewer(plugin="X Bar", filter=[["a", "==", 2]])
200
+ viewer.load(table)
201
+ assert viewer.filter == [["a", "==", 2]]
202
+ viewer.delete(delete_table=False)
203
+ assert viewer.table_name is not None
204
+ assert viewer.table is not None
205
+ assert viewer.filter == []
206
+
207
+ def test_save_restore(self):
208
+ table = Table({"a": [1, 2, 3]})
209
+ viewer = PerspectiveViewer(
210
+ plugin="X Bar", filter=[["a", "==", 2]], expressions={'"a" * 2': '"a" * 2'}
211
+ )
212
+ viewer.load(table)
213
+
214
+ # Save config
215
+ config = viewer.save()
216
+ assert viewer.filter == [["a", "==", 2]]
217
+ assert config["filter"] == [["a", "==", 2]]
218
+ assert viewer.plugin == "X Bar"
219
+ assert config["plugin"] == "X Bar"
220
+ assert config["expressions"] == {'"a" * 2': '"a" * 2'}
221
+
222
+ # reset configuration
223
+ viewer.reset()
224
+ assert viewer.plugin == "Datagrid"
225
+ assert viewer.filter == []
226
+ assert viewer.expressions == {}
227
+
228
+ # restore configuration
229
+ viewer.restore(**config)
230
+ assert viewer.filter == [["a", "==", 2]]
231
+ assert viewer.plugin == "X Bar"
232
+ assert viewer.expressions == {'"a" * 2': '"a" * 2'}
233
+
234
+ def test_save_restore_plugin_config(self):
235
+ viewer = PerspectiveViewer(
236
+ plugin="Datagrid", plugin_config={"columns": {"a": {"fixed": 4}}}
237
+ )
238
+ config = viewer.save()
239
+
240
+ assert config["plugin_config"] == {"columns": {"a": {"fixed": 4}}}
241
+
242
+ viewer.reset()
243
+ assert viewer.plugin_config == {}
244
+
245
+ viewer.restore(**config)
246
+ assert viewer.plugin_config == config["plugin_config"]
@@ -0,0 +1,12 @@
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
+
@@ -0,0 +1,209 @@
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 io
14
+ import json
15
+ from decimal import Decimal
16
+
17
+ import pyarrow as pa
18
+ import pyarrow.ipc as ipc
19
+
20
+ from perspective import VirtualDataSlice
21
+
22
+
23
+ def round_trip(arrow_table):
24
+ """Serialize a PyArrow table to IPC, feed through VirtualDataSlice, read back as JSON."""
25
+ buf = io.BytesIO()
26
+ with ipc.new_stream(buf, arrow_table.schema) as writer:
27
+ writer.write_table(arrow_table)
28
+ ds = VirtualDataSlice()
29
+ ds.from_arrow_ipc(buf.getvalue())
30
+ return json.loads(ds.render_to_columns_json())
31
+
32
+
33
+ class TestCoerceSmallIntegers:
34
+ def test_coerce_int8(self):
35
+ table = pa.table({"col": pa.array([-1, 127, None], type=pa.int8())})
36
+ result = round_trip(table)
37
+ assert result["col"] == [-1, 127, None]
38
+
39
+ def test_coerce_int16(self):
40
+ table = pa.table({"col": pa.array([-300, 32000, None], type=pa.int16())})
41
+ result = round_trip(table)
42
+ assert result["col"] == [-300, 32000, None]
43
+
44
+
45
+ class TestCoerceUnsignedIntegers:
46
+ def test_coerce_uint8(self):
47
+ table = pa.table({"col": pa.array([0, 255, None], type=pa.uint8())})
48
+ result = round_trip(table)
49
+ assert result["col"] == [0, 255, None]
50
+
51
+ def test_coerce_uint16(self):
52
+ table = pa.table({"col": pa.array([0, 65535, None], type=pa.uint16())})
53
+ result = round_trip(table)
54
+ assert result["col"] == [0, 65535, None]
55
+
56
+ def test_coerce_uint32(self):
57
+ table = pa.table({"col": pa.array([0, 4_294_967_295, None], type=pa.uint32())})
58
+ result = round_trip(table)
59
+ assert result["col"] == [0.0, 4_294_967_295.0, None]
60
+
61
+ def test_coerce_uint64(self):
62
+ table = pa.table({"col": pa.array([0, 1 << 53, None], type=pa.uint64())})
63
+ result = round_trip(table)
64
+ assert result["col"] == [0.0, 9_007_199_254_740_992.0, None]
65
+
66
+
67
+ class TestCoerceFloat32:
68
+ def test_coerce_float32(self):
69
+ table = pa.table({"col": pa.array([3.14, -0.0, None], type=pa.float32())})
70
+ result = round_trip(table)
71
+ assert abs(result["col"][0] - 3.14) < 0.001
72
+ assert result["col"][1] == 0.0
73
+ assert result["col"][2] is None
74
+
75
+
76
+ class TestCoerceDate64:
77
+ def test_coerce_date64(self):
78
+ day = 19738
79
+ table = pa.table({"col": pa.array([day * 86_400_000, None], type=pa.date64())})
80
+ result = round_trip(table)
81
+ assert result["col"] == [day * 86_400_000, None]
82
+
83
+
84
+ class TestCoerceTime:
85
+ def test_coerce_time32_second(self):
86
+ table = pa.table({"col": pa.array([49530, None], type=pa.time32("s"))})
87
+ result = round_trip(table)
88
+ assert result["col"] == [49_530_000, None]
89
+
90
+ def test_coerce_time32_millisecond(self):
91
+ table = pa.table({"col": pa.array([49_530_000, None], type=pa.time32("ms"))})
92
+ result = round_trip(table)
93
+ assert result["col"] == [49_530_000, None]
94
+
95
+ def test_coerce_time64_microsecond(self):
96
+ table = pa.table(
97
+ {"col": pa.array([49_530_000_000, None], type=pa.time64("us"))}
98
+ )
99
+ result = round_trip(table)
100
+ assert result["col"] == [49_530_000, None]
101
+
102
+ def test_coerce_time64_nanosecond(self):
103
+ table = pa.table(
104
+ {"col": pa.array([49_530_000_000_000, None], type=pa.time64("ns"))}
105
+ )
106
+ result = round_trip(table)
107
+ assert result["col"] == [49_530_000, None]
108
+
109
+
110
+ class TestCoerceLargeUtf8:
111
+ def test_coerce_large_utf8(self):
112
+ table = pa.table({"col": pa.array(["hello", "", None], type=pa.large_utf8())})
113
+ result = round_trip(table)
114
+ assert result["col"] == ["hello", "", None]
115
+
116
+
117
+ class TestCoerceDecimal128:
118
+ def test_coerce_decimal128(self):
119
+ arr = pa.array([Decimal("1234.5678"), None], type=pa.decimal128(10, 4))
120
+ table = pa.table({"col": arr})
121
+ result = round_trip(table)
122
+ assert result["col"] == [1234.5678, None]
123
+
124
+
125
+ class TestCoerceInt64:
126
+ def test_coerce_int64(self):
127
+ table = pa.table({"col": pa.array([1, -1, None], type=pa.int64())})
128
+ result = round_trip(table)
129
+ assert result["col"] == [1.0, -1.0, None]
130
+
131
+
132
+ class TestCoerceTimestamp:
133
+ def test_coerce_timestamp_second(self):
134
+ table = pa.table({"col": pa.array([1000, None], type=pa.timestamp("s"))})
135
+ result = round_trip(table)
136
+ assert result["col"] == [1_000_000, None]
137
+
138
+ def test_coerce_timestamp_microsecond(self):
139
+ table = pa.table({"col": pa.array([1_000_000, None], type=pa.timestamp("us"))})
140
+ result = round_trip(table)
141
+ assert result["col"] == [1000, None]
142
+
143
+ def test_coerce_timestamp_nanosecond(self):
144
+ table = pa.table(
145
+ {"col": pa.array([1_000_000_000, None], type=pa.timestamp("ns"))}
146
+ )
147
+ result = round_trip(table)
148
+ assert result["col"] == [1000, None]
149
+
150
+
151
+ class TestPassthrough:
152
+ def test_passthrough_bool(self):
153
+ table = pa.table({"col": pa.array([True, False, None], type=pa.bool_())})
154
+ result = round_trip(table)
155
+ assert result["col"] == [True, False, None]
156
+
157
+ def test_passthrough_utf8(self):
158
+ table = pa.table({"col": pa.array(["a", "b", None], type=pa.utf8())})
159
+ result = round_trip(table)
160
+ assert result["col"] == ["a", "b", None]
161
+
162
+ def test_passthrough_float64(self):
163
+ table = pa.table({"col": pa.array([1.5, -2.5, None], type=pa.float64())})
164
+ result = round_trip(table)
165
+ assert result["col"] == [1.5, -2.5, None]
166
+
167
+ def test_passthrough_int32(self):
168
+ table = pa.table({"col": pa.array([1, -1, None], type=pa.int32())})
169
+ result = round_trip(table)
170
+ assert result["col"] == [1, -1, None]
171
+
172
+ def test_passthrough_date32(self):
173
+ table = pa.table({"col": pa.array([19738, None], type=pa.date32())})
174
+ result = round_trip(table)
175
+ assert result["col"] == [19738 * 86_400_000, None]
176
+
177
+ def test_passthrough_timestamp_millisecond(self):
178
+ table = pa.table({"col": pa.array([1000, None], type=pa.timestamp("ms"))})
179
+ result = round_trip(table)
180
+ assert result["col"] == [1000, None]
181
+
182
+
183
+ class TestFallback:
184
+ def test_fallback_fixed_size_binary(self):
185
+ table = pa.table(
186
+ {"col": pa.array([b"\x01\x02", b"\x03\x04", None], type=pa.binary(2))}
187
+ )
188
+ result = round_trip(table)
189
+ col = result["col"]
190
+ assert isinstance(col[0], str)
191
+ assert isinstance(col[1], str)
192
+ assert col[0] != col[1]
193
+ assert col[2] is None
194
+
195
+
196
+ class TestEmpty:
197
+ def test_empty_batch(self):
198
+ # PyArrow's new_stream with an empty table writes no record batches,
199
+ # so we use RecordBatchStreamWriter directly to produce a valid empty batch.
200
+ schema = pa.schema([("col", pa.int8())])
201
+ batch = pa.record_batch({"col": pa.array([], type=pa.int8())})
202
+ buf = io.BytesIO()
203
+ writer = ipc.RecordBatchStreamWriter(buf, schema)
204
+ writer.write_batch(batch)
205
+ writer.close()
206
+ ds = VirtualDataSlice()
207
+ ds.from_arrow_ipc(buf.getvalue())
208
+ result = json.loads(ds.render_to_columns_json())
209
+ assert result["col"] == []