Metadata-Version: 2.5
Name: spaday-perspective
Version: 0.1.1
Summary: perspective for spaday
Project-URL: Repository, https://github.com/1kbgz/spaday-perspective
Project-URL: Homepage, https://github.com/1kbgz/spaday-perspective
Author-email: 1kbgz <dev@1kbgz.com>
License: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.11
Requires-Dist: spaday>=0.3.0
Provides-Extra: develop
Requires-Dist: build; extra == 'develop'
Requires-Dist: bump-my-version; extra == 'develop'
Requires-Dist: check-dist; extra == 'develop'
Requires-Dist: codespell; extra == 'develop'
Requires-Dist: hatch-js; extra == 'develop'
Requires-Dist: hatchling; extra == 'develop'
Requires-Dist: httpx; extra == 'develop'
Requires-Dist: mdformat; extra == 'develop'
Requires-Dist: mdformat-tables>=1; extra == 'develop'
Requires-Dist: perspective-python<4.6,>=4.5; extra == 'develop'
Requires-Dist: pydantic>=2; extra == 'develop'
Requires-Dist: pytest; extra == 'develop'
Requires-Dist: pytest-cov; extra == 'develop'
Requires-Dist: ruff; extra == 'develop'
Requires-Dist: starlette; extra == 'develop'
Requires-Dist: transports; extra == 'develop'
Requires-Dist: twine; extra == 'develop'
Requires-Dist: ty; extra == 'develop'
Requires-Dist: uv; extra == 'develop'
Requires-Dist: uvicorn; extra == 'develop'
Requires-Dist: websockets; extra == 'develop'
Requires-Dist: wheel; extra == 'develop'
Provides-Extra: examples
Requires-Dist: perspective-python<4.6,>=4.5; extra == 'examples'
Requires-Dist: pydantic>=2; extra == 'examples'
Requires-Dist: starlette; extra == 'examples'
Requires-Dist: transports; extra == 'examples'
Requires-Dist: uvicorn; extra == 'examples'
Requires-Dist: websockets; extra == 'examples'
Description-Content-Type: text/markdown

# spaday-perspective

[Perspective](https://perspective-dev.github.io) for [spaday](https://1kbgz.github.io/spaday/)

[![Build Status](https://github.com/1kbgz/spaday-perspective/actions/workflows/build.yaml/badge.svg?branch=main&event=push)](https://github.com/1kbgz/spaday-perspective/actions/workflows/build.yaml)
[![codecov](https://codecov.io/gh/1kbgz/spaday-perspective/branch/main/graph/badge.svg)](https://codecov.io/gh/1kbgz/spaday-perspective)
[![License](https://img.shields.io/github/license/1kbgz/spaday-perspective)](https://github.com/1kbgz/spaday-perspective)
[![PyPI](https://img.shields.io/pypi/v/spaday-perspective.svg)](https://pypi.python.org/pypi/spaday-perspective)

## Overview

`spaday-perspective` moves the existing `PerspectivePanel` integration out of spaday core. Its
self-contained `<perspective-panel>` bundle includes Perspective's client, viewer, workspace, datagrid,
themes, and viewer WASM.

## Documentation

- [Build a live market workspace](docs/src/tutorial.md) — guided first application.
- [Stream data and change layouts](docs/src/how-to.md) — task-focused server and UI recipes.
- [API reference](docs/src/reference.md) — component props, layout shape, and package descriptor.
- [Why Perspective uses its own data channel](docs/src/explanation.md) — integration design and tradeoffs.

## Quick example

This complete app serves a live Perspective table. **Add trade** streams a new row into the grid,
the select switches between flat and grouped layouts, and the checkbox changes the viewer theme.

```python
import perspective
from perspective.handlers.starlette import PerspectiveStarletteHandler
from starlette.responses import JSONResponse
from starlette.routing import Route, WebSocketRoute
from starlette.websockets import WebSocket

from spaday import CallEndpoint, cond, element, eq, field, obj
from spaday.backends.starlette import serve
from spaday_perspective import PerspectivePanel

server = perspective.Server()
client = server.new_local_client()
trades = client.table(
    [
        {"id": 1, "symbol": "AAPL", "side": "buy", "price": 211.10},
        {"id": 2, "symbol": "MSFT", "side": "sell", "price": 503.02},
    ],
    name="trades",
)
next_id = 2


async def add_trade(_request):
    global next_id
    next_id += 1
    trades.update(
        [
            {
                "id": next_id,
                "symbol": "AAPL" if next_id % 2 else "MSFT",
                "side": "buy" if next_id % 2 else "sell",
                "price": 210 + next_id,
            }
        ]
    )
    return JSONResponse({"id": next_id})


async def perspective_socket(websocket: WebSocket):
    await PerspectiveStarletteHandler(perspective_server=server, websocket=websocket).run()


def layout(*, grouped=False):
    viewer = {"table": "trades", "plugin": "Datagrid", "columns": ["symbol", "side", "price"]}
    if grouped:
        viewer.update({"group_by": ["symbol"], "columns": ["price"], "aggregates": {"price": "avg"}})
    return {
        "sizes": [1],
        "detail": {"main": {"type": "tab-area", "widgets": ["trades"], "currentIndex": 0}},
        "master": {"sizes": [], "widgets": []},
        "mode": "globalFilters",
        "viewers": {"trades": viewer},
    }


panel = (
    PerspectivePanel()
    .compute("theme", cond(field("dark"), "dark", "light"))
    .compute(
        "config",
        obj(
            {
                "ws_url": "/perspective",
                "tables": ["trades"],
                "layout": cond(eq(field("view"), "grouped"), layout(grouped=True), layout()),
            }
        ),
    )
    .style(display="block", height="70vh", margin_top="1rem")
)

view = element("select").bind("value", "view", mode="two-way").child(
    element("option", "Blotter", value="blotter"),
    element("option", "Average by symbol", value="grouped"),
)
dark = element("input", type="checkbox").bind("checked", "dark", mode="two-way")
page = (
    element("main")
    .style(margin="1rem", font_family="system-ui")
    .child(element("button").text("Add trade").on("click", CallEndpoint("POST", "/api/trades")))
    .child(view)
    .child(element("label").child(dark, " Dark theme"))
    .child(panel)
)

app = serve(
    page,
    packages=["perspective"],
    store={"view": "blotter", "dark": False},
    routes=[
        Route("/api/trades", add_trade, methods=["POST"]),
        WebSocketRoute("/perspective", perspective_socket),
    ],
)
```

Save this as `app.py`, then run
`pip install spaday-perspective perspective-python starlette uvicorn` and `uvicorn app:app`.
Open <http://127.0.0.1:8000>.

This keeps Perspective's bulk data on its native websocket. A spaday/transports model only needs to
carry the small serializable connection/layout config. Replacing `config` reconnects when `ws_url`
changes and restores changed layouts; `theme` accepts `light`, `dark`, or a Perspective theme name.

Installing this project registers the `perspective` entry point with spaday. The equivalent explicit
forms are `packages=[spaday_perspective.package]` and `packages=["spaday_perspective:package"]`.

## Run the local example

```bash
python -m pip install -e ".[examples]"
python -m spaday_perspective.example
```

Open `http://127.0.0.1:8015` to inspect the [complete market-monitor example](spaday_perspective/example.py): live native
Perspective streaming, flat and grouped layouts, server-authoritative order submission, reactive metrics,
and light/dark themes. It passes the local package descriptor directly, so it does not install or resolve
the integration from GitHub.

> [!NOTE]
> This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).
