Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/registry/server-base/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
io.deephaven.project.ProjectType=DOCKER_REGISTRY
deephaven.registry.imageName=ghcr.io/deephaven/server-base:edge
deephaven.registry.imageId=ghcr.io/deephaven/server-base@sha256:bf7d30a09484c40b85f9c2edfa4082dfd8df276295ac636204d42addab6080a5
deephaven.registry.imageId=ghcr.io/deephaven/server-base@sha256:f774bf3349210a1d44988bde3b6b61c858e4e01f11de9b0b2040b46eceede2c2
2 changes: 1 addition & 1 deletion docker/registry/slim-base/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
io.deephaven.project.ProjectType=DOCKER_REGISTRY
deephaven.registry.imageName=ghcr.io/deephaven/server-slim-base:edge
deephaven.registry.imageId=ghcr.io/deephaven/server-slim-base@sha256:6159422839b101500be29884788342201a58ce7b0461179164867b43a0cbfa1a
deephaven.registry.imageId=ghcr.io/deephaven/server-slim-base@sha256:1cdcda42f911ca0dbaf2baf20a8cd17c2136bdaffd9149135c5ce9074913ba95
14 changes: 7 additions & 7 deletions docker/server-jetty/src/main/server-jetty/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
adbc-driver-manager==1.11.0
adbc-driver-postgresql==1.11.0
adbc-driver-manager==1.12.0
adbc-driver-postgresql==1.12.0
connectorx==0.4.5; platform.machine == 'x86_64'
deephaven-plugin==0.7.0
deephaven-plugin==0.8.0
docstring_parser==0.18.0
importlib_resources==7.1.0
java-utilities==0.3.0
Expand All @@ -10,10 +10,10 @@ jpy==2.1.0
llvmlite==0.48.0
numba==0.66.0
numpy==2.4.6
packaging==26.2
pandas==3.0.3
packaging==26.3
pandas==3.0.5
parso==0.8.7
pyarrow==24.0.0
pyarrow==25.0.1
python-dateutil==2.9.0.post0
six==1.17.0
typing_extensions==4.15.0
typing_extensions==4.16.0
14 changes: 7 additions & 7 deletions docker/server/src/main/server-netty/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
adbc-driver-manager==1.11.0
adbc-driver-postgresql==1.11.0
adbc-driver-manager==1.12.0
adbc-driver-postgresql==1.12.0
connectorx==0.4.5; platform.machine == 'x86_64'
deephaven-plugin==0.7.0
deephaven-plugin==0.8.0
docstring_parser==0.18.0
importlib_resources==7.1.0
java-utilities==0.3.0
Expand All @@ -10,10 +10,10 @@ jpy==2.1.0
llvmlite==0.48.0
numba==0.66.0
numpy==2.4.6
packaging==26.2
pandas==3.0.3
packaging==26.3
pandas==3.0.5
parso==0.8.7
pyarrow==24.0.0
pyarrow==25.0.1
python-dateutil==2.9.0.post0
six==1.17.0
typing_extensions==4.15.0
typing_extensions==4.16.0
51 changes: 26 additions & 25 deletions docker/web-plugin-packager/src/main/docker/files/pack-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,16 @@ TMP_PACKAGE_DIR=${TMP_DIR}/package
# Make a output folder
mkdir "${OUTPUT_DIR}"

# Start the manifest.json file. It will add all the dependencies automatically
echo "{\"plugins\":[" > "${MANIFEST_FILE}"

# Keep track of the count so we add a comma when necessary
PLUGIN_COUNT=0
# Keep track of the directories of all the packed plugins so we can build the manifest.json once everything has
# been downloaded and extracted
PACKAGE_DIRS=()

echo "Packing plugins $@..."

# Iterate through each plugin defined in the plugin list, download the package and adding info to the manifest
# Iterate through each plugin defined in the plugin list, download the package and move it into place
# Can/should include the version number in the plugin line item
for PACKAGE in "$@"
do
# Add a comma to the manifest.json if this is not the first plugin
if [ $PLUGIN_COUNT -gt 0 ]; then
echo "," >> "${MANIFEST_FILE}"
fi

# Make a temporary directory for downloading/extracting the package into
mkdir -p "${TMP_DIR}"
cd "${TMP_DIR}"
Expand All @@ -53,30 +46,38 @@ do

echo "In working dir $(pwd)"

# It always unzips to the folder "package". Get the name and version info from the package so we can move it to
# the correct location and add the info to the manifest
# It always unzips to the folder "package". Get the name from the package so we can move it to the correct
# location
PACKAGE_NAME=$(npm pkg get name --prefix="${TMP_PACKAGE_DIR}")
PACKAGE_INFO=$(npm pkg get name version main --prefix="${TMP_PACKAGE_DIR}")
# Need to remove quotes from the package name
PACKAGE_NAME="${PACKAGE_NAME%\"}"
PACKAGE_NAME="${PACKAGE_NAME#\"}"

echo "Got package name ${PACKAGE_NAME}"

# Add the info to the manifest file
echo "${PACKAGE_INFO}" >> "${MANIFEST_FILE}"

