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