perspective-python 4.2.0__cp311-abi3-win_amd64.whl

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