Skip to content

Commit

Permalink
feat: Plotly Express iframes, DHC pydeephaven, envoy header fix (#29)
Browse files Browse the repository at this point in the history
- Updated dependencies and types to the latest versions
- Updated formatting
- Fixes #17, Tested by running the following snippet in a Jupyter lab and ensuring the plot appeared correctly
```python
from deephaven_server import Server
s = Server(jvm_args=["-Dauthentication.psk=iris"])
s.start()

from deephaven_ipywidgets import DeephavenWidget
import deephaven.plot.express as dx
from deephaven import time_table

tt = time_table("PT1S").update(formulas=[
    "Latitude = Math.random()*180.0 - 90.0", 
    "Longitude = Math.random()*360.0 - 180.0", 
    "Magnitude = Math.random()*3 + 5"
    ])


fig = dx.density_mapbox(tt,  lat='Latitude', lon='Longitude', z='Magnitude', radius=10,
                        center=dict(lat=0, lon=180), zoom=0, mapbox_style="carto-darkmatter")
display(DeephavenWidget(fig))
```
- Add support for opening DHC pydeephaven tables
  - Check if we're in a DndSession/the session_manager is there before trying to pull properties from it
  - Fixes #26, Fixes #27
  - Tested using the following snippet:
```python
from pydeephaven import Session
from deephaven_ipywidgets import DeephavenWidget
session = Session(auth_token="iris", auth_type="io.deephaven.authentication.psk.PskAuthenticationHandler")
tt = session.time_table(period=1_000_000_000).update("X=i")
display(DeephavenWidget(tt))
```
- Fetch figure/objects by name when using pydeephaven
  - Allow passing in an object by name and a session when using pydeephaven
  - Fixes #28
  - Tested using the following snippet:
```python
from pydeephaven import Session
from deephaven_ipywidgets import DeephavenWidget
session = Session(auth_token="iris", auth_type="io.deephaven.authentication.psk.PskAuthenticationHandler")
session.run_script("""
from deephaven import empty_table
from deephaven.plot.figure import Figure
t = empty_table(100).update(["X=i", "Y=i*i"])
f = Figure().plot_xy(series_name="X vs Y", t=t, x="X", y="Y").show()
""")
display(DeephavenWidget("f", session=session))
```
- fix: Incompatibility with older versions of Jupyter
  - Fixes #30 
  - Test by installing older version of notebook:
```sh
pip install deephaven-server notebook==6.5.6 jupyterlab
pip install -e ".[test, examples]"
jupyter notebook
```
  - Then running in the Jupyter notebook:
```python
from deephaven_server import Server
s = Server(jvm_args=["-Dauthentication.psk=iris"])
s.start()

from deephaven import empty_table
from deephaven_ipywidgets import DeephavenWidget
t = empty_table(1000).update("x=i")
display(DeephavenWidget(t))
```
  • Loading branch information
mofojed authored Jan 5, 2024
1 parent 428670b commit c9399a3
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 513 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ jupyter nbextension enable --py [--sys-prefix|--user|--system] deephaven-ipywidg
First you'll need to start the [Deephaven server](https://github.com/deephaven/deephaven-core/blob/d73ef01cdf6fda43f7d03110995add26d16d4eae/py/embedded-server/README.md).

```python
# Start up the Deephaven Server
# Start up the Deephaven Server on port 8080 with token `iris`
from deephaven_server import Server
s = Server(port=8080)
s = Server(port=8080, jvm_args=["-Dauthentication.psk=iris"])
s.start()
```

Expand All @@ -50,11 +50,14 @@ display(DeephavenWidget(t, width=100, height=250))
```

### Alternate Deephaven Server URL
By default, the Deephaven server is located at `http://localhost:{port}`, where `{port}` is the port set in the Deephaven server creation call. If the server is not there, such as when running a Jupyter notebook in a Docker container, modify the `DEEPHAVEN_IPY_URL` environmental variable to the correct URL before creating a `DeephavenWidget`.

By default, the Deephaven server is located at `http://localhost:{port}`, where `{port}` is the port set in the Deephaven server creation call. If the server is not there, such as when running a Jupyter notebook in a Docker container, modify the `DEEPHAVEN_IPY_URL` environmental variable to the correct URL before creating a `DeephavenWidget`.

```python
import os
import os
os.environ["DEEPHAVEN_IPY_URL"] = "http://localhost:1234"
```

## Development Installation

Before starting, you will need [python3](https://www.python.org/downloads/), [node](https://nodejs.org/en/download/), and [yarn](https://classic.yarnpkg.com/lang/en/docs/install/) installed.
Expand Down Expand Up @@ -132,6 +135,13 @@ After a change wait for the build to finish and then refresh your browser and th

If you make a change to the python code then you will need to restart the notebook kernel to have it take effect.

### Testing your changes

There are separate test suites for the python and TypeScript code.

- **Python:** To run the python tests, run `pytest` in the root directory of the repository.
- **TypeScript:** To run the TypeScript tests, run `yarn run lint:check` in the root directory of the repository to run the `eslint` tests. Then run `yarn run test` to run the rest of the unit tests.

## Releasing your initial packages:

- Add tests
Expand Down
115 changes: 68 additions & 47 deletions deephaven_ipywidgets/deephaven.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
import atexit


TABLE_TYPES = {"deephaven.table.Table", "pandas.core.frame.DataFrame", "pydeephaven.table.Table"}
FIGURE_TYPES = {"deephaven.plot.figure.Figure"}


def _str_object_type(obj):
"""Returns the object type as a string value"""
return f"{obj.__class__.__module__}.{obj.__class__.__name__}"
Expand All @@ -29,11 +33,13 @@ def _path_for_object(obj):
"""Return the iframe path for the specified object. Inspects the class name to determine."""
name = _str_object_type(obj)

if name in ('deephaven.table.Table', 'pandas.core.frame.DataFrame', 'pydeephaven.table.Table'):
return 'table'
if name == 'deephaven.plot.figure.Figure':
return 'chart'
raise TypeError(f"Unknown object type: {name}")
if name in TABLE_TYPES:
return "table"
if name in FIGURE_TYPES:
return "chart"

# No special handling for this type, just try it as a widget
return "widget"


def _cleanup(widget: DeephavenWidget):
Expand All @@ -43,70 +49,86 @@ def _cleanup(widget: DeephavenWidget):
Args:
widget (DeephavenWidget): The widget to remove
"""
widget.set_trait('kernel_active', False)
widget.set_trait("kernel_active", False)


class DeephavenWidget(DOMWidget):
"""A wrapper for viewing DeephavenWidgets in IPython
"""
_model_name = Unicode('DeephavenModel').tag(sync=True)
"""A wrapper for viewing DeephavenWidgets in IPython"""

_model_name = Unicode("DeephavenModel").tag(sync=True)
_model_module = Unicode(module_name).tag(sync=True)
_model_module_version = Unicode(module_version).tag(sync=True)
_view_name = Unicode('DeephavenView').tag(sync=True)
_view_name = Unicode("DeephavenView").tag(sync=True)
_view_module = Unicode(module_name).tag(sync=True)
_view_module_version = Unicode(module_version).tag(sync=True)

object_id = Unicode().tag(sync=True)
object_type = Unicode().tag(sync=True)
server_url = Unicode().tag(sync=True)
iframe_url = Unicode().tag(sync=True)
width = Integer().tag(sync=True)
height = Integer().tag(sync=True)
token = Unicode().tag(sync=True)
kernel_active = Bool().tag(sync=True)

def __init__(self, deephaven_object, height=600, width=0):
def __init__(self, deephaven_object, height=600, width=0, session=None):
"""Create a Deephaven widget for displaying in an interactive Python console.
Args:
deephaven_object (Table): the Deephaven object to display
deephaven_object (deephaven.table.Table | pandas.core.frame.DataFrame | pydeephaven.table.Table | str): the Deephaven object to display, or the name of the object if using pydeephaven
height (int): the height of the table
width (int): the width of the table. Set to 0 to take up full width of notebook.
session (pydeephaven.session.Session): the session to load the table from. Required only if using a remote pydeephaven object by name.
"""
super(DeephavenWidget, self).__init__()

# Generate a new table ID using a UUID prepended with a `t_` prefix
object_id = f"t_{str(uuid4()).replace('-', '_')}"

params = {
"name": object_id
}

token = ''

if _str_object_type(deephaven_object) == 'pydeephaven.table.Table':
object_id = (
deephaven_object
if isinstance(deephaven_object, str)
else f"_{str(uuid4()).replace('-', '_')}"
)

params = {"name": object_id}
port = 10000
token = ""

if isinstance(deephaven_object, str):
if session is None:
raise ValueError(
"session must be specified when using a remote pydeephaven object by name"
)
port = session.port
server_url = f"http://{session.host}:{port}/"
elif _str_object_type(deephaven_object) == "pydeephaven.table.Table":
session = deephaven_object.session

envoy_prefix = session._extra_headers[
b'envoy-prefix'].decode('ascii')

token = base64.b64encode(
session.session_manager.auth_client.get_token("RemoteQueryProcessor").SerializeToString()
).decode('us-ascii')

params.update({
"authProvider": "parent",
"envoyPrefix": envoy_prefix
})
if b"envoy-prefix" in session._extra_headers:
params["envoyPrefix"] = session._extra_headers[b"envoy-prefix"].decode(
"ascii"
)

port = deephaven_object.session.port
server_url = deephaven_object.session.pqinfo().state.connectionDetails.staticUrl
server_url = f"http://{deephaven_object.session.host}:{port}/"

if hasattr(session, "session_manager"):
params["authProvider"] = "parent"
# We have a DnD session, and we can get the authentication and connection details from the session manager
token = base64.b64encode(
session.session_manager.auth_client.get_token(
"RemoteQueryProcessor"
).SerializeToString()
).decode("us-ascii")
server_url = (
deephaven_object.session.pqinfo().state.connectionDetails.staticUrl
)

session.bind_table(object_id, deephaven_object)
else:
port = Server.instance.port
server_url = f"http://localhost:{port}/"

# Add the table to the main modules globals list so it can be retrieved by the iframe
__main__.__dict__[object_id] = deephaven_object

param_values = [f"{k}={v}" for k, v in params.items()]
param_string = "?" + "&".join(param_values)

Expand All @@ -115,6 +137,7 @@ def __init__(self, deephaven_object, height=600, width=0):

try:
from google.colab.output import eval_js

server_url = eval_js(f"google.colab.kernel.proxyPort({port})")
except ImportError:
pass
Expand All @@ -123,17 +146,15 @@ def __init__(self, deephaven_object, height=600, width=0):
server_url = f"{server_url}/"

# Generate the iframe_url from the object type
iframe_url = f"{server_url}iframe/{_path_for_object(deephaven_object)}/{param_string}"
# Add the table to the main modules globals list so it can be retrieved by the iframe
__main__.__dict__[object_id] = deephaven_object

self.set_trait('server_url', server_url)
self.set_trait('iframe_url', iframe_url)
self.set_trait('object_id', object_id)
self.set_trait('object_type', _str_object_type(deephaven_object))
self.set_trait('width', width)
self.set_trait('height', height)
self.set_trait('token', token)
self.set_trait('kernel_active', True)
iframe_url = (
f"{server_url}iframe/{_path_for_object(deephaven_object)}/{param_string}"
)

self.set_trait("server_url", server_url)
self.set_trait("iframe_url", iframe_url)
self.set_trait("width", width)
self.set_trait("height", height)
self.set_trait("token", token)
self.set_trait("kernel_active", True)

atexit.register(_cleanup, self)
2 changes: 1 addition & 1 deletion deephaven_ipywidgets/tests/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ def test_example_creation_blank(MockTableClass):
mock_table.__class__.__name__ = 'Table'
w = DeephavenWidget(mock_table)
assert w.server_url == 'http://localhost:9876/'
assert str(w.iframe_url).startswith('http://localhost:9876/iframe/table/?name=t_')
assert str(w.iframe_url).startswith('http://localhost:9876/iframe/table/?name=_')
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,17 @@
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@deephaven/jsapi-shim": "^0.41.0",
"@deephaven/jsapi-utils": "^0.41.3",
"@deephaven/log": "^0.41.0",
"@deephaven/jsapi-shim": "^0.58.0",
"@deephaven/jsapi-utils": "^0.58.0",
"@deephaven/log": "^0.58.0",
"@jupyter-widgets/base": "^6.0.0",
"uuid": "8.3.2"
},
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/preset-env": "^7.5.0",
"@deephaven/eslint-config": "^0.41.0",
"@deephaven/prettier-config": "^0.41.0",
"@deephaven/eslint-config": "^0.58.0",
"@deephaven/prettier-config": "^0.58.0",
"@jupyter-widgets/base-manager": "^1.0.7",
"@jupyterlab/builder": "^4.0.6",
"@lumino/application": "^1.6.0",
Expand All @@ -74,13 +74,13 @@
"babel-eslint": "^10.1.0",
"css-loader": "^3.2.0",
"eslint": "^8.29.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-es": "^4.1.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "7.30.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.3.4",
Expand All @@ -89,7 +89,7 @@
"jest": "^26.0.0",
"mkdirp": "^0.5.1",
"npm-run-all": "^4.1.3",
"prettier": "^2.0.5",
"prettier": "^3.0.0",
"rimraf": "^2.6.2",
"source-map-loader": "^1.1.3",
"style-loader": "^1.0.0",
Expand Down
Loading

0 comments on commit c9399a3

Please sign in to comment.