perspective-python 3.0.0__cp39-abi3-macosx_13_0_x86_64.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 +81 -0
- perspective/darwin-x86_64-libpsp.so +0 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +26 -0
- perspective/handlers/aiohttp.py +54 -0
- perspective/handlers/starlette.py +51 -0
- perspective/handlers/tornado.py +61 -0
- perspective/perspective.abi3.so +0 -0
- perspective/psp_cffi.py +127 -0
- perspective/templates/exported_widget.html.jinja +35 -0
- perspective/tests/__init__.py +11 -0
- perspective/tests/conftest.py +272 -0
- perspective/tests/core/__init__.py +11 -0
- perspective/tests/core/test_async.py +413 -0
- perspective/tests/core/test_threadpool.py +48 -0
- perspective/tests/server/__init__.py +11 -0
- perspective/tests/server/test_server.py +1058 -0
- perspective/tests/server/test_session.py +55 -0
- perspective/tests/single_threaded/test_single_threaded.py +61 -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_delete.py +124 -0
- perspective/tests/table/test_exception.py +53 -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 +612 -0
- perspective/tests/table/test_table_arrow.py +452 -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 +41 -0
- perspective/tests/table/test_table_numpy.py +1022 -0
- perspective/tests/table/test_table_pandas.py +1018 -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_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 +2234 -0
- perspective/tests/table/test_view_expression.py +1940 -0
- perspective/tests/viewer/__init__.py +11 -0
- perspective/tests/viewer/test_validate.py +69 -0
- perspective/tests/viewer/test_viewer.py +245 -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/viewer/__init__.py +15 -0
- perspective/viewer/validate.py +22 -0
- perspective/viewer/viewer.py +331 -0
- perspective/viewer/viewer_traitlets.py +101 -0
- perspective/widget/__init__.py +16 -0
- perspective/widget/widget.py +269 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/install.json +5 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/package.json +80 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/253.3b3f5e7f7f7cce48f967.js +18 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/253.3b3f5e7f7f7cce48f967.js.LICENSE.txt +59 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/905.bda9d13f81bc1d4190ff.js +1 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/remoteEntry.1f19f47ae899be5b3e51.js +1 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/style.js +4 -0
- perspective_python-3.0.0.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/third-party-licenses.json +16 -0
- perspective_python-3.0.0.dist-info/METADATA +30 -0
- perspective_python-3.0.0.dist-info/RECORD +71 -0
- perspective_python-3.0.0.dist-info/WHEEL +4 -0
perspective/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
__version__ = "3.0.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"_jupyter_labextension_paths",
|
|
16
|
+
"PerspectiveError",
|
|
17
|
+
"PerspectiveWidget",
|
|
18
|
+
"PerspectiveViewer",
|
|
19
|
+
"PerspectiveTornadoHandler",
|
|
20
|
+
"ProxySession",
|
|
21
|
+
"Table",
|
|
22
|
+
"View",
|
|
23
|
+
"Server",
|
|
24
|
+
"Client",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
from .perspective import (
|
|
28
|
+
Client as PySyncClient,
|
|
29
|
+
PerspectiveError,
|
|
30
|
+
Table,
|
|
31
|
+
View,
|
|
32
|
+
ProxySession,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .widget import PerspectiveWidget
|
|
36
|
+
from .viewer import PerspectiveViewer
|
|
37
|
+
|
|
38
|
+
from .psp_cffi import ServerBase
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from .handlers import PerspectiveTornadoHandler
|
|
42
|
+
except ImportError:
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_loop_cb(fn, *args, **kwargs):
|
|
47
|
+
return fn(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Server(ServerBase):
|
|
51
|
+
def set_threadpool_size(self, n_cpus):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def new_local_client(self, loop_callback=default_loop_cb):
|
|
55
|
+
"""Create a new `Client` instance bound to this in-process `Server`."""
|
|
56
|
+
return Client.from_server(self, loop_callback)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Client(PySyncClient):
|
|
60
|
+
def from_server(
|
|
61
|
+
server: Server,
|
|
62
|
+
loop_callback=default_loop_cb,
|
|
63
|
+
):
|
|
64
|
+
"""Create a new `Client` instance bound synchronously to an Python
|
|
65
|
+
instance of `PerspectiveServer`."""
|
|
66
|
+
|
|
67
|
+
def handle_request(bytes):
|
|
68
|
+
session.handle_request(bytes)
|
|
69
|
+
loop_callback(lambda: session.poll())
|
|
70
|
+
|
|
71
|
+
def handle_response(bytes):
|
|
72
|
+
client.handle_response(bytes)
|
|
73
|
+
|
|
74
|
+
session = server.new_session(handle_response)
|
|
75
|
+
client = Client(handle_request)
|
|
76
|
+
return client
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Read by `jupyter labextension develop`
|
|
80
|
+
def _jupyter_labextension_paths():
|
|
81
|
+
return [{"src": "labextension", "dest": "@finos/perspective-jupyterlab"}]
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
try:
|
|
14
|
+
from .aiohttp import PerspectiveAIOHTTPHandler # noqa: F401
|
|
15
|
+
except ImportError:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from .starlette import PerspectiveStarletteHandler # noqa: F401
|
|
20
|
+
except ImportError:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .tornado import PerspectiveTornadoHandler # noqa: F401
|
|
25
|
+
except ImportError:
|
|
26
|
+
...
|
|
@@ -0,0 +1,54 @@
|
|
|
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 aiohttp import web, WSMsgType
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PerspectiveAIOHTTPHandler(object):
|
|
18
|
+
"""PerspectiveAIOHTTPHandler is a drop-in implementation of Perspective.
|
|
19
|
+
|
|
20
|
+
Use it inside AIOHTTP routing to create a server-side Perspective that is
|
|
21
|
+
ready to receive websocket messages from the front-end `perspective-viewer`.
|
|
22
|
+
|
|
23
|
+
The Perspective client and server will automatically keep the Websocket
|
|
24
|
+
alive without timing out.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> server = Server()
|
|
28
|
+
>>> async def websocket_handler(request):
|
|
29
|
+
... handler = PerspectiveAIOHTTPHandler(perspective_server=server, request=request)
|
|
30
|
+
... await handler.run()
|
|
31
|
+
|
|
32
|
+
>>> app = web.Application()
|
|
33
|
+
>>> app.router.add_get("/websocket", websocket_handler)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, **kwargs):
|
|
37
|
+
self.server = kwargs.pop("perspective_server")
|
|
38
|
+
self._request = kwargs.pop("request")
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
|
|
41
|
+
async def run(self) -> None:
|
|
42
|
+
def inner(msg):
|
|
43
|
+
asyncio.get_running_loop().create_task(self._ws.send_bytes(msg))
|
|
44
|
+
|
|
45
|
+
self.session = self.server.new_session(inner)
|
|
46
|
+
try:
|
|
47
|
+
self._ws = web.WebSocketResponse()
|
|
48
|
+
await self._ws.prepare(self._request)
|
|
49
|
+
async for msg in self._ws:
|
|
50
|
+
if msg.type == WSMsgType.BINARY:
|
|
51
|
+
self.session.handle_request(msg.data)
|
|
52
|
+
|
|
53
|
+
finally:
|
|
54
|
+
self.session.close()
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
|
|
15
|
+
|
|
16
|
+
class PerspectiveStarletteHandler(object):
|
|
17
|
+
"""PerspectiveStarletteHandler is a drop-in implementation of Perspective.
|
|
18
|
+
|
|
19
|
+
The Perspective client and server will automatically keep the Websocket
|
|
20
|
+
alive without timing out.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> server = Server()
|
|
24
|
+
>>> client = server.client()
|
|
25
|
+
>>> client.table(pd.read_csv("superstore.csv"), name="data_source_one")
|
|
26
|
+
>>> app = FastAPI()
|
|
27
|
+
>>> async def endpoint(websocket: Websocket):
|
|
28
|
+
... handler = PerspectiveStarletteHandler(server, websocket)
|
|
29
|
+
... await handler.run()
|
|
30
|
+
... app.add_api_websocket_route('/websocket', endpoint)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, **kwargs):
|
|
34
|
+
self._server = kwargs.pop("perspective_server")
|
|
35
|
+
self._websocket = kwargs.pop("websocket")
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
|
|
38
|
+
async def run(self) -> None:
|
|
39
|
+
def inner(msg):
|
|
40
|
+
asyncio.get_running_loop().create_task(self._websocket.send_bytes(msg))
|
|
41
|
+
|
|
42
|
+
self.session = self._server.new_session(inner)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
await self._websocket.accept()
|
|
46
|
+
while True:
|
|
47
|
+
message = await self._websocket.receive()
|
|
48
|
+
self._websocket._raise_on_disconnect(message)
|
|
49
|
+
self.session.handle_request(message["bytes"])
|
|
50
|
+
finally:
|
|
51
|
+
self.session.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PerspectiveTornadoHandler(WebSocketHandler):
|
|
17
|
+
"""PerspectiveTornadoHandler is a drop-in implementation of Perspective.
|
|
18
|
+
|
|
19
|
+
Use it inside Tornado routing to create a server-side Perspective that is
|
|
20
|
+
ready to receive websocket messages from the front-end `perspective-viewer`.
|
|
21
|
+
Because Tornado implements an event loop, this handler links Perspective
|
|
22
|
+
with `IOLoop.current()` in order to defer expensive operations until the
|
|
23
|
+
next free iteration of the event loop.
|
|
24
|
+
|
|
25
|
+
The Perspective client and server will automatically keep the Websocket
|
|
26
|
+
alive without timing out.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> server = psp.Server()
|
|
30
|
+
>>> client = server.new_local_client()
|
|
31
|
+
>>> client.table(pd.read_csv("superstore.csv"), name="data_source_one")
|
|
32
|
+
>>> app = tornado.web.Application([
|
|
33
|
+
... (r"/", MainHandler),
|
|
34
|
+
... (r"/websocket", PerspectiveTornadoHandler, {
|
|
35
|
+
... "perspective_server": server,
|
|
36
|
+
... "check_origin": True
|
|
37
|
+
... })
|
|
38
|
+
... ])
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def initialize(self, perspective_server):
|
|
42
|
+
self.server = perspective_server
|
|
43
|
+
|
|
44
|
+
def open(self):
|
|
45
|
+
def inner(msg):
|
|
46
|
+
self.write_message(msg, binary=True)
|
|
47
|
+
|
|
48
|
+
self.session = self.server.new_session(inner)
|
|
49
|
+
|
|
50
|
+
def on_close(self) -> None:
|
|
51
|
+
self.session.close()
|
|
52
|
+
del self.session
|
|
53
|
+
|
|
54
|
+
def on_message(self, msg: bytes):
|
|
55
|
+
if not isinstance(msg, bytes):
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
self.session.handle_request(msg)
|
|
59
|
+
|
|
60
|
+
# TODO schedule me
|
|
61
|
+
self.session.poll()
|
|
Binary file
|
perspective/psp_cffi.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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 ctypes
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
if "PSP_CXX_SO_PATH" in os.environ:
|
|
19
|
+
lib = ctypes.CDLL(os.environ["PSP_CXX_SO_PATH"])
|
|
20
|
+
else:
|
|
21
|
+
ext = "so"
|
|
22
|
+
if platform.system().lower() == "windows":
|
|
23
|
+
ext = "dll"
|
|
24
|
+
arch = platform.machine().lower()
|
|
25
|
+
if arch == "amd64":
|
|
26
|
+
arch = "x86_64"
|
|
27
|
+
dylib_name = f"{platform.system().lower()}-{arch}-libpsp.{ext}"
|
|
28
|
+
lib = ctypes.CDLL(os.path.join(os.path.dirname(__file__), dylib_name))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EncodedApiResp(ctypes.Structure):
|
|
32
|
+
_pack_ = 1
|
|
33
|
+
_fields_ = [
|
|
34
|
+
("data", ctypes.POINTER(ctypes.c_char)),
|
|
35
|
+
("size", ctypes.c_uint32),
|
|
36
|
+
("client_id", ctypes.c_uint32),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EncodedApiEntries(ctypes.Structure):
|
|
41
|
+
_pack_ = 1
|
|
42
|
+
_fields_ = [
|
|
43
|
+
("size", ctypes.c_uint32),
|
|
44
|
+
("entries", ctypes.POINTER(EncodedApiResp)),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
ProtoApiServerPtr = ctypes.c_void_p
|
|
49
|
+
lib.psp_new_server.restype = ProtoApiServerPtr
|
|
50
|
+
|
|
51
|
+
lib.psp_new_session.argtypes = [ProtoApiServerPtr]
|
|
52
|
+
lib.psp_new_session.restype = ctypes.c_uint32
|
|
53
|
+
|
|
54
|
+
lib.psp_handle_request.argtypes = [
|
|
55
|
+
ProtoApiServerPtr,
|
|
56
|
+
ctypes.c_uint32,
|
|
57
|
+
ctypes.c_char_p,
|
|
58
|
+
ctypes.c_size_t,
|
|
59
|
+
]
|
|
60
|
+
lib.psp_handle_request.restype = ctypes.POINTER(EncodedApiEntries)
|
|
61
|
+
|
|
62
|
+
lib.psp_poll.argtypes = [ProtoApiServerPtr]
|
|
63
|
+
lib.psp_poll.restype = ctypes.POINTER(EncodedApiEntries)
|
|
64
|
+
|
|
65
|
+
lib.psp_new_session.argtypes = [ProtoApiServerPtr]
|
|
66
|
+
lib.psp_new_session.restype = ctypes.c_uint32
|
|
67
|
+
|
|
68
|
+
lib.psp_close_session.argtypes = [ProtoApiServerPtr, ctypes.c_uint32]
|
|
69
|
+
lib.psp_close_session.restype = None
|
|
70
|
+
|
|
71
|
+
lib.psp_delete_server.argtypes = [ProtoApiServerPtr]
|
|
72
|
+
lib.psp_delete_server.restype = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Session:
|
|
76
|
+
def __init__(self, server: "ServerBase"):
|
|
77
|
+
self._server: "ServerBase" = server
|
|
78
|
+
self._session_id = lib.psp_new_session(server._server)
|
|
79
|
+
|
|
80
|
+
def __del__(self):
|
|
81
|
+
lib.psp_close_session(self._server._server, self._session_id)
|
|
82
|
+
|
|
83
|
+
def handle_request(self, bytes_msg):
|
|
84
|
+
resps = lib.psp_handle_request(
|
|
85
|
+
self._server._server, self._session_id, bytes_msg, len(bytes_msg)
|
|
86
|
+
)
|
|
87
|
+
try:
|
|
88
|
+
size = resps.contents.size
|
|
89
|
+
for i in range(size):
|
|
90
|
+
resp = resps.contents.entries[i]
|
|
91
|
+
self._server._client_cbs[resp.client_id](resp.data[: resp.size])
|
|
92
|
+
finally:
|
|
93
|
+
for i in range(size):
|
|
94
|
+
lib.psp_free(resps.contents.entries[i].data)
|
|
95
|
+
lib.psp_free(resps.contents.entries)
|
|
96
|
+
lib.psp_free(resps)
|
|
97
|
+
|
|
98
|
+
def poll(self):
|
|
99
|
+
resps = lib.psp_poll(self._server._server)
|
|
100
|
+
try:
|
|
101
|
+
size = resps.contents.size
|
|
102
|
+
for i in range(size):
|
|
103
|
+
resp = resps.contents.entries[i]
|
|
104
|
+
self._server._client_cbs[resp.client_id](resp.data[: resp.size])
|
|
105
|
+
finally:
|
|
106
|
+
for i in range(size):
|
|
107
|
+
lib.psp_free(resps.contents.entries[i].data)
|
|
108
|
+
lib.psp_free(resps.contents.entries)
|
|
109
|
+
lib.psp_free(resps)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ServerBase:
|
|
113
|
+
def __init__(self):
|
|
114
|
+
self._client_cbs: dict[str, Callable[[bytes], None]] = {}
|
|
115
|
+
self._sessions: list[Session] = []
|
|
116
|
+
self._server: int = lib.psp_new_server()
|
|
117
|
+
|
|
118
|
+
def __del__(self):
|
|
119
|
+
for session in self._sessions:
|
|
120
|
+
del session
|
|
121
|
+
lib.psp_delete_server(self._server)
|
|
122
|
+
|
|
123
|
+
def new_session(self, cb: Callable[[bytes], None]):
|
|
124
|
+
session = Session(self)
|
|
125
|
+
self._client_cbs[session._session_id] = cb
|
|
126
|
+
self._sessions.append(session)
|
|
127
|
+
return session
|
|
@@ -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', 'css/pro.css') }}" />
|
|
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 | tojson }};
|
|
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 | tojson }};
|
|
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
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|