perspective-python 5.6.0__cp311-abi3-pyemscripten_2026_0_wasm32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. perspective/__init__.py +403 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +11 -0
  4. perspective/handlers/aiohttp.py +69 -0
  5. perspective/handlers/starlette.py +63 -0
  6. perspective/handlers/tornado.py +192 -0
  7. perspective/perspective.abi3.so +0 -0
  8. perspective/templates/exported_widget.html.template +39 -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/column_paths.py +33 -0
  13. perspective/tests/conftest.py +276 -0
  14. perspective/tests/core/__init__.py +11 -0
  15. perspective/tests/core/test_async.py +351 -0
  16. perspective/tests/multi_threaded/__init__.py +11 -0
  17. perspective/tests/multi_threaded/test_derived_threaded.py +103 -0
  18. perspective/tests/multi_threaded/test_multi_threaded.py +293 -0
  19. perspective/tests/server/__init__.py +11 -0
  20. perspective/tests/server/test_server.py +1016 -0
  21. perspective/tests/server/test_session.py +110 -0
  22. perspective/tests/table/__init__.py +11 -0
  23. perspective/tests/table/arrow/date32.arrow +0 -0
  24. perspective/tests/table/arrow/date64.arrow +0 -0
  25. perspective/tests/table/arrow/dict.arrow +0 -0
  26. perspective/tests/table/arrow/dict_update.arrow +0 -0
  27. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  28. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  29. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  30. perspective/tests/table/object_sequence.py +402 -0
  31. perspective/tests/table/test_column_paths.py +88 -0
  32. perspective/tests/table/test_delete.py +124 -0
  33. perspective/tests/table/test_derived.py +145 -0
  34. perspective/tests/table/test_exception.py +73 -0
  35. perspective/tests/table/test_join.py +115 -0
  36. perspective/tests/table/test_leaks.py +105 -0
  37. perspective/tests/table/test_on_remove.py +140 -0
  38. perspective/tests/table/test_ports.py +178 -0
  39. perspective/tests/table/test_remove.py +102 -0
  40. perspective/tests/table/test_table.py +641 -0
  41. perspective/tests/table/test_table_arrow.py +500 -0
  42. perspective/tests/table/test_table_arrow_nested.py +535 -0
  43. perspective/tests/table/test_table_datetime.py +2544 -0
  44. perspective/tests/table/test_table_infer.py +201 -0
  45. perspective/tests/table/test_table_limit.py +45 -0
  46. perspective/tests/table/test_table_numpy.py +1022 -0
  47. perspective/tests/table/test_table_page_to_disk.py +304 -0
  48. perspective/tests/table/test_table_pandas.py +1018 -0
  49. perspective/tests/table/test_table_polars.py +251 -0
  50. perspective/tests/table/test_table_view_table.py +130 -0
  51. perspective/tests/table/test_to_arrow.py +416 -0
  52. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  53. perspective/tests/table/test_to_arrow_zstd.py +41 -0
  54. perspective/tests/table/test_to_format.py +1030 -0
  55. perspective/tests/table/test_to_polars.py +26 -0
  56. perspective/tests/table/test_update.py +561 -0
  57. perspective/tests/table/test_update_arrow.py +980 -0
  58. perspective/tests/table/test_update_pandas.py +211 -0
  59. perspective/tests/table/test_view.py +2503 -0
  60. perspective/tests/table/test_view_expression.py +2256 -0
  61. perspective/tests/test_dependencies.py +53 -0
  62. perspective/tests/viewer/__init__.py +11 -0
  63. perspective/tests/viewer/test_viewer.py +246 -0
  64. perspective/tests/virtual_servers/__init__.py +12 -0
  65. perspective/tests/virtual_servers/test_coerce_types.py +464 -0
  66. perspective/tests/virtual_servers/test_duckdb.py +1280 -0
  67. perspective/tests/virtual_servers/test_polars.py +1075 -0
  68. perspective/tests/virtual_servers/test_postgres.py +964 -0
  69. perspective/tests/widget/__init__.py +11 -0
  70. perspective/tests/widget/test_anywidget_smoke.py +109 -0
  71. perspective/tests/widget/test_widget.py +278 -0
  72. perspective/tests/widget/test_widget_html_export.py +113 -0
  73. perspective/tests/widget/test_widget_pandas.py +453 -0
  74. perspective/virtual_servers/__init__.py +206 -0
  75. perspective/virtual_servers/clickhouse.py +296 -0
  76. perspective/virtual_servers/duckdb.py +367 -0
  77. perspective/virtual_servers/polars.py +750 -0
  78. perspective/virtual_servers/postgres.py +448 -0
  79. perspective/widget/__init__.py +351 -0
  80. perspective/widget/static/perspective-anywidget.css +1 -0
  81. perspective/widget/static/perspective-anywidget.js +31 -0
  82. perspective/widget/viewer/__init__.py +15 -0
  83. perspective/widget/viewer/viewer.py +355 -0
  84. perspective/widget/viewer/viewer_traitlets.py +98 -0
  85. perspective_python-5.6.0.dist-info/METADATA +127 -0
  86. perspective_python-5.6.0.dist-info/RECORD +90 -0
  87. perspective_python-5.6.0.dist-info/WHEEL +4 -0
  88. perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
  89. perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
  90. perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
@@ -0,0 +1,124 @@
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 perspective as psp
14
+
15
+ client = psp.Server().new_local_client()
16
+ Table = client.table
17
+
18
+
19
+ class TestDelete(object):
20
+ # delete
21
+
22
+ def test_table_delete(self):
23
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
24
+ tbl = Table(data)
25
+ tbl.delete()
26
+ # don't segfault
27
+
28
+ def test_table_delete_callback(self, sentinel):
29
+ s = sentinel(False)
30
+
31
+ def callback():
32
+ s.set(True)
33
+
34
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
35
+ tbl = Table(data)
36
+ tbl.on_delete(callback)
37
+ tbl.delete()
38
+ assert s.get() is True
39
+
40
+ def test_table_delete_with_view(self, sentinel):
41
+ s = sentinel(False)
42
+
43
+ def callback():
44
+ s.set(True)
45
+
46
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
47
+ tbl = Table(data)
48
+ tbl.on_delete(callback)
49
+ view = tbl.view()
50
+ view.delete()
51
+ tbl.delete()
52
+ assert s.get() is True
53
+
54
+ def test_table_delete_multiple_callback(self, sentinel):
55
+ s1 = sentinel(False)
56
+ s2 = sentinel(False)
57
+
58
+ def callback1():
59
+ s1.set(True)
60
+
61
+ def callback2():
62
+ s2.set(True)
63
+
64
+ tbl = Table([{"a": 1}])
65
+ tbl.on_delete(callback1)
66
+ tbl.on_delete(callback2)
67
+
68
+ tbl.delete()
69
+
70
+ assert s1.get() is True
71
+ assert s2.get() is True
72
+
73
+ def test_table_remove_delete_callback(self, sentinel):
74
+ s = sentinel(False)
75
+
76
+ def callback():
77
+ s.set(True)
78
+
79
+ tbl = Table([{"a": 1}])
80
+ callback_id = tbl.on_delete(callback)
81
+ tbl.remove_delete(callback_id)
82
+
83
+ tbl.delete()
84
+
85
+ assert s.get() is False
86
+
87
+ def test_view_delete_multiple_callback(self, sentinel):
88
+ s1 = sentinel(False)
89
+ s2 = sentinel(False)
90
+
91
+ def callback1():
92
+ s1.set(True)
93
+
94
+ def callback2():
95
+ s2.set(True)
96
+
97
+ tbl = Table([{"a": 1}])
98
+ view = tbl.view()
99
+
100
+ view.on_delete(callback1)
101
+ view.on_delete(callback2)
102
+
103
+ view.delete()
104
+ tbl.delete()
105
+
106
+ assert s1.get() is True
107
+ assert s2.get() is True
108
+
109
+ def test_view_remove_delete_callback(self, sentinel):
110
+ s = sentinel(False)
111
+
112
+ def callback():
113
+ s.set(True)
114
+
115
+ tbl = Table([{"a": 1}])
116
+ view = tbl.view()
117
+
118
+ callback_id = view.on_delete(callback)
119
+ view.remove_delete(callback_id)
120
+
121
+ view.delete()
122
+ tbl.delete()
123
+
124
+ assert s.get() is False
@@ -0,0 +1,145 @@
1
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ # ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ # ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ from perspective import Server
14
+
15
+ import pytest
16
+
17
+
18
+ def _rows(n, offset=0):
19
+ return [
20
+ {
21
+ "id": i,
22
+ "g": ["a", "b", "c"][i % 3],
23
+ "s": ["Mon", "Tue"][(i // 3) % 2],
24
+ "x": i * 1.5,
25
+ "y": i % 7,
26
+ }
27
+ for i in range(offset, offset + n)
28
+ ]
29
+
30
+
31
+ def _unrolled(view, group_by):
32
+ columns = view.to_columns()
33
+ paths = columns.pop("__ROW_PATH__", [])
34
+ names = list(columns.keys())
35
+ out = []
36
+ for i, path in enumerate(paths):
37
+ row = {
38
+ g: (path[d] if d < len(path) else None)
39
+ for d, g in enumerate(group_by)
40
+ }
41
+
42
+ row.update({n: columns[n][i] for n in names})
43
+ out.append(row)
44
+
45
+ return out
46
+
47
+
48
+ def _key(row):
49
+ return sorted((k, str(v)) for k, v in row.items())
50
+
51
+
52
+ def _same(a, b):
53
+ return sorted(map(_key, a)) == sorted(map(_key, b))
54
+
55
+
56
+ class TestDerivedTable(object):
57
+ def _source(self):
58
+ client = Server().new_local_client()
59
+ table = client.table(
60
+ {"id": "integer", "g": "string", "s": "string", "x": "float", "y": "integer"},
61
+ index="id",
62
+ )
63
+
64
+ table.update(_rows(24))
65
+ return client, table
66
+
67
+ def test_group_by_parent_tracks_updates_and_removes(self):
68
+ client, table = self._source()
69
+ view = table.view(
70
+ group_by=["g"], columns=["x", "y"], aggregates={"x": "sum", "y": "mean"}
71
+ )
72
+
73
+ derived = client.table(view)
74
+ assert derived.schema() == {
75
+ "g": "string",
76
+ "x": "float",
77
+ "y": "float",
78
+ }
79
+
80
+ child = derived.view()
81
+ assert _same(child.to_records(), _unrolled(view, ["g"]))
82
+ for i in range(10):
83
+ table.update([{"id": i, "x": i * 100.0}])
84
+
85
+ assert derived.size() == 4
86
+ table.remove([0, 3, 6, 9, 12, 15, 18, 21])
87
+ assert derived.size() == 3
88
+ assert _same(child.to_records(), _unrolled(view, ["g"]))
89
+
90
+ def test_split_by_parent(self):
91
+ client, table = self._source()
92
+ view = table.view(
93
+ group_by=["g"], split_by=["s"], columns=["x"], group_rollup_mode="flat"
94
+ )
95
+
96
+ derived = client.table(view)
97
+ child = derived.view()
98
+ table.update(_rows(12, 18))
99
+ assert _same(child.to_records(), _unrolled(view, ["g"]))
100
+
101
+ def test_flat_parent_follows_filter(self):
102
+ client, table = self._source()
103
+ view = table.view(filter=[["y", ">", 3]])
104
+ derived = client.table(view)
105
+ assert derived.get_index() == "id"
106
+ child = derived.view()
107
+ assert _same(child.to_records(), view.to_records())
108
+ table.update([{"id": 0, "y": 6}, {"id": 4, "y": 0}])
109
+ table.remove([5])
110
+ assert _same(child.to_records(), view.to_records())
111
+
112
+ def test_explicit_schema(self):
113
+ client, table = self._source()
114
+ view = table.view(
115
+ group_by=["g"], split_by=["s"], columns=["x"], group_rollup_mode="flat"
116
+ )
117
+
118
+ derived = client.table(
119
+ view, schema={"g": "string", "Tue|x": "float", "Wed|x": "float"}
120
+ )
121
+
122
+ assert derived.columns() == ["g", "Tue|x", "Wed|x"]
123
+ child = derived.view()
124
+ assert all(v is None for v in child.to_columns()["Wed|x"])
125
+ table.update([{"id": 100, "g": "a", "s": "Wed", "x": 7.0, "y": 1}])
126
+ assert child.to_columns()["Wed|x"].count(7.0) == 1
127
+
128
+ def test_read_only(self):
129
+ client, table = self._source()
130
+ view = table.view(group_by=["g"], columns=["x"])
131
+ derived = client.table(view)
132
+ with pytest.raises(Exception):
133
+ derived.update([{"x": 1.0}])
134
+
135
+ with pytest.raises(Exception):
136
+ derived.clear()
137
+
138
+ def test_index_and_limit_rejected(self):
139
+ client, table = self._source()
140
+ view = table.view(group_by=["g"], columns=["x"])
141
+ with pytest.raises(Exception):
142
+ client.table(view, index="x")
143
+
144
+ with pytest.raises(Exception):
145
+ client.table(view, limit=10)
@@ -0,0 +1,73 @@
1
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ # ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ # ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ from pytest import raises
14
+ from perspective import PerspectiveError
15
+
16
+ import perspective as psp
17
+
18
+ client = psp.Server().new_local_client()
19
+ Table = client.table
20
+
21
+
22
+ class TestException(object):
23
+ def test_instantiation_exception_table(self):
24
+ from perspective import Table
25
+ with raises(TypeError) as ex:
26
+ Table()
27
+ assert "Do not call Table's constructor directly" in str(ex.value)
28
+
29
+ def test_instantiation_exception_view(self):
30
+ from perspective import View
31
+ with raises(TypeError) as ex:
32
+ View()
33
+ assert "Do not call View's constructor directly" in str(ex.value)
34
+
35
+ def test_exception_from_core(self):
36
+ tbl = Table({"a": [1, 2, 3]})
37
+
38
+ with raises(PerspectiveError) as ex:
39
+ # creating view with unknown column should throw
40
+ tbl.view(group_by=["b"])
41
+
42
+ assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
43
+
44
+ def test_exception_duplicate_group_by(self):
45
+ tbl = Table({"a": [1, 2, 3]})
46
+
47
+ with raises(PerspectiveError) as ex:
48
+ tbl.view(group_by=["a", "a"])
49
+
50
+ assert str(ex.value) == "Abort(): Duplicate column 'a' found in View group_by.\n"
51
+
52
+ def test_exception_from_core_catch_generic(self):
53
+ tbl = Table({"a": [1, 2, 3]})
54
+ # `PerspectiveCppError` should inherit from `Exception`
55
+ with raises(Exception) as ex:
56
+ tbl.view(group_by=["b"])
57
+
58
+ assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
59
+
60
+ def test_exception_from_core_correct_types(self):
61
+ tbl = Table({"a": [1, 2, 3]})
62
+
63
+ # `PerspectiveError` should be raised from the Python layer
64
+ with raises(PerspectiveError) as ex:
65
+ tbl.view()
66
+ tbl.delete()
67
+
68
+ assert str(ex.value) == "Abort(): Cannot delete table with views"
69
+
70
+ with raises(PerspectiveError) as ex:
71
+ tbl.view(group_by=["b"])
72
+
73
+ assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
@@ -0,0 +1,115 @@
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 perspective as psp
14
+
15
+ client = psp.Server().new_local_client()
16
+
17
+
18
+ class TestJoin:
19
+ def test_inner_join_two_tables(self):
20
+ left = client.table(
21
+ [{"id": 1, "x": 10}, {"id": 2, "x": 20}, {"id": 3, "x": 30}]
22
+ )
23
+ right = client.table(
24
+ [{"id": 1, "y": "a"}, {"id": 2, "y": "b"}, {"id": 4, "y": "d"}]
25
+ )
26
+ joined = client.join(left, right, "id")
27
+ view = joined.view()
28
+ json = view.to_json()
29
+ assert len(json) == 2
30
+ view.delete()
31
+ joined.delete()
32
+ right.delete()
33
+ left.delete()
34
+
35
+ def test_join_has_correct_schema(self):
36
+ left = client.table({"id": "integer", "x": "float"})
37
+ right = client.table({"id": "integer", "y": "string"})
38
+ joined = client.join(left, right, "id")
39
+ schema = joined.schema()
40
+ assert schema == {"id": "integer", "x": "float", "y": "string"}
41
+ joined.delete()
42
+ right.delete()
43
+ left.delete()
44
+
45
+ def test_join_reacts_to_left_updates(self):
46
+ left = client.table(
47
+ [{"id": 1, "x": 10}, {"id": 2, "x": 20}]
48
+ )
49
+ right = client.table(
50
+ [{"id": 1, "y": "a"}, {"id": 2, "y": "b"}]
51
+ )
52
+ joined = client.join(left, right, "id")
53
+ view = joined.view()
54
+ left.update([{"id": 1, "x": 99}])
55
+ json = view.to_json()
56
+ assert json == [
57
+ {"id": 1, "x": 10, "y": "a"},
58
+ {"id": 2, "x": 20, "y": "b"},
59
+ {"id": 1, "x": 99, "y": "a"},
60
+ ]
61
+ view.delete()
62
+ joined.delete()
63
+ right.delete()
64
+ left.delete()
65
+
66
+ def test_left_join(self):
67
+ left = client.table(
68
+ [{"id": 1, "x": 10}, {"id": 2, "x": 20}, {"id": 3, "x": 30}]
69
+ )
70
+ right = client.table(
71
+ [{"id": 1, "y": "a"}, {"id": 2, "y": "b"}]
72
+ )
73
+ joined = client.join(left, right, "id", "left")
74
+ view = joined.view()
75
+ json = view.to_json()
76
+ assert len(json) == 3
77
+ view.delete()
78
+ joined.delete()
79
+ right.delete()
80
+ left.delete()
81
+
82
+ def test_join_by_table_names(self):
83
+ left = client.table(
84
+ [{"id": 1, "x": 10}, {"id": 2, "x": 20}],
85
+ name="left_py",
86
+ )
87
+ right = client.table(
88
+ [{"id": 1, "y": "a"}, {"id": 2, "y": "b"}],
89
+ name="right_py",
90
+ )
91
+ joined = client.join("left_py", "right_py", "id")
92
+ view = joined.view()
93
+ json = view.to_json()
94
+ assert len(json) == 2
95
+ view.delete()
96
+ joined.delete()
97
+ right.delete()
98
+ left.delete()
99
+
100
+ def test_join_mixed_table_and_string(self):
101
+ left = client.table(
102
+ [{"id": 1, "x": 10}, {"id": 2, "x": 20}]
103
+ )
104
+ right = client.table(
105
+ [{"id": 1, "y": "a"}, {"id": 2, "y": "b"}],
106
+ name="right_py_mixed",
107
+ )
108
+ joined = client.join(left, "right_py_mixed", "id")
109
+ view = joined.view()
110
+ json = view.to_json()
111
+ assert len(json) == 2
112
+ view.delete()
113
+ joined.delete()
114
+ right.delete()
115
+ left.delete()
@@ -0,0 +1,105 @@
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
+
14
+ import psutil
15
+ import os
16
+ import perspective as psp
17
+
18
+ client = psp.Server().new_local_client()
19
+ Table = client.table
20
+
21
+
22
+ class TestDelete(object):
23
+ # delete
24
+
25
+ def test_table_delete(self):
26
+ process = psutil.Process(os.getpid())
27
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
28
+ tbl = Table(data)
29
+ tbl.delete()
30
+ mem = process.memory_info().rss
31
+
32
+ for x in range(10000):
33
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
34
+ tbl = Table(data)
35
+ tbl.delete()
36
+
37
+ mem2 = process.memory_info().rss
38
+
39
+ # assert 1 < (max2 / max) < 1.01
40
+ assert (mem2 - mem) < 2000000
41
+
42
+ def test_table_delete_with_view(self, sentinel):
43
+ data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
44
+ tbl = Table(data)
45
+
46
+ process = psutil.Process(os.getpid())
47
+ mem = process.memory_info().rss
48
+ for x in range(10000):
49
+ view = tbl.view()
50
+ view.delete()
51
+
52
+ tbl.delete()
53
+ mem2 = process.memory_info().rss
54
+ assert (mem2 - mem) < 2000000
55
+
56
+
57
+ class TestExpressionVocab(object):
58
+ # Regression tests for the `t_expression_vocab` leak fixed in PR #3175.
59
+ # Pre-fix, updates against a view with a string-producing expression
60
+ # interned each result into a new 4KB vocab page once the prior page
61
+ # filled, and never deduplicated against older pages. RSS grew without
62
+ # bound. The fix adds cross-page deduplication; these tests assert the
63
+ # steady state is flat.
64
+
65
+ def test_string_expression_update_no_leak(self):
66
+ long_literal = "X" * 256
67
+ tbl = Table({"x": "integer", "c": "string"}, index="x")
68
+ view = tbl.view(expressions={"e": 'concat("c", \'' + long_literal + "')"})
69
+
70
+ for _ in range(100):
71
+ tbl.update([{"x": 1, "c": "value"}])
72
+
73
+ process = psutil.Process(os.getpid())
74
+ mem = process.memory_info().rss
75
+ for _ in range(5000):
76
+ tbl.update([{"x": 1, "c": "value"}])
77
+ mem2 = process.memory_info().rss
78
+
79
+ view.delete()
80
+ tbl.delete()
81
+
82
+ assert (mem2 - mem) < 2000000
83
+
84
+ def test_string_expression_update_bounded_vocab_no_leak(self):
85
+ # Cycles through a small fixed set of distinct values. Exercises the
86
+ # cross-page `string_exists` lookup the fix relies on: once the
87
+ # vocabulary is fully populated, subsequent interns must resolve to
88
+ # existing pointers rather than allocating new ones.
89
+ values = ["alpha", "bravo", "charlie", "delta", "echo"]
90
+ tbl = Table({"x": "integer", "c": "string"}, index="x")
91
+ view = tbl.view(expressions={"e": 'upper("c")'})
92
+
93
+ for i in range(100):
94
+ tbl.update([{"x": 1, "c": values[i % len(values)]}])
95
+
96
+ process = psutil.Process(os.getpid())
97
+ mem = process.memory_info().rss
98
+ for i in range(5000):
99
+ tbl.update([{"x": 1, "c": values[i % len(values)]}])
100
+ mem2 = process.memory_info().rss
101
+
102
+ view.delete()
103
+ tbl.delete()
104
+
105
+ assert (mem2 - mem) < 2000000