Skip to content

Commit

Permalink
Add aiohttp server and client implementation, add aiohttp server exam…
Browse files Browse the repository at this point in the history
…ple, add aiohttp handler tests
  • Loading branch information
timkpaine committed Jun 8, 2022
1 parent ff425c0 commit d241f50
Show file tree
Hide file tree
Showing 44 changed files with 1,112 additions and 595 deletions.
26 changes: 19 additions & 7 deletions docs/md/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ well as Python-specific data loading support for [NumPy](https://numpy.org/),

Additionally, `perspective-python` provides a session manager suitable for
integration into server systems such as
[Tornado websockets](https://www.tornadoweb.org/en/stable/websocket.html), which
allows fully _virtual_ Perspective tables to be interacted with by multiple
`<perspective-viewer>` in a web browser.
[Tornado websockets](https://www.tornadoweb.org/en/stable/websocket.html),
[AIOHTTP](https://docs.aiohttp.org/en/stable/web_quickstart.html#websockets),
or [Starlette](https://www.starlette.io/websockets/)/[FastAPI](https://fastapi.tiangolo.com/advanced/websockets/),
which allows fully _virtual_ Perspective tables to be interacted with by multiple
`<perspective-viewer>` in a web browser. You can also interact with a Perspective
table from python clients, and to that end client libraries are implemented for
both Tornado and AIOHTTP.

As `<perspective-viewer>` will only consume the data necessary to render the
current screen, this runtime mode allows _ludicrously-sized_ datasets with
Expand All @@ -31,9 +35,13 @@ The `perspective` module exports several tools:
- `Table`, the table constructor for Perspective, which implements the `table`
and `view` API in the same manner as the JavaScript library.
- `PerspectiveWidget` the JupyterLab widget for interactive visualization.
- `PerspectiveTornadoHandler`, an integration with
[Tornado](https://www.tornadoweb.org/) that interfaces seamlessly with
- Perspective webserver handlers that interfaces seamlessly with
`<perspective-viewer>` in JavaScript.
- `PerspectiveTornadoHandler` for [Tornado](https://www.tornadoweb.org/)
- `PerspectiveStarletteHandler` for [Starlette](https://www.starlette.io/) and [FastAPI](https://fastapi.tiangolo.com)
- `PerspectiveAIOHTTPHandler` for [AIOHTTP](https://docs.aiohttp.org),
- `tornado_websocket`, a Tornado-based websocket client
- `aiohttp_websocket` an AIOHTTP-based websocket client
- `PerspectiveManager` the session manager for a shared server deployment of
`perspective-python`.

Expand Down Expand Up @@ -342,7 +350,7 @@ Using Tornado and
as well as `Perspective`'s JavaScript library, we can set up "distributed"
Perspective instances that allows multiple browser `perspective-viewer` clients
to read from a common `perspective-python` server, as in the
[Tornado Example Project](https://github.com/finos/perspective/tree/master/examples/tornado-python).
[Tornado Example Project](https://github.com/finos/perspective/tree/master/examples/python-tornado).

This architecture works by maintaining two `Tables`—one on the server, and one
on the client that mirrors the server's `Table` automatically using `on_update`.
Expand Down Expand Up @@ -409,7 +417,11 @@ _*index.html*_

For a more complex example that offers distributed editing of the server
dataset, see
[client_server_editing.html](https://github.com/finos/perspective/blob/master/examples/tornado-python/client_server_editing.html).
[client_server_editing.html](https://github.com/finos/perspective/blob/master/examples/python-tornado/client_server_editing.html).

We also provide examples for Starlette/FastAPI and AIOHTTP:
- [Starlette Example Project](https://github.com/finos/perspective/tree/master/examples/python-starlette).
- [AIOHTTP Example Project](https://github.com/finos/perspective/tree/master/examples/python-aiohttp).

### Server-only Mode

Expand Down
7 changes: 7 additions & 0 deletions examples/python-aiohttp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# python-aiohttp

This example contains a simple `perspective-python` folder that uses AIOHTTP to serve a static dataset to the user through various [data bindings](https://perspective.finos.org/docs/md/server.html):

- `index.html`: a [client/server replicated](https://perspective.finos.org/docs/md/server.html#clientserver-replicated) setup that synchronizes the client and server data using Apache Arrow.
- `server_mode.html`: a [server-only](https://perspective.finos.org/docs/md/server.html#server-only) setup that reads data and performs operations directly on the server using commands sent through the Websocket.
- `client_server_editing`: a client-server replicated setup that also enables editing, where edits from multiple clients are applied properly to the server, and then synchronized back to the clients.
24 changes: 24 additions & 0 deletions examples/python-aiohttp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "python-aiohttp",
"private": true,
"version": "1.4.0",
"description": "An example of editing a `perspective-python` server from the browser.",
"scripts": {
"start": "PYTHONPATH=../../python/perspective python3 server.py"
},
"keywords": [],
"license": "Apache-2.0",
"dependencies": {
"@finos/perspective": "^1.4.0",
"@finos/perspective-viewer": "^1.4.0",
"@finos/perspective-viewer-d3fc": "^1.4.0",
"@finos/perspective-viewer-datagrid": "^1.4.0",
"@finos/perspective-workspace": "^1.4.0",
"superstore-arrow": "^1.0.0"
},
"devDependencies": {
"@finos/perspective-webpack-plugin": "^1.4.0",
"npm-run-all": "^4.1.3",
"rimraf": "^2.5.2"
}
}
63 changes: 63 additions & 0 deletions examples/python-aiohttp/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
################################################################################
#
# Copyright (c) 2019, the Perspective Authors.
#
# This file is part of the Perspective library, distributed under the terms of
# the Apache License 2.0. The full license can be found in the LICENSE file.
#
import asyncio
import os
import os.path
import logging
import threading

from aiohttp import web

from perspective import Table, PerspectiveManager, PerspectiveAIOHTTPHandler


here = os.path.abspath(os.path.dirname(__file__))
file_path = os.path.join(
here, "..", "..", "node_modules", "superstore-arrow", "superstore.arrow"
)


def perspective_thread(manager):
"""Perspective application thread starts its own event loop, and
adds the table with the name "data_source_one", which will be used
in the front-end."""
psp_loop = asyncio.new_event_loop()
manager.set_loop_callback(psp_loop.call_soon_threadsafe)
with open(file_path, mode="rb") as file:
table = Table(file.read(), index="Row ID")
manager.host_table("data_source_one", table)
psp_loop.run_forever()


def make_app():
manager = PerspectiveManager()

thread = threading.Thread(target=perspective_thread, args=(manager,))
thread.daemon = True
thread.start()

async def websocket_handler(request):
handler = PerspectiveAIOHTTPHandler(manager=manager, request=request)
await handler.run()

app = web.Application()
app.router.add_get("/websocket", websocket_handler)
app.router.add_static(
"/node_modules/@finos", "../../node_modules/@finos", follow_symlinks=True
)
app.router.add_static(
"/node_modules", "../../node_modules/@finos", follow_symlinks=True
)
app.router.add_static("/", "../python-tornado", show_index=True)
return app


if __name__ == "__main__":
app = make_app()
logging.critical("Listening on http://localhost:8080")
web.run_app(app, host="0.0.0.0", port=8080)
2 changes: 1 addition & 1 deletion examples/python-starlette/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# starlette-python
# python-starlette

This example contains a simple `perspective-python` folder that uses Starlette/FastAPI to serve a static dataset to the user through various [data bindings](https://perspective.finos.org/docs/md/server.html):

Expand Down
14 changes: 7 additions & 7 deletions examples/python-starlette/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"name": "python-starlette",
"private": true,
"version": "1.3.13",
"version": "1.4.0",
"description": "An example of editing a `perspective-python` server from the browser.",
"scripts": {
"start": "PYTHONPATH=../../python/perspective python3 server.py"
},
"keywords": [],
"license": "Apache-2.0",
"dependencies": {
"@finos/perspective": "^1.3.13",
"@finos/perspective-viewer": "^1.3.13",
"@finos/perspective-viewer-d3fc": "^1.3.13",
"@finos/perspective-viewer-datagrid": "^1.3.13",
"@finos/perspective-workspace": "^1.3.13",
"@finos/perspective": "^1.4.0",
"@finos/perspective-viewer": "^1.4.0",
"@finos/perspective-viewer-d3fc": "^1.4.0",
"@finos/perspective-viewer-datagrid": "^1.4.0",
"@finos/perspective-workspace": "^1.4.0",
"superstore-arrow": "^1.0.0"
},
"devDependencies": {
"@finos/perspective-webpack-plugin": "^1.3.13",
"@finos/perspective-webpack-plugin": "^1.4.0",
"npm-run-all": "^4.1.3",
"rimraf": "^2.5.2"
}
Expand Down
10 changes: 6 additions & 4 deletions examples/python-starlette/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
################################################################################
#
# Copyright (c) 2022, the Perspective Authors.
# Copyright (c) 2019, the Perspective Authors.
#
# This file is part of the Perspective library, distributed under the terms of
# the Apache License 2.0. The full license can be found in the LICENSE file.
Expand Down Expand Up @@ -43,6 +43,7 @@ def perspective_thread(manager):
manager.host_table("data_source_one", table)
psp_loop.run_forever()


def make_app():
manager = PerspectiveManager()

Expand All @@ -51,14 +52,14 @@ def make_app():
thread.start()

async def websocket_handler(websocket: WebSocket):
handler = PerspectiveStarletteHandler(manager, websocket)
handler = PerspectiveStarletteHandler(manager=manager, websocket=websocket)
await handler.run()

# static_html_files = StaticFiles(directory="../python-tornado", html=True)
static_html_files = StaticFiles(directory="../python-tornado", html=True)

app = FastAPI()
app.add_api_websocket_route('/websocket', websocket_handler)
app.add_api_websocket_route("/websocket", websocket_handler)
app.get("/node_modules/{rest_of_path:path}")(static_nodemodules_handler)
app.mount("/", static_html_files)

Expand All @@ -71,7 +72,8 @@ async def websocket_handler(websocket: WebSocket):
)
return app


if __name__ == "__main__":
app = make_app()
logging.critical("Listening on http://localhost:8080")
uvicorn.run(app, host='0.0.0.0', port=8080)
uvicorn.run(app, host="0.0.0.0", port=8080)
2 changes: 1 addition & 1 deletion python/perspective/bench/tornado/async_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def make_app(manager):
[
(
r"/",
perspective.tornado_handler.PerspectiveTornadoHandler,
perspective.handlers.tornado.PerspectiveTornadoHandler,
{"manager": manager},
)
]
Expand Down
2 changes: 1 addition & 1 deletion python/perspective/bench/tornado/bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async def client(self, client_id):
)
await asyncio.sleep(delay)

psp_client = await perspective.tornado_handler.websocket(self.url)
psp_client = await perspective.client.tornado.websocket(self.url)
results = []

for i in range(self.num_runs):
Expand Down
33 changes: 27 additions & 6 deletions python/perspective/docs/perspective.core.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
``perspective.core`` contains modules that implements ``perspective-python`` in various environments,
most notably ``PerspectiveWidget`` and ``PerspectiveTornadoHandler``.
most notably ``PerspectiveWidget`` and the various Perspective web server handlers.

Additionally, ``perspective.core`` defines several enums that provide easy access to aggregate options, different plugins, sort directions etc.

For usage of ``PerspectiveWidget`` and ``PerspectiveTornadoHandler``, see the User Guide in the sidebar.
For usage of ``PerspectiveWidget`` and the Perspective web server handlers, see the User Guide in the sidebar.

.. automodule:: perspective.core
:members:
Expand All @@ -24,16 +24,37 @@ PerspectiveWidget
:show-inheritance:
:exclude-members: random

PerspectiveTornadoHandler
=========================
Perspective Webserver Handlers
=================================

``PerspectiveTornadoHandler`` is a ready-made Perspective server that interfaces seamlessly with
Perspective provides several ready-made integrations with webserver libraries that interfaces seamlessly with
``@finos/perspective-viewer`` in Javascript.

.. automodule:: perspective.tornado_handler.tornado_handler
.. automodule:: perspective.handlers.tornado
:members:
:show-inheritance:

.. automodule:: perspective.handlers.starlette
:members:
:show-inheritance:

.. automodule:: perspective.handlers.aiohttp
:members:
:show-inheritance:

Perspective Websocket Clients
==============================
Perspective also provides several client interfaces to integrate with the above Perspective webserver handlers.

.. automodule:: perspective.client.tornado
:members:
:show-inheritance:

.. automodule:: perspective.client.aiohttp
:members:
:show-inheritance:


PerspectiveManager
==================

Expand Down
86 changes: 85 additions & 1 deletion python/perspective/perspective/client/aiohttp.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,91 @@
################################################################################
#
# Copyright (c) 2022, the Perspective Authors.
# Copyright (c) 2019, the Perspective Authors.
#
# This file is part of the Perspective library, distributed under the terms of
# the Apache License 2.0. The full license can be found in the LICENSE file.
#

import aiohttp
import asyncio

from .websocket import (
PerspectiveWebsocketClient,
PerspectiveWebsocketConnection,
Periodic,
)


class AIOHTTPPeriodic(Periodic):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._running = True

async def _run(self):
while self._running:
await self._callback()
await asyncio.sleep(self._interval)

async def start(self):
return asyncio.create_task(self._run())

async def stop(self):
self._running = False


class PerspectiveAIOHTTPWebsocketConnection(PerspectiveWebsocketConnection):
def __init__(self, session=None):
self._ws = None
self._session = session or aiohttp.ClientSession()
self._run = True

async def _receive_messages(self):
async for msg in self._ws:
if msg.type == aiohttp.WSMsgType.TEXT:
self._on_message(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
self._on_message(msg.data)
elif msg.type == aiohttp.WSMsgType.CLOSE:
return

async def connect(self, url, on_message, max_message_size) -> None:
self._ws_cm = self._session.ws_connect(url)
self._ws = await self._ws_cm.__aenter__()
self._on_message = on_message
self._task = asyncio.create_task(self._receive_messages())

def periodic(self, callback, interval) -> Periodic:
return AIOHTTPPeriodic(callback=callback, interval=interval)

async def write(self, message, binary=False):
if binary:
return await self._ws.send_bytes(message)
else:
return await self._ws.send_str(message)

async def close(self):
try:
self._task.cancel()
await self._task
except asyncio.CancelledError:
...
await self._ws.close()


class PerspectiveAIOHTTPClient(PerspectiveWebsocketClient):
def __init__(self, session=None):
"""Create a `PerspectiveAIOHTTPClient` that interfaces with a Perspective server over a Websocket"""
super(PerspectiveAIOHTTPClient, self).__init__(
PerspectiveAIOHTTPWebsocketConnection(session=session)
)


async def websocket(url, session=None):
"""Create a new websocket client at the given `url`.
Args:
session (:obj:`aiohttp.ClientSession`): An optional aiohtttp session
"""
client = PerspectiveAIOHTTPClient(session=session)
await client.connect(url)
return client
Loading

0 comments on commit d241f50

Please sign in to comment.