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,184 @@
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 tornado.websocket import WebSocketHandler, WebSocketClosedError
14
+ from tornado.ioloop import IOLoop
15
+ import perspective
16
+
17
+ __doc__ = """
18
+ Perspective ships with a pre-built Tornado handler that makes integration with
19
+ `tornado.websockets` extremely easy. This allows you to run an instance of
20
+ `Perspective` on a server using Python, open a websocket to a `Table`, and
21
+ access the `Table` in JavaScript and through `<perspective-viewer>`. All
22
+ instructions sent to the `Table` are processed in Python, which executes the
23
+ commands, and returns its output through the websocket back to Javascript.
24
+
25
+ ### Python setup
26
+
27
+ To use the handler, we need to first have a `Server`, a `Client` and an instance
28
+ of a `Table`:
29
+
30
+ ```python
31
+ SERVER = Server()
32
+ CLIENT = SERVER.new_local_client()
33
+ ```
34
+
35
+ Once the server has been created, create a `Table` instance with a name. The
36
+ name that you host the table under is important — it acts as a unique accessor
37
+ on the JavaScript side, which will look for a Table hosted at the websocket with
38
+ the name you specify.
39
+
40
+ ```python
41
+ TABLE = client.table(data, name="data_source_one")
42
+ ```
43
+
44
+ After the server and table setup is complete, create a websocket endpoint and
45
+ provide it a reference to `PerspectiveTornadoHandler`. You must provide the
46
+ configuration object in the route tuple, and it must contain
47
+ `"perspective_server"`, which is a reference to the `Server` you just created.
48
+
49
+ ```python
50
+ from perspective.handlers.tornado import PerspectiveTornadoHandler
51
+
52
+ app = tornado.web.Application([
53
+
54
+ # ... other handlers ...
55
+
56
+ # Create a websocket endpoint that the client JavaScript can access
57
+ (r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER, "check_origin": True})
58
+ ])
59
+ ```
60
+
61
+ Optionally, the configuration object can also include `check_origin`, a boolean
62
+ that determines whether the websocket accepts requests from origins other than
63
+ where the server is hosted. See
64
+ [Tornado docs](https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler.check_origin)
65
+ for more details.
66
+
67
+ ### JavaScript setup
68
+
69
+ Once the server is up and running, you can access the Table you just hosted
70
+ using `perspective.websocket` and `open_table()`. First, create a client that
71
+ expects a Perspective server to accept connections at the specified URL:
72
+
73
+ ```javascript
74
+ const websocket = await perspective.websocket("ws://localhost:8888/websocket");
75
+ ```
76
+
77
+ Next open the `Table` we created on the server by name:
78
+
79
+ ```javascript
80
+ const table = await websocket.open_table("data_source_one");
81
+ ```
82
+
83
+ `table` is a proxy for the `Table` we created on the server. All operations that
84
+ are possible through the JavaScript API are possible on the Python API as well,
85
+ thus calling `view()`, `schema()`, `update()` etc. on `const table` will pass
86
+ those operations to the Python `Table`, execute the commands, and return the
87
+ result back to JavaScript. Similarly, providing this `table` to a
88
+ `<perspective-viewer>` instance will allow virtual rendering:
89
+
90
+ ```javascript
91
+ await viewer.load(table);
92
+ ```
93
+
94
+ `perspective.websocket` expects a Websocket URL where it will send instructions.
95
+ When `open_table` is called, the name to a hosted Table is passed through, and a
96
+ request is sent through the socket to fetch the Table. No actual `Table`
97
+ instance is passed inbetween the runtimes; all instructions are proxied through
98
+ websockets.
99
+
100
+ This provides for great flexibility — while `Perspective.js` is full of
101
+ features, browser WebAssembly runtimes currently have some performance
102
+ restrictions on memory and CPU feature utilization, and the architecture in
103
+ general suffers when the dataset itself is too large to download to the client
104
+ in full.
105
+
106
+ The Python runtime does not suffer from memory limitations, utilizes Apache
107
+ Arrow internal threadpools for threading and parallel processing, and generates
108
+ architecture optimized code, which currently makes it more suitable as a
109
+ server-side runtime than `node.js`.
110
+ """
111
+
112
+
113
+ class PerspectiveTornadoHandler(WebSocketHandler):
114
+ """`PerspectiveTornadoHandler` is a `perspective.Server` API as a `tornado`
115
+ websocket handler.
116
+
117
+ Use it inside `tornado` routing to create a `perspective.Server` that can
118
+ connect to a JavaScript (Wasm) `Client`, providing a virtual interface to
119
+ the `Server`'s resources for e.g. `<perspective-viewer>`.
120
+
121
+ You may need to increase the `websocket_max_message_size` kwarg
122
+ to the `tornado.web.Application` constructor, as well as provide the
123
+ `max_buffer_size` optional arg, for large datasets.
124
+
125
+ # Arguments
126
+
127
+ - `loop`: An optional `IOLoop` instance to use for scheduling IO calls,
128
+ defaults to `IOLoop.current()`.
129
+ - `executor`: An optional executor for scheduling `perspective.Server`
130
+ message processing calls from websocket `Client`s.
131
+
132
+ # Examples
133
+
134
+ >>> server = psp.Server()
135
+ >>> client = server.new_local_client()
136
+ >>> client.table(pd.read_csv("superstore.csv"), name="data_source_one")
137
+ >>> app = tornado.web.Application([
138
+ ... (r"/", MainHandler),
139
+ ... (r"/websocket", PerspectiveTornadoHandler, {
140
+ ... "perspective_server": server,
141
+ ... })
142
+ ... ])
143
+ """
144
+
145
+ def check_origin(self, origin):
146
+ return True
147
+
148
+ def initialize(
149
+ self,
150
+ perspective_server=perspective.GLOBAL_SERVER,
151
+ loop=None,
152
+ executor=None,
153
+ max_buffer_size=None,
154
+ ):
155
+ self.server = perspective_server
156
+ self.loop = loop or IOLoop.current()
157
+ self.executor = executor
158
+ if max_buffer_size is not None:
159
+ self.request.connection.stream.max_buffer_size = max_buffer_size
160
+
161
+ def open(self):
162
+ def write(msg):
163
+ try:
164
+ self.write_message(msg, binary=True)
165
+ except WebSocketClosedError:
166
+ self.close()
167
+
168
+ def send_response(msg):
169
+ self.loop.add_callback(write, msg)
170
+
171
+ self.session = self.server.new_session(send_response)
172
+
173
+ def on_close(self) -> None:
174
+ self.session.close()
175
+ del self.session
176
+
177
+ def on_message(self, msg: bytes):
178
+ if not isinstance(msg, bytes):
179
+ return
180
+
181
+ if self.executor is None:
182
+ self.session.handle_request(msg)
183
+ else:
184
+ self.executor.submit(self.session.handle_request, msg)
Binary file
@@ -0,0 +1,35 @@
1
+ <script type="module" src="$psp_cdn_perspective"></script>
2
+ <script type="module" src="$psp_cdn_perspective_viewer"></script>
3
+ <script type="module" src="$psp_cdn_perspective_viewer_datagrid"></script>
4
+ <script type="module" src="$psp_cdn_perspective_viewer_d3fc"></script>
5
+ <link rel="stylesheet" crossorigin="anonymous" href="$psp_cdn_perspective_viewer_themes" />
6
+
7
+ <div class="perspective-envelope" id="perspective-envelope-$viewer_id">
8
+ <script type="application/vnd.apache.arrow.file">
9
+ $b64_data
10
+ </script>
11
+ <perspective-viewer style="height: 690px;"></perspective-viewer>
12
+ <script type="module">
13
+ // from MDN
14
+ function base64ToBytes(base64) {
15
+ const binString = atob(base64);
16
+ return Uint8Array.from(binString, (m) => m.codePointAt(0));
17
+ }
18
+
19
+ import * as perspective from "$psp_cdn_perspective";
20
+ const viewerId = $viewer_id;
21
+ const currentScript = document.scripts[document.scripts.length - 1];
22
+ const envelope = document.getElementById(`perspective-envelope-$${viewerId}`);
23
+ const dataScript = envelope.querySelector('script[type="application/vnd.apache.arrow.file"]');;
24
+ if (!dataScript)
25
+ throw new Error('data script missing for viewer', viewerId);
26
+ const data = base64ToBytes(dataScript.textContent);
27
+ const viewerAttrs = $viewer_attrs;
28
+
29
+ // Create a new worker, then a new table promise on that worker.
30
+ const table = await perspective.worker().table(data.buffer);
31
+ const viewer = envelope.querySelector('perspective-viewer');
32
+ viewer.load(table);
33
+ viewer.restore(viewerAttrs);
34
+ </script>
35
+ </div>
@@ -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
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
@@ -0,0 +1,83 @@
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 (
14
+ AsyncClient,
15
+ AsyncServer,
16
+ PerspectiveError,
17
+ )
18
+
19
+
20
+ import pytest
21
+ from unittest.mock import Mock
22
+
23
+
24
+ @pytest.fixture
25
+ def server():
26
+ return AsyncServer()
27
+
28
+
29
+ @pytest.fixture
30
+ def client(server):
31
+ async def send_request(msg):
32
+ await sess.handle_request(msg)
33
+
34
+ async def send_response(msg):
35
+ await client.handle_response(msg)
36
+
37
+ sess = server.new_session(send_response)
38
+ client = AsyncClient(send_request)
39
+ return client
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_send_request_callback_awaited(client):
44
+ table_names = await client.get_hosted_table_names()
45
+ assert table_names == []
46
+
47
+
48
+ # This test also exists in the pyodide-tests/ pytest suite, with the same name.
49
+ # Unfortunately code sharing between the two test suites is currently not easy.
50
+ @pytest.mark.asyncio
51
+ async def test_async_client_kitchen_sink(client):
52
+ """run various things on the async client"""
53
+ table = await client.table({"a": [0]}, name="my-cool-data", limit=100)
54
+ view = await table.view()
55
+ # synchronous methods
56
+ assert table.get_index() is None
57
+ assert table.get_name() == "my-cool-data"
58
+ limit = table.get_limit()
59
+ assert limit == 100
60
+
61
+ # view update callbacks
62
+ view_updated = Mock()
63
+ await view.on_update(view_updated)
64
+ for i in range(1, limit):
65
+ await table.update([{"a": i}])
66
+ await table.size() # force update to process
67
+ assert (await table.size()) == limit
68
+ assert view_updated.call_count == limit - 1
69
+ rex = await view.to_records()
70
+ assert rex == [{"a": i} for i in range(limit)]
71
+
72
+ # view/table delete callbacks
73
+ view_deleted = Mock()
74
+ table_deleted = Mock()
75
+ await table.on_delete(table_deleted)
76
+ await view.on_delete(view_deleted)
77
+ with pytest.raises(PerspectiveError) as excinfo:
78
+ await table.delete()
79
+ assert excinfo.match(r"Cannot delete table with views")
80
+ await view.delete()
81
+ view_deleted.assert_called_once()
82
+ await table.delete()
83
+ table_deleted.assert_called_once()
@@ -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 asyncio
14
+ import threading
15
+ import websocket
16
+
17
+ import tornado.websocket
18
+ import tornado.web
19
+ import tornado.ioloop
20
+
21
+ import perspective
22
+ import perspective.handlers.tornado
23
+
24
+ PORT = 8082
25
+
26
+
27
+ def test_big_multi_thing(superstore):
28
+ async def init_table(client):
29
+ global SERVER_DATA
30
+ global SERVER_TABLE
31
+
32
+ SERVER_DATA = "x,y\n1,2\n3,4"
33
+ # with open(file_path, mode="rb") as file:
34
+ SERVER_TABLE = client.table(SERVER_DATA, name="superstore")
35
+
36
+ global ws
37
+ ws = websocket.WebSocketApp(
38
+ "ws://localhost:{}/websocket".format(PORT),
39
+ on_open=on_open,
40
+ on_message=on_message,
41
+ # on_error=on_error,
42
+ # on_close=on_close,
43
+ )
44
+
45
+ global ws_thread
46
+ ws_thread = threading.Thread(target=ws.run_forever)
47
+ ws_thread.start()
48
+
49
+ def server_thread():
50
+ def make_app(perspective_server):
51
+ return tornado.web.Application(
52
+ [
53
+ (
54
+ r"/websocket",
55
+ perspective.handlers.tornado.PerspectiveTornadoHandler,
56
+ {"perspective_server": perspective_server},
57
+ ),
58
+ ]
59
+ )
60
+
61
+ perspective_server = perspective.Server()
62
+ app = make_app(perspective_server)
63
+ global server
64
+ server = app.listen(PORT, "0.0.0.0")
65
+
66
+ global server_loop
67
+ server_loop = tornado.ioloop.IOLoop.current()
68
+ client = perspective_server.new_local_client()
69
+ server_loop.call_later(0, init_table, client)
70
+ server_loop.start()
71
+
72
+ server_thread = threading.Thread(target=server_thread)
73
+ server_thread.start()
74
+
75
+ client_loop = asyncio.new_event_loop()
76
+ client_loop.set_debug(True)
77
+ client_thread = threading.Thread(target=client_loop.run_forever)
78
+ client_thread.start()
79
+
80
+ async def send_request(msg):
81
+ global ws
82
+ ws.send(msg, websocket.ABNF.OPCODE_BINARY)
83
+
84
+ def on_message(ws, message):
85
+ async def poke_client():
86
+ await client.handle_response(message)
87
+
88
+ asyncio.run_coroutine_threadsafe(poke_client(), client_loop)
89
+
90
+ # def on_error(ws, error):
91
+ # print(f"Error!: {error}")
92
+
93
+ # def on_close(ws, close_status_code, close_msg):
94
+ # print("Connection closed")
95
+
96
+ def on_open(ws):
97
+ global client
98
+ client = perspective.AsyncClient(send_request)
99
+ asyncio.run_coroutine_threadsafe(test(client), client_loop)
100
+
101
+ global count
102
+ count = 0
103
+
104
+ def update(x):
105
+ global count
106
+ count += 1
107
+
108
+ async def test(client):
109
+ table = await client.open_table("superstore")
110
+ view = await table.view()
111
+ await view.on_update(update)
112
+ SERVER_TABLE.update(SERVER_DATA)
113
+ assert await table.size() == 4
114
+ assert count == 1
115
+ await server.close_all_connections()
116
+ client_loop.stop()
117
+
118
+ client_thread.join()
119
+ client_loop.close()
120
+ ws.close()
121
+ ws_thread.join()
122
+ server_loop.add_callback(server_loop.stop)
123
+ server_thread.join()
124
+ server_loop.close()