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.
- perspective/__init__.py +396 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +11 -0
- perspective/handlers/aiohttp.py +61 -0
- perspective/handlers/starlette.py +55 -0
- perspective/handlers/tornado.py +184 -0
- perspective/perspective.pyd +0 -0
- perspective/templates/exported_widget.html.template +35 -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/conftest.py +272 -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_multi_threaded.py +201 -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 +89 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_exception.py +65 -0
- perspective/tests/table/test_leaks.py +54 -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 +503 -0
- perspective/tests/table/test_table_datetime.py +2409 -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_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 +417 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_format.py +1024 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +545 -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 +2261 -0
- perspective/tests/table/test_view_expression.py +1940 -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/widget/__init__.py +11 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +134 -0
- perspective/virtual_servers/clickhouse.py +245 -0
- perspective/virtual_servers/duckdb.py +236 -0
- perspective/widget/__init__.py +349 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/validate.py +22 -0
- perspective/widget/viewer/viewer.py +343 -0
- perspective/widget/viewer/viewer_traitlets.py +101 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/install.json +5 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/package.json +71 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js +2 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js.LICENSE.txt +25 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/523.c030af5d3c4f67ff83f6.js +1 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/remoteEntry.95a8ea1b44d96032833f.js +1 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/style.js +4 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/third-party-licenses.json +16 -0
- perspective_python-4.2.0.dist-info/METADATA +27 -0
- perspective_python-4.2.0.dist-info/RECORD +79 -0
- perspective_python-4.2.0.dist-info/WHEEL +4 -0
- perspective_python-4.2.0.dist-info/licenses/LICENSE.md +193 -0
- 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()
|