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.
- perspective/__init__.py +403 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +11 -0
- perspective/handlers/aiohttp.py +69 -0
- perspective/handlers/starlette.py +63 -0
- perspective/handlers/tornado.py +192 -0
- perspective/perspective.abi3.so +0 -0
- perspective/templates/exported_widget.html.template +39 -0
- perspective/tests/__init__.py +11 -0
- perspective/tests/async/test_async_client.py +83 -0
- perspective/tests/async/test_websocket_client.py +124 -0
- perspective/tests/column_paths.py +33 -0
- perspective/tests/conftest.py +276 -0
- perspective/tests/core/__init__.py +11 -0
- perspective/tests/core/test_async.py +351 -0
- perspective/tests/multi_threaded/__init__.py +11 -0
- perspective/tests/multi_threaded/test_derived_threaded.py +103 -0
- perspective/tests/multi_threaded/test_multi_threaded.py +293 -0
- perspective/tests/server/__init__.py +11 -0
- perspective/tests/server/test_server.py +1016 -0
- perspective/tests/server/test_session.py +110 -0
- perspective/tests/table/__init__.py +11 -0
- perspective/tests/table/arrow/date32.arrow +0 -0
- perspective/tests/table/arrow/date64.arrow +0 -0
- perspective/tests/table/arrow/dict.arrow +0 -0
- perspective/tests/table/arrow/dict_update.arrow +0 -0
- perspective/tests/table/arrow/int_float_str.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
- perspective/tests/table/object_sequence.py +402 -0
- perspective/tests/table/test_column_paths.py +88 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_derived.py +145 -0
- perspective/tests/table/test_exception.py +73 -0
- perspective/tests/table/test_join.py +115 -0
- perspective/tests/table/test_leaks.py +105 -0
- perspective/tests/table/test_on_remove.py +140 -0
- perspective/tests/table/test_ports.py +178 -0
- perspective/tests/table/test_remove.py +102 -0
- perspective/tests/table/test_table.py +641 -0
- perspective/tests/table/test_table_arrow.py +500 -0
- perspective/tests/table/test_table_arrow_nested.py +535 -0
- perspective/tests/table/test_table_datetime.py +2544 -0
- perspective/tests/table/test_table_infer.py +201 -0
- perspective/tests/table/test_table_limit.py +45 -0
- perspective/tests/table/test_table_numpy.py +1022 -0
- perspective/tests/table/test_table_page_to_disk.py +304 -0
- perspective/tests/table/test_table_pandas.py +1018 -0
- perspective/tests/table/test_table_polars.py +251 -0
- perspective/tests/table/test_table_view_table.py +130 -0
- perspective/tests/table/test_to_arrow.py +416 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_arrow_zstd.py +41 -0
- perspective/tests/table/test_to_format.py +1030 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +561 -0
- perspective/tests/table/test_update_arrow.py +980 -0
- perspective/tests/table/test_update_pandas.py +211 -0
- perspective/tests/table/test_view.py +2503 -0
- perspective/tests/table/test_view_expression.py +2256 -0
- perspective/tests/test_dependencies.py +53 -0
- perspective/tests/viewer/__init__.py +11 -0
- perspective/tests/viewer/test_viewer.py +246 -0
- perspective/tests/virtual_servers/__init__.py +12 -0
- perspective/tests/virtual_servers/test_coerce_types.py +464 -0
- perspective/tests/virtual_servers/test_duckdb.py +1280 -0
- perspective/tests/virtual_servers/test_polars.py +1075 -0
- perspective/tests/virtual_servers/test_postgres.py +964 -0
- perspective/tests/widget/__init__.py +11 -0
- perspective/tests/widget/test_anywidget_smoke.py +109 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_html_export.py +113 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +206 -0
- perspective/virtual_servers/clickhouse.py +296 -0
- perspective/virtual_servers/duckdb.py +367 -0
- perspective/virtual_servers/polars.py +750 -0
- perspective/virtual_servers/postgres.py +448 -0
- perspective/widget/__init__.py +351 -0
- perspective/widget/static/perspective-anywidget.css +1 -0
- perspective/widget/static/perspective-anywidget.js +31 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/viewer.py +355 -0
- perspective/widget/viewer/viewer_traitlets.py +98 -0
- perspective_python-5.6.0.dist-info/METADATA +127 -0
- perspective_python-5.6.0.dist-info/RECORD +90 -0
- perspective_python-5.6.0.dist-info/WHEEL +4 -0
- perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
- perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
- perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
|
@@ -0,0 +1,103 @@
|
|
|
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 random
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
PARENTS = {
|
|
20
|
+
"flat": dict(filter=[["y", ">", 2]]),
|
|
21
|
+
"group_by": dict(
|
|
22
|
+
group_by=["g"],
|
|
23
|
+
columns=["x", "y"],
|
|
24
|
+
aggregates={"x": "sum", "y": "mean"},
|
|
25
|
+
group_rollup_mode="flat",
|
|
26
|
+
),
|
|
27
|
+
"split_by": dict(
|
|
28
|
+
group_by=["g"],
|
|
29
|
+
split_by=["s"],
|
|
30
|
+
columns=["x"],
|
|
31
|
+
group_rollup_mode="flat",
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _row(rand, i):
|
|
37
|
+
return {
|
|
38
|
+
"id": i,
|
|
39
|
+
"g": rand.choice(["a", "b", "c", "d"]),
|
|
40
|
+
"s": rand.choice(["Mon", "Tue", "Wed"]),
|
|
41
|
+
"x": rand.randint(0, 4000) / 4,
|
|
42
|
+
"y": rand.randint(0, 9),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _key(row):
|
|
47
|
+
return sorted((k, str(v)) for k, v in row.items())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run(config):
|
|
51
|
+
rand = random.Random(7)
|
|
52
|
+
client = Server().new_local_client()
|
|
53
|
+
table = client.table(
|
|
54
|
+
{"id": "integer", "g": "string", "s": "string", "x": "float", "y": "integer"},
|
|
55
|
+
index="id",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
table.update([_row(rand, i) for i in range(60)])
|
|
59
|
+
view = table.view(**config)
|
|
60
|
+
derived = client.table(view)
|
|
61
|
+
child = derived.view()
|
|
62
|
+
errors = []
|
|
63
|
+
done = threading.Event()
|
|
64
|
+
|
|
65
|
+
def read():
|
|
66
|
+
try:
|
|
67
|
+
while not done.is_set():
|
|
68
|
+
child.to_columns()
|
|
69
|
+
derived.size()
|
|
70
|
+
except Exception as e:
|
|
71
|
+
errors.append(e)
|
|
72
|
+
|
|
73
|
+
readers = [threading.Thread(target=read) for _ in range(4)]
|
|
74
|
+
for reader in readers:
|
|
75
|
+
reader.start()
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
for _ in range(500):
|
|
79
|
+
if rand.random() < 0.2:
|
|
80
|
+
table.remove([rand.randint(0, 59) for _ in range(3)])
|
|
81
|
+
else:
|
|
82
|
+
table.update([_row(rand, rand.randint(0, 59)) for _ in range(4)])
|
|
83
|
+
finally:
|
|
84
|
+
done.set()
|
|
85
|
+
for reader in readers:
|
|
86
|
+
reader.join()
|
|
87
|
+
|
|
88
|
+
assert errors == []
|
|
89
|
+
fresh = client.table(view)
|
|
90
|
+
expected = fresh.view().to_records()
|
|
91
|
+
actual = child.to_records()
|
|
92
|
+
assert sorted(map(_key, actual)) == sorted(map(_key, expected))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestDerivedTableThreaded(object):
|
|
96
|
+
def test_flat_parent(self):
|
|
97
|
+
_run(PARENTS["flat"])
|
|
98
|
+
|
|
99
|
+
def test_group_by_parent(self):
|
|
100
|
+
_run(PARENTS["group_by"])
|
|
101
|
+
|
|
102
|
+
def test_split_by_parent(self):
|
|
103
|
+
_run(PARENTS["split_by"])
|
|
@@ -0,0 +1,293 @@
|
|
|
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 asyncio
|
|
16
|
+
import random
|
|
17
|
+
import threading
|
|
18
|
+
|
|
19
|
+
from random import sample
|
|
20
|
+
from string import ascii_letters
|
|
21
|
+
from threading import Thread
|
|
22
|
+
from time import sleep, time
|
|
23
|
+
|
|
24
|
+
class TestServer(object):
|
|
25
|
+
def test_sync_updates_with_loop_callback_are_sync(self):
|
|
26
|
+
def feed(table):
|
|
27
|
+
y = 1000
|
|
28
|
+
while y > 0:
|
|
29
|
+
y -= 1
|
|
30
|
+
table.update([{"a": random.randint(0, 10), "index": y}])
|
|
31
|
+
|
|
32
|
+
perspective_server = Server()
|
|
33
|
+
loop = asyncio.new_event_loop()
|
|
34
|
+
thread = threading.Thread(target=loop.run_forever)
|
|
35
|
+
thread.start()
|
|
36
|
+
|
|
37
|
+
client = perspective_server.new_local_client()
|
|
38
|
+
table = client.table(
|
|
39
|
+
{"a": "integer", "index": "integer"}, index="index", name="test"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
view = table.view()
|
|
43
|
+
global count
|
|
44
|
+
count = 0
|
|
45
|
+
|
|
46
|
+
def update(x):
|
|
47
|
+
global count
|
|
48
|
+
count += 1
|
|
49
|
+
|
|
50
|
+
view.on_update(update)
|
|
51
|
+
feed(table)
|
|
52
|
+
assert table.size() == 1000
|
|
53
|
+
assert count == 1000
|
|
54
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
55
|
+
thread.join()
|
|
56
|
+
loop.close()
|
|
57
|
+
|
|
58
|
+
def test_concurrent_updates(self):
|
|
59
|
+
async def feed(table, loop):
|
|
60
|
+
y = 1000
|
|
61
|
+
while y > 0:
|
|
62
|
+
y -= 1
|
|
63
|
+
table.update([{"a": random.randint(0, 10), "index": y}])
|
|
64
|
+
await asyncio.sleep(0.001)
|
|
65
|
+
|
|
66
|
+
perspective_server = Server()
|
|
67
|
+
loop = asyncio.new_event_loop()
|
|
68
|
+
thread = threading.Thread(target=loop.run_forever)
|
|
69
|
+
thread.start()
|
|
70
|
+
|
|
71
|
+
client = perspective_server.new_local_client()
|
|
72
|
+
table = client.table(
|
|
73
|
+
{"a": "integer", "index": "integer"}, index="index", name="test"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
view = table.view()
|
|
77
|
+
global count
|
|
78
|
+
count = 0
|
|
79
|
+
|
|
80
|
+
def update(x):
|
|
81
|
+
global count
|
|
82
|
+
count += 1
|
|
83
|
+
|
|
84
|
+
view.on_update(update)
|
|
85
|
+
asyncio.run_coroutine_threadsafe(feed(table, loop), loop).result()
|
|
86
|
+
assert table.size() == 1000
|
|
87
|
+
assert count == 1000
|
|
88
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
89
|
+
thread.join()
|
|
90
|
+
loop.close()
|
|
91
|
+
|
|
92
|
+
def test_concurrent_updates_with_limit_tables_are_threadsafe(self):
|
|
93
|
+
TEST_ITERATIONS = 100
|
|
94
|
+
global running
|
|
95
|
+
perspective_server = Server()
|
|
96
|
+
client = perspective_server.new_local_client()
|
|
97
|
+
table = client.table(
|
|
98
|
+
{"col{}".format(i): "integer" for i in range(100)}, limit=100
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
running = True
|
|
102
|
+
|
|
103
|
+
# Create an updating thread that overlaps the index alot
|
|
104
|
+
def feed(table):
|
|
105
|
+
row = {"col{}".format(i): random.randint(0, 100) for i in range(100)}
|
|
106
|
+
while running:
|
|
107
|
+
table.update([row for _ in range(100)])
|
|
108
|
+
|
|
109
|
+
thread = threading.Thread(target=feed, args=(table,))
|
|
110
|
+
thread.start()
|
|
111
|
+
|
|
112
|
+
results = []
|
|
113
|
+
|
|
114
|
+
# Create a thread that serialized the table alot, checking for nulls
|
|
115
|
+
def feed2(table):
|
|
116
|
+
global running
|
|
117
|
+
view = table.view()
|
|
118
|
+
while len(results) < TEST_ITERATIONS:
|
|
119
|
+
arr = view.to_arrow()
|
|
120
|
+
table2 = client.table(arr)
|
|
121
|
+
view2 = table2.view()
|
|
122
|
+
json = view2.to_json(end_row=1)
|
|
123
|
+
view2.delete()
|
|
124
|
+
table2.delete()
|
|
125
|
+
results.append(json)
|
|
126
|
+
|
|
127
|
+
view.delete()
|
|
128
|
+
running = False
|
|
129
|
+
|
|
130
|
+
thread2 = threading.Thread(target=feed2, args=(table,))
|
|
131
|
+
thread2.start()
|
|
132
|
+
|
|
133
|
+
thread.join()
|
|
134
|
+
thread2.join()
|
|
135
|
+
|
|
136
|
+
assert table.size() == 100
|
|
137
|
+
for result in results:
|
|
138
|
+
for row in result:
|
|
139
|
+
for col, val in row.items():
|
|
140
|
+
assert val is not None
|
|
141
|
+
|
|
142
|
+
table.delete()
|
|
143
|
+
|
|
144
|
+
def test_concurrent_view_creation_on_separate_servers_are_threadsafe(self):
|
|
145
|
+
def run_perspective():
|
|
146
|
+
x = 1000
|
|
147
|
+
s = Server()
|
|
148
|
+
c = s.new_local_client()
|
|
149
|
+
t = c.table({"a": "string", "b": int})
|
|
150
|
+
t.view(expressions=["//tmp\nfalse"])
|
|
151
|
+
while x > 0:
|
|
152
|
+
t.update([{"a": "foo", "b": 42}])
|
|
153
|
+
x -= 1
|
|
154
|
+
|
|
155
|
+
thread1 = threading.Thread(target=run_perspective, daemon=True)
|
|
156
|
+
thread2 = threading.Thread(target=run_perspective, daemon=True)
|
|
157
|
+
thread1.start()
|
|
158
|
+
thread2.start()
|
|
159
|
+
thread1.join()
|
|
160
|
+
thread2.join()
|
|
161
|
+
|
|
162
|
+
def test_concurrent_view_creation_with_updates_are_threadsafe(self):
|
|
163
|
+
global running
|
|
164
|
+
s = Server()
|
|
165
|
+
schema = {
|
|
166
|
+
"a": "string",
|
|
167
|
+
"b": "string",
|
|
168
|
+
"c": "string",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
group_bys = ["a", "b", "c"]
|
|
172
|
+
c = s.new_local_client()
|
|
173
|
+
t = c.table(schema, limit=10000)
|
|
174
|
+
running = True
|
|
175
|
+
|
|
176
|
+
def gen_views():
|
|
177
|
+
global running
|
|
178
|
+
for _ in range(100):
|
|
179
|
+
t.view(columns=list(schema.keys()), group_by=group_bys)
|
|
180
|
+
sleep(0.01)
|
|
181
|
+
running = False
|
|
182
|
+
|
|
183
|
+
def run_psp():
|
|
184
|
+
global running
|
|
185
|
+
while running:
|
|
186
|
+
t.update(
|
|
187
|
+
[
|
|
188
|
+
{
|
|
189
|
+
"a": "".join(sample(ascii_letters, 4)),
|
|
190
|
+
"b": "".join(sample(ascii_letters, 4)),
|
|
191
|
+
"c": "".join(sample(ascii_letters, 4)),
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
thread1 = Thread(target=run_psp, daemon=True)
|
|
197
|
+
thread2 = Thread(target=gen_views, daemon=True)
|
|
198
|
+
thread1.start()
|
|
199
|
+
thread2.start()
|
|
200
|
+
thread1.join()
|
|
201
|
+
thread2.join()
|
|
202
|
+
|
|
203
|
+
def test_to_columns_id_while_filtered_rows_shrink(self):
|
|
204
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
205
|
+
|
|
206
|
+
executor = ThreadPoolExecutor()
|
|
207
|
+
poll_lock = threading.Lock()
|
|
208
|
+
|
|
209
|
+
def poll(server):
|
|
210
|
+
try:
|
|
211
|
+
server.poll()
|
|
212
|
+
finally:
|
|
213
|
+
poll_lock.release()
|
|
214
|
+
|
|
215
|
+
def on_poll_request(server):
|
|
216
|
+
if poll_lock.acquire(blocking=False):
|
|
217
|
+
executor.submit(poll, server)
|
|
218
|
+
|
|
219
|
+
perspective_server = Server(on_poll_request=on_poll_request)
|
|
220
|
+
client = perspective_server.new_local_client()
|
|
221
|
+
table = client.table(
|
|
222
|
+
{"a": "float", "index": "integer"}, index="index", name="test"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
stop = threading.Event()
|
|
226
|
+
|
|
227
|
+
def feed():
|
|
228
|
+
while not stop.is_set():
|
|
229
|
+
table.update(
|
|
230
|
+
[
|
|
231
|
+
{"a": random.random() * 100, "index": random.randint(0, 200)}
|
|
232
|
+
for _ in range(50)
|
|
233
|
+
]
|
|
234
|
+
)
|
|
235
|
+
sleep(0.001)
|
|
236
|
+
|
|
237
|
+
thread = threading.Thread(target=feed)
|
|
238
|
+
thread.start()
|
|
239
|
+
reader = perspective_server.new_local_client()
|
|
240
|
+
view = reader.open_table("test").view(filter=[["a", ">", 50]])
|
|
241
|
+
deadline = time() + 2.5
|
|
242
|
+
checked = 0
|
|
243
|
+
try:
|
|
244
|
+
while time() < deadline:
|
|
245
|
+
n = view.num_rows()
|
|
246
|
+
cols = view.to_columns(start_row=0, end_row=n, id=True)
|
|
247
|
+
assert len(cols["__ID__"]) == len(cols["a"])
|
|
248
|
+
assert all(len(x) == 1 for x in cols["__ID__"])
|
|
249
|
+
checked += 1
|
|
250
|
+
finally:
|
|
251
|
+
stop.set()
|
|
252
|
+
thread.join()
|
|
253
|
+
executor.shutdown()
|
|
254
|
+
|
|
255
|
+
assert checked > 0
|
|
256
|
+
|
|
257
|
+
def test_concurrent_flat_view_reads_are_threadsafe(self):
|
|
258
|
+
client = Server().new_local_client()
|
|
259
|
+
table = client.table(
|
|
260
|
+
{"a": "integer", "b": "string", "index": "integer"}, index="index"
|
|
261
|
+
)
|
|
262
|
+
table.update(
|
|
263
|
+
[{"a": i, "b": ascii_letters[i % 52], "index": i} for i in range(60)]
|
|
264
|
+
)
|
|
265
|
+
for config in ({}, {"filter": [["a", ">", 2]]}):
|
|
266
|
+
view = table.view(**config)
|
|
267
|
+
errors = []
|
|
268
|
+
stop = threading.Event()
|
|
269
|
+
|
|
270
|
+
def read():
|
|
271
|
+
try:
|
|
272
|
+
while not stop.is_set():
|
|
273
|
+
view.column_paths()
|
|
274
|
+
view.to_columns()
|
|
275
|
+
view.to_arrow()
|
|
276
|
+
except Exception as e:
|
|
277
|
+
errors.append(e)
|
|
278
|
+
|
|
279
|
+
readers = [threading.Thread(target=read) for _ in range(8)]
|
|
280
|
+
for reader in readers:
|
|
281
|
+
reader.start()
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
for i in range(300):
|
|
285
|
+
table.update([{"a": i, "b": "z", "index": i % 60}])
|
|
286
|
+
finally:
|
|
287
|
+
stop.set()
|
|
288
|
+
for reader in readers:
|
|
289
|
+
reader.join()
|
|
290
|
+
|
|
291
|
+
assert errors == []
|
|
292
|
+
assert view.num_rows() == len(view.to_columns()["a"])
|
|
293
|
+
view.delete()
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|