Skip to content

Commit bfc12d8

Browse files
authored
Merge pull request #3125 from perspective-dev/polars-virtual-server
`polars` Virtual Server
2 parents ead2fc3 + 16fbd68 commit bfc12d8

File tree

22 files changed

+2454
-21
lines changed

22 files changed

+2454
-21
lines changed

docs/md/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
- [Virtual Servers](./how_to/python/virtual_server.md)
5757
- [DuckDB](./how_to/python/virtual_server/duckdb.md)
5858
- [ClickHouse](./how_to/python/virtual_server/clickhouse.md)
59+
- [Polars](./how_to/python/virtual_server/polars.md)
5960
- [Custom](./how_to/python/virtual_server/custom.md)
6061

6162
# Rust

docs/md/how_to/python/virtual_server.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Perspective ships with built-in virtual server implementations for:
1313
`duckdb` Python package.
1414
- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server
1515
using the `clickhouse-connect` Python package.
16+
- [**Polars**](./virtual_server/polars.md) — query in-memory Polars DataFrames
17+
using the `polars` Python package.
1618

1719
You can also [**implement your own**](./virtual_server/custom.md) virtual server
1820
to connect Perspective to any data source by subclassing `VirtualServerHandler`.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Polars Virtual Server
2+
3+
Perspective provides a built-in virtual server for
4+
[Polars](https://pola.rs/), allowing `<perspective-viewer>` clients to query
5+
in-memory Polars DataFrames over WebSocket.
6+
7+
## Installation
8+
9+
```bash
10+
pip install perspective-python polars
11+
```
12+
13+
## Usage
14+
15+
Create a server that exposes Polars DataFrames to browser clients:
16+
17+
```python
18+
import polars as pl
19+
import tornado.web
20+
import tornado.ioloop
21+
from perspective.virtual_servers.polars import PolarsVirtualServer
22+
from perspective.handlers.tornado import PerspectiveTornadoHandler
23+
24+
# Load data into Polars DataFrames
25+
df = pl.read_parquet("data.parquet")
26+
27+
# Create virtual server backed by Polars (dict of name -> DataFrame)
28+
server = PolarsVirtualServer({"my_table": df})
29+
30+
# Serve over WebSocket
31+
app = tornado.web.Application([
32+
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
33+
])
34+
35+
app.listen(8080)
36+
tornado.ioloop.IOLoop.current().start()
37+
```
38+
39+
Connect from the browser:
40+
41+
```javascript
42+
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
43+
const table = await websocket.open_table("my_table");
44+
document.getElementById("viewer").load(table);
45+
```
46+
47+
## Examples
48+
49+
- [Python Polars example](https://github.com/perspective-dev/perspective/tree/master/examples/python-polars-virtual)

examples/python-clickhouse-virtual/server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13+
import logging
1314
from pathlib import Path
1415

1516
import clickhouse_connect
@@ -20,9 +21,13 @@
2021
import tornado.ioloop
2122
import tornado.web
2223

23-
from loguru import logger
2424
from tornado.web import StaticFileHandler
2525

26+
logging.basicConfig(
27+
level=logging.DEBUG,
28+
)
29+
30+
logger = logging.getLogger(__name__)
2631

2732
INPUT_FILE = (
2833
Path(__file__).parent.resolve()

examples/python-duckdb-virtual/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
// Websocket connections at the specified URL.
2424
const websocket = await perspective.websocket("ws://localhost:3000/websocket");
2525
viewer.load(websocket);
26-
viewer.restore({table: "data_source_one"})
26+
viewer.restore({table: "memory.data_source_one"})
2727
</script>
2828
</body>
2929
</html>

examples/python-duckdb-virtual/server.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13+
import logging
1314
from pathlib import Path
1415

1516
import duckdb
@@ -20,9 +21,13 @@
2021
import tornado.web
2122
import tornado.websocket
2223

23-
from loguru import logger
2424
from tornado.web import StaticFileHandler
2525

26+
logging.basicConfig(
27+
level=logging.DEBUG,
28+
)
29+
30+
logger = logging.getLogger(__name__)
2631

2732
INPUT_FILE = (
2833
Path(__file__).parent.resolve()
@@ -31,7 +36,6 @@
3136
/ "superstore.parquet"
3237
)
3338

34-
3539
if __name__ == "__main__":
3640
db = duckdb.connect(":memory:perspective")
3741
db.sql(
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
5+
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
6+
<style>
7+
perspective-viewer {
8+
position: absolute;
9+
inset: 0;
10+
}
11+
</style>
12+
</head>
13+
<body>
14+
<perspective-viewer id="viewer" ,> </perspective-viewer>
15+
<script type="module">
16+
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
17+
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
18+
import "/node_modules/@perspective-dev/viewer-d3fc/dist/cdn/perspective-viewer-d3fc.js";
19+
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
20+
const viewer = document.getElementById("viewer");
21+
22+
// Create a client that expects a Perspective server to accept
23+
// Websocket connections at the specified URL.
24+
const websocket = await perspective.websocket("ws://localhost:3000/websocket");
25+
viewer.load(websocket);
26+
viewer.restore({table: "data_source_one"})
27+
</script>
28+
</body>
29+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "python-polars-virtual",
3+
"private": true,
4+
"version": "4.2.0",
5+
"description": "An example of a Polars-backed Perspective virtual server.",
6+
"scripts": {
7+
"start": "PYTHONPATH=../../python/perspective python3 server.py"
8+
},
9+
"keywords": [],
10+
"license": "Apache-2.0",
11+
"dependencies": {
12+
"@perspective-dev/client": "workspace:^",
13+
"@perspective-dev/viewer": "workspace:^",
14+
"@perspective-dev/viewer-d3fc": "workspace:^",
15+
"@perspective-dev/viewer-datagrid": "workspace:^",
16+
"@perspective-dev/workspace": "workspace:^",
17+
"superstore-arrow": "catalog:"
18+
},
19+
"devDependencies": {
20+
"npm-run-all": "catalog:"
21+
}
22+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
import logging
14+
from pathlib import Path
15+
16+
import polars as pl
17+
import perspective
18+
import perspective.handlers.tornado
19+
import perspective.virtual_servers.polars
20+
import tornado.ioloop
21+
import tornado.web
22+
import tornado.websocket
23+
24+
from tornado.web import StaticFileHandler
25+
26+
logger = logging.getLogger(__name__)
27+
28+
INPUT_FILE = (
29+
Path(__file__).parent.resolve()
30+
/ "node_modules"
31+
/ "superstore-arrow"
32+
/ "superstore.parquet"
33+
)
34+
35+
36+
if __name__ == "__main__":
37+
df = pl.read_parquet(INPUT_FILE)
38+
tables = {"data_source_one": df}
39+
40+
virtual_server = perspective.virtual_servers.polars.PolarsVirtualServer(tables)
41+
app = tornado.web.Application(
42+
[
43+
(
44+
r"/websocket",
45+
perspective.handlers.tornado.PerspectiveTornadoHandler,
46+
{"perspective_server": virtual_server},
47+
),
48+
(r"/node_modules/(.*)", StaticFileHandler, {"path": "../../node_modules/"}),
49+
(
50+
r"/(.*)",
51+
StaticFileHandler,
52+
{"path": "./", "default_filename": "index.html"},
53+
),
54+
],
55+
websocket_max_message_size=100 * 1024 * 1024,
56+
)
57+
58+
app.listen(3000)
59+
logger.info("Listening on http://localhost:3000")
60+
loop = tornado.ioloop.IOLoop.current()
61+
loop.start()

pnpm-lock.yaml

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)