perspective-python 3.0.0__cp39-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 (71) hide show
  1. perspective/__init__.py +81 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +26 -0
  4. perspective/handlers/aiohttp.py +54 -0
  5. perspective/handlers/starlette.py +51 -0
  6. perspective/handlers/tornado.py +61 -0
  7. perspective/perspective.pyd +0 -0
  8. perspective/psp_cffi.py +127 -0
  9. perspective/templates/exported_widget.html.jinja +35 -0
  10. perspective/tests/__init__.py +11 -0
  11. perspective/tests/conftest.py +272 -0
  12. perspective/tests/core/__init__.py +11 -0
  13. perspective/tests/core/test_async.py +413 -0
  14. perspective/tests/core/test_threadpool.py +48 -0
  15. perspective/tests/server/__init__.py +11 -0
  16. perspective/tests/server/test_server.py +1058 -0
  17. perspective/tests/server/test_session.py +55 -0
  18. perspective/tests/single_threaded/test_single_threaded.py +61 -0
  19. perspective/tests/table/__init__.py +11 -0
  20. perspective/tests/table/arrow/date32.arrow +0 -0
  21. perspective/tests/table/arrow/date64.arrow +0 -0
  22. perspective/tests/table/arrow/dict.arrow +0 -0
  23. perspective/tests/table/arrow/dict_update.arrow +0 -0
  24. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  25. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  26. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  27. perspective/tests/table/object_sequence.py +402 -0
  28. perspective/tests/table/test_delete.py +124 -0
  29. perspective/tests/table/test_exception.py +53 -0
  30. perspective/tests/table/test_leaks.py +54 -0
  31. perspective/tests/table/test_ports.py +178 -0
  32. perspective/tests/table/test_remove.py +102 -0
  33. perspective/tests/table/test_table.py +612 -0
  34. perspective/tests/table/test_table_arrow.py +452 -0
  35. perspective/tests/table/test_table_datetime.py +2409 -0
  36. perspective/tests/table/test_table_infer.py +201 -0
  37. perspective/tests/table/test_table_limit.py +41 -0
  38. perspective/tests/table/test_table_numpy.py +1022 -0
  39. perspective/tests/table/test_table_pandas.py +1018 -0
  40. perspective/tests/table/test_to_arrow.py +417 -0
  41. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  42. perspective/tests/table/test_to_format.py +1024 -0
  43. perspective/tests/table/test_update.py +545 -0
  44. perspective/tests/table/test_update_arrow.py +980 -0
  45. perspective/tests/table/test_update_pandas.py +211 -0
  46. perspective/tests/table/test_view.py +2234 -0
  47. perspective/tests/table/test_view_expression.py +1940 -0
  48. perspective/tests/viewer/__init__.py +11 -0
  49. perspective/tests/viewer/test_validate.py +69 -0
  50. perspective/tests/viewer/test_viewer.py +245 -0
  51. perspective/tests/widget/__init__.py +11 -0
  52. perspective/tests/widget/test_widget.py +278 -0
  53. perspective/tests/widget/test_widget_pandas.py +453 -0
  54. perspective/viewer/__init__.py +15 -0
  55. perspective/viewer/validate.py +22 -0
  56. perspective/viewer/viewer.py +331 -0
  57. perspective/viewer/viewer_traitlets.py +101 -0
  58. perspective/widget/__init__.py +16 -0
  59. perspective/widget/widget.py +269 -0
  60. perspective/windows-x86_64-libpsp.dll +0 -0
  61. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/install.json +5 -0
  62. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/package.json +80 -0
  63. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/253.3b3f5e7f7f7cce48f967.js +18 -0
  64. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/253.3b3f5e7f7f7cce48f967.js.LICENSE.txt +59 -0
  65. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/905.bda9d13f81bc1d4190ff.js +1 -0
  66. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/remoteEntry.23914ddf681f7cbe24fe.js +1 -0
  67. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/style.js +4 -0
  68. perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/third-party-licenses.json +16 -0
  69. perspective_python-3.0.0.dist-info/METADATA +30 -0
  70. perspective_python-3.0.0.dist-info/RECORD +71 -0
  71. perspective_python-3.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,2234 @@
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
+ dimms = view.dimensions()
41
+ assert dimms["num_view_rows"] == 2
42
+ assert dimms["num_view_columns"] == 2
43
+ assert view.schema() == {"a": "integer", "b": "integer"}
44
+ assert view.to_records() == data
45
+
46
+ def test_view_one(self):
47
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
48
+ tbl = Table(data)
49
+ view = tbl.view(group_by=["a"])
50
+ dimms = view.dimensions()
51
+ assert dimms["num_view_rows"] == 3
52
+ assert dimms["num_view_columns"] == 2
53
+ assert view.schema() == {"a": "integer", "b": "integer"}
54
+ assert view.to_records() == [
55
+ {"__ROW_PATH__": [], "a": 4, "b": 6},
56
+ {"__ROW_PATH__": [1], "a": 1, "b": 2},
57
+ {"__ROW_PATH__": [3], "a": 3, "b": 4},
58
+ ]
59
+
60
+ def test_view_two(self):
61
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
62
+ tbl = Table(data)
63
+ view = tbl.view(group_by=["a"], split_by=["b"])
64
+ dimms = view.dimensions()
65
+ assert dimms["num_view_rows"] == 3
66
+ assert dimms["num_view_columns"] == 4
67
+ assert view.schema() == {"a": "integer", "b": "integer"}
68
+ assert view.to_records() == [
69
+ {"2|a": 1, "2|b": 2, "4|a": 3, "4|b": 4, "__ROW_PATH__": []},
70
+ {"2|a": 1, "2|b": 2, "4|a": None, "4|b": None, "__ROW_PATH__": [1]},
71
+ {"2|a": None, "2|b": None, "4|a": 3, "4|b": 4, "__ROW_PATH__": [3]},
72
+ ]
73
+
74
+ def test_view_two_column_only(self):
75
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
76
+ tbl = Table(data)
77
+ view = tbl.view(split_by=["b"])
78
+ dimms = view.dimensions()
79
+ assert dimms["num_view_rows"] == 2
80
+ assert dimms["num_view_columns"] == 4
81
+ assert view.schema() == {"a": "integer", "b": "integer"}
82
+ assert view.to_records() == [
83
+ {"2|a": 1, "2|b": 2, "4|a": None, "4|b": None},
84
+ {"2|a": None, "2|b": None, "4|a": 3, "4|b": 4},
85
+ ]
86
+
87
+ # column path
88
+
89
+ def test_view_column_path_zero(self):
90
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5]}
91
+ tbl = Table(data)
92
+ view = tbl.view()
93
+ paths = view.column_paths()
94
+ assert paths == ["a", "b"]
95
+
96
+ def test_view_column_path_zero_schema(self):
97
+ data = {"a": "integer", "b": "float"}
98
+ tbl = Table(data)
99
+ view = tbl.view()
100
+ paths = view.column_paths()
101
+ assert paths == ["a", "b"]
102
+
103
+ def test_view_column_path_zero_hidden(self):
104
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5]}
105
+ tbl = Table(data)
106
+ view = tbl.view(columns=["b"])
107
+ paths = view.column_paths()
108
+ assert paths == ["b"]
109
+
110
+ def test_view_column_path_zero_respects_order(self):
111
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5]}
112
+ tbl = Table(data)
113
+ view = tbl.view(columns=["b", "a"])
114
+ paths = view.column_paths()
115
+ assert paths == ["b", "a"]
116
+
117
+ def test_view_column_path_one(self):
118
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5]}
119
+ tbl = Table(data)
120
+ view = tbl.view(group_by=["a"])
121
+ paths = view.column_paths()
122
+ assert paths == ["__ROW_PATH__", "a", "b"]
123
+
124
+ def test_view_column_path_one_numeric_names(self):
125
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5], "1234": [5, 6, 7]}
126
+ tbl = Table(data)
127
+ view = tbl.view(group_by=["a"], columns=["b", "1234", "a"])
128
+ paths = view.column_paths()
129
+ assert paths == ["__ROW_PATH__", "b", "1234", "a"]
130
+
131
+ def test_view_column_path_two(self):
132
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5]}
133
+ tbl = Table(data)
134
+ view = tbl.view(group_by=["a"], split_by=["b"])
135
+ paths = view.column_paths()
136
+ assert paths == [
137
+ "__ROW_PATH__",
138
+ "1.5|a",
139
+ "1.5|b",
140
+ "2.5|a",
141
+ "2.5|b",
142
+ "3.5|a",
143
+ "3.5|b",
144
+ ]
145
+
146
+ def test_view_column_path_two_column_only(self):
147
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5]}
148
+ tbl = Table(data)
149
+ view = tbl.view(split_by=["b"])
150
+ paths = view.column_paths()
151
+ assert paths == ["1.5|a", "1.5|b", "2.5|a", "2.5|b", "3.5|a", "3.5|b"]
152
+
153
+ def test_view_column_path_hidden_sort(self):
154
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5], "c": [3, 2, 1]}
155
+ tbl = Table(data)
156
+ view = tbl.view(columns=["a", "b"], sort=[["c", "desc"]])
157
+ paths = view.column_paths()
158
+ assert paths == ["a", "b"]
159
+
160
+ def test_view_column_path_hidden_col_sort(self):
161
+ data = {"a": [1, 2, 3], "b": [1.5, 2.5, 3.5], "c": [3, 2, 1]}
162
+ tbl = Table(data)
163
+ view = tbl.view(split_by=["a"], columns=["a", "b"], sort=[["c", "col desc"]])
164
+ paths = view.column_paths()
165
+ assert paths == ["1|a", "1|b", "2|a", "2|b", "3|a", "3|b"]
166
+
167
+ def test_view_column_path_pivot_by_bool(self):
168
+ data = {"a": [1, 2, 3], "b": [True, False, True], "c": [3, 2, 1]}
169
+ tbl = Table(data)
170
+ view = tbl.view(split_by=["b"], columns=["a", "b", "c"])
171
+ paths = view.column_paths()
172
+ assert paths == ["false|a", "false|b", "false|c", "true|a", "true|b", "true|c"]
173
+
174
+ # schema correctness
175
+
176
+ def test_string_view_schema(self):
177
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
178
+ tbl = Table(data)
179
+ view = tbl.view()
180
+ assert view.schema() == {"a": "integer", "b": "integer"}
181
+
182
+ def test_zero_view_schema(self):
183
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
184
+ tbl = Table(data)
185
+ view = tbl.view()
186
+ assert view.schema() == {"a": "integer", "b": "integer"}
187
+
188
+ def test_one_view_schema(self):
189
+ data = [{"a": "abc", "b": 2}, {"a": "abc", "b": 4}]
190
+ tbl = Table(data)
191
+ view = tbl.view(group_by=["a"], aggregates={"a": "distinct count"})
192
+ assert view.schema() == {"a": "integer", "b": "integer"}
193
+
194
+ def test_two_view_schema(self):
195
+ data = [{"a": "abc", "b": "def"}, {"a": "abc", "b": "def"}]
196
+ tbl = Table(data)
197
+ view = tbl.view(
198
+ group_by=["a"], split_by=["b"], aggregates={"a": "count", "b": "count"}
199
+ )
200
+ assert view.schema() == {"a": "integer", "b": "integer"}
201
+
202
+ # aggregates and column specification
203
+
204
+ def test_view_no_columns(self):
205
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
206
+ tbl = Table(data)
207
+ view = tbl.view(columns=[])
208
+ assert view.dimensions()["num_view_columns"] == 0
209
+ assert view.to_records() == []
210
+
211
+ def test_view_no_columns_pivoted(self):
212
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
213
+ tbl = Table(data)
214
+ view = tbl.view(group_by=["a"], columns=[])
215
+ assert view.dimensions()["num_view_columns"] == 0
216
+ assert view.to_records() == [
217
+ {"__ROW_PATH__": []},
218
+ {"__ROW_PATH__": [1]},
219
+ {"__ROW_PATH__": [3]},
220
+ ]
221
+
222
+ def test_view_specific_column(self):
223
+ data = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
224
+ tbl = Table(data)
225
+ view = tbl.view(columns=["a", "c", "d"])
226
+ assert view.dimensions()["num_view_columns"] == 3
227
+ assert view.to_records() == [{"a": 1, "c": 3, "d": 4}, {"a": 3, "c": 5, "d": 6}]
228
+
229
+ def test_view_column_order(self):
230
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
231
+ tbl = Table(data)
232
+ view = tbl.view(columns=["b", "a"])
233
+ assert view.to_records() == [{"b": 2, "a": 1}, {"b": 4, "a": 3}]
234
+
235
+ def test_view_dataframe_column_order(self):
236
+ table = Table(
237
+ pd.DataFrame(
238
+ {
239
+ "0.1": [5, 6, 7, 8],
240
+ "-0.05": [5, 6, 7, 8],
241
+ "0.0": [1, 2, 3, 4],
242
+ "-0.1": [1, 2, 3, 4],
243
+ "str": ["a", "b", "c", "d"],
244
+ }
245
+ )
246
+ )
247
+ view = table.view(columns=["-0.1", "-0.05", "0.0", "0.1"], group_by=["str"])
248
+ assert view.column_paths() == ["__ROW_PATH__", "-0.1", "-0.05", "0.0", "0.1"]
249
+
250
+ def test_view_aggregate_order_with_columns(self):
251
+ """If `columns` is provided, order is always guaranteed."""
252
+ data = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
253
+ tbl = Table(data)
254
+ view = tbl.view(
255
+ group_by=["a"],
256
+ columns=["a", "b", "c", "d"],
257
+ aggregates={"d": "avg", "c": "avg", "b": "last", "a": "last"},
258
+ )
259
+
260
+ order = ["__ROW_PATH__", "a", "b", "c", "d"]
261
+ assert view.column_paths() == order
262
+
263
+ def test_view_df_aggregate_order_with_columns(self):
264
+ """If `columns` is provided, order is always guaranteed."""
265
+ data = pd.DataFrame(
266
+ {"a": [1, 2, 3], "b": [2, 3, 4], "c": [3, 4, 5], "d": [4, 5, 6]},
267
+ columns=["d", "a", "c", "b"],
268
+ )
269
+ tbl = Table(data)
270
+ view = tbl.view(
271
+ group_by=["a"],
272
+ aggregates={"d": "avg", "c": "avg", "b": "last", "a": "last"},
273
+ )
274
+
275
+ order = ["__ROW_PATH__", "index", "d", "a", "c", "b"]
276
+ assert view.column_paths() == order
277
+
278
+ def test_view_aggregates_with_no_columns(self):
279
+ data = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
280
+ tbl = Table(data)
281
+ view = tbl.view(
282
+ group_by=["a"], aggregates={"c": "avg", "a": "last"}, columns=[]
283
+ )
284
+ assert view.column_paths() == ["__ROW_PATH__"]
285
+ assert view.to_records() == [
286
+ {"__ROW_PATH__": []},
287
+ {"__ROW_PATH__": [1]},
288
+ {"__ROW_PATH__": [3]},
289
+ ]
290
+
291
+ def test_view_aggregates_default_column_order(self):
292
+ """Order of columns are entirely determined by the `columns` kwarg. If
293
+ it is not provided, order of columns is default based on the order
294
+ of table.columns()."""
295
+ data = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
296
+ tbl = Table(data)
297
+ cols = tbl.columns()
298
+ view = tbl.view(group_by=["a"], aggregates={"c": "avg", "a": "last"})
299
+
300
+ order = ["__ROW_PATH__"] + cols
301
+ assert view.column_paths() == order
302
+
303
+ # check that default aggregates have been applied
304
+ result = view.to_columns()
305
+ assert result["b"] == [6, 2, 4]
306
+ assert result["d"] == [10, 4, 6]
307
+
308
+ # and that specified aggregates are applied
309
+ assert result["a"] == [3, 1, 3]
310
+ assert result["c"] == [4, 3, 5]
311
+
312
+ # row and split by paths
313
+ def test_view_group_by_datetime_row_paths_are_same_as_data(self, util):
314
+ """Tests row paths for datetimes in UTC. Timezone-related tests are
315
+ in the `test_table_datetime` file."""
316
+ data = {"a": [datetime(2019, 7, 11, 12, 30)], "b": [1]}
317
+ tbl = Table(data)
318
+ view = tbl.view(group_by=["a"])
319
+ data = view.to_columns()
320
+
321
+ for rp in data["__ROW_PATH__"]:
322
+ if len(rp) > 0:
323
+ assert rp[0] == util.to_timestamp(datetime(2019, 7, 11, 12, 30))
324
+
325
+ assert tbl.view().to_columns() == {
326
+ "a": [util.to_timestamp(datetime(2019, 7, 11, 12, 30))],
327
+ "b": [1],
328
+ }
329
+
330
+ def test_view_split_by_datetime_names_utc(self):
331
+ """Tests column paths for datetimes in UTC. Timezone-related tests are
332
+ in the `test_table_datetime` file."""
333
+ data = {"a": [datetime(2019, 7, 11, 12, 30)], "b": [1]}
334
+ tbl = Table(data)
335
+ view = tbl.view(split_by=["a"])
336
+ cols = view.column_paths()
337
+ assert cols == ["2019-07-11 12:30:00.000|a", "2019-07-11 12:30:00.000|b"]
338
+
339
+ # TODO: time slightly off! thinks its NYE 1969
340
+ @mark.skip # We do not support python datetimes.
341
+ def test_view_split_by_datetime_names_min(self):
342
+ """Tests column paths for datetimes in UTC. Timezone-related tests are
343
+ in the `test_table_datetime` file."""
344
+ import os
345
+
346
+ os.environ["TZ"] = "UTC"
347
+ data = {"a": [datetime.min], "b": [1]}
348
+ tbl = Table({"a": "datetime", "b": "integer"})
349
+ tbl.update(data)
350
+ view = tbl.view(split_by=["a"])
351
+ cols = view.column_paths()
352
+ assert cols == ["1970-01-01 00:00:00.000|a", "1970-01-01 00:00:00.000|b"]
353
+
354
+ @mark.skip # We dont support python datetimes.
355
+ def test_view_split_by_datetime_names_max(self):
356
+ """Tests column paths for datetimes in UTC. Timezone-related tests are
357
+ in the `test_table_datetime` file."""
358
+ data = {"a": [datetime.max], "b": [1]}
359
+ tbl = Table(data)
360
+ view = tbl.view(split_by=["a"])
361
+ cols = view.column_paths()
362
+ assert cols == ["10000-01-01 00:00:00.000|a", "10000-01-01 00:00:00.000|b"]
363
+
364
+ # aggregate
365
+
366
+ def test_view_aggregate_int(self):
367
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
368
+ tbl = Table(data)
369
+ view = tbl.view(aggregates={"a": "avg"}, group_by=["a"])
370
+ assert view.to_records() == [
371
+ {"__ROW_PATH__": [], "a": 2.0, "b": 6},
372
+ {"__ROW_PATH__": [1], "a": 1.0, "b": 2},
373
+ {"__ROW_PATH__": [3], "a": 3.0, "b": 4},
374
+ ]
375
+
376
+ def test_view_aggregate_str(self):
377
+ data = [{"a": "abc", "b": 2}, {"a": "def", "b": 4}]
378
+ tbl = Table(data)
379
+ view = tbl.view(aggregates={"a": "count"}, group_by=["a"])
380
+ assert view.to_records() == [
381
+ {"__ROW_PATH__": [], "a": 2, "b": 6},
382
+ {"__ROW_PATH__": ["abc"], "a": 1, "b": 2},
383
+ {"__ROW_PATH__": ["def"], "a": 1, "b": 4},
384
+ ]
385
+
386
+ def test_view_aggregate_datetime(self, util):
387
+ data = [
388
+ {"a": datetime(2019, 10, 1, 11, 30)},
389
+ {"a": datetime(2019, 10, 1, 11, 30)},
390
+ ]
391
+ tbl = Table(data)
392
+ view = tbl.view(aggregates={"a": "distinct count"}, group_by=["a"])
393
+ assert view.to_records() == [
394
+ {"__ROW_PATH__": [], "a": 1},
395
+ {
396
+ "__ROW_PATH__": [util.to_timestamp(datetime(2019, 10, 1, 11, 30))],
397
+ "a": 1,
398
+ },
399
+ ]
400
+
401
+ def test_view_aggregate_datetime_leading_zeroes(self, util):
402
+ data = [
403
+ {"a": datetime(2019, 1, 1, 5, 5, 5)},
404
+ {"a": datetime(2019, 1, 1, 5, 5, 5)},
405
+ ]
406
+ tbl = Table(data)
407
+ view = tbl.view(aggregates={"a": "distinct count"}, group_by=["a"])
408
+ assert view.to_records() == [
409
+ {"__ROW_PATH__": [], "a": 1},
410
+ {
411
+ "__ROW_PATH__": [util.to_timestamp(datetime(2019, 1, 1, 5, 5, 5))],
412
+ "a": 1,
413
+ },
414
+ ]
415
+
416
+ def test_view_aggregate_mean(self):
417
+ data = [
418
+ {"a": "a", "x": 1, "y": 200},
419
+ {"a": "a", "x": 2, "y": 100},
420
+ {"a": "a", "x": 3, "y": None},
421
+ ]
422
+ tbl = Table(data)
423
+ view = tbl.view(aggregates={"y": "mean"}, group_by=["a"], columns=["y"])
424
+ assert view.to_records() == [
425
+ {"__ROW_PATH__": [], "y": 300 / 2},
426
+ {"__ROW_PATH__": ["a"], "y": 300 / 2},
427
+ ]
428
+
429
+ def test_view_aggregate_mean_from_schema(self):
430
+ data = [
431
+ {"a": "a", "x": 1, "y": 200},
432
+ {"a": "a", "x": 2, "y": 100},
433
+ {"a": "a", "x": 3, "y": None},
434
+ ]
435
+ tbl = Table({"a": "string", "x": "integer", "y": "float"})
436
+ view = tbl.view(aggregates={"y": "mean"}, group_by=["a"], columns=["y"])
437
+ tbl.update(data)
438
+ assert view.to_records() == [
439
+ {"__ROW_PATH__": [], "y": 300 / 2},
440
+ {"__ROW_PATH__": ["a"], "y": 300 / 2},
441
+ ]
442
+
443
+ def test_view_aggregate_weighted_mean(self):
444
+ data = [
445
+ {"a": "a", "x": 1, "y": 200},
446
+ {"a": "a", "x": 2, "y": 100},
447
+ {"a": "a", "x": 3, "y": None},
448
+ ]
449
+ tbl = Table(data)
450
+ view = tbl.view(
451
+ aggregates={"y": ["weighted mean", "x"]}, group_by=["a"], columns=["y"]
452
+ )
453
+ assert view.to_records() == [
454
+ {"__ROW_PATH__": [], "y": (1.0 * 200 + 2 * 100) / (1.0 + 2)},
455
+ {"__ROW_PATH__": ["a"], "y": (1.0 * 200 + 2 * 100) / (1.0 + 2)},
456
+ ]
457
+
458
+ def test_view_aggregate_weighted_mean_with_negative_weights(self):
459
+ data = [
460
+ {"a": "a", "x": 1, "y": 200},
461
+ {"a": "a", "x": -2, "y": 100},
462
+ {"a": "a", "x": 3, "y": None},
463
+ ]
464
+ tbl = Table(data)
465
+ view = tbl.view(
466
+ aggregates={"y": ["weighted mean", "x"]}, group_by=["a"], columns=["y"]
467
+ )
468
+ assert view.to_records() == [
469
+ {"__ROW_PATH__": [], "y": (1 * 200 + (-2) * 100) / (1 - 2)},
470
+ {"__ROW_PATH__": ["a"], "y": (1 * 200 + (-2) * 100) / (1 - 2)},
471
+ ]
472
+
473
+ def test_view_variance(self):
474
+ data = {"x": list(np.random.rand(10)), "y": ["a" for _ in range(10)]}
475
+
476
+ table = Table(data)
477
+ view = table.view(aggregates={"x": "var"}, group_by=["y"])
478
+
479
+ result = view.to_columns()
480
+ expected = np.var(data["x"])
481
+
482
+ assert result["x"] == approx([expected, expected])
483
+
484
+ def test_view_variance_multi(self):
485
+ data = {
486
+ "a": [
487
+ 91.96,
488
+ 258.576,
489
+ 29.6,
490
+ 243.16,
491
+ 36.24,
492
+ 25.248,
493
+ 79.99,
494
+ 206.1,
495
+ 31.5,
496
+ 55.6,
497
+ ],
498
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
499
+ }
500
+ table = Table(data)
501
+ view = table.view(aggregates={"a": "var"}, group_by=["b"])
502
+
503
+ result = view.to_columns()
504
+ expected_total = np.var(data["a"])
505
+ expected_zero = np.var(
506
+ [data["a"][1], data["a"][3], data["a"][5], data["a"][7], data["a"][9]]
507
+ )
508
+ expected_one = np.var(
509
+ [data["a"][0], data["a"][2], data["a"][4], data["a"][6], data["a"][8]]
510
+ )
511
+
512
+ assert result["a"] == approx([expected_total, expected_zero, expected_one])
513
+
514
+ def test_view_variance_update_none(self):
515
+ data = {"a": [0.1, 0.5, None, 0.8], "b": [0, 1, 0, 1], "c": [1, 2, 3, 4]}
516
+ table = Table(data, index="c")
517
+ view = table.view(columns=["a"], group_by=["b"], aggregates={"a": "var"})
518
+ result = view.to_columns()
519
+ assert result["a"][0] == approx(np.var([0.1, 0.5, 0.8]))
520
+ assert result["a"][1] is None
521
+ assert result["a"][2] == approx(np.var([0.5, 0.8]))
522
+
523
+ table.update({"a": [0.3], "c": [3]})
524
+
525
+ result = view.to_columns()
526
+ assert result["a"] == approx(
527
+ [np.var([0.1, 0.5, 0.3, 0.8]), np.var([0.1, 0.3]), np.var([0.5, 0.8])]
528
+ )
529
+
530
+ table.update({"a": [None], "c": [1]})
531
+
532
+ result = view.to_columns()
533
+ assert result["a"][0] == approx(np.var([0.5, 0.3, 0.8]))
534
+ assert result["a"][1] is None
535
+ assert result["a"][2] == approx(np.var([0.5, 0.8]))
536
+
537
+ def test_view_variance_multi_update(self):
538
+ data = {
539
+ "a": [
540
+ 91.96,
541
+ 258.576,
542
+ 29.6,
543
+ 243.16,
544
+ 36.24,
545
+ 25.248,
546
+ 79.99,
547
+ 206.1,
548
+ 31.5,
549
+ 55.6,
550
+ ],
551
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
552
+ }
553
+ table = Table(data)
554
+ view = table.view(aggregates={"a": "var"}, group_by=["b"])
555
+
556
+ result = view.to_columns()
557
+ expected_total = data["a"]
558
+ expected_zero = [
559
+ data["a"][1],
560
+ data["a"][3],
561
+ data["a"][5],
562
+ data["a"][7],
563
+ data["a"][9],
564
+ ]
565
+ expected_one = [
566
+ data["a"][0],
567
+ data["a"][2],
568
+ data["a"][4],
569
+ data["a"][6],
570
+ data["a"][8],
571
+ ]
572
+
573
+ assert result["a"] == approx(
574
+ [np.var(expected_total), np.var(expected_zero), np.var(expected_one)]
575
+ )
576
+
577
+ # 2 here should result in null var because the group size is 1
578
+ update_data = {"a": [15.12, 9.102, 0.99, 12.8], "b": [1, 0, 1, 2]}
579
+ table.update(update_data)
580
+
581
+ result = view.to_columns()
582
+ expected_total += update_data["a"]
583
+ expected_zero += [update_data["a"][1]]
584
+ expected_one += [update_data["a"][0], update_data["a"][2]]
585
+
586
+ assert result["__ROW_PATH__"] == [[], [0], [1], [2]]
587
+ assert result["a"][:-1] == approx(
588
+ [np.var(expected_total), np.var(expected_zero), np.var(expected_one)]
589
+ )
590
+ assert result["a"][-1] is None
591
+
592
+ def test_view_variance_multi_update_delta(self):
593
+ data = {
594
+ "a": [
595
+ 91.96,
596
+ 258.576,
597
+ 29.6,
598
+ 243.16,
599
+ 36.24,
600
+ 25.248,
601
+ 79.99,
602
+ 206.1,
603
+ 31.5,
604
+ 55.6,
605
+ ],
606
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
607
+ }
608
+ table = Table(data)
609
+ view = table.view(aggregates={"a": "var"}, group_by=["b"])
610
+
611
+ result = view.to_columns()
612
+ expected_total = data["a"]
613
+ expected_zero = [
614
+ data["a"][1],
615
+ data["a"][3],
616
+ data["a"][5],
617
+ data["a"][7],
618
+ data["a"][9],
619
+ ]
620
+ expected_one = [
621
+ data["a"][0],
622
+ data["a"][2],
623
+ data["a"][4],
624
+ data["a"][6],
625
+ data["a"][8],
626
+ ]
627
+
628
+ assert result["a"] == approx(
629
+ [np.var(expected_total), np.var(expected_zero), np.var(expected_one)]
630
+ )
631
+
632
+ # 2 here should result in null var because the group size is 1
633
+ update_data = {"a": [15.12, 9.102, 0.99, 12.8], "b": [1, 0, 1, 2]}
634
+
635
+ def cb1(port_id, delta):
636
+ table2 = Table(delta)
637
+ view2 = table2.view()
638
+ result = view2.to_columns()
639
+
640
+ flat_view = table.view()
641
+ flat_data = flat_view.to_columns()
642
+ result = view.to_columns()
643
+
644
+ expected_total = flat_data["a"]
645
+ expected_zero = []
646
+ expected_one = []
647
+
648
+ for i, num in enumerate(expected_total):
649
+ if flat_data["b"][i] == 1:
650
+ expected_one.append(num)
651
+ elif flat_data["b"][i] == 0:
652
+ expected_zero.append(num)
653
+
654
+ assert result["a"][0] == approx(np.var(expected_total))
655
+ assert result["a"][1] == approx(np.var(expected_zero))
656
+ assert result["a"][2] == approx(np.var(expected_one))
657
+ assert result["a"][3] is None
658
+
659
+ view.on_update(cb1, mode="row")
660
+
661
+ table.update(update_data)
662
+
663
+ def test_view_variance_multi_update_indexed(self):
664
+ data = {
665
+ "a": [
666
+ 91.96,
667
+ 258.576,
668
+ 29.6,
669
+ 243.16,
670
+ 36.24,
671
+ 25.248,
672
+ 79.99,
673
+ 206.1,
674
+ 31.5,
675
+ 55.6,
676
+ ],
677
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
678
+ "c": [i for i in range(10)],
679
+ }
680
+ table = Table(data, index="c")
681
+ view = table.view(aggregates={"a": "var"}, group_by=["b"])
682
+
683
+ result = view.to_columns()
684
+ expected_total = data["a"]
685
+ expected_zero = [
686
+ data["a"][1],
687
+ data["a"][3],
688
+ data["a"][5],
689
+ data["a"][7],
690
+ data["a"][9],
691
+ ]
692
+ expected_one = [
693
+ data["a"][0],
694
+ data["a"][2],
695
+ data["a"][4],
696
+ data["a"][6],
697
+ data["a"][8],
698
+ ]
699
+
700
+ assert result["a"] == approx(
701
+ [np.var(expected_total), np.var(expected_zero), np.var(expected_one)]
702
+ )
703
+
704
+ # "b" = 2 here should result in null var because the group size is 1
705
+ update_data = {
706
+ "a": [15.12, 9.102, 0.99, 12.8],
707
+ "b": [1, 0, 1, 2],
708
+ "c": [1, 5, 2, 7],
709
+ }
710
+
711
+ table.update(update_data)
712
+
713
+ result = view.to_columns()
714
+
715
+ view2 = table.view()
716
+ flat_data = view2.to_columns()
717
+ expected_total = flat_data["a"]
718
+
719
+ expected_zero = []
720
+ expected_one = []
721
+
722
+ for i, val in enumerate(flat_data["a"]):
723
+ if flat_data["b"][i] == 1:
724
+ expected_one.append(val)
725
+ elif flat_data["b"][i] == 0:
726
+ expected_zero.append(val)
727
+
728
+ assert result["__ROW_PATH__"] == [[], [0], [1], [2]]
729
+ assert result["a"][:-1] == approx(
730
+ [np.var(expected_total), np.var(expected_zero), np.var(expected_one)]
731
+ )
732
+ assert result["a"][-1] is None
733
+
734
+ def test_view_variance_multi_update_indexed_delta(self):
735
+ data = {
736
+ "a": [
737
+ 91.96,
738
+ 258.576,
739
+ 29.6,
740
+ 243.16,
741
+ 36.24,
742
+ 25.248,
743
+ 79.99,
744
+ 206.1,
745
+ 31.5,
746
+ 55.6,
747
+ ],
748
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
749
+ "c": [i for i in range(10)],
750
+ }
751
+ table = Table(data, index="c")
752
+ view = table.view(
753
+ aggregates={"a": "var", "b": "last", "c": "last"}, group_by=["b"]
754
+ )
755
+
756
+ result = view.to_columns()
757
+ expected_total = data["a"]
758
+ expected_zero = [
759
+ data["a"][1],
760
+ data["a"][3],
761
+ data["a"][5],
762
+ data["a"][7],
763
+ data["a"][9],
764
+ ]
765
+ expected_one = [
766
+ data["a"][0],
767
+ data["a"][2],
768
+ data["a"][4],
769
+ data["a"][6],
770
+ data["a"][8],
771
+ ]
772
+
773
+ assert result["a"] == approx(
774
+ [np.var(expected_total), np.var(expected_zero), np.var(expected_one)]
775
+ )
776
+
777
+ # 2 here should result in null var because the group size is 1
778
+ update_data = {
779
+ "a": [15.12, 9.102, 0.99, 12.8],
780
+ "b": [1, 0, 1, 2],
781
+ "c": [0, 4, 1, 6],
782
+ }
783
+
784
+ def cb1(port_id, delta):
785
+ table2 = Table(delta)
786
+ view2 = table2.view()
787
+ result = view2.to_columns()
788
+
789
+ flat_view = table.view()
790
+ flat_result = flat_view.to_columns()
791
+
792
+ new_a = flat_result["a"]
793
+ b = flat_result["b"]
794
+ expected_zero = []
795
+ expected_one = []
796
+
797
+ for i, num in enumerate(new_a):
798
+ if b[i] == 0:
799
+ expected_zero.append(num)
800
+ elif b[i] == 1:
801
+ expected_one.append(num)
802
+
803
+ assert result["a"][0] == approx(np.var(new_a))
804
+ assert result["a"][1] == approx(np.var(expected_zero))
805
+ assert result["a"][2] == approx(np.var(expected_one))
806
+ assert result["a"][3] is None
807
+ assert result["b"] == [2, 0, 1, 2]
808
+ assert result["c"] == [6, 9, 8, 6]
809
+
810
+ view.on_update(cb1, mode="row")
811
+
812
+ table.update(update_data)
813
+
814
+ def test_view_variance_less_than_two(self):
815
+ data = {"a": list(np.random.rand(10)), "b": [i for i in range(10)]}
816
+
817
+ table = Table(data)
818
+ view = table.view(aggregates={"a": "var"}, group_by=["b"])
819
+
820
+ result = view.to_columns()
821
+ assert result["a"][0] == approx(np.var(data["a"]))
822
+ assert result["a"][1:] == [None] * 10
823
+
824
+ def test_view_variance_normal_distribution(self):
825
+ data = {"a": list(np.random.standard_normal(100)), "b": [1] * 100}
826
+
827
+ table = Table(data)
828
+ view = table.view(aggregates={"a": "var"}, group_by=["b"])
829
+
830
+ result = view.to_columns()
831
+ assert result["a"] == approx([np.var(data["a"]), np.var(data["a"])])
832
+
833
+ def test_view_standard_deviation(self):
834
+ data = {"x": list(np.random.rand(10)), "y": ["a" for _ in range(10)]}
835
+
836
+ table = Table(data)
837
+ view = table.view(aggregates={"x": "stddev"}, group_by=["y"])
838
+
839
+ result = view.to_columns()
840
+ expected = np.std(data["x"])
841
+
842
+ assert result["x"] == approx([expected, expected])
843
+
844
+ def test_view_standard_deviation_multi(self):
845
+ data = {
846
+ "a": [
847
+ 91.96,
848
+ 258.576,
849
+ 29.6,
850
+ 243.16,
851
+ 36.24,
852
+ 25.248,
853
+ 79.99,
854
+ 206.1,
855
+ 31.5,
856
+ 55.6,
857
+ ],
858
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
859
+ }
860
+ table = Table(data)
861
+ view = table.view(aggregates={"a": "stddev"}, group_by=["b"])
862
+
863
+ result = view.to_columns()
864
+ expected_total = np.std(data["a"])
865
+ expected_zero = np.std(
866
+ [data["a"][1], data["a"][3], data["a"][5], data["a"][7], data["a"][9]]
867
+ )
868
+ expected_one = np.std(
869
+ [data["a"][0], data["a"][2], data["a"][4], data["a"][6], data["a"][8]]
870
+ )
871
+
872
+ assert result["a"] == approx([expected_total, expected_zero, expected_one])
873
+
874
+ def test_view_standard_deviation_update_none(self):
875
+ data = {"a": [0.1, 0.5, None, 0.8], "b": [0, 1, 0, 1], "c": [1, 2, 3, 4]}
876
+ table = Table(data, index="c")
877
+ view = table.view(columns=["a"], group_by=["b"], aggregates={"a": "stddev"})
878
+ result = view.to_columns()
879
+ assert result["a"][0] == approx(np.std([0.1, 0.5, 0.8]))
880
+ assert result["a"][1] is None
881
+ assert result["a"][2] == approx(np.std([0.5, 0.8]))
882
+
883
+ table.update({"a": [0.3], "c": [3]})
884
+
885
+ result = view.to_columns()
886
+ assert result["a"] == approx(
887
+ [np.std([0.1, 0.5, 0.3, 0.8]), np.std([0.1, 0.3]), np.std([0.5, 0.8])]
888
+ )
889
+
890
+ table.update({"a": [None], "c": [1]})
891
+
892
+ result = view.to_columns()
893
+ assert result["a"][0] == approx(np.std([0.5, 0.3, 0.8]))
894
+ assert result["a"][1] is None
895
+ assert result["a"][2] == approx(np.std([0.5, 0.8]))
896
+
897
+ def test_view_standard_deviation_multi_update(self):
898
+ data = {
899
+ "a": [
900
+ 91.96,
901
+ 258.576,
902
+ 29.6,
903
+ 243.16,
904
+ 36.24,
905
+ 25.248,
906
+ 79.99,
907
+ 206.1,
908
+ 31.5,
909
+ 55.6,
910
+ ],
911
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
912
+ }
913
+ table = Table(data)
914
+ view = table.view(aggregates={"a": "stddev"}, group_by=["b"])
915
+
916
+ result = view.to_columns()
917
+ expected_total = data["a"]
918
+ expected_zero = [
919
+ data["a"][1],
920
+ data["a"][3],
921
+ data["a"][5],
922
+ data["a"][7],
923
+ data["a"][9],
924
+ ]
925
+ expected_one = [
926
+ data["a"][0],
927
+ data["a"][2],
928
+ data["a"][4],
929
+ data["a"][6],
930
+ data["a"][8],
931
+ ]
932
+
933
+ assert result["a"] == approx(
934
+ [np.std(expected_total), np.std(expected_zero), np.std(expected_one)]
935
+ )
936
+
937
+ # 2 here should result in null stddev because the group size is 1
938
+ update_data = {"a": [15.12, 9.102, 0.99, 12.8], "b": [1, 0, 1, 2]}
939
+ table.update(update_data)
940
+
941
+ result = view.to_columns()
942
+ expected_total += update_data["a"]
943
+ expected_zero += [update_data["a"][1]]
944
+ expected_one += [update_data["a"][0], update_data["a"][2]]
945
+
946
+ assert result["__ROW_PATH__"] == [[], [0], [1], [2]]
947
+ assert result["a"][:-1] == approx(
948
+ [np.std(expected_total), np.std(expected_zero), np.std(expected_one)]
949
+ )
950
+ assert result["a"][-1] is None
951
+
952
+ def test_view_standard_deviation_multi_update_delta(self):
953
+ data = {
954
+ "a": [
955
+ 91.96,
956
+ 258.576,
957
+ 29.6,
958
+ 243.16,
959
+ 36.24,
960
+ 25.248,
961
+ 79.99,
962
+ 206.1,
963
+ 31.5,
964
+ 55.6,
965
+ ],
966
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
967
+ }
968
+ table = Table(data)
969
+ view = table.view(aggregates={"a": "stddev"}, group_by=["b"])
970
+
971
+ result = view.to_columns()
972
+ expected_total = data["a"]
973
+ expected_zero = [
974
+ data["a"][1],
975
+ data["a"][3],
976
+ data["a"][5],
977
+ data["a"][7],
978
+ data["a"][9],
979
+ ]
980
+ expected_one = [
981
+ data["a"][0],
982
+ data["a"][2],
983
+ data["a"][4],
984
+ data["a"][6],
985
+ data["a"][8],
986
+ ]
987
+
988
+ assert result["a"] == approx(
989
+ [np.std(expected_total), np.std(expected_zero), np.std(expected_one)]
990
+ )
991
+
992
+ # 2 here should result in null stddev because the group size is 1
993
+ update_data = {"a": [15.12, 9.102, 0.99, 12.8], "b": [1, 0, 1, 2]}
994
+
995
+ def cb1(port_id, delta):
996
+ table2 = Table(delta)
997
+ view2 = table2.view()
998
+ result = view2.to_columns()
999
+
1000
+ flat_view = table.view()
1001
+ flat_data = flat_view.to_columns()
1002
+ result = view.to_columns()
1003
+
1004
+ expected_total = flat_data["a"]
1005
+ expected_zero = []
1006
+ expected_one = []
1007
+
1008
+ for i, num in enumerate(expected_total):
1009
+ if flat_data["b"][i] == 1:
1010
+ expected_one.append(num)
1011
+ elif flat_data["b"][i] == 0:
1012
+ expected_zero.append(num)
1013
+
1014
+ assert result["a"][0] == approx(np.std(expected_total))
1015
+ assert result["a"][1] == approx(np.std(expected_zero))
1016
+ assert result["a"][2] == approx(np.std(expected_one))
1017
+ assert result["a"][3] is None
1018
+
1019
+ view.on_update(cb1, mode="row")
1020
+
1021
+ table.update(update_data)
1022
+
1023
+ def test_view_standard_deviation_multi_update_indexed(self):
1024
+ data = {
1025
+ "a": [
1026
+ 91.96,
1027
+ 258.576,
1028
+ 29.6,
1029
+ 243.16,
1030
+ 36.24,
1031
+ 25.248,
1032
+ 79.99,
1033
+ 206.1,
1034
+ 31.5,
1035
+ 55.6,
1036
+ ],
1037
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
1038
+ "c": [i for i in range(10)],
1039
+ }
1040
+ table = Table(data, index="c")
1041
+ view = table.view(aggregates={"a": "stddev"}, group_by=["b"])
1042
+
1043
+ result = view.to_columns()
1044
+ expected_total = data["a"]
1045
+ expected_zero = [
1046
+ data["a"][1],
1047
+ data["a"][3],
1048
+ data["a"][5],
1049
+ data["a"][7],
1050
+ data["a"][9],
1051
+ ]
1052
+ expected_one = [
1053
+ data["a"][0],
1054
+ data["a"][2],
1055
+ data["a"][4],
1056
+ data["a"][6],
1057
+ data["a"][8],
1058
+ ]
1059
+
1060
+ assert result["a"] == approx(
1061
+ [np.std(expected_total), np.std(expected_zero), np.std(expected_one)]
1062
+ )
1063
+
1064
+ # "b" = 2 here should result in null stddev because the group size is 1
1065
+ update_data = {
1066
+ "a": [15.12, 9.102, 0.99, 12.8],
1067
+ "b": [1, 0, 1, 2],
1068
+ "c": [1, 5, 2, 7],
1069
+ }
1070
+
1071
+ table.update(update_data)
1072
+
1073
+ result = view.to_columns()
1074
+
1075
+ view2 = table.view()
1076
+ flat_data = view2.to_columns()
1077
+ expected_total = flat_data["a"]
1078
+
1079
+ expected_zero = []
1080
+ expected_one = []
1081
+
1082
+ for i, val in enumerate(flat_data["a"]):
1083
+ if flat_data["b"][i] == 1:
1084
+ expected_one.append(val)
1085
+ elif flat_data["b"][i] == 0:
1086
+ expected_zero.append(val)
1087
+
1088
+ assert result["__ROW_PATH__"] == [[], [0], [1], [2]]
1089
+ assert result["a"][:-1] == approx(
1090
+ [np.std(expected_total), np.std(expected_zero), np.std(expected_one)]
1091
+ )
1092
+ assert result["a"][-1] is None
1093
+
1094
+ def test_view_standard_deviation_multi_update_indexed_delta(self):
1095
+ data = {
1096
+ "a": [
1097
+ 91.96,
1098
+ 258.576,
1099
+ 29.6,
1100
+ 243.16,
1101
+ 36.24,
1102
+ 25.248,
1103
+ 79.99,
1104
+ 206.1,
1105
+ 31.5,
1106
+ 55.6,
1107
+ ],
1108
+ "b": [1 if i % 2 == 0 else 0 for i in range(10)],
1109
+ "c": [i for i in range(10)],
1110
+ }
1111
+ table = Table(data, index="c")
1112
+ view = table.view(
1113
+ aggregates={"a": "stddev", "b": "last", "c": "last"}, group_by=["b"]
1114
+ )
1115
+
1116
+ result = view.to_columns()
1117
+ expected_total = data["a"]
1118
+ expected_zero = [
1119
+ data["a"][1],
1120
+ data["a"][3],
1121
+ data["a"][5],
1122
+ data["a"][7],
1123
+ data["a"][9],
1124
+ ]
1125
+ expected_one = [
1126
+ data["a"][0],
1127
+ data["a"][2],
1128
+ data["a"][4],
1129
+ data["a"][6],
1130
+ data["a"][8],
1131
+ ]
1132
+
1133
+ assert result["a"] == approx(
1134
+ [np.std(expected_total), np.std(expected_zero), np.std(expected_one)]
1135
+ )
1136
+
1137
+ # 2 here should result in null stddev because the group size is 1
1138
+ update_data = {
1139
+ "a": [15.12, 9.102, 0.99, 12.8],
1140
+ "b": [1, 0, 1, 2],
1141
+ "c": [0, 4, 1, 6],
1142
+ }
1143
+
1144
+ def cb1(port_id, delta):
1145
+ table2 = Table(delta)
1146
+ view2 = table2.view()
1147
+ result = view2.to_columns()
1148
+
1149
+ flat_view = table.view()
1150
+ flat_result = flat_view.to_columns()
1151
+
1152
+ new_a = flat_result["a"]
1153
+ b = flat_result["b"]
1154
+ expected_zero = []
1155
+ expected_one = []
1156
+
1157
+ for i, num in enumerate(new_a):
1158
+ if b[i] == 0:
1159
+ expected_zero.append(num)
1160
+ elif b[i] == 1:
1161
+ expected_one.append(num)
1162
+
1163
+ assert result["a"][0] == approx(np.std(new_a))
1164
+ assert result["a"][1] == approx(np.std(expected_zero))
1165
+ assert result["a"][2] == approx(np.std(expected_one))
1166
+ assert result["a"][3] is None
1167
+ assert result["b"] == [2, 0, 1, 2]
1168
+ assert result["c"] == [6, 9, 8, 6]
1169
+
1170
+ view.on_update(cb1, mode="row")
1171
+
1172
+ table.update(update_data)
1173
+
1174
+ def test_view_standard_deviation_less_than_two(self):
1175
+ data = {"a": list(np.random.rand(10)), "b": [i for i in range(10)]}
1176
+
1177
+ table = Table(data)
1178
+ view = table.view(aggregates={"a": "stddev"}, group_by=["b"])
1179
+
1180
+ result = view.to_columns()
1181
+ assert result["a"][0] == approx(np.std(data["a"]))
1182
+ assert result["a"][1:] == [None] * 10
1183
+
1184
+ def test_view_standard_deviation_normal_distribution(self):
1185
+ data = {"a": list(np.random.standard_normal(100)), "b": [1] * 100}
1186
+
1187
+ table = Table(data)
1188
+ view = table.view(aggregates={"a": "stddev"}, group_by=["b"])
1189
+
1190
+ result = view.to_columns()
1191
+ assert result["a"] == approx([np.std(data["a"]), np.std(data["a"])])
1192
+
1193
+ # sort
1194
+
1195
+ def test_view_sort_int(self):
1196
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1197
+ tbl = Table(data)
1198
+ view = tbl.view(sort=[["a", "desc"]])
1199
+ assert view.to_records() == [{"a": 3, "b": 4}, {"a": 1, "b": 2}]
1200
+
1201
+ def test_view_sort_float(self):
1202
+ data = [{"a": 1.1, "b": 2}, {"a": 1.2, "b": 4}]
1203
+ tbl = Table(data)
1204
+ view = tbl.view(sort=[["a", "desc"]])
1205
+ assert view.to_records() == [{"a": 1.2, "b": 4}, {"a": 1.1, "b": 2}]
1206
+
1207
+ def test_view_sort_string(self):
1208
+ data = [{"a": "abc", "b": 2}, {"a": "def", "b": 4}]
1209
+ tbl = Table(data)
1210
+ view = tbl.view(sort=[["a", "desc"]])
1211
+ assert view.to_records() == [{"a": "def", "b": 4}, {"a": "abc", "b": 2}]
1212
+
1213
+ def test_view_sort_date(self, util):
1214
+ data = [{"a": date(2019, 7, 11), "b": 2}, {"a": date(2019, 7, 12), "b": 4}]
1215
+ tbl = Table(data)
1216
+ view = tbl.view(sort=[["a", "desc"]])
1217
+ assert view.to_records() == [
1218
+ {"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 4},
1219
+ {"a": util.to_timestamp(datetime(2019, 7, 11)), "b": 2},
1220
+ ]
1221
+
1222
+ def test_view_sort_datetime(self, util):
1223
+ data = [
1224
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1225
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1226
+ ]
1227
+ tbl = Table(data)
1228
+ view = tbl.view(sort=[["a", "desc"]])
1229
+ assert view.to_records() == [
1230
+ {"a": util.to_timestamp(datetime(2019, 7, 11, 8, 16)), "b": 4},
1231
+ {"a": util.to_timestamp(datetime(2019, 7, 11, 8, 15)), "b": 2},
1232
+ ]
1233
+
1234
+ def test_view_sort_hidden(self):
1235
+ data = [{"a": 1.1, "b": 2}, {"a": 1.2, "b": 4}]
1236
+ tbl = Table(data)
1237
+ view = tbl.view(sort=[["a", "desc"]], columns=["b"])
1238
+ assert view.to_records() == [{"b": 4}, {"b": 2}]
1239
+
1240
+ def test_view_sort_avg_nan(self):
1241
+ data = {
1242
+ "w": [3.5, 4.5, None, None, None, None, 1.5, 2.5],
1243
+ "x": [1, 2, 3, 4, 4, 3, 2, 1],
1244
+ "y": ["a", "b", "c", "d", "e", "f", "g", "h"],
1245
+ }
1246
+ tbl = Table(data)
1247
+ view = tbl.view(
1248
+ columns=["x", "w"],
1249
+ group_by=["y"],
1250
+ sort=[["w", "asc"]],
1251
+ aggregates={"w": "avg", "x": "unique"},
1252
+ )
1253
+ assert view.to_columns() == {
1254
+ "__ROW_PATH__": [
1255
+ [],
1256
+ ["c"],
1257
+ ["d"],
1258
+ ["e"],
1259
+ ["f"],
1260
+ ["g"],
1261
+ ["h"],
1262
+ ["a"],
1263
+ ["b"],
1264
+ ],
1265
+ "w": [3, None, None, None, None, 1.5, 2.5, 3.5, 4.5],
1266
+ "x": [None, 3, 4, 4, 3, 2, 1, 1, 2],
1267
+ }
1268
+
1269
+ def test_view_sort_sum_nan(self):
1270
+ data = {
1271
+ "w": [3.5, 4.5, None, None, None, None, 1.5, 2.5],
1272
+ "x": [1, 2, 3, 4, 4, 3, 2, 1],
1273
+ "y": ["a", "b", "c", "d", "e", "f", "g", "h"],
1274
+ }
1275
+ tbl = Table(data)
1276
+ view = tbl.view(
1277
+ columns=["x", "w"],
1278
+ group_by=["y"],
1279
+ sort=[["w", "asc"]],
1280
+ aggregates={"w": "sum", "x": "unique"},
1281
+ )
1282
+ assert view.to_columns() == {
1283
+ "__ROW_PATH__": [
1284
+ [],
1285
+ ["c"],
1286
+ ["d"],
1287
+ ["e"],
1288
+ ["f"],
1289
+ ["g"],
1290
+ ["h"],
1291
+ ["a"],
1292
+ ["b"],
1293
+ ],
1294
+ "w": [12, 0, 0, 0, 0, 1.5, 2.5, 3.5, 4.5],
1295
+ "x": [None, 3, 4, 4, 3, 2, 1, 1, 2],
1296
+ }
1297
+
1298
+ def test_view_sort_unique_nan(self):
1299
+ data = {
1300
+ "w": [3.5, 4.5, None, None, None, None, 1.5, 2.5],
1301
+ "x": [1, 2, 3, 4, 4, 3, 2, 1],
1302
+ "y": ["a", "b", "c", "d", "e", "f", "g", "h"],
1303
+ }
1304
+ tbl = Table(data)
1305
+ view = tbl.view(
1306
+ columns=["x", "w"],
1307
+ group_by=["y"],
1308
+ sort=[["w", "asc"]],
1309
+ aggregates={"w": "unique", "x": "unique"},
1310
+ )
1311
+ assert view.to_columns() == {
1312
+ "__ROW_PATH__": [
1313
+ [],
1314
+ ["c"],
1315
+ ["d"],
1316
+ ["e"],
1317
+ ["f"],
1318
+ ["g"],
1319
+ ["h"],
1320
+ ["a"],
1321
+ ["b"],
1322
+ ],
1323
+ "w": [None, None, None, None, None, 1.5, 2.5, 3.5, 4.5],
1324
+ "x": [None, 3, 4, 4, 3, 2, 1, 1, 2],
1325
+ }
1326
+
1327
+ # filter
1328
+
1329
+ def test_view_filter_int_eq(self):
1330
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1331
+ tbl = Table(data)
1332
+ view = tbl.view(filter=[["a", "==", 1]])
1333
+ assert view.to_records() == [{"a": 1, "b": 2}]
1334
+
1335
+ def test_view_filter_int_neq(self):
1336
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1337
+ tbl = Table(data)
1338
+ view = tbl.view(filter=[["a", "!=", 1]])
1339
+ assert view.to_records() == [{"a": 3, "b": 4}]
1340
+
1341
+ def test_view_filter_int_gt(self):
1342
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1343
+ tbl = Table(data)
1344
+ view = tbl.view(filter=[["a", ">", 1]])
1345
+ assert view.to_records() == [{"a": 3, "b": 4}]
1346
+
1347
+ def test_view_filter_int_lt(self):
1348
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1349
+ tbl = Table(data)
1350
+ view = tbl.view(filter=[["a", "<", 3]])
1351
+ assert view.to_records() == [{"a": 1, "b": 2}]
1352
+
1353
+ def test_view_filter_float_eq(self):
1354
+ data = [{"a": 1.1, "b": 2}, {"a": 1.2, "b": 4}]
1355
+ tbl = Table(data)
1356
+ view = tbl.view(filter=[["a", "==", 1.2]])
1357
+ assert view.to_records() == [{"a": 1.2, "b": 4}]
1358
+
1359
+ def test_view_filter_float_neq(self):
1360
+ data = [{"a": 1.1, "b": 2}, {"a": 1.2, "b": 4}]
1361
+ tbl = Table(data)
1362
+ view = tbl.view(filter=[["a", "!=", 1.2]])
1363
+ assert view.to_records() == [{"a": 1.1, "b": 2}]
1364
+
1365
+ def test_view_filter_string_eq(self):
1366
+ data = [{"a": "abc", "b": 2}, {"a": "def", "b": 4}]
1367
+ tbl = Table(data)
1368
+ view = tbl.view(filter=[["a", "==", "def"]])
1369
+ assert view.to_records() == [{"a": "def", "b": 4}]
1370
+
1371
+ def test_view_filter_string_neq(self):
1372
+ data = [{"a": "abc", "b": 2}, {"a": "def", "b": 4}]
1373
+ tbl = Table(data)
1374
+ view = tbl.view(filter=[["a", "!=", "def"]])
1375
+ assert view.to_records() == [{"a": "abc", "b": 2}]
1376
+
1377
+ def test_view_filter_string_gt(self):
1378
+ data = [{"a": "abc", "b": 2}, {"a": "def", "b": 4}]
1379
+ tbl = Table(data)
1380
+ view = tbl.view(filter=[["a", ">", "abc"]])
1381
+ assert view.to_records() == [{"a": "def", "b": 4}]
1382
+
1383
+ def test_view_filter_string_lt(self):
1384
+ data = [{"a": "abc", "b": 2}, {"a": "def", "b": 4}]
1385
+ tbl = Table(data)
1386
+ view = tbl.view(filter=[["a", "<", "def"]])
1387
+ assert view.to_records() == [{"a": "abc", "b": 2}]
1388
+
1389
+ # @mark.skip # We do not support using `datetime.date` in operators.
1390
+ def test_view_filter_date_eq(self, util):
1391
+ data = [{"a": date(2019, 7, 11), "b": 2}, {"a": date(2019, 7, 12), "b": 4}]
1392
+ tbl = Table(data)
1393
+ view = tbl.view(filter=[["a", "==", str(date(2019, 7, 12))]])
1394
+ assert view.to_records() == [
1395
+ {"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 4}
1396
+ ]
1397
+
1398
+ # @mark.skip # We do not support using `datetime.date` in operators.
1399
+ def test_view_filter_date_neq(self, util):
1400
+ data = [{"a": date(2019, 7, 11), "b": 2}, {"a": date(2019, 7, 12), "b": 4}]
1401
+ tbl = Table(data)
1402
+ view = tbl.view(filter=[["a", "!=", str(date(2019, 7, 12))]])
1403
+ assert view.to_records() == [
1404
+ {"a": util.to_timestamp(datetime(2019, 7, 11)), "b": 2}
1405
+ ]
1406
+
1407
+ def test_view_filter_date_str_eq(self, util):
1408
+ data = [{"a": date(2019, 7, 11), "b": 2}, {"a": date(2019, 7, 12), "b": 4}]
1409
+ tbl = Table(data)
1410
+ view = tbl.view(filter=[["a", "==", "2019/7/12"]])
1411
+ assert view.to_records() == [
1412
+ {"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 4}
1413
+ ]
1414
+
1415
+ def test_view_filter_date_str_neq(self, util):
1416
+ data = [{"a": date(2019, 7, 11), "b": 2}, {"a": date(2019, 7, 12), "b": 4}]
1417
+ tbl = Table(data)
1418
+ view = tbl.view(filter=[["a", "!=", "2019/7/12"]])
1419
+ assert view.to_records() == [
1420
+ {"a": util.to_timestamp(datetime(2019, 7, 11)), "b": 2}
1421
+ ]
1422
+
1423
+ @mark.skip # We do not support using `datetime.datetime` in operators
1424
+ def test_view_filter_datetime_eq(self, util):
1425
+ data = [
1426
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1427
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1428
+ ]
1429
+ tbl = Table(data)
1430
+ view = tbl.view(filter=[["a", "==", datetime(2019, 7, 11, 8, 15)]])
1431
+ assert view.to_records() == [
1432
+ {"a": util.to_timestamp(datetime(2019, 7, 11, () * 1000)), "b": 2}
1433
+ ]
1434
+
1435
+ @mark.skip # We do not support using `datetime.datetime` in operators
1436
+ def test_view_filter_datetime_neq(self, util):
1437
+ data = [
1438
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1439
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1440
+ ]
1441
+ tbl = Table(data)
1442
+ view = tbl.view(filter=[["a", "!=", datetime(2019, 7, 11, 8, 15)]])
1443
+ assert view.to_records() == [
1444
+ {"a": util.to_timestamp(datetime(2019, 7, 11, () * 1000)), "b": 4}
1445
+ ]
1446
+
1447
+ @mark.skip # We do not support numpy anymore
1448
+ def test_view_filter_datetime_np_eq(self, util):
1449
+ data = [
1450
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1451
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1452
+ ]
1453
+ tbl = Table(data)
1454
+ view = tbl.view(
1455
+ filter=[["a", "==", np.datetime64(datetime(2019, 7, 11, 8, 15))]]
1456
+ )
1457
+ assert view.to_records() == [
1458
+ {"a": util.to_timestamp(datetime(2019, 7, 11, () * 1000)), "b": 2}
1459
+ ]
1460
+
1461
+ @mark.skip # We do not support numpy anymore.
1462
+ def test_view_filter_datetime_np_neq(self, util):
1463
+ data = [
1464
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1465
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1466
+ ]
1467
+ tbl = Table(data)
1468
+ view = tbl.view(
1469
+ filter=[["a", "!=", np.datetime64(datetime(2019, 7, 11, 8, 15))]]
1470
+ )
1471
+ assert view.to_records() == [
1472
+ {"a": util.to_timestamp(datetime(2019, 7, 11, () * 1000)), "b": 4}
1473
+ ]
1474
+
1475
+ def test_view_filter_datetime_str_eq(self, util):
1476
+ data = [
1477
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1478
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1479
+ ]
1480
+ tbl = Table(data)
1481
+ view = tbl.view(filter=[["a", "==", "2019/7/11 8:15"]])
1482
+ assert view.to_records() == [
1483
+ {"a": util.to_timestamp(datetime(2019, 7, 11, 8, 15)), "b": 2}
1484
+ ]
1485
+
1486
+ def test_view_filter_datetime_str_neq(self, util):
1487
+ data = [
1488
+ {"a": datetime(2019, 7, 11, 8, 15), "b": 2},
1489
+ {"a": datetime(2019, 7, 11, 8, 16), "b": 4},
1490
+ ]
1491
+ tbl = Table(data)
1492
+ view = tbl.view(filter=[["a", "!=", "2019/7/11 8:15"]])
1493
+ assert view.to_records() == [
1494
+ {"a": util.to_timestamp(datetime(2019, 7, 11, 8, 16)), "b": 4}
1495
+ ]
1496
+
1497
+ def test_view_filter_string_is_none(self):
1498
+ data = [{"a": None, "b": 2}, {"a": "abc", "b": 4}]
1499
+ tbl = Table(data)
1500
+ view = tbl.view(
1501
+ filter=[["a", "is null", ""]]
1502
+ ) # XXX: Having to add this "" is not soooo great.
1503
+ assert view.to_records() == [{"a": None, "b": 2}]
1504
+
1505
+ def test_view_filter_string_is_not_none(self):
1506
+ data = [{"a": None, "b": 2}, {"a": "abc", "b": 4}]
1507
+ tbl = Table(data)
1508
+ view = tbl.view(
1509
+ filter=[["a", "is not null", ""]]
1510
+ ) # XXX: Having to add this "" is not soooo great.
1511
+ assert view.to_records() == [{"a": "abc", "b": 4}]
1512
+
1513
+ # on_update
1514
+ def test_view_on_update(self, sentinel):
1515
+ s = sentinel(False)
1516
+
1517
+ def callback(port_id):
1518
+ s.set(True)
1519
+
1520
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1521
+ tbl = Table(data)
1522
+ view = tbl.view()
1523
+ view.on_update(callback)
1524
+ tbl.update(data)
1525
+ assert s.get() is True
1526
+
1527
+ def test_view_on_update_multiple_callback(self, sentinel):
1528
+ s = sentinel(0)
1529
+
1530
+ def callback(port_id):
1531
+ s.set(s.get() + 1)
1532
+
1533
+ def callback1(port_id):
1534
+ s.set(s.get() - 1)
1535
+
1536
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1537
+ tbl = Table(data)
1538
+ view = tbl.view()
1539
+ view.on_update(callback)
1540
+ view.on_update(callback1)
1541
+ tbl.update(data)
1542
+ assert s.get() == 0
1543
+
1544
+ # on_delete
1545
+
1546
+ def test_view_on_delete(self, sentinel):
1547
+ s = sentinel(False)
1548
+
1549
+ def callback():
1550
+ s.set(True)
1551
+
1552
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1553
+ tbl = Table(data)
1554
+ view = tbl.view()
1555
+ view.on_delete(callback)
1556
+ view.delete()
1557
+ assert s.get() is True
1558
+
1559
+ # delete
1560
+
1561
+ def test_view_delete(self):
1562
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1563
+ tbl = Table(data)
1564
+ view = tbl.view()
1565
+ with raises(PerspectiveError):
1566
+ tbl.delete()
1567
+ view.delete()
1568
+ tbl.delete()
1569
+
1570
+ def test_view_delete_multiple_callbacks(self, sentinel):
1571
+ # make sure that callbacks on views get filtered
1572
+ s1 = sentinel(0)
1573
+ s2 = sentinel(0)
1574
+
1575
+ def cb1(port_id):
1576
+ s1.set(s1.get() + 1)
1577
+
1578
+ def cb2(port_id):
1579
+ s2.set(s2.get() + 1)
1580
+
1581
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1582
+ tbl = Table(data)
1583
+ v1 = tbl.view()
1584
+ v2 = tbl.view()
1585
+ v1.on_update(cb1)
1586
+ v2.on_update(cb2)
1587
+ tbl.update(data)
1588
+ assert s1.get() == 1
1589
+ assert s2.get() == 1
1590
+ v1.delete()
1591
+ tbl.update(data)
1592
+ assert s1.get() == 1
1593
+ assert s2.get() == 2
1594
+
1595
+ def test_view_delete_full_cleanup(self, sentinel):
1596
+ s = sentinel(0)
1597
+
1598
+ def cb1(port_id):
1599
+ s.set(s.get() + 1)
1600
+
1601
+ def cb2(port_id):
1602
+ s.set(s.get() + 2)
1603
+
1604
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1605
+ tbl = Table(data)
1606
+ v1 = tbl.view()
1607
+ v2 = tbl.view()
1608
+ v1.on_update(cb1)
1609
+ v2.on_update(cb2)
1610
+ v1.delete()
1611
+ v2.delete()
1612
+ tbl.update(data)
1613
+ assert s.get() == 0
1614
+
1615
+ # remove_update
1616
+
1617
+ def test_view_remove_update(self, sentinel):
1618
+ s = sentinel(0)
1619
+
1620
+ def cb1(port_id):
1621
+ s.set(s.get() + 1)
1622
+
1623
+ def cb2(port_id):
1624
+ s.set(s.get() + 2)
1625
+
1626
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1627
+ tbl = Table(data)
1628
+ view = tbl.view()
1629
+ t1 = view.on_update(cb1)
1630
+ view.on_update(cb2)
1631
+ view.remove_update(t1)
1632
+ tbl.update(data)
1633
+ assert s.get() == 2
1634
+
1635
+ def test_view_remove_multiple_update(self, sentinel):
1636
+ s1 = sentinel(0)
1637
+ s2 = sentinel(0)
1638
+
1639
+ def cb1(port_id):
1640
+ s1.set(s1.get() + 1)
1641
+
1642
+ def cb2(port_id):
1643
+ s2.set(s2.get() + 1)
1644
+
1645
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1646
+ tbl = Table(data)
1647
+ view = tbl.view()
1648
+ t1 = view.on_update(cb1)
1649
+ view.on_update(cb2)
1650
+ view.on_update(cb1)
1651
+ tbl.update(data)
1652
+ assert s1.get() == 2
1653
+ assert s2.get() == 1
1654
+ view.remove_update(t1)
1655
+ tbl.update(data)
1656
+ assert s1.get() == 3
1657
+ assert s2.get() == 2
1658
+
1659
+ # row delta
1660
+
1661
+ def test_view_row_delta_zero(self, util):
1662
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1663
+ update_data = {"a": [5], "b": [6]}
1664
+
1665
+ def cb1(port_id, delta):
1666
+ compare_delta(delta, update_data)
1667
+
1668
+ tbl = Table(data)
1669
+ view = tbl.view()
1670
+ view.on_update(cb1, mode="row")
1671
+ tbl.update(update_data)
1672
+
1673
+ def test_view_row_delta_zero_column_subset(self, util):
1674
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1675
+ update_data = {"a": [5], "b": [6]}
1676
+
1677
+ def cb1(port_id, delta):
1678
+ compare_delta(delta, {"b": [6]})
1679
+
1680
+ tbl = Table(data)
1681
+ view = tbl.view(columns=["b"])
1682
+ view.on_update(cb1, mode="row")
1683
+ tbl.update(update_data)
1684
+
1685
+ def test_view_row_delta_zero_from_schema(self, util):
1686
+ update_data = {"a": [5], "b": [6]}
1687
+
1688
+ def cb1(port_id, delta):
1689
+ compare_delta(delta, update_data)
1690
+
1691
+ tbl = Table({"a": "integer", "b": "integer"})
1692
+ view = tbl.view()
1693
+ view.on_update(cb1, mode="row")
1694
+ tbl.update(update_data)
1695
+
1696
+ def test_view_row_delta_zero_from_schema_column_subset(self, util):
1697
+ update_data = {"a": [5], "b": [6]}
1698
+
1699
+ def cb1(port_id, delta):
1700
+ compare_delta(delta, {"b": [6]})
1701
+
1702
+ tbl = Table({"a": "integer", "b": "integer"})
1703
+
1704
+ view = tbl.view(columns=["b"])
1705
+ view.on_update(cb1, mode="row")
1706
+ tbl.update(update_data)
1707
+
1708
+ def test_view_row_delta_zero_from_schema_filtered(self, util):
1709
+ update_data = {"a": [8, 9, 10, 11], "b": [1, 2, 3, 4]}
1710
+
1711
+ def cb1(port_id, delta):
1712
+ compare_delta(delta, {"a": [11], "b": [4]})
1713
+
1714
+ tbl = Table({"a": "integer", "b": "integer"})
1715
+ view = tbl.view(filter=[["a", ">", 10]])
1716
+ view.on_update(cb1, mode="row")
1717
+ tbl.update(update_data)
1718
+
1719
+ def test_view_row_delta_zero_from_schema_indexed(self, util):
1720
+ update_data = {"a": ["a", "b", "a"], "b": [1, 2, 3]}
1721
+
1722
+ def cb1(port_id, delta):
1723
+ compare_delta(delta, {"a": ["a", "b"], "b": [3, 2]})
1724
+
1725
+ tbl = Table({"a": "string", "b": "integer"}, index="a")
1726
+
1727
+ view = tbl.view()
1728
+ view.on_update(cb1, mode="row")
1729
+
1730
+ tbl.update(update_data)
1731
+
1732
+ def test_view_row_delta_zero_from_schema_indexed_filtered(self, util):
1733
+ update_data = {"a": [8, 9, 10, 11, 11], "b": [1, 2, 3, 4, 5]}
1734
+
1735
+ def cb1(port_id, delta):
1736
+ compare_delta(delta, {"a": [11], "b": [5]})
1737
+
1738
+ tbl = Table({"a": "integer", "b": "integer"}, index="a")
1739
+ view = tbl.view(filter=[["a", ">", 10]])
1740
+ view.on_update(cb1, mode="row")
1741
+ tbl.update(update_data)
1742
+
1743
+ def test_view_row_delta_one(self, util):
1744
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1745
+ update_data = {"a": [5], "b": [6]}
1746
+
1747
+ def cb1(port_id, delta):
1748
+ compare_delta(delta, {"a": [9, 5], "b": [12, 6]})
1749
+
1750
+ tbl = Table(data)
1751
+ view = tbl.view(group_by=["a"])
1752
+ assert view.to_columns() == {
1753
+ "__ROW_PATH__": [[], [1], [3]],
1754
+ "a": [4, 1, 3],
1755
+ "b": [6, 2, 4],
1756
+ }
1757
+ view.on_update(cb1, mode="row")
1758
+ tbl.update(update_data)
1759
+
1760
+ def test_view_row_delta_one_from_schema(self, util):
1761
+ update_data = {"a": [1, 2, 3, 4, 5], "b": [6, 7, 8, 9, 10]}
1762
+
1763
+ def cb1(port_id, delta):
1764
+ compare_delta(delta, {"a": [15, 1, 2, 3, 4, 5], "b": [40, 6, 7, 8, 9, 10]})
1765
+
1766
+ tbl = Table({"a": "integer", "b": "integer"})
1767
+ view = tbl.view(group_by=["a"])
1768
+ view.on_update(cb1, mode="row")
1769
+ tbl.update(update_data)
1770
+
1771
+ def test_view_row_delta_one_from_schema_sorted(self, util):
1772
+ update_data = {"a": [1, 2, 3, 4, 5], "b": [6, 7, 8, 9, 10]}
1773
+
1774
+ def cb1(port_id, delta):
1775
+ compare_delta(delta, {"a": [15, 5, 4, 3, 2, 1], "b": [40, 10, 9, 8, 7, 6]})
1776
+
1777
+ tbl = Table({"a": "integer", "b": "integer"})
1778
+ view = tbl.view(group_by=["a"], sort=[["a", "desc"]])
1779
+ view.on_update(cb1, mode="row")
1780
+ tbl.update(update_data)
1781
+
1782
+ def test_view_row_delta_one_from_schema_filtered(self, util):
1783
+ update_data = {"a": [1, 2, 3, 4, 5], "b": [6, 7, 8, 9, 10]}
1784
+
1785
+ def cb1(port_id, delta):
1786
+ compare_delta(delta, {"a": [9, 4, 5], "b": [19, 9, 10]})
1787
+
1788
+ tbl = Table({"a": "integer", "b": "integer"})
1789
+ view = tbl.view(group_by=["a"], filter=[["a", ">", 3]])
1790
+ view.on_update(cb1, mode="row")
1791
+ tbl.update(update_data)
1792
+
1793
+ def test_view_row_delta_one_from_schema_sorted_filtered(self, util):
1794
+ update_data = {"a": [1, 2, 3, 4, 5], "b": [6, 7, 8, 9, 10]}
1795
+
1796
+ def cb1(port_id, delta):
1797
+ compare_delta(delta, {"a": [9, 5, 4], "b": [19, 10, 9]})
1798
+
1799
+ tbl = Table({"a": "integer", "b": "integer"})
1800
+ view = tbl.view(group_by=["a"], sort=[["a", "desc"]], filter=[["a", ">", 3]])
1801
+ view.on_update(cb1, mode="row")
1802
+ tbl.update(update_data)
1803
+
1804
+ def test_view_row_delta_one_from_schema_indexed(self, util):
1805
+ update_data = {"a": [1, 2, 3, 4, 5, 5, 4], "b": [6, 7, 8, 9, 10, 11, 12]}
1806
+
1807
+ def cb1(port_id, delta):
1808
+ compare_delta(delta, {"a": [15, 1, 2, 3, 4, 5], "b": [44, 6, 7, 8, 12, 11]})
1809
+
1810
+ tbl = Table({"a": "integer", "b": "integer"}, index="a")
1811
+
1812
+ view = tbl.view(group_by=["a"])
1813
+ view.on_update(cb1, mode="row")
1814
+
1815
+ tbl.update(update_data)
1816
+
1817
+ def test_view_row_delta_one_from_schema_sorted_indexed(self, util):
1818
+ update_data = {"a": [1, 2, 3, 4, 5, 5, 4], "b": [6, 7, 8, 9, 10, 11, 12]}
1819
+
1820
+ def cb1(port_id, delta):
1821
+ compare_delta(delta, {"a": [15, 4, 5, 3, 2, 1], "b": [44, 12, 11, 8, 7, 6]})
1822
+
1823
+ tbl = Table({"a": "integer", "b": "integer"}, index="a")
1824
+
1825
+ view = tbl.view(group_by=["a"], sort=[["b", "desc"]])
1826
+ view.on_update(cb1, mode="row")
1827
+
1828
+ tbl.update(update_data)
1829
+
1830
+ def test_view_row_delta_one_from_schema_filtered_indexed(self, util):
1831
+ update_data = {"a": [1, 2, 3, 4, 5, 5, 4], "b": [6, 7, 8, 9, 10, 11, 12]}
1832
+
1833
+ def cb1(port_id, delta):
1834
+ compare_delta(delta, {"a": [9, 4, 5], "b": [23, 12, 11]})
1835
+
1836
+ tbl = Table({"a": "integer", "b": "integer"}, index="a")
1837
+
1838
+ view = tbl.view(group_by=["a"], filter=[["a", ">", 3]])
1839
+ view.on_update(cb1, mode="row")
1840
+
1841
+ tbl.update(update_data)
1842
+
1843
+ def test_view_row_delta_two(self, util):
1844
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1845
+ update_data = {"a": [5], "b": [6]}
1846
+
1847
+ def cb1(port_id, delta):
1848
+ compare_delta(
1849
+ delta,
1850
+ {
1851
+ "2|a": [1, None],
1852
+ "2|b": [2, None],
1853
+ "4|a": [3, None],
1854
+ "4|b": [4, None],
1855
+ "6|a": [5, 5],
1856
+ "6|b": [6, 6],
1857
+ },
1858
+ )
1859
+
1860
+ tbl = Table(data)
1861
+ view = tbl.view(group_by=["a"], split_by=["b"])
1862
+ assert view.to_columns() == {
1863
+ "__ROW_PATH__": [[], [1], [3]],
1864
+ "2|a": [1, 1, None],
1865
+ "2|b": [2, 2, None],
1866
+ "4|a": [3, None, 3],
1867
+ "4|b": [4, None, 4],
1868
+ }
1869
+ view.on_update(cb1, mode="row")
1870
+ tbl.update(update_data)
1871
+
1872
+ def test_view_row_delta_two_from_schema(self, util):
1873
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1874
+
1875
+ def cb1(port_id, delta):
1876
+ compare_delta(
1877
+ delta,
1878
+ {
1879
+ "2|a": [1, 1, None],
1880
+ "2|b": [2, 2, None],
1881
+ "4|a": [3, None, 3],
1882
+ "4|b": [4, None, 4],
1883
+ },
1884
+ )
1885
+
1886
+ tbl = Table({"a": "integer", "b": "integer"})
1887
+ view = tbl.view(group_by=["a"], split_by=["b"])
1888
+ view.on_update(cb1, mode="row")
1889
+ tbl.update(data)
1890
+
1891
+ def test_view_row_delta_two_from_schema_indexed(self, util):
1892
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 3, "b": 5}]
1893
+
1894
+ def cb1(port_id, delta):
1895
+ compare_delta(
1896
+ delta,
1897
+ {
1898
+ "2|a": [1, 1, None],
1899
+ "2|b": [2, 2, None],
1900
+ "5|a": [3, None, 3],
1901
+ "5|b": [5, None, 5],
1902
+ },
1903
+ )
1904
+
1905
+ tbl = Table({"a": "integer", "b": "integer"}, index="a")
1906
+ view = tbl.view(group_by=["a"], split_by=["b"])
1907
+ view.on_update(cb1, mode="row")
1908
+ tbl.update(data)
1909
+
1910
+ def test_view_row_delta_two_column_only(self, util):
1911
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1912
+ update_data = {"a": [5], "b": [6]}
1913
+
1914
+ def cb1(port_id, delta):
1915
+ compare_delta(
1916
+ delta,
1917
+ {
1918
+ "2|a": [1, None],
1919
+ "2|b": [2, None],
1920
+ "4|a": [3, None],
1921
+ "4|b": [4, None],
1922
+ "6|a": [5, 5],
1923
+ "6|b": [6, 6],
1924
+ },
1925
+ )
1926
+
1927
+ tbl = Table(data)
1928
+ view = tbl.view(split_by=["b"])
1929
+ assert view.to_columns() == {
1930
+ "2|a": [1, None],
1931
+ "2|b": [2, None],
1932
+ "4|a": [None, 3],
1933
+ "4|b": [None, 4],
1934
+ }
1935
+ view.on_update(cb1, mode="row")
1936
+ tbl.update(update_data)
1937
+
1938
+ def test_view_row_delta_two_column_only_indexed(self, util):
1939
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 3, "b": 5}]
1940
+ update_data = {"a": [5], "b": [6]}
1941
+
1942
+ def cb1(port_id, delta):
1943
+ compare_delta(
1944
+ delta,
1945
+ {
1946
+ "2|a": [1, None],
1947
+ "2|b": [2, None],
1948
+ "5|a": [3, None],
1949
+ "5|b": [5, None],
1950
+ "6|a": [5, 5],
1951
+ "6|b": [6, 6],
1952
+ },
1953
+ )
1954
+
1955
+ tbl = Table(data, index="a")
1956
+ view = tbl.view(split_by=["b"])
1957
+ assert view.to_columns() == {
1958
+ "2|a": [1, None],
1959
+ "2|b": [2, None],
1960
+ "5|a": [None, 3],
1961
+ "5|b": [None, 5],
1962
+ }
1963
+ view.on_update(cb1, mode="row")
1964
+ tbl.update(update_data)
1965
+
1966
+ def test_view_row_delta_two_column_only_from_schema(self, util):
1967
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
1968
+
1969
+ def cb1(port_id, delta):
1970
+ compare_delta(
1971
+ delta,
1972
+ {
1973
+ "2|a": [1, 1, None],
1974
+ "2|b": [2, 2, None],
1975
+ "4|a": [3, None, 3],
1976
+ "4|b": [4, None, 4],
1977
+ },
1978
+ )
1979
+
1980
+ tbl = Table({"a": "integer", "b": "integer"})
1981
+ view = tbl.view(split_by=["b"])
1982
+ view.on_update(cb1, mode="row")
1983
+ tbl.update(data)
1984
+
1985
+ def test_view_row_delta_two_column_only_from_schema_indexed(self, util):
1986
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 3, "b": 5}]
1987
+
1988
+ def cb1(port_id, delta):
1989
+ compare_delta(
1990
+ delta,
1991
+ {
1992
+ "2|a": [1, 1, None],
1993
+ "2|b": [2, 2, None],
1994
+ "5|a": [3, None, 3],
1995
+ "5|b": [5, None, 5],
1996
+ },
1997
+ )
1998
+
1999
+ tbl = Table({"a": "integer", "b": "integer"}, index="a")
2000
+ view = tbl.view(split_by=["b"])
2001
+ view.on_update(cb1, mode="row")
2002
+ tbl.update(data)
2003
+
2004
+ # hidden cols
2005
+
2006
+ def test_view_num_hidden_cols(self):
2007
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
2008
+ tbl = Table(data)
2009
+ view = tbl.view(columns=["a"], sort=[["b", "desc"]])
2010
+ cols = view.to_columns()
2011
+ assert cols == {"a": [3, 1]}
2012
+
2013
+ def test_view_context_two_update_clears_column_regression(self, util):
2014
+ """Tests that, when a 2-sided View() is updated to a state where one of
2015
+ the column groups is empty, an infinite loop is not encountered.
2016
+ """
2017
+ data = [
2018
+ {"a": "a", "b": 1, "c": 1.5, "i": 0},
2019
+ {"a": "a", "b": 2, "c": 2.5, "i": 1},
2020
+ {"a": "a", "b": 3, "c": 3.5, "i": 2},
2021
+ {"a": "b", "b": 1, "c": 4.5, "i": 3},
2022
+ {"a": "b", "b": 2, "c": 5.5, "i": 4},
2023
+ {"a": "b", "b": 3, "c": 6.5, "i": 5},
2024
+ ]
2025
+
2026
+ tbl = Table(data, index="i")
2027
+ view = tbl.view(
2028
+ group_by=["b"],
2029
+ split_by=["a"],
2030
+ columns=["c"],
2031
+ filter=[["c", ">", 0]],
2032
+ sort=[["c", "asc"], ["a", "col asc"]],
2033
+ )
2034
+
2035
+ assert view.to_records() == [
2036
+ {"__ROW_PATH__": [], "a|c": 7.5, "b|c": 16.5},
2037
+ {"__ROW_PATH__": [1], "a|c": 1.5, "b|c": 4.5},
2038
+ {"__ROW_PATH__": [2], "a|c": 2.5, "b|c": 5.5},
2039
+ {"__ROW_PATH__": [3], "a|c": 3.5, "b|c": 6.5},
2040
+ ]
2041
+
2042
+ tbl.update(
2043
+ [
2044
+ {"c": -1, "i": 0},
2045
+ {"c": -1, "i": 1},
2046
+ {"c": -1, "i": 2},
2047
+ ]
2048
+ )
2049
+
2050
+ assert view.to_records() == [
2051
+ {"__ROW_PATH__": [], "b|c": 16.5},
2052
+ {"__ROW_PATH__": [1], "b|c": 4.5},
2053
+ {"__ROW_PATH__": [2], "b|c": 5.5},
2054
+ {"__ROW_PATH__": [3], "b|c": 6.5},
2055
+ ]
2056
+
2057
+ tbl.update(
2058
+ [
2059
+ {"a": "a", "b": 1, "c": 1.5, "i": 6},
2060
+ {"a": "a", "b": 2, "c": 2.5, "i": 7},
2061
+ {"a": "a", "b": 3, "c": 3.5, "i": 8},
2062
+ ]
2063
+ )
2064
+
2065
+ assert view.to_records() == [
2066
+ {"__ROW_PATH__": [], "a|c": 7.5, "b|c": 16.5},
2067
+ {"__ROW_PATH__": [1], "a|c": 1.5, "b|c": 4.5},
2068
+ {"__ROW_PATH__": [2], "a|c": 2.5, "b|c": 5.5},
2069
+ {"__ROW_PATH__": [3], "a|c": 3.5, "b|c": 6.5},
2070
+ ]
2071
+
2072
+ assert tbl.size() == 9
2073
+
2074
+ # expand/collapse
2075
+
2076
+ def test_view_collapse_one(self):
2077
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
2078
+ tbl = Table(data)
2079
+ view = tbl.view(group_by=["a"])
2080
+ assert view.collapse(0) == 2
2081
+
2082
+ def test_view_collapse_two(self):
2083
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2084
+ tbl = Table(data)
2085
+ view = tbl.view(group_by=["a"], split_by=["c"])
2086
+ assert view.collapse(0) == 2
2087
+
2088
+ # TODO collapse/espand should be no-ops on column only contexts, but
2089
+ # the concept of "column only" is not yet implemented in C++
2090
+ @mark.skip
2091
+ def test_view_collapse_two_column_only(self):
2092
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2093
+ tbl = Table(data)
2094
+ view = tbl.view(split_by=["c"])
2095
+ assert view.collapse(0) == 0
2096
+
2097
+ def test_view_expand_one(self):
2098
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
2099
+ tbl = Table(data)
2100
+ view = tbl.view(group_by=["a"])
2101
+ assert view.expand(0) == 0
2102
+
2103
+ def test_view_expand_two(self):
2104
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2105
+ tbl = Table(data)
2106
+ view = tbl.view(group_by=["a"], split_by=["c"])
2107
+ assert view.expand(1) == 1
2108
+
2109
+ def test_view_expand_two_column_only(self):
2110
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2111
+ tbl = Table(data)
2112
+ view = tbl.view(split_by=["c"])
2113
+ assert view.expand(0) == 0
2114
+
2115
+ # view config validation
2116
+
2117
+ def test_invalid_column_should_throw(self):
2118
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2119
+ tbl = Table(data)
2120
+ with raises(PerspectiveError) as ex:
2121
+ tbl.view(columns=["x"])
2122
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View columns.\n"
2123
+
2124
+ def test_invalid_column_should_throw_and_updates_should_work(self):
2125
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2126
+ tbl = Table(data)
2127
+
2128
+ with raises(PerspectiveError) as ex:
2129
+ tbl.view(columns=["x"])
2130
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View columns.\n"
2131
+
2132
+ for i in range(100):
2133
+ tbl.update(data)
2134
+ # force call to _process which should shake out invalid column ptrs
2135
+ tbl.size()
2136
+
2137
+ view2 = tbl.view()
2138
+ assert view2.num_rows() == 202
2139
+
2140
+ def test_invalid_column_aggregate_should_throw(self):
2141
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2142
+ tbl = Table(data)
2143
+
2144
+ with raises(PerspectiveError) as ex:
2145
+ tbl.view(columns=["x"], aggregates={"x": "sum"})
2146
+
2147
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View columns.\n"
2148
+
2149
+ def test_invalid_column_aggregate_should_throw_and_updates_should_work(self):
2150
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2151
+ tbl = Table(data)
2152
+
2153
+ with raises(PerspectiveError) as ex:
2154
+ tbl.view(columns=["x"], aggregates={"x": "sum"})
2155
+
2156
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View columns.\n"
2157
+
2158
+ for i in range(100):
2159
+ tbl.update(data)
2160
+ # force call to _process which should shake out invalid column ptrs
2161
+ tbl.size()
2162
+
2163
+ view2 = tbl.view()
2164
+ assert view2.num_rows() == 202
2165
+
2166
+ def test_invalid_group_by_should_throw(self):
2167
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2168
+ tbl = Table(data)
2169
+ with raises(PerspectiveError) as ex:
2170
+ tbl.view(group_by=["x"])
2171
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View group_by.\n"
2172
+
2173
+ def test_invalid_split_by_should_throw(self):
2174
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2175
+ tbl = Table(data)
2176
+ with raises(PerspectiveError) as ex:
2177
+ tbl.view(split_by=["x"])
2178
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View split_by.\n"
2179
+
2180
+ def test_invalid_filters_should_throw(self):
2181
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2182
+ tbl = Table(data)
2183
+ with raises(PerspectiveError) as ex:
2184
+ tbl.view(filter=[["x", "==", "abc"]])
2185
+ assert str(ex.value) == "Abort(): Filter column not in schema: x"
2186
+
2187
+ def test_invalid_sorts_should_throw(self):
2188
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2189
+ tbl = Table(data)
2190
+ with raises(PerspectiveError) as ex:
2191
+ tbl.view(sort=[["x", "desc"]])
2192
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View sorts.\n"
2193
+
2194
+ def test_should_throw_on_first_invalid(self):
2195
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2196
+ tbl = Table(data)
2197
+ with raises(PerspectiveError) as ex:
2198
+ tbl.view(
2199
+ group_by=["a"],
2200
+ split_by=["c"],
2201
+ filter=[["a", ">", 1]],
2202
+ aggregates={"a": "avg"},
2203
+ sort=[["x", "desc"]],
2204
+ )
2205
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View sorts.\n"
2206
+
2207
+ def test_invalid_columns_not_in_expression_should_throw(self):
2208
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2209
+ tbl = Table(data)
2210
+ with raises(PerspectiveError) as ex:
2211
+ tbl.view(columns=["abc", "x"], expressions={"abc": "1 + 2"})
2212
+ assert str(ex.value) == "Abort(): Invalid column 'x' found in View columns.\n"
2213
+
2214
+ def test_should_not_throw_valid_expression(self):
2215
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2216
+ tbl = Table(data)
2217
+ view = tbl.view(columns=["abc"], expressions={"abc": "'hello!'"})
2218
+
2219
+ assert view.schema() == {"abc": "string"}
2220
+
2221
+ def test_should_not_throw_valid_expression_config(self):
2222
+ data = [{"a": 1, "b": 2, "c": "a"}, {"a": 3, "b": 4, "c": "b"}]
2223
+ tbl = Table(data)
2224
+ view = tbl.view(
2225
+ aggregates={"abc": "dominant"},
2226
+ columns=["abc"],
2227
+ sort=[["abc", "desc"]],
2228
+ filter=[["abc", "==", "A"]],
2229
+ group_by=["abc"],
2230
+ split_by=["abc"],
2231
+ expressions={"abc": "'hello!'"},
2232
+ )
2233
+
2234
+ assert view.schema() == {"abc": "string"}