perspective-python 4.2.0__cp311-abi3-win_amd64.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 +396 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +11 -0
  4. perspective/handlers/aiohttp.py +61 -0
  5. perspective/handlers/starlette.py +55 -0
  6. perspective/handlers/tornado.py +184 -0
  7. perspective/perspective.pyd +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 +272 -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_leaks.py +54 -0
  33. perspective/tests/table/test_ports.py +178 -0
  34. perspective/tests/table/test_remove.py +102 -0
  35. perspective/tests/table/test_table.py +641 -0
  36. perspective/tests/table/test_table_arrow.py +503 -0
  37. perspective/tests/table/test_table_datetime.py +2409 -0
  38. perspective/tests/table/test_table_infer.py +201 -0
  39. perspective/tests/table/test_table_limit.py +45 -0
  40. perspective/tests/table/test_table_numpy.py +1022 -0
  41. perspective/tests/table/test_table_pandas.py +1018 -0
  42. perspective/tests/table/test_table_polars.py +251 -0
  43. perspective/tests/table/test_table_view_table.py +130 -0
  44. perspective/tests/table/test_to_arrow.py +417 -0
  45. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  46. perspective/tests/table/test_to_format.py +1024 -0
  47. perspective/tests/table/test_to_polars.py +26 -0
  48. perspective/tests/table/test_update.py +545 -0
  49. perspective/tests/table/test_update_arrow.py +980 -0
  50. perspective/tests/table/test_update_pandas.py +211 -0
  51. perspective/tests/table/test_view.py +2261 -0
  52. perspective/tests/table/test_view_expression.py +1940 -0
  53. perspective/tests/test_dependencies.py +53 -0
  54. perspective/tests/viewer/__init__.py +11 -0
  55. perspective/tests/viewer/test_viewer.py +246 -0
  56. perspective/tests/widget/__init__.py +11 -0
  57. perspective/tests/widget/test_widget.py +278 -0
  58. perspective/tests/widget/test_widget_pandas.py +453 -0
  59. perspective/virtual_servers/__init__.py +134 -0
  60. perspective/virtual_servers/clickhouse.py +245 -0
  61. perspective/virtual_servers/duckdb.py +236 -0
  62. perspective/widget/__init__.py +349 -0
  63. perspective/widget/viewer/__init__.py +15 -0
  64. perspective/widget/viewer/validate.py +22 -0
  65. perspective/widget/viewer/viewer.py +343 -0
  66. perspective/widget/viewer/viewer_traitlets.py +101 -0
  67. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/install.json +5 -0
  68. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/package.json +71 -0
  69. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js +2 -0
  70. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js.LICENSE.txt +25 -0
  71. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/523.c030af5d3c4f67ff83f6.js +1 -0
  72. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/remoteEntry.95a8ea1b44d96032833f.js +1 -0
  73. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/style.js +4 -0
  74. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/third-party-licenses.json +16 -0
  75. perspective_python-4.2.0.dist-info/METADATA +27 -0
  76. perspective_python-4.2.0.dist-info/RECORD +79 -0
  77. perspective_python-4.2.0.dist-info/WHEEL +4 -0
  78. perspective_python-4.2.0.dist-info/licenses/LICENSE.md +193 -0
  79. perspective_python-4.2.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +17395 -0
