perspective-python 3.0.0rc1__cp39-abi3-macosx_13_0_arm64.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 +78 -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/templates/exported_widget.html.jinja +35 -0
- perspective/tests/__init__.py +11 -0
- perspective/tests/conftest.py +268 -0
- perspective/tests/core/__init__.py +11 -0
- perspective/tests/core/test_async.py +436 -0
- perspective/tests/core/test_threadpool.py +48 -0
- perspective/tests/server/__init__.py +11 -0
- perspective/tests/server/test_server.py +1062 -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 +610 -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 +43 -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 +414 -0
- perspective/tests/table/test_to_arrow_lz4.py +33 -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_numpy.py +252 -0
- perspective/tests/table/test_update_pandas.py +211 -0
- perspective/tests/table/test_view.py +2235 -0
- perspective/tests/table/test_view_expression.py +1940 -0
- perspective/tests/viewer/__init__.py +11 -0
- perspective/tests/viewer/test_validate.py +70 -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.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/install.json +5 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/package.json +81 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/253.6f17b87bb4eb1e656365.js +18 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/253.6f17b87bb4eb1e656365.js.LICENSE.txt +59 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/905.d3bbc3d5954582d507bb.js +1 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/remoteEntry.88bba5e6b1094381fe05.js +1 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/style.js +4 -0
- perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/static/third-party-licenses.json +16 -0
- perspective_python-3.0.0rc1.dist-info/METADATA +13 -0
- perspective_python-3.0.0rc1.dist-info/RECORD +70 -0
- perspective_python-3.0.0rc1.dist-info/WHEEL +4 -0
perspective/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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-rc.1"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"_jupyter_labextension_paths",
|
|
16
|
+
"PerspectiveError",
|
|
17
|
+
"PerspectiveWidget",
|
|
18
|
+
"PerspectiveViewer",
|
|
19
|
+
"PerspectiveTornadoHandler",
|
|
20
|
+
"Table",
|
|
21
|
+
"View",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
from .perspective import (
|
|
25
|
+
PySyncClient,
|
|
26
|
+
PerspectiveError,
|
|
27
|
+
PySyncServer,
|
|
28
|
+
Table,
|
|
29
|
+
View,
|
|
30
|
+
PySyncProxySession as ProxySession,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .widget import PerspectiveWidget
|
|
34
|
+
from .viewer import PerspectiveViewer
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from .handlers import PerspectiveTornadoHandler
|
|
39
|
+
except ImportError:
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def default_loop_cb(fn, *args, **kwargs):
|
|
44
|
+
return fn(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Server(PySyncServer):
|
|
48
|
+
def set_threadpool_size(self, n_cpus):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def new_local_client(self, loop_callback=default_loop_cb):
|
|
52
|
+
"""Create a new `Client` instance bound to this in-process `Server`."""
|
|
53
|
+
return Client.from_server(self, loop_callback)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Client(PySyncClient):
|
|
57
|
+
def from_server(
|
|
58
|
+
server: Server,
|
|
59
|
+
loop_callback=default_loop_cb,
|
|
60
|
+
):
|
|
61
|
+
"""Create a new `Client` instance bound synchronously to an Python
|
|
62
|
+
instance of `PerspectiveServer`."""
|
|
63
|
+
|
|
64
|
+
def handle_request(bytes):
|
|
65
|
+
session.handle_request(bytes)
|
|
66
|
+
loop_callback(lambda: session.poll())
|
|
67
|
+
|
|
68
|
+
def handle_response(bytes):
|
|
69
|
+
client.handle_response(bytes)
|
|
70
|
+
|
|
71
|
+
session = server.new_session(handle_response)
|
|
72
|
+
client = Client(handle_request)
|
|
73
|
+
return client
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# read by `jupyter labextension develop`
|
|
77
|
+
def _jupyter_labextension_paths():
|
|
78
|
+
return [{"src": "labextension", "dest": "@finos/perspective-jupyterlab"}]
|
|
@@ -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
|
|
15
|
+
except ImportError:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from .starlette import PerspectiveStarletteHandler
|
|
20
|
+
except ImportError:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .tornado import PerspectiveTornadoHandler
|
|
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
|
|
@@ -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
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
@@ -0,0 +1,268 @@
|
|
|
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 datetime import datetime, date
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import pyarrow as pa
|
|
18
|
+
from pytest import fixture
|
|
19
|
+
from random import random, randint, choice
|
|
20
|
+
from faker import Faker
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
fake = Faker()
|
|
24
|
+
|
|
25
|
+
# Our tests construct naive datetimes everywhere
|
|
26
|
+
# so setting it here is an easy way to fix it globally.
|
|
27
|
+
import os
|
|
28
|
+
os.environ["TZ"] = "UTC"
|
|
29
|
+
|
|
30
|
+
def _make_date_time_index(size, time_unit):
|
|
31
|
+
return pd.date_range("2000-01-01", periods=size, freq=time_unit)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_period_index(size, time_unit):
|
|
35
|
+
return pd.period_range(start="2000", periods=size, freq=time_unit)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _make_dataframe(index, size=10):
|
|
39
|
+
"""Create a new random dataframe of `size` and with a DateTimeIndex of
|
|
40
|
+
frequency `time_unit`.
|
|
41
|
+
"""
|
|
42
|
+
return pd.DataFrame(
|
|
43
|
+
index=index,
|
|
44
|
+
data={
|
|
45
|
+
"a": np.random.rand(size),
|
|
46
|
+
"b": np.random.rand(size),
|
|
47
|
+
"c": np.random.rand(size),
|
|
48
|
+
"d": np.random.rand(size),
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Util:
|
|
54
|
+
@staticmethod
|
|
55
|
+
def make_arrow(names, data, types=None, legacy=False):
|
|
56
|
+
"""Create an arrow binary that can be loaded and manipulated from memory.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
names (list): a list of str column names
|
|
60
|
+
data (list): a list of lists containing data for each column
|
|
61
|
+
types (list): an optional list of `pyarrow.type` function references.
|
|
62
|
+
Types will be inferred if not provided.
|
|
63
|
+
legacy (bool): if True, use legacy IPC format (pre-pyarrow 0.15). Defaults to False.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bytes : a bytes object containing the arrow-serialized output.
|
|
67
|
+
"""
|
|
68
|
+
stream = pa.BufferOutputStream()
|
|
69
|
+
arrays = []
|
|
70
|
+
|
|
71
|
+
for idx, column in enumerate(data):
|
|
72
|
+
# only apply types if array is present
|
|
73
|
+
kwargs = {}
|
|
74
|
+
if types:
|
|
75
|
+
kwargs["type"] = types[idx]
|
|
76
|
+
arrays.append(pa.array(column, **kwargs))
|
|
77
|
+
|
|
78
|
+
batch = pa.RecordBatch.from_arrays(arrays, names)
|
|
79
|
+
table = pa.Table.from_batches([batch])
|
|
80
|
+
writer = pa.RecordBatchStreamWriter(
|
|
81
|
+
stream, table.schema, use_legacy_format=legacy
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
writer.write_table(table)
|
|
85
|
+
writer.close()
|
|
86
|
+
return stream.getvalue().to_pybytes()
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def make_arrow_from_pandas(df, schema=None, legacy=False):
|
|
90
|
+
"""Create an arrow binary from a Pandas dataframe.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
df (:obj:`pandas.DataFrame`)
|
|
94
|
+
schema (:obj:`pyarrow.Schema`)
|
|
95
|
+
legacy (bool): if True, use legacy IPC format (pre-pyarrow 0.15). Defaults to False.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
bytes : a bytes object containing the arrow-serialized output.
|
|
99
|
+
"""
|
|
100
|
+
stream = pa.BufferOutputStream()
|
|
101
|
+
table = pa.Table.from_pandas(df, schema=schema)
|
|
102
|
+
|
|
103
|
+
writer = pa.RecordBatchStreamWriter(
|
|
104
|
+
stream, table.schema, use_legacy_format=legacy
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
writer.write_table(table)
|
|
108
|
+
writer.close()
|
|
109
|
+
return stream.getvalue().to_pybytes()
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def make_dictionary_arrow(names, data, types=None, legacy=False):
|
|
113
|
+
"""Create an arrow binary that can be loaded and manipulated from memory, with
|
|
114
|
+
each column being a dictionary array of `str` values and `int` indices.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
names (list): a list of str column names
|
|
118
|
+
data (list:tuple): a list of tuples, the first value being a list of indices,
|
|
119
|
+
and the second value being a list of values.
|
|
120
|
+
types (list:list:pyarrow.func): a list of lists, containing the indices type and
|
|
121
|
+
dictionary value type for each array.
|
|
122
|
+
legacy (bool): if True, use legacy IPC format (pre-pyarrow 0.15). Defaults to False.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
bytes : a bytes object containing the arrow-serialized output.
|
|
126
|
+
"""
|
|
127
|
+
stream = pa.BufferOutputStream()
|
|
128
|
+
|
|
129
|
+
arrays = []
|
|
130
|
+
for idx, column in enumerate(data):
|
|
131
|
+
indice_type = pa.int64()
|
|
132
|
+
value_type = pa.string()
|
|
133
|
+
|
|
134
|
+
if types is not None:
|
|
135
|
+
indice_type = types[idx][0]
|
|
136
|
+
value_type = types[idx][1]
|
|
137
|
+
|
|
138
|
+
indices = pa.array(column[0], type=indice_type)
|
|
139
|
+
values = pa.array(column[1], type=value_type)
|
|
140
|
+
parray = pa.DictionaryArray.from_arrays(indices, values)
|
|
141
|
+
arrays.append(parray)
|
|
142
|
+
|
|
143
|
+
batch = pa.RecordBatch.from_arrays(arrays, names)
|
|
144
|
+
table = pa.Table.from_batches([batch])
|
|
145
|
+
writer = pa.RecordBatchStreamWriter(
|
|
146
|
+
stream, table.schema, use_legacy_format=legacy
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
writer.write_table(table)
|
|
150
|
+
writer.close()
|
|
151
|
+
return stream.getvalue().to_pybytes()
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def to_timestamp(obj):
|
|
155
|
+
"""Return an integer timestamp based on a date/datetime object."""
|
|
156
|
+
classname = obj.__class__.__name__
|
|
157
|
+
if classname == "date":
|
|
158
|
+
return int(datetime(obj.year, obj.month, obj.day).timestamp() * 1000)
|
|
159
|
+
elif classname == "datetime":
|
|
160
|
+
return int(obj.timestamp() * 1000)
|
|
161
|
+
else:
|
|
162
|
+
return -1
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def make_dataframe(size=10, freq="D"):
|
|
166
|
+
index = _make_date_time_index(size, freq)
|
|
167
|
+
return _make_dataframe(index, size)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def make_period_dataframe(size=10):
|
|
171
|
+
index = _make_period_index(size, "M")
|
|
172
|
+
return _make_dataframe(index, size)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def make_series(size=10, freq="D"):
|
|
176
|
+
index = _make_date_time_index(size, freq)
|
|
177
|
+
return pd.Series(data=np.random.rand(size), index=index)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Sentinel(object):
|
|
181
|
+
"""Generic sentinel class for testing side-effectful code in Python 2 and
|
|
182
|
+
3.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, value):
|
|
186
|
+
self.value = value
|
|
187
|
+
|
|
188
|
+
def get(self):
|
|
189
|
+
return self.value
|
|
190
|
+
|
|
191
|
+
def set(self, new_value):
|
|
192
|
+
self.value = new_value
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@fixture()
|
|
196
|
+
def sentinel():
|
|
197
|
+
"""Pass `sentinel` into a test and call it with `value` to create a new
|
|
198
|
+
instance of the Sentinel class.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> def test_with_sentinel(self, sentinel):
|
|
202
|
+
>>> s = sentinel(True)
|
|
203
|
+
>>> s.set(False)
|
|
204
|
+
>>> s.get() # returns False
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def _sentinel(value):
|
|
208
|
+
return Sentinel(value)
|
|
209
|
+
|
|
210
|
+
return _sentinel
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@fixture
|
|
214
|
+
def util():
|
|
215
|
+
"""Pass the `Util` class in to a test."""
|
|
216
|
+
return Util
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@fixture
|
|
220
|
+
def superstore(count=100):
|
|
221
|
+
data = []
|
|
222
|
+
for id in range(count):
|
|
223
|
+
dat = {}
|
|
224
|
+
dat["Row ID"] = id
|
|
225
|
+
dat["Order ID"] = "{}-{}".format(fake.ein(), fake.zipcode())
|
|
226
|
+
dat["Order Date"] = fake.date_this_year()
|
|
227
|
+
dat["Ship Date"] = fake.date_between_dates(dat["Order Date"]).strftime(
|
|
228
|
+
"%Y-%m-%d"
|
|
229
|
+
)
|
|
230
|
+
dat["Order Date"] = dat["Order Date"].strftime("%Y-%m-%d")
|
|
231
|
+
dat["Ship Mode"] = choice(["First Class", "Standard Class", "Second Class"])
|
|
232
|
+
dat["Ship Mode"] = choice(["First Class", "Standard Class", "Second Class"])
|
|
233
|
+
dat["Customer ID"] = fake.zipcode()
|
|
234
|
+
dat["Segment"] = choice(["A", "B", "C", "D"])
|
|
235
|
+
dat["Country"] = "US"
|
|
236
|
+
dat["City"] = fake.city()
|
|
237
|
+
dat["State"] = fake.state()
|
|
238
|
+
dat["Postal Code"] = fake.zipcode()
|
|
239
|
+
dat["Region"] = choice(["Region %d" % i for i in range(5)])
|
|
240
|
+
dat["Product ID"] = fake.bban()
|
|
241
|
+
sector = choice(["Industrials", "Technology", "Financials"])
|
|
242
|
+
industry = choice(["A", "B", "C"])
|
|
243
|
+
dat["Category"] = sector
|
|
244
|
+
dat["Sub-Category"] = industry
|
|
245
|
+
dat["Sales"] = randint(1, 100) * 100
|
|
246
|
+
dat["Quantity"] = randint(1, 100) * 10
|
|
247
|
+
dat["Discount"] = round(random() * 100, 2)
|
|
248
|
+
dat["Profit"] = round(random() * 1000, 2)
|
|
249
|
+
data.append(dat)
|
|
250
|
+
return pd.DataFrame(data)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Perspective used to support datetime.date and datetime.datetime
|
|
254
|
+
# as Table() constructor arguments, but now we forward the parameters
|
|
255
|
+
# directly to JSON.loads. So to make sure the tests dont need to be
|
|
256
|
+
# so utterly transmogrified, we have this little hack :)
|
|
257
|
+
import json
|
|
258
|
+
old = json.JSONEncoder.default
|
|
259
|
+
|
|
260
|
+
def new_encoder(self, obj):
|
|
261
|
+
if isinstance(obj, datetime):
|
|
262
|
+
return str(obj)
|
|
263
|
+
elif isinstance(obj, date):
|
|
264
|
+
return str(obj)
|
|
265
|
+
else:
|
|
266
|
+
return old(self, obj)
|
|
267
|
+
|
|
268
|
+
json.JSONEncoder.default = new_encoder
|
|
@@ -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
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|