chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
@@ -0,0 +1,15 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from .viewer import PerspectiveViewer
__all__ = ["PerspectiveViewer"]
@@ -0,0 +1,22 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
def validate_version(version):
# basic semver of form \d+\.\d+\.\d+(\+.+)?
spl = version.split(".", 2)
return (
len(spl) == 3
and spl[0].isdigit()
and spl[1].isdigit()
and (spl[2].split("+")[0]).isdigit()
)
@@ -0,0 +1,343 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from random import random
from .viewer_traitlets import PerspectiveTraitlets
import perspective
# from .. import Server
# ─ │ ┌ ┬ ┐
# ┄ ┆ ├ ┼ ┤ ╲
# ┈ ┊ └ ┴ ┘
# ━ ┃ ┏ ┳ ┓ ┏ ┯ ┓ ┏ ┳ ┓ ┏ ┯ ┓
# ┅ ┇ ┣ ╋ ┫ ┣ ┿ ┫ ┠ ╂ ┨ ┠ ┼ ┨
# ┉ ┋ ┗ ┻ ┛ ┗ ┷ ┛ ┗ ┻ ┛ ┗ ┷ ┛
# global_server = PySyncServer()
class PerspectiveViewer(PerspectiveTraitlets, object):
"""PerspectiveViewer wraps the `perspective.Table` API and exposes an API
around creating views, loading data, and updating data.
"""
# Viewer attributes that should be saved in `save()` and restored using
# `restore()`. Symmetric to `PERSISTENT_ATTRIBUTES` in `perspective-viewer`.
PERSISTENT_ATTRIBUTES = (
"group_by",
"split_by",
"filter",
"sort",
"aggregates",
"columns",
"expressions",
"plugin",
"plugin_config",
"theme",
"settings",
"title",
"version",
)
def __init__(
self,
plugin="Datagrid",
columns=None,
group_by=None,
split_by=None,
aggregates=None,
sort=None,
filter=None,
expressions=None,
plugin_config=None,
settings=True,
theme=None,
title=None,
# ignored, here for restore compatibility
version=None,
):
"""Initialize an instance of `PerspectiveViewer` with the given viewer
configuration. Do not pass a `Table` or data into the constructor -
use the :func:`load()` method to provide the viewer with data.
Keyword Arguments:
columns (:obj:`list` of :obj:`str`): A list of column names to be
visible to the user.
group_by (:obj:`list` of :obj:`str`): A list of column names to
use as group by.
split_by (:obj:`list` of :obj:`str`): A list of column names
to use as split by.
aggregates (:obj:`dict` of :obj:`str` to :obj:`str`): A dictionary
of column names to aggregate types, which specify aggregates
for individual columns.
sort (:obj:`list` of :obj:`list` of :obj:`str`): A list of lists,
each list containing a column name and a sort direction
(``asc``, ``desc``, ``asc abs``, ``desc abs``, ``col asc``,
``col desc``, ``col asc abs``, ``col desc abs``).
filter (:obj:`list` of :obj:`list` of :obj:`str`): A list of lists,
each list containing a column name, a filter comparator, and a
value to filter by.
expressions (:obj:`list` of :obj:`str`): A list of string
expressions which are applied to the view.
plugin (:obj:`str`/:obj:`perspective.Plugin`): Which plugin to
select by default.
plugin_config (:obj:`dict`): A configuration for the plugin, i.e.
the datagrid plugin or a chart plugin.
settings(:obj:`bool`): Whether the perspective query settings
panel should be open.
theme (:obj:`str`): The color theme to use.
version (:obj:`str`): The version this configuration is restored from.
This should only be used when restoring a configuration,
and should not be set manually.
Examples:
>>> viewer = PerspectiveViewer(
... aggregates={"a": "avg"},
... group_by=["a"],
... sort=[["b", "desc"]],
... filter=[["a", ">", 1]],
... expressions=["\"a\" + 100"]
... )
"""
# The Table under management by this viewer and its
# attached PerspectiveManager
self._table = None
self._client = None
# Viewer configuration
self.plugin = plugin # validate_plugin(plugin)
self.columns = columns or [] # validate_columns(columns) or []
self.group_by = group_by or [] # validate_group_by(group_by) or []
self.split_by = split_by or [] # validate_split_by(split_by) or []
self.aggregates = aggregates or {} # validate_aggregates(aggregates) or {}
self.sort = sort or [] # validate_sort(sort) or []
self.filter = filter or [] # validate_filter(filter) or []
self.expressions = expressions or {} # validate_expressions(expressions) or {}
self.plugin_config = (
plugin_config or {}
) # validate_plugin_config(plugin_config) or {}
self.settings = settings
self.theme = theme
self.title = title
def new_proxy_session(self, cb):
return perspective.ProxySession(self._client, cb)
@property
def client(self):
"""Returns the ``perspective.Client`` or ``perspective.AsyncClient`` under management by the viewer."""
return self._client
@property
def table(self):
"""Returns the ``perspective.Table`` or ``perspective.AsyncTable`` under management by the viewer."""
return self._table
def is_async(self):
"""Returns whether this widget has an async interface or synchronous"""
return isinstance(self._table, perspective.perspective.AsyncTable)
def load(self, data, **options):
"""Given a ``perspective.Table``, a ``perspective.AsyncTable``,
or data that can be handled by ``perspective.Table``, pass it to the
viewer. Like `__init__`, load accepts a `perspective.Table`, a dataset,
or a schema.
``load()`` resets the state of the viewer: if a ``perspective.Table``
has already been loaded, ``**options`` is ignored as the options
already set on the ``Table`` take precedence.
If data is passed in, a ``perspective.Table`` is automatically created
by this method, and the options passed to ``**config`` are extended to
the new Table. If the widget already has a dataset, and the new data
has different columns to the old one, then the widget state (pivots,
sort, etc.) is cleared to prevent applying settings on columns that
don't exist.
When a ``perspective.AsyncTable`` is loaded, the widget's interface
becomes async. Methods which operate on the underlying Perspective
view, inclusive of the ``load()`` call itself, return coroutine values
which must be awaited.
Loading a ``perspective.Table`` or plain data will make the interface
synchronous again.
Args:
data (:obj:`Table`|:obj:`AsyncTable`|:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`|:obj:`bytes`|:obj:`str`): a
`perspective.Table` instance, a `perspective.AsyncTable`
instance, or a dataset to be loaded in the viewer.
Keyword Arguments:
name (:obj:`str`): An optional name to reference the table by so it can
be accessed from the front-end. If not provided, a name will
be generated.
index (:obj:`str`): A column name to be used as the primary key.
Ignored if a ``Table`` or ``AsyncTable`` is supplied.
limit (:obj:`int`): A upper limit on the number of rows in the Table.
Cannot be set at the same time as `index`. Ignored if a
``Table`` or ``AsyncTable`` is supplied.
Returns:
coro (:obj:`coroutine`): when `AsyncTable` is passed, the `load()` call must be awaited
"""
name = options.pop("name", str(random()))
# Reset the viewer when `load()` is called multiple times.
if self.table is not None:
self.reset()
if isinstance(data, perspective.perspective.AsyncTable):
self._table = data
async def load_table():
self._client = await self.table.get_client()
self.table_name = self.table.get_name()
# If the user does not set columns to show, synchronize viewer state
# with dataset.
if len(self.columns) == 0:
self.columns = await self.table.columns()
return load_table()
elif isinstance(data, perspective.perspective.Table):
self._table = data
self._client = data.get_client()
elif isinstance(data, perspective.perspective.View) or isinstance(data, perspective.perspective.AsyncView):
raise TypeError(
"Views cannot be loaded directly, load a table or raw data instead"
)
else:
self._table = perspective.table(data, name=name, **options)
self._client = perspective.GLOBAL_CLIENT
# If the user does not set columns to show, synchronize viewer state
# with dataset.
if len(self.columns) == 0:
self.columns = self.table.columns()
self.table_name = self.table.get_name()
def update(self, data):
"""Update the table under management by the viewer with new data.
This function follows the semantics of `Table.update()`, and will be
affected by whether an index is set on the underlying table.
When this widget has loaded an ``AsyncTable``, returns a coroutine
which must be awaited.
Args:
data (:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`): the
update data for the table.
Returns:
coro (:obj:`coroutine`): when async, must be awaited
"""
return self.table.update(data)
def clear(self):
"""Clears the rows of this viewer's ``Table``."""
if self.table is not None:
return self.table.clear()
def replace(self, data):
"""Replaces the rows of this viewer's `Table` with new data.
Args:
data (:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`): new data
to set into the table - must conform to the table's schema.
Returns:
coro (:obj:`coroutine`): when async, must be awaited
"""
if self.table is not None:
return self.table.replace(data)
def save(self):
"""Get the viewer's attributes as a dictionary, symmetric with `restore`
so that a viewer's configuration can be reproduced."""
return {
attr: getattr(self, attr)
for attr in PerspectiveViewer.PERSISTENT_ATTRIBUTES
}
def restore(self, **kwargs):
"""Restore a given set of attributes, passed as kwargs
(e.g. dictionary). Symmetric with `save` so that a given viewer's
configuration can be reproduced."""
for k, v in kwargs.items():
if k in PerspectiveViewer.PERSISTENT_ATTRIBUTES:
setattr(self, k, v)
def to_kwargs(self):
"""Get the viewer's attributes as a list of kwargs, which can be passed to
the viewer constructor"""
attrs = self.save()
defaults = {
"columns": [],
"group_by": [],
"split_by": [],
"aggregates": {},
"sort": [],
"filter": [],
"expressions": {},
"plugin_config": {},
"version": "2.10.0",
}
kwargs = {}
for key, default in defaults.items():
if attrs.get(key) != default:
kwargs[key] = attrs[key]
return ", ".join(
["{}={}".format(attr, repr(val)) for attr, val in kwargs.items()]
)
def reset(self):
"""Resets the viewer's attributes and state, but does not delete or
modify the underlying `Table`.
Example:
widget = PerspectiveWidget(data, group_by=["date"], plugin=Plugin.XBAR)
widget.reset()
widget.plugin #
"""
self.group_by = []
self.split_by = []
self.filter = []
self.sort = []
self.expressions = {}
self.aggregates = {}
self.columns = []
self.plugin = "Datagrid"
self.plugin_config = {}
def delete(self, delete_table=True):
"""Delete the Viewer's data and clears its internal state. If
``delete_table`` is True, the underlying `perspective.Table` and the
internal `View` object will be deleted.
Args:
delete_table (:obj:`bool`) : whether the underlying `Table` will be
deleted. Defaults to True.
Returns:
coro (:obj:`coroutine`): when async and `delete_table` is `True`,
must be awaited
"""
ret = None
if delete_table:
# Delete table
ret = self.table.delete()
self.table_name = None
self._table = None
self.reset()
return ret
@@ -0,0 +1,101 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from traitlets import HasTraits, Unicode, List, Bool, Dict, validate, Enum
import importlib.metadata
__version__ = importlib.metadata.version("perspective-python")
from .validate import (
validate_version,
)
class PerspectiveTraitlets(HasTraits):
"""Define the traitlet interface with `PerspectiveJupyterWidget` on the
front end. Attributes which are set here are synchronized between the
front-end and back-end.
Examples:
>>> widget = perspective.PerspectiveWidget(
... data, group_by=["a", "b", "c"])
PerspectiveWidget(group_by=["a", "b", "c"])
>>> widget.split_by=["b"]
>>> widget
PerspectiveWidget(group_by=["a", "b", "c"], split_by=["b"])
"""
# `perspective-viewer` options
plugin = Unicode("Datagrid").tag(sync=True)
columns = List(default_value=[]).tag(sync=True)
group_by = List(trait=Unicode(), default_value=[]).tag(sync=True, o=True)
split_by = List(trait=Unicode(), default_value=[]).tag(sync=True)
aggregates = Dict(default_value={}).tag(sync=True)
sort = List(default_value=[]).tag(sync=True)
filter = List(default_value=[]).tag(sync=True)
expressions = Dict(default_value=[]).tag(sync=True)
plugin_config = Dict(default_value={}).tag(sync=True)
settings = Bool(True).tag(sync=True)
theme = Unicode("Pro Light", allow_none=True).tag(sync=True)
# used to tell the frontend which table to connect to
table_name = Unicode(None, allow_none=True).tag(sync=True)
server = Bool(False).tag(sync=True)
binding_mode = Enum(("server", "client-server")).tag(default="server", sync=True)
title = Unicode(None, allow_none=True).tag(sync=True)
version = Unicode(__version__).tag(sync=True)
# @validate("plugin")
# def _validate_plugin(self, proposal):
# return validate_plugin(proposal.value)
# @validate("columns")
# def _validate_columns(self, proposal):
# return validate_columns(proposal.value)
# @validate("group_by")
# def _validate_group_by(self, proposal):
# return validate_group_by(proposal.value)
# @validate("split_by")
# def _validate_split_by(self, proposal):
# return validate_split_by(proposal.value)
# @validate("aggregates")
# def _validate_aggregates(self, proposal):
# return validate_aggregates(proposal.value)
# @validate("sort")
# def _validate_sort(self, proposal):
# return validate_sort(proposal.value)
# @validate("filter")
# def _validate_filter(self, proposal):
# return validate_filter(proposal.value)
# @validate("expressions")
# def _validate_expressions(self, proposal):
# return validate_expressions(proposal.value)
# @validate("plugin_config")
# def _validate_plugin_config(self, proposal):
# return validate_plugin_config(proposal.value)
# @validate("title")
# def _validate_title(self, proposal):
# return validate_title(proposal.value)
@validate("version")
def _validate_version(self, proposal):
return validate_version(proposal.value)