@@ -0,0 +1,251 @@
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
+ from datetime import date, datetime
14
+ import numpy as np
15
+ import polars as pl
16
+ from pytest import mark
17
+ import perspective as psp
18
+
19
+ client = psp.Server().new_local_client()
20
+ Table = client.table
21
+
22
+
23
+ def arrow_bytes_to_polars(view):
24
+ import pyarrow
25
+
26
+ with pyarrow.ipc.open_stream(pyarrow.BufferReader(view.to_arrow())) as reader:
27
+ return pl.from_dataframe(reader.read_pandas())
28
+
29
+
30
+ class TestTablePolars(object):
31
+ def test_empty_table(self):
32
+ tbl = Table([])
33
+ assert tbl.size() == 0
34
+ assert tbl.schema() == {}
35
+
36
+ def test_table_dataframe(self):
37
+ d = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
38
+ data = pl.DataFrame(d)
39
+ tbl = Table(data)
40
+ assert tbl.size() == 2
41
+ assert tbl.schema() == {"a": "integer", "b": "integer"}
42
+ assert tbl.view().to_records() == [
43
+ {"a": 1, "b": 2},
44
+ {"a": 3, "b": 4},
45
+ ]
46
+
47
+ def test_table_lazyframe(self):
48
+ d = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
49
+ data = pl.DataFrame(d).lazy()
50
+ tbl = Table(data)
51
+ assert tbl.size() == 2
52
+ assert tbl.schema() == {"a": "integer", "b": "integer"}
53
+ assert tbl.view().to_records() == [
54
+ {"a": 1, "b": 2},
55
+ {"a": 3, "b": 4},
56
+ ]
57
+
58
+ def test_table_dataframe_column_order(self):
59
+ d = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
60
+ data = pl.DataFrame(d).select(["b", "c", "a", "d"])
61
+ tbl = Table(data)
62
+ assert tbl.size() == 2
63
+ assert tbl.columns() == ["b", "c", "a", "d"]
64
+
65
+ def test_table_dataframe_selective_column_order(self):
66
+ d = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
67
+ data = pl.DataFrame(d).select(["b", "c", "a"])
68
+ tbl = Table(data)
69
+ assert tbl.size() == 2
70
+ assert tbl.columns() == ["b", "c", "a"]
71
+
72
+ def test_table_dataframe_does_not_mutate(self):
73
+ # make sure we don't mutate the dataframe that a user passes in
74
+ data = pl.DataFrame(
75
+ {
76
+ "a": [None, 1, None, 2],
77
+ "b": [1.5, None, 2.5, None],
78
+ }
79
+ )
80
+ assert data["a"].to_list() == [None, 1, None, 2]
81
+ assert data["b"].to_list() == [1.5, None, 2.5, None]
82
+
83
+ tbl = Table(data)
84
+ assert tbl.size() == 4
85
+ assert tbl.schema() == {"a": "integer", "b": "float"}
86
+
87
+ assert data["a"].to_list() == [None, 1, None, 2]
88
+ assert data["b"].to_list() == [1.5, None, 2.5, None]
89
+
90
+ def test_table_polars_from_schema_int(self):
91
+ data = [None, 1, None, 2, None, 3, 4]
92
+ df = pl.DataFrame({"a": data})
93
+ table = Table({"a": "integer"})
94
+ table.update(df)
95
+ assert table.view().to_columns()["a"] == data
96
+
97
+ def test_table_polars_from_schema_bool(self):
98
+ data = [True, False, True, False]
99
+ df = pl.DataFrame({"a": data})
100
+ table = Table({"a": "boolean"})
101
+ table.update(df)
102
+ assert table.view().to_columns()["a"] == data
103
+
104
+ def test_table_polars_from_schema_float(self):
105
+ data = [None, 1.5, None, 2.5, None, 3.5, 4.5]
106
+ df = pl.DataFrame({"a": data})
107
+ table = Table({"a": "float"})
108
+ table.update(df)
109
+ assert table.view().to_columns()["a"] == data
110
+
111
+ def test_table_polars_from_schema_float_all_nan(self):
112
+ data = [np.nan, np.nan, np.nan, np.nan]
113
+ df = pl.DataFrame({"a": data})
114
+ table = Table({"a": "float"})
115
+ table.update(df)
116
+ assert table.view().to_columns()["a"] == [None, None, None, None]
117
+
118
+ def test_table_polars_from_schema_float_to_int(self):
119
+ data = [None, 1.5, None, 2.5, None, 3.5, 4.5]
120
+ df = pl.DataFrame({"a": data})
121
+ table = Table({"a": "integer"})
122
+ table.update(df)
123
+ # truncates decimal
124
+ assert table.view().to_columns()["a"] == [None, 1, None, 2, None, 3, 4]
125
+
126
+ def test_table_polars_from_schema_int_to_float(self):
127
+ data = [None, 1, None, 2, None, 3, 4]
128
+ df = pl.DataFrame({"a": data})
129
+ table = Table({"a": "float"})
130
+ table.update(df)
131
+ assert table.view().to_columns()["a"] == [None, 1.0, None, 2.0, None, 3.0, 4.0]
132
+
133
+ def test_table_polars_from_schema_date(self, util):
134
+ data = [date(2019, 8, 15), None, date(2019, 8, 16)]
135
+ df = pl.DataFrame({"a": data})
136
+ table = Table({"a": "date"})
137
+ table.update(df)
138
+ assert table.view().to_columns()["a"] == [
139
+ util.to_timestamp(datetime(2019, 8, 15)),
140
+ None,
141
+ util.to_timestamp(datetime(2019, 8, 16)),
142
+ ]
143
+
144
+ def test_table_polars_from_schema_str(self):
145
+ data = ["a", None, "b", None, "c"]
146
+ df = pl.DataFrame({"a": data})
147
+ table = Table({"a": "string"})
148
+ table.update(df)
149
+ assert table.view().to_columns()["a"] == data
150
+
151
+ def test_table_polars_none(self):
152
+ data = [None, None, None]
153
+ df = pl.DataFrame({"a": data})
154
+ table = Table(df)
155
+ assert table.view().to_columns()["a"] == data
156
+
157
+ def test_table_polars_symmetric_table(self):
158
+ # make sure that updates are symmetric to table creation
159
+ df = pl.DataFrame({"a": [1, 2, 3, 4], "b": [1.5, 2.5, 3.5, 4.5]})
160
+ t1 = Table(df)
161
+ t2 = Table({"a": "integer", "b": "float"})
162
+ t2.update(df)
163
+ assert t1.view().to_columns() == {
164
+ "a": [1, 2, 3, 4],
165
+ "b": [1.5, 2.5, 3.5, 4.5],
166
+ }
167
+
168
+ def test_table_polars_symmetric_stacked_updates(self):
169
+ # make sure that updates are symmetric to table creation
170
+ df = pl.DataFrame({"a": [1, 2, 3, 4], "b": [1.5, 2.5, 3.5, 4.5]})
171
+
172
+ t1 = Table(df)
173
+ t1.update(df)
174
+
175
+ t2 = Table({"a": "integer", "b": "float"})
176
+ t2.update(df)
177
+ t2.update(df)
178
+
179
+ assert t1.view().to_columns() == {
180
+ "a": [1, 2, 3, 4, 1, 2, 3, 4],
181
+ "b": [1.5, 2.5, 3.5, 4.5, 1.5, 2.5, 3.5, 4.5],
182
+ }
183
+
184
+ @mark.skip(reason="Not supported, polars doesnt like input")
185
+ def test_table_polars_transitive(self):
186
+ # serialized output -> table -> serialized output
187
+ records = {
188
+ "a": [1, 2, 3, 4],
189
+ "b": [1.5, 2.5, 3.5, 4.5],
190
+ "c": [np.nan, np.nan, "abc", np.nan],
191
+ "d": [None, True, None, False],
192
+ "e": [
193
+ float("nan"),
194
+ datetime(2019, 7, 11, 12, 30),
195
+ float("nan"),
196
+ datetime(2019, 7, 11, 12, 30),
197
+ ],
198
+ }
199
+
200
+ df = pl.DataFrame(records, strict=False)
201
+ t1 = Table(df)
202
+ out1 = arrow_bytes_to_polars(t1.view(columns=["a", "b", "c", "d", "e"]))
203
+ t2 = Table(out1)
204
+ assert t1.schema() == t2.schema()
205
+ out2 = t2.view().to_columns()
206
+ assert t1.view().to_columns() == out2
207
+
208
+ # dtype=object should have correct inferred types
209
+
210
+ def test_table_polars_object_to_int(self):
211
+ df = pl.DataFrame({"a": [1, 2, None, 2, None, 3, 4]})
212
+ table = Table(df)
213
+ assert table.schema() == {"a": "integer"}
214
+ assert table.view().to_columns()["a"] == [1, 2, None, 2, None, 3, 4]
215
+
216
+ def test_table_polars_object_to_float(self):
217
+ df = pl.DataFrame({"a": [None, 1, None, 2, None, 3, 4]})
218
+ table = Table(df)
219
+ assert table.schema() == {"a": "integer"}
220
+ assert table.view().to_columns()["a"] == [None, 1.0, None, 2.0, None, 3.0, 4.0]
221
+
222
+ def test_table_polars_object_to_bool(self):
223
+ df = pl.DataFrame({"a": [True, False, True, False, True, False]})
224
+ table = Table(df)
225
+ assert table.schema() == {"a": "boolean"}
226
+ assert table.view().to_columns()["a"] == [True, False, True, False, True, False]
227
+
228
+ def test_table_polars_object_to_datetime(self):
229
+ df = pl.DataFrame(
230
+ {
231
+ "a": [
232
+ datetime(2019, 7, 11, 1, 2, 3),
233
+ datetime(2019, 7, 12, 1, 2, 3),
234
+ None,
235
+ ]
236
+ }
237
+ )
238
+
239
+ table = Table(df)
240
+ assert table.schema() == {"a": "datetime"}
241
+ assert table.view().to_columns()["a"] == [
242
+ datetime(2019, 7, 11, 1, 2, 3).timestamp() * 1000,
243
+ datetime(2019, 7, 12, 1, 2, 3).timestamp() * 1000,
244
+ None,
245
+ ]
246
+
247
+ def test_table_polars_object_to_str(self):
248
+ df = pl.DataFrame({"a": np.array(["abc", "def", None, "ghi"], dtype=object)})
249
+ table = Table(df)
250
+ assert table.schema() == {"a": "string"}
251
+ assert table.view().to_columns()["a"] == ["abc", "def", None, "ghi"]
@@ -0,0 +1,130 @@
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 pandas as pd
14
+ import numpy as np
15
+ from perspective import PerspectiveError
16
+ from datetime import date, datetime
17
+ from pytest import approx, mark, raises
18
+
19
+ import perspective as psp
20
+
21
+ client = psp.Server().new_local_client()
22
+ Table = client.table
23
+
24
+
25
+ def date_timestamp(date):
26
+ return int(datetime.combine(date, datetime.min.time()).timestamp()) * 1000
27
+
28
+
29
+ def compare_delta(received, expected):
30
+ """Compare an arrow-serialized row delta by constructing a Table."""
31
+ tbl = Table(received)
32
+ assert tbl.view().to_columns() == expected
33
+
34
+
35
+ class TestView(object):
36
+ def test_view_zero(self):
37
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
38
+ tbl = Table(data)
39
+ view = tbl.view()
40
+ tbl2 = Table(view)
41
+ view2 = tbl2.view()
42
+ dimms = view2.dimensions()
43
+ assert dimms["num_view_rows"] == 2
44
+ assert dimms["num_view_columns"] == 2
45
+ assert view2.schema() == {"a": "integer", "b": "integer"}
46
+ assert view2.to_records() == data
47
+
48
+ def test_view_one(self):
49
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
50
+ tbl = Table(data)
51
+ view = tbl.view(group_by=["a"])
52
+ tbl2 = Table(view)
53
+ view2 = tbl2.view()
54
+
55
+ dimms = view2.dimensions()
56
+ assert dimms["num_view_rows"] == 3
57
+ assert dimms["num_view_columns"] == 3
58
+ assert view2.schema() == {
59
+ "a (Group by 1)": "integer",
60
+ "a": "integer",
61
+ "b": "integer",
62
+ }
63
+
64
+ assert view2.to_records() == [
65
+ {"a (Group by 1)": None, "a": 4, "b": 6},
66
+ {"a (Group by 1)": 1, "a": 1, "b": 2},
67
+ {"a (Group by 1)": 3, "a": 3, "b": 4},
68
+ ]
69
+
70
+ def test_view_two(self):
71
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
72
+ tbl = Table(data)
73
+ view = tbl.view(group_by=["a"], split_by=["b"])
74
+ tbl2 = Table(view)
75
+ view2 = tbl2.view()
76
+ dimms = view2.dimensions()
77
+ assert dimms["num_view_rows"] == 3
78
+ assert dimms["num_view_columns"] == 5
79
+ assert view2.schema() == {
80
+ "a (Group by 1)": "integer",
81
+ "2|a": "integer",
82
+ "2|b": "integer",
83
+ "4|a": "integer",
84
+ "4|b": "integer",
85
+ }
86
+
87
+ assert view2.to_records() == [
88
+ {
89
+ "a (Group by 1)": None,
90
+ "2|a": 1,
91
+ "2|b": 2,
92
+ "4|a": 3,
93
+ "4|b": 4,
94
+ },
95
+ {
96
+ "a (Group by 1)": 1,
97
+ "2|a": 1,
98
+ "2|b": 2,
99
+ "4|a": None,
100
+ "4|b": None,
101
+ },
102
+ {
103
+ "a (Group by 1)": 3,
104
+ "2|a": None,
105
+ "2|b": None,
106
+ "4|a": 3,
107
+ "4|b": 4,
108
+ },
109
+ ]
110
+
111
+ def test_view_two_column_only(self):
112
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
113
+ tbl = Table(data)
114
+ view = tbl.view(split_by=["b"])
115
+ tbl2 = Table(view)
116
+ view2 = tbl2.view()
117
+ dimms = view2.dimensions()
118
+ assert dimms["num_view_rows"] == 2
119
+ assert dimms["num_view_columns"] == 4
120
+ assert view2.schema() == {
121
+ "2|a": "integer",
122
+ "2|b": "integer",
123
+ "4|a": "integer",
124
+ "4|b": "integer",
125
+ }
126
+
127
+ assert view2.to_records() == [
128
+ {"2|a": 1, "2|b": 2, "4|a": None, "4|b": None},
129
+ {"2|a": None, "2|b": None, "4|a": 3, "4|b": 4},
130
+ ]