diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 92a5e0d..42cac77 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -14,12 +14,3 @@ updates: labels: - "lang: python" - "part: dependencies" - - - package-ecosystem: "npm" - directory: "/js" - schedule: - interval: "monthly" - labels: - - "lang: javascript" - - "part: dependencies" - \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 888f82e..9e05636 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -39,10 +39,6 @@ jobs: with: version: ${{ matrix.python-version }} - - uses: actions-ext/node/setup@main - with: - version: 22.x - - name: Install dependencies run: make develop @@ -64,7 +60,7 @@ jobs: - name: Upload test results uses: actions/upload-artifact@v7 with: - name: test-results-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.node-version }} + name: test-results-${{ matrix.os }}-${{ matrix.python-version }} path: '**/junit.xml' if: ${{ always() }} diff --git a/.gitignore b/.gitignore index e79b750..f72fe76 100644 --- a/.gitignore +++ b/.gitignore @@ -120,17 +120,6 @@ docs/src/_build/ docs/superpowers index.md -# JS -js/coverage -js/dist -js/lib -js/node_modules -js/test-results -js/playwright-report -js/*.tgz -raydar/dashboard/static/* -!raydar/dashboard/static/index.psp2.js - # Jupyter .ipynb_checkpoints .autoversion diff --git a/.vscode/settings.json b/.vscode/settings.json index dab99f9..0967ef4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1 @@ -{ - "eslint.workingDirectories": ["./js"] -} \ No newline at end of file +{} diff --git a/Makefile b/Makefile index 65c59b9..59b518a 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,20 @@ ######### # BUILD # ######### -.PHONY: develop-py develop-js develop -develop-py: +.PHONY: develop +develop: ## setup project for development uv pip install -e .[develop] -develop-js: requirements-js - -develop: develop-js develop-py ## setup project for development - -.PHONY: requirements-py requirements-js requirements -requirements-py: ## install prerequisite python build requirements +.PHONY: requirements +requirements: ## install prerequisite python build requirements python -m pip install --upgrade pip toml python -m pip install `python -c 'import toml; c = toml.load("pyproject.toml"); print("\n".join(c["build-system"]["requires"]))'` python -m pip install `python -c 'import toml; c = toml.load("pyproject.toml"); print(" ".join(c["project"]["optional-dependencies"]["develop"]))'` -requirements-js: ## install prerequisite javascript build requirements - cd js; pnpm install && npx playwright install - -requirements: requirements-js requirements-py ## setup project for development - -.PHONY: build-py build-js build -build-py: +.PHONY: build +build: ## build the project python -m build -w -n -build-js: - cd js; pnpm build - -build: build-js build-py ## build the project - .PHONY: install install: ## install python library uv pip install . @@ -36,36 +22,30 @@ install: ## install python library ######### # LINTS # ######### -.PHONY: lint-py lint-js lint lints +.PHONY: lint-py lint-docs lint lints lint-py: ## run python linter with ruff python -m ruff check raydar python -m ruff format --check raydar -lint-js: ## run js linter - cd js; pnpm lint - lint-docs: ## lint docs with mdformat and codespell python -m mdformat --check README.md docs/wiki/ python -m codespell_lib README.md docs/wiki/ -lint: lint-js lint-py lint-docs ## run project linters +lint: lint-py lint-docs ## run project linters # alias lints: lint -.PHONY: fix-py fix-js fix-docs fix format +.PHONY: fix-py fix-docs fix format fix-py: ## fix python formatting with ruff python -m ruff check --fix raydar python -m ruff format raydar -fix-js: ## fix js formatting - cd js; pnpm fix - fix-docs: ## autoformat docs with mdformat and codespell python -m mdformat README.md docs/wiki/ python -m codespell_lib --write README.md docs/wiki/ -fix: fix-js fix-py fix-docs ## run project autoformatters +fix: fix-py fix-docs ## run project autoformatters # alias format: fix @@ -99,18 +79,9 @@ tests-py: test-py coverage-py: ## run python tests and collect test coverage python -m pytest -v raydar/tests --cov=raydar --cov-report term-missing --cov-report xml -.PHONY: test-js tests-js coverage-js -test-js: ## run js tests - cd js; pnpm test - -# alias -tests-js: test-js - -coverage-js: test-js ## run js tests and collect test coverage - .PHONY: test coverage tests -test: test-py test-js ## run all tests -coverage: coverage-py coverage-js ## run all tests and collect test coverage +test: test-py ## run all tests +coverage: coverage-py ## run all tests and collect test coverage # alias tests: test @@ -135,18 +106,15 @@ major: ## bump a major version ######## # DIST # ######## -.PHONY: dist dist-py dist-js dist-check publish +.PHONY: dist dist-py dist-check publish dist-py: ## build python dists python -m build -w -s -dist-js: # build js dists - cd js; pnpm pack - dist-check: ## run python dist checker with twine python -m twine check dist/* -dist: clean build dist-js dist-py dist-check ## build all dists +dist: clean build dist-py dist-check ## build all dists publish: dist ## publish python assets diff --git a/README.md b/README.md index a8a19eb..c17adfb 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,21 @@ [![License](https://img.shields.io/github/license/Point72/raydar)](https://github.com/Point72/raydar) [![PyPI](https://img.shields.io/pypi/v/raydar.svg)](https://pypi.python.org/pypi/raydar) -A [perspective](https://perspective.finos.org/) powered, user editable ray dashboard via ray serve. +A [perspective](https://perspective.finos.org/) powered, user editable ray dashboard. Ray offers powerful metrics visualizations powered by graphana and prometheus. Although useful, the setup can take time - and customizations can be challenging. Raydar, enables out-of-the-box live cluster metrics and user visualizations for Ray workflows with just a simple pip install. It helps unlock distributed machine learning visualizations on Anyscale clusters, runs live and at scale, is easily customizable, and enables all the in-browser aggregations that [perspective](https://perspective.finos.org/) has to offer. +By default the dashboard runs in **your** process and pulls data over the Ray connection you already have, so the cluster never needs an inbound port. + ![Example](https://media.githubusercontent.com/media/Point72/raydar/refs/heads/main/docs/img/ml_example.gif) ## Features - Convenience wrappers for the tracking and persistence of ray GCS task metadata. Can scale beyond the existing ray dashboard / GCS task tracking limitations. -- Serves a UI through [ray serve](https://docs.ray.io/en/latest/serve/index.html) for the visualization of [perspective](https://github.com/finos/perspective) tables. +- A UI built with [spaday](https://github.com/1kbgz/spaday), [spaday-perspective](https://github.com/1kbgz/spaday-perspective) and [spaday-webawesome](https://github.com/1kbgz/spaday-webawesome) — authored in Python, with UI state synced over [transports](https://github.com/1kbgz/transports). +- Serve it locally (no open port on the cluster) or from [ray serve](https://docs.ray.io/en/latest/serve/index.html). - A python interface to create and update perspective tables from within ray tasks. [More information is available in our wiki](https://github.com/Point72/raydar/wiki) @@ -36,7 +39,8 @@ The raydar module provides an actor which can process collections of ray object ```python from raydar import RayTaskTracker -task_tracker = RayTaskTracker(enable_perspective_dashboard=True) +task_tracker = RayTaskTracker(dashboard="local") +print(task_tracker.dashboard_url) ``` Passing collections of object references to this actor's process method causes those references to be tracked in an internal polars dataframe, as they finish running. @@ -55,7 +59,9 @@ refs = [example_remote_function.remote() for _ in range(100)] task_tracker.process(refs) ``` -The perspective UI is served on port 8000 by default. +The UI is served from this process on a free local port, printed by `task_tracker.dashboard_url`. Pass `dashboard_port=` to pin it. Data reaches the dashboard over Ray's existing connection, so nothing needs to listen on the cluster. + +If your cluster already exposes Ray Serve's HTTP ingress, `dashboard="cluster"` serves the same UI from a Ray Serve deployment instead. ![Example](https://media.githubusercontent.com/media/Point72/raydar/refs/heads/main/docs/img/example_perspective_dashboard.gif) @@ -65,7 +71,7 @@ Passing a `name` and `namespace` arguments allows the RayTaskTracker to skip con from raydar import RayTaskTracker task_tracker = RayTaskTracker( - enable_perspective_dashboard=True, + dashboard="local", name="my_actor_name", namespace="my_actor_namespace" ) @@ -109,13 +115,15 @@ for i in range(100): - _Where is the perspective data stored?_ -Currently, in memory. There are plans to integrate alternatives to this configuration, but currently the data is stored in machine memory on the ray head. +Currently, in memory. With `dashboard="local"` that is the memory of the process that created the `RayTaskTracker`; with `dashboard="cluster"` it is the Ray Serve replica on the ray head. -- _How can I save and restore my perspective layouts?_ +- _Does the cluster need an open port?_ -The `Save Layout` button saves a json file containing layout information. Dragging and dropping this file into the UI browser window restores that layout. +Not with `dashboard="local"`, the default topology. The dashboard binds a port on your own machine and pulls table updates from the tracker actor over Ray, so the browser only ever talks to localhost. `dashboard="cluster"` does need Ray Serve's HTTP ingress to be reachable. + +- _How can I save and restore my perspective layouts?_ -![Example](https://media.githubusercontent.com/media/Point72/raydar/refs/heads/main/docs/img/layout_restoration.gif) +Layouts are Python-side. Pass a perspective-workspace layout to the dashboard and it is restored in every connected tab. ## License diff --git a/docs/wiki/Installation.md b/docs/wiki/Installation.md index d11ae42..99108a8 100644 --- a/docs/wiki/Installation.md +++ b/docs/wiki/Installation.md @@ -1,6 +1,6 @@ ## Pre-requisites -You need Python >=3.10 on your machine to install `raydar`. +You need Python >=3.11 on your machine to install `raydar`. ## Install with `pip` diff --git a/docs/wiki/Key-Features.md b/docs/wiki/Key-Features.md index 1e5b605..7a45f66 100644 --- a/docs/wiki/Key-Features.md +++ b/docs/wiki/Key-Features.md @@ -37,42 +37,41 @@ This internal dataframe can be accessed via the `.get_df()` method. | e0dc174c83... | `null` | 0 | `example_remote_function` | ... | 2024-01-29 07:17:09.343 EST | 2024-01-29 07:17:12.115 EST | `{"/tmp/ray/session_2024-01-29_07...` | `null` | | f4402ec78d... | `null` | 0 | `example_remote_function` | ... | 2024-01-29 07:17:09.343 EST | 2024-01-29 07:17:12.115 EST | `{"/tmp/ray/session_2024-01-29_07...` | `null` | -Additionally, setting the `enable_perspective_dashboard` flag to `True` in the `RayTaskTracker`'s construction serves a perspective dashboard with live views of your completed references. +Additionally, passing `dashboard="local"` to the `RayTaskTracker`'s construction serves a perspective dashboard with live views of your completed references. ```python -task_tracker = RayTaskTracker(enable_perspective_dashboard=True) +task_tracker = RayTaskTracker(dashboard="local") +print(task_tracker.dashboard_url) ``` +The dashboard runs in this process and pulls updates from the tracker actor over Ray, so the cluster needs no inbound port. Use `dashboard="cluster"` to serve it from Ray Serve instead, when the cluster's HTTP ingress is reachable. + ![Example](images/example_perspective_dashboard.gif) ## Create/Store Custom Views -From the developer console, save your workspace layout locally. - -```javascript -let workspace = document.getElementById("perspective-workspace"); - -// Save the current layout -workspace.save().then((config) => { - // Convert the configuration object to a JSON string - let json = JSON.stringify(config); - - // Create a Blob object from the JSON string - let blob = new Blob([json], { type: "application/json" }); +Layouts live in Python. Pass a [perspective-workspace](https://perspective.finos.org/) layout and it is restored in every connected tab: - // Create a download link - let link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = "workspace.json"; +```python +layout = { + "sizes": [1], + "detail": {"main": {"type": "tab-area", "widgets": ["task_tracker_data"], "currentIndex": 0}}, + "master": {"sizes": [], "widgets": []}, + "mode": "globalFilters", + "viewers": { + "task_tracker_data": { + "table": "task_tracker_data", + "plugin": "Datagrid", + "group_by": ["func_or_class_name"], + "columns": ["state"], + } + }, +} - // Append the link to the document body and click it to start the download - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -}); +task_tracker = RayTaskTracker(dashboard="local", dashboard_options={"layout": layout}) ``` -Then, move this json file to `js/src/layouts/default.json`. +`dashboard_options` also accepts `title` and `limit` (a per-table row cap). Without a layout override, raydar generates one datagrid tab per table. ![Example](images/example_perspective_dashboard_layouts.gif) @@ -111,13 +110,11 @@ Specifically, tracked fields include: ## Custom Sources / Update Logic -The proxy server helpd by the `RayTaskTracker` is exposed via the `.proxy_server()` property, meaning we can create new tables as follows: +The `RayTaskTracker` can create and update arbitrary tables: ```python -task_tracker = RayTaskTracker(enable_perspective_dashboard=True) -proxy_server = task_tracker.proxy_server() -proxy_server.remote( - "new", +task_tracker = RayTaskTracker(dashboard="local") +task_tracker.create_table( "metrics_table", { "node_id": "string", @@ -133,18 +130,17 @@ proxy_server.remote( If a user were to then update this table with data coming from, for example, a pytorch model training loop with metrics: ```python -def my_model_training_loop() - - for epoch in range(num_epochs): +def my_model_training_loop(): + for epoch in range(num_epochs): # ... my training code here ... - data = dict( - node_id=ray.get_runtime_context().get_node_id(), - metric_name="loss", - value=loss.item(), - timestamp=time.time(), - ) - proxy_server.remote("update", "metrics_table", [data]) + data = dict( + node_id=ray.get_runtime_context().get_node_id(), + metric_name="loss", + value=loss.item(), + timestamp=datetime.datetime.now(), + ) + task_tracker.update_table("metrics_table", [data]) ``` Then they can expose a live view at per-node loss metrics across our model training process: diff --git a/docs/wiki/contribute/Build-from-Source.md b/docs/wiki/contribute/Build-from-Source.md index 1986de3..a70b339 100644 --- a/docs/wiki/contribute/Build-from-Source.md +++ b/docs/wiki/contribute/Build-from-Source.md @@ -1,4 +1,4 @@ -`raydar` is written in Python and JavaScript. While prebuilt wheels are provided for end users, it is also straightforward to build `raydar` from either the Python [source distribution](https://packaging.python.org/en/latest/specifications/source-distribution-format/) or the GitHub repository. +`raydar` is written in Python. While prebuilt wheels are provided for end users, it is also straightforward to build `raydar` from either the Python [source distribution](https://packaging.python.org/en/latest/specifications/source-distribution-format/) or the GitHub repository. - [Make commands](#make-commands) - [Prerequisites](#prerequisites) @@ -36,14 +36,6 @@ git clone https://github.com/Point72/raydar.git cd raydar ``` -## Install NodeJS - -Follow the instructions for [installing NodeJS](https://nodejs.org/en/download/package-manager/all) for your system. Once installed, you can [install `pnpm`](https://pnpm.io/installation) with: - -```bash -npm install --global pnpm -``` - ## Install Python dependencies Python build and develop dependencies are specified in the `pyproject.toml`, but you can manually install them: @@ -66,13 +58,12 @@ make build `raydar` has linting and auto formatting. -| Language | Linter | Autoformatter | Description | -| :--------- | :---------- | :------------ | :---------- | -| Python | `ruff` | `ruff` | Style | -| Python | `ruff` | `ruff` | Imports | -| JavaScript | `prettier` | `prettier` | Style | -| Markdown | `mdformat` | `mdformat` | Style | -| Markdown | `codespell` | | Spelling | +| Language | Linter | Autoformatter | Description | +| :------- | :---------- | :------------ | :---------- | +| Python | `ruff` | `ruff` | Style | +| Python | `ruff` | `ruff` | Imports | +| Markdown | `mdformat` | `mdformat` | Style | +| Markdown | `codespell` | | Spelling | **Python Linting** @@ -86,18 +77,6 @@ make lint-py make fix-py ``` -**JavaScript Linting** - -```bash -make lint-js -``` - -**JavaScript Autoformatting** - -```bash -make fix-js -``` - **Documentation Linting** ```bash @@ -123,9 +102,3 @@ make develop ```bash make test-py ``` - -**JavaScript** - -```bash -make test-js -``` diff --git a/js/build.mjs b/js/build.mjs deleted file mode 100644 index 8c687b9..0000000 --- a/js/build.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { build } from "./tools/build.js"; -import { compile } from "./tools/css.js"; -import fs from "fs"; -import cpy from "cpy"; - -const BUILD = [ - { - define: { - global: "window", - }, - entryPoints: ["src/index.js"], - bundle: true, - plugins: [], - format: "esm", - loader: { - ".css": "text", - ".html": "text", - ".jsx": "jsx", - ".png": "file", - ".ttf": "file", - ".wasm": "file", - }, - outfile: "./dist/index.js", - publicPath: "/static/", - }, -]; - -async function build_all() { - /* make directories */ - fs.mkdirSync("../raydar/dashboard/static/", { recursive: true }); - - /* Compile JS */ - await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); - // await cp_to_paths("./src/style/*.css"); - await cpy("./src/*.html", "./dist", { flat: true }); - await cpy("./src/layouts/*", "./dist/layouts", { flat: true }); - await cpy( - "./node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm", - "./dist", - { flat: true }, - ); - await cpy( - "./node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm", - "./dist", - { flat: true }, - ); - - /* Compile css */ - await compile(); - - /* Copy to raydar static */ - await cpy("./dist/*", "../raydar/dashboard/static/", { - flat: true, - recursive: true, - }); - await cpy("./dist/layouts/*", "../raydar/dashboard/static/layouts/", { - flat: true, - recursive: true, - }); -} - -build_all(); diff --git a/js/package.json b/js/package.json deleted file mode 100644 index 56f8c8e..0000000 --- a/js/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "raydar", - "version": "0.3.0", - "description": "A perspective powered, user editable ray dashboard via ray serve", - "repository": "git@github.com:Point72/raydar.git", - "author": "Point72, L.P. ", - "license": "Apache-2.0", - "private": true, - "type": "module", - "unpkg": "dist/cdn/index.js", - "jsdelivr": "dist/cdn/index.js", - "exports": { - ".": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "./dist/*": "./dist/*", - "./package.json": "./package.json" - }, - "files": [ - "dist/**/*", - "index.d.ts" - ], - "types": "./dist/esm/index.d.ts", - "publishConfig": { - "access": "public" - }, - "scripts": { - "build:debug": "node build.mjs --debug", - "build:esbuild": "node build.mjs", - "build": "npm-run-all -s build:*", - "clean": "rm -rf dist playwright-report ../raydar/dashboard/static", - "dev": "npm-run-all -p start watch", - "lint": "prettier --check \"src/**/*.{js,ts,jsx,tsx,css}\" \"*.mjs\" \"*.json\"", - "fix": "prettier --write \"src/**/*.{js,ts,jsx,tsx,css}\" \"*.mjs\" \"*.json\"", - "preinstall": "npx only-allow pnpm", - "prepack": "npm run build", - "test": ":", - "watch:esbuild": "pnpm run build:esbuild --watch", - "watch": "npm-run-all -p watch:*" - }, - "dependencies": { - "@perspective-dev/client": "^4.3.0", - "@perspective-dev/server": "^4.3.0", - "@perspective-dev/viewer": "^4.3.0", - "@perspective-dev/viewer-d3fc": "^4.3.0", - "@perspective-dev/viewer-datagrid": "^4.3.0", - "@perspective-dev/workspace": "^4.3.0" - }, - "devDependencies": { - "cpy": "^13.2.1", - "esbuild": "^0.28.1", - "lightningcss": "^1.33.0", - "mkdirp": "^3.0.1", - "npm-run-all": "^4.1.5", - "prettier": "^3.8.3" - } -} diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml deleted file mode 100644 index 18b0cef..0000000 --- a/js/pnpm-lock.yaml +++ /dev/null @@ -1,3141 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@perspective-dev/client': - specifier: ^4.3.0 - version: 4.3.0 - '@perspective-dev/server': - specifier: ^4.3.0 - version: 4.3.0 - '@perspective-dev/viewer': - specifier: ^4.3.0 - version: 4.3.0 - '@perspective-dev/viewer-d3fc': - specifier: ^4.3.0 - version: 4.3.0(d3-brush@3.0.0)(d3-dispatch@3.0.1)(d3-fetch@3.0.1)(d3-path@3.1.0)(d3-random@3.0.1)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-shape@3.2.0)(d3-time@3.1.0)(d3-zoom@3.0.0) - '@perspective-dev/viewer-datagrid': - specifier: ^4.3.0 - version: 4.3.0 - '@perspective-dev/workspace': - specifier: ^4.3.0 - version: 4.3.0 - devDependencies: - cpy: - specifier: ^13.2.1 - version: 13.2.1 - esbuild: - specifier: ^0.28.1 - version: 0.28.1 - lightningcss: - specifier: ^1.33.0 - version: 1.33.0 - mkdirp: - specifier: ^3.0.1 - version: 3.0.1 - npm-run-all: - specifier: ^4.1.5 - version: 4.1.5 - prettier: - specifier: ^3.8.3 - version: 3.8.3 - -packages: - - '@d3fc/d3fc-annotation@3.0.16': - resolution: {integrity: sha512-4tA7RHLUbj/i3KqWRqCVDOHi435qBRXbAFrAajVYM2juNzFdX5RCoF3opyXKOFncs339caxgeySGStTVfulO9w==} - peerDependencies: - d3-scale: '*' - d3-selection: '*' - - '@d3fc/d3fc-axis@3.0.7': - resolution: {integrity: sha512-S4pILxkQUkD7WQmimWxIEHfXUYEonlXWuWvMP6iq3KXL3d+cj4flvwNqLYZvSVaQDdK990cKkjrK/ZAKnReGlg==} - peerDependencies: - d3-scale: '*' - d3-selection: '*' - d3-shape: '*' - - '@d3fc/d3fc-brush@3.0.3': - resolution: {integrity: sha512-fc1XBuNWl6DQVFSnBdauI/WNRvtNaeUWwDiLEIMY+v0VlrmGOgWEGCB5otVepGZiL56cjgRtHdwYBdKCvgGBSg==} - peerDependencies: - d3-brush: '*' - d3-dispatch: '*' - d3-scale: '*' - d3-selection: '*' - - '@d3fc/d3fc-chart@5.1.9': - resolution: {integrity: sha512-xFO9lDUi2wAkjfyWlVZuFwSWEgnk0WOEagR2R9KzQ+uxeg3oZOPzwE+AZbpALyQbjjQ2uIxcOk56xnmnnayk3w==} - peerDependencies: - d3-scale: '*' - d3-selection: '*' - - '@d3fc/d3fc-data-join@6.0.3': - resolution: {integrity: sha512-fd1D2Cl4YGjzl3gBhcrvTl/VxaSncY0ZcokWsN8ahtmk9DZK4DnAgHGrdecnXVLkOx+ANDcqxqscYz6MWXLbcA==} - peerDependencies: - d3-selection: '*' - - '@d3fc/d3fc-discontinuous-scale@4.1.1': - resolution: {integrity: sha512-cyhtq4XPtK8RCSBtzctRAl4RkDyYJqJY0SJpi3QcfJz/OX9iB6At7UjITO7tAmJ7RUHb/xZvAVpJU8BM5gaQqg==} - peerDependencies: - d3-scale: '*' - d3-time: '*' - - '@d3fc/d3fc-element@6.2.0': - resolution: {integrity: sha512-AvdZ3V4mVxF9dGYLiDCoqr3GhrFOUQEc1FcP20QEhQ3fJ3qYRwx7/uhL7G/L2xbe6k4delPgnLOvtoaDenhpZw==} - - '@d3fc/d3fc-extent@4.0.2': - resolution: {integrity: sha512-m7w7Dof6KAIDtgzIsTcprWTEoiqExJGsGoQbb97bF+EwIkuEZWRUl1jkoeNL00efpX1o6zSdqSr6lojoR0aI/g==} - peerDependencies: - d3-array: '*' - - '@d3fc/d3fc-financial-feed@7.1.0': - resolution: {integrity: sha512-K8jktdRJQAiJepglErsuY2ZMKsm0YFWTeuhYnTFb8rWmyhwoPeem9QW+e6xBTiAvbElJm4yTrkal09KmO2cLlQ==} - peerDependencies: - d3-fetch: '*' - - '@d3fc/d3fc-group@3.0.1': - resolution: {integrity: sha512-GBUR6a4hkqfSo77iaFS4qPMS5tupH8hmJ8eniiD45GFmWQs69+Dlf8Uhx+GCCvQkE+px6rQyZWVCJKBq6gkz2Q==} - - '@d3fc/d3fc-label-layout@7.0.4': - resolution: {integrity: sha512-4CCyOx6uQA/Eq1VHnNy9dpI5QE2sDd1EP4W0Nw2rwnhFtlQqv37V38b9y5zT4YcdFrEgJqSB81fXIt3ZK1yxSg==} - peerDependencies: - d3-array: '*' - d3-scale: '*' - d3-selection: '*' - - '@d3fc/d3fc-pointer@3.0.3': - resolution: {integrity: sha512-hXY7LqliDEJBH/do4YZusdLoikLYlWoN7efPC7YKYJ8igoEQFa48BqEHcANLs1qH+r7vzL2SS8V39MzgnJ1yQA==} - peerDependencies: - d3-dispatch: '*' - d3-selection: '*' - - '@d3fc/d3fc-random-data@4.0.2': - resolution: {integrity: sha512-T7+PbG1n23jyVMOWIuHJjY12PhPcYeRShuF0MK0ohifcNmwslurFMQKLBWkZk4x19In6JX7lNR0lwsaUUrcZNA==} - peerDependencies: - d3-random: '*' - d3-time: '*' - - '@d3fc/d3fc-rebind@6.0.1': - resolution: {integrity: sha512-+ryBZ53ALMffbADwnFAtTYQJcT7PE5BwpducGYS0X6Jux6ESnp+fP+cDQvBGbDBOVqaziGnfeLeJXjtMnZujmQ==} - - '@d3fc/d3fc-sample@5.0.2': - resolution: {integrity: sha512-+ZJu+TOL4MFzFvAYc+QqDKude9DS7x4uK+133vCknAlSxL/l8Sh6ecWFXTSaC8tqHGywT7SK1arf0DiYCyS3Nw==} - peerDependencies: - d3-array: '*' - - '@d3fc/d3fc-series@6.1.3': - resolution: {integrity: sha512-OSbt60SohTIib1xihX9ufneyJY7s9Feg9hvXVyEBZEBkwNE1NaeesZJ4nkmslu39RWuwsvpK3apL/XuBw7i5WA==} - peerDependencies: - d3-array: '*' - d3-scale: '*' - d3-scale-chromatic: '*' - d3-selection: '*' - d3-shape: '*' - - '@d3fc/d3fc-shape@6.0.1': - resolution: {integrity: sha512-/dD3S8BWrOjO2mSptUmwe38V7KG4Kw6liIE5NXZJjX/XidfZhuDu7WWuya3i90HeNYDZNcs6Z+4qM3FnvlZf8g==} - peerDependencies: - d3-path: '*' - - '@d3fc/d3fc-technical-indicator@8.1.1': - resolution: {integrity: sha512-ci5q+/4jCbbnT9M/JnrsBoHJfNJI4HFeCGVT+sy221OTdaFnrLj+IELFKFUS2h0uEOSu+CYT3D1K0BcAnMooxA==} - peerDependencies: - d3-array: '*' - - '@d3fc/d3fc-webgl@3.2.1': - resolution: {integrity: sha512-yNYHW/tC05rrJs5fpVM5zdmInL8NH6UP09ZJ2tmKw62ELYHWySV8vuXOVZN3V/3WihPa60p2w23H0Hrp2b5qeg==} - peerDependencies: - d3-scale: '*' - d3-shape: '*' - - '@d3fc/d3fc-zoom@1.2.0': - resolution: {integrity: sha512-NfY6gPfcatw1gUtZoWq17SA7LV00t7Rl3eq6Bgj17++7K1H77Gpsb1hFa3XhzF85RrODuUzQW33/IIMYPgim9A==} - peerDependencies: - d3-dispatch: '*' - d3-selection: '*' - d3-zoom: '*' - - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@lumino/algorithm@2.0.4': - resolution: {integrity: sha512-gddBhESPqu25KWLeAK9Kz8tS9Ph7P45i0CNG7Ia4XMhK9PHLtTsBdJTC9jP+MqhbzC8zDT/4ekvYRV9ojRPj7Q==} - - '@lumino/collections@2.0.4': - resolution: {integrity: sha512-D/Py9L5HET6+XUYGxFqDEEth4B65X2c7B/GQVRR8q5Fl7EArVL6e98ZXw8BMkuPcTNa0zlENpCKXzlcoJZxXgQ==} - - '@lumino/commands@2.3.3': - resolution: {integrity: sha512-7Ci0QdFzt4NKFMhULr19sJPpOLHJw/oYlq6Pb0/Kq1s05+cIoLimr5wiyjkbAlNoGO/8A8SEBGHy3uctZz6G3A==} - - '@lumino/coreutils@2.2.2': - resolution: {integrity: sha512-zaKJaK7rawPATn2BGHkbMrR6oK3s9PxNe9KreLwWF2dB4ZBHDiEmNLRyHRorfJ7XqVOEXAsAAj0jFn+qJPC/4Q==} - - '@lumino/disposable@2.1.5': - resolution: {integrity: sha512-hO9AkJK0oEGzxopuxI8LaZqwzSNwXJTGCdr5K4gh6al+zxpN7rOCh6Aq3zDxkIHJU4zybxv8r02ardx9XJsG3A==} - - '@lumino/domutils@2.0.4': - resolution: {integrity: sha512-naYGUQn3e0CLtz/tjKOZP8SOBg0SW7EguhkxLpNUXlVUvx7rVsfr0VI22FVL+jgI0FbxXpEkxpSMxtK73jxJAg==} - - '@lumino/dragdrop@2.1.8': - resolution: {integrity: sha512-5sBYkTka598+XsgjY2tWOC+WYCh9NEgx8RhLvQ3x+V182YhcpEXw38RWGQZyNpQ4m4vtQWKv42A26q+ae6sMwg==} - - '@lumino/keyboard@2.0.4': - resolution: {integrity: sha512-kIVkdSz8F5wtZr8hZp0CMX+E0eMCOnFH6XCT7j2UBQ80ERJHFy0eX+IbNo3dtRQ7+CcDhBV4hQquFNFa+/04QQ==} - - '@lumino/messaging@2.0.4': - resolution: {integrity: sha512-NbZnchAPOciSe9Qn/g6EzG0LRaw7bygFIXbCD440ZhzvugdBeAerwYhrA795jkXPNrrl3olp5AlO0cBB/XZNtg==} - - '@lumino/properties@2.0.4': - resolution: {integrity: sha512-XsL2qLZk+1FbfuTrkyjciI8PMDw3YcaBkqVQ+iv7OOJf9bUlrmTpCMY0Hu5d3hV2W3TWlRsdbvRRLEBJSKv0iA==} - - '@lumino/signaling@2.1.5': - resolution: {integrity: sha512-Wkx6WR45ynmKBlW0GBEoh4xk9+QluKr1JHuMftqcStBHSQBCnN54UKRRDbySXHGRhhx6p4neu7sGomgQSlQK8w==} - - '@lumino/virtualdom@2.0.4': - resolution: {integrity: sha512-7MFthA9KUsqZTGm/D98FZt1QupjIGyd3XyB4SIugn6DQAqhjBiyykCZydnRq3qmuMHybQel33dNIbHpzyNyQwA==} - - '@lumino/widgets@2.7.5': - resolution: {integrity: sha512-i11PlbTsZYIvC/uhcC4FeeLnu/7vveG8WzXFbxPunjT1yGjleqQIPlpMOAJ5d4PwCKqeM8LYttYke6ZOXvXDLA==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@perspective-dev/client@4.3.0': - resolution: {integrity: sha512-LhVDyYzZrSmtuU04qX+MzpnA1BKPEW9nMi4RPE6y3KiGAWwFFqEFgZKF233U8/Fx3v2URCijcwmXn0qTVfoPMQ==} - - '@perspective-dev/server@4.3.0': - resolution: {integrity: sha512-UH3ADscynozVx42RF07DTBmPE/0PUwH+SS0cgvMLuLfBjtvnPm6msfOU0tHlgFnNZOHJO0q6RZ9WI5fD6wF07A==} - - '@perspective-dev/viewer-d3fc@4.3.0': - resolution: {integrity: sha512-Su0pcno2RLnzotnMGLBSlLsCL3qE2BSTtso6KcUXFNO+Cbx2jzTo+5hDk+kcZtDYbMpZk/a7p+NoUhltL5qOGA==} - - '@perspective-dev/viewer-datagrid@4.3.0': - resolution: {integrity: sha512-edzgMCzhXcB+gfh7SoPnjVjzQxd5UB7O4RUZq+fB9dgSqh7KkdwTGz4XxHLr8VHhDB9cb8mbTmaVT4Mk32b9AQ==} - - '@perspective-dev/viewer@4.3.0': - resolution: {integrity: sha512-zF2xRqk4DfemMJ0Y1CCFFQKSk6L+v/hcQCPs0jVUs3cwS73bOVRnz1Hl9V3rwpfXSviILS+66d2HIlllvyd2vg==} - - '@perspective-dev/workspace@4.3.0': - resolution: {integrity: sha512-sEdeyw0jcPj6l7hCdckDm4KfCP8PuoomLG0/FrMQASKYscMq+Z1RDjo2T+5YJWOezEFCjpJy284YHnr4Vq2guw==} - - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - - '@types/d3-selection@1.0.10': - resolution: {integrity: sha512-mHICSFHpIwgTycsvgINYCwItk039eofbGRzVNdeUUtv0S2BD1vXFFUKaeMJN3ARbVl+hlsVOIwdzhzub5tjr6Q==} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - basic-auth@2.0.1: - resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} - engines: {node: '>= 0.8'} - - bn.js@5.2.3: - resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} - - brace-expansion@1.1.13: - resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - buffer-pipe@0.0.3: - resolution: {integrity: sha512-GlxfuD/NrKvCNs0Ut+7b1IHjylfdegMBxQIlZHj7bObKVQBxB5S84gtm2yu1mQ8/sSggceWBDPY0cPXgvX2MuA==} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chroma-js@3.2.0: - resolution: {integrity: sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - copy-file@11.1.0: - resolution: {integrity: sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==} - engines: {node: '>=18'} - - corser@2.0.1: - resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} - engines: {node: '>= 0.4.0'} - - cpy@13.2.1: - resolution: {integrity: sha512-/H2B3WW9gccZJKjKoDZsIrDU3MkkHlxgheT82hUbInC5fEdi4+54zyYpFueZT9pLfr5ObrtgN4MsYYrmTmHzeg==} - engines: {node: '>=20'} - - cross-spawn@6.0.6: - resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} - engines: {node: '>=4.8'} - - d3-array@1.0.1: - resolution: {integrity: sha512-VPS5OH5Xb43tkFkxHEc4r5yWhlDwST47zh1q+qvgTj7xB9xDXn+UEcofhvNC7s8gD55y9Q/MCSPSBUVvnzo3Dw==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - - d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} - - d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - - d3-collection@1.0.7: - resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} - - d3-color@1.4.1: - resolution: {integrity: sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} - - d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} - - d3-dispatch@1.0.1: - resolution: {integrity: sha512-BRTp95mobTSKx8EtpOLbxXuYVtNNr0PmelkH9Uzg5cgcO5O1M0i3+2C0FeM2I95BwQoIlsuZXQTPIoIt5xOtmw==} - - d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - - d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - - d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true - - d3-ease@1.0.7: - resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==} - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} - - d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} - - d3-format@1.0.2: - resolution: {integrity: sha512-VHFdLLjGkeGrRL8T/rlIIDhI3vvVX/oOTM/GaDJfB1sIb4dU5ZgiEjg3EeidJdQ/70u60tM015TSWa1gqqLRhg==} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-geo@3.1.1: - resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} - engines: {node: '>=12'} - - d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - - d3-interpolate@1.4.0: - resolution: {integrity: sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} - - d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - - d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} - - d3-scale-chromatic@3.1.0: - resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} - engines: {node: '>=12'} - - d3-scale@1.0.3: - resolution: {integrity: sha512-ah2Xqywu96gau2iET3T0ZTsu0/X0gfoB8vDTuZ1OaG5F0SgGJLXreBVBknSZf2HKnxjenRvFok3qY2FgY4RpFg==} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-selection@1.0.2: - resolution: {integrity: sha512-nInNdsdhljkDqkU/83bdWwtiJ7xsX3l57YZMlqsAOMeQROeCv7osPqQgYnao0NmRZEGc11hNakY+EOkaIdsWpQ==} - - d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-svg-legend@2.25.6: - resolution: {integrity: sha512-6dueSjQr3+g9SlQ1SOzc4V58cCjjBeyo4WEcY8PW80i9XD/s562W/4xk05bpky0vzQx+i2XmXj3CYT+9KIRlnw==} - - d3-time-format@2.3.0: - resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@1.1.0: - resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@1.0.10: - resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - - d3-transition@1.0.3: - resolution: {integrity: sha512-Facxcbma0nA2GVrx7B/Mgnn5ju6SwUMzGa9YcYmQjpqmaIq1Zbp5vVJLjtH6b08Lu0vcX7O6a4z+AlLmdCxrCQ==} - - d3-transition@3.0.1: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 - - d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} - - d3@7.9.0: - resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} - engines: {node: '>=12'} - - d3fc@15.2.13: - resolution: {integrity: sha512-wfBvY9TcoeV/bRxPjawlzDHS/r2y03STspyZc9ydiz4ANMt+y+ynL9oahTZ4aAbp5JK850VxWNkQ8jaaN/cDYg==} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - delaunator@5.1.0: - resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@16.1.1: - resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} - engines: {node: '>=20'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gradient-parser@1.2.0: - resolution: {integrity: sha512-6ABGa9CR7WR/0pAJicBy5SJkiikbFM6kf/JjykwX7x+t+s8ORWVnlbi6FkHeFFb36yWsjUpHqSYrygd7ofEUqA==} - engines: {node: '>=0.10.0'} - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - - html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} - - http-proxy@1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - - http-server@14.1.1: - resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} - engines: {node: '>=12'} - hasBin: true - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-path-inside@4.0.0: - resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} - engines: {node: '>=12'} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - - junk@4.0.1: - resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} - engines: {node: '>=12.20'} - - leb128@0.0.5: - resolution: {integrity: sha512-elbNtfmu3GndZbesVF6+iQAfVjOXW9bM/aax9WwMlABZW+oK9sbAZEXoewaPHmL34sxa8kVwWsru8cNE/yn2gg==} - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - load-json-file@4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} - - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - memorystream@0.3.1: - resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} - engines: {node: '>= 0.10.0'} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - - normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - - npm-run-all@4.1.5: - resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} - engines: {node: '>= 4'} - hasBin: true - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - opener@1.5.2: - resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} - hasBin: true - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-event@6.0.1: - resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} - engines: {node: '>=16.17'} - - p-filter@4.1.0: - resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} - engines: {node: '>=18'} - - p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} - engines: {node: '>=18'} - - p-timeout@6.1.4: - resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} - engines: {node: '>=14.16'} - - parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - - path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-type@3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} - - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - pidtree@0.3.1: - resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} - engines: {node: '>=0.10'} - hasBin: true - - pify@3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - - portfinder@1.0.38: - resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} - engines: {node: '>= 10.12'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} - engines: {node: '>=14'} - hasBin: true - - pro_self_extracting_wasm@0.0.9: - resolution: {integrity: sha512-95/dZfLmlGc/6Xp7gqvRBgXF8M+osw/Xtalz1U/Va8MpSC1TiR7rM4lEvAs1p/q4v/EZk6bow3tKEclbMGsSFQ==} - hasBin: true - - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} - engines: {node: '>=0.6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - read-pkg@3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regular-table@0.8.3: - resolution: {integrity: sha512-GANAV656dyTza89S1pz1NwQdcIZv+uUyV5z9k2mPnVfQxWVY150Ft75p35El0ICGpKvUG9mz6BP5vq4+d9BwUg==} - engines: {node: '>=16'} - - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - robust-predicates@3.0.3: - resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - secure-compare@3.0.1: - resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} - engines: {node: '>= 0.4'} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.22: - resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} - - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - stoppable@1.1.0: - resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} - engines: {node: '>=4', npm: '>=6'} - - string.prototype.padend@3.1.6: - resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} - engines: {node: '>= 0.4'} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} - engines: {node: '>=20'} - - union@0.5.0: - resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} - engines: {node: '>= 0.8.0'} - - url-join@4.0.1: - resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - - whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - zx@8.8.5: - resolution: {integrity: sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==} - engines: {node: '>= 12.17.0'} - hasBin: true - -snapshots: - - '@d3fc/d3fc-annotation@3.0.16(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0)': - dependencies: - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-rebind': 6.0.1 - '@d3fc/d3fc-series': 6.1.3(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-shape': 6.0.1(d3-path@3.1.0) - d3-scale: 4.0.2 - d3-selection: 3.0.0 - transitivePeerDependencies: - - d3-array - - d3-path - - d3-scale-chromatic - - d3-shape - - '@d3fc/d3fc-axis@3.0.7(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0)': - dependencies: - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-rebind': 6.0.1 - d3-scale: 4.0.2 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - - '@d3fc/d3fc-brush@3.0.3(d3-brush@3.0.0)(d3-dispatch@3.0.1)(d3-scale@4.0.2)(d3-selection@3.0.0)': - dependencies: - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-rebind': 6.0.1 - d3-brush: 3.0.0 - d3-dispatch: 3.0.1 - d3-scale: 4.0.2 - d3-selection: 3.0.0 - - '@d3fc/d3fc-chart@5.1.9(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0)': - dependencies: - '@d3fc/d3fc-axis': 3.0.7(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-element': 6.2.0 - '@d3fc/d3fc-rebind': 6.0.1 - '@d3fc/d3fc-series': 6.1.3(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - d3-scale: 4.0.2 - d3-selection: 3.0.0 - transitivePeerDependencies: - - d3-array - - d3-path - - d3-scale-chromatic - - d3-shape - - '@d3fc/d3fc-data-join@6.0.3(d3-selection@3.0.0)': - dependencies: - d3-selection: 3.0.0 - - '@d3fc/d3fc-discontinuous-scale@4.1.1(d3-scale@4.0.2)(d3-time@3.1.0)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-scale: 4.0.2 - d3-time: 3.1.0 - - '@d3fc/d3fc-element@6.2.0': {} - - '@d3fc/d3fc-extent@4.0.2(d3-array@3.2.4)': - dependencies: - d3-array: 3.2.4 - - '@d3fc/d3fc-financial-feed@7.1.0(d3-fetch@3.0.1)': - dependencies: - d3-fetch: 3.0.1 - - '@d3fc/d3fc-group@3.0.1': {} - - '@d3fc/d3fc-label-layout@7.0.4(d3-array@3.2.4)(d3-scale@4.0.2)(d3-selection@3.0.0)': - dependencies: - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-rebind': 6.0.1 - d3-array: 3.2.4 - d3-scale: 4.0.2 - d3-selection: 3.0.0 - - '@d3fc/d3fc-pointer@3.0.3(d3-dispatch@3.0.1)(d3-selection@3.0.0)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - - '@d3fc/d3fc-random-data@4.0.2(d3-random@3.0.1)(d3-time@3.1.0)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-random: 3.0.1 - d3-time: 3.1.0 - - '@d3fc/d3fc-rebind@6.0.1': {} - - '@d3fc/d3fc-sample@5.0.2(d3-array@3.2.4)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-array: 3.2.4 - - '@d3fc/d3fc-series@6.1.3(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0)': - dependencies: - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-rebind': 6.0.1 - '@d3fc/d3fc-shape': 6.0.1(d3-path@3.1.0) - '@d3fc/d3fc-webgl': 3.2.1(d3-scale@4.0.2)(d3-shape@3.2.0) - d3-array: 3.2.4 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - transitivePeerDependencies: - - d3-path - - '@d3fc/d3fc-shape@6.0.1(d3-path@3.1.0)': - dependencies: - d3-path: 3.1.0 - - '@d3fc/d3fc-technical-indicator@8.1.1(d3-array@3.2.4)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-array: 3.2.4 - - '@d3fc/d3fc-webgl@3.2.1(d3-scale@4.0.2)(d3-shape@3.2.0)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - - '@d3fc/d3fc-zoom@1.2.0(d3-dispatch@3.0.1)(d3-selection@3.0.0)(d3-zoom@3.0.0)': - dependencies: - '@d3fc/d3fc-rebind': 6.0.1 - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - - '@esbuild/aix-ppc64@0.28.1': - optional: true - - '@esbuild/android-arm64@0.28.1': - optional: true - - '@esbuild/android-arm@0.28.1': - optional: true - - '@esbuild/android-x64@0.28.1': - optional: true - - '@esbuild/darwin-arm64@0.28.1': - optional: true - - '@esbuild/darwin-x64@0.28.1': - optional: true - - '@esbuild/freebsd-arm64@0.28.1': - optional: true - - '@esbuild/freebsd-x64@0.28.1': - optional: true - - '@esbuild/linux-arm64@0.28.1': - optional: true - - '@esbuild/linux-arm@0.28.1': - optional: true - - '@esbuild/linux-ia32@0.28.1': - optional: true - - '@esbuild/linux-loong64@0.28.1': - optional: true - - '@esbuild/linux-mips64el@0.28.1': - optional: true - - '@esbuild/linux-ppc64@0.28.1': - optional: true - - '@esbuild/linux-riscv64@0.28.1': - optional: true - - '@esbuild/linux-s390x@0.28.1': - optional: true - - '@esbuild/linux-x64@0.28.1': - optional: true - - '@esbuild/netbsd-arm64@0.28.1': - optional: true - - '@esbuild/netbsd-x64@0.28.1': - optional: true - - '@esbuild/openbsd-arm64@0.28.1': - optional: true - - '@esbuild/openbsd-x64@0.28.1': - optional: true - - '@esbuild/openharmony-arm64@0.28.1': - optional: true - - '@esbuild/sunos-x64@0.28.1': - optional: true - - '@esbuild/win32-arm64@0.28.1': - optional: true - - '@esbuild/win32-ia32@0.28.1': - optional: true - - '@esbuild/win32-x64@0.28.1': - optional: true - - '@lumino/algorithm@2.0.4': {} - - '@lumino/collections@2.0.4': - dependencies: - '@lumino/algorithm': 2.0.4 - - '@lumino/commands@2.3.3': - dependencies: - '@lumino/algorithm': 2.0.4 - '@lumino/coreutils': 2.2.2 - '@lumino/disposable': 2.1.5 - '@lumino/domutils': 2.0.4 - '@lumino/keyboard': 2.0.4 - '@lumino/signaling': 2.1.5 - '@lumino/virtualdom': 2.0.4 - - '@lumino/coreutils@2.2.2': - dependencies: - '@lumino/algorithm': 2.0.4 - - '@lumino/disposable@2.1.5': - dependencies: - '@lumino/signaling': 2.1.5 - - '@lumino/domutils@2.0.4': {} - - '@lumino/dragdrop@2.1.8': - dependencies: - '@lumino/coreutils': 2.2.2 - '@lumino/disposable': 2.1.5 - - '@lumino/keyboard@2.0.4': {} - - '@lumino/messaging@2.0.4': - dependencies: - '@lumino/algorithm': 2.0.4 - '@lumino/collections': 2.0.4 - - '@lumino/properties@2.0.4': {} - - '@lumino/signaling@2.1.5': - dependencies: - '@lumino/algorithm': 2.0.4 - '@lumino/coreutils': 2.2.2 - - '@lumino/virtualdom@2.0.4': - dependencies: - '@lumino/algorithm': 2.0.4 - - '@lumino/widgets@2.7.5': - dependencies: - '@lumino/algorithm': 2.0.4 - '@lumino/commands': 2.3.3 - '@lumino/coreutils': 2.2.2 - '@lumino/disposable': 2.1.5 - '@lumino/domutils': 2.0.4 - '@lumino/dragdrop': 2.1.8 - '@lumino/keyboard': 2.0.4 - '@lumino/messaging': 2.0.4 - '@lumino/properties': 2.0.4 - '@lumino/signaling': 2.1.5 - '@lumino/virtualdom': 2.0.4 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@perspective-dev/client@4.3.0': - dependencies: - '@perspective-dev/server': 4.3.0 - pro_self_extracting_wasm: 0.0.9 - stoppable: 1.1.0 - ws: 8.20.0 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@perspective-dev/server@4.3.0': {} - - '@perspective-dev/viewer-d3fc@4.3.0(d3-brush@3.0.0)(d3-dispatch@3.0.1)(d3-fetch@3.0.1)(d3-path@3.1.0)(d3-random@3.0.1)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-shape@3.2.0)(d3-time@3.1.0)(d3-zoom@3.0.0)': - dependencies: - '@d3fc/d3fc-chart': 5.1.9(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-element': 6.2.0 - '@perspective-dev/client': 4.3.0 - '@perspective-dev/viewer': 4.3.0 - chroma-js: 3.2.0 - d3: 7.9.0 - d3-array: 3.2.4 - d3-color: 3.1.0 - d3-selection: 3.0.0 - d3-svg-legend: 2.25.6 - d3fc: 15.2.13(d3-array@3.2.4)(d3-brush@3.0.0)(d3-dispatch@3.0.1)(d3-fetch@3.0.1)(d3-path@3.1.0)(d3-random@3.0.1)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0)(d3-time@3.1.0)(d3-zoom@3.0.0) - gradient-parser: 1.2.0 - transitivePeerDependencies: - - bufferutil - - d3-brush - - d3-dispatch - - d3-fetch - - d3-path - - d3-random - - d3-scale - - d3-scale-chromatic - - d3-shape - - d3-time - - d3-zoom - - debug - - supports-color - - utf-8-validate - - '@perspective-dev/viewer-datagrid@4.3.0': - dependencies: - '@perspective-dev/client': 4.3.0 - '@perspective-dev/viewer': 4.3.0 - chroma-js: 3.2.0 - regular-table: 0.8.3 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@perspective-dev/viewer@4.3.0': - dependencies: - '@perspective-dev/client': 4.3.0 - pro_self_extracting_wasm: 0.0.9 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@perspective-dev/workspace@4.3.0': - dependencies: - '@lumino/algorithm': 2.0.4 - '@lumino/commands': 2.3.3 - '@lumino/coreutils': 2.2.2 - '@lumino/domutils': 2.0.4 - '@lumino/messaging': 2.0.4 - '@lumino/signaling': 2.1.5 - '@lumino/virtualdom': 2.0.4 - '@lumino/widgets': 2.7.5 - '@perspective-dev/client': 4.3.0 - lodash: 4.18.1 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@sindresorhus/merge-streams@4.0.0': {} - - '@types/d3-selection@1.0.10': {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - async-function@1.0.0: {} - - async@3.2.6: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - balanced-match@1.0.2: {} - - basic-auth@2.0.1: - dependencies: - safe-buffer: 5.1.2 - - bn.js@5.2.3: {} - - brace-expansion@1.1.13: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - buffer-pipe@0.0.3: - dependencies: - safe-buffer: 5.2.1 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chroma-js@3.2.0: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - commander@7.2.0: {} - - concat-map@0.0.1: {} - - copy-file@11.1.0: - dependencies: - graceful-fs: 4.2.11 - p-event: 6.0.1 - - corser@2.0.1: {} - - cpy@13.2.1: - dependencies: - copy-file: 11.1.0 - globby: 16.1.1 - junk: 4.0.1 - micromatch: 4.0.8 - p-filter: 4.1.0 - p-map: 7.0.4 - - cross-spawn@6.0.6: - dependencies: - nice-try: 1.0.5 - path-key: 2.0.1 - semver: 5.7.2 - shebang-command: 1.2.0 - which: 1.3.1 - - d3-array@1.0.1: {} - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-axis@3.0.0: {} - - d3-brush@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3-chord@3.0.1: - dependencies: - d3-path: 3.1.0 - - d3-collection@1.0.7: {} - - d3-color@1.4.1: {} - - d3-color@3.1.0: {} - - d3-contour@4.0.2: - dependencies: - d3-array: 3.2.4 - - d3-delaunay@6.0.4: - dependencies: - delaunator: 5.1.0 - - d3-dispatch@1.0.1: {} - - d3-dispatch@3.0.1: {} - - d3-drag@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - - d3-dsv@3.0.1: - dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - - d3-ease@1.0.7: {} - - d3-ease@3.0.1: {} - - d3-fetch@3.0.1: - dependencies: - d3-dsv: 3.0.1 - - d3-force@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - - d3-format@1.0.2: {} - - d3-format@3.1.2: {} - - d3-geo@3.1.1: - dependencies: - d3-array: 3.2.4 - - d3-hierarchy@3.1.2: {} - - d3-interpolate@1.4.0: - dependencies: - d3-color: 1.4.1 - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@3.1.0: {} - - d3-polygon@3.0.1: {} - - d3-quadtree@3.0.1: {} - - d3-random@3.0.1: {} - - d3-scale-chromatic@3.1.0: - dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - - d3-scale@1.0.3: - dependencies: - d3-array: 1.0.1 - d3-collection: 1.0.7 - d3-color: 1.4.1 - d3-format: 1.0.2 - d3-interpolate: 1.4.0 - d3-time: 1.1.0 - d3-time-format: 2.3.0 - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-selection@1.0.2: {} - - d3-selection@3.0.0: {} - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-svg-legend@2.25.6: - dependencies: - '@types/d3-selection': 1.0.10 - d3-array: 1.0.1 - d3-dispatch: 1.0.1 - d3-format: 1.0.2 - d3-scale: 1.0.3 - d3-selection: 1.0.2 - d3-transition: 1.0.3 - - d3-time-format@2.3.0: - dependencies: - d3-time: 1.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@1.1.0: {} - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@1.0.10: {} - - d3-timer@3.0.1: {} - - d3-transition@1.0.3: - dependencies: - d3-color: 1.4.1 - d3-dispatch: 1.0.1 - d3-ease: 1.0.7 - d3-interpolate: 1.4.0 - d3-selection: 1.0.2 - d3-timer: 1.0.10 - - d3-transition@3.0.1(d3-selection@3.0.0): - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - - d3-zoom@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3@7.9.0: - dependencies: - d3-array: 3.2.4 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.2 - d3-geo: 3.1.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - - d3fc@15.2.13(d3-array@3.2.4)(d3-brush@3.0.0)(d3-dispatch@3.0.1)(d3-fetch@3.0.1)(d3-path@3.1.0)(d3-random@3.0.1)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0)(d3-time@3.1.0)(d3-zoom@3.0.0): - dependencies: - '@d3fc/d3fc-annotation': 3.0.16(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-axis': 3.0.7(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-brush': 3.0.3(d3-brush@3.0.0)(d3-dispatch@3.0.1)(d3-scale@4.0.2)(d3-selection@3.0.0) - '@d3fc/d3fc-chart': 5.1.9(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-data-join': 6.0.3(d3-selection@3.0.0) - '@d3fc/d3fc-discontinuous-scale': 4.1.1(d3-scale@4.0.2)(d3-time@3.1.0) - '@d3fc/d3fc-element': 6.2.0 - '@d3fc/d3fc-extent': 4.0.2(d3-array@3.2.4) - '@d3fc/d3fc-financial-feed': 7.1.0(d3-fetch@3.0.1) - '@d3fc/d3fc-group': 3.0.1 - '@d3fc/d3fc-label-layout': 7.0.4(d3-array@3.2.4)(d3-scale@4.0.2)(d3-selection@3.0.0) - '@d3fc/d3fc-pointer': 3.0.3(d3-dispatch@3.0.1)(d3-selection@3.0.0) - '@d3fc/d3fc-random-data': 4.0.2(d3-random@3.0.1)(d3-time@3.1.0) - '@d3fc/d3fc-rebind': 6.0.1 - '@d3fc/d3fc-sample': 5.0.2(d3-array@3.2.4) - '@d3fc/d3fc-series': 6.1.3(d3-array@3.2.4)(d3-path@3.1.0)(d3-scale-chromatic@3.1.0)(d3-scale@4.0.2)(d3-selection@3.0.0)(d3-shape@3.2.0) - '@d3fc/d3fc-shape': 6.0.1(d3-path@3.1.0) - '@d3fc/d3fc-technical-indicator': 8.1.1(d3-array@3.2.4) - '@d3fc/d3fc-webgl': 3.2.1(d3-scale@4.0.2)(d3-shape@3.2.0) - '@d3fc/d3fc-zoom': 1.2.0(d3-dispatch@3.0.1)(d3-selection@3.0.0)(d3-zoom@3.0.0) - transitivePeerDependencies: - - d3-array - - d3-brush - - d3-dispatch - - d3-fetch - - d3-path - - d3-random - - d3-scale - - d3-scale-chromatic - - d3-selection - - d3-shape - - d3-time - - d3-zoom - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - delaunator@5.1.0: - dependencies: - robust-predicates: 3.0.3 - - detect-libc@2.1.2: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.24.0: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - esbuild@0.28.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 - - escape-string-regexp@1.0.5: {} - - eventemitter3@4.0.7: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - follow-redirects@1.16.0: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functions-have-names@1.2.3: {} - - generator-function@2.0.1: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@16.1.1: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - fast-glob: 3.3.3 - ignore: 7.0.5 - is-path-inside: 4.0.0 - slash: 5.1.0 - unicorn-magic: 0.4.0 - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - gradient-parser@1.2.0: {} - - has-bigints@1.1.0: {} - - has-flag@3.0.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - he@1.2.0: {} - - hosted-git-info@2.8.9: {} - - html-encoding-sniffer@3.0.0: - dependencies: - whatwg-encoding: 2.0.0 - - http-proxy@1.18.1: - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.16.0 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - - http-server@14.1.1: - dependencies: - basic-auth: 2.0.1 - chalk: 4.1.2 - corser: 2.0.1 - he: 1.2.0 - html-encoding-sniffer: 3.0.0 - http-proxy: 1.18.1 - mime: 1.6.0 - minimist: 1.2.8 - opener: 1.5.2 - portfinder: 1.0.38 - secure-compare: 3.0.1 - union: 0.5.0 - url-join: 4.0.1 - transitivePeerDependencies: - - debug - - supports-color - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - ignore@7.0.5: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - internmap@2.0.3: {} - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-callable@1.2.7: {} - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-path-inside@4.0.0: {} - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - json-parse-better-errors@1.0.2: {} - - junk@4.0.1: {} - - leb128@0.0.5: - dependencies: - bn.js: 5.2.3 - buffer-pipe: 0.0.3 - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - load-json-file@4.0.0: - dependencies: - graceful-fs: 4.2.11 - parse-json: 4.0.0 - pify: 3.0.0 - strip-bom: 3.0.0 - - lodash@4.18.1: {} - - math-intrinsics@1.1.0: {} - - memorystream@0.3.1: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.2 - - mime@1.6.0: {} - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.13 - - minimist@1.2.8: {} - - mkdirp@3.0.1: {} - - ms@2.1.3: {} - - nice-try@1.0.5: {} - - normalize-package-data@2.5.0: - dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.11 - semver: 5.7.2 - validate-npm-package-license: 3.0.4 - - npm-run-all@4.1.5: - dependencies: - ansi-styles: 3.2.1 - chalk: 2.4.2 - cross-spawn: 6.0.6 - memorystream: 0.3.1 - minimatch: 3.1.5 - pidtree: 0.3.1 - read-pkg: 3.0.0 - shell-quote: 1.8.3 - string.prototype.padend: 3.1.6 - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - opener@1.5.2: {} - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-event@6.0.1: - dependencies: - p-timeout: 6.1.4 - - p-filter@4.1.0: - dependencies: - p-map: 7.0.4 - - p-map@7.0.4: {} - - p-timeout@6.1.4: {} - - parse-json@4.0.0: - dependencies: - error-ex: 1.3.4 - json-parse-better-errors: 1.0.2 - - path-key@2.0.1: {} - - path-parse@1.0.7: {} - - path-type@3.0.0: - dependencies: - pify: 3.0.0 - - picomatch@2.3.2: {} - - pidtree@0.3.1: {} - - pify@3.0.0: {} - - portfinder@1.0.38: - dependencies: - async: 3.2.6 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - possible-typed-array-names@1.1.0: {} - - prettier@3.8.3: {} - - pro_self_extracting_wasm@0.0.9: - dependencies: - http-server: 14.1.1 - leb128: 0.0.5 - zx: 8.8.5 - transitivePeerDependencies: - - debug - - supports-color - - qs@6.15.0: - dependencies: - side-channel: 1.1.0 - - queue-microtask@1.2.3: {} - - read-pkg@3.0.0: - dependencies: - load-json-file: 4.0.0 - normalize-package-data: 2.5.0 - path-type: 3.0.0 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regular-table@0.8.3: {} - - requires-port@1.0.0: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - robust-predicates@3.0.3: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - rw@1.3.3: {} - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safer-buffer@2.1.2: {} - - secure-compare@3.0.1: {} - - semver@5.7.2: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - shebang-command@1.2.0: - dependencies: - shebang-regex: 1.0.0 - - shebang-regex@1.0.0: {} - - shell-quote@1.8.3: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - slash@5.1.0: {} - - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.22 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.22 - - spdx-license-ids@3.0.22: {} - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - stoppable@1.1.0: {} - - string.prototype.padend@3.1.6: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - strip-bom@3.0.0: {} - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - unicorn-magic@0.4.0: {} - - union@0.5.0: - dependencies: - qs: 6.15.0 - - url-join@4.0.1: {} - - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - - whatwg-encoding@2.0.0: - dependencies: - iconv-lite: 0.6.3 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@1.3.1: - dependencies: - isexe: 2.0.0 - - ws@8.20.0: {} - - zx@8.8.5: {} diff --git a/js/pnpm-workspace.yaml b/js/pnpm-workspace.yaml deleted file mode 100644 index 5ed0b5a..0000000 --- a/js/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -allowBuilds: - esbuild: true diff --git a/js/src/index.css b/js/src/index.css deleted file mode 100644 index 1fc9493..0000000 --- a/js/src/index.css +++ /dev/null @@ -1,44 +0,0 @@ -@import "perspective-viewer-pro.css"; -@import "perspective-viewer-pro-dark.css"; -@import "perspective-viewer-monokai.css"; -@import "perspective-viewer-vaporwave.css"; -@import "perspective-viewer-solarized.css"; -@import "perspective-viewer-solarized-dark.css"; -@import "perspective-viewer-gruvbox.css"; -@import "perspective-viewer-gruvbox-dark.css"; - -html { - font-family: "Roboto Mono"; -} - -perspective-workspace { - position: absolute; - top: 40px; - bottom: 0; - left: 0; - right: 0; -} - -.title-bar { - position: absolute; - top: 0; - left: 0; - right: 0; - height: 40px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 10px; - z-index: 1000; - border-bottom: 1px solid #ccc; -} - -.title { - font-size: 18px; - font-weight: bold; -} -.save-button { - padding: 5px 10px; - border: none; - cursor: pointer; -} diff --git a/js/src/index.html b/js/src/index.html deleted file mode 100644 index 74a54bc..0000000 --- a/js/src/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - raydar - - - - - - - - - - - -
- Raydar v{{ version }} - -
- - - diff --git a/js/src/index.js b/js/src/index.js deleted file mode 100644 index 82cffda..0000000 --- a/js/src/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import perspective from "@perspective-dev/client"; -import perspective_viewer from "@perspective-dev/viewer"; -import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm"; -import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"; - -import "@perspective-dev/workspace"; -import "@perspective-dev/viewer-datagrid"; -import "@perspective-dev/viewer-d3fc"; - -const perspective_init_promise = Promise.all([ - perspective.init_server(fetch(SERVER_WASM)), - perspective_viewer.init_client(fetch(CLIENT_WASM)), -]); - -function removeTrailingSlash(url) { - return url.replace(/\/$/, ""); -} - -async function load() { - await perspective_init_promise; - const workspace = document.querySelector("perspective-workspace"); - const saveButton = document.getElementById("save-layout-button"); - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const websocket = await perspective.websocket( - `${protocol}//${window.location.host}${removeTrailingSlash( - window.location.pathname, - )}/ws`, - ); - const registeredTables = new Set(); - - const updateTables = async () => { - const response = await fetch( - `${removeTrailingSlash(window.location.href)}/tables`, - ); - const tables = await response.json(); - - tables.map(async (tableName) => { - if (registeredTables.has(tableName)) return; - console.log(`Registering table: ${tableName}`); - registeredTables.add(tableName); - await workspace.addTable( - tableName, - await websocket.open_table(tableName), - ); - }); - }; - - document.body.addEventListener("dragover", (e) => { - e.preventDefault(); - }); - - document.body.addEventListener("drop", (e) => { - e.preventDefault(); - - const file = e.dataTransfer.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (event) => { - try { - const json = JSON.parse(event.target.result); - workspace.restore(json); - console.log("File contents:", json); - } catch (error) { - console.error("Error parsing JSON:", error); - } - }; - reader.readAsText(file); - } - }); - - saveButton.addEventListener("click", function () { - let workspace = document.querySelector("perspective-workspace"); - - workspace.save().then((config) => { - // Convert the configuration object to a JSON string - let json = JSON.stringify(config); - - // Create a Blob object from the JSON string - let blob = new Blob([json], { type: "application/json" }); - - // Create a download link - let link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = "workspace.json"; - - // Append the link to the document body and click it to start the download - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); - }); - - const layouts = await fetch("static/layouts/default.json"); - const layoutData = await layouts.json(); - console.log("Loading layout from static/layouts/default.json..."); - - if (Object.keys(layoutData).length > 0) { - await workspace.restore(layoutData); - } - - await updateTables(); - - // update tables every 5s - setInterval(updateTables, 5000); -} - -load(); diff --git a/js/src/layouts/default.json b/js/src/layouts/default.json deleted file mode 100644 index 0967ef4..0000000 --- a/js/src/layouts/default.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/js/tools/build.js b/js/tools/build.js deleted file mode 100644 index d95f072..0000000 --- a/js/tools/build.js +++ /dev/null @@ -1,38 +0,0 @@ -import esbuild from "esbuild"; - -const CUTOFF_PERCENT = 0.02; - -const DEFAULT_BUILD = { - target: ["es2022"], - bundle: true, - minify: !process.env.PSP_DEBUG, - sourcemap: true, - metafile: true, - entryNames: "[name]", - chunkNames: "[name]", - assetNames: "[name]", -}; - -export const build = async (config) => { - const result = await esbuild.build({ - ...DEFAULT_BUILD, - ...config, - }); - - if (result.metafile) { - for (const output of Object.keys(result.metafile.outputs)) { - const { inputs, bytes } = result.metafile.outputs[output]; - for (const input of Object.keys(inputs)) { - if (inputs[input].bytesInOutput / bytes < CUTOFF_PERCENT) { - delete inputs[input]; - } - } - } - - const text = await esbuild.analyzeMetafile(result.metafile, { - color: true, - }); - - console.log(text); - } -}; diff --git a/js/tools/css.js b/js/tools/css.js deleted file mode 100644 index cb68059..0000000 --- a/js/tools/css.js +++ /dev/null @@ -1,38 +0,0 @@ -import { bundleAsync } from "lightningcss"; -import { getarg } from "./getarg.js"; -import fs from "fs"; -import path from "path"; - -const DEBUG = getarg("--debug"); - -const DEFAULT_RESOLVER = { - resolve(specifier, originatingFile) { - if (/^https?:\/\//.test(specifier)) { - return specifier; - } - - if (specifier.startsWith("perspective-viewer-")) { - const viewerCssDir = path.resolve( - "node_modules/@perspective-dev/viewer/dist/css", - ); - const normalized = specifier.replace(/^perspective-viewer-/, ""); - const normalizedPath = path.join(viewerCssDir, normalized); - if (fs.existsSync(normalizedPath)) { - return normalizedPath; - } - return path.join(viewerCssDir, specifier); - } - return path.resolve(path.dirname(originatingFile), specifier); - }, -}; - -export const compile = async (root = "src/index.css", resolver = null) => { - const { code } = await bundleAsync({ - filename: path.resolve(root), - minify: !DEBUG, - sourceMap: false, - resolver: resolver || DEFAULT_RESOLVER, - }); - fs.mkdirSync("./dist", { recursive: true }); - fs.writeFileSync("./dist/index.css", code); -} diff --git a/js/tools/getarg.js b/js/tools/getarg.js deleted file mode 100644 index 9cfe88c..0000000 --- a/js/tools/getarg.js +++ /dev/null @@ -1,23 +0,0 @@ -export const getarg = (flag, ...args) => { - if (Array.isArray(flag)) { - flag = flag.map((x, i) => x + (args[i] || "")).join(""); - } - const argv = process.argv.slice(2); - if (flag) { - const index = argv.indexOf(flag); - if (index > -1) { - const next = argv[index + 1]; - if (next) { - return next; - } else { - return true; - } - } - } else { - return argv - .map(function (arg) { - return "'" + arg.replace(/'/g, "'\\''") + "'"; - }) - .join(" "); - } - }; diff --git a/pyproject.toml b/pyproject.toml index d562542..34ee198 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "hatchling", - "hatch-js", ] build-backend = "hatchling.build" @@ -10,11 +9,11 @@ name = "raydar" authors = [ {name = "Point72, L.P.", email = "OpenSource@point72.com"}, ] -description = "A perspective powered, user editable ray dashboard via ray serve" +description = "A perspective powered, user editable ray dashboard" readme = "README.md" license = { text = "Apache-2.0" } version = "0.3.0" -requires-python = ">=3.10" +requires-python = ">=3.11" keywords = [ "perspective", "ray", @@ -31,7 +30,6 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -40,14 +38,21 @@ classifiers = [ dependencies = [ "coolname", - "fastapi<0.139.2", + # ray.serve imports jinja2 at import time but does not declare it "jinja2", - "packaging", - "perspective-python>=3.4,<3.5", + "pandas", + "perspective-python>=4.5,<4.6", "polars", "pyarrow", - "pydantic", + "pydantic>=2", "ray[serve]>=2.8", + "spaday>=0.7,<0.8", + "spaday-perspective>=0.2,<0.3", + "spaday-webawesome>=0.2,<0.3", + "starlette", + "transports>=0.8,<0.9", + "uvicorn", + "websockets", ] [project.optional-dependencies] @@ -56,8 +61,8 @@ develop = [ "bump-my-version", "check-dist", "codespell", - "hatch-js", "hatchling", + "httpx", "mdformat", "mdformat-tables>=1", "pytest", @@ -69,8 +74,6 @@ develop = [ "ty", "uv", "wheel", - # Test deps - "pandas", ] [project.scripts] @@ -95,16 +98,6 @@ filename = "pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' -[[tool.bumpversion.files]] -filename = "js/package.json" -search = '"version": "{current_version}"' -replace = '"version": "{new_version}"' - -[[tool.bumpversion.files]] -filename = "js/src/index.html" -search = 'Raydar v{current_version}' -replace = 'Raydar v{new_version}' - [tool.coverage.run] branch = true omit = [ @@ -120,39 +113,18 @@ exclude_also = [ ignore_errors = true fail_under = 40 -[tool.hatch.build] -artifacts = [ - "raydar/dashboard/static" -] - [tool.hatch.build.sources] src = "/" [tool.hatch.build.targets.sdist] packages = [ "raydar", - "js", -] -exclude = [ - "js/dist", - "js/node_modules", ] [tool.hatch.build.targets.wheel] packages = [ "raydar", ] -exclude = [ - "js", -] - -[tool.hatch.build.hooks.hatch-js] -path = "js" -build_cmd = "build" -tool = "pnpm" -targets = [ - "raydar/dashboard/static/index.js", -] [tool.pytest.ini_options] addopts = [ diff --git a/raydar/dashboard/__init__.py b/raydar/dashboard/__init__.py index 02a2992..738887f 100644 --- a/raydar/dashboard/__init__.py +++ b/raydar/dashboard/__init__.py @@ -1 +1,5 @@ -from .server import PerspectiveProxyRayServer, PerspectiveRayServer +from .dashboard import Dashboard, TableHost +from .local import LocalDashboard +from .state import DashboardState, default_layout + +__all__ = ("Dashboard", "DashboardState", "LocalDashboard", "TableHost", "default_layout") diff --git a/raydar/dashboard/dashboard.py b/raydar/dashboard/dashboard.py new file mode 100644 index 0000000..3500208 --- /dev/null +++ b/raydar/dashboard/dashboard.py @@ -0,0 +1,175 @@ +"""Perspective tables plus the spaday app that renders them. + +A :class:`Dashboard` owns everything the browser talks to: the Perspective +server holding the tables, the transports session holding the UI state, and the +Starlette app that serves the page. It is deliberately free of any Ray +dependency so it can run in the caller's process, in a Ray Serve replica, or +standalone. +""" + +import asyncio +from collections.abc import Awaitable, Callable, Sequence +from contextlib import asynccontextmanager +from datetime import UTC, date, datetime, time +from typing import Any + +import perspective +import transports +from perspective.handlers.starlette import PerspectiveStarletteHandler +from spaday import Wire +from spaday.backends.starlette import serve as spaday_serve +from spaday_perspective import package as perspective_package +from spaday_webawesome import package as webawesome_package +from starlette.routing import WebSocketRoute +from starlette.websockets import WebSocket, WebSocketDisconnect + +from .. import __version__ +from .page import STYLES, build_page +from .state import DashboardState, default_layout + +__all__ = ("Dashboard", "TableHost") + +_TEMPORAL_TYPES = ("datetime", "date") + + +def _to_epoch_millis(value): + """Perspective encodes rows as JSON, which has no datetime, so send millis.""" + if isinstance(value, datetime): + return int(value.timestamp() * 1000) + if isinstance(value, date): + return int(datetime.combine(value, time()).timestamp() * 1000) + return value + + +class TableHost: + """Owns a Perspective server and the tables served over its websocket.""" + + def __init__(self, limit: int | None = None): + self.server = perspective.Server() + self._client = self.server.new_local_client() + self._limit = limit + self._schemas: dict[str, dict] = {} + self._tables: dict[str, Any] = {} + self._temporal_columns: dict[str, set[str]] = {} + + def names(self) -> list[str]: + return list(self._schemas) + + def total_rows(self) -> int: + """Rows currently held, which `limit` and `clear` both reduce.""" + return sum(table.size() for table in self._tables.values()) + + def new_table(self, tablename: str, schema: dict) -> bool: + """Create a table, returning whether it did not already exist.""" + if tablename in self._schemas: + return False + self._schemas[tablename] = schema + self._temporal_columns[tablename] = {column for column, kind in schema.items() if kind in _TEMPORAL_TYPES} + kwargs = {"name": tablename} + if self._limit is not None: + kwargs["limit"] = self._limit + self._tables[tablename] = self._client.table(schema, **kwargs) + return True + + def update(self, tablename: str, data) -> None: + if isinstance(data, dict): + data = [data] + if tablename not in self._tables: + raise KeyError(f"No such table: {tablename}") + temporal = self._temporal_columns[tablename] + if temporal: + data = [{key: _to_epoch_millis(value) if key in temporal else value for key, value in row.items()} for row in data] + self._tables[tablename].update(data) + + def clear(self, tablename: str) -> None: + if tablename in self._tables: + self._tables[tablename].clear() + + +class Dashboard: + """The raydar UI: Perspective tables, UI state, and the Starlette app. + + Args: + title: Page title and brand text. + limit: Optional per-table row cap, applied to every table created. + layout: A perspective-workspace layout to use instead of the generated + one-tab-per-table default. + background: Factories for coroutines to run for the lifetime of the app. + Factories rather than coroutines so nothing is created for a + dashboard that is never served. + """ + + def __init__( + self, + title: str = "raydar", + limit: int | None = None, + layout: dict | None = None, + background: Sequence[Callable[[], Awaitable]] = (), + ): + self.tables = TableHost(limit=limit) + self.state = DashboardState() + self._layout_override = layout + + self._session = transports.Session() + self._session.host(self.state) + self._transport = transports.Server(self._session) + + @asynccontextmanager + async def lifespan(_app): + factories = (lambda: transports.autosync(self._transport), *background) + tasks = [asyncio.ensure_future(factory()) for factory in factories] + try: + yield + finally: + for task in tasks: + task.cancel() + + self.app = spaday_serve( + build_page(title, __version__), + title=title, + packages=[perspective_package, webawesome_package], + # spaday infers "source" from a `js/` dir next to its package, which some + # unrelated wheels create in site-packages. raydar always consumes the + # packaged assets, so say so rather than rely on the heuristic. + layout="installed", + wire=[Wire("/ws", namespace="rd", flatten=False)], + routes=[ + WebSocketRoute("/ws", transports.ws_endpoint(self._transport)), + WebSocketRoute("/perspective", self._perspective_socket), + ], + lifespan=lifespan, + store={"dark": False}, + head=STYLES, + ) + + async def _perspective_socket(self, websocket: WebSocket) -> None: + try: + await PerspectiveStarletteHandler(perspective_server=self.tables.server, websocket=websocket).run() + except WebSocketDisconnect: + pass + + def apply(self, batch: dict) -> None: + """Apply a drained :class:`~raydar.ops.OpBuffer` batch to the tables.""" + changed = False + for tablename, schema in (batch.get("schemas") or {}).items(): + changed |= self.tables.new_table(tablename, schema) + for tablename in batch.get("cleared") or (): + self.tables.clear(tablename) + changed = True + for tablename, rows in (batch.get("updates") or {}).items(): + if rows: + self.tables.update(tablename, rows) + changed = True + # Schemas are replayed on every drain, so most batches are empty; only + # touch the synced model when something actually moved. + if changed: + self._refresh_state() + + def _refresh_state(self) -> None: + names = self.tables.names() + if names != self.state.tables: + self.state.tables = names + self.state.layout = self._layout_override or default_layout(names) + self.state.rows = f"{self.tables.total_rows():,}" + self.state.status = "Live" if names else "Waiting for data" + self.state.updated = datetime.now(tz=UTC).astimezone().strftime("%H:%M:%S") diff --git a/raydar/dashboard/demo.py b/raydar/dashboard/demo.py index b1ea80b..0e65c46 100644 --- a/raydar/dashboard/demo.py +++ b/raydar/dashboard/demo.py @@ -1,48 +1,53 @@ -import os +"""A runnable example of the local dashboard: `python -m raydar.dashboard.demo`. + +The dashboard is served from this process and pulls updates from the tracker +actor over Ray, so the cluster needs no inbound port. +""" + import random import time +from datetime import UTC, datetime import ray -from .server import PerspectiveProxyRayServer, PerspectiveRayServer +from ..task_tracker import RayTaskTracker + +TABLE = "demo" +SCHEMA = { + "start": "datetime", + "end": "datetime", + "runtime": "float", + "backoff": "float", + "random": "float", +} @ray.remote -def test_job(backoff, tablename, proxy): - start = time.time() +def demo_job(backoff: float) -> dict: + start = datetime.now(tz=UTC) time.sleep(backoff) - end = time.time() - runtime = end - start - data = {"start": start, "end": end, "runtime": runtime, "backoff": backoff, "random": random.random()} - proxy.remote("update", tablename, data) - return data + end = datetime.now(tz=UTC) + return { + "start": start, + "end": end, + "runtime": (end - start).total_seconds(), + "backoff": backoff, + "random": random.random(), + } if __name__ == "__main__": - os.environ["RAY_SERVE_ENABLE_EXPERIMENTAL_STREAMING"] = "1" - - host = "127.0.0.1" # NOTE: change if you run on another machine - port = 8989 - ray.init(dashboard_host=host, dashboard_port=port) - ray.serve.start(http_options={"host": host, "port": port + 1}) - - webserver = ray.serve.run(PerspectiveRayServer.bind(), name="webserver", route_prefix="/") - proxy_server = ray.serve.run(PerspectiveProxyRayServer.bind(webserver), name="proxy", route_prefix="/proxy") - - # setup perspective table - proxy_server.remote( - "new", - "data", - { - "start": "datetime", - "end": "datetime", - "runtime": "float", - "backoff": "float", - "random": "float", - }, - ) - - # launch jobs - while True: - test_job.remote(backoff=random.random(), tablename="data", proxy=proxy_server) - time.sleep(0.5) + ray.init() + + task_tracker = RayTaskTracker(namespace="raydar-demo", dashboard="local") + task_tracker.create_table(TABLE, SCHEMA) + print(f"raydar dashboard: {task_tracker.dashboard_url}") + + try: + while True: + ref = demo_job.remote(backoff=random.random()) + task_tracker.process([ref]) + task_tracker.update_table(TABLE, [ray.get(ref)]) + time.sleep(0.5) + except KeyboardInterrupt: + task_tracker.exit() diff --git a/raydar/dashboard/local.py b/raydar/dashboard/local.py new file mode 100644 index 0000000..b046938 --- /dev/null +++ b/raydar/dashboard/local.py @@ -0,0 +1,106 @@ +"""Run the dashboard in the caller's process. + +This is the mode that needs no inbound port on the Ray cluster. The web server +binds to the caller's machine and pulls table operations from the tracker actor +over the connection Ray already holds, so the browser only ever talks to +localhost and the cluster only ever accepts Ray traffic. +""" + +import asyncio +import logging +import socket +import threading +from collections.abc import Callable + +import uvicorn + +from .dashboard import Dashboard + +logger = logging.getLogger(__name__) + +__all__ = ("LocalDashboard",) + + +class LocalDashboard: + """A :class:`~raydar.dashboard.dashboard.Dashboard` served from a background thread. + + Args: + drain: A callable returning the next batch of table operations, or None + when there is nothing to apply. Called from a worker thread, so it + may block. + host: Interface to bind. Defaults to loopback. + port: Port to bind, or 0 to let the OS pick a free one. + poll_interval: Seconds between calls to ``drain``. + **kwargs: Forwarded to :class:`~raydar.dashboard.dashboard.Dashboard`. + """ + + def __init__( + self, + drain: Callable[[], dict | None], + host: str = "127.0.0.1", + port: int = 0, + poll_interval: float = 0.5, + **kwargs, + ): + self._drain = drain + self._host = host + self._poll_interval = poll_interval + self._thread: threading.Thread | None = None + + if host not in ("127.0.0.1", "localhost", "::1"): + logger.warning(f"raydar dashboard is bound to {host} and serves Ray task metadata without authentication") + + # Bind up front so `url` is accurate before the server thread starts. + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((host, port)) + self._socket.listen(128) + self.port = self._socket.getsockname()[1] + + self.dashboard = Dashboard(background=[self._poll], **kwargs) + config = uvicorn.Config(self.dashboard.app, log_level="warning", lifespan="on") + self._server = uvicorn.Server(config) + # uvicorn installs signal handlers, which is only legal on the main thread. + self._server.install_signal_handlers = lambda: None + + @property + def url(self) -> str: + return f"http://{self._host}:{self.port}" + + async def _poll(self) -> None: + while True: + # Applying is inside the guard too: one bad row must not kill the loop + # and leave the dashboard silently frozen. + try: + batch = await asyncio.to_thread(self._drain) + if batch: + self.dashboard.apply(batch) + except Exception: + logger.exception("Failed to apply dashboard updates") + await asyncio.sleep(self._poll_interval) + + def start(self) -> str: + """Start serving in a daemon thread and return the dashboard URL.""" + if self._thread is not None and self._thread.is_alive(): + return self.url + self._thread = threading.Thread( + target=lambda: asyncio.run(self._server.serve(sockets=[self._socket])), + name="raydar-dashboard", + daemon=True, + ) + self._thread.start() + logger.info(f"raydar dashboard serving at {self.url}") + return self.url + + def stop(self) -> None: + """Ask the server to exit and wait for the thread to finish. Idempotent.""" + self._server.should_exit = True + thread = self._thread + if thread is not None: + thread.join(timeout=5) + if thread.is_alive(): + # Keep the reference so `start` cannot bind a second server to this socket. + logger.warning("raydar dashboard thread did not stop within 5s") + return + self._thread = None + self._socket.close() diff --git a/raydar/dashboard/page.py b/raydar/dashboard/page.py new file mode 100644 index 0000000..1d96b52 --- /dev/null +++ b/raydar/dashboard/page.py @@ -0,0 +1,67 @@ +"""The raydar page, authored as a spaday component tree.""" + +from spaday import cond, element, field, obj +from spaday_perspective import PerspectivePanel +from spaday_webawesome import WaBadge, WaSwitch + +__all__ = ("STYLES", "build_page") + +STYLES = """ + +""" + + +def build_page(title: str, version: str): + """Build the component tree. + + ``rd.*`` fields come from the transports-hosted :class:`~raydar.dashboard.state.DashboardState`; + ``dark`` is a client-only store field, so the theme toggle needs no round trip. + """ + panel = ( + PerspectivePanel(id="raydar-workspace") + .compute("theme", cond(field("dark"), "dark", "light")) + .compute( + "config", + obj({"ws_url": "/perspective", "tables": field("rd.tables"), "layout": field("rd.layout")}), + ) + ) + + header = element( + "header", + element("span", class_="rd-brand").text(title), + element("span", class_="rd-version").text(f"v{version}"), + element( + "div", + WaBadge(variant="brand").bind("textContent", "rd.status"), + element("span").bind("textContent", "rd.rows"), + element("span").text("rows"), + element("span", class_="rd-version").bind("textContent", "rd.updated"), + WaSwitch().bind("checked", "dark", mode="two-way"), + element("span").text("Dark"), + class_="rd-metrics", + ), + class_="rd-header", + ) + + return element( + "div", + header, + element("section", panel, class_="rd-workspace"), + ).compute("class", cond(field("dark"), "rd wa-dark", "rd")) diff --git a/raydar/dashboard/serve.py b/raydar/dashboard/serve.py new file mode 100644 index 0000000..82df5d2 --- /dev/null +++ b/raydar/dashboard/serve.py @@ -0,0 +1,32 @@ +"""Serve the dashboard from inside the Ray cluster. + +This mode requires an inbound port on the cluster, which is what +:mod:`raydar.dashboard.local` exists to avoid. It remains available for +clusters that already expose Ray Serve's HTTP ingress. +""" + +from ray.serve import deployment, ingress + +from .dashboard import Dashboard + +__all__ = ("RaydarDeployment",) + + +@deployment(name="raydar_dashboard", num_replicas=1) +@ingress() +class RaydarDeployment: + """A single replica owning the Perspective tables and the spaday app. + + The app is built by ``__serve_build_asgi_app__`` rather than handed to + ``ingress``: it holds a Perspective server and a transports store, neither + of which survives the pickling Ray Serve does to ship an app to a replica. + """ + + def __init__(self, title: str = "raydar", limit: int | None = None, layout: dict | None = None): + self.dashboard = Dashboard(title=title, limit=limit, layout=layout) + + def __serve_build_asgi_app__(self): + return self.dashboard.app + + async def apply(self, batch: dict) -> None: + self.dashboard.apply(batch) diff --git a/raydar/dashboard/server.py b/raydar/dashboard/server.py deleted file mode 100644 index b645ac3..0000000 --- a/raydar/dashboard/server.py +++ /dev/null @@ -1,131 +0,0 @@ -import logging -from os import environ -from os.path import abspath, dirname, join -from traceback import format_exc - -import perspective -from fastapi import FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from perspective.handlers.starlette import PerspectiveStarletteHandler -from pydantic import BaseModel, Field -from ray.serve import Application, deployment, ingress - -from .. import __version__ - - -class PerspectiveRayServerArgs(BaseModel): - name: str = Field(default="Perspective") - - -app = FastAPI() -logger = logging.getLogger("ray.serve") -static_files_dir = join(abspath(dirname(__file__)), "static") -templates = Jinja2Templates(static_files_dir) -app.mount("/static", StaticFiles(directory=static_files_dir, check_dir=False, html=True)) - - -@deployment(name="Perspective_Web_Server", num_replicas=1) -@ingress(app) -class PerspectiveRayServer: - def __init__(self, args: PerspectiveRayServerArgs = None): - logger.setLevel(logging.ERROR) - args = args or PerspectiveRayServerArgs() - self._schemas = {} - self._tables = {} - - def new_table(self, tablename: str, schema) -> None: - if tablename in self._schemas: - return self._schemas[tablename] - self._schemas[tablename] = schema - self._tables[tablename] = perspective.table(schema, name=tablename) - - def clear_table(self, tablename: str, schema) -> None: - if tablename in self._tables: - self._tables[tablename].clear() - - def update(self, tablename: str, data): - if isinstance(data, dict): - data = [data] - self._tables[tablename].update(data) - - @app.websocket("/ws") - async def ws(self, ws: WebSocket): - handler = PerspectiveStarletteHandler(websocket=ws) - try: - await handler.run() - except WebSocketDisconnect: - ... - - @app.get("/") - async def site(self, request: Request): - return templates.TemplateResponse(request=request, name="index.html", context={"version": __version__, "javascript": "index.js"}) - - @app.get("/version") - async def version(self): - return __version__ - - @app.get("/tables") - async def tables(self): - return list(self._schemas.keys()) - - @app.post("/new/{tablename}") - async def new_table_rest(self, tablename: str, request: Request) -> Response: - if tablename in self._schemas: - raise HTTPException(501, "Table already exists, replace not yet supported") - try: - schema = await request.json() - except BaseException as exception: - raise HTTPException(503, "Exception during schema parsing") from exception - - try: - self.new_table(tablename, schema) - return Response(content=schema) - except BaseException as exception: - raise HTTPException( - 503, - f"Exception during table creation: {schema} / {tablename} / {format_exc()}", - ) from exception - - @app.get("/get/{tablename}") - async def get_table_rest(self, tablename: str): - if tablename not in self._schemas: - raise HTTPException(404, "Table does not exist: {tablename}") - return Response(content=self._schemas[tablename]) - - @app.post("/update/{tablename}") - async def update_table_rest(self, tablename: str, request: Request) -> Response: - if tablename not in self._schemas: - # just create table for now - self.new_table(tablename, []) - try: - data = await request.json() - except BaseException as exception: - raise HTTPException(503, "Exception during data parsing") from exception - - try: - self.update(tablename, data) - except BaseException as exception: - raise HTTPException(503, f"Exception during data ingestion: {tablename} / {format_exc()}") from exception - - -@deployment(name="Perspective_Proxy_Server") -class PerspectiveProxyRayServer: - def __init__(self, psp_handle): - logger.setLevel(logging.ERROR) - self._psp_handle = psp_handle - - def __call__(self, op, tablename, data_or_schema): - if op == "new" and tablename and data_or_schema: - self._psp_handle.new_table.remote(tablename, data_or_schema) - if op == "update" and tablename and data_or_schema: - self._psp_handle.update.remote(tablename, data_or_schema) - if op == "clear" and tablename: - self._psp_handle.clear_table.remote(tablename, data_or_schema) - - -def main(args: PerspectiveRayServerArgs = None) -> Application: - args = args or PerspectiveRayServerArgs() - if environ.get("RAY_SERVE_ENABLE_EXPERIMENTAL_STREAMING") is not None: - return PerspectiveProxyRayServer.bind(PerspectiveRayServer.bind(args)) - raise RuntimeError("Perspective server requires websockets, rerun with RAY_SERVE_ENABLE_EXPERIMENTAL_STREAMING=1") diff --git a/raydar/dashboard/state.py b/raydar/dashboard/state.py new file mode 100644 index 0000000..f2f5c56 --- /dev/null +++ b/raydar/dashboard/state.py @@ -0,0 +1,32 @@ +"""The model the browser mirrors over transports.""" + +from pydantic import BaseModel, Field + +__all__ = ("DashboardState", "default_layout") + + +class DashboardState(BaseModel): + """State pushed to every connected browser. + + Bulk table data rides Perspective's own websocket; only this summary and the + workspace layout travel over transports. + """ + + status: str = "Waiting for data" + tables: list[str] = Field(default_factory=list) + layout: dict = Field(default_factory=dict) + rows: str = "0" + updated: str = "" + + +def default_layout(tables: list[str]) -> dict: + """A perspective-workspace layout showing one datagrid tab per table.""" + if not tables: + return {} + return { + "sizes": [1], + "detail": {"main": {"type": "tab-area", "widgets": list(tables), "currentIndex": 0}}, + "master": {"sizes": [], "widgets": []}, + "mode": "globalFilters", + "viewers": {name: {"table": name, "plugin": "Datagrid", "title": name} for name in tables}, + } diff --git a/raydar/ops.py b/raydar/ops.py new file mode 100644 index 0000000..b100ab7 --- /dev/null +++ b/raydar/ops.py @@ -0,0 +1,64 @@ +"""Serializable batches of table operations. + +The tracker actor and the dashboard may live in different processes, so table +creates, updates and clears are expressed as plain data rather than as calls +against a table handle. This module deliberately imports nothing heavy: it is +loaded inside cluster actors that never render a dashboard. +""" + +from collections import deque + +__all__ = ("OpBuffer",) + + +class OpBuffer: + """Accumulates table operations until a consumer drains them. + + Used when the dashboard runs outside the cluster and pulls over Ray's own + connection. ``schemas`` are replayed on every drain so a dashboard that + starts late still learns about tables created before it connected. + """ + + def __init__(self, max_rows_per_table: int = 100_000): + self._max_rows_per_table = max_rows_per_table + self._schemas: dict[str, dict] = {} + self._updates: dict[str, deque] = {} + self._cleared: list[str] = [] + + def new_table(self, tablename: str, schema: dict) -> None: + self._schemas.setdefault(tablename, schema) + + def update(self, tablename: str, data) -> None: + if isinstance(data, dict): + data = [data] + if not data: + return + rows = self._updates.get(tablename) + if rows is None: + rows = self._updates[tablename] = deque(maxlen=self._max_rows_per_table) + rows.extend(data) + + def clear(self, tablename: str) -> None: + self._updates.pop(tablename, None) + if tablename not in self._cleared: + self._cleared.append(tablename) + + def extend(self, batch: dict) -> None: + """Absorb a batch in the same shape :meth:`drain` produces.""" + for tablename, schema in (batch.get("schemas") or {}).items(): + self.new_table(tablename, schema) + for tablename in batch.get("cleared") or (): + self.clear(tablename) + for tablename, rows in (batch.get("updates") or {}).items(): + self.update(tablename, rows) + + def drain(self) -> dict: + """Return the operations buffered since the last call and reset them.""" + batch = { + "schemas": dict(self._schemas), + "updates": {name: list(rows) for name, rows in self._updates.items() if rows}, + "cleared": list(self._cleared), + } + self._updates.clear() + self._cleared.clear() + return batch diff --git a/raydar/task_tracker/task_tracker.py b/raydar/task_tracker/task_tracker.py index 2dfbeab..bcd9eb4 100644 --- a/raydar/task_tracker/task_tracker.py +++ b/raydar/task_tracker/task_tracker.py @@ -1,49 +1,29 @@ import asyncio import itertools import logging -import os from collections.abc import Iterable +from typing import Literal import coolname import pandas as pd import polars as pl import ray -from packaging.version import Version -from ray.serve import shutdown -from ray.serve.handle import DeploymentHandle +from ray.serve import delete as delete_serve_app +from ..ops import OpBuffer from .schema import schema as default_schema logger = logging.getLogger(__name__) -__all__ = ("AsyncMetadataTracker", "RayTaskTracker", "setup_proxy_server") +DashboardMode = Literal["local", "cluster"] + +__all__ = ("AsyncMetadataTracker", "RayTaskTracker") def get_callback_actor_name(name: str) -> str: return f"{name}_callback_actor" -def setup_proxy_server(proxy_server_name="proxy", proxy_server_route_prefix="/proxy", **kwargs) -> DeploymentHandle: - """Construct a webserver, and bind it to a PerspectiveProxyRayServer. - - Args: - proxy_server_name: the name passed to ray.serve.run for the PerspectiveProxyRayServer - proxy_server_route_prefix: the route_prefix passed to ray.serve.run for the PerspectiveProxyRayServer - **kwargs: arguments forwarded to ray.serve.run() for the webserver - - Returns: A DeploymentHandle for the PerspectiveProxyRayServer - """ - from raydar.dashboard.server import PerspectiveProxyRayServer - - webserver = ray.serve.run(**kwargs) - proxy_server = ray.serve.run( - PerspectiveProxyRayServer.bind(webserver), - name=proxy_server_name, - route_prefix=proxy_server_route_prefix, - ) - return proxy_server - - @ray.remote(resources={"node:__internal_head__": 0.1}, num_cpus=0) class AsyncMetadataTrackerCallback: """ @@ -87,7 +67,8 @@ def __init__( name: str, namespace: str, path: str | None = None, - enable_perspective_dashboard: bool = False, + dashboard: DashboardMode | None = None, + max_buffered_rows: int = 100_000, ): """An async Ray Actor Class to track task level metadata. @@ -99,8 +80,9 @@ def __init__( name: Ray actor name, used to construct its AsyncMetadataTrackerCallback actor attribute. namespace: Ray Namespace path: A Cloudpathlib.AnyPath, used for saving its internal polars DataFrame object. - enable_perspective_dashboard: To enable an experimental perspective dashboard. - + dashboard: "local" buffers table operations for a dashboard running outside the cluster to + drain; "cluster" pushes them to a Ray Serve deployment; None disables the dashboard. + max_buffered_rows: Per-table cap on rows held for a "local" dashboard to drain. """ logger.info(f"Initializing an AsyncMetadataTracker in namespace {namespace} with name {name}.") # Passing 'self' to the AsyncMetadataTrackerCallback converts this actor class to a @@ -116,9 +98,11 @@ def __init__( self.df = None self.finished_tasks = {} self.user_defined_metadata = {} - self.perspective_dashboard_enabled = enable_perspective_dashboard + self.dashboard_mode = dashboard self.pending_tasks = [] self.perspective_table_name = f"{name}_data" + self._buffer = None + self._handle = None # WARNING: Do not move this import. Importing these modules elsewhere can cause # difficult to diagnose, "There is no current event loop in thread 'ray_client_server_" errors. @@ -127,45 +111,60 @@ def __init__( self.client = StateApiClient(address=ray.get_runtime_context().gcs_address) - if self.perspective_dashboard_enabled: - from raydar.dashboard.server import PerspectiveRayServer - - kwargs = { - "target": PerspectiveRayServer.bind(), - "name": "webserver", - "route_prefix": "/", - } + if dashboard == "local": + self._buffer = OpBuffer(max_rows_per_table=max_buffered_rows) + elif dashboard == "cluster": + from raydar.dashboard.serve import RaydarDeployment - if Version(ray.__version__) < Version("2.10"): - kwargs["port"] = int(os.environ.get("RAYDAR_PORT", "8000")) + self._handle = ray.serve.run(RaydarDeployment.bind(), name="raydar", route_prefix="/") + elif dashboard is not None: + raise ValueError(f"Unknown dashboard mode: {dashboard!r}") - self.proxy_server = setup_proxy_server(**kwargs) - self.proxy_server.remote( - "new", - self.perspective_table_name, + if dashboard is not None: + self.emit( { - "task_id": "string", - "user_defined_metadata": "string", - "attempt_number": "integer", - "name": "string", - "state": "string", - "job_id": "string", - "actor_id": "float", - "type": "string", - "func_or_class_name": "string", - "parent_task_id": "string", - "node_id": "string", - "worker_id": "string", - "error_type": "string", - "language": "string", - "placement_group_id": "float", - "creation_time_ms": "datetime", - "start_time_ms": "datetime", - "end_time_ms": "datetime", - "error_message": "string", - }, + "schemas": { + self.perspective_table_name: { + "task_id": "string", + "user_defined_metadata": "string", + "attempt_number": "integer", + "name": "string", + "state": "string", + "job_id": "string", + "actor_id": "float", + "type": "string", + "func_or_class_name": "string", + "parent_task_id": "string", + "node_id": "string", + "worker_id": "string", + "error_type": "string", + "language": "string", + "placement_group_id": "float", + "creation_time_ms": "datetime", + "start_time_ms": "datetime", + "end_time_ms": "datetime", + "error_message": "string", + } + } + } ) + def emit(self, batch: dict) -> None: + """Route a batch of table operations to whichever dashboard is configured.""" + if self._buffer is not None: + self._buffer.extend(batch) + elif self._handle is not None: + self._handle.apply.remote(batch) + + def drain(self) -> dict | None: + """Return buffered table operations for a dashboard running outside the cluster.""" + if self._buffer is None: + return None + return self._buffer.drain() + + def get_dashboard_mode(self) -> str | None: + return self.dashboard_mode + def callback(self, tasks: Iterable[ray.ObjectRef]) -> None: """A remote function used by this actor's processor actor attribute. Will be called by a separate actor with a collection of ray object references once those ObjectReferences are not in the "RUNNING" or @@ -208,13 +207,11 @@ def metadata_filter(task) -> bool: for task, metadata in completed_tasks: self.finished_tasks[task.task_id().hex()] = metadata - if self.perspective_dashboard_enabled: - self.update_perspective_dashboard(completed_tasks) - - def update_perspective_dashboard(self, completed_tasks) -> None: - """A helper function, which updates this actor's proxy_server attribute with processed data. + if self.dashboard_mode is not None: + self.publish_tasks(completed_tasks) - That proxy_server serves perspective tables which anticipate the data formats we provide. + def publish_tasks(self, completed_tasks) -> None: + """Emit completed task metadata as rows for the dashboard's task table. Args: completed_tasks: A list of tuples of the form (ObjectReference, TaskMetadata), where the ObjectReferences are neither Running nor Pending Assignment. @@ -243,7 +240,7 @@ def update_perspective_dashboard(self, completed_tasks) -> None: } for task, metadata in completed_tasks ] - self.proxy_server.remote("update", self.perspective_table_name, data) + self.emit({"updates": {self.perspective_table_name: data}}) async def process(self, obj_refs: Iterable[ray.ObjectRef], metadata: Iterable[str] | None = None, chunk_size: int = 25_000) -> None: """An asynchronous function to process a collection of Ray object references. @@ -294,14 +291,6 @@ def get_df(self) -> pl.DataFrame: ) return self.df - def get_proxy_server(self) -> ray.serve.handle.DeploymentHandle: - """A getter for this actors proxy server attribute. Can be used to create custom perspective visuals. - Returns: this actors proxy_server attribute - """ - if self.proxy_server: - return self.proxy_server - raise RuntimeError("This task_tracker has no active proxy_server.") - def save_df(self) -> None: """Saves the internally maintained dataframe of task related information from the ray GCS""" self.get_df() @@ -315,19 +304,38 @@ def clear_df(self) -> None: """Clears the internally maintained dataframe of task related information from the ray GCS""" self.df = None self.finished_tasks = {} - if self.perspective_dashboard_enabled: - self.proxy_server.remote("clear", self.perspective_table_name, None) + if self.dashboard_mode is not None: + self.emit({"cleared": [self.perspective_table_name]}) class RayTaskTracker: - def __init__(self, name: str = "task_tracker", namespace: str | None = None, **kwargs): + def __init__( + self, + name: str = "task_tracker", + namespace: str | None = None, + dashboard: DashboardMode | None = None, + dashboard_host: str = "127.0.0.1", + dashboard_port: int = 0, + dashboard_options: dict | None = None, + poll_interval: float = 0.5, + **kwargs, + ): """A utility to construct AsyncMetadataTracker actors. Wraps several remote AsyncMetadataTracker functions in a ray.get() call for convenience. Args: - Optional[name]: The named used to construct a AsyncMetadataTracker, also used to form the name of its AsyncMetadataTrackerCallback. - Optional[namespace]: Ray namespace for the AsyncMetadataTracker and its AsyncMetadataTrackerCallback. + name: The name used to construct a AsyncMetadataTracker, also used to form the name of its AsyncMetadataTrackerCallback. + namespace: Ray namespace for the AsyncMetadataTracker and its AsyncMetadataTrackerCallback. + dashboard: "local" serves the dashboard from this process and pulls updates over Ray, + so the cluster needs no inbound port. "cluster" serves it from Ray Serve, which + does. None disables the dashboard. + dashboard_host: Interface the "local" dashboard binds. + dashboard_port: Port the "local" dashboard binds, or 0 to pick a free one. + dashboard_options: Forwarded to :class:`~raydar.dashboard.dashboard.Dashboard` + (``title``, ``layout``, ``limit``). + poll_interval: Seconds between "local" dashboard polls of the tracker actor. + **kwargs: Forwarded to the AsyncMetadataTracker actor. """ if namespace is None: namespace = coolname.generate_slug(2) @@ -335,6 +343,8 @@ def __init__(self, name: str = "task_tracker", namespace: str | None = None, **k self.name = name self.namespace = namespace + self.dashboard_mode = dashboard + self.dashboard = None self.tracker = AsyncMetadataTracker.options( lifetime="detached", name=name, @@ -343,9 +353,36 @@ def __init__(self, name: str = "task_tracker", namespace: str | None = None, **k ).remote( name=name, namespace=namespace, + dashboard=dashboard, **kwargs, ) + if dashboard == "local": + from raydar.dashboard import LocalDashboard + + # `get_if_exists` returns a pre-existing actor and drops these constructor + # args, so a mode mismatch would otherwise show as an empty dashboard. + active = ray.get(self.tracker.get_dashboard_mode.remote()) + if active != dashboard: + logger.warning( + f'Actor "{name}" in namespace "{namespace}" already exists with dashboard={active!r}, ' + f"so it will not feed a {dashboard!r} dashboard. Use a new name or namespace." + ) + + self.dashboard = LocalDashboard( + drain=lambda: ray.get(self.tracker.drain.remote()), + host=dashboard_host, + port=dashboard_port, + poll_interval=poll_interval, + **(dashboard_options or {}), + ) + self.dashboard.start() + + @property + def dashboard_url(self) -> str | None: + """The URL of the local dashboard, or None when it is not running in this process.""" + return self.dashboard.url if self.dashboard else None + def process(self, object_refs: Iterable[ray.ObjectRef], metadata: Iterable[str] | None = None, chunk_size: int = 25_000) -> None: """A helper function, to send this object's AsyncMetadataTracker actor a collection of object references to track""" self.tracker.process.remote(object_refs, metadata=metadata, chunk_size=chunk_size) @@ -368,21 +405,21 @@ def clear(self) -> None: return ray.get(self.tracker.clear_df.remote()) def create_table(self, table_name: str, table_schema: dict[str, str]) -> None: - """Create a new perspective table using the proxy server used by the RayTaskTracker's AsyncMetadataTracker actor""" - proxy_server = self.proxy_server() - return proxy_server.remote("new", table_name, table_schema) + """Create a new perspective table on the dashboard""" + self.tracker.emit.remote({"schemas": {table_name: table_schema}}) def update_table(self, table_name: str, data: list[dict]) -> None: - """Update rows of perspective table held by the proxy server used by the RayTaskTracker's AsyncMetadataTracker actor""" - proxy_server = self.proxy_server() - return proxy_server.remote("update", table_name, data) - - def proxy_server(self) -> ray.serve.handle.DeploymentHandle: - """Fetch the proxy server used by this object's AsyncMetadataTracker actor""" - return ray.get(self.tracker.get_proxy_server.remote()) + """Append rows to a perspective table on the dashboard""" + self.tracker.emit.remote({"updates": {table_name: data}}) def exit(self) -> None: """Perform cleanup tasks, kill associated actors, and shutdown.""" + if self.dashboard is not None: + self.dashboard.stop() + self.dashboard = None ray.kill(ray.get_actor(name=self.name, namespace=self.namespace)) ray.kill(ray.get_actor(name=get_callback_actor_name(self.name), namespace=self.namespace)) - shutdown() + if self.dashboard_mode == "cluster": + # Delete only our own application; a global serve shutdown would take + # unrelated deployments with it. + delete_serve_app("raydar") diff --git a/raydar/tests/test_dashboard.py b/raydar/tests/test_dashboard.py new file mode 100644 index 0000000..d115e7f --- /dev/null +++ b/raydar/tests/test_dashboard.py @@ -0,0 +1,163 @@ +import json +import socket +import time +from datetime import UTC, date, datetime + +import pytest +from starlette.testclient import TestClient + +from raydar.dashboard import Dashboard, LocalDashboard, default_layout + +SCHEMA = {"a": "integer", "b": "string"} + + +@pytest.fixture +def dashboard(): + return Dashboard(title="test") + + +class TestDefaultLayout: + def test_no_tables_yields_an_empty_layout(self): + assert default_layout([]) == {} + + def test_each_table_gets_a_widget_and_a_viewer(self): + layout = default_layout(["x", "y"]) + assert layout["detail"]["main"]["widgets"] == ["x", "y"] + assert set(layout["viewers"]) == {"x", "y"} + assert layout["viewers"]["x"]["table"] == "x" + + +class TestDashboard: + def test_apply_creates_tables_and_counts_rows(self, dashboard): + dashboard.apply({"schemas": {"t": SCHEMA}, "updates": {"t": [{"a": 1, "b": "x"}]}}) + + assert dashboard.tables.names() == ["t"] + assert dashboard.state.tables == ["t"] + assert dashboard.state.rows == "1" + assert dashboard.state.status == "Live" + + def test_state_starts_empty(self, dashboard): + assert dashboard.state.tables == [] + assert dashboard.state.layout == {} + assert dashboard.state.status == "Waiting for data" + + def test_layout_follows_the_tables(self, dashboard): + dashboard.apply({"schemas": {"t": SCHEMA}}) + assert dashboard.state.layout == default_layout(["t"]) + + def test_layout_override_wins(self): + override = {"sizes": [1], "viewers": {}} + dashboard = Dashboard(layout=override) + dashboard.apply({"schemas": {"t": SCHEMA}}) + assert dashboard.state.layout == override + + def test_repeated_schemas_do_not_recreate_tables(self, dashboard): + dashboard.apply({"schemas": {"t": SCHEMA}, "updates": {"t": [{"a": 1, "b": "x"}]}}) + dashboard.apply({"schemas": {"t": SCHEMA}, "updates": {"t": [{"a": 2, "b": "y"}]}}) + assert dashboard.state.rows == "2" + + def test_clear_empties_the_table(self, dashboard): + dashboard.apply({"schemas": {"t": SCHEMA}, "updates": {"t": [{"a": 1, "b": "x"}]}}) + dashboard.apply({"cleared": ["t"]}) + + assert dashboard.tables.names() == ["t"] + assert dashboard.tables.total_rows() == 0 + assert dashboard.state.rows == "0" + + def test_update_of_an_unknown_table_raises(self, dashboard): + with pytest.raises(KeyError): + dashboard.apply({"updates": {"nope": [{"a": 1}]}}) + + def test_limit_caps_retained_rows(self): + dashboard = Dashboard(limit=2) + dashboard.apply({"schemas": {"t": SCHEMA}, "updates": {"t": [{"a": i, "b": "x"} for i in range(5)]}}) + + assert dashboard.tables.total_rows() == 2 + assert dashboard.state.rows == "2" + + def test_an_empty_batch_does_not_touch_the_synced_state(self, dashboard): + dashboard.apply({"schemas": {"t": SCHEMA}}) + before = dashboard.state.model_copy(deep=True) + + # Schemas are replayed on every drain, so this is the steady-state batch. + dashboard.apply({"schemas": {"t": SCHEMA}, "updates": {}, "cleared": []}) + assert dashboard.state == before + + def test_datetime_values_are_accepted(self, dashboard): + # Perspective encodes rows as JSON, which rejects datetime outright. + moment = datetime(2026, 8, 21, 12, 30, 15, tzinfo=UTC) + dashboard.apply( + { + "schemas": {"t": {"when": "datetime", "day": "date"}}, + "updates": {"t": [{"when": moment, "day": date(2026, 8, 21)}]}, + } + ) + + assert dashboard.tables.total_rows() == 1 + stored = dashboard.tables._tables["t"].view().to_columns()["when"][0] + assert stored == int(moment.timestamp() * 1000) + + +class TestDashboardApp: + def test_page_and_assets_are_served(self, dashboard): + with TestClient(dashboard.app) as client: + page = client.get("/") + assert page.status_code == 200 + # spaday's source/installed asset detection is pinned; a regression there 404s the runtime. + for asset in ("/js/cdn/index.js", "/components/perspective/cdn/index.js", "/components/webawesome/cdn/index.js"): + assert client.get(asset).status_code == 200, asset + + def test_tree_wires_the_panel_to_the_state_model(self, dashboard): + with TestClient(dashboard.app) as client: + tree = json.dumps(client.get("/tree.json").json()) + assert "perspective-panel" in tree + for path in ("rd.tables", "rd.layout", "rd.status"): + assert path in tree + + def test_state_is_pushed_over_the_transports_socket(self, dashboard): + dashboard.apply({"schemas": {"t": SCHEMA}}) + with TestClient(dashboard.app) as client, client.websocket_connect("/ws") as ws: + snapshot = json.loads(ws.receive_text()) + assert snapshot["t"] == "snapshot" + assert snapshot["type"] == "DashboardState" + + def test_perspective_has_its_own_socket(self, dashboard): + with TestClient(dashboard.app) as client, client.websocket_connect("/perspective") as ws: + assert ws is not None + + +class TestLocalDashboard: + def test_poll_loop_survives_a_bad_batch(self): + batches = [ + {"updates": {"missing": [{"a": 1}]}}, # unknown table -> KeyError + {"schemas": {"t": SCHEMA}, "updates": {"t": [{"a": 1, "b": "x"}]}}, + ] + local = LocalDashboard(drain=lambda: batches.pop(0) if batches else {}, poll_interval=0.05) + local.start() + try: + deadline = time.time() + 10 + while time.time() < deadline and local.dashboard.tables.names() != ["t"]: + time.sleep(0.1) + # The good batch only lands if the failed one did not kill the loop. + assert local.dashboard.tables.names() == ["t"] + assert local.dashboard.tables.total_rows() == 1 + finally: + local.stop() + + def test_stop_is_idempotent_and_releases_the_port(self): + local = LocalDashboard(drain=lambda: None, poll_interval=0.05) + port = local.port + local.start() + local.stop() + local.stop() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.bind(("127.0.0.1", port)) + + def test_an_unstarted_dashboard_releases_its_socket(self): + local = LocalDashboard(drain=lambda: None) + port = local.port + local.stop() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.bind(("127.0.0.1", port)) diff --git a/raydar/tests/test_ops.py b/raydar/tests/test_ops.py new file mode 100644 index 0000000..04b62ee --- /dev/null +++ b/raydar/tests/test_ops.py @@ -0,0 +1,63 @@ +import pytest + +from raydar.ops import OpBuffer + + +class TestOpBuffer: + def test_drain_returns_schemas_and_updates(self): + buffer = OpBuffer() + buffer.new_table("t", {"a": "integer"}) + buffer.update("t", [{"a": 1}, {"a": 2}]) + + batch = buffer.drain() + assert batch == {"schemas": {"t": {"a": "integer"}}, "updates": {"t": [{"a": 1}, {"a": 2}]}, "cleared": []} + + def test_drain_replays_schemas_but_not_rows(self): + buffer = OpBuffer() + buffer.new_table("t", {"a": "integer"}) + buffer.update("t", [{"a": 1}]) + buffer.drain() + + batch = buffer.drain() + assert batch["schemas"] == {"t": {"a": "integer"}} + assert batch["updates"] == {} + + def test_update_accepts_a_single_row(self): + buffer = OpBuffer() + buffer.update("t", {"a": 1}) + assert buffer.drain()["updates"] == {"t": [{"a": 1}]} + + def test_new_table_does_not_replace_an_existing_schema(self): + buffer = OpBuffer() + buffer.new_table("t", {"a": "integer"}) + buffer.new_table("t", {"b": "string"}) + assert buffer.drain()["schemas"] == {"t": {"a": "integer"}} + + def test_rows_are_capped_per_table(self): + buffer = OpBuffer(max_rows_per_table=3) + buffer.update("t", [{"a": i} for i in range(10)]) + assert buffer.drain()["updates"]["t"] == [{"a": 7}, {"a": 8}, {"a": 9}] + + def test_clear_drops_pending_rows(self): + buffer = OpBuffer() + buffer.update("t", [{"a": 1}]) + buffer.clear("t") + + batch = buffer.drain() + assert batch["updates"] == {} + assert batch["cleared"] == ["t"] + + @pytest.mark.parametrize("batch", [{}, {"updates": {"t": []}}, {"schemas": None, "cleared": None}]) + def test_extend_tolerates_sparse_batches(self, batch): + buffer = OpBuffer() + buffer.extend(batch) + assert buffer.drain() == {"schemas": {}, "updates": {}, "cleared": []} + + def test_extend_round_trips_a_drained_batch(self): + source = OpBuffer() + source.new_table("t", {"a": "integer"}) + source.update("t", [{"a": 1}]) + + target = OpBuffer() + target.extend(source.drain()) + assert target.drain() == {"schemas": {"t": {"a": "integer"}}, "updates": {"t": [{"a": 1}]}, "cleared": []} diff --git a/raydar/tests/test_serve.py b/raydar/tests/test_serve.py new file mode 100644 index 0000000..e6adad2 --- /dev/null +++ b/raydar/tests/test_serve.py @@ -0,0 +1,31 @@ +import httpx +import pytest +import ray + +from raydar.dashboard.serve import RaydarDeployment + +PORT = 8899 +BASE = f"http://127.0.0.1:{PORT}" + + +@pytest.fixture(scope="module") +def deployment(unittest_ray_cluster): + ray.serve.start(http_options={"host": "127.0.0.1", "port": PORT}) + handle = ray.serve.run(RaydarDeployment.bind(), name="raydar", route_prefix="/") + yield handle + ray.serve.shutdown() + + +class TestRaydarDeployment: + def test_the_app_is_built_on_the_replica(self, deployment): + # Regression: passing the app to `ingress` makes Ray pickle it, which fails + # on the Perspective server and the transports store it closes over. + assert httpx.get(BASE + "/").status_code == 200 + + def test_assets_are_served(self, deployment): + for asset in ("/tree.json", "/js/cdn/index.js", "/components/perspective/cdn/index.js"): + assert httpx.get(BASE + asset).status_code == 200, asset + + def test_apply_reaches_the_replica(self, deployment): + deployment.apply.remote({"schemas": {"t": {"a": "integer"}}, "updates": {"t": [{"a": 1}]}}).result() + assert httpx.get(BASE + "/").status_code == 200 diff --git a/raydar/tests/test_task_tracker.py b/raydar/tests/test_task_tracker.py index 655f5e9..08265cc 100644 --- a/raydar/tests/test_task_tracker.py +++ b/raydar/tests/test_task_tracker.py @@ -1,10 +1,10 @@ import time +import httpx import pytest import ray -import requests -from raydar import RayTaskTracker, setup_proxy_server +from raydar import RayTaskTracker @ray.remote @@ -13,29 +13,60 @@ def do_some_work(): return True +def wait_for(fetch, ready, timeout=120, interval=0.5): + """Poll `fetch` until `ready` accepts the value, then return it.""" + deadline = time.time() + timeout + value = fetch() + while not ready(value) and time.time() < deadline: + time.sleep(interval) + value = fetch() + return value + + @pytest.mark.usefixtures("unittest_ray_cluster") class TestRayTaskTracker: def test_construction_and_dataframe(self): - task_tracker = RayTaskTracker(enable_perspective_dashboard=True) - assert len(task_tracker.namespace.split("-")) == 2 - refs = [do_some_work.remote() for _ in range(10)] - task_tracker.process(refs) - time.sleep(30) - df = task_tracker.get_df() - assert df[["name", "state"]].row(0) == ("do_some_work", "FINISHED") - - def test_get_proxy_server(self): - from raydar.dashboard.server import PerspectiveRayServer - - kwargs = { - "target": PerspectiveRayServer.bind(), - "name": "webserver", - "route_prefix": "/", - } - server = setup_proxy_server(**kwargs) - server.remote("new", "test_table", {"a": "string", "b": "integer", "c": "float", "d": "datetime"}) - time.sleep(2) - server.remote("update", "test_table", [{"a": "foo", "b": 1, "c": 1.0, "d": time.time()}]) - time.sleep(2) - response = requests.get("http://localhost:8000/tables") - assert eval(response.text) == ["test_table"] + task_tracker = RayTaskTracker(dashboard="local") + try: + assert len(task_tracker.namespace.split("-")) == 2 + refs = [do_some_work.remote() for _ in range(10)] + task_tracker.process(refs) + + # Metadata arrives via GCS polling, so wait on the result rather than + # on a fixed sleep, which was slow on a fast box and flaky on a slow one. + df = wait_for(task_tracker.get_df, lambda d: not d.is_empty()) + assert not df.is_empty(), "tracker recorded no finished tasks" + assert df[["name", "state"]].row(0) == ("do_some_work", "FINISHED") + finally: + task_tracker.dashboard.stop() + + def test_dashboard_is_off_by_default(self): + task_tracker = RayTaskTracker() + assert task_tracker.dashboard_url is None + + def test_dashboard_options_reach_the_dashboard(self): + layout = {"sizes": [1], "viewers": {}} + task_tracker = RayTaskTracker(dashboard="local", dashboard_options={"title": "custom", "layout": layout}) + try: + dashboard = task_tracker.dashboard.dashboard + dashboard.apply({"schemas": {"t": {"a": "integer"}}}) + assert dashboard.state.layout == layout + assert "custom" in httpx.get(task_tracker.dashboard_url).text + finally: + task_tracker.dashboard.stop() + + def test_local_dashboard_serves_tables_pulled_from_the_actor(self): + task_tracker = RayTaskTracker(dashboard="local") + try: + assert task_tracker.dashboard_url.startswith("http://127.0.0.1:") + task_tracker.create_table("custom", {"a": "string", "b": "integer"}) + task_tracker.update_table("custom", [{"a": "foo", "b": 1}]) + + tables = task_tracker.dashboard.dashboard.tables + expected = ["custom", "task_tracker_data"] + names = wait_for(lambda: sorted(tables.names()), lambda n: n == expected, timeout=60) + + assert names == expected + assert httpx.get(task_tracker.dashboard_url).status_code == 200 + finally: + task_tracker.dashboard.stop()