# Move the plugin to the correct directory
# Need to make the directory based on the name first
mkdir -p "${OUTPUT_DIR}/${PACKAGE_NAME}"
mv "${TMP_PACKAGE_DIR}/"* "${OUTPUT_DIR}/${PACKAGE_NAME}"
PACKAGE_DIR=${OUTPUT_DIR}/${PACKAGE_NAME}
mkdir -p "${PACKAGE_DIR}"
mv "${TMP_PACKAGE_DIR}/"* "${PACKAGE_DIR}"
rm --recursive "${TMP_DIR}"

# Increment the plugin count
PLUGIN_COUNT=$((PLUGIN_COUNT + 1))
PACKAGE_DIRS+=("${PACKAGE_DIR}")
done

echo "Done!"
# Assemble the full manifest in one shot by reading each plugin's package.json. The optional "loader" field is
# preserved as-is so plugin authors cannot shadow core fields like name/version/main.
"${NODE_EXE:-node}" -e '
const fs = require("fs");
const path = require("path");
const plugins = process.argv.slice(1).map((dir) => {
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
const entry = { name: pkg.name, version: pkg.version, main: pkg.main };
if (pkg.loader != null) {
entry.loader = pkg.loader;
}
return entry;
});
process.stdout.write(JSON.stringify({ plugins }, null, 2));
' ${PACKAGE_DIRS[@]+"${PACKAGE_DIRS[@]}"} > "${MANIFEST_FILE}"

# Close out the manifest file
echo "]}" >> "${MANIFEST_FILE}"
echo "Done!"
12 changes: 12 additions & 0 deletions plugin/src/main/java/io/deephaven/plugin/js/JsPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

/**
* A JS plugin is a {@link Plugin} that allows for custom javascript and related content to be served, see
Expand Down Expand Up @@ -100,6 +101,15 @@ public Paths paths() {
return Paths.all();
}

/**
* The JS plugin loader configuration, as sourced from the "loader" field of the plugin's package.json (if present).
* This is an arbitrary, plugin-specific JSON object that is opaque to the server and is simply passed through as
* the "loader" field for the manifest entry in "js-plugins/manifest.json".
*
* @return the loader configuration
*/
public abstract Optional<Object> loader();
Comment thread
mofojed marked this conversation as resolved.

@Override
public final <T, V extends Plugin.Visitor<T>> T walk(V visitor) {
return visitor.visit(this);
Expand Down Expand Up @@ -142,6 +152,8 @@ public interface Builder {

Builder paths(Paths paths);

Builder loader(Object loader);

JsPlugin build();
}
}
5 changes: 5 additions & 0 deletions py/server/deephaven/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

"""

# The deephaven package is split across multiple distributions (e.g. deephaven-core and deephaven-plugin); extend the
# search path so subpackages installed elsewhere (such as deephaven.plugin) remain importable when this copy shadows
# site-packages.
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

import importlib.metadata

from deephaven_internal import jvm
Expand Down
38 changes: 38 additions & 0 deletions py/server/deephaven/jcompat.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"io.deephaven.integrations.common.PrimitiveArrayConversionUtility"
)
_JTableDefinition = jpy.get_type("io.deephaven.engine.table.TableDefinition")
_JLinkedHashMap = jpy.get_type("java.util.LinkedHashMap")
_JArrayList = jpy.get_type("java.util.ArrayList")

_DH_PANDAS_NULLABLE_TYPE_MAP: dict[DType, type[ExtensionDtype]] = {
bool_: pd.BooleanDtype,
Expand Down Expand Up @@ -90,6 +92,42 @@ def j_hashmap(d: Optional[dict] = None) -> Optional[jpy.JType]:
return r


JsonValue = Union[
None,
bool,
int,
float,
str,
dict[Any, "JsonValue"],
list["JsonValue"],
tuple["JsonValue", ...],
]
"""A JSON-compatible Python value: a scalar, or a dict/list/tuple of them."""


def j_json_value(value: JsonValue) -> Union[jpy.JType, None, bool, int, float, str]:
"""Recursively converts a JSON-compatible Python value into Java objects.

A dict is converted to a java.util.LinkedHashMap and a list or tuple is converted to a
java.util.ArrayList, with their keys and values converted recursively. Any other value is
unwrapped and left for jpy to convert.

Nested containers must be converted explicitly; otherwise jpy passes them through as opaque
org.jpy.PyObject values.
"""
if isinstance(value, dict):
r = _JLinkedHashMap(len(value))
for k, v in value.items():
r.put(j_json_value(k), j_json_value(v))
return r
if isinstance(value, (list, tuple)):
r = _JArrayList(len(value))
for v in value:
r.add(j_json_value(v))
return r
return unwrap(value)


def j_hashset(s: Optional[Union[set, Sequence]] = None) -> Optional[jpy.JType]:
"""Creates a Java HashSet from a set."""
if s is None:
Expand Down
7 changes: 7 additions & 0 deletions py/server/deephaven_internal/plugin/js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pathlib

import jpy
from deephaven.jcompat import j_json_value
from deephaven.plugin.js import JsPlugin

_JJsPlugin = jpy.get_type("io.deephaven.plugin.js.JsPlugin")
Expand All @@ -26,4 +27,10 @@ def to_j_js_plugin(js_plugin: JsPlugin) -> jpy.JType:
builder.version(js_plugin.version)
builder.main(main_path)
builder.path(j_path)
# "loader" is not required field on the deephaven.plugin.js.JsPlugin interface, so we duck-type check for it
# here to allow plugins to opt-in to providing loader configuration without requiring a new release of the
# deephaven-plugin package.
loader = getattr(js_plugin, "loader", None)
if loader is not None:
builder.loader(j_json_value(loader))
return builder.build()
90 changes: 90 additions & 0 deletions py/server/tests/test_jcompat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending
#

import json
import unittest

import jpy
Expand All @@ -13,11 +14,15 @@
j_collection_to_list,
j_function,
j_hashset,
j_json_value,
j_lambda,
)
from tests.testbase import BaseTestCase

_JSharedContext = jpy.get_type("io.deephaven.engine.table.SharedContext")
_JMap = jpy.get_type("java.util.Map")
_JList = jpy.get_type("java.util.List")
_JObjectMapper = jpy.get_type("com.fasterxml.jackson.databind.ObjectMapper")


class JCompatTestCase(BaseTestCase):
Expand Down Expand Up @@ -56,6 +61,91 @@ def test_j_collection_to_list(self):
j_set = j_hashset(s)
self.assertEqual(s, set(j_collection_to_list(j_set)))

def assert_json_equals(self, j_object, expected) -> None:
"""Serializes a Java object with Jackson and asserts the resulting JSON matches expected."""
actual = json.loads(_JObjectMapper().writeValueAsString(j_object))
self.assertEqual(actual, expected)

def assert_round_trip(self, value) -> None:
"""Asserts that value survives conversion to Java and JSON serialization unchanged."""
self.assert_json_equals(j_json_value(value), value)

def test_j_json_value_basic_values_pass_through(self):
for value in [None, True, False, 0, 42, -7, 0.0, 1.5, -2.25, "", "a string"]:
with self.subTest(value=value):
self.assertIs(j_json_value(value), value)

def test_j_json_value_dict(self):
j_map = j_json_value({"key": "value", "count": 3})
self.assertTrue(_JMap.jclass.isInstance(j_map))
self.assertEqual(j_map.size(), 2)
self.assertEqual(j_map.get("key"), "value")
self.assertEqual(j_map.get("count"), 3)

def test_j_json_value_dict_of_basic_values(self):
self.assert_round_trip(
{
"none": None,
"true": True,
"false": False,
"int": 42,
"negative_int": -7,
"float": 1.5,
"str": "a string",
}
)

def test_j_json_value_nested_dict(self):
j_map = j_json_value({"outer": {"inner": {"deepest": "value"}}})
j_outer = j_map.get("outer")
self.assertTrue(_JMap.jclass.isInstance(j_outer))
j_inner = j_outer.get("inner")
self.assertTrue(_JMap.jclass.isInstance(j_inner))
self.assertEqual(j_inner.get("deepest"), "value")

def test_j_json_value_list(self):
j_list = j_json_value(["a", "b", "c"])
self.assertTrue(_JList.jclass.isInstance(j_list))
self.assertEqual(j_list.size(), 3)
self.assertEqual(j_list.get(0), "a")
self.assertEqual(j_list.get(2), "c")

def test_j_json_value_tuple(self):
j_list = j_json_value(("a", "b"))
self.assertTrue(_JList.jclass.isInstance(j_list))
self.assert_json_equals(j_list, ["a", "b"])

def test_j_json_value_empty_containers(self):
self.assert_round_trip({})
self.assert_round_trip([])
self.assert_round_trip({"empty_map": {}, "empty_list": []})

def test_j_json_value_nested_containers(self):
self.assert_round_trip(
{
"list_of_dicts": [{"a": 1}, {"b": [2, 3]}],
"dict_of_lists": {"x": [[1, 2], []], "y": [{"z": None}]},
"deep": {"a": {"b": {"c": {"d": ["e", {"f": True}]}}}},
}
)

def test_j_json_value_list_of_lists(self):
j_list = j_json_value([[1, 2], [3]])
self.assertTrue(_JList.jclass.isInstance(j_list.get(0)))
self.assert_json_equals(j_list, [[1, 2], [3]])

def test_j_json_value_non_string_keys(self):
# Jackson serializes non-string map keys as strings, which is the same behavior as json.dumps
self.assert_json_equals(
j_json_value({1: "a", 2.5: "b"}), {"1": "a", "2.5": "b"}
)

def test_j_json_value_unwraps_wrapped_objects(self):
j_map = j_json_value(
{"context": AutoCloseable(_JSharedContext.makeSharedContext())}
)
self.assertTrue(_JSharedContext.jclass.isInstance(j_map.get("context")))


if __name__ == "__main__":
unittest.main()
Loading
Loading