From d23999632824ba43a7e619f54104f1eeacf416f6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 11:00:42 +0000 Subject: [PATCH 1/3] Add DICOM data loader tutorial (Rust/WASM Foxglove extension) Co-authored-by: Mateusz Sadowski --- README.md | 2 + foxglove_sdk/dicom_data_loader/.gitignore | 8 + .../dicom_data_loader/.prettierrc.yaml | 5 + foxglove_sdk/dicom_data_loader/CHANGELOG.md | 5 + foxglove_sdk/dicom_data_loader/README.md | 77 + foxglove_sdk/dicom_data_loader/config.ts | 9 + foxglove_sdk/dicom_data_loader/data/README.md | 3 + .../dicom_data_loader/eslint.config.js | 23 + .../foxglove_layouts/dicom_layout.json | 151 + .../dicom_data_loader/package-lock.json | 5799 +++++++++++++++++ foxglove_sdk/dicom_data_loader/package.json | 33 + .../dicom_data_loader/rust/Cargo.toml | 23 + .../dicom_data_loader/rust/src/lib.rs | 20 + .../dicom_data_loader/rust/src/loader.rs | 119 + .../dicom_data_loader/rust/src/messages.rs | 301 + .../dicom_data_loader/rust/src/parse.rs | 372 ++ .../dicom_data_loader/rust/src/render.rs | 617 ++ .../dicom_data_loader/rust/src/study.rs | 1145 ++++ .../scripts/download_sample_data.py | 61 + .../scripts/requirements.txt | 1 + .../dicom_data_loader/scripts/wasm_smoke.mjs | 357 + .../dicom_data_loader/src/globals.d.ts | 5 + foxglove_sdk/dicom_data_loader/src/index.ts | 12 + foxglove_sdk/dicom_data_loader/tsconfig.json | 26 + 24 files changed, 9174 insertions(+) create mode 100644 foxglove_sdk/dicom_data_loader/.gitignore create mode 100644 foxglove_sdk/dicom_data_loader/.prettierrc.yaml create mode 100644 foxglove_sdk/dicom_data_loader/CHANGELOG.md create mode 100644 foxglove_sdk/dicom_data_loader/README.md create mode 100644 foxglove_sdk/dicom_data_loader/config.ts create mode 100644 foxglove_sdk/dicom_data_loader/data/README.md create mode 100644 foxglove_sdk/dicom_data_loader/eslint.config.js create mode 100644 foxglove_sdk/dicom_data_loader/foxglove_layouts/dicom_layout.json create mode 100644 foxglove_sdk/dicom_data_loader/package-lock.json create mode 100644 foxglove_sdk/dicom_data_loader/package.json create mode 100644 foxglove_sdk/dicom_data_loader/rust/Cargo.toml create mode 100644 foxglove_sdk/dicom_data_loader/rust/src/lib.rs create mode 100644 foxglove_sdk/dicom_data_loader/rust/src/loader.rs create mode 100644 foxglove_sdk/dicom_data_loader/rust/src/messages.rs create mode 100644 foxglove_sdk/dicom_data_loader/rust/src/parse.rs create mode 100644 foxglove_sdk/dicom_data_loader/rust/src/render.rs create mode 100644 foxglove_sdk/dicom_data_loader/rust/src/study.rs create mode 100644 foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py create mode 100644 foxglove_sdk/dicom_data_loader/scripts/requirements.txt create mode 100644 foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs create mode 100644 foxglove_sdk/dicom_data_loader/src/globals.d.ts create mode 100644 foxglove_sdk/dicom_data_loader/src/index.ts create mode 100644 foxglove_sdk/dicom_data_loader/tsconfig.json diff --git a/README.md b/README.md index d086a04..6d9d34a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Below is a list of all tutorials available in this repository: - 📊 [Visualize](https://app.foxglove.dev/~/view?ds=foxglove-sample-stream&ds.recordingId=vqKKQcot421Kwg84&ds.overrideLayoutId=b7513959-1d46-4a89-bc24-1584d9677ca1&ds.start=2023-09-01T13:19:45.047438263Z&ds.end=2023-09-01T13:20:15.047438263Z) ## Foxglove SDK +### [Play DICOM CT series in Foxglove](foxglove_sdk/dicom_data_loader/README.md) +- 📝 A Rust data loader that opens uncompressed DICOM files and plays slices or breathing phases on the timeline. ### [Using Foxglove to Visualize Ethernet/IP data](foxglove_sdk/ethernet_ip_integration/README.md) - 📝 Using Foxglove data, it's easier than ever to stream time series data. In this project, we show you how. - 🔗 [Related Blog Post](https://foxglove.dev/blog/use-foxglove-sdk-for-real-time-industrial-plc-data-visualization-and-playback) diff --git a/foxglove_sdk/dicom_data_loader/.gitignore b/foxglove_sdk/dicom_data_loader/.gitignore new file mode 100644 index 0000000..a718cb4 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/.gitignore @@ -0,0 +1,8 @@ +dist +node_modules +rust/Cargo.lock +rust/target +data/* +!data/README.md +*.foxe +scripts/.smoke diff --git a/foxglove_sdk/dicom_data_loader/.prettierrc.yaml b/foxglove_sdk/dicom_data_loader/.prettierrc.yaml new file mode 100644 index 0000000..e57cc20 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/.prettierrc.yaml @@ -0,0 +1,5 @@ +arrowParens: always +printWidth: 100 +trailingComma: "all" +tabWidth: 2 +semi: true diff --git a/foxglove_sdk/dicom_data_loader/CHANGELOG.md b/foxglove_sdk/dicom_data_loader/CHANGELOG.md new file mode 100644 index 0000000..20dc29b --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial DICOM data loader: sweep through a CT series, or play a 4D breathing study, on the Foxglove timeline. diff --git a/foxglove_sdk/dicom_data_loader/README.md b/foxglove_sdk/dicom_data_loader/README.md new file mode 100644 index 0000000..6eaf953 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/README.md @@ -0,0 +1,77 @@ +--- +title: "Play DICOM CT series in Foxglove" +short_description: "A Rust data loader that opens uncompressed DICOM files and plays slices or breathing phases on the timeline." +--- + +# Play DICOM CT series in Foxglove + +Foxglove does not open DICOM. This tutorial is a data loader extension, written in Rust and compiled to WebAssembly, that does. Drag a folder of `.dcm` files into Foxglove and the timeline becomes the navigation control: a sweep through axial slices, or a short animation of a patient breathing. No MCAP conversion and no custom panel. + +The loader publishes lung-window images, a 3D point cloud of bone and lung, a plot of lung area or volume, and a metadata message. + +## Prerequisites + +- Rust stable, with the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`) +- Node.js 18 or newer +- Python 3, only for downloading the sample series + +## Download sample data + +Both series are public, CC BY 3.0, and hosted by the NCI Imaging Data Commons. The script uses the `idc-index` package. Nothing is committed to the repository. + +```bash +pip install -r scripts/requirements.txt +python scripts/download_sample_data.py --dataset chest # one 133-slice chest CT +python scripts/download_sample_data.py --dataset breathing # ten respiratory phases +python scripts/download_sample_data.py --dataset all +``` + +Files are written under `data/chest` and `data/breathing`. + +## Build and install + +From this directory: + +```bash +npm install +npm run package +``` + +`npm run package` builds the release WebAssembly module and writes `foxglove.foxglove-dicom-data-loader-0.1.0.foxe`. In Foxglove, open Settings → Extensions and install that file. For the desktop app you can instead run `npm run local-install`. + +## Open the data + +Open one dataset at a time. Select or drag every `.dcm` file from `data/chest` or from `data/breathing` together. The loader only sees the files you hand it, so a partial selection is a partial volume. + +Import `foxglove_layouts/dicom_layout.json` for a layout with axial, coronal, and sagittal images, a 3D view, the stats plot, and the metadata panel. The coronal and sagittal panels subscribe to `/dicom/coronal/annotations` and `/dicom/sagittal/annotations`. Those topics exist in sweep mode and draw a line at the current axial slice. 4D mode does not publish them. + +Press play. + +- **Chest CT** is sweep mode. Each frame is the next axial slice, inferior to superior, at 100 ms per slice (about 13 seconds). Coronal and sagittal views are the centre multi-planar reconstruction, published once. The line on those views tracks the slice. The 3D panel shows bone, lung, and the current slice as a point cloud. +- **4D lung** is breathing mode. The loader finds ten series that share a frame of reference and the same geometry, ordered by the phase percentage in `SeriesDescription`. Each frame is one phase at 400 ms, and the cycle repeats three times so playback looks continuous. The centre coronal image and the lung cloud update every phase, so the diaphragm and lung volume move. Bone, the bounding box, and the metadata message stay at the start of the timeline; Foxglove keeps them visible by backfill when you seek. + +## How it works + +`rust/src/lib.rs` exports a [`DataLoader`](https://docs.rs/foxglove_data_loader). Foxglove calls `initialize` with the paths you opened. The loader reads each file through the host `reader` interface, keeps uncompressed little-endian monochrome slices, and groups them by `SeriesInstanceUID`. + +Slices in a series are ordered along the slice normal (`ImagePositionPatient` dotted with the cross product of the image orientation). The first frame is the inferior end. + +Mode detection: + +- **4D** when at least two series share `FrameOfReferenceUID` and have the same rows, columns, and slice count. Phases are ordered by a percentage parsed from the series description, such as `Gated, 40.0%`, and otherwise by series number. +- **Sweep** otherwise, using the series with the most slices. Extra series produce a warning. + +`StudyDate` and `StudyTime` become the timeline origin, interpreted as UTC. If they do not parse, the origin is zero. Messages are not materialised up front. `create_iter` yields one frame at a time, and `get_backfill` returns the latest message at or before the seek time so panels are not empty when playback starts in the middle. + +Images are windowed with a CT lung window (level -600 HU, width 1500) and encoded as `mono8`. Coronal and sagittal views resample along the stack so the spacing matches the in-plane pixels, with superior at the top. Positions are DICOM LPS millimetres converted to metres in a `patient` frame whose origin is the centre of the volume, so the anatomy sits at the origin of Foxglove's z-up view. Every 4D phase uses the first phase's origin. Recentering each phase would hide the breathing motion. + +The lung mask is a demo heuristic. A flood fill from the image border marks air connected to the outside of the body (voxels below -400 HU). Voxels that are not outside air and sit between -1000 and -400 HU count as lung. Body is everything that is not outside air. Bone is HU of 250 or higher. Point clouds are stride-subsampled so each cloud stays at or under 300,000 points. + +## Limitations + +Only uncompressed little-endian transfer syntaxes are read (Implicit VR and Explicit VR). JPEG and other encapsulated pixel data are skipped, with a warning. Multi-frame files are skipped. The lung and body masks are not a segmentation. The loader does not publish patient name, ID, or other identifying fields. + +## Data + +- Chest CT: LIDC-IDRI, series `1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192`. Armato SG 3rd, McLennan G, Bidaut L, McNitt-Gray MF, Meyer CR, Reeves AP, Zhao B, Aberle DR, Henschke CI, Hoffman EA, Kazerooni EA, MacMahon H, Van Beek EJR, Yankelevitz D, et al.: The Lung Image Database Consortium (LIDC) and Image Database Resource Initiative (IDRI): A completed reference database of lung nodules on CT scans. Medical Physics, 38: 915--931, 2011. [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). Data from [The Cancer Imaging Archive](https://www.cancerimagingarchive.net/). +- Breathing CT: 4D-Lung, patient `100_HM10395`, study S100. Hugo GD, Weiss E, Sleeman WC, Balik S, Keall PJ, Lu J, Williamson JF. (2016). Data from 4D Lung Imaging of NSCLC Patients. The Cancer Imaging Archive. [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). diff --git a/foxglove_sdk/dicom_data_loader/config.ts b/foxglove_sdk/dicom_data_loader/config.ts new file mode 100644 index 0000000..3d51558 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/config.ts @@ -0,0 +1,9 @@ +module.exports = { + webpack: (config) => { + config.module.rules.push({ + test: /\.wasm$/i, + type: "asset/inline", + }); + return config; + }, +}; diff --git a/foxglove_sdk/dicom_data_loader/data/README.md b/foxglove_sdk/dicom_data_loader/data/README.md new file mode 100644 index 0000000..d69d57a --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/data/README.md @@ -0,0 +1,3 @@ +Sample CT series downloaded by `scripts/download_sample_data.py` land in this directory. + +`data/chest` is one LIDC-IDRI axial series. `data/breathing` is ten 4D-Lung phases. The files are not part of the repository. diff --git a/foxglove_sdk/dicom_data_loader/eslint.config.js b/foxglove_sdk/dicom_data_loader/eslint.config.js new file mode 100644 index 0000000..63cc3a2 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/eslint.config.js @@ -0,0 +1,23 @@ +// @ts-check + +const foxglove = require("@foxglove/eslint-plugin"); +const globals = require("globals"); +const tseslint = require("typescript-eslint"); + +module.exports = tseslint.config({ + files: ["src/**/*.ts", "src/**/*.tsx"], + extends: [foxglove.configs.base, foxglove.configs.react, foxglove.configs.typescript], + languageOptions: { + globals: { + ...globals.es2020, + ...globals.browser, + }, + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, + }, + rules: { + "react-hooks/exhaustive-deps": "error", + }, +}); diff --git a/foxglove_sdk/dicom_data_loader/foxglove_layouts/dicom_layout.json b/foxglove_sdk/dicom_data_loader/foxglove_layouts/dicom_layout.json new file mode 100644 index 0000000..2a40cfb --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/foxglove_layouts/dicom_layout.json @@ -0,0 +1,151 @@ +{ + "configById": { + "Image!axial": { + "foxglovePanelTitle": "Axial", + "imageMode": { + "imageTopic": "/dicom/axial", + "minValue": 0, + "maxValue": 255 + }, + "followMode": "follow-none", + "synchronize": false + }, + "Image!coronal": { + "foxglovePanelTitle": "Coronal", + "imageMode": { + "imageTopic": "/dicom/coronal", + "minValue": 0, + "maxValue": 255, + "annotations": { + "/dicom/coronal/annotations": { + "visible": true + } + } + }, + "followMode": "follow-none", + "synchronize": false + }, + "Image!sagittal": { + "foxglovePanelTitle": "Sagittal", + "imageMode": { + "imageTopic": "/dicom/sagittal", + "minValue": 0, + "maxValue": 255, + "annotations": { + "/dicom/sagittal/annotations": { + "visible": true + } + } + }, + "followMode": "follow-none", + "synchronize": false + }, + "3D!volume": { + "foxglovePanelTitle": "Volume", + "followTf": "patient", + "followMode": "follow-pose", + "fixedFrame": "world", + "scene": { + "backgroundColor": "#111111" + }, + "cameraState": { + "perspective": true, + "distance": 1.6, + "phi": 60, + "thetaOffset": 35, + "fovy": 50, + "near": 0.01, + "far": 20, + "target": [0, 0, 0], + "targetOffset": [0, 0, 0] + }, + "topics": { + "/dicom/bone": { + "visible": true, + "colorMode": "rgba-fields", + "pointSize": 2 + }, + "/dicom/lungs": { + "visible": true, + "colorMode": "rgba-fields", + "pointSize": 2 + }, + "/dicom/slice": { + "visible": true, + "colorMode": "rgba-fields", + "pointSize": 2 + }, + "/dicom/scene": { + "visible": true + } + } + }, + "Plot!stats": { + "foxglovePanelTitle": "Stats", + "paths": [ + { + "value": "/dicom/stats.mean_hu", + "enabled": true, + "timestampMethod": "receiveTime", + "color": "#f7df71" + }, + { + "value": "/dicom/stats.lung_area_cm2", + "enabled": true, + "timestampMethod": "receiveTime", + "color": "#4e98e2" + }, + { + "value": "/dicom/stats.lung_volume_ml", + "enabled": true, + "timestampMethod": "receiveTime", + "color": "#5cd6a9" + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + }, + "RawMessages!meta": { + "foxglovePanelTitle": "Metadata", + "topicPath": "/dicom/metadata", + "diffEnabled": false, + "showFullMessageForDiff": false + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "layout": { + "first": { + "first": "Image!axial", + "second": { + "first": "Image!coronal", + "second": "Image!sagittal", + "direction": "row" + }, + "direction": "column", + "splitPercentage": 55 + }, + "second": { + "first": "3D!volume", + "second": { + "first": "Plot!stats", + "second": "RawMessages!meta", + "direction": "column", + "splitPercentage": 62 + }, + "direction": "column", + "splitPercentage": 68 + }, + "direction": "row", + "splitPercentage": 48 + } +} diff --git a/foxglove_sdk/dicom_data_loader/package-lock.json b/foxglove_sdk/dicom_data_loader/package-lock.json new file mode 100644 index 0000000..36ac7b1 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/package-lock.json @@ -0,0 +1,5799 @@ +{ + "name": "foxglove-dicom-data-loader", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "foxglove-dicom-data-loader", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@foxglove/eslint-plugin": "2.0.0", + "@foxglove/extension": "3.2.1", + "@foxglove/schemas": "1.6.4", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@types/wicg-file-system-access": "2023.10.6", + "create-foxglove-extension": "1.3.2", + "eslint": "9.15.0", + "prettier": "3.3.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "typescript": "5.7.2" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", + "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@foxglove/eslint-plugin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@foxglove/eslint-plugin/-/eslint-plugin-2.0.0.tgz", + "integrity": "sha512-0HLMt84d1pE3Z5BfK3ExXCQyoo2lw4teCbYaUx10UuU6RYO3NSm9JJ09+LqPO93qyXdp80PQn5JMEKMT/iYVKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/compat": "^1", + "@eslint/js": "^9", + "@typescript-eslint/utils": "^8", + "eslint-config-prettier": "^9", + "eslint-plugin-es": "^4", + "eslint-plugin-filenames": "^1", + "eslint-plugin-import": "^2", + "eslint-plugin-jest": "^28", + "eslint-plugin-prettier": "^5", + "eslint-plugin-react": "^7", + "eslint-plugin-react-hooks": "^5", + "tsutils": "^3", + "typescript-eslint": "^8" + }, + "peerDependencies": { + "eslint": "^9" + } + }, + "node_modules/@foxglove/extension": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@foxglove/extension/-/extension-3.2.1.tgz", + "integrity": "sha512-HKULuW+uAijXN/VLJcuOWd4Zz/yiqo9Hw27kVyucv/r6lzaWKSgMEdHiaCgFIGvLD6Cd2hpE10GKXdgVA7hoqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@foxglove/message-definition": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@foxglove/message-definition/-/message-definition-0.5.0.tgz", + "integrity": "sha512-B/2GvWn4wRSsPDywcmy6uNXgAfZRhiOAnmy3qAaS6MHVqiHQqs/RoDsoW6Gw9sXSweJpeH5qAqAihvm1pR9Thg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@foxglove/rosmsg": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@foxglove/rosmsg/-/rosmsg-5.0.5.tgz", + "integrity": "sha512-+qY6xW+4HdbPI3ItAlFFMjXgAiplXyRsv+kqdBk8OcWGH38BmEd7s6v/VnCkbYovAoB5rfR5giuKfmPRNzwbrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@foxglove/message-definition": "0.5.0", + "md5-typescript": "^1.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@foxglove/rosmsg-msgs-common": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@foxglove/rosmsg-msgs-common/-/rosmsg-msgs-common-3.2.3.tgz", + "integrity": "sha512-i+pdyOAGb7DHDaIRON+zGy2izW3kQewRImOcHGzGWib3dOaTvoI5RoapOykDQyND9L9E8QbLntCSSAKHBBfOsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@foxglove/message-definition": "0.5.0", + "@foxglove/rosmsg": "^5.0.5" + } + }, + "node_modules/@foxglove/schemas": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@foxglove/schemas/-/schemas-1.6.4.tgz", + "integrity": "sha512-lKjF4QEYN7hSS64bG5izdix3ngfnWqaHy6XHVeUZ2gBy9QIuzFGAeIATXXbw1I2ICY6Of+Ri1Iu4OAAyizGiGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@foxglove/rosmsg-msgs-common": "^3.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/wicg-file-system-access": { + "version": "2023.10.6", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.6.tgz", + "integrity": "sha512-YO/183gNRzZFSdKu+ikkD7ambAj4PhgjFAF2A/Mw/7wroSF6ne8r804RkpZzqrJ/F6DO2/IYlQF/ULOZ/bhKyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", + "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/type-utils": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz", + "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz", + "integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.1", + "@typescript-eslint/types": "^8.70.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz", + "integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz", + "integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz", + "integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz", + "integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz", + "integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.1", + "@typescript-eslint/tsconfig-utils": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz", + "integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz", + "integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.1.tgz", + "integrity": "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.25", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.438", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001812", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001812.tgz", + "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-foxglove-extension": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/create-foxglove-extension/-/create-foxglove-extension-1.3.2.tgz", + "integrity": "sha512-JR1CwWdNQS/NpAJ5kB7LOIdEtH6WR8OFto/2OYS3xGgeCcf0MhNHhmaA3aIyv4qQO1iCzFmh4Yo3e30vAlc/mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "12.1.0", + "jszip": "3.10.1", + "mkdirp": "3.0.1", + "ncp": "2.0.0", + "node-fetch": "2.7.0", + "path-browserify": "1.0.1", + "rimraf": "6.1.2", + "sanitize-filename": "1.6.3", + "tar": "7.5.8", + "ts-loader": "9.5.1", + "webpack": "5.96.1" + }, + "bin": { + "create-foxglove-extension": "dist/bin/create-foxglove-extension.js", + "foxglove-extension": "dist/bin/foxglove-extension.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.439", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.439.tgz", + "integrity": "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.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", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-filenames": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz", + "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.upperfirst": "4.3.1" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/md5-typescript/-/md5-typescript-1.0.5.tgz", + "integrity": "sha512-ovAc4EtiNt2dY8JPhPr/wkC9h4U5k/nuClNVcG0Ga3V1rMlYpAY24ZaaymFXJlz+ccJ6UMPo3FSaVKe7czBsXw==", + "dev": true, + "license": "ISC" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.8.tgz", + "integrity": "sha512-SYkBtK99u0yXa+IWL0JRzzcl7RxNpvX/U08Z+8DKnysfno7M+uExnTZH8K+VGgShf2qFPKtbNr9QBl8n7WBP6Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.1.tgz", + "integrity": "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.1", + "@typescript-eslint/parser": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.3.tgz", + "integrity": "sha512-F/zS5rEcCKxS6hrhxkLjOzblTsuv89rkuDyP+vToquds6KfPJ3fDqg9UNzbtJcA2AgXZOLMYLQqZpuyE5zDfxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/foxglove_sdk/dicom_data_loader/package.json b/foxglove_sdk/dicom_data_loader/package.json new file mode 100644 index 0000000..d762824 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/package.json @@ -0,0 +1,33 @@ +{ + "name": "foxglove-dicom-data-loader", + "displayName": "DICOM data loader", + "description": "Play uncompressed DICOM CT series on the Foxglove timeline", + "publisher": "Foxglove", + "version": "0.1.0", + "license": "MIT", + "main": "./dist/extension.js", + "scripts": { + "build": "npm run build:wasm && foxglove-extension build", + "build:wasm": "cd rust && cargo build --release --target wasm32-unknown-unknown", + "foxglove:prepublish": "npm run build:wasm && foxglove-extension build --mode production", + "lint:ci": "eslint --report-unused-disable-directives .", + "lint": "eslint --report-unused-disable-directives --fix .", + "local-install": "foxglove-extension install", + "package": "foxglove-extension package", + "pretest": "foxglove-extension pretest" + }, + "devDependencies": { + "@foxglove/eslint-plugin": "2.0.0", + "@foxglove/extension": "3.2.1", + "@foxglove/schemas": "1.6.4", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@types/wicg-file-system-access": "2023.10.6", + "create-foxglove-extension": "1.3.2", + "eslint": "9.15.0", + "prettier": "3.3.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "typescript": "5.7.2" + } +} diff --git a/foxglove_sdk/dicom_data_loader/rust/Cargo.toml b/foxglove_sdk/dicom_data_loader/rust/Cargo.toml new file mode 100644 index 0000000..af2bf0c --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "foxglove-dicom-data-loader" +version = "0.1.0" +edition = "2024" +description = "Foxglove data loader that plays DICOM CT series on the timeline" +license = "MIT" +publish = false + +[lib] +name = "foxglove_dicom_data_loader" +crate-type = ["cdylib", "rlib"] + +[dependencies] +anyhow = "1.0" +dicom-core = "0.8" +dicom-dictionary-std = "0.8" +dicom-object = { version = "0.8", default-features = false } +foxglove_data_loader = "0.1.0" + +[dependencies.foxglove] +version = "0.9.0" +default-features = false +features = ["derive"] diff --git a/foxglove_sdk/dicom_data_loader/rust/src/lib.rs b/foxglove_sdk/dicom_data_loader/rust/src/lib.rs new file mode 100644 index 0000000..618f88f --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/src/lib.rs @@ -0,0 +1,20 @@ +//! DICOM CT data loader for Foxglove. +//! +//! [`load_study`] turns `.dcm` readers into timeline messages. The WASM adapter +//! is the only piece that talks to the Foxglove host. + +mod messages; +mod parse; +mod render; +mod study; + +pub use study::{EmptyStudy, Study, load_study}; + +#[cfg(target_arch = "wasm32")] +mod loader; + +#[cfg(target_arch = "wasm32")] +pub use loader::DicomLoader; + +#[cfg(target_arch = "wasm32")] +foxglove_data_loader::export!(DicomLoader); diff --git a/foxglove_sdk/dicom_data_loader/rust/src/loader.rs b/foxglove_sdk/dicom_data_loader/rust/src/loader.rs new file mode 100644 index 0000000..6d0905b --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/src/loader.rs @@ -0,0 +1,119 @@ +//! WASM entry points. Foxglove calls these; the study itself is host-agnostic. + +use foxglove::schemas::{FrameTransforms, ImageAnnotations, PointCloud, RawImage, SceneUpdate}; +use foxglove_data_loader::{ + BackfillArgs, DataLoader, DataLoaderArgs, Initialization, InitializationBuilder, Message, + MessageIterator, MessageIteratorArgs, Problem, console, reader, +}; + +use crate::messages::{MetadataMsg, PhaseStatsMsg, SliceStatsMsg}; +use crate::study::{EmptyStudy, MessageIter, SchemaKind, Study, load_study}; + +pub struct DicomLoader { + paths: Vec, + study: Option, +} + +impl DataLoader for DicomLoader { + type MessageIterator = HostIter; + type Error = anyhow::Error; + + fn new(args: DataLoaderArgs) -> Self { + Self { + paths: args.paths, + study: None, + } + } + + fn initialize(&mut self) -> Result { + let readers = self.paths.iter().map(|path| reader::open(path)); + match load_study(readers) { + Ok(study) => { + console::log(study.log_line()); + let init = describe(&study)?; + self.study = Some(study); + Ok(init) + } + Err(empty) => Ok(empty_init(empty)), + } + } + + fn create_iter( + &mut self, + args: MessageIteratorArgs, + ) -> Result { + let inner = self + .study + .as_ref() + .map(|study| study.messages(&args.channels, args.start_time, args.end_time)); + Ok(HostIter { inner }) + } + + fn get_backfill(&mut self, args: BackfillArgs) -> Result, Self::Error> { + match &self.study { + Some(study) => study.backfill(args.time, &args.channels), + None => Ok(Vec::new()), + } + } +} + +pub struct HostIter { + inner: Option, +} + +impl MessageIterator for HostIter { + type Error = anyhow::Error; + + fn next(&mut self) -> Option> { + self.inner.as_mut()?.next() + } +} + +fn describe(study: &Study) -> anyhow::Result { + let (start, end) = study.time_range(); + let mut builder = Initialization::builder().start_time(start).end_time(end); + for warning in study.warnings() { + builder = builder.add_problem(Problem::warn(warning.as_str())); + } + add_schema::(&mut builder, study, SchemaKind::RawImage)?; + add_schema::(&mut builder, study, SchemaKind::ImageAnnotations)?; + add_schema::(&mut builder, study, SchemaKind::PointCloud)?; + add_schema::(&mut builder, study, SchemaKind::SceneUpdate)?; + add_schema::(&mut builder, study, SchemaKind::FrameTransforms)?; + add_schema::(&mut builder, study, SchemaKind::SliceStats)?; + add_schema::(&mut builder, study, SchemaKind::PhaseStats)?; + add_schema::(&mut builder, study, SchemaKind::Metadata)?; + Ok(builder.build()) +} + +fn add_schema( + builder: &mut InitializationBuilder, + study: &Study, + kind: SchemaKind, +) -> anyhow::Result<()> { + let relevant: Vec<_> = study + .channels() + .iter() + .filter(|channel| channel.kind == kind) + .collect(); + if relevant.is_empty() { + return Ok(()); + } + let schema = builder.add_encode::()?; + for channel in relevant { + schema + .add_channel_with_id(channel.id, channel.topic) + .expect("channel id is unique") + .message_count(channel.message_count); + } + Ok(()) +} + +fn empty_init(empty: EmptyStudy) -> Initialization { + let mut builder = + Initialization::builder().add_problem(Problem::error(empty.message).tip(empty.tip)); + for warning in empty.warnings { + builder = builder.add_problem(Problem::warn(warning)); + } + builder.build() +} diff --git a/foxglove_sdk/dicom_data_loader/rust/src/messages.rs b/foxglove_sdk/dicom_data_loader/rust/src/messages.rs new file mode 100644 index 0000000..c183754 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/src/messages.rs @@ -0,0 +1,301 @@ +//! Foxglove protobuf messages for the DICOM topics. + +use foxglove::Encode; +use foxglove::bytes::Bytes; +use foxglove::schemas::{ + Color, FrameTransform, FrameTransforms, ImageAnnotations, LinePrimitive, PackedElementField, + Point2, Point3, PointCloud, PointsAnnotation, Pose, Quaternion, RawImage, SceneEntity, + SceneUpdate, TextPrimitive, Vector3, +}; +use foxglove_data_loader::Message; + +use crate::render::{Bounds, Cloud, GrayImage, POINT_STRIDE}; + +pub const POINT_CLOUD_STRIDE: u32 = POINT_STRIDE as u32; + +#[derive(Encode)] +pub struct SliceStatsMsg { + pub z_mm: f64, + pub mean_hu: f64, + pub lung_area_cm2: f64, + pub body_area_cm2: f64, +} + +#[derive(Encode)] +pub struct PhaseStatsMsg { + pub phase_percent: f64, + pub lung_volume_ml: f64, +} + +#[derive(Encode)] +pub struct MetadataMsg { + pub modality: String, + pub manufacturer: String, + pub study_description: String, + pub series_description: String, + pub rows: u32, + pub cols: u32, + pub row_spacing_mm: f64, + pub col_spacing_mm: f64, + pub slice_thickness_mm: f64, + pub slice_count: u32, + pub phase_count: u32, + pub mode: String, +} + +pub fn raw_image( + channel: u16, + time: u64, + frame_id: &str, + image: &GrayImage, +) -> anyhow::Result { + let msg = RawImage { + timestamp: timestamp(time), + frame_id: frame_id.to_string(), + width: image.width, + height: image.height, + encoding: "mono8".to_string(), + step: image.width, + data: Bytes::copy_from_slice(&image.pixels), + }; + pack(channel, time, &msg) +} + +pub fn slice_line(channel: u16, time: u64, width: u32, y: f64) -> anyhow::Result { + let msg = ImageAnnotations { + circles: Vec::new(), + points: vec![PointsAnnotation { + timestamp: timestamp(time), + r#type: foxglove::schemas::points_annotation::Type::LineStrip as i32, + points: vec![ + Point2 { x: 0.0, y }, + Point2 { + x: f64::from(width.saturating_sub(1)), + y, + }, + ], + outline_color: Some(Color { + r: 1.0, + g: 0.82, + b: 0.15, + a: 1.0, + }), + outline_colors: Vec::new(), + fill_color: None, + thickness: 2.0, + }], + texts: Vec::new(), + }; + pack(channel, time, &msg) +} + +pub fn cloud(channel: u16, time: u64, cloud: &Cloud) -> anyhow::Result { + let msg = PointCloud { + timestamp: timestamp(time), + frame_id: "patient".to_string(), + pose: Some(identity_pose()), + point_stride: POINT_CLOUD_STRIDE, + fields: point_fields(), + data: Bytes::copy_from_slice(&cloud.data), + }; + pack(channel, time, &msg) +} + +pub fn scene(channel: u16, time: u64, bounds: Bounds, label: &str) -> anyhow::Result { + let msg = SceneUpdate { + deletions: Vec::new(), + entities: vec![SceneEntity { + timestamp: timestamp(time), + frame_id: "patient".to_string(), + id: "dicom-volume".to_string(), + lifetime: Some(foxglove::schemas::Duration::default()), + frame_locked: true, + metadata: Vec::new(), + arrows: Vec::new(), + cubes: Vec::new(), + spheres: Vec::new(), + cylinders: Vec::new(), + lines: vec![LinePrimitive { + r#type: foxglove::schemas::line_primitive::Type::LineList as i32, + pose: Some(identity_pose()), + thickness: 1.5, + scale_invariant: true, + points: box_edges(bounds), + color: Some(Color { + r: 0.95, + g: 0.85, + b: 0.45, + a: 0.95, + }), + colors: Vec::new(), + indices: Vec::new(), + }], + triangles: Vec::new(), + texts: vec![TextPrimitive { + pose: Some(pose_at( + f64::from(bounds.min[0] + bounds.max[0]) * 0.5, + f64::from(bounds.min[1] + bounds.max[1]) * 0.5, + f64::from(bounds.max[2]) + 0.03, + )), + billboard: true, + font_size: 18.0, + scale_invariant: true, + color: Some(Color { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, + }), + text: label.to_string(), + }], + models: Vec::new(), + }], + }; + pack(channel, time, &msg) +} + +pub fn transforms(channel: u16, time: u64) -> anyhow::Result { + let msg = FrameTransforms { + transforms: vec![FrameTransform { + timestamp: timestamp(time), + parent_frame_id: "world".to_string(), + child_frame_id: "patient".to_string(), + translation: Some(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }), + rotation: Some(identity_quat()), + }], + }; + pack(channel, time, &msg) +} + +pub fn slice_stats(channel: u16, time: u64, stats: &SliceStatsMsg) -> anyhow::Result { + pack(channel, time, stats) +} + +pub fn phase_stats(channel: u16, time: u64, stats: &PhaseStatsMsg) -> anyhow::Result { + pack(channel, time, stats) +} + +pub fn metadata(channel: u16, time: u64, meta: &MetadataMsg) -> anyhow::Result { + pack(channel, time, meta) +} + +fn pack(channel: u16, time: u64, value: &T) -> anyhow::Result +where + T::Error: Send + Sync + 'static, +{ + let mut data = Vec::new(); + value.encode(&mut data)?; + Ok(Message { + channel_id: channel, + log_time: time, + publish_time: time, + data, + }) +} + +fn timestamp(nanos: u64) -> Option { + let sec = nanos / 1_000_000_000; + let nsec = (nanos % 1_000_000_000) as u32; + u32::try_from(sec) + .ok() + .and_then(|sec| foxglove::schemas::Timestamp::new_checked(sec, nsec)) +} + +fn point_fields() -> Vec { + use foxglove::schemas::packed_element_field::NumericType; + vec![ + field("x", 0, NumericType::Float32), + field("y", 4, NumericType::Float32), + field("z", 8, NumericType::Float32), + field("red", 12, NumericType::Uint8), + field("green", 13, NumericType::Uint8), + field("blue", 14, NumericType::Uint8), + field("alpha", 15, NumericType::Uint8), + ] +} + +fn field( + name: &str, + offset: u32, + ty: foxglove::schemas::packed_element_field::NumericType, +) -> PackedElementField { + PackedElementField { + name: name.to_string(), + offset, + r#type: ty as i32, + } +} + +fn identity_quat() -> Quaternion { + Quaternion { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0, + } +} + +fn identity_pose() -> Pose { + Pose { + position: Some(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }), + orientation: Some(identity_quat()), + } +} + +fn pose_at(x: f64, y: f64, z: f64) -> Pose { + Pose { + position: Some(Vector3 { x, y, z }), + orientation: Some(identity_quat()), + } +} + +fn box_edges(bounds: Bounds) -> Vec { + let [x0, y0, z0] = bounds.min; + let [x1, y1, z1] = bounds.max; + let corners = [ + [x0, y0, z0], + [x1, y0, z0], + [x1, y1, z0], + [x0, y1, z0], + [x0, y0, z1], + [x1, y0, z1], + [x1, y1, z1], + [x0, y1, z1], + ]; + const EDGES: [(usize, usize); 12] = [ + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (4, 5), + (5, 6), + (6, 7), + (7, 4), + (0, 4), + (1, 5), + (2, 6), + (3, 7), + ]; + let mut points = Vec::with_capacity(EDGES.len() * 2); + for (a, b) in EDGES { + points.push(point3(corners[a])); + points.push(point3(corners[b])); + } + points +} + +fn point3(xyz: [f32; 3]) -> Point3 { + Point3 { + x: f64::from(xyz[0]), + y: f64::from(xyz[1]), + z: f64::from(xyz[2]), + } +} diff --git a/foxglove_sdk/dicom_data_loader/rust/src/parse.rs b/foxglove_sdk/dicom_data_loader/rust/src/parse.rs new file mode 100644 index 0000000..2b88279 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/src/parse.rs @@ -0,0 +1,372 @@ +//! Read one uncompressed monochrome DICOM image into Hounsfield units. + +use std::io::Read; + +use anyhow::Error; +use dicom_core::DicomValue; +use dicom_dictionary_std::tags; +use dicom_object::file::ReadPreamble; +use dicom_object::{DefaultDicomObject, OpenFileOptions, ReadError}; + +const EXPLICIT_LE: &str = "1.2.840.10008.1.2.1"; +const IMPLICIT_LE: &str = "1.2.840.10008.1.2"; + +/// One CT slice, stored as rescaled Hounsfield units. +pub struct RawSlice { + pub hu: Vec, + pub rows: usize, + pub cols: usize, + pub ipp: [f64; 3], + pub row_dir: [f64; 3], + pub col_dir: [f64; 3], + pub row_spacing: f64, + pub col_spacing: f64, + pub instance: i32, + pub thickness: Option, + pub invert: bool, + pub series_uid: String, + pub series_description: String, + pub series_number: i32, + pub frame_of_reference: String, + pub modality: String, + pub manufacturer: String, + pub study_description: String, + pub study_date: String, + pub study_time: String, +} + +pub enum SliceRead { + Image(Box), + /// Counted and reported once; the file is not an image we can draw. + Skip(&'static str), +} + +pub fn read_slice(reader: R) -> Result { + // Part-10 files start with a 128-byte preamble. Auto-detection peeks through + // whatever the host returns on the first read, which is not reliable here. + let obj = OpenFileOptions::new() + .read_preamble(ReadPreamble::Always) + .from_reader(reader); + let obj = match obj { + Ok(obj) => obj, + Err(ReadError::ReadUnsupportedTransferSyntax { .. }) => { + return Ok(SliceRead::Skip("with an unsupported transfer syntax")); + } + Err(err) => return Err(err.into()), + }; + interpret(obj) +} + +fn interpret(obj: DefaultDicomObject) -> Result { + let ts = obj.meta().transfer_syntax(); + if ts != EXPLICIT_LE && ts != IMPLICIT_LE { + return Ok(SliceRead::Skip("with a compressed transfer syntax")); + } + + let Some(pixels) = obj.get(tags::PIXEL_DATA) else { + return Ok(SliceRead::Skip("without pixel data")); + }; + let DicomValue::Primitive(_) = pixels.value() else { + return Ok(SliceRead::Skip("with encapsulated pixel data")); + }; + + let rows = tag_u16(&obj, tags::ROWS).unwrap_or(0) as usize; + let cols = tag_u16(&obj, tags::COLUMNS).unwrap_or(0) as usize; + if rows == 0 || cols == 0 { + return Ok(SliceRead::Skip("with an unsupported pixel format")); + } + let frames = obj + .get(tags::NUMBER_OF_FRAMES) + .and_then(|e| e.to_int::().ok()) + .unwrap_or(1); + if frames != 1 { + return Ok(SliceRead::Skip("with more than one frame")); + } + let samples = tag_u16(&obj, tags::SAMPLES_PER_PIXEL).unwrap_or(1); + let bits = tag_u16(&obj, tags::BITS_ALLOCATED).unwrap_or(0); + if samples != 1 || (bits != 16 && bits != 8) { + return Ok(SliceRead::Skip("with an unsupported pixel format")); + } + let photo = tag_string(&obj, tags::PHOTOMETRIC_INTERPRETATION); + let invert = match photo.as_str() { + "MONOCHROME1" => true, + "MONOCHROME2" => false, + _ => { + return Ok(SliceRead::Skip( + "with an unsupported photometric interpretation", + )); + } + }; + + let Some(spacing) = tag_f64s(&obj, tags::PIXEL_SPACING) else { + return Ok(SliceRead::Skip("missing geometry")); + }; + if spacing.len() < 2 || spacing[0] <= 0.0 || spacing[1] <= 0.0 { + return Ok(SliceRead::Skip("missing geometry")); + } + let Some(ipp) = tag_vec3(&obj, tags::IMAGE_POSITION_PATIENT) else { + return Ok(SliceRead::Skip("missing geometry")); + }; + let Some(iop) = tag_f64s(&obj, tags::IMAGE_ORIENTATION_PATIENT) else { + return Ok(SliceRead::Skip("missing geometry")); + }; + if iop.len() < 6 { + return Ok(SliceRead::Skip("missing geometry")); + } + + let signed = tag_u16(&obj, tags::PIXEL_REPRESENTATION).unwrap_or(0) == 1; + let slope = tag_f64(&obj, tags::RESCALE_SLOPE).unwrap_or(1.0); + let intercept = tag_f64(&obj, tags::RESCALE_INTERCEPT).unwrap_or(0.0); + let bytes = pixels + .to_bytes() + .map_err(|_| anyhow::anyhow!("pixel data is not a primitive value"))?; + let Some(hu) = decode_hu(&bytes, rows, cols, bits, signed, slope, intercept) else { + return Ok(SliceRead::Skip("with an unsupported pixel format")); + }; + + Ok(SliceRead::Image(Box::new(RawSlice { + hu, + rows, + cols, + ipp, + row_dir: [iop[0], iop[1], iop[2]], + col_dir: [iop[3], iop[4], iop[5]], + row_spacing: spacing[0], + col_spacing: spacing[1], + instance: obj + .get(tags::INSTANCE_NUMBER) + .and_then(|e| e.to_int::().ok()) + .unwrap_or(0), + thickness: tag_f64(&obj, tags::SLICE_THICKNESS).filter(|t| *t > 0.0), + invert, + series_uid: tag_string(&obj, tags::SERIES_INSTANCE_UID), + series_description: tag_string(&obj, tags::SERIES_DESCRIPTION), + series_number: obj + .get(tags::SERIES_NUMBER) + .and_then(|e| e.to_int::().ok()) + .unwrap_or(0), + frame_of_reference: tag_string(&obj, tags::FRAME_OF_REFERENCE_UID), + modality: tag_string(&obj, tags::MODALITY), + manufacturer: tag_string(&obj, tags::MANUFACTURER), + study_description: tag_string(&obj, tags::STUDY_DESCRIPTION), + study_date: tag_string(&obj, tags::STUDY_DATE), + study_time: tag_string(&obj, tags::STUDY_TIME), + }))) +} + +fn decode_hu( + bytes: &[u8], + rows: usize, + cols: usize, + bits: u16, + signed: bool, + slope: f64, + intercept: f64, +) -> Option> { + let samples = rows * cols; + let width = (bits / 8) as usize; + if bytes.len() < samples * width { + return None; + } + let mut hu = Vec::with_capacity(samples); + if bits == 16 { + for chunk in bytes[..samples * 2].as_chunks::<2>().0 { + let raw = u16::from_le_bytes(*chunk); + let stored = if signed { + f64::from(raw as i16) + } else { + f64::from(raw) + }; + hu.push(quantize_hu(stored * slope + intercept)); + } + } else { + for &byte in &bytes[..samples] { + let stored = if signed { + i8::from_le_bytes([byte]) as f64 + } else { + f64::from(byte) + }; + hu.push(quantize_hu(stored * slope + intercept)); + } + } + Some(hu) +} + +fn quantize_hu(value: f64) -> i16 { + let rounded = value.round(); + if rounded <= f64::from(i16::MIN) { + i16::MIN + } else if rounded >= f64::from(i16::MAX) { + i16::MAX + } else { + rounded as i16 + } +} + +fn tag_string(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> String { + obj.get(tag) + .and_then(|e| e.string().ok()) + .map(clean_dicom) + .unwrap_or_default() +} + +fn tag_u16(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> Option { + obj.get(tag)?.to_int::().ok() +} + +fn tag_f64(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> Option { + obj.get(tag)?.to_float64().ok() +} + +fn tag_f64s(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> Option> { + let values = obj.get(tag)?.to_multi_float64().ok()?; + if values.is_empty() { + None + } else { + Some(values) + } +} + +fn tag_vec3(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> Option<[f64; 3]> { + let values = tag_f64s(obj, tag)?; + if values.len() < 3 { + return None; + } + Some([values[0], values[1], values[2]]) +} + +fn clean_dicom(value: &str) -> String { + value + .trim_matches(|c: char| c.is_whitespace() || c == '\0') + .to_string() +} + +/// Breathing phase from a series description such as `Gated, 40.0%`. +pub fn phase_percent(description: &str) -> Option { + let end = description.rfind('%')?; + let bytes = description.as_bytes(); + let mut start = end; + while start > 0 { + let c = bytes[start - 1]; + if c.is_ascii_digit() || c == b'.' { + start -= 1; + } else { + break; + } + } + let token = &description[start..end]; + if !token.bytes().any(|c| c.is_ascii_digit()) { + return None; + } + token.parse().ok() +} + +/// `StudyDate` (`YYYYMMDD`) plus `StudyTime` (`HHMMSS.FFFFFF`) as UTC nanoseconds. +pub fn study_epoch_nanos(date: &str, time: &str) -> Option { + let date = date.trim(); + if date.len() < 8 { + return None; + } + let year: i32 = date[0..4].parse().ok()?; + let month: u32 = date[4..6].parse().ok()?; + let day: u32 = date[6..8].parse().ok()?; + let days = days_from_civil(year, month, day)?; + let (hour, minute, second, frac) = parse_hms(time); + if hour > 23 || minute > 59 || second > 60 { + return None; + } + let secs = days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second); + u64::try_from(secs) + .ok() + .map(|secs| secs * 1_000_000_000 + frac) +} + +fn parse_hms(time: &str) -> (u32, u32, u32, u64) { + let time = time.trim(); + let hour = parse_digits(time, 0, 2).unwrap_or(0); + let minute = parse_digits(time, 2, 2).unwrap_or(0); + let second = parse_digits(time, 4, 2).unwrap_or(0); + let frac = fractional_nanos(time.get(6..).unwrap_or("")); + (hour, minute, second, frac) +} + +fn parse_digits(text: &str, start: usize, len: usize) -> Option { + let end = start + len; + if text.len() < end { + return None; + } + text[start..end].parse().ok() +} + +fn fractional_nanos(rest: &str) -> u64 { + let digits: String = rest + .trim() + .trim_start_matches('.') + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect(); + if digits.is_empty() { + return 0; + } + let mut padded = digits; + if padded.len() > 9 { + padded.truncate(9); + } else { + while padded.len() < 9 { + padded.push('0'); + } + } + padded.parse().unwrap_or(0) +} + +/// Days since the Unix epoch. Howard Hinnant's `days_from_civil`. +fn days_from_civil(mut year: i32, month: u32, day: u32) -> Option { + if !(1..=12).contains(&month) || !(1..=31).contains(&day) { + return None; + } + if month <= 2 { + year -= 1; + } + let era = if year >= 0 { + year / 400 + } else { + (year - 399) / 400 + }; + let yoe = u32::try_from(year - era * 400).ok()?; + let month_adj = if month > 2 { month - 3 } else { month + 9 }; + let doy = (153 * month_adj + 2) / 5 + day - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + Some(i64::from(era) * 146_097 + i64::from(doe) - 719_468) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_phase_percent() { + assert_eq!(phase_percent("P4^P100^S100^I0, Gated, 40.0%"), Some(40.0)); + assert_eq!(phase_percent("Gated, 0.0%"), Some(0.0)); + assert_eq!(phase_percent("100%"), Some(100.0)); + assert_eq!(phase_percent("no phase here"), None); + } + + #[test] + fn parses_study_clock_as_utc() { + assert_eq!(study_epoch_nanos("19700101", "000000"), Some(0)); + assert_eq!( + study_epoch_nanos("20000101", ""), + Some(946_684_800_000_000_000) + ); + assert_eq!( + study_epoch_nanos("19970915", "180715"), + Some(874_346_835_000_000_000) + ); + assert_eq!( + study_epoch_nanos("19970915", "180715.5"), + Some(874_346_835_500_000_000) + ); + assert_eq!(study_epoch_nanos("", "120000"), None); + assert_eq!(study_epoch_nanos("20001301", ""), None); + } +} diff --git a/foxglove_sdk/dicom_data_loader/rust/src/render.rs b/foxglove_sdk/dicom_data_loader/rust/src/render.rs new file mode 100644 index 0000000..a479d0c --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/src/render.rs @@ -0,0 +1,617 @@ +//! Window CT slices, build orthogonal views, and pack point clouds. +//! +//! Lung and body masks are a border-connected air heuristic for the demo, not a +//! clinical segmentation. + +pub const MAX_CLOUD_POINTS: usize = 300_000; +pub const POINT_STRIDE: usize = 16; + +const WINDOW_LEVEL: f32 = -600.0; +const WINDOW_WIDTH: f32 = 1500.0; +const WINDOW_MIN: f32 = WINDOW_LEVEL - WINDOW_WIDTH / 2.0; +const AIR_HU: i16 = -400; +const LUNG_MIN_HU: i16 = -1000; +const BONE_HU: i16 = 250; +const SLICE_AIR_HU: i16 = -900; + +const BONE_RGBA: [u8; 4] = [230, 220, 200, 255]; +const LUNG_RGBA: [u8; 4] = [40, 210, 220, 140]; + +pub struct GrayImage { + pub width: u32, + pub height: u32, + pub pixels: Vec, +} + +pub struct Cloud { + pub data: Vec, +} + +impl Cloud { + pub fn point_count(&self) -> usize { + self.data.len() / POINT_STRIDE + } +} + +#[derive(Clone, Copy)] +pub struct Bounds { + pub min: [f32; 3], + pub max: [f32; 3], +} + +#[derive(Clone, Copy)] +pub struct PatientFrame { + pub origin_mm: [f64; 3], + pub bounds: Bounds, +} + +pub struct Geom { + pub rows: usize, + pub cols: usize, + pub row_spacing: f64, + pub col_spacing: f64, + pub row_dir: [f64; 3], + pub col_dir: [f64; 3], + pub invert: bool, +} + +pub struct SliceView<'a> { + pub hu: &'a [i16], + pub ipp: [f64; 3], + pub stack_mm: f64, +} + +pub struct SliceStat { + pub z_mm: f64, + pub mean_hu: f64, + pub lung_area_cm2: f64, + pub body_area_cm2: f64, + pub lung_voxels: usize, +} + +pub struct SweepRender { + pub axial: Vec, + pub coronal: GrayImage, + pub sagittal: GrayImage, + pub coronal_y: Vec, + pub sagittal_y: Vec, + pub bone: Cloud, + pub lungs: Cloud, + pub slice_clouds: Vec, + pub stats: Vec, + pub frame: PatientFrame, +} + +pub struct PhaseRender { + pub axial: GrayImage, + pub coronal: GrayImage, + pub sagittal: GrayImage, + pub lungs: Cloud, + pub bone: Option, + pub lung_volume_ml: f64, +} + +struct Measured<'a> { + view: SliceView<'a>, + outside: Vec, + stat: SliceStat, +} + +pub fn sweep(geom: &Geom, slices: &[SliceView<'_>]) -> SweepRender { + let measured = measure_all(geom, slices); + let frame = patient_frame(geom, slices); + let axial = slices + .iter() + .map(|slice| gray_image(slice.hu, geom.cols, geom.rows, geom.invert)) + .collect(); + let (coronal, coronal_y) = coronal(geom, slices); + let (sagittal, sagittal_y) = sagittal(geom, slices); + let bone = volume_cloud(geom, &measured, &frame, |hu, _| hu >= BONE_HU, BONE_RGBA); + let lungs = volume_cloud(geom, &measured, &frame, is_lung, LUNG_RGBA); + let slice_clouds = measured + .iter() + .map(|slice| { + slice_cloud( + geom, + &slice.view, + &frame, + |hu| hu >= SLICE_AIR_HU, + |hu| { + let g = window_u8(hu, geom.invert); + [g, g, g, 255] + }, + ) + }) + .collect(); + SweepRender { + axial, + coronal, + sagittal, + coronal_y, + sagittal_y, + bone, + lungs, + slice_clouds, + stats: measured.into_iter().map(|slice| slice.stat).collect(), + frame, + } +} + +pub fn phase( + geom: &Geom, + slices: &[SliceView<'_>], + origin: &PatientFrame, + with_bone: bool, +) -> PhaseRender { + let measured = measure_all(geom, slices); + let mid = slices.len() / 2; + let axial = gray_image(slices[mid].hu, geom.cols, geom.rows, geom.invert); + let (coronal, _) = coronal(geom, slices); + let (sagittal, _) = sagittal(geom, slices); + let bone = + with_bone.then(|| volume_cloud(geom, &measured, origin, |hu, _| hu >= BONE_HU, BONE_RGBA)); + let lungs = volume_cloud(geom, &measured, origin, is_lung, LUNG_RGBA); + let thicknesses = slice_thicknesses(slices, geom); + let voxel_area = geom.row_spacing * geom.col_spacing; + let lung_volume_ml = measured + .iter() + .zip(thicknesses) + .map(|(slice, thickness)| slice.stat.lung_voxels as f64 * voxel_area * thickness / 1000.0) + .sum(); + PhaseRender { + axial, + coronal, + sagittal, + lungs, + bone, + lung_volume_ml, + } +} + +/// Patient frame centered on the volume so the anatomy sits at the origin. +/// +/// 4D playback reuses the first phase's frame. Recentering each phase would +/// cancel the diaphragm motion the coronal view is meant to show. +pub fn patient_frame(geom: &Geom, slices: &[SliceView<'_>]) -> PatientFrame { + let corners = corners(geom, slices); + let mut min_mm = corners[0]; + let mut max_mm = corners[0]; + for corner in &corners { + for axis in 0..3 { + min_mm[axis] = min_mm[axis].min(corner[axis]); + max_mm[axis] = max_mm[axis].max(corner[axis]); + } + } + let origin_mm = [ + (min_mm[0] + max_mm[0]) * 0.5, + (min_mm[1] + max_mm[1]) * 0.5, + (min_mm[2] + max_mm[2]) * 0.5, + ]; + let mut bounds = Bounds { + min: to_meters(min_mm, origin_mm), + max: to_meters(max_mm, origin_mm), + }; + for axis in 0..3 { + if bounds.max[axis] - bounds.min[axis] < 1e-4 { + bounds.min[axis] -= 0.01; + bounds.max[axis] += 0.01; + } + } + PatientFrame { origin_mm, bounds } +} + +fn measure_all<'a>(geom: &Geom, slices: &[SliceView<'a>]) -> Vec> { + let pixel_cm2 = geom.row_spacing * geom.col_spacing / 100.0; + slices + .iter() + .map(|view| { + let outside = outside_air(view.hu, geom.rows, geom.cols); + let mut lung_voxels = 0usize; + let mut body_count = 0usize; + let mut body_sum = 0i64; + for (index, &hu) in view.hu.iter().enumerate() { + if outside[index] != 0 { + continue; + } + body_count += 1; + body_sum += i64::from(hu); + if is_lung(hu, 0) { + lung_voxels += 1; + } + } + let mean_hu = if body_count == 0 { + 0.0 + } else { + body_sum as f64 / body_count as f64 + }; + Measured { + view: SliceView { + hu: view.hu, + ipp: view.ipp, + stack_mm: view.stack_mm, + }, + outside, + stat: SliceStat { + z_mm: view.ipp[2], + mean_hu, + lung_area_cm2: lung_voxels as f64 * pixel_cm2, + body_area_cm2: body_count as f64 * pixel_cm2, + lung_voxels, + }, + } + }) + .collect() +} + +fn outside_air(hu: &[i16], rows: usize, cols: usize) -> Vec { + let mut outside = vec![0u8; hu.len()]; + let mut stack = Vec::new(); + let push = |idx: usize, outside: &mut [u8], stack: &mut Vec| { + if outside[idx] == 0 && hu[idx] < AIR_HU { + outside[idx] = 1; + stack.push(idx); + } + }; + if rows == 0 || cols == 0 { + return outside; + } + for col in 0..cols { + push(col, &mut outside, &mut stack); + push((rows - 1) * cols + col, &mut outside, &mut stack); + } + for row in 0..rows { + push(row * cols, &mut outside, &mut stack); + push(row * cols + cols - 1, &mut outside, &mut stack); + } + while let Some(index) = stack.pop() { + let row = index / cols; + let col = index % cols; + if row > 0 { + push(index - cols, &mut outside, &mut stack); + } + if row + 1 < rows { + push(index + cols, &mut outside, &mut stack); + } + if col > 0 { + push(index - 1, &mut outside, &mut stack); + } + if col + 1 < cols { + push(index + 1, &mut outside, &mut stack); + } + } + outside +} + +fn is_lung(hu: i16, outside: u8) -> bool { + outside == 0 && (LUNG_MIN_HU..AIR_HU).contains(&hu) +} + +fn coronal(geom: &Geom, slices: &[SliceView<'_>]) -> (GrayImage, Vec) { + let grid = mpr_grid(slices, in_plane_spacing(geom)); + let mid_row = geom.rows / 2; + let mut pixels = Vec::with_capacity(geom.cols * grid.height); + for y in 0..grid.height { + let z = grid.z_at(y); + let (lo, hi, t) = bracket(slices, z); + for col in 0..geom.cols { + let hu = interp(slices, geom, lo, hi, t, mid_row, col); + pixels.push(window_u8(hu, geom.invert)); + } + } + let ys = slices + .iter() + .map(|slice| grid.y_of(slice.stack_mm)) + .collect(); + ( + GrayImage { + width: geom.cols as u32, + height: grid.height as u32, + pixels, + }, + ys, + ) +} + +fn sagittal(geom: &Geom, slices: &[SliceView<'_>]) -> (GrayImage, Vec) { + let grid = mpr_grid(slices, in_plane_spacing(geom)); + let mid_col = geom.cols / 2; + let mut pixels = Vec::with_capacity(geom.rows * grid.height); + for y in 0..grid.height { + let z = grid.z_at(y); + let (lo, hi, t) = bracket(slices, z); + for row in 0..geom.rows { + let hu = interp(slices, geom, lo, hi, t, row, mid_col); + pixels.push(window_u8(hu, geom.invert)); + } + } + let ys = slices + .iter() + .map(|slice| grid.y_of(slice.stack_mm)) + .collect(); + ( + GrayImage { + width: geom.rows as u32, + height: grid.height as u32, + pixels, + }, + ys, + ) +} + +struct Grid { + height: usize, + z_min: f64, + z_max: f64, +} + +impl Grid { + fn z_at(&self, y: usize) -> f64 { + if self.height <= 1 { + return self.z_max; + } + let t = y as f64 / (self.height - 1) as f64; + self.z_max - t * (self.z_max - self.z_min) + } + + fn y_of(&self, z: f64) -> f64 { + if self.height <= 1 || (self.z_max - self.z_min).abs() < 1e-6 { + return 0.0; + } + ((self.z_max - z) / (self.z_max - self.z_min) * (self.height - 1) as f64) + .clamp(0.0, (self.height - 1) as f64) + } +} + +fn mpr_grid(slices: &[SliceView<'_>], spacing: f64) -> Grid { + let z_min = slices.first().map(|slice| slice.stack_mm).unwrap_or(0.0); + let z_max = slices.last().map(|slice| slice.stack_mm).unwrap_or(z_min); + let span = (z_max - z_min).max(0.0); + let height = if span < 1e-6 || spacing <= 0.0 { + 1 + } else { + (span / spacing).round().max(1.0) as usize + 1 + }; + Grid { + height, + z_min, + z_max, + } +} + +fn bracket(slices: &[SliceView<'_>], target: f64) -> (usize, usize, f32) { + if slices.len() <= 1 || target <= slices[0].stack_mm { + return (0, 0, 0.0); + } + let last = slices.len() - 1; + if target >= slices[last].stack_mm { + return (last, last, 0.0); + } + let mut lo = 0; + let mut hi = last; + while hi - lo > 1 { + let mid = (lo + hi) / 2; + if slices[mid].stack_mm <= target { + lo = mid; + } else { + hi = mid; + } + } + let span = slices[hi].stack_mm - slices[lo].stack_mm; + let t = if span.abs() < 1e-9 { + 0.0 + } else { + ((target - slices[lo].stack_mm) / span) as f32 + }; + (lo, hi, t.clamp(0.0, 1.0)) +} + +fn interp( + slices: &[SliceView<'_>], + geom: &Geom, + lo: usize, + hi: usize, + t: f32, + row: usize, + col: usize, +) -> i16 { + let a = slices[lo].hu[row * geom.cols + col]; + let b = slices[hi].hu[row * geom.cols + col]; + (f32::from(a) + (f32::from(b) - f32::from(a)) * t).round() as i16 +} + +fn gray_image(hu: &[i16], cols: usize, rows: usize, invert: bool) -> GrayImage { + GrayImage { + width: cols as u32, + height: rows as u32, + pixels: hu.iter().map(|&value| window_u8(value, invert)).collect(), + } +} + +fn window_u8(hu: i16, invert: bool) -> u8 { + let t = ((f32::from(hu) - WINDOW_MIN) / WINDOW_WIDTH).clamp(0.0, 1.0); + let value = (t * 255.0).round() as u8; + if invert { 255 - value } else { value } +} + +fn in_plane_spacing(geom: &Geom) -> f64 { + (geom.row_spacing + geom.col_spacing) * 0.5 +} + +fn volume_cloud( + geom: &Geom, + slices: &[Measured<'_>], + frame: &PatientFrame, + pred: impl Fn(i16, u8) -> bool, + rgba: [u8; 4], +) -> Cloud { + let stride = choose_stride(geom, slices, &pred); + let mut data = Vec::new(); + for (index, slice) in slices.iter().enumerate() { + if index % stride != 0 { + continue; + } + for row in (0..geom.rows).step_by(stride) { + for col in (0..geom.cols).step_by(stride) { + let offset = row * geom.cols + col; + let hu = slice.view.hu[offset]; + if pred(hu, slice.outside[offset]) { + push_point( + &mut data, + voxel_meters(geom, &slice.view, frame, row, col), + rgba, + ); + } + } + } + } + Cloud { data } +} + +fn slice_cloud( + geom: &Geom, + slice: &SliceView<'_>, + frame: &PatientFrame, + pred: impl Fn(i16) -> bool, + color: impl Fn(i16) -> [u8; 4], +) -> Cloud { + let stride = choose_slice_stride(geom, slice, &pred); + let mut data = Vec::new(); + for row in (0..geom.rows).step_by(stride) { + for col in (0..geom.cols).step_by(stride) { + let hu = slice.hu[row * geom.cols + col]; + if pred(hu) { + push_point( + &mut data, + voxel_meters(geom, slice, frame, row, col), + color(hu), + ); + } + } + } + Cloud { data } +} + +fn choose_stride(geom: &Geom, slices: &[Measured<'_>], pred: &impl Fn(i16, u8) -> bool) -> usize { + let mut stride = 2; + loop { + let mut count = 0usize; + for (index, slice) in slices.iter().enumerate() { + if index % stride != 0 { + continue; + } + for row in (0..geom.rows).step_by(stride) { + for col in (0..geom.cols).step_by(stride) { + let offset = row * geom.cols + col; + if pred(slice.view.hu[offset], slice.outside[offset]) { + count += 1; + } + } + } + } + if count <= MAX_CLOUD_POINTS || stride >= 12 { + return stride; + } + stride += 1; + } +} + +fn choose_slice_stride(geom: &Geom, slice: &SliceView<'_>, pred: &impl Fn(i16) -> bool) -> usize { + let mut stride = 2; + loop { + let mut count = 0usize; + for row in (0..geom.rows).step_by(stride) { + for col in (0..geom.cols).step_by(stride) { + if pred(slice.hu[row * geom.cols + col]) { + count += 1; + } + } + } + if count <= MAX_CLOUD_POINTS || stride >= 12 { + return stride; + } + stride += 1; + } +} + +fn push_point(data: &mut Vec, point: [f32; 3], rgba: [u8; 4]) { + data.extend_from_slice(&point[0].to_le_bytes()); + data.extend_from_slice(&point[1].to_le_bytes()); + data.extend_from_slice(&point[2].to_le_bytes()); + data.extend_from_slice(&rgba); +} + +fn voxel_meters( + geom: &Geom, + slice: &SliceView<'_>, + frame: &PatientFrame, + row: usize, + col: usize, +) -> [f32; 3] { + let mm = [ + slice.ipp[0] + + col as f64 * geom.col_spacing * geom.row_dir[0] + + row as f64 * geom.row_spacing * geom.col_dir[0], + slice.ipp[1] + + col as f64 * geom.col_spacing * geom.row_dir[1] + + row as f64 * geom.row_spacing * geom.col_dir[1], + slice.ipp[2] + + col as f64 * geom.col_spacing * geom.row_dir[2] + + row as f64 * geom.row_spacing * geom.col_dir[2], + ]; + to_meters(mm, frame.origin_mm) +} + +fn to_meters(mm: [f64; 3], origin_mm: [f64; 3]) -> [f32; 3] { + [ + ((mm[0] - origin_mm[0]) * 0.001) as f32, + ((mm[1] - origin_mm[1]) * 0.001) as f32, + ((mm[2] - origin_mm[2]) * 0.001) as f32, + ] +} + +fn corners(geom: &Geom, slices: &[SliceView<'_>]) -> [[f64; 3]; 8] { + let last = slices.len().saturating_sub(1); + let rows = geom.rows.saturating_sub(1); + let cols = geom.cols.saturating_sub(1); + let mut out = [[0.0; 3]; 8]; + let mut index = 0; + for &slice_index in &[0, last] { + for &row in &[0, rows] { + for &col in &[0, cols] { + let slice = &slices[slice_index]; + out[index] = [ + slice.ipp[0] + + col as f64 * geom.col_spacing * geom.row_dir[0] + + row as f64 * geom.row_spacing * geom.col_dir[0], + slice.ipp[1] + + col as f64 * geom.col_spacing * geom.row_dir[1] + + row as f64 * geom.row_spacing * geom.col_dir[1], + slice.ipp[2] + + col as f64 * geom.col_spacing * geom.row_dir[2] + + row as f64 * geom.row_spacing * geom.col_dir[2], + ]; + index += 1; + } + } + } + out +} + +fn slice_thicknesses(slices: &[SliceView<'_>], geom: &Geom) -> Vec { + let fallback = in_plane_spacing(geom).max(1.0); + if slices.len() <= 1 { + return vec![fallback]; + } + let mut thicknesses = Vec::with_capacity(slices.len()); + for index in 0..slices.len() { + let delta = if index == 0 { + slices[1].stack_mm - slices[0].stack_mm + } else if index + 1 == slices.len() { + slices[index].stack_mm - slices[index - 1].stack_mm + } else { + (slices[index + 1].stack_mm - slices[index - 1].stack_mm) * 0.5 + }; + thicknesses.push(delta.abs().max(1e-3)); + } + thicknesses +} diff --git a/foxglove_sdk/dicom_data_loader/rust/src/study.rs b/foxglove_sdk/dicom_data_loader/rust/src/study.rs new file mode 100644 index 0000000..ff5c595 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/rust/src/study.rs @@ -0,0 +1,1145 @@ +//! Group slices into a sweep or a breathing cycle and play them on a timeline. + +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Read; +use std::rc::Rc; + +use foxglove_data_loader::Message; + +use crate::messages::{self, MetadataMsg, PhaseStatsMsg, SliceStatsMsg}; +use crate::parse::{self, RawSlice, SliceRead}; +use crate::render::{self, Bounds, Cloud, Geom, GrayImage, SliceView}; + +const SWEEP_DT_NS: u64 = 100_000_000; +const PHASE_DT_NS: u64 = 400_000_000; +const BREATH_CYCLES: usize = 3; + +const CH_AXIAL: u16 = 1; +const CH_CORONAL: u16 = 2; +const CH_SAGITTAL: u16 = 3; +const CH_CORONAL_ANN: u16 = 4; +const CH_SAGITTAL_ANN: u16 = 5; +const CH_BONE: u16 = 6; +const CH_LUNGS: u16 = 7; +const CH_SLICE: u16 = 8; +const CH_SCENE: u16 = 9; +const CH_TF: u16 = 10; +const CH_STATS: u16 = 11; +const CH_META: u16 = 12; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SchemaKind { + RawImage, + ImageAnnotations, + PointCloud, + SceneUpdate, + FrameTransforms, + SliceStats, + PhaseStats, + Metadata, +} + +#[derive(Clone)] +pub struct ChannelInfo { + pub id: u16, + pub topic: &'static str, + pub message_count: u64, + pub kind: SchemaKind, +} + +pub struct EmptyStudy { + pub message: String, + pub tip: String, + pub warnings: Vec, +} + +#[derive(Clone)] +pub struct Study { + inner: Rc, +} + +struct Inner { + mode: &'static str, + base_ns: u64, + dt_ns: u64, + frame_count: usize, + slice_count: usize, + phase_percents: Vec, + channels: Vec, + warnings: Vec, + log_line: String, + dynamic: Vec, + static_channels: Vec, + payload: Payload, + meta: MetadataMsg, + label: String, + bounds: Bounds, +} + +enum Payload { + Sweep(render::SweepRender), + Breathing { phases: Vec, bone: Cloud }, +} + +struct PhaseBody { + axial: GrayImage, + coronal: GrayImage, + sagittal: GrayImage, + lungs: Cloud, + stats: PhaseStatsMsg, +} + +struct SlicePx { + hu: Vec, + ipp: [f64; 3], + stack_mm: f64, + instance: i32, +} + +struct Series { + description: String, + number: i32, + for_uid: String, + phase: Option, + rows: usize, + cols: usize, + row_spacing: f64, + col_spacing: f64, + row_dir: [f64; 3], + col_dir: [f64; 3], + normal: [f64; 3], + invert: bool, + thickness: Option, + modality: String, + manufacturer: String, + study_description: String, + study_date: String, + study_time: String, + slices: Vec, +} + +pub fn load_study(readers: impl IntoIterator) -> Result { + let mut raw = Vec::new(); + let mut skips: BTreeMap<&'static str, usize> = BTreeMap::new(); + let mut files = 0usize; + for reader in readers { + files += 1; + match parse::read_slice(reader) { + Ok(SliceRead::Image(slice)) => raw.push(*slice), + Ok(SliceRead::Skip(reason)) => *skips.entry(reason).or_default() += 1, + Err(_) => *skips.entry("that could not be parsed").or_default() += 1, + } + } + if raw.is_empty() { + return Err(empty_failure(files, &skips)); + } + assemble(raw, skips) +} + +impl Study { + pub fn mode(&self) -> &str { + self.inner.mode + } + + pub fn slice_count(&self) -> usize { + self.inner.slice_count + } + + pub fn phase_percents(&self) -> &[f64] { + &self.inner.phase_percents + } + + pub fn frame_count(&self) -> usize { + self.inner.frame_count + } + + pub fn time_range(&self) -> (u64, u64) { + let start = self.inner.base_ns; + let end = if self.inner.frame_count == 0 { + start + } else { + self.time_of(self.inner.frame_count - 1) + }; + (start, end) + } + + pub fn channels(&self) -> &[ChannelInfo] { + &self.inner.channels + } + + pub fn warnings(&self) -> &[String] { + &self.inner.warnings + } + + pub fn log_line(&self) -> &str { + &self.inner.log_line + } + + pub fn image_size(&self, channel: u16, frame: usize) -> Option<(u32, u32)> { + self.image(channel, frame) + .map(|image| (image.width, image.height)) + } + + pub fn image_bytes(&self, channel: u16, frame: usize) -> Option<&[u8]> { + self.image(channel, frame) + .map(|image| image.pixels.as_slice()) + } + + pub fn point_count(&self, channel: u16, frame: usize) -> Option { + self.cloud(channel, frame).map(Cloud::point_count) + } + + pub fn slice_z_mm(&self) -> Vec { + match &self.inner.payload { + Payload::Sweep(sweep) => sweep.stats.iter().map(|stat| stat.z_mm).collect(), + Payload::Breathing { .. } => Vec::new(), + } + } + + pub fn lung_volume_ml(&self, frame: usize) -> Option { + match &self.inner.payload { + Payload::Breathing { phases, .. } => { + Some(phases[frame % phases.len()].stats.lung_volume_ml) + } + Payload::Sweep(_) => None, + } + } + + pub fn messages(&self, channels: &[u16], start: Option, end: Option) -> MessageIter { + let start = start.unwrap_or(0); + let end = end.unwrap_or(u64::MAX); + let frame = if start > end || self.inner.frame_count == 0 { + self.inner.frame_count + } else { + self.first_frame(start) + }; + MessageIter { + study: self.clone(), + channels: channels.iter().copied().collect(), + end, + frame, + slot: 0, + slots: Vec::new(), + } + } + + pub fn backfill(&self, time: u64, channels: &[u16]) -> anyhow::Result> { + let mut messages = Vec::new(); + for &channel in channels { + let Some(frame) = self.latest_frame(channel, time) else { + continue; + }; + messages.push(self.encode(channel, frame, self.time_of(frame))?); + } + Ok(messages) + } + + fn time_of(&self, frame: usize) -> u64 { + self.inner.base_ns + self.inner.dt_ns.saturating_mul(frame as u64) + } + + fn first_frame(&self, start: u64) -> usize { + if start <= self.inner.base_ns { + return 0; + } + let Some(div) = (start - self.inner.base_ns).checked_div(self.inner.dt_ns) else { + return 0; + }; + let mut frame = div as usize; + if frame < self.inner.frame_count && self.time_of(frame) < start { + frame += 1; + } + frame + } + + fn latest_frame(&self, channel: u16, time: u64) -> Option { + if self.inner.frame_count == 0 || time < self.inner.base_ns { + return None; + } + if self.inner.static_channels.contains(&channel) { + return Some(0); + } + if !self.inner.dynamic.contains(&channel) { + return None; + } + let frame = (time - self.inner.base_ns) + .checked_div(self.inner.dt_ns) + .unwrap_or(0) as usize; + Some(frame.min(self.inner.frame_count - 1)) + } + + fn slots_for(&self, frame: usize, channels: &BTreeSet) -> Vec { + let mut ids = Vec::new(); + for &id in &self.inner.dynamic { + if channels.contains(&id) { + ids.push(id); + } + } + if frame == 0 { + for &id in &self.inner.static_channels { + if channels.contains(&id) { + ids.push(id); + } + } + } + ids.sort_unstable(); + ids + } + + fn encode(&self, channel: u16, frame: usize, time: u64) -> anyhow::Result { + match channel { + CH_AXIAL => messages::raw_image(channel, time, "axial", self.axial(frame)), + CH_CORONAL => messages::raw_image(channel, time, "coronal", self.coronal(frame)), + CH_SAGITTAL => messages::raw_image(channel, time, "sagittal", self.sagittal(frame)), + CH_CORONAL_ANN => { + let image = self.coronal(frame); + let y = self.line_y(true, frame); + messages::slice_line(channel, time, image.width, y) + } + CH_SAGITTAL_ANN => { + let image = self.sagittal(frame); + let y = self.line_y(false, frame); + messages::slice_line(channel, time, image.width, y) + } + CH_BONE => messages::cloud(channel, time, self.bone()), + CH_LUNGS => messages::cloud(channel, time, self.lungs(frame)), + CH_SLICE => messages::cloud(channel, time, self.slice_cloud(frame)), + CH_SCENE => messages::scene(channel, time, self.inner.bounds, &self.inner.label), + CH_TF => messages::transforms(channel, time), + CH_STATS => match &self.inner.payload { + Payload::Sweep(sweep) => { + let stat = &sweep.stats[frame]; + messages::slice_stats( + channel, + time, + &SliceStatsMsg { + z_mm: stat.z_mm, + mean_hu: stat.mean_hu, + lung_area_cm2: stat.lung_area_cm2, + body_area_cm2: stat.body_area_cm2, + }, + ) + } + Payload::Breathing { phases, .. } => { + messages::phase_stats(channel, time, &phases[frame % phases.len()].stats) + } + }, + CH_META => messages::metadata(channel, time, &self.inner.meta), + _ => anyhow::bail!("unknown channel {channel}"), + } + } + + fn image(&self, channel: u16, frame: usize) -> Option<&GrayImage> { + match channel { + CH_AXIAL => Some(self.axial(frame)), + CH_CORONAL => Some(self.coronal(frame)), + CH_SAGITTAL => Some(self.sagittal(frame)), + _ => None, + } + } + + fn axial(&self, frame: usize) -> &GrayImage { + match &self.inner.payload { + Payload::Sweep(sweep) => &sweep.axial[frame], + Payload::Breathing { phases, .. } => &phases[frame % phases.len()].axial, + } + } + + fn coronal(&self, frame: usize) -> &GrayImage { + match &self.inner.payload { + Payload::Sweep(sweep) => &sweep.coronal, + Payload::Breathing { phases, .. } => &phases[frame % phases.len()].coronal, + } + } + + fn sagittal(&self, frame: usize) -> &GrayImage { + match &self.inner.payload { + Payload::Sweep(sweep) => &sweep.sagittal, + Payload::Breathing { phases, .. } => &phases[frame % phases.len()].sagittal, + } + } + + fn line_y(&self, coronal: bool, frame: usize) -> f64 { + match &self.inner.payload { + Payload::Sweep(sweep) => { + if coronal { + sweep.coronal_y[frame] + } else { + sweep.sagittal_y[frame] + } + } + Payload::Breathing { .. } => 0.0, + } + } + + fn bone(&self) -> &Cloud { + match &self.inner.payload { + Payload::Sweep(sweep) => &sweep.bone, + Payload::Breathing { bone, .. } => bone, + } + } + + fn lungs(&self, frame: usize) -> &Cloud { + match &self.inner.payload { + Payload::Sweep(sweep) => &sweep.lungs, + Payload::Breathing { phases, .. } => &phases[frame % phases.len()].lungs, + } + } + + fn slice_cloud(&self, frame: usize) -> &Cloud { + match &self.inner.payload { + Payload::Sweep(sweep) => &sweep.slice_clouds[frame], + Payload::Breathing { .. } => panic!("slice clouds are only built in sweep mode"), + } + } + + fn cloud(&self, channel: u16, frame: usize) -> Option<&Cloud> { + match channel { + CH_BONE => Some(self.bone()), + CH_LUNGS => Some(self.lungs(frame)), + CH_SLICE => match &self.inner.payload { + Payload::Sweep(sweep) => Some(&sweep.slice_clouds[frame]), + Payload::Breathing { .. } => None, + }, + _ => None, + } + } +} + +pub struct MessageIter { + study: Study, + channels: BTreeSet, + end: u64, + frame: usize, + slot: usize, + slots: Vec, +} + +impl Iterator for MessageIter { + type Item = anyhow::Result; + + fn next(&mut self) -> Option { + loop { + if self.frame >= self.study.frame_count() { + return None; + } + let time = self.study.time_of(self.frame); + if time > self.end { + return None; + } + if self.slot == 0 && self.slots.is_empty() { + self.slots = self.study.slots_for(self.frame, &self.channels); + } + if self.slot >= self.slots.len() { + self.frame += 1; + self.slot = 0; + self.slots.clear(); + continue; + } + let channel = self.slots[self.slot]; + self.slot += 1; + return Some(self.study.encode(channel, self.frame, time)); + } + } +} + +fn assemble( + raw: Vec, + mut skips: BTreeMap<&'static str, usize>, +) -> Result { + let mut grouped: BTreeMap = BTreeMap::new(); + for slice in raw { + push_slice(&mut grouped, slice, &mut skips); + } + let mut series: Vec = grouped + .into_values() + .filter(|s| !s.slices.is_empty()) + .collect(); + for series in &mut series { + finalize(series); + } + if series.is_empty() { + return Err(empty_failure(0, &skips)); + } + + let mut warnings = Vec::new(); + if let Some(skip) = format_skips(&skips) { + warnings.push(skip); + } + + if let Some(indices) = breathing_group(&series) { + let ignored = series.len().saturating_sub(indices.len()); + if ignored > 0 { + warnings.push(format!( + "Ignored {ignored} series that did not match the breathing volume" + )); + } + let mut chosen = take_indices(series, indices); + chosen.sort_by(phase_order); + Ok(build_breathing(chosen, warnings)) + } else { + let index = sweep_index(&series); + let kept = series[index].slices.len(); + let ignored = series.len().saturating_sub(1); + if ignored > 0 { + warnings.push(format!( + "Ignored {ignored} series; showing the largest ({kept} slices)" + )); + } + let chosen = take_indices(series, vec![index]); + Ok(build_sweep( + chosen.into_iter().next().expect("sweep series"), + warnings, + )) + } +} + +fn push_slice( + grouped: &mut BTreeMap, + slice: RawSlice, + skips: &mut BTreeMap<&'static str, usize>, +) { + if let Some(series) = grouped.get_mut(&slice.series_uid) { + if !compatible(series, &slice) { + *skips.entry("with inconsistent geometry").or_default() += 1; + return; + } + series.slices.push(SlicePx { + hu: slice.hu, + ipp: slice.ipp, + stack_mm: 0.0, + instance: slice.instance, + }); + return; + } + let uid = slice.series_uid.clone(); + grouped.insert(uid, Series::from_first(slice)); +} + +impl Series { + fn from_first(slice: RawSlice) -> Self { + let normal = unit_normal(slice.row_dir, slice.col_dir); + let phase = parse::phase_percent(&slice.series_description); + let pixels = SlicePx { + hu: slice.hu, + ipp: slice.ipp, + stack_mm: 0.0, + instance: slice.instance, + }; + Self { + description: slice.series_description, + number: slice.series_number, + for_uid: slice.frame_of_reference, + phase, + rows: slice.rows, + cols: slice.cols, + row_spacing: slice.row_spacing, + col_spacing: slice.col_spacing, + row_dir: slice.row_dir, + col_dir: slice.col_dir, + normal, + invert: slice.invert, + thickness: slice.thickness, + modality: slice.modality, + manufacturer: slice.manufacturer, + study_description: slice.study_description, + study_date: slice.study_date, + study_time: slice.study_time, + slices: vec![pixels], + } + } +} + +fn compatible(series: &Series, slice: &RawSlice) -> bool { + series.rows == slice.rows + && series.cols == slice.cols + && (series.row_spacing - slice.row_spacing).abs() < 0.05 + && (series.col_spacing - slice.col_spacing).abs() < 0.05 + && dot(series.row_dir, slice.row_dir) > 0.999 + && dot(series.col_dir, slice.col_dir) > 0.999 +} + +fn finalize(series: &mut Series) { + let sign = if series.normal[2] < 0.0 { -1.0 } else { 1.0 }; + for slice in &mut series.slices { + slice.stack_mm = sign * dot(slice.ipp, series.normal); + } + series.slices.sort_by(|a, b| { + a.stack_mm + .total_cmp(&b.stack_mm) + .then(a.instance.cmp(&b.instance)) + }); + series + .slices + .dedup_by(|a, b| (a.stack_mm - b.stack_mm).abs() < 0.05); +} + +fn breathing_group(series: &[Series]) -> Option> { + let mut groups: BTreeMap<(String, usize, usize, usize), Vec> = BTreeMap::new(); + for (index, series) in series.iter().enumerate() { + groups + .entry(( + series.for_uid.clone(), + series.rows, + series.cols, + series.slices.len(), + )) + .or_default() + .push(index); + } + groups + .into_values() + .max_by_key(|indices| indices.len()) + .filter(|indices| indices.len() >= 2) +} + +fn sweep_index(series: &[Series]) -> usize { + series + .iter() + .enumerate() + .max_by(|(_, a), (_, b)| { + a.slices + .len() + .cmp(&b.slices.len()) + .then(b.number.cmp(&a.number)) + }) + .map(|(index, _)| index) + .unwrap_or(0) +} + +fn phase_order(a: &Series, b: &Series) -> std::cmp::Ordering { + match (a.phase, b.phase) { + (Some(left), Some(right)) => left.total_cmp(&right).then(a.number.cmp(&b.number)), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => a.number.cmp(&b.number), + } +} + +fn take_indices(series: Vec, mut indices: Vec) -> Vec { + indices.sort_unstable(); + indices.dedup(); + let mut chosen = Vec::with_capacity(indices.len()); + for (index, series) in series.into_iter().enumerate() { + if indices.binary_search(&index).is_ok() { + chosen.push(series); + } + } + chosen +} + +fn build_sweep(series: Series, warnings: Vec) -> Study { + let rendered = { + let geom = geom_of(&series); + let views = views(&series); + render::sweep(&geom, &views) + }; + let frames = series.slices.len(); + let (channels, dynamic, static_channels) = sweep_channels(frames as u64); + finish( + "sweep", + &series, + std::slice::from_ref(&series), + frames, + 1, + SWEEP_DT_NS, + channels, + dynamic, + static_channels, + warnings, + Vec::new(), + rendered.frame.bounds, + sweep_label(&series), + Payload::Sweep(rendered), + ) +} + +fn build_breathing(series: Vec, warnings: Vec) -> Study { + let origin = { + let geom = geom_of(&series[0]); + let views = views(&series[0]); + render::patient_frame(&geom, &views) + }; + let mut phases = Vec::with_capacity(series.len()); + let mut percents = Vec::with_capacity(series.len()); + let mut bone = None; + for (index, series) in series.iter().enumerate() { + percents.push(series.phase.unwrap_or(index as f64)); + let geom = geom_of(series); + let views = views(series); + let rendered = render::phase(&geom, &views, &origin, index == 0); + if let Some(cloud) = rendered.bone { + bone = Some(cloud); + } + phases.push(PhaseBody { + axial: rendered.axial, + coronal: rendered.coronal, + sagittal: rendered.sagittal, + lungs: rendered.lungs, + stats: PhaseStatsMsg { + phase_percent: percents[index], + lung_volume_ml: rendered.lung_volume_ml, + }, + }); + } + let phase_count = phases.len(); + let frames = phase_count * BREATH_CYCLES; + let (channels, dynamic, static_channels) = breathing_channels(frames as u64); + let head = &series[0]; + finish( + "4d", + head, + &series, + head.slices.len(), + phase_count, + PHASE_DT_NS, + channels, + dynamic, + static_channels, + warnings, + percents, + origin.bounds, + format!("CT breathing · {phase_count} phases"), + Payload::Breathing { + phases, + bone: bone.expect("the first phase builds the bone cloud"), + }, + ) +} + +#[allow(clippy::too_many_arguments)] +fn finish( + mode: &'static str, + head: &Series, + described: &[Series], + slice_count: usize, + phase_count: usize, + dt_ns: u64, + channels: Vec, + dynamic: Vec, + static_channels: Vec, + warnings: Vec, + phase_percents: Vec, + bounds: Bounds, + label: String, + payload: Payload, +) -> Study { + let frame_count = match &payload { + Payload::Sweep(sweep) => sweep.axial.len(), + Payload::Breathing { phases, .. } => phases.len() * BREATH_CYCLES, + }; + let base_ns = parse::study_epoch_nanos(&head.study_date, &head.study_time).unwrap_or(0); + let log_line = format!( + "{mode} mode: {slice_count} slices, {phase_count} phase(s), {frame_count} frames, {} ms/frame", + dt_ns / 1_000_000 + ); + Study { + inner: Rc::new(Inner { + mode, + base_ns, + dt_ns, + frame_count, + slice_count, + phase_percents, + channels, + warnings, + log_line, + dynamic, + static_channels, + payload, + meta: metadata(head, described, mode, slice_count, phase_count), + label, + bounds, + }), + } +} + +fn sweep_channels(frames: u64) -> (Vec, Vec, Vec) { + let channels = vec![ + channel(CH_AXIAL, "/dicom/axial", frames, SchemaKind::RawImage), + channel(CH_CORONAL, "/dicom/coronal", 1, SchemaKind::RawImage), + channel(CH_SAGITTAL, "/dicom/sagittal", 1, SchemaKind::RawImage), + channel( + CH_CORONAL_ANN, + "/dicom/coronal/annotations", + frames, + SchemaKind::ImageAnnotations, + ), + channel( + CH_SAGITTAL_ANN, + "/dicom/sagittal/annotations", + frames, + SchemaKind::ImageAnnotations, + ), + channel(CH_BONE, "/dicom/bone", 1, SchemaKind::PointCloud), + channel(CH_LUNGS, "/dicom/lungs", 1, SchemaKind::PointCloud), + channel(CH_SLICE, "/dicom/slice", frames, SchemaKind::PointCloud), + channel(CH_SCENE, "/dicom/scene", 1, SchemaKind::SceneUpdate), + channel(CH_TF, "/tf", 1, SchemaKind::FrameTransforms), + channel(CH_STATS, "/dicom/stats", frames, SchemaKind::SliceStats), + channel(CH_META, "/dicom/metadata", 1, SchemaKind::Metadata), + ]; + let dynamic = vec![ + CH_AXIAL, + CH_CORONAL_ANN, + CH_SAGITTAL_ANN, + CH_SLICE, + CH_STATS, + ]; + let static_channels = vec![ + CH_CORONAL, + CH_SAGITTAL, + CH_BONE, + CH_LUNGS, + CH_SCENE, + CH_TF, + CH_META, + ]; + (channels, dynamic, static_channels) +} + +fn breathing_channels(frames: u64) -> (Vec, Vec, Vec) { + let channels = vec![ + channel(CH_AXIAL, "/dicom/axial", frames, SchemaKind::RawImage), + channel(CH_CORONAL, "/dicom/coronal", frames, SchemaKind::RawImage), + channel(CH_SAGITTAL, "/dicom/sagittal", frames, SchemaKind::RawImage), + channel(CH_BONE, "/dicom/bone", 1, SchemaKind::PointCloud), + channel(CH_LUNGS, "/dicom/lungs", frames, SchemaKind::PointCloud), + channel(CH_SCENE, "/dicom/scene", 1, SchemaKind::SceneUpdate), + channel(CH_TF, "/tf", 1, SchemaKind::FrameTransforms), + channel(CH_STATS, "/dicom/stats", frames, SchemaKind::PhaseStats), + channel(CH_META, "/dicom/metadata", 1, SchemaKind::Metadata), + ]; + let dynamic = vec![CH_AXIAL, CH_CORONAL, CH_SAGITTAL, CH_LUNGS, CH_STATS]; + let static_channels = vec![CH_BONE, CH_SCENE, CH_TF, CH_META]; + (channels, dynamic, static_channels) +} + +fn channel(id: u16, topic: &'static str, message_count: u64, kind: SchemaKind) -> ChannelInfo { + ChannelInfo { + id, + topic, + message_count, + kind, + } +} + +fn metadata( + head: &Series, + described: &[Series], + mode: &str, + slice_count: usize, + phase_count: usize, +) -> MetadataMsg { + MetadataMsg { + modality: head.modality.clone(), + manufacturer: head.manufacturer.clone(), + study_description: head.study_description.clone(), + series_description: joined_descriptions(described), + rows: head.rows as u32, + cols: head.cols as u32, + row_spacing_mm: head.row_spacing, + col_spacing_mm: head.col_spacing, + slice_thickness_mm: head.thickness.unwrap_or_else(|| median_spacing(head)), + slice_count: slice_count as u32, + phase_count: phase_count as u32, + mode: mode.to_string(), + } +} + +fn joined_descriptions(series: &[Series]) -> String { + let mut parts = Vec::new(); + for series in series { + if !series.description.is_empty() && !parts.contains(&series.description) { + parts.push(series.description.clone()); + } + } + parts.join("; ") +} + +fn sweep_label(series: &Series) -> String { + if series.description.is_empty() { + format!("CT · {} slices", series.slices.len()) + } else { + series.description.clone() + } +} + +fn median_spacing(series: &Series) -> f64 { + if series.slices.len() < 2 { + return 1.0; + } + let mut deltas: Vec = series + .slices + .windows(2) + .map(|pair| (pair[1].stack_mm - pair[0].stack_mm).abs()) + .collect(); + deltas.sort_by(|a, b| a.total_cmp(b)); + deltas[deltas.len() / 2] +} + +fn geom_of(series: &Series) -> Geom { + Geom { + rows: series.rows, + cols: series.cols, + row_spacing: series.row_spacing, + col_spacing: series.col_spacing, + row_dir: series.row_dir, + col_dir: series.col_dir, + invert: series.invert, + } +} + +fn views(series: &Series) -> Vec> { + series + .slices + .iter() + .map(|slice| SliceView { + hu: &slice.hu, + ipp: slice.ipp, + stack_mm: slice.stack_mm, + }) + .collect() +} + +fn format_skips(skips: &BTreeMap<&'static str, usize>) -> Option { + let total: usize = skips.values().copied().sum(); + if total == 0 { + return None; + } + let detail: Vec = skips + .iter() + .map(|(reason, count)| format!("{count} {reason}")) + .collect(); + Some(format!("Skipped {total} file(s): {}", detail.join(", "))) +} + +fn empty_failure(files: usize, skips: &BTreeMap<&'static str, usize>) -> EmptyStudy { + let warnings = format_skips(skips).into_iter().collect(); + EmptyStudy { + message: if files == 0 { + "No DICOM files were provided".to_string() + } else { + "No supported CT slices were found".to_string() + }, + tip: "Open every .dcm file from one series (or one 4D study) together. Only uncompressed little-endian monochrome images are read.".to_string(), + warnings, + } +} + +fn unit_normal(row: [f64; 3], col: [f64; 3]) -> [f64; 3] { + let cross = [ + row[1] * col[2] - row[2] * col[1], + row[2] * col[0] - row[0] * col[2], + row[0] * col[1] - row[1] * col[0], + ]; + let norm = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt(); + if norm < 1e-8 { + [0.0, 0.0, 1.0] + } else { + [cross[0] / norm, cross[1] / norm, cross[2] / norm] + } +} + +fn dot(a: [f64; 3], b: [f64; 3]) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::{Path, PathBuf}; + + fn load_optional(name: &str) -> Option { + let Ok(root) = std::env::var("DICOM_DATA_DIR") else { + eprintln!("skip {name}: DICOM_DATA_DIR is not set"); + return None; + }; + let dir = PathBuf::from(root).join(name); + let files = collect_dcm(&dir); + assert!(!files.is_empty(), "no .dcm files under {}", dir.display()); + eprintln!("loading {name} ({} files)", files.len()); + let study = load_study(files.iter().map(|path| std::fs::File::open(path).unwrap())) + .unwrap_or_else(|err| panic!("{}: {}", err.message, err.tip)); + eprintln!("{}", study.log_line()); + Some(study) + } + + fn collect_dcm(dir: &Path) -> Vec { + let mut out = Vec::new(); + let mut stack = vec![dir.to_path_buf()]; + while let Some(dir) = stack.pop() { + let entries = + std::fs::read_dir(&dir).unwrap_or_else(|err| panic!("{}: {err}", dir.display())); + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if path + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("dcm")) + { + out.push(path); + } + } + } + out.sort(); + out + } + + fn assert_playback(study: &Study, dt: u64) { + let (start, end) = study.time_range(); + assert!(start > 0, "study clock should parse"); + assert_eq!(end - start, (study.frame_count() as u64 - 1) * dt); + assert!(study.warnings().is_empty(), "{:?}", study.warnings()); + + let ids: Vec = study.channels().iter().map(|channel| channel.id).collect(); + let mut last = None; + let mut counts: BTreeMap = BTreeMap::new(); + let mut total = 0u64; + for message in study.messages(&ids, None, None) { + let message = message.unwrap(); + if let Some(prev) = last { + assert!(message.log_time >= prev); + } + assert!((start..=end).contains(&message.log_time)); + *counts.entry(message.channel_id).or_default() += 1; + last = Some(message.log_time); + total += 1; + } + assert_eq!(last, Some(end)); + let mut expected = 0u64; + for channel in study.channels() { + assert_eq!( + counts.get(&channel.id).copied().unwrap_or(0), + channel.message_count, + "{}", + channel.topic + ); + expected += channel.message_count; + } + assert_eq!(total, expected); + + let partial: Vec<_> = study + .messages(&[CH_AXIAL], Some(start + dt), Some(start + 2 * dt)) + .map(|message| message.unwrap()) + .collect(); + assert_eq!(partial.len(), 2); + assert_eq!(partial[0].log_time, start + dt); + assert_eq!(partial[1].log_time, start + 2 * dt); + assert!(partial.iter().all(|message| message.channel_id == CH_AXIAL)); + + assert!(study.backfill(start - 1, &ids).unwrap().is_empty()); + let at_start = study.backfill(start, &ids).unwrap(); + assert_eq!(at_start.len(), ids.len()); + assert!(at_start.iter().all(|message| message.log_time == start)); + let between = study.backfill(start + dt / 2, &ids).unwrap(); + assert!(between.iter().all(|message| message.log_time == start)); + let stepped = study.backfill(start + dt, &[CH_AXIAL]).unwrap(); + assert_eq!(stepped.len(), 1); + assert_eq!(stepped[0].log_time, start + dt); + let past = study.backfill(end + dt, &[CH_AXIAL, CH_BONE]).unwrap(); + assert_eq!( + past.iter() + .find(|message| message.channel_id == CH_AXIAL) + .unwrap() + .log_time, + end + ); + assert_eq!( + past.iter() + .find(|message| message.channel_id == CH_BONE) + .unwrap() + .log_time, + start + ); + } + + fn assert_image(study: &Study, channel: u16, frame: usize, width: u32) { + let (w, h) = study.image_size(channel, frame).unwrap(); + assert_eq!(w, width); + assert!(h > 0); + let pixels = study.image_bytes(channel, frame).unwrap(); + assert_eq!(pixels.len(), w as usize * h as usize); + assert!(pixels.iter().any(|value| *value > 20)); + assert!(pixels.iter().any(|value| *value < 230)); + } + + #[test] + fn lidc_is_a_sweep_through_the_chest() { + let Some(study) = load_optional("lidc") else { + return; + }; + assert_eq!(study.mode(), "sweep"); + assert_eq!(study.slice_count(), 133); + assert_eq!(study.frame_count(), 133); + assert!(study.phase_percents().is_empty()); + assert_playback(&study, SWEEP_DT_NS); + assert_image(&study, CH_AXIAL, 66, 512); + let (coronal_w, coronal_h) = study.image_size(CH_CORONAL, 0).unwrap(); + assert_eq!(coronal_w, 512); + assert!(coronal_h > 200, "coronal height {coronal_h}"); + let (sagittal_w, sagittal_h) = study.image_size(CH_SAGITTAL, 0).unwrap(); + assert_eq!(sagittal_w, 512); + assert!(sagittal_h > 200, "sagittal height {sagittal_h}"); + let z = study.slice_z_mm(); + assert_eq!(z.len(), 133); + assert!(z.windows(2).all(|pair| pair[1] >= pair[0])); + assert!(z.last().unwrap() - z.first().unwrap() > 200.0); + for channel in [CH_BONE, CH_LUNGS, CH_SLICE] { + let points = study.point_count(channel, 66).unwrap(); + assert!( + points > 100 && points <= render::MAX_CLOUD_POINTS, + "{channel} {points}" + ); + } + } + + #[test] + fn lung_4d_plays_ten_breathing_phases() { + let Some(study) = load_optional("4dlung") else { + return; + }; + assert_eq!(study.mode(), "4d"); + assert_eq!(study.slice_count(), 50); + assert_eq!(study.frame_count(), 30); + assert_eq!( + study.phase_percents(), + [0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0] + ); + assert!( + study + .channels() + .iter() + .all(|channel| channel.id != CH_SLICE) + ); + assert_playback(&study, PHASE_DT_NS); + assert_image(&study, CH_AXIAL, 0, 512); + let (width, height) = study.image_size(CH_CORONAL, 0).unwrap(); + assert_eq!(width, 512); + assert!(height > 80, "coronal height {height}"); + assert_ne!( + study.image_bytes(CH_CORONAL, 0).unwrap(), + study.image_bytes(CH_CORONAL, 5).unwrap() + ); + assert_eq!( + study.image_bytes(CH_CORONAL, 5).unwrap(), + study.image_bytes(CH_CORONAL, 15).unwrap() + ); + let inhale = study.lung_volume_ml(0).unwrap(); + let mid = study.lung_volume_ml(5).unwrap(); + assert!(inhale > 200.0 && inhale < 20_000.0, "{inhale}"); + assert!(mid > 200.0 && mid < 20_000.0, "{mid}"); + assert!((inhale - mid).abs() > 1.0, "{inhale} vs {mid}"); + for frame in 0..10 { + let points = study.point_count(CH_LUNGS, frame).unwrap(); + assert!( + points > 100 && points <= render::MAX_CLOUD_POINTS, + "frame {frame} {points}" + ); + } + let bone = study.point_count(CH_BONE, 0).unwrap(); + assert!(bone > 100 && bone <= render::MAX_CLOUD_POINTS, "{bone}"); + } +} diff --git a/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py b/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py new file mode 100644 index 0000000..b3e52b3 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Download the public CT series used by this tutorial. + +Files land under data/chest and data/breathing, next to this tutorial. They are +not committed. +""" + +import argparse +from pathlib import Path + +# LIDC-IDRI-0001 chest CT, one axial series. +CHEST_SERIES = [ + "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", +] + +# 4D-Lung patient 100_HM10395, study S100: ten gated phases, 0% through 90%. +BREATHING_SERIES = [ + "1.3.6.1.4.1.14519.5.2.1.6834.5010.124525254231969293228570633659", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.183508601171753954609852620508", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.187024278831950398228901118188", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.204026634860397031036823480116", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.215193814203822462481389051414", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.303761398024606109161463363548", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.322628904903035357840500590726", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.339352878581931019587697303406", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.742525481756625894234574121639", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.901242277587004706935962916718", +] + + +def download(dest: Path, series: list[str]) -> None: + from idc_index import IDCClient + + dest.mkdir(parents=True, exist_ok=True) + print(f"Downloading {len(series)} series into {dest}") + IDCClient().download_from_selection( + str(dest), + seriesInstanceUID=series, + dirTemplate="%Modality_%SeriesInstanceUID", + quiet=False, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--dataset", + choices=["chest", "breathing", "all"], + default="all", + help="Which sample to fetch (default: all)", + ) + args = parser.parse_args() + data = Path(__file__).resolve().parents[1] / "data" + if args.dataset in ("chest", "all"): + download(data / "chest", CHEST_SERIES) + if args.dataset in ("breathing", "all"): + download(data / "breathing", BREATHING_SERIES) + + +if __name__ == "__main__": + main() diff --git a/foxglove_sdk/dicom_data_loader/scripts/requirements.txt b/foxglove_sdk/dicom_data_loader/scripts/requirements.txt new file mode 100644 index 0000000..88edce7 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/scripts/requirements.txt @@ -0,0 +1 @@ +idc-index diff --git a/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs b/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs new file mode 100644 index 0000000..35b3c7a --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs @@ -0,0 +1,357 @@ +// Run the release data-loader component outside Foxglove. +// +// Requires wasm-tools and @bytecodealliance/jco on PATH (or WASM_TOOLS / JCO). +// DICOM_DATA_DIR should contain lidc/ and 4dlung/. Previews go to +// /tmp/dicom_plan/previews by default (PREVIEW_DIR). + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import zlib from "node:zlib"; +import { pathToFileURL } from "node:url"; + +const tutorialRoot = path.resolve(import.meta.dirname, ".."); +const dataRoot = process.env.DICOM_DATA_DIR ?? "/tmp/dicom_data"; +const previewDir = process.env.PREVIEW_DIR ?? "/tmp/dicom_plan/previews"; +const wasmTools = process.env.WASM_TOOLS ?? "wasm-tools"; +const jco = process.env.JCO ?? "jco"; +const coreWasm = path.join( + tutorialRoot, + "rust/target/wasm32-unknown-unknown/release/foxglove_dicom_data_loader.wasm", +); +const outDir = path.join(tutorialRoot, "scripts/.smoke"); + +function collectDcm(dir) { + const out = []; + const stack = [dir]; + while (stack.length > 0) { + const current = stack.pop(); + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const full = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(full); + } else if (entry.name.toLowerCase().endsWith(".dcm")) { + out.push(full); + } + } + } + out.sort(); + return out; +} + +class Reader { + constructor(filePath, memory) { + this.data = fs.readFileSync(filePath); + this.pos = 0; + this.memory = memory; + } + + read(ptr, len) { + const n = Math.min(len, this.data.length - this.pos); + if (n > 0) { + new Uint8Array(this.memory().buffer).set(this.data.subarray(this.pos, this.pos + n), ptr); + this.pos += n; + } + return BigInt(n); + } +} + +function decodeRawImage(buf) { + const bytes = Buffer.from(buf); + let offset = 0; + let width; + let height; + let step; + let encoding; + let frameId; + let data; + const readVarint = () => { + let value = 0; + let shift = 0; + while (offset < bytes.length) { + const byte = bytes[offset]; + offset += 1; + value |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) { + return value; + } + shift += 7; + } + throw new Error("truncated varint"); + }; + while (offset < bytes.length) { + const key = readVarint(); + const field = key >>> 3; + const wire = key & 7; + if (wire === 0) { + readVarint(); + } else if (wire === 1) { + offset += 8; + } else if (wire === 5) { + const value = bytes.readUInt32LE(offset); + offset += 4; + if (field === 2) { + width = value; + } else if (field === 3) { + height = value; + } else if (field === 5) { + step = value; + } + } else if (wire === 2) { + const len = readVarint(); + const slice = bytes.subarray(offset, offset + len); + offset += len; + if (field === 4) { + encoding = slice.toString("utf8"); + } else if (field === 6) { + data = slice; + } else if (field === 7) { + frameId = slice.toString("utf8"); + } + } else { + throw new Error(`unsupported protobuf wire type ${wire}`); + } + } + if (width == null || height == null || data == null) { + throw new Error("RawImage is missing width, height, or pixels"); + } + return { width, height, step, encoding, frameId, data }; +} + +function crc32(buffer) { + let crc = ~0; + for (const byte of buffer) { + crc ^= byte; + for (let bit = 0; bit < 8; bit += 1) { + crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + } + return ~crc >>> 0; +} + +function chunk(type, data) { + const body = Buffer.concat([Buffer.from(type), data]); + const out = Buffer.alloc(12 + data.length); + out.writeUInt32BE(data.length, 0); + body.copy(out, 4); + out.writeUInt32BE(crc32(body), 8 + data.length); + return out; +} + +function writePng(file, image) { + const { width, height, data } = image; + if (data.length !== width * height) { + throw new Error(`${file}: expected ${width * height} pixels, got ${data.length}`); + } + const raw = Buffer.alloc((width + 1) * height); + for (let y = 0; y < height; y += 1) { + data.copy(raw, y * (width + 1) + 1, y * width, (y + 1) * width); + } + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; + ihdr[9] = 0; + const png = Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + chunk("IHDR", ihdr), + chunk("IDAT", zlib.deflateSync(raw)), + chunk("IEND", Buffer.alloc(0)), + ]); + fs.writeFileSync(file, png); +} + +function transpile() { + fs.rmSync(outDir, { recursive: true, force: true }); + fs.mkdirSync(outDir, { recursive: true }); + const component = path.join(outDir, "component.wasm"); + execFileSync(wasmTools, ["component", "new", coreWasm, "-o", component], { stdio: "inherit" }); + execFileSync( + jco, + [ + "transpile", + component, + "-o", + outDir, + "--name", + "dicom", + "--instantiation", + "sync", + "--no-nodejs-compat", + "--no-typescript", + ], + { stdio: "inherit" }, + ); +} + +async function loadComponent() { + const js = pathToFileURL(path.join(outDir, "dicom.js")).href; + const imported = await import(js); + return imported.instantiate; +} + +async function play(name, dir, instantiate) { + const files = collectDcm(dir); + if (files.length === 0) { + throw new Error(`no .dcm files under ${dir}`); + } + let memory; + const memoryRef = () => { + if (!memory) { + throw new Error("guest memory is not available"); + } + return memory; + }; + let instance = instantiate( + (filename) => new WebAssembly.Module(fs.readFileSync(path.join(outDir, filename))), + { + "foxglove:loader/console": { + log(message) { + console.log(`[${name}] ${message}`); + }, + }, + "foxglove:loader/reader": { + Reader, + open(filePath) { + return new Reader(filePath, memoryRef); + }, + }, + }, + (module, importObject) => { + const created = new WebAssembly.Instance(module, importObject); + const exported = created.exports.memory; + if (exported && (!memory || exported.buffer.byteLength >= memory.buffer.byteLength)) { + memory = exported; + } + return created; + }, + ); + if (instance instanceof Promise) { + instance = await instance; + } + const loader = new instance.loader.DataLoader({ paths: files }); + const init = loader.initialize(); + const memoryBytes = memory.buffer.byteLength; + console.log(`[${name}] wasm memory after initialize: ${memoryBytes} bytes (${files.length} files)`); + const errors = init.problems.filter((problem) => problem.severity === "error"); + if (errors.length > 0) { + throw new Error(`${name} initialize errors: ${errors.map((problem) => problem.message).join("; ")}`); + } + if (init.problems.length > 0) { + throw new Error(`${name} unexpected problems: ${JSON.stringify(init.problems)}`); + } + const { startTime, endTime } = init.timeRange; + if (startTime <= 0n || endTime < startTime) { + throw new Error(`${name} time range ${startTime}..${endTime}`); + } + const ids = init.channels.map((channel) => channel.id); + const iterator = loader.createIterator({ channels: Uint16Array.from(ids) }); + const counts = new Map(); + const images = new Map(); + let previous; + let total = 0; + while (true) { + const next = iterator.next(); + if (next == null) { + break; + } + if (next.tag !== "ok") { + throw new Error(`${name} iterator error: ${next.val}`); + } + const message = next.val; + if (previous != null && message.logTime < previous) { + throw new Error(`${name} log time went backwards`); + } + if (message.logTime < startTime || message.logTime > endTime) { + throw new Error(`${name} log time ${message.logTime} outside ${startTime}..${endTime}`); + } + previous = message.logTime; + counts.set(message.channelId, (counts.get(message.channelId) ?? 0) + 1); + const topic = init.channels.find((channel) => channel.id === message.channelId)?.topicName; + if (topic === "/dicom/axial" || topic === "/dicom/coronal" || topic === "/dicom/sagittal") { + const list = images.get(topic) ?? []; + list.push(message.data); + images.set(topic, list); + } + total += 1; + } + if (previous !== endTime) { + throw new Error(`${name} last log time ${previous} != end ${endTime}`); + } + let expected = 0n; + for (const channel of init.channels) { + const got = BigInt(counts.get(channel.id) ?? 0); + if (got !== channel.messageCount) { + throw new Error(`${name} ${channel.topicName}: ${got} messages, expected ${channel.messageCount}`); + } + expected += channel.messageCount ?? 0n; + } + if (BigInt(total) !== expected) { + throw new Error(`${name} iterated ${total}, expected ${expected}`); + } + const backfill = loader.getBackfill({ time: startTime, channels: Uint16Array.from(ids) }); + if (backfill.length !== ids.length) { + throw new Error(`${name} backfill returned ${backfill.length} messages for ${ids.length} channels`); + } + return { images, memoryBytes, channels: init.channels }; +} + +function expectImage(images, topic, index, label) { + const list = images.get(topic); + if (!list || !list[index]) { + throw new Error(`missing ${topic} frame ${index}`); + } + const image = decodeRawImage(list[index]); + if (image.encoding !== "mono8") { + throw new Error(`${label} encoding ${image.encoding}`); + } + if (image.width !== 512 || image.data.length !== image.width * image.height) { + throw new Error(`${label} is ${image.width}x${image.height} (${image.data.length} bytes)`); + } + writePng(path.join(previewDir, `${label}.png`), image); + console.log(`[preview] ${label}.png ${image.width}x${image.height} frame=${image.frameId}`); + return image.data; +} + +transpile(); +const instantiate = await loadComponent(); +fs.mkdirSync(previewDir, { recursive: true }); + +const chest = await play("lidc", path.join(dataRoot, "lidc"), instantiate); +expectImage(chest.images, "/dicom/axial", 66, "lidc-axial"); +expectImage(chest.images, "/dicom/coronal", 0, "lidc-coronal"); +expectImage(chest.images, "/dicom/sagittal", 0, "lidc-sagittal"); +if (chest.images.get("/dicom/axial")?.length !== 133) { + throw new Error(`lidc axial frames ${chest.images.get("/dicom/axial")?.length}`); +} + +const breathing = await play("4dlung", path.join(dataRoot, "4dlung"), instantiate); +const phase0 = expectImage(breathing.images, "/dicom/coronal", 0, "4dlung-coronal-0"); +const phase50 = expectImage(breathing.images, "/dicom/coronal", 5, "4dlung-coronal-50"); +const phase50Cycle2 = breathing.images.get("/dicom/coronal")?.[15]; +if (!phase50Cycle2) { + throw new Error("missing 4D coronal frame 15"); +} +if (Buffer.compare(phase0, phase50) === 0) { + throw new Error("4D coronal phase 0% and 50% are identical"); +} +if (Buffer.compare(phase50, decodeRawImage(phase50Cycle2).data) !== 0) { + throw new Error("4D coronal phase 50% was not reused on the next cycle"); +} +expectImage(breathing.images, "/dicom/axial", 0, "4dlung-axial"); +expectImage(breathing.images, "/dicom/sagittal", 0, "4dlung-sagittal"); +if (breathing.images.get("/dicom/coronal")?.length !== 30) { + throw new Error(`4dlung coronal frames ${breathing.images.get("/dicom/coronal")?.length}`); +} + +console.log( + JSON.stringify( + { + lidcMemoryBytes: chest.memoryBytes, + lung4dMemoryBytes: breathing.memoryBytes, + previewDir, + }, + null, + 2, + ), +); diff --git a/foxglove_sdk/dicom_data_loader/src/globals.d.ts b/foxglove_sdk/dicom_data_loader/src/globals.d.ts new file mode 100644 index 0000000..3ab1782 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/src/globals.d.ts @@ -0,0 +1,5 @@ +// Webpack is configured to import .wasm files as a base64 URL +declare module "*.wasm" { + const url: string; + export default url; +} diff --git a/foxglove_sdk/dicom_data_loader/src/index.ts b/foxglove_sdk/dicom_data_loader/src/index.ts new file mode 100644 index 0000000..3093b77 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/src/index.ts @@ -0,0 +1,12 @@ +import type { Experimental } from "@foxglove/extension"; + +import wasmUrl from "../rust/target/wasm32-unknown-unknown/release/foxglove_dicom_data_loader.wasm"; + +export function activate(extensionContext: Experimental.ExtensionContext): void { + extensionContext.registerDataLoader({ + type: "file", + wasmUrl, + supportedFileType: ".dcm", + supportsMultiFile: true, + }); +} diff --git a/foxglove_sdk/dicom_data_loader/tsconfig.json b/foxglove_sdk/dicom_data_loader/tsconfig.json new file mode 100644 index 0000000..c2bdc05 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "create-foxglove-extension/tsconfig/tsconfig.json", + + "include": ["./src/**/*"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "lib": ["DOM"], + + // These two settings prevent typescript from emitting .d.ts files we don't need in + // the compiled extension. + "composite": false, + "declaration": false, + + // Additional TypeScript error reporting checks are enabled by default to improve code quality. + // Enable/disable these checks as necessary to suit your coding preferences or work with + // existing code + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true + } +} From e3f2f8ad36be1ce6386db7911f37b670e527a061 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 11:41:36 +0000 Subject: [PATCH 2/3] Stream DICOM phases, switch 4D-Lung sample to study S300, simplify loader - Two-pass loading (headers, then one series of pixels at a time) keeps peak WASM memory ~130 MB for the chest CT and ~95 MB for the 750 MB 4D study - Precomputed timeline of encoded messages drives create_iter and backfill - 4D-Lung study S300 (142 slices/phase) keeps the diaphragm in view - Tests decode with prost; tests and smoke script read data/chest and data/breathing - Smoke script implements the full WIT reader resource Co-authored-by: Mateusz Sadowski --- foxglove_sdk/dicom_data_loader/README.md | 27 +- foxglove_sdk/dicom_data_loader/data/README.md | 2 +- .../dicom_data_loader/rust/Cargo.toml | 3 + .../dicom_data_loader/rust/src/lib.rs | 5 +- .../dicom_data_loader/rust/src/loader.rs | 11 +- .../dicom_data_loader/rust/src/messages.rs | 185 +-- .../dicom_data_loader/rust/src/parse.rs | 169 +-- .../dicom_data_loader/rust/src/render.rs | 606 +++----- .../dicom_data_loader/rust/src/study.rs | 1297 +++++++++-------- .../scripts/download_sample_data.py | 23 +- .../dicom_data_loader/scripts/wasm_smoke.mjs | 325 ++--- 11 files changed, 1171 insertions(+), 1482 deletions(-) diff --git a/foxglove_sdk/dicom_data_loader/README.md b/foxglove_sdk/dicom_data_loader/README.md index 6eaf953..7f62f47 100644 --- a/foxglove_sdk/dicom_data_loader/README.md +++ b/foxglove_sdk/dicom_data_loader/README.md @@ -21,12 +21,12 @@ Both series are public, CC BY 3.0, and hosted by the NCI Imaging Data Commons. T ```bash pip install -r scripts/requirements.txt -python scripts/download_sample_data.py --dataset chest # one 133-slice chest CT -python scripts/download_sample_data.py --dataset breathing # ten respiratory phases +python scripts/download_sample_data.py --dataset chest # one 133-slice chest CT, ~70 MB +python scripts/download_sample_data.py --dataset breathing # ten phases, ~750 MB python scripts/download_sample_data.py --dataset all ``` -Files are written under `data/chest` and `data/breathing`. +Files are written under `data/chest` and `data/breathing`. The breathing study is about 750 MB (ten phases, 142 slices each). From `rust/`, `cargo test --release` reads those two folders and skips a dataset when its folder is missing. `DICOM_DATA_DIR` overrides the parent directory. ## Build and install @@ -48,24 +48,33 @@ Import `foxglove_layouts/dicom_layout.json` for a layout with axial, coronal, an Press play. - **Chest CT** is sweep mode. Each frame is the next axial slice, inferior to superior, at 100 ms per slice (about 13 seconds). Coronal and sagittal views are the centre multi-planar reconstruction, published once. The line on those views tracks the slice. The 3D panel shows bone, lung, and the current slice as a point cloud. -- **4D lung** is breathing mode. The loader finds ten series that share a frame of reference and the same geometry, ordered by the phase percentage in `SeriesDescription`. Each frame is one phase at 400 ms, and the cycle repeats three times so playback looks continuous. The centre coronal image and the lung cloud update every phase, so the diaphragm and lung volume move. Bone, the bounding box, and the metadata message stay at the start of the timeline; Foxglove keeps them visible by backfill when you seek. +- **4D lung** is breathing mode, using 4D-Lung patient `100_HM10395`, study S300 (142 slices per phase, so the diaphragm stays in view). The loader finds the ten series that share a frame of reference and the same geometry, ordered by the phase percentage in `SeriesDescription` (`Gated, 40.0%A`). Each frame is one phase at 400 ms, and the cycle repeats three times so playback looks continuous. The centre coronal image and the lung cloud update every phase. Bone, the bounding box, and the metadata message stay at the start of the timeline; Foxglove keeps them visible by backfill when you seek. ## How it works -`rust/src/lib.rs` exports a [`DataLoader`](https://docs.rs/foxglove_data_loader). Foxglove calls `initialize` with the paths you opened. The loader reads each file through the host `reader` interface, keeps uncompressed little-endian monochrome slices, and groups them by `SeriesInstanceUID`. +`rust/src/lib.rs` exports a [`DataLoader`](https://docs.rs/foxglove_data_loader). Foxglove calls `initialize` with the paths you opened. The loader first reads each file only up to the pixel-data tag, groups the headers by `SeriesInstanceUID`, and decides sweep or 4D mode. It then re-opens one series at a time, encodes that phase, and drops the HU volume before the next series. The result is a timeline of encoded messages. Repeated breathing cycles share the same bytes. Slices in a series are ordered along the slice normal (`ImagePositionPatient` dotted with the cross product of the image orientation). The first frame is the inferior end. Mode detection: -- **4D** when at least two series share `FrameOfReferenceUID` and have the same rows, columns, and slice count. Phases are ordered by a percentage parsed from the series description, such as `Gated, 40.0%`, and otherwise by series number. +- **4D** when at least two series share `FrameOfReferenceUID` and have the same rows, columns, and slice count. Phases are ordered by a percentage parsed from the series description, such as `Gated, 40.0%A`, and otherwise by series number. - **Sweep** otherwise, using the series with the most slices. Extra series produce a warning. -`StudyDate` and `StudyTime` become the timeline origin, interpreted as UTC. If they do not parse, the origin is zero. Messages are not materialised up front. `create_iter` yields one frame at a time, and `get_backfill` returns the latest message at or before the seek time so panels are not empty when playback starts in the middle. +`StudyDate` and `StudyTime` become the timeline origin, interpreted as UTC. If they do not parse, the origin is zero. `create_iter` walks that timeline from the start time, and `get_backfill` returns the latest message at or before the seek time on each requested channel so panels are not empty when playback starts in the middle. Images are windowed with a CT lung window (level -600 HU, width 1500) and encoded as `mono8`. Coronal and sagittal views resample along the stack so the spacing matches the in-plane pixels, with superior at the top. Positions are DICOM LPS millimetres converted to metres in a `patient` frame whose origin is the centre of the volume, so the anatomy sits at the origin of Foxglove's z-up view. Every 4D phase uses the first phase's origin. Recentering each phase would hide the breathing motion. -The lung mask is a demo heuristic. A flood fill from the image border marks air connected to the outside of the body (voxels below -400 HU). Voxels that are not outside air and sit between -1000 and -400 HU count as lung. Body is everything that is not outside air. Bone is HU of 250 or higher. Point clouds are stride-subsampled so each cloud stays at or under 300,000 points. +The lung mask is a demo heuristic. A flood fill from the image border marks air connected to the outside of the body (voxels below -400 HU). Voxels that are not outside air and sit between -1000 and -400 HU count as lung. Body is everything that is not outside air. Bone is HU of 250 or higher. Point clouds step 4 pixels in-plane and, for a whole volume, every second slice, which keeps each cloud under 300,000 points for these series. + +## Optional: smoke test + +`scripts/wasm_smoke.mjs` plays `data/chest` and `data/breathing` through the release module. Build it from `rust/` with `cargo build --release --target wasm32-unknown-unknown`. You also need [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) and [`@bytecodealliance/jco`](https://www.npmjs.com/package/@bytecodealliance/jco). `DICOM_DATA_DIR` overrides the data parent, `WASM_TOOLS` and `JCO` select the tools, and `PREVIEW_DIR` (optional) is where grayscale PNGs are written. + +```bash +cargo build --release --target wasm32-unknown-unknown --manifest-path rust/Cargo.toml +node scripts/wasm_smoke.mjs +``` ## Limitations @@ -74,4 +83,4 @@ Only uncompressed little-endian transfer syntaxes are read (Implicit VR and Expl ## Data - Chest CT: LIDC-IDRI, series `1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192`. Armato SG 3rd, McLennan G, Bidaut L, McNitt-Gray MF, Meyer CR, Reeves AP, Zhao B, Aberle DR, Henschke CI, Hoffman EA, Kazerooni EA, MacMahon H, Van Beek EJR, Yankelevitz D, et al.: The Lung Image Database Consortium (LIDC) and Image Database Resource Initiative (IDRI): A completed reference database of lung nodules on CT scans. Medical Physics, 38: 915--931, 2011. [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). Data from [The Cancer Imaging Archive](https://www.cancerimagingarchive.net/). -- Breathing CT: 4D-Lung, patient `100_HM10395`, study S100. Hugo GD, Weiss E, Sleeman WC, Balik S, Keall PJ, Lu J, Williamson JF. (2016). Data from 4D Lung Imaging of NSCLC Patients. The Cancer Imaging Archive. [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). +- Breathing CT: 4D-Lung, patient `100_HM10395`, study S300 (ten gated series, 142 slices of 512×512, 0.9766 mm pixels, 3 mm slices, Implicit VR Little Endian). Hugo GD, Weiss E, Sleeman WC, Balik S, Keall PJ, Lu J, Williamson JF. (2016). Data from 4D Lung Imaging of NSCLC Patients. The Cancer Imaging Archive. [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). diff --git a/foxglove_sdk/dicom_data_loader/data/README.md b/foxglove_sdk/dicom_data_loader/data/README.md index d69d57a..7ae3f24 100644 --- a/foxglove_sdk/dicom_data_loader/data/README.md +++ b/foxglove_sdk/dicom_data_loader/data/README.md @@ -1,3 +1,3 @@ Sample CT series downloaded by `scripts/download_sample_data.py` land in this directory. -`data/chest` is one LIDC-IDRI axial series. `data/breathing` is ten 4D-Lung phases. The files are not part of the repository. +`data/chest` is one LIDC-IDRI axial series. `data/breathing` is ten 4D-Lung phases from study S300 (about 750 MB). The files are not part of the repository. diff --git a/foxglove_sdk/dicom_data_loader/rust/Cargo.toml b/foxglove_sdk/dicom_data_loader/rust/Cargo.toml index af2bf0c..4d84af1 100644 --- a/foxglove_sdk/dicom_data_loader/rust/Cargo.toml +++ b/foxglove_sdk/dicom_data_loader/rust/Cargo.toml @@ -21,3 +21,6 @@ foxglove_data_loader = "0.1.0" version = "0.9.0" default-features = false features = ["derive"] + +[dev-dependencies] +prost = "0.13" diff --git a/foxglove_sdk/dicom_data_loader/rust/src/lib.rs b/foxglove_sdk/dicom_data_loader/rust/src/lib.rs index 618f88f..7dbbde5 100644 --- a/foxglove_sdk/dicom_data_loader/rust/src/lib.rs +++ b/foxglove_sdk/dicom_data_loader/rust/src/lib.rs @@ -1,7 +1,4 @@ -//! DICOM CT data loader for Foxglove. -//! -//! [`load_study`] turns `.dcm` readers into timeline messages. The WASM adapter -//! is the only piece that talks to the Foxglove host. +//! DICOM CT series played on a Foxglove timeline. mod messages; mod parse; diff --git a/foxglove_sdk/dicom_data_loader/rust/src/loader.rs b/foxglove_sdk/dicom_data_loader/rust/src/loader.rs index 6d0905b..bcc7745 100644 --- a/foxglove_sdk/dicom_data_loader/rust/src/loader.rs +++ b/foxglove_sdk/dicom_data_loader/rust/src/loader.rs @@ -1,5 +1,3 @@ -//! WASM entry points. Foxglove calls these; the study itself is host-agnostic. - use foxglove::schemas::{FrameTransforms, ImageAnnotations, PointCloud, RawImage, SceneUpdate}; use foxglove_data_loader::{ BackfillArgs, DataLoader, DataLoaderArgs, Initialization, InitializationBuilder, Message, @@ -26,8 +24,7 @@ impl DataLoader for DicomLoader { } fn initialize(&mut self) -> Result { - let readers = self.paths.iter().map(|path| reader::open(path)); - match load_study(readers) { + match load_study(&self.paths, |path| Ok(reader::open(path))) { Ok(study) => { console::log(study.log_line()); let init = describe(&study)?; @@ -50,10 +47,10 @@ impl DataLoader for DicomLoader { } fn get_backfill(&mut self, args: BackfillArgs) -> Result, Self::Error> { - match &self.study { + Ok(match &self.study { Some(study) => study.backfill(args.time, &args.channels), - None => Ok(Vec::new()), - } + None => Vec::new(), + }) } } diff --git a/foxglove_sdk/dicom_data_loader/rust/src/messages.rs b/foxglove_sdk/dicom_data_loader/rust/src/messages.rs index c183754..fe54fb4 100644 --- a/foxglove_sdk/dicom_data_loader/rust/src/messages.rs +++ b/foxglove_sdk/dicom_data_loader/rust/src/messages.rs @@ -1,5 +1,3 @@ -//! Foxglove protobuf messages for the DICOM topics. - use foxglove::Encode; use foxglove::bytes::Bytes; use foxglove::schemas::{ @@ -7,12 +5,9 @@ use foxglove::schemas::{ Point2, Point3, PointCloud, PointsAnnotation, Pose, Quaternion, RawImage, SceneEntity, SceneUpdate, TextPrimitive, Vector3, }; -use foxglove_data_loader::Message; use crate::render::{Bounds, Cloud, GrayImage, POINT_STRIDE}; -pub const POINT_CLOUD_STRIDE: u32 = POINT_STRIDE as u32; - #[derive(Encode)] pub struct SliceStatsMsg { pub z_mm: f64, @@ -43,13 +38,8 @@ pub struct MetadataMsg { pub mode: String, } -pub fn raw_image( - channel: u16, - time: u64, - frame_id: &str, - image: &GrayImage, -) -> anyhow::Result { - let msg = RawImage { +pub fn raw_image(time: u64, frame_id: &str, image: &GrayImage) -> Vec { + pack(&RawImage { timestamp: timestamp(time), frame_id: frame_id.to_string(), width: image.width, @@ -57,13 +47,11 @@ pub fn raw_image( encoding: "mono8".to_string(), step: image.width, data: Bytes::copy_from_slice(&image.pixels), - }; - pack(channel, time, &msg) + }) } -pub fn slice_line(channel: u16, time: u64, width: u32, y: f64) -> anyhow::Result { - let msg = ImageAnnotations { - circles: Vec::new(), +pub fn slice_line(time: u64, width: u32, y: f64) -> Vec { + pack(&ImageAnnotations { points: vec![PointsAnnotation { timestamp: timestamp(time), r#type: foxglove::schemas::points_annotation::Type::LineStrip as i32, @@ -74,148 +62,112 @@ pub fn slice_line(channel: u16, time: u64, width: u32, y: f64) -> anyhow::Result y, }, ], - outline_color: Some(Color { - r: 1.0, - g: 0.82, - b: 0.15, - a: 1.0, - }), - outline_colors: Vec::new(), - fill_color: None, + outline_color: Some(color(1.0, 0.82, 0.15, 1.0)), thickness: 2.0, + ..Default::default() }], - texts: Vec::new(), - }; - pack(channel, time, &msg) + ..Default::default() + }) } -pub fn cloud(channel: u16, time: u64, cloud: &Cloud) -> anyhow::Result { - let msg = PointCloud { +pub fn cloud(time: u64, cloud: &Cloud) -> Vec { + use foxglove::schemas::packed_element_field::NumericType; + pack(&PointCloud { timestamp: timestamp(time), frame_id: "patient".to_string(), pose: Some(identity_pose()), - point_stride: POINT_CLOUD_STRIDE, - fields: point_fields(), + point_stride: POINT_STRIDE as u32, + fields: vec![ + field("x", 0, NumericType::Float32), + field("y", 4, NumericType::Float32), + field("z", 8, NumericType::Float32), + field("red", 12, NumericType::Uint8), + field("green", 13, NumericType::Uint8), + field("blue", 14, NumericType::Uint8), + field("alpha", 15, NumericType::Uint8), + ], data: Bytes::copy_from_slice(&cloud.data), - }; - pack(channel, time, &msg) + }) } -pub fn scene(channel: u16, time: u64, bounds: Bounds, label: &str) -> anyhow::Result { - let msg = SceneUpdate { - deletions: Vec::new(), +pub fn scene(time: u64, bounds: Bounds, label: &str) -> Vec { + let [x0, y0, _] = bounds.min; + let [x1, y1, z1] = bounds.max; + pack(&SceneUpdate { entities: vec![SceneEntity { timestamp: timestamp(time), frame_id: "patient".to_string(), id: "dicom-volume".to_string(), lifetime: Some(foxglove::schemas::Duration::default()), frame_locked: true, - metadata: Vec::new(), - arrows: Vec::new(), - cubes: Vec::new(), - spheres: Vec::new(), - cylinders: Vec::new(), lines: vec![LinePrimitive { r#type: foxglove::schemas::line_primitive::Type::LineList as i32, pose: Some(identity_pose()), thickness: 1.5, scale_invariant: true, points: box_edges(bounds), - color: Some(Color { - r: 0.95, - g: 0.85, - b: 0.45, - a: 0.95, - }), - colors: Vec::new(), - indices: Vec::new(), + color: Some(color(0.95, 0.85, 0.45, 0.95)), + ..Default::default() }], - triangles: Vec::new(), texts: vec![TextPrimitive { pose: Some(pose_at( - f64::from(bounds.min[0] + bounds.max[0]) * 0.5, - f64::from(bounds.min[1] + bounds.max[1]) * 0.5, - f64::from(bounds.max[2]) + 0.03, + f64::from(x0 + x1) * 0.5, + f64::from(y0 + y1) * 0.5, + f64::from(z1) + 0.03, )), billboard: true, font_size: 18.0, scale_invariant: true, - color: Some(Color { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }), + color: Some(color(1.0, 1.0, 1.0, 1.0)), text: label.to_string(), }], - models: Vec::new(), + ..Default::default() }], - }; - pack(channel, time, &msg) + ..Default::default() + }) } -pub fn transforms(channel: u16, time: u64) -> anyhow::Result { - let msg = FrameTransforms { +pub fn transforms(time: u64) -> Vec { + pack(&FrameTransforms { transforms: vec![FrameTransform { timestamp: timestamp(time), parent_frame_id: "world".to_string(), child_frame_id: "patient".to_string(), - translation: Some(Vector3 { + translation: Some(Vector3::default()), + rotation: Some(Quaternion { x: 0.0, y: 0.0, z: 0.0, + w: 1.0, }), - rotation: Some(identity_quat()), }], - }; - pack(channel, time, &msg) + }) } -pub fn slice_stats(channel: u16, time: u64, stats: &SliceStatsMsg) -> anyhow::Result { - pack(channel, time, stats) +pub fn slice_stats(stats: &SliceStatsMsg) -> Vec { + pack(stats) } -pub fn phase_stats(channel: u16, time: u64, stats: &PhaseStatsMsg) -> anyhow::Result { - pack(channel, time, stats) +pub fn phase_stats(stats: &PhaseStatsMsg) -> Vec { + pack(stats) } -pub fn metadata(channel: u16, time: u64, meta: &MetadataMsg) -> anyhow::Result { - pack(channel, time, meta) +pub fn metadata(meta: &MetadataMsg) -> Vec { + pack(meta) } -fn pack(channel: u16, time: u64, value: &T) -> anyhow::Result +fn pack(value: &T) -> Vec where T::Error: Send + Sync + 'static, { let mut data = Vec::new(); - value.encode(&mut data)?; - Ok(Message { - channel_id: channel, - log_time: time, - publish_time: time, - data, - }) + value.encode(&mut data).expect("schema encode"); + data } fn timestamp(nanos: u64) -> Option { - let sec = nanos / 1_000_000_000; - let nsec = (nanos % 1_000_000_000) as u32; - u32::try_from(sec) - .ok() - .and_then(|sec| foxglove::schemas::Timestamp::new_checked(sec, nsec)) -} - -fn point_fields() -> Vec { - use foxglove::schemas::packed_element_field::NumericType; - vec![ - field("x", 0, NumericType::Float32), - field("y", 4, NumericType::Float32), - field("z", 8, NumericType::Float32), - field("red", 12, NumericType::Uint8), - field("green", 13, NumericType::Uint8), - field("blue", 14, NumericType::Uint8), - field("alpha", 15, NumericType::Uint8), - ] + let sec = u32::try_from(nanos / 1_000_000_000).ok()?; + foxglove::schemas::Timestamp::new_checked(sec, (nanos % 1_000_000_000) as u32) } fn field( @@ -230,30 +182,31 @@ fn field( } } -fn identity_quat() -> Quaternion { - Quaternion { - x: 0.0, - y: 0.0, - z: 0.0, - w: 1.0, - } +fn color(r: f64, g: f64, b: f64, a: f64) -> Color { + Color { r, g, b, a } } fn identity_pose() -> Pose { Pose { - position: Some(Vector3 { + position: Some(Vector3::default()), + orientation: Some(Quaternion { x: 0.0, y: 0.0, z: 0.0, + w: 1.0, }), - orientation: Some(identity_quat()), } } fn pose_at(x: f64, y: f64, z: f64) -> Pose { Pose { position: Some(Vector3 { x, y, z }), - orientation: Some(identity_quat()), + orientation: Some(Quaternion { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0, + }), } } @@ -284,12 +237,10 @@ fn box_edges(bounds: Bounds) -> Vec { (2, 6), (3, 7), ]; - let mut points = Vec::with_capacity(EDGES.len() * 2); - for (a, b) in EDGES { - points.push(point3(corners[a])); - points.push(point3(corners[b])); - } - points + EDGES + .into_iter() + .flat_map(|(a, b)| [point3(corners[a]), point3(corners[b])]) + .collect() } fn point3(xyz: [f32; 3]) -> Point3 { diff --git a/foxglove_sdk/dicom_data_loader/rust/src/parse.rs b/foxglove_sdk/dicom_data_loader/rust/src/parse.rs index 2b88279..2123b1e 100644 --- a/foxglove_sdk/dicom_data_loader/rust/src/parse.rs +++ b/foxglove_sdk/dicom_data_loader/rust/src/parse.rs @@ -1,5 +1,3 @@ -//! Read one uncompressed monochrome DICOM image into Hounsfield units. - use std::io::Read; use anyhow::Error; @@ -11,9 +9,9 @@ use dicom_object::{DefaultDicomObject, OpenFileOptions, ReadError}; const EXPLICIT_LE: &str = "1.2.840.10008.1.2.1"; const IMPLICIT_LE: &str = "1.2.840.10008.1.2"; -/// One CT slice, stored as rescaled Hounsfield units. -pub struct RawSlice { - pub hu: Vec, +/// Geometry and study tags. `hu` is set only by [`read_pixels`]. +pub struct SliceMeta { + pub hu: Option>, pub rows: usize, pub cols: usize, pub ipp: [f64; 3], @@ -36,17 +34,29 @@ pub struct RawSlice { } pub enum SliceRead { - Image(Box), - /// Counted and reported once; the file is not an image we can draw. + Image(Box), Skip(&'static str), } -pub fn read_slice(reader: R) -> Result { +pub fn read_header(reader: R) -> Result { + interpret(open(reader, true), false) +} + +pub fn read_pixels(reader: R) -> Result { + interpret(open(reader, false), true) +} + +fn open(reader: R, header_only: bool) -> Result { // Part-10 files start with a 128-byte preamble. Auto-detection peeks through // whatever the host returns on the first read, which is not reliable here. - let obj = OpenFileOptions::new() - .read_preamble(ReadPreamble::Always) - .from_reader(reader); + let mut opts = OpenFileOptions::new().read_preamble(ReadPreamble::Always); + if header_only { + opts = opts.read_until(tags::PIXEL_DATA); + } + opts.from_reader(reader) +} + +fn interpret(obj: Result, pixels: bool) -> Result { let obj = match obj { Ok(obj) => obj, Err(ReadError::ReadUnsupportedTransferSyntax { .. }) => { @@ -54,22 +64,10 @@ pub fn read_slice(reader: R) -> Result { } Err(err) => return Err(err.into()), }; - interpret(obj) -} - -fn interpret(obj: DefaultDicomObject) -> Result { let ts = obj.meta().transfer_syntax(); if ts != EXPLICIT_LE && ts != IMPLICIT_LE { return Ok(SliceRead::Skip("with a compressed transfer syntax")); } - - let Some(pixels) = obj.get(tags::PIXEL_DATA) else { - return Ok(SliceRead::Skip("without pixel data")); - }; - let DicomValue::Primitive(_) = pixels.value() else { - return Ok(SliceRead::Skip("with encapsulated pixel data")); - }; - let rows = tag_u16(&obj, tags::ROWS).unwrap_or(0) as usize; let cols = tag_u16(&obj, tags::COLUMNS).unwrap_or(0) as usize; if rows == 0 || cols == 0 { @@ -77,7 +75,7 @@ fn interpret(obj: DefaultDicomObject) -> Result { } let frames = obj .get(tags::NUMBER_OF_FRAMES) - .and_then(|e| e.to_int::().ok()) + .and_then(|element| element.to_int::().ok()) .unwrap_or(1); if frames != 1 { return Ok(SliceRead::Skip("with more than one frame")); @@ -87,8 +85,7 @@ fn interpret(obj: DefaultDicomObject) -> Result { if samples != 1 || (bits != 16 && bits != 8) { return Ok(SliceRead::Skip("with an unsupported pixel format")); } - let photo = tag_string(&obj, tags::PHOTOMETRIC_INTERPRETATION); - let invert = match photo.as_str() { + let invert = match tag_string(&obj, tags::PHOTOMETRIC_INTERPRETATION).as_str() { "MONOCHROME1" => true, "MONOCHROME2" => false, _ => { @@ -97,7 +94,6 @@ fn interpret(obj: DefaultDicomObject) -> Result { )); } }; - let Some(spacing) = tag_f64s(&obj, tags::PIXEL_SPACING) else { return Ok(SliceRead::Skip("missing geometry")); }; @@ -114,17 +110,28 @@ fn interpret(obj: DefaultDicomObject) -> Result { return Ok(SliceRead::Skip("missing geometry")); } - let signed = tag_u16(&obj, tags::PIXEL_REPRESENTATION).unwrap_or(0) == 1; - let slope = tag_f64(&obj, tags::RESCALE_SLOPE).unwrap_or(1.0); - let intercept = tag_f64(&obj, tags::RESCALE_INTERCEPT).unwrap_or(0.0); - let bytes = pixels - .to_bytes() - .map_err(|_| anyhow::anyhow!("pixel data is not a primitive value"))?; - let Some(hu) = decode_hu(&bytes, rows, cols, bits, signed, slope, intercept) else { - return Ok(SliceRead::Skip("with an unsupported pixel format")); + let hu = if pixels { + let Some(element) = obj.get(tags::PIXEL_DATA) else { + return Ok(SliceRead::Skip("without pixel data")); + }; + let DicomValue::Primitive(_) = element.value() else { + return Ok(SliceRead::Skip("with encapsulated pixel data")); + }; + let signed = tag_u16(&obj, tags::PIXEL_REPRESENTATION).unwrap_or(0) == 1; + let slope = tag_f64(&obj, tags::RESCALE_SLOPE).unwrap_or(1.0); + let intercept = tag_f64(&obj, tags::RESCALE_INTERCEPT).unwrap_or(0.0); + let bytes = element + .to_bytes() + .map_err(|_| anyhow::anyhow!("pixel data is not a primitive value"))?; + let Some(hu) = decode_hu(&bytes, rows, cols, bits, signed, slope, intercept) else { + return Ok(SliceRead::Skip("with an unsupported pixel format")); + }; + Some(hu) + } else { + None }; - Ok(SliceRead::Image(Box::new(RawSlice { + Ok(SliceRead::Image(Box::new(SliceMeta { hu, rows, cols, @@ -135,15 +142,15 @@ fn interpret(obj: DefaultDicomObject) -> Result { col_spacing: spacing[1], instance: obj .get(tags::INSTANCE_NUMBER) - .and_then(|e| e.to_int::().ok()) + .and_then(|element| element.to_int::().ok()) .unwrap_or(0), - thickness: tag_f64(&obj, tags::SLICE_THICKNESS).filter(|t| *t > 0.0), + thickness: tag_f64(&obj, tags::SLICE_THICKNESS).filter(|thickness| *thickness > 0.0), invert, series_uid: tag_string(&obj, tags::SERIES_INSTANCE_UID), series_description: tag_string(&obj, tags::SERIES_DESCRIPTION), series_number: obj .get(tags::SERIES_NUMBER) - .and_then(|e| e.to_int::().ok()) + .and_then(|element| element.to_int::().ok()) .unwrap_or(0), frame_of_reference: tag_string(&obj, tags::FRAME_OF_REFERENCE_UID), modality: tag_string(&obj, tags::MODALITY), @@ -182,7 +189,7 @@ fn decode_hu( } else { for &byte in &bytes[..samples] { let stored = if signed { - i8::from_le_bytes([byte]) as f64 + f64::from(byte as i8) } else { f64::from(byte) }; @@ -205,8 +212,12 @@ fn quantize_hu(value: f64) -> i16 { fn tag_string(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> String { obj.get(tag) - .and_then(|e| e.string().ok()) - .map(clean_dicom) + .and_then(|element| element.string().ok()) + .map(|value| { + value + .trim_matches(|c: char| c.is_whitespace() || c == '\0') + .to_string() + }) .unwrap_or_default() } @@ -230,18 +241,12 @@ fn tag_f64s(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> Option> fn tag_vec3(obj: &DefaultDicomObject, tag: dicom_core::Tag) -> Option<[f64; 3]> { let values = tag_f64s(obj, tag)?; if values.len() < 3 { - return None; + None + } else { + Some([values[0], values[1], values[2]]) } - Some([values[0], values[1], values[2]]) } -fn clean_dicom(value: &str) -> String { - value - .trim_matches(|c: char| c.is_whitespace() || c == '\0') - .to_string() -} - -/// Breathing phase from a series description such as `Gated, 40.0%`. pub fn phase_percent(description: &str) -> Option { let end = description.rfind('%')?; let bytes = description.as_bytes(); @@ -261,7 +266,6 @@ pub fn phase_percent(description: &str) -> Option { token.parse().ok() } -/// `StudyDate` (`YYYYMMDD`) plus `StudyTime` (`HHMMSS.FFFFFF`) as UTC nanoseconds. pub fn study_epoch_nanos(date: &str, time: &str) -> Option { let date = date.trim(); if date.len() < 8 { @@ -271,52 +275,26 @@ pub fn study_epoch_nanos(date: &str, time: &str) -> Option { let month: u32 = date[4..6].parse().ok()?; let day: u32 = date[6..8].parse().ok()?; let days = days_from_civil(year, month, day)?; - let (hour, minute, second, frac) = parse_hms(time); - if hour > 23 || minute > 59 || second > 60 { - return None; - } + let (hour, minute, second) = parse_hms(time)?; let secs = days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second); - u64::try_from(secs) - .ok() - .map(|secs| secs * 1_000_000_000 + frac) + u64::try_from(secs).ok().map(|secs| secs * 1_000_000_000) } -fn parse_hms(time: &str) -> (u32, u32, u32, u64) { +fn parse_hms(time: &str) -> Option<(u32, u32, u32)> { let time = time.trim(); - let hour = parse_digits(time, 0, 2).unwrap_or(0); - let minute = parse_digits(time, 2, 2).unwrap_or(0); - let second = parse_digits(time, 4, 2).unwrap_or(0); - let frac = fractional_nanos(time.get(6..).unwrap_or("")); - (hour, minute, second, frac) -} - -fn parse_digits(text: &str, start: usize, len: usize) -> Option { - let end = start + len; - if text.len() < end { - return None; + if time.is_empty() { + return Some((0, 0, 0)); } - text[start..end].parse().ok() -} - -fn fractional_nanos(rest: &str) -> u64 { - let digits: String = rest - .trim() - .trim_start_matches('.') - .chars() - .take_while(|c| c.is_ascii_digit()) - .collect(); - if digits.is_empty() { - return 0; + if time.len() < 6 { + return None; } - let mut padded = digits; - if padded.len() > 9 { - padded.truncate(9); - } else { - while padded.len() < 9 { - padded.push('0'); - } + let hour: u32 = time[0..2].parse().ok()?; + let minute: u32 = time[2..4].parse().ok()?; + let second: u32 = time[4..6].parse().ok()?; + if hour > 23 || minute > 59 || second > 60 { + return None; } - padded.parse().unwrap_or(0) + Some((hour, minute, second)) } /// Days since the Unix epoch. Howard Hinnant's `days_from_civil`. @@ -345,7 +323,10 @@ mod tests { #[test] fn parses_phase_percent() { - assert_eq!(phase_percent("P4^P100^S100^I0, Gated, 40.0%"), Some(40.0)); + assert_eq!( + phase_percent("P4^P100^S300^I00008, Gated, 50.0%A"), + Some(50.0) + ); assert_eq!(phase_percent("Gated, 0.0%"), Some(0.0)); assert_eq!(phase_percent("100%"), Some(100.0)); assert_eq!(phase_percent("no phase here"), None); @@ -363,8 +344,8 @@ mod tests { Some(874_346_835_000_000_000) ); assert_eq!( - study_epoch_nanos("19970915", "180715.5"), - Some(874_346_835_500_000_000) + study_epoch_nanos("20030702", ""), + Some(1_057_104_000_000_000_000) ); assert_eq!(study_epoch_nanos("", "120000"), None); assert_eq!(study_epoch_nanos("20001301", ""), None); diff --git a/foxglove_sdk/dicom_data_loader/rust/src/render.rs b/foxglove_sdk/dicom_data_loader/rust/src/render.rs index a479d0c..89c1873 100644 --- a/foxglove_sdk/dicom_data_loader/rust/src/render.rs +++ b/foxglove_sdk/dicom_data_loader/rust/src/render.rs @@ -1,8 +1,6 @@ -//! Window CT slices, build orthogonal views, and pack point clouds. -//! -//! Lung and body masks are a border-connected air heuristic for the demo, not a -//! clinical segmentation. +//! Lung and body masks are a border-connected air heuristic, not a segmentation. +#[cfg_attr(not(test), allow(dead_code))] pub const MAX_CLOUD_POINTS: usize = 300_000; pub const POINT_STRIDE: usize = 16; @@ -14,6 +12,10 @@ const LUNG_MIN_HU: i16 = -1000; const BONE_HU: i16 = 250; const SLICE_AIR_HU: i16 = -900; +/// In-plane step for every cloud. Volume clouds also skip this many slices. +const PLANE_STRIDE: usize = 4; +const STACK_STRIDE: usize = 2; + const BONE_RGBA: [u8; 4] = [230, 220, 200, 255]; const LUNG_RGBA: [u8; 4] = [40, 210, 220, 140]; @@ -27,12 +29,6 @@ pub struct Cloud { pub data: Vec, } -impl Cloud { - pub fn point_count(&self) -> usize { - self.data.len() / POINT_STRIDE - } -} - #[derive(Clone, Copy)] pub struct Bounds { pub min: [f32; 3], @@ -55,7 +51,7 @@ pub struct Geom { pub invert: bool, } -pub struct SliceView<'a> { +pub struct SlicePx<'a> { pub hu: &'a [i16], pub ipp: [f64; 3], pub stack_mm: f64, @@ -69,117 +65,154 @@ pub struct SliceStat { pub lung_voxels: usize, } -pub struct SweepRender { - pub axial: Vec, - pub coronal: GrayImage, - pub sagittal: GrayImage, - pub coronal_y: Vec, - pub sagittal_y: Vec, - pub bone: Cloud, - pub lungs: Cloud, - pub slice_clouds: Vec, - pub stats: Vec, - pub frame: PatientFrame, +pub struct Mpr { + pub image: GrayImage, + pub line_y: Vec, } -pub struct PhaseRender { - pub axial: GrayImage, - pub coronal: GrayImage, - pub sagittal: GrayImage, - pub lungs: Cloud, - pub bone: Option, - pub lung_volume_ml: f64, +pub fn window_image(hu: &[i16], cols: usize, rows: usize, invert: bool) -> GrayImage { + GrayImage { + width: cols as u32, + height: rows as u32, + pixels: hu.iter().map(|&value| window_u8(value, invert)).collect(), + } } -struct Measured<'a> { - view: SliceView<'a>, - outside: Vec, - stat: SliceStat, +pub fn coronal(geom: &Geom, slices: &[SlicePx<'_>]) -> Mpr { + resample(geom, slices, geom.cols, |geom, row_col| { + (geom.rows / 2, row_col) + }) } -pub fn sweep(geom: &Geom, slices: &[SliceView<'_>]) -> SweepRender { - let measured = measure_all(geom, slices); - let frame = patient_frame(geom, slices); - let axial = slices - .iter() - .map(|slice| gray_image(slice.hu, geom.cols, geom.rows, geom.invert)) - .collect(); - let (coronal, coronal_y) = coronal(geom, slices); - let (sagittal, sagittal_y) = sagittal(geom, slices); - let bone = volume_cloud(geom, &measured, &frame, |hu, _| hu >= BONE_HU, BONE_RGBA); - let lungs = volume_cloud(geom, &measured, &frame, is_lung, LUNG_RGBA); - let slice_clouds = measured - .iter() - .map(|slice| { - slice_cloud( - geom, - &slice.view, - &frame, - |hu| hu >= SLICE_AIR_HU, - |hu| { - let g = window_u8(hu, geom.invert); - [g, g, g, 255] - }, - ) - }) - .collect(); - SweepRender { - axial, - coronal, - sagittal, - coronal_y, - sagittal_y, - bone, - lungs, - slice_clouds, - stats: measured.into_iter().map(|slice| slice.stat).collect(), - frame, +pub fn sagittal(geom: &Geom, slices: &[SlicePx<'_>]) -> Mpr { + resample(geom, slices, geom.rows, |geom, row_col| { + (row_col, geom.cols / 2) + }) +} + +fn slice_stat(geom: &Geom, slice: &SlicePx<'_>, outside: &[u8]) -> SliceStat { + let pixel_cm2 = geom.row_spacing * geom.col_spacing / 100.0; + let mut lung_voxels = 0usize; + let mut body_count = 0usize; + let mut body_sum = 0i64; + for (index, &hu) in slice.hu.iter().enumerate() { + if outside[index] != 0 { + continue; + } + body_count += 1; + body_sum += i64::from(hu); + if is_lung(hu, outside[index]) { + lung_voxels += 1; + } + } + let mean_hu = if body_count == 0 { + 0.0 + } else { + body_sum as f64 / body_count as f64 + }; + SliceStat { + z_mm: slice.ipp[2], + mean_hu, + lung_area_cm2: lung_voxels as f64 * pixel_cm2, + body_area_cm2: body_count as f64 * pixel_cm2, + lung_voxels, } } -pub fn phase( - geom: &Geom, - slices: &[SliceView<'_>], - origin: &PatientFrame, - with_bone: bool, -) -> PhaseRender { - let measured = measure_all(geom, slices); - let mid = slices.len() / 2; - let axial = gray_image(slices[mid].hu, geom.cols, geom.rows, geom.invert); - let (coronal, _) = coronal(geom, slices); - let (sagittal, _) = sagittal(geom, slices); - let bone = - with_bone.then(|| volume_cloud(geom, &measured, origin, |hu, _| hu >= BONE_HU, BONE_RGBA)); - let lungs = volume_cloud(geom, &measured, origin, is_lung, LUNG_RGBA); - let thicknesses = slice_thicknesses(slices, geom); - let voxel_area = geom.row_spacing * geom.col_spacing; - let lung_volume_ml = measured +pub fn lung_volume_ml(geom: &Geom, slices: &[SlicePx<'_>], stats: &[SliceStat]) -> f64 { + let area = geom.row_spacing * geom.col_spacing; + stats .iter() - .zip(thicknesses) - .map(|(slice, thickness)| slice.stat.lung_voxels as f64 * voxel_area * thickness / 1000.0) - .sum(); - PhaseRender { - axial, - coronal, - sagittal, - lungs, - bone, - lung_volume_ml, + .zip(slice_thicknesses(slices, geom)) + .map(|(stat, thickness)| stat.lung_voxels as f64 * area * thickness / 1000.0) + .sum() +} + +/// Slice stats and the lung cloud share one outside-air mask per slice. +pub fn lung_stats( + geom: &Geom, + slices: &[SlicePx<'_>], + frame: &PatientFrame, +) -> (Vec, Cloud) { + let mut stats = Vec::with_capacity(slices.len()); + let mut data = Vec::new(); + for (index, slice) in slices.iter().enumerate() { + let outside = outside_air(slice.hu, geom.rows, geom.cols); + stats.push(slice_stat(geom, slice, &outside)); + if index % STACK_STRIDE != 0 { + continue; + } + for row in (0..geom.rows).step_by(PLANE_STRIDE) { + for col in (0..geom.cols).step_by(PLANE_STRIDE) { + let offset = row * geom.cols + col; + if is_lung(slice.hu[offset], outside[offset]) { + push_point( + &mut data, + point_meters(geom, slice, frame, row, col), + LUNG_RGBA, + ); + } + } + } } + (stats, Cloud { data }) } -/// Patient frame centered on the volume so the anatomy sits at the origin. -/// -/// 4D playback reuses the first phase's frame. Recentering each phase would -/// cancel the diaphragm motion the coronal view is meant to show. -pub fn patient_frame(geom: &Geom, slices: &[SliceView<'_>]) -> PatientFrame { - let corners = corners(geom, slices); - let mut min_mm = corners[0]; - let mut max_mm = corners[0]; - for corner in &corners { - for axis in 0..3 { - min_mm[axis] = min_mm[axis].min(corner[axis]); - max_mm[axis] = max_mm[axis].max(corner[axis]); +pub fn bone_cloud(geom: &Geom, slices: &[SlicePx<'_>], frame: &PatientFrame) -> Cloud { + let mut data = Vec::new(); + for (index, slice) in slices.iter().enumerate() { + if index % STACK_STRIDE != 0 { + continue; + } + for row in (0..geom.rows).step_by(PLANE_STRIDE) { + for col in (0..geom.cols).step_by(PLANE_STRIDE) { + let offset = row * geom.cols + col; + if slice.hu[offset] >= BONE_HU { + push_point( + &mut data, + point_meters(geom, slice, frame, row, col), + BONE_RGBA, + ); + } + } + } + } + Cloud { data } +} + +pub fn slice_cloud(geom: &Geom, slice: &SlicePx<'_>, frame: &PatientFrame) -> Cloud { + let mut data = Vec::new(); + for row in (0..geom.rows).step_by(PLANE_STRIDE) { + for col in (0..geom.cols).step_by(PLANE_STRIDE) { + let hu = slice.hu[row * geom.cols + col]; + if hu >= SLICE_AIR_HU { + let gray = window_u8(hu, geom.invert); + push_point( + &mut data, + point_meters(geom, slice, frame, row, col), + [gray, gray, gray, 255], + ); + } + } + } + Cloud { data } +} + +pub fn patient_frame(geom: &Geom, slices: &[SlicePx<'_>]) -> PatientFrame { + let mut min_mm = voxel_mm(geom, slices[0].ipp, 0, 0); + let mut max_mm = min_mm; + let last = slices.len() - 1; + let rows = geom.rows.saturating_sub(1); + let cols = geom.cols.saturating_sub(1); + for &index in &[0, last] { + for &row in &[0, rows] { + for &col in &[0, cols] { + let corner = voxel_mm(geom, slices[index].ipp, row, col); + for axis in 0..3 { + min_mm[axis] = min_mm[axis].min(corner[axis]); + max_mm[axis] = max_mm[axis].max(corner[axis]); + } + } } } let origin_mm = [ @@ -200,83 +233,71 @@ pub fn patient_frame(geom: &Geom, slices: &[SliceView<'_>]) -> PatientFrame { PatientFrame { origin_mm, bounds } } -fn measure_all<'a>(geom: &Geom, slices: &[SliceView<'a>]) -> Vec> { - let pixel_cm2 = geom.row_spacing * geom.col_spacing / 100.0; - slices - .iter() - .map(|view| { - let outside = outside_air(view.hu, geom.rows, geom.cols); - let mut lung_voxels = 0usize; - let mut body_count = 0usize; - let mut body_sum = 0i64; - for (index, &hu) in view.hu.iter().enumerate() { - if outside[index] != 0 { - continue; - } - body_count += 1; - body_sum += i64::from(hu); - if is_lung(hu, 0) { - lung_voxels += 1; - } - } - let mean_hu = if body_count == 0 { - 0.0 - } else { - body_sum as f64 / body_count as f64 - }; - Measured { - view: SliceView { - hu: view.hu, - ipp: view.ipp, - stack_mm: view.stack_mm, - }, - outside, - stat: SliceStat { - z_mm: view.ipp[2], - mean_hu, - lung_area_cm2: lung_voxels as f64 * pixel_cm2, - body_area_cm2: body_count as f64 * pixel_cm2, - lung_voxels, - }, - } - }) - .collect() +fn resample( + geom: &Geom, + slices: &[SlicePx<'_>], + width: usize, + sample_at: impl Fn(&Geom, usize) -> (usize, usize), +) -> Mpr { + let grid = mpr_grid(slices, (geom.row_spacing + geom.col_spacing) * 0.5); + let mut pixels = Vec::with_capacity(width * grid.height); + for y in 0..grid.height { + let (lo, hi, t) = bracket(slices, grid.z_at(y)); + for x in 0..width { + let (row, col) = sample_at(geom, x); + pixels.push(window_u8( + interp(slices, geom, lo, hi, t, row, col), + geom.invert, + )); + } + } + Mpr { + image: GrayImage { + width: width as u32, + height: grid.height as u32, + pixels, + }, + line_y: slices + .iter() + .map(|slice| grid.y_of(slice.stack_mm)) + .collect(), + } } fn outside_air(hu: &[i16], rows: usize, cols: usize) -> Vec { let mut outside = vec![0u8; hu.len()]; - let mut stack = Vec::new(); - let push = |idx: usize, outside: &mut [u8], stack: &mut Vec| { - if outside[idx] == 0 && hu[idx] < AIR_HU { - outside[idx] = 1; - stack.push(idx); - } - }; if rows == 0 || cols == 0 { return outside; } + let mut stack = Vec::new(); + let consider = |index: usize, outside: &mut [u8], stack: &mut Vec| { + if outside[index] == 0 && hu[index] < AIR_HU { + outside[index] = 1; + stack.push(index); + } + }; for col in 0..cols { - push(col, &mut outside, &mut stack); - push((rows - 1) * cols + col, &mut outside, &mut stack); + consider(col, &mut outside, &mut stack); + consider((rows - 1) * cols + col, &mut outside, &mut stack); } for row in 0..rows { - push(row * cols, &mut outside, &mut stack); - push(row * cols + cols - 1, &mut outside, &mut stack); + consider(row * cols, &mut outside, &mut stack); + consider(row * cols + cols - 1, &mut outside, &mut stack); } while let Some(index) = stack.pop() { let row = index / cols; let col = index % cols; if row > 0 { - push(index - cols, &mut outside, &mut stack); + consider(index - cols, &mut outside, &mut stack); } if row + 1 < rows { - push(index + cols, &mut outside, &mut stack); + consider(index + cols, &mut outside, &mut stack); } if col > 0 { - push(index - 1, &mut outside, &mut stack); + consider(index - 1, &mut outside, &mut stack); } if col + 1 < cols { - push(index + 1, &mut outside, &mut stack); + consider(index + 1, &mut outside, &mut stack); } } outside @@ -286,58 +307,6 @@ fn is_lung(hu: i16, outside: u8) -> bool { outside == 0 && (LUNG_MIN_HU..AIR_HU).contains(&hu) } -fn coronal(geom: &Geom, slices: &[SliceView<'_>]) -> (GrayImage, Vec) { - let grid = mpr_grid(slices, in_plane_spacing(geom)); - let mid_row = geom.rows / 2; - let mut pixels = Vec::with_capacity(geom.cols * grid.height); - for y in 0..grid.height { - let z = grid.z_at(y); - let (lo, hi, t) = bracket(slices, z); - for col in 0..geom.cols { - let hu = interp(slices, geom, lo, hi, t, mid_row, col); - pixels.push(window_u8(hu, geom.invert)); - } - } - let ys = slices - .iter() - .map(|slice| grid.y_of(slice.stack_mm)) - .collect(); - ( - GrayImage { - width: geom.cols as u32, - height: grid.height as u32, - pixels, - }, - ys, - ) -} - -fn sagittal(geom: &Geom, slices: &[SliceView<'_>]) -> (GrayImage, Vec) { - let grid = mpr_grid(slices, in_plane_spacing(geom)); - let mid_col = geom.cols / 2; - let mut pixels = Vec::with_capacity(geom.rows * grid.height); - for y in 0..grid.height { - let z = grid.z_at(y); - let (lo, hi, t) = bracket(slices, z); - for row in 0..geom.rows { - let hu = interp(slices, geom, lo, hi, t, row, mid_col); - pixels.push(window_u8(hu, geom.invert)); - } - } - let ys = slices - .iter() - .map(|slice| grid.y_of(slice.stack_mm)) - .collect(); - ( - GrayImage { - width: geom.rows as u32, - height: grid.height as u32, - pixels, - }, - ys, - ) -} - struct Grid { height: usize, z_min: f64, @@ -362,7 +331,7 @@ impl Grid { } } -fn mpr_grid(slices: &[SliceView<'_>], spacing: f64) -> Grid { +fn mpr_grid(slices: &[SlicePx<'_>], spacing: f64) -> Grid { let z_min = slices.first().map(|slice| slice.stack_mm).unwrap_or(0.0); let z_max = slices.last().map(|slice| slice.stack_mm).unwrap_or(z_min); let span = (z_max - z_min).max(0.0); @@ -378,7 +347,7 @@ fn mpr_grid(slices: &[SliceView<'_>], spacing: f64) -> Grid { } } -fn bracket(slices: &[SliceView<'_>], target: f64) -> (usize, usize, f32) { +fn bracket(slices: &[SlicePx<'_>], target: f64) -> (usize, usize, f32) { if slices.len() <= 1 || target <= slices[0].stack_mm { return (0, 0, 0.0); } @@ -406,7 +375,7 @@ fn bracket(slices: &[SliceView<'_>], target: f64) -> (usize, usize, f32) { } fn interp( - slices: &[SliceView<'_>], + slices: &[SlicePx<'_>], geom: &Geom, lo: usize, hi: usize, @@ -419,146 +388,53 @@ fn interp( (f32::from(a) + (f32::from(b) - f32::from(a)) * t).round() as i16 } -fn gray_image(hu: &[i16], cols: usize, rows: usize, invert: bool) -> GrayImage { - GrayImage { - width: cols as u32, - height: rows as u32, - pixels: hu.iter().map(|&value| window_u8(value, invert)).collect(), - } -} - fn window_u8(hu: i16, invert: bool) -> u8 { let t = ((f32::from(hu) - WINDOW_MIN) / WINDOW_WIDTH).clamp(0.0, 1.0); let value = (t * 255.0).round() as u8; if invert { 255 - value } else { value } } -fn in_plane_spacing(geom: &Geom) -> f64 { - (geom.row_spacing + geom.col_spacing) * 0.5 -} - -fn volume_cloud( - geom: &Geom, - slices: &[Measured<'_>], - frame: &PatientFrame, - pred: impl Fn(i16, u8) -> bool, - rgba: [u8; 4], -) -> Cloud { - let stride = choose_stride(geom, slices, &pred); - let mut data = Vec::new(); - for (index, slice) in slices.iter().enumerate() { - if index % stride != 0 { - continue; - } - for row in (0..geom.rows).step_by(stride) { - for col in (0..geom.cols).step_by(stride) { - let offset = row * geom.cols + col; - let hu = slice.view.hu[offset]; - if pred(hu, slice.outside[offset]) { - push_point( - &mut data, - voxel_meters(geom, &slice.view, frame, row, col), - rgba, - ); - } - } - } - } - Cloud { data } -} - -fn slice_cloud( - geom: &Geom, - slice: &SliceView<'_>, - frame: &PatientFrame, - pred: impl Fn(i16) -> bool, - color: impl Fn(i16) -> [u8; 4], -) -> Cloud { - let stride = choose_slice_stride(geom, slice, &pred); - let mut data = Vec::new(); - for row in (0..geom.rows).step_by(stride) { - for col in (0..geom.cols).step_by(stride) { - let hu = slice.hu[row * geom.cols + col]; - if pred(hu) { - push_point( - &mut data, - voxel_meters(geom, slice, frame, row, col), - color(hu), - ); - } - } - } - Cloud { data } -} - -fn choose_stride(geom: &Geom, slices: &[Measured<'_>], pred: &impl Fn(i16, u8) -> bool) -> usize { - let mut stride = 2; - loop { - let mut count = 0usize; - for (index, slice) in slices.iter().enumerate() { - if index % stride != 0 { - continue; - } - for row in (0..geom.rows).step_by(stride) { - for col in (0..geom.cols).step_by(stride) { - let offset = row * geom.cols + col; - if pred(slice.view.hu[offset], slice.outside[offset]) { - count += 1; - } - } - } - } - if count <= MAX_CLOUD_POINTS || stride >= 12 { - return stride; - } - stride += 1; - } -} - -fn choose_slice_stride(geom: &Geom, slice: &SliceView<'_>, pred: &impl Fn(i16) -> bool) -> usize { - let mut stride = 2; - loop { - let mut count = 0usize; - for row in (0..geom.rows).step_by(stride) { - for col in (0..geom.cols).step_by(stride) { - if pred(slice.hu[row * geom.cols + col]) { - count += 1; - } - } - } - if count <= MAX_CLOUD_POINTS || stride >= 12 { - return stride; - } - stride += 1; +fn slice_thicknesses(slices: &[SlicePx<'_>], geom: &Geom) -> Vec { + let fallback = ((geom.row_spacing + geom.col_spacing) * 0.5).max(1.0); + if slices.len() <= 1 { + return vec![fallback]; } + (0..slices.len()) + .map(|index| { + let delta = if index == 0 { + slices[1].stack_mm - slices[0].stack_mm + } else if index + 1 == slices.len() { + slices[index].stack_mm - slices[index - 1].stack_mm + } else { + (slices[index + 1].stack_mm - slices[index - 1].stack_mm) * 0.5 + }; + delta.abs().max(1e-3) + }) + .collect() } -fn push_point(data: &mut Vec, point: [f32; 3], rgba: [u8; 4]) { - data.extend_from_slice(&point[0].to_le_bytes()); - data.extend_from_slice(&point[1].to_le_bytes()); - data.extend_from_slice(&point[2].to_le_bytes()); - data.extend_from_slice(&rgba); -} - -fn voxel_meters( +fn point_meters( geom: &Geom, - slice: &SliceView<'_>, + slice: &SlicePx<'_>, frame: &PatientFrame, row: usize, col: usize, ) -> [f32; 3] { - let mm = [ - slice.ipp[0] + to_meters(voxel_mm(geom, slice.ipp, row, col), frame.origin_mm) +} + +fn voxel_mm(geom: &Geom, ipp: [f64; 3], row: usize, col: usize) -> [f64; 3] { + [ + ipp[0] + col as f64 * geom.col_spacing * geom.row_dir[0] + row as f64 * geom.row_spacing * geom.col_dir[0], - slice.ipp[1] + ipp[1] + col as f64 * geom.col_spacing * geom.row_dir[1] + row as f64 * geom.row_spacing * geom.col_dir[1], - slice.ipp[2] + ipp[2] + col as f64 * geom.col_spacing * geom.row_dir[2] + row as f64 * geom.row_spacing * geom.col_dir[2], - ]; - to_meters(mm, frame.origin_mm) + ] } fn to_meters(mm: [f64; 3], origin_mm: [f64; 3]) -> [f32; 3] { @@ -569,49 +445,9 @@ fn to_meters(mm: [f64; 3], origin_mm: [f64; 3]) -> [f32; 3] { ] } -fn corners(geom: &Geom, slices: &[SliceView<'_>]) -> [[f64; 3]; 8] { - let last = slices.len().saturating_sub(1); - let rows = geom.rows.saturating_sub(1); - let cols = geom.cols.saturating_sub(1); - let mut out = [[0.0; 3]; 8]; - let mut index = 0; - for &slice_index in &[0, last] { - for &row in &[0, rows] { - for &col in &[0, cols] { - let slice = &slices[slice_index]; - out[index] = [ - slice.ipp[0] - + col as f64 * geom.col_spacing * geom.row_dir[0] - + row as f64 * geom.row_spacing * geom.col_dir[0], - slice.ipp[1] - + col as f64 * geom.col_spacing * geom.row_dir[1] - + row as f64 * geom.row_spacing * geom.col_dir[1], - slice.ipp[2] - + col as f64 * geom.col_spacing * geom.row_dir[2] - + row as f64 * geom.row_spacing * geom.col_dir[2], - ]; - index += 1; - } - } - } - out -} - -fn slice_thicknesses(slices: &[SliceView<'_>], geom: &Geom) -> Vec { - let fallback = in_plane_spacing(geom).max(1.0); - if slices.len() <= 1 { - return vec![fallback]; - } - let mut thicknesses = Vec::with_capacity(slices.len()); - for index in 0..slices.len() { - let delta = if index == 0 { - slices[1].stack_mm - slices[0].stack_mm - } else if index + 1 == slices.len() { - slices[index].stack_mm - slices[index - 1].stack_mm - } else { - (slices[index + 1].stack_mm - slices[index - 1].stack_mm) * 0.5 - }; - thicknesses.push(delta.abs().max(1e-3)); - } - thicknesses +fn push_point(data: &mut Vec, point: [f32; 3], rgba: [u8; 4]) { + data.extend_from_slice(&point[0].to_le_bytes()); + data.extend_from_slice(&point[1].to_le_bytes()); + data.extend_from_slice(&point[2].to_le_bytes()); + data.extend_from_slice(&rgba); } diff --git a/foxglove_sdk/dicom_data_loader/rust/src/study.rs b/foxglove_sdk/dicom_data_loader/rust/src/study.rs index ff5c595..af2cf7b 100644 --- a/foxglove_sdk/dicom_data_loader/rust/src/study.rs +++ b/foxglove_sdk/dicom_data_loader/rust/src/study.rs @@ -1,14 +1,12 @@ -//! Group slices into a sweep or a breathing cycle and play them on a timeline. - use std::collections::{BTreeMap, BTreeSet}; -use std::io::Read; +use std::io::{self, Read}; use std::rc::Rc; use foxglove_data_loader::Message; use crate::messages::{self, MetadataMsg, PhaseStatsMsg, SliceStatsMsg}; -use crate::parse::{self, RawSlice, SliceRead}; -use crate::render::{self, Bounds, Cloud, Geom, GrayImage, SliceView}; +use crate::parse::{self, SliceMeta, SliceRead}; +use crate::render::{self, Geom, SlicePx}; const SWEEP_DT_NS: u64 = 100_000_000; const PHASE_DT_NS: u64 = 400_000_000; @@ -39,6 +37,29 @@ pub enum SchemaKind { Metadata, } +const TOPICS: &[(u16, &str, SchemaKind)] = &[ + (CH_AXIAL, "/dicom/axial", SchemaKind::RawImage), + (CH_CORONAL, "/dicom/coronal", SchemaKind::RawImage), + (CH_SAGITTAL, "/dicom/sagittal", SchemaKind::RawImage), + ( + CH_CORONAL_ANN, + "/dicom/coronal/annotations", + SchemaKind::ImageAnnotations, + ), + ( + CH_SAGITTAL_ANN, + "/dicom/sagittal/annotations", + SchemaKind::ImageAnnotations, + ), + (CH_BONE, "/dicom/bone", SchemaKind::PointCloud), + (CH_LUNGS, "/dicom/lungs", SchemaKind::PointCloud), + (CH_SLICE, "/dicom/slice", SchemaKind::PointCloud), + (CH_SCENE, "/dicom/scene", SchemaKind::SceneUpdate), + (CH_TF, "/tf", SchemaKind::FrameTransforms), + (CH_STATS, "/dicom/stats", SchemaKind::SliceStats), + (CH_META, "/dicom/metadata", SchemaKind::Metadata), +]; + #[derive(Clone)] pub struct ChannelInfo { pub id: u16, @@ -53,47 +74,31 @@ pub struct EmptyStudy { pub warnings: Vec, } +struct Entry { + log_time: u64, + channel: u16, + data: Rc<[u8]>, +} + #[derive(Clone)] pub struct Study { inner: Rc, } struct Inner { - mode: &'static str, - base_ns: u64, - dt_ns: u64, - frame_count: usize, - slice_count: usize, - phase_percents: Vec, + entries: Vec, channels: Vec, warnings: Vec, log_line: String, - dynamic: Vec, - static_channels: Vec, - payload: Payload, - meta: MetadataMsg, - label: String, - bounds: Bounds, -} - -enum Payload { - Sweep(render::SweepRender), - Breathing { phases: Vec, bone: Cloud }, -} - -struct PhaseBody { - axial: GrayImage, - coronal: GrayImage, - sagittal: GrayImage, - lungs: Cloud, - stats: PhaseStatsMsg, + start: u64, + end: u64, } -struct SlicePx { - hu: Vec, +struct SliceRef { + path_idx: usize, ipp: [f64; 3], - stack_mm: f64, instance: i32, + stack_mm: f64, } struct Series { @@ -115,52 +120,45 @@ struct Series { study_description: String, study_date: String, study_time: String, - slices: Vec, + slices: Vec, +} + +struct Loaded { + hu: Vec, + ipp: [f64; 3], + stack_mm: f64, } -pub fn load_study(readers: impl IntoIterator) -> Result { - let mut raw = Vec::new(); +/// Header pass groups every path, then each chosen series is opened again for pixels. +pub fn load_study( + paths: &[String], + mut open: impl FnMut(&str) -> io::Result, +) -> Result { let mut skips: BTreeMap<&'static str, usize> = BTreeMap::new(); - let mut files = 0usize; - for reader in readers { - files += 1; - match parse::read_slice(reader) { - Ok(SliceRead::Image(slice)) => raw.push(*slice), + let mut headers = Vec::new(); + for (path_idx, path) in paths.iter().enumerate() { + let reader = match open(path) { + Ok(reader) => reader, + Err(_) => { + *skips.entry("that could not be opened").or_default() += 1; + continue; + } + }; + match parse::read_header(reader) { + Ok(SliceRead::Image(meta)) => headers.push((path_idx, *meta)), Ok(SliceRead::Skip(reason)) => *skips.entry(reason).or_default() += 1, Err(_) => *skips.entry("that could not be parsed").or_default() += 1, } } - if raw.is_empty() { - return Err(empty_failure(files, &skips)); + if headers.is_empty() { + return Err(empty_failure(paths.len(), &skips)); } - assemble(raw, skips) + assemble(headers, paths, &mut open, skips) } impl Study { - pub fn mode(&self) -> &str { - self.inner.mode - } - - pub fn slice_count(&self) -> usize { - self.inner.slice_count - } - - pub fn phase_percents(&self) -> &[f64] { - &self.inner.phase_percents - } - - pub fn frame_count(&self) -> usize { - self.inner.frame_count - } - pub fn time_range(&self) -> (u64, u64) { - let start = self.inner.base_ns; - let end = if self.inner.frame_count == 0 { - start - } else { - self.time_of(self.inner.frame_count - 1) - }; - (start, end) + (self.inner.start, self.inner.end) } pub fn channels(&self) -> &[ChannelInfo] { @@ -175,234 +173,41 @@ impl Study { &self.inner.log_line } - pub fn image_size(&self, channel: u16, frame: usize) -> Option<(u32, u32)> { - self.image(channel, frame) - .map(|image| (image.width, image.height)) - } - - pub fn image_bytes(&self, channel: u16, frame: usize) -> Option<&[u8]> { - self.image(channel, frame) - .map(|image| image.pixels.as_slice()) - } - - pub fn point_count(&self, channel: u16, frame: usize) -> Option { - self.cloud(channel, frame).map(Cloud::point_count) - } - - pub fn slice_z_mm(&self) -> Vec { - match &self.inner.payload { - Payload::Sweep(sweep) => sweep.stats.iter().map(|stat| stat.z_mm).collect(), - Payload::Breathing { .. } => Vec::new(), - } - } - - pub fn lung_volume_ml(&self, frame: usize) -> Option { - match &self.inner.payload { - Payload::Breathing { phases, .. } => { - Some(phases[frame % phases.len()].stats.lung_volume_ml) - } - Payload::Sweep(_) => None, - } - } - pub fn messages(&self, channels: &[u16], start: Option, end: Option) -> MessageIter { let start = start.unwrap_or(0); let end = end.unwrap_or(u64::MAX); - let frame = if start > end || self.inner.frame_count == 0 { - self.inner.frame_count + let index = if start > end { + self.inner.entries.len() } else { - self.first_frame(start) + self.inner + .entries + .partition_point(|entry| entry.log_time < start) }; MessageIter { study: self.clone(), channels: channels.iter().copied().collect(), end, - frame, - slot: 0, - slots: Vec::new(), + index, } } - pub fn backfill(&self, time: u64, channels: &[u16]) -> anyhow::Result> { + pub fn backfill(&self, time: u64, channels: &[u16]) -> Vec { + let end = self + .inner + .entries + .partition_point(|entry| entry.log_time <= time); let mut messages = Vec::new(); for &channel in channels { - let Some(frame) = self.latest_frame(channel, time) else { + let Some(entry) = self.inner.entries[..end] + .iter() + .rev() + .find(|entry| entry.channel == channel) + else { continue; }; - messages.push(self.encode(channel, frame, self.time_of(frame))?); - } - Ok(messages) - } - - fn time_of(&self, frame: usize) -> u64 { - self.inner.base_ns + self.inner.dt_ns.saturating_mul(frame as u64) - } - - fn first_frame(&self, start: u64) -> usize { - if start <= self.inner.base_ns { - return 0; - } - let Some(div) = (start - self.inner.base_ns).checked_div(self.inner.dt_ns) else { - return 0; - }; - let mut frame = div as usize; - if frame < self.inner.frame_count && self.time_of(frame) < start { - frame += 1; - } - frame - } - - fn latest_frame(&self, channel: u16, time: u64) -> Option { - if self.inner.frame_count == 0 || time < self.inner.base_ns { - return None; - } - if self.inner.static_channels.contains(&channel) { - return Some(0); - } - if !self.inner.dynamic.contains(&channel) { - return None; - } - let frame = (time - self.inner.base_ns) - .checked_div(self.inner.dt_ns) - .unwrap_or(0) as usize; - Some(frame.min(self.inner.frame_count - 1)) - } - - fn slots_for(&self, frame: usize, channels: &BTreeSet) -> Vec { - let mut ids = Vec::new(); - for &id in &self.inner.dynamic { - if channels.contains(&id) { - ids.push(id); - } - } - if frame == 0 { - for &id in &self.inner.static_channels { - if channels.contains(&id) { - ids.push(id); - } - } - } - ids.sort_unstable(); - ids - } - - fn encode(&self, channel: u16, frame: usize, time: u64) -> anyhow::Result { - match channel { - CH_AXIAL => messages::raw_image(channel, time, "axial", self.axial(frame)), - CH_CORONAL => messages::raw_image(channel, time, "coronal", self.coronal(frame)), - CH_SAGITTAL => messages::raw_image(channel, time, "sagittal", self.sagittal(frame)), - CH_CORONAL_ANN => { - let image = self.coronal(frame); - let y = self.line_y(true, frame); - messages::slice_line(channel, time, image.width, y) - } - CH_SAGITTAL_ANN => { - let image = self.sagittal(frame); - let y = self.line_y(false, frame); - messages::slice_line(channel, time, image.width, y) - } - CH_BONE => messages::cloud(channel, time, self.bone()), - CH_LUNGS => messages::cloud(channel, time, self.lungs(frame)), - CH_SLICE => messages::cloud(channel, time, self.slice_cloud(frame)), - CH_SCENE => messages::scene(channel, time, self.inner.bounds, &self.inner.label), - CH_TF => messages::transforms(channel, time), - CH_STATS => match &self.inner.payload { - Payload::Sweep(sweep) => { - let stat = &sweep.stats[frame]; - messages::slice_stats( - channel, - time, - &SliceStatsMsg { - z_mm: stat.z_mm, - mean_hu: stat.mean_hu, - lung_area_cm2: stat.lung_area_cm2, - body_area_cm2: stat.body_area_cm2, - }, - ) - } - Payload::Breathing { phases, .. } => { - messages::phase_stats(channel, time, &phases[frame % phases.len()].stats) - } - }, - CH_META => messages::metadata(channel, time, &self.inner.meta), - _ => anyhow::bail!("unknown channel {channel}"), - } - } - - fn image(&self, channel: u16, frame: usize) -> Option<&GrayImage> { - match channel { - CH_AXIAL => Some(self.axial(frame)), - CH_CORONAL => Some(self.coronal(frame)), - CH_SAGITTAL => Some(self.sagittal(frame)), - _ => None, - } - } - - fn axial(&self, frame: usize) -> &GrayImage { - match &self.inner.payload { - Payload::Sweep(sweep) => &sweep.axial[frame], - Payload::Breathing { phases, .. } => &phases[frame % phases.len()].axial, - } - } - - fn coronal(&self, frame: usize) -> &GrayImage { - match &self.inner.payload { - Payload::Sweep(sweep) => &sweep.coronal, - Payload::Breathing { phases, .. } => &phases[frame % phases.len()].coronal, - } - } - - fn sagittal(&self, frame: usize) -> &GrayImage { - match &self.inner.payload { - Payload::Sweep(sweep) => &sweep.sagittal, - Payload::Breathing { phases, .. } => &phases[frame % phases.len()].sagittal, - } - } - - fn line_y(&self, coronal: bool, frame: usize) -> f64 { - match &self.inner.payload { - Payload::Sweep(sweep) => { - if coronal { - sweep.coronal_y[frame] - } else { - sweep.sagittal_y[frame] - } - } - Payload::Breathing { .. } => 0.0, - } - } - - fn bone(&self) -> &Cloud { - match &self.inner.payload { - Payload::Sweep(sweep) => &sweep.bone, - Payload::Breathing { bone, .. } => bone, - } - } - - fn lungs(&self, frame: usize) -> &Cloud { - match &self.inner.payload { - Payload::Sweep(sweep) => &sweep.lungs, - Payload::Breathing { phases, .. } => &phases[frame % phases.len()].lungs, - } - } - - fn slice_cloud(&self, frame: usize) -> &Cloud { - match &self.inner.payload { - Payload::Sweep(sweep) => &sweep.slice_clouds[frame], - Payload::Breathing { .. } => panic!("slice clouds are only built in sweep mode"), - } - } - - fn cloud(&self, channel: u16, frame: usize) -> Option<&Cloud> { - match channel { - CH_BONE => Some(self.bone()), - CH_LUNGS => Some(self.lungs(frame)), - CH_SLICE => match &self.inner.payload { - Payload::Sweep(sweep) => Some(&sweep.slice_clouds[frame]), - Payload::Breathing { .. } => None, - }, - _ => None, + messages.push(message(entry)); } + messages } } @@ -410,64 +215,60 @@ pub struct MessageIter { study: Study, channels: BTreeSet, end: u64, - frame: usize, - slot: usize, - slots: Vec, + index: usize, } impl Iterator for MessageIter { type Item = anyhow::Result; fn next(&mut self) -> Option { - loop { - if self.frame >= self.study.frame_count() { - return None; - } - let time = self.study.time_of(self.frame); - if time > self.end { + let entries = &self.study.inner.entries; + while self.index < entries.len() { + let entry = &entries[self.index]; + if entry.log_time > self.end { return None; } - if self.slot == 0 && self.slots.is_empty() { - self.slots = self.study.slots_for(self.frame, &self.channels); + self.index += 1; + if self.channels.contains(&entry.channel) { + return Some(Ok(message(entry))); } - if self.slot >= self.slots.len() { - self.frame += 1; - self.slot = 0; - self.slots.clear(); - continue; - } - let channel = self.slots[self.slot]; - self.slot += 1; - return Some(self.study.encode(channel, self.frame, time)); } + None + } +} + +fn message(entry: &Entry) -> Message { + Message { + channel_id: entry.channel, + log_time: entry.log_time, + publish_time: entry.log_time, + data: entry.data.to_vec(), } } -fn assemble( - raw: Vec, +fn assemble( + headers: Vec<(usize, SliceMeta)>, + paths: &[String], + open: &mut impl FnMut(&str) -> io::Result, mut skips: BTreeMap<&'static str, usize>, ) -> Result { let mut grouped: BTreeMap = BTreeMap::new(); - for slice in raw { - push_slice(&mut grouped, slice, &mut skips); + for (path_idx, meta) in headers { + push_header(&mut grouped, path_idx, meta, &mut skips); } let mut series: Vec = grouped .into_values() - .filter(|s| !s.slices.is_empty()) + .filter(|series| !series.slices.is_empty()) .collect(); for series in &mut series { finalize(series); } if series.is_empty() { - return Err(empty_failure(0, &skips)); + return Err(empty_failure(paths.len(), &skips)); } let mut warnings = Vec::new(); - if let Some(skip) = format_skips(&skips) { - warnings.push(skip); - } - - if let Some(indices) = breathing_group(&series) { + let built = if let Some(indices) = breathing_group(&series) { let ignored = series.len().saturating_sub(indices.len()); if ignored > 0 { warnings.push(format!( @@ -476,7 +277,7 @@ fn assemble( } let mut chosen = take_indices(series, indices); chosen.sort_by(phase_order); - Ok(build_breathing(chosen, warnings)) + build_breathing(chosen, paths, open, &mut skips)? } else { let index = sweep_index(&series); let kept = series[index].slices.len(); @@ -487,76 +288,386 @@ fn assemble( )); } let chosen = take_indices(series, vec![index]); - Ok(build_sweep( + build_sweep( chosen.into_iter().next().expect("sweep series"), + paths, + open, + &mut skips, + )? + }; + if let Some(text) = format_skips(&skips) { + warnings.insert(0, text); + } + Ok(finish(built, warnings)) +} + +fn build_sweep( + series: Series, + paths: &[String], + open: &mut impl FnMut(&str) -> io::Result, + skips: &mut BTreeMap<&'static str, usize>, +) -> Result { + let loaded = load_pixels(&series, paths, open, skips); + if loaded.is_empty() { + return Err(empty_failure(paths.len(), skips)); + } + let geom = geom_of(&series); + let view = views(&loaded); + let frame = render::patient_frame(&geom, &view); + let coronal = render::coronal(&geom, &view); + let sagittal = render::sagittal(&geom, &view); + let (stats, lungs) = render::lung_stats(&geom, &view, &frame); + let bone = render::bone_cloud(&geom, &view, &frame); + let base = parse::study_epoch_nanos(&series.study_date, &series.study_time).unwrap_or(0); + let mut entries = Vec::new(); + for (index, slice) in view.iter().enumerate() { + let time = base + SWEEP_DT_NS * index as u64; + let image = render::window_image(slice.hu, geom.cols, geom.rows, geom.invert); + push( + &mut entries, + time, + CH_AXIAL, + messages::raw_image(time, "axial", &image), + ); + push( + &mut entries, + time, + CH_CORONAL_ANN, + messages::slice_line(time, coronal.image.width, coronal.line_y[index]), + ); + push( + &mut entries, + time, + CH_SAGITTAL_ANN, + messages::slice_line(time, sagittal.image.width, sagittal.line_y[index]), + ); + let cloud = render::slice_cloud(&geom, slice, &frame); + push(&mut entries, time, CH_SLICE, messages::cloud(time, &cloud)); + push( + &mut entries, + time, + CH_STATS, + messages::slice_stats(&SliceStatsMsg { + z_mm: stats[index].z_mm, + mean_hu: stats[index].mean_hu, + lung_area_cm2: stats[index].lung_area_cm2, + body_area_cm2: stats[index].body_area_cm2, + }), + ); + } + push( + &mut entries, + base, + CH_CORONAL, + messages::raw_image(base, "coronal", &coronal.image), + ); + push( + &mut entries, + base, + CH_SAGITTAL, + messages::raw_image(base, "sagittal", &sagittal.image), + ); + push(&mut entries, base, CH_BONE, messages::cloud(base, &bone)); + push(&mut entries, base, CH_LUNGS, messages::cloud(base, &lungs)); + let label = if series.description.is_empty() { + format!("CT · {} slices", loaded.len()) + } else { + series.description.clone() + }; + push( + &mut entries, + base, + CH_SCENE, + messages::scene(base, frame.bounds, &label), + ); + push(&mut entries, base, CH_TF, messages::transforms(base)); + push( + &mut entries, + base, + CH_META, + messages::metadata(&metadata( + &series, + std::slice::from_ref(&series), + "sweep", + loaded.len(), + 1, + )), + ); + Ok(Built { + mode: "sweep", + slices: loaded.len(), + phases: 1, + dt_ns: SWEEP_DT_NS, + entries, + }) +} + +struct PhaseBody { + axial: Rc<[u8]>, + coronal: Rc<[u8]>, + sagittal: Rc<[u8]>, + lungs: Rc<[u8]>, + stats: Rc<[u8]>, +} + +fn build_breathing( + series: Vec, + paths: &[String], + open: &mut impl FnMut(&str) -> io::Result, + skips: &mut BTreeMap<&'static str, usize>, +) -> Result { + let mut phases = Vec::new(); + // One origin for every phase. Recentering a later phase would cancel the diaphragm motion. + let mut origin = None; + let mut bone = None; + let base = parse::study_epoch_nanos(&series[0].study_date, &series[0].study_time).unwrap_or(0); + for (index, series) in series.iter().enumerate() { + let loaded = load_pixels(series, paths, open, skips); + if loaded.is_empty() { + continue; + } + let geom = geom_of(series); + let view = views(&loaded); + let frame = *origin.get_or_insert_with(|| render::patient_frame(&geom, &view)); + let (stats, lungs) = render::lung_stats(&geom, &view, &frame); + let mid = loaded.len() / 2; + let time = base + PHASE_DT_NS * phases.len() as u64; + let axial = + render::window_image(loaded[mid].hu.as_slice(), geom.cols, geom.rows, geom.invert); + let coronal = render::coronal(&geom, &view); + let sagittal = render::sagittal(&geom, &view); + if bone.is_none() { + bone = Some(render::bone_cloud(&geom, &view, &frame)); + } + let percent = series.phase.unwrap_or(index as f64); + phases.push(PhaseBody { + axial: Rc::from(messages::raw_image(time, "axial", &axial)), + coronal: Rc::from(messages::raw_image(time, "coronal", &coronal.image)), + sagittal: Rc::from(messages::raw_image(time, "sagittal", &sagittal.image)), + lungs: Rc::from(messages::cloud(time, &lungs)), + stats: Rc::from(messages::phase_stats(&PhaseStatsMsg { + phase_percent: percent, + lung_volume_ml: render::lung_volume_ml(&geom, &view, &stats), + })), + }); + } + if phases.is_empty() { + return Err(empty_failure(paths.len(), skips)); + } + let head = &series[0]; + let frame = origin.expect("a phase was rendered"); + let bone = bone.expect("a phase was rendered"); + let mut entries = Vec::new(); + for cycle in 0..BREATH_CYCLES { + for (index, phase) in phases.iter().enumerate() { + let time = base + PHASE_DT_NS * (cycle * phases.len() + index) as u64; + push_rc(&mut entries, time, CH_AXIAL, &phase.axial); + push_rc(&mut entries, time, CH_CORONAL, &phase.coronal); + push_rc(&mut entries, time, CH_SAGITTAL, &phase.sagittal); + push_rc(&mut entries, time, CH_LUNGS, &phase.lungs); + push_rc(&mut entries, time, CH_STATS, &phase.stats); + } + } + let label = format!("CT breathing · {} phases", phases.len()); + push(&mut entries, base, CH_BONE, messages::cloud(base, &bone)); + push( + &mut entries, + base, + CH_SCENE, + messages::scene(base, frame.bounds, &label), + ); + push(&mut entries, base, CH_TF, messages::transforms(base)); + push( + &mut entries, + base, + CH_META, + messages::metadata(&metadata( + head, + &series, + "4d", + head.slices.len(), + phases.len(), + )), + ); + Ok(Built { + mode: "4d", + slices: head.slices.len(), + phases: phases.len(), + dt_ns: PHASE_DT_NS, + entries, + }) +} + +struct Built { + mode: &'static str, + slices: usize, + phases: usize, + dt_ns: u64, + entries: Vec, +} + +fn finish(built: Built, warnings: Vec) -> Study { + let Built { + mode, + slices, + phases, + dt_ns, + mut entries, + } = built; + entries.sort_by(|a, b| a.log_time.cmp(&b.log_time).then(a.channel.cmp(&b.channel))); + let start = entries.first().map(|entry| entry.log_time).unwrap_or(0); + let end = entries.last().map(|entry| entry.log_time).unwrap_or(start); + let frame_count = ((end - start) / dt_ns) as usize + 1; + let channels = channels_from(&entries, mode); + let log_line = format!( + "{mode} mode: {slices} slices, {phases} phase(s), {frame_count} frames, {} ms/frame", + dt_ns / 1_000_000 + ); + Study { + inner: Rc::new(Inner { + entries, + channels, warnings, - )) + log_line, + start, + end, + }), + } +} + +fn channels_from(entries: &[Entry], mode: &str) -> Vec { + TOPICS + .iter() + .filter_map(|(id, topic, kind)| { + let kind = if *id == CH_STATS && mode == "4d" { + SchemaKind::PhaseStats + } else { + *kind + }; + let count = entries.iter().filter(|entry| entry.channel == *id).count() as u64; + (count > 0).then_some(ChannelInfo { + id: *id, + topic, + message_count: count, + kind, + }) + }) + .collect() +} + +fn push(entries: &mut Vec, time: u64, channel: u16, data: Vec) { + entries.push(Entry { + log_time: time, + channel, + data: Rc::from(data), + }); +} + +fn push_rc(entries: &mut Vec, time: u64, channel: u16, data: &Rc<[u8]>) { + entries.push(Entry { + log_time: time, + channel, + data: Rc::clone(data), + }); +} + +fn load_pixels( + series: &Series, + paths: &[String], + open: &mut impl FnMut(&str) -> io::Result, + skips: &mut BTreeMap<&'static str, usize>, +) -> Vec { + let mut loaded = Vec::with_capacity(series.slices.len()); + for slice in &series.slices { + let reader = match open(&paths[slice.path_idx]) { + Ok(reader) => reader, + Err(_) => { + *skips.entry("that could not be opened").or_default() += 1; + continue; + } + }; + match parse::read_pixels(reader) { + Ok(SliceRead::Image(meta)) => { + let Some(hu) = meta.hu else { + *skips.entry("without pixel data").or_default() += 1; + continue; + }; + loaded.push(Loaded { + hu, + ipp: slice.ipp, + stack_mm: slice.stack_mm, + }); + } + Ok(SliceRead::Skip(reason)) => *skips.entry(reason).or_default() += 1, + Err(_) => *skips.entry("that could not be parsed").or_default() += 1, + } } + loaded } -fn push_slice( +fn push_header( grouped: &mut BTreeMap, - slice: RawSlice, + path_idx: usize, + meta: SliceMeta, skips: &mut BTreeMap<&'static str, usize>, ) { - if let Some(series) = grouped.get_mut(&slice.series_uid) { - if !compatible(series, &slice) { + if let Some(series) = grouped.get_mut(&meta.series_uid) { + if !compatible(series, &meta) { *skips.entry("with inconsistent geometry").or_default() += 1; return; } - series.slices.push(SlicePx { - hu: slice.hu, - ipp: slice.ipp, + series.slices.push(SliceRef { + path_idx, + ipp: meta.ipp, + instance: meta.instance, stack_mm: 0.0, - instance: slice.instance, }); return; } - let uid = slice.series_uid.clone(); - grouped.insert(uid, Series::from_first(slice)); + let uid = meta.series_uid.clone(); + grouped.insert(uid, Series::from_first(path_idx, meta)); } impl Series { - fn from_first(slice: RawSlice) -> Self { - let normal = unit_normal(slice.row_dir, slice.col_dir); - let phase = parse::phase_percent(&slice.series_description); - let pixels = SlicePx { - hu: slice.hu, - ipp: slice.ipp, - stack_mm: 0.0, - instance: slice.instance, - }; + fn from_first(path_idx: usize, meta: SliceMeta) -> Self { + let phase = parse::phase_percent(&meta.series_description); + let normal = unit_normal(meta.row_dir, meta.col_dir); Self { - description: slice.series_description, - number: slice.series_number, - for_uid: slice.frame_of_reference, + description: meta.series_description, + number: meta.series_number, + for_uid: meta.frame_of_reference, phase, - rows: slice.rows, - cols: slice.cols, - row_spacing: slice.row_spacing, - col_spacing: slice.col_spacing, - row_dir: slice.row_dir, - col_dir: slice.col_dir, + rows: meta.rows, + cols: meta.cols, + row_spacing: meta.row_spacing, + col_spacing: meta.col_spacing, + row_dir: meta.row_dir, + col_dir: meta.col_dir, normal, - invert: slice.invert, - thickness: slice.thickness, - modality: slice.modality, - manufacturer: slice.manufacturer, - study_description: slice.study_description, - study_date: slice.study_date, - study_time: slice.study_time, - slices: vec![pixels], + invert: meta.invert, + thickness: meta.thickness, + modality: meta.modality, + manufacturer: meta.manufacturer, + study_description: meta.study_description, + study_date: meta.study_date, + study_time: meta.study_time, + slices: vec![SliceRef { + path_idx, + ipp: meta.ipp, + instance: meta.instance, + stack_mm: 0.0, + }], } } } -fn compatible(series: &Series, slice: &RawSlice) -> bool { - series.rows == slice.rows - && series.cols == slice.cols - && (series.row_spacing - slice.row_spacing).abs() < 0.05 - && (series.col_spacing - slice.col_spacing).abs() < 0.05 - && dot(series.row_dir, slice.row_dir) > 0.999 - && dot(series.col_dir, slice.col_dir) > 0.999 +fn compatible(series: &Series, meta: &SliceMeta) -> bool { + series.rows == meta.rows + && series.cols == meta.cols + && (series.row_spacing - meta.row_spacing).abs() < 0.05 + && (series.col_spacing - meta.col_spacing).abs() < 0.05 + && dot(series.row_dir, meta.row_dir) > 0.999 + && dot(series.col_dir, meta.col_dir) > 0.999 } fn finalize(series: &mut Series) { @@ -628,202 +739,6 @@ fn take_indices(series: Vec, mut indices: Vec) -> Vec { chosen } -fn build_sweep(series: Series, warnings: Vec) -> Study { - let rendered = { - let geom = geom_of(&series); - let views = views(&series); - render::sweep(&geom, &views) - }; - let frames = series.slices.len(); - let (channels, dynamic, static_channels) = sweep_channels(frames as u64); - finish( - "sweep", - &series, - std::slice::from_ref(&series), - frames, - 1, - SWEEP_DT_NS, - channels, - dynamic, - static_channels, - warnings, - Vec::new(), - rendered.frame.bounds, - sweep_label(&series), - Payload::Sweep(rendered), - ) -} - -fn build_breathing(series: Vec, warnings: Vec) -> Study { - let origin = { - let geom = geom_of(&series[0]); - let views = views(&series[0]); - render::patient_frame(&geom, &views) - }; - let mut phases = Vec::with_capacity(series.len()); - let mut percents = Vec::with_capacity(series.len()); - let mut bone = None; - for (index, series) in series.iter().enumerate() { - percents.push(series.phase.unwrap_or(index as f64)); - let geom = geom_of(series); - let views = views(series); - let rendered = render::phase(&geom, &views, &origin, index == 0); - if let Some(cloud) = rendered.bone { - bone = Some(cloud); - } - phases.push(PhaseBody { - axial: rendered.axial, - coronal: rendered.coronal, - sagittal: rendered.sagittal, - lungs: rendered.lungs, - stats: PhaseStatsMsg { - phase_percent: percents[index], - lung_volume_ml: rendered.lung_volume_ml, - }, - }); - } - let phase_count = phases.len(); - let frames = phase_count * BREATH_CYCLES; - let (channels, dynamic, static_channels) = breathing_channels(frames as u64); - let head = &series[0]; - finish( - "4d", - head, - &series, - head.slices.len(), - phase_count, - PHASE_DT_NS, - channels, - dynamic, - static_channels, - warnings, - percents, - origin.bounds, - format!("CT breathing · {phase_count} phases"), - Payload::Breathing { - phases, - bone: bone.expect("the first phase builds the bone cloud"), - }, - ) -} - -#[allow(clippy::too_many_arguments)] -fn finish( - mode: &'static str, - head: &Series, - described: &[Series], - slice_count: usize, - phase_count: usize, - dt_ns: u64, - channels: Vec, - dynamic: Vec, - static_channels: Vec, - warnings: Vec, - phase_percents: Vec, - bounds: Bounds, - label: String, - payload: Payload, -) -> Study { - let frame_count = match &payload { - Payload::Sweep(sweep) => sweep.axial.len(), - Payload::Breathing { phases, .. } => phases.len() * BREATH_CYCLES, - }; - let base_ns = parse::study_epoch_nanos(&head.study_date, &head.study_time).unwrap_or(0); - let log_line = format!( - "{mode} mode: {slice_count} slices, {phase_count} phase(s), {frame_count} frames, {} ms/frame", - dt_ns / 1_000_000 - ); - Study { - inner: Rc::new(Inner { - mode, - base_ns, - dt_ns, - frame_count, - slice_count, - phase_percents, - channels, - warnings, - log_line, - dynamic, - static_channels, - payload, - meta: metadata(head, described, mode, slice_count, phase_count), - label, - bounds, - }), - } -} - -fn sweep_channels(frames: u64) -> (Vec, Vec, Vec) { - let channels = vec![ - channel(CH_AXIAL, "/dicom/axial", frames, SchemaKind::RawImage), - channel(CH_CORONAL, "/dicom/coronal", 1, SchemaKind::RawImage), - channel(CH_SAGITTAL, "/dicom/sagittal", 1, SchemaKind::RawImage), - channel( - CH_CORONAL_ANN, - "/dicom/coronal/annotations", - frames, - SchemaKind::ImageAnnotations, - ), - channel( - CH_SAGITTAL_ANN, - "/dicom/sagittal/annotations", - frames, - SchemaKind::ImageAnnotations, - ), - channel(CH_BONE, "/dicom/bone", 1, SchemaKind::PointCloud), - channel(CH_LUNGS, "/dicom/lungs", 1, SchemaKind::PointCloud), - channel(CH_SLICE, "/dicom/slice", frames, SchemaKind::PointCloud), - channel(CH_SCENE, "/dicom/scene", 1, SchemaKind::SceneUpdate), - channel(CH_TF, "/tf", 1, SchemaKind::FrameTransforms), - channel(CH_STATS, "/dicom/stats", frames, SchemaKind::SliceStats), - channel(CH_META, "/dicom/metadata", 1, SchemaKind::Metadata), - ]; - let dynamic = vec![ - CH_AXIAL, - CH_CORONAL_ANN, - CH_SAGITTAL_ANN, - CH_SLICE, - CH_STATS, - ]; - let static_channels = vec![ - CH_CORONAL, - CH_SAGITTAL, - CH_BONE, - CH_LUNGS, - CH_SCENE, - CH_TF, - CH_META, - ]; - (channels, dynamic, static_channels) -} - -fn breathing_channels(frames: u64) -> (Vec, Vec, Vec) { - let channels = vec![ - channel(CH_AXIAL, "/dicom/axial", frames, SchemaKind::RawImage), - channel(CH_CORONAL, "/dicom/coronal", frames, SchemaKind::RawImage), - channel(CH_SAGITTAL, "/dicom/sagittal", frames, SchemaKind::RawImage), - channel(CH_BONE, "/dicom/bone", 1, SchemaKind::PointCloud), - channel(CH_LUNGS, "/dicom/lungs", frames, SchemaKind::PointCloud), - channel(CH_SCENE, "/dicom/scene", 1, SchemaKind::SceneUpdate), - channel(CH_TF, "/tf", 1, SchemaKind::FrameTransforms), - channel(CH_STATS, "/dicom/stats", frames, SchemaKind::PhaseStats), - channel(CH_META, "/dicom/metadata", 1, SchemaKind::Metadata), - ]; - let dynamic = vec![CH_AXIAL, CH_CORONAL, CH_SAGITTAL, CH_LUNGS, CH_STATS]; - let static_channels = vec![CH_BONE, CH_SCENE, CH_TF, CH_META]; - (channels, dynamic, static_channels) -} - -fn channel(id: u16, topic: &'static str, message_count: u64, kind: SchemaKind) -> ChannelInfo { - ChannelInfo { - id, - topic, - message_count, - kind, - } -} - fn metadata( head: &Series, described: &[Series], @@ -831,11 +746,17 @@ fn metadata( slice_count: usize, phase_count: usize, ) -> MetadataMsg { + let mut parts = Vec::new(); + for series in described { + if !series.description.is_empty() && !parts.contains(&series.description) { + parts.push(series.description.clone()); + } + } MetadataMsg { modality: head.modality.clone(), manufacturer: head.manufacturer.clone(), study_description: head.study_description.clone(), - series_description: joined_descriptions(described), + series_description: parts.join("; "), rows: head.rows as u32, cols: head.cols as u32, row_spacing_mm: head.row_spacing, @@ -847,24 +768,6 @@ fn metadata( } } -fn joined_descriptions(series: &[Series]) -> String { - let mut parts = Vec::new(); - for series in series { - if !series.description.is_empty() && !parts.contains(&series.description) { - parts.push(series.description.clone()); - } - } - parts.join("; ") -} - -fn sweep_label(series: &Series) -> String { - if series.description.is_empty() { - format!("CT · {} slices", series.slices.len()) - } else { - series.description.clone() - } -} - fn median_spacing(series: &Series) -> f64 { if series.slices.len() < 2 { return 1.0; @@ -890,11 +793,10 @@ fn geom_of(series: &Series) -> Geom { } } -fn views(series: &Series) -> Vec> { - series - .slices +fn views(loaded: &[Loaded]) -> Vec> { + loaded .iter() - .map(|slice| SliceView { + .map(|slice| SlicePx { hu: &slice.hu, ipp: slice.ipp, stack_mm: slice.stack_mm, @@ -915,7 +817,6 @@ fn format_skips(skips: &BTreeMap<&'static str, usize>) -> Option { } fn empty_failure(files: usize, skips: &BTreeMap<&'static str, usize>) -> EmptyStudy { - let warnings = format_skips(skips).into_iter().collect(); EmptyStudy { message: if files == 0 { "No DICOM files were provided".to_string() @@ -923,7 +824,7 @@ fn empty_failure(files: usize, skips: &BTreeMap<&'static str, usize>) -> EmptySt "No supported CT slices were found".to_string() }, tip: "Open every .dcm file from one series (or one 4D study) together. Only uncompressed little-endian monochrome images are read.".to_string(), - warnings, + warnings: format_skips(skips).into_iter().collect(), } } @@ -948,18 +849,76 @@ fn dot(a: [f64; 3], b: [f64; 3]) -> f64 { #[cfg(test)] mod tests { use super::*; + use prost::Message as ProstMessage; use std::path::{Path, PathBuf}; + // Foxglove's Timestamp::merge_field is unimplemented, so the schema types cannot be decoded. + #[derive(Clone, PartialEq, prost::Message)] + struct ImageMsg { + #[prost(fixed32, tag = "2")] + width: u32, + #[prost(fixed32, tag = "3")] + height: u32, + #[prost(string, tag = "4")] + encoding: String, + #[prost(bytes = "vec", tag = "6")] + data: Vec, + } + + #[derive(Clone, PartialEq, prost::Message)] + struct CloudMsg { + #[prost(bytes = "vec", tag = "6")] + data: Vec, + } + + #[derive(Clone, PartialEq, prost::Message)] + struct SliceStats { + #[prost(double, tag = "1")] + z_mm: f64, + } + + #[derive(Clone, PartialEq, prost::Message)] + struct PhaseStats { + #[prost(double, tag = "1")] + phase_percent: f64, + #[prost(double, tag = "2")] + lung_volume_ml: f64, + } + + #[derive(Clone, PartialEq, prost::Message)] + struct Meta { + #[prost(uint32, tag = "10")] + slice_count: u32, + #[prost(uint32, tag = "11")] + phase_count: u32, + #[prost(string, tag = "12")] + mode: String, + } + + fn data_root() -> PathBuf { + match std::env::var("DICOM_DATA_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../data"), + } + } + fn load_optional(name: &str) -> Option { - let Ok(root) = std::env::var("DICOM_DATA_DIR") else { - eprintln!("skip {name}: DICOM_DATA_DIR is not set"); + let dir = data_root().join(name); + if !dir.is_dir() { + eprintln!("skip {name}: {} is absent", dir.display()); return None; - }; - let dir = PathBuf::from(root).join(name); + } let files = collect_dcm(&dir); - assert!(!files.is_empty(), "no .dcm files under {}", dir.display()); - eprintln!("loading {name} ({} files)", files.len()); - let study = load_study(files.iter().map(|path| std::fs::File::open(path).unwrap())) + if files.is_empty() { + eprintln!("skip {name}: no .dcm files in {}", dir.display()); + return None; + } + let paths: Vec = files + .iter() + .map(|path| path.display().to_string()) + .collect(); + eprintln!("loading {name} ({} files)", paths.len()); + let study = load_study(&paths, |path| std::fs::File::open(path)) .unwrap_or_else(|err| panic!("{}: {}", err.message, err.tip)); eprintln!("{}", study.log_line()); Some(study) @@ -988,12 +947,50 @@ mod tests { out } + fn of_channel(study: &Study, channel: u16) -> Vec { + study + .messages(&[channel], None, None) + .map(|message| message.unwrap()) + .collect() + } + + fn decode(message: &Message) -> T { + T::decode(message.data.as_slice()).expect("prost decode") + } + + fn assert_image(message: &Message, width: u32) -> ImageMsg { + let image: ImageMsg = decode(message); + assert_eq!(image.width, width); + assert!(image.height > 0); + assert_eq!(image.encoding, "mono8"); + assert_eq!( + image.data.len(), + image.width as usize * image.height as usize + ); + assert!(image.data.iter().any(|value| *value > 20)); + assert!(image.data.iter().any(|value| *value < 230)); + image + } + + fn point_count(message: &Message) -> usize { + decode::(message).data.len() / render::POINT_STRIDE + } + fn assert_playback(study: &Study, dt: u64) { let (start, end) = study.time_range(); assert!(start > 0, "study clock should parse"); - assert_eq!(end - start, (study.frame_count() as u64 - 1) * dt); + assert_eq!( + end - start, + (study + .channels() + .iter() + .map(|channel| channel.message_count) + .max() + .unwrap_or(1) + - 1) + * dt + ); assert!(study.warnings().is_empty(), "{:?}", study.warnings()); - let ids: Vec = study.channels().iter().map(|channel| channel.id).collect(); let mut last = None; let mut counts: BTreeMap = BTreeMap::new(); @@ -1028,18 +1025,21 @@ mod tests { assert_eq!(partial.len(), 2); assert_eq!(partial[0].log_time, start + dt); assert_eq!(partial[1].log_time, start + 2 * dt); - assert!(partial.iter().all(|message| message.channel_id == CH_AXIAL)); - assert!(study.backfill(start - 1, &ids).unwrap().is_empty()); - let at_start = study.backfill(start, &ids).unwrap(); + assert!(study.backfill(start - 1, &ids).is_empty()); + let at_start = study.backfill(start, &ids); assert_eq!(at_start.len(), ids.len()); assert!(at_start.iter().all(|message| message.log_time == start)); - let between = study.backfill(start + dt / 2, &ids).unwrap(); - assert!(between.iter().all(|message| message.log_time == start)); - let stepped = study.backfill(start + dt, &[CH_AXIAL]).unwrap(); + assert!( + study + .backfill(start + dt / 2, &ids) + .iter() + .all(|message| message.log_time == start) + ); + let stepped = study.backfill(start + dt, &[CH_AXIAL]); assert_eq!(stepped.len(), 1); assert_eq!(stepped[0].log_time, start + dt); - let past = study.backfill(end + dt, &[CH_AXIAL, CH_BONE]).unwrap(); + let past = study.backfill(end + dt, &[CH_AXIAL, CH_BONE]); assert_eq!( past.iter() .find(|message| message.channel_id == CH_AXIAL) @@ -1056,39 +1056,33 @@ mod tests { ); } - fn assert_image(study: &Study, channel: u16, frame: usize, width: u32) { - let (w, h) = study.image_size(channel, frame).unwrap(); - assert_eq!(w, width); - assert!(h > 0); - let pixels = study.image_bytes(channel, frame).unwrap(); - assert_eq!(pixels.len(), w as usize * h as usize); - assert!(pixels.iter().any(|value| *value > 20)); - assert!(pixels.iter().any(|value| *value < 230)); - } - #[test] - fn lidc_is_a_sweep_through_the_chest() { - let Some(study) = load_optional("lidc") else { + fn chest_is_a_sweep() { + let Some(study) = load_optional("chest") else { return; }; - assert_eq!(study.mode(), "sweep"); - assert_eq!(study.slice_count(), 133); - assert_eq!(study.frame_count(), 133); - assert!(study.phase_percents().is_empty()); + assert!(study.log_line().starts_with("sweep mode")); assert_playback(&study, SWEEP_DT_NS); - assert_image(&study, CH_AXIAL, 66, 512); - let (coronal_w, coronal_h) = study.image_size(CH_CORONAL, 0).unwrap(); - assert_eq!(coronal_w, 512); - assert!(coronal_h > 200, "coronal height {coronal_h}"); - let (sagittal_w, sagittal_h) = study.image_size(CH_SAGITTAL, 0).unwrap(); - assert_eq!(sagittal_w, 512); - assert!(sagittal_h > 200, "sagittal height {sagittal_h}"); - let z = study.slice_z_mm(); + let meta: Meta = decode(&of_channel(&study, CH_META)[0]); + assert_eq!(meta.mode, "sweep"); + assert_eq!(meta.slice_count, 133); + let axial = of_channel(&study, CH_AXIAL); + assert_eq!(axial.len(), 133); + assert_image(&axial[66], 512); + let coronal = assert_image(&of_channel(&study, CH_CORONAL)[0], 512); + assert!(coronal.height > 300, "coronal height {}", coronal.height); + let sagittal = assert_image(&of_channel(&study, CH_SAGITTAL)[0], 512); + assert!(sagittal.height > 300, "sagittal height {}", sagittal.height); + let z: Vec = of_channel(&study, CH_STATS) + .iter() + .map(|message| decode::(message).z_mm) + .collect(); assert_eq!(z.len(), 133); assert!(z.windows(2).all(|pair| pair[1] >= pair[0])); assert!(z.last().unwrap() - z.first().unwrap() > 200.0); for channel in [CH_BONE, CH_LUNGS, CH_SLICE] { - let points = study.point_count(channel, 66).unwrap(); + let points = + point_count(&of_channel(&study, channel)[if channel == CH_SLICE { 66 } else { 0 }]); assert!( points > 100 && points <= render::MAX_CLOUD_POINTS, "{channel} {points}" @@ -1097,49 +1091,58 @@ mod tests { } #[test] - fn lung_4d_plays_ten_breathing_phases() { - let Some(study) = load_optional("4dlung") else { + fn breathing_plays_ten_phases() { + let Some(study) = load_optional("breathing") else { return; }; - assert_eq!(study.mode(), "4d"); - assert_eq!(study.slice_count(), 50); - assert_eq!(study.frame_count(), 30); - assert_eq!( - study.phase_percents(), - [0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0] - ); + assert!(study.log_line().starts_with("4d mode")); + assert_playback(&study, PHASE_DT_NS); + let meta: Meta = decode(&of_channel(&study, CH_META)[0]); + assert_eq!(meta.mode, "4d"); + assert_eq!(meta.slice_count, 142); + assert_eq!(meta.phase_count, 10); assert!( study .channels() .iter() .all(|channel| channel.id != CH_SLICE) ); - assert_playback(&study, PHASE_DT_NS); - assert_image(&study, CH_AXIAL, 0, 512); - let (width, height) = study.image_size(CH_CORONAL, 0).unwrap(); - assert_eq!(width, 512); - assert!(height > 80, "coronal height {height}"); - assert_ne!( - study.image_bytes(CH_CORONAL, 0).unwrap(), - study.image_bytes(CH_CORONAL, 5).unwrap() - ); + let coronal = of_channel(&study, CH_CORONAL); + assert_eq!(coronal.len(), 30); + let phase0 = assert_image(&coronal[0], 512); + assert!(phase0.height > 300, "coronal height {}", phase0.height); + let phase50 = assert_image(&coronal[5], 512); + let phase50_again = assert_image(&coronal[15], 512); + assert_ne!(phase0.data, phase50.data); + assert_eq!(phase50.data, phase50_again.data); + let stats: Vec = of_channel(&study, CH_STATS) + .into_iter() + .take(10) + .map(|message| decode(&message)) + .collect(); + let percents: Vec = stats.iter().map(|stat| stat.phase_percent).collect(); assert_eq!( - study.image_bytes(CH_CORONAL, 5).unwrap(), - study.image_bytes(CH_CORONAL, 15).unwrap() + percents, + [0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0] ); - let inhale = study.lung_volume_ml(0).unwrap(); - let mid = study.lung_volume_ml(5).unwrap(); - assert!(inhale > 200.0 && inhale < 20_000.0, "{inhale}"); - assert!(mid > 200.0 && mid < 20_000.0, "{mid}"); - assert!((inhale - mid).abs() > 1.0, "{inhale} vs {mid}"); - for frame in 0..10 { - let points = study.point_count(CH_LUNGS, frame).unwrap(); + let volumes: Vec = stats.iter().map(|stat| stat.lung_volume_ml).collect(); + eprintln!("lung_volume_ml: {volumes:?}"); + assert!( + volumes + .iter() + .all(|volume| (800.0..8_000.0).contains(volume)), + "{volumes:?}" + ); + let min = volumes.iter().copied().fold(f64::MAX, f64::min); + let max = volumes.iter().copied().fold(f64::MIN, f64::max); + assert!(max - min > 100.0, "{volumes:?}"); + for channel in [CH_BONE, CH_LUNGS] { + let index = if channel == CH_LUNGS { 5 } else { 0 }; + let points = point_count(&of_channel(&study, channel)[index]); assert!( points > 100 && points <= render::MAX_CLOUD_POINTS, - "frame {frame} {points}" + "{channel} {points}" ); } - let bone = study.point_count(CH_BONE, 0).unwrap(); - assert!(bone > 100 && bone <= render::MAX_CLOUD_POINTS, "{bone}"); } } diff --git a/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py b/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py index b3e52b3..d9b0b15 100644 --- a/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py +++ b/foxglove_sdk/dicom_data_loader/scripts/download_sample_data.py @@ -13,18 +13,19 @@ "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", ] -# 4D-Lung patient 100_HM10395, study S100: ten gated phases, 0% through 90%. +# 4D-Lung patient 100_HM10395, study S300: ten gated phases, 0% through 90%. +# About 75 MB per phase, ~750 MB total. BREATHING_SERIES = [ - "1.3.6.1.4.1.14519.5.2.1.6834.5010.124525254231969293228570633659", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.183508601171753954609852620508", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.187024278831950398228901118188", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.204026634860397031036823480116", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.215193814203822462481389051414", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.303761398024606109161463363548", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.322628904903035357840500590726", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.339352878581931019587697303406", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.742525481756625894234574121639", - "1.3.6.1.4.1.14519.5.2.1.6834.5010.901242277587004706935962916718", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.660205801121995704108596829193", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.124579141450014659493072582400", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.238102828057750190064634581534", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.104367227242206784402589566113", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.346792788302694778312512645229", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.235823857759482707932854857212", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.211370457211109403388277199194", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.309178825287344133434085485451", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.103521604337852189272990015277", + "1.3.6.1.4.1.14519.5.2.1.6834.5010.334245065657098504984831070956", ] diff --git a/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs b/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs index 35b3c7a..02c9fd5 100644 --- a/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs +++ b/foxglove_sdk/dicom_data_loader/scripts/wasm_smoke.mjs @@ -1,25 +1,22 @@ -// Run the release data-loader component outside Foxglove. -// -// Requires wasm-tools and @bytecodealliance/jco on PATH (or WASM_TOOLS / JCO). -// DICOM_DATA_DIR should contain lidc/ and 4dlung/. Previews go to -// /tmp/dicom_plan/previews by default (PREVIEW_DIR). +// Play the release loader outside Foxglove. +// Requires wasm-tools and jco (WASM_TOOLS, JCO). Data is data/chest and data/breathing +// unless DICOM_DATA_DIR is set. PREVIEW_DIR, if set, receives grayscale PNGs. import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import zlib from "node:zlib"; import { pathToFileURL } from "node:url"; - -const tutorialRoot = path.resolve(import.meta.dirname, ".."); -const dataRoot = process.env.DICOM_DATA_DIR ?? "/tmp/dicom_data"; -const previewDir = process.env.PREVIEW_DIR ?? "/tmp/dicom_plan/previews"; +const root = path.resolve(import.meta.dirname, ".."); +const dataRoot = process.env.DICOM_DATA_DIR ?? path.join(root, "data"); +const previewDir = process.env.PREVIEW_DIR; const wasmTools = process.env.WASM_TOOLS ?? "wasm-tools"; const jco = process.env.JCO ?? "jco"; const coreWasm = path.join( - tutorialRoot, + root, "rust/target/wasm32-unknown-unknown/release/foxglove_dicom_data_loader.wasm", ); -const outDir = path.join(tutorialRoot, "scripts/.smoke"); +const outDir = path.join(root, "scripts/.smoke"); function collectDcm(dir) { const out = []; @@ -28,11 +25,8 @@ function collectDcm(dir) { const current = stack.pop(); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full = path.join(current, entry.name); - if (entry.isDirectory()) { - stack.push(full); - } else if (entry.name.toLowerCase().endsWith(".dcm")) { - out.push(full); - } + if (entry.isDirectory()) stack.push(full); + else if (entry.name.toLowerCase().endsWith(".dcm")) out.push(full); } } out.sort(); @@ -46,6 +40,19 @@ class Reader { this.memory = memory; } + // Absolute position. The guest maps SeekFrom::* onto this plus position() and size(). + seek(pos) { + const requested = BigInt(pos); + const len = BigInt(this.data.length); + const at = requested < 0n ? 0n : requested > len ? len : requested; + this.pos = Number(at); + return at; + } + + position() { + return BigInt(this.pos); + } + read(ptr, len) { const n = Math.min(len, this.data.length - this.pos); if (n > 0) { @@ -54,111 +61,79 @@ class Reader { } return BigInt(n); } + + size() { + return BigInt(this.data.length); + } } -function decodeRawImage(buf) { +function rawImage(buf) { const bytes = Buffer.from(buf); let offset = 0; let width; let height; - let step; - let encoding; - let frameId; let data; - const readVarint = () => { + const varint = () => { let value = 0; let shift = 0; - while (offset < bytes.length) { - const byte = bytes[offset]; - offset += 1; + for (;;) { + const byte = bytes[offset++]; value |= (byte & 0x7f) << shift; - if ((byte & 0x80) === 0) { - return value; - } + if ((byte & 0x80) === 0) return value; shift += 7; } - throw new Error("truncated varint"); }; while (offset < bytes.length) { - const key = readVarint(); - const field = key >>> 3; + const key = varint(); const wire = key & 7; - if (wire === 0) { - readVarint(); - } else if (wire === 1) { - offset += 8; - } else if (wire === 5) { - const value = bytes.readUInt32LE(offset); + const field = key >>> 3; + if (wire === 0) varint(); + else if (wire === 1) offset += 8; + else if (wire === 5) { + if (field === 2) width = bytes.readUInt32LE(offset); + if (field === 3) height = bytes.readUInt32LE(offset); offset += 4; - if (field === 2) { - width = value; - } else if (field === 3) { - height = value; - } else if (field === 5) { - step = value; - } } else if (wire === 2) { - const len = readVarint(); - const slice = bytes.subarray(offset, offset + len); + const len = varint(); + if (field === 6) data = bytes.subarray(offset, offset + len); offset += len; - if (field === 4) { - encoding = slice.toString("utf8"); - } else if (field === 6) { - data = slice; - } else if (field === 7) { - frameId = slice.toString("utf8"); - } - } else { - throw new Error(`unsupported protobuf wire type ${wire}`); - } - } - if (width == null || height == null || data == null) { - throw new Error("RawImage is missing width, height, or pixels"); + } else throw new Error(`wire ${wire}`); } - return { width, height, step, encoding, frameId, data }; + if (width == null || height == null || !data) throw new Error("RawImage is incomplete"); + return { width, height, data }; } -function crc32(buffer) { - let crc = ~0; - for (const byte of buffer) { - crc ^= byte; - for (let bit = 0; bit < 8; bit += 1) { - crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); - } - } - return ~crc >>> 0; -} - -function chunk(type, data) { - const body = Buffer.concat([Buffer.from(type), data]); - const out = Buffer.alloc(12 + data.length); - out.writeUInt32BE(data.length, 0); - body.copy(out, 4); - out.writeUInt32BE(crc32(body), 8 + data.length); - return out; -} - -function writePng(file, image) { - const { width, height, data } = image; - if (data.length !== width * height) { - throw new Error(`${file}: expected ${width * height} pixels, got ${data.length}`); - } +function writePng(file, { width, height, data }) { const raw = Buffer.alloc((width + 1) * height); - for (let y = 0; y < height; y += 1) { + for (let y = 0; y < height; y += 1) data.copy(raw, y * (width + 1) + 1, y * width, (y + 1) * width); - } + const chunk = (type, payload) => { + const body = Buffer.concat([Buffer.from(type), payload]); + let crc = ~0; + for (const byte of body) { + crc ^= byte; + for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + const out = Buffer.alloc(12 + payload.length); + out.writeUInt32BE(payload.length, 0); + body.copy(out, 4); + out.writeUInt32BE(~crc >>> 0, 8 + payload.length); + return out; + }; const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; - ihdr[9] = 0; - const png = Buffer.concat([ - Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), - chunk("IHDR", ihdr), - chunk("IDAT", zlib.deflateSync(raw)), - chunk("IEND", Buffer.alloc(0)), - ]); - fs.writeFileSync(file, png); + const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + fs.writeFileSync( + file, + Buffer.concat([ + sig, + chunk("IHDR", ihdr), + chunk("IDAT", zlib.deflateSync(raw)), + chunk("IEND", Buffer.alloc(0)), + ]), + ); } function transpile() { @@ -184,66 +159,39 @@ function transpile() { ); } -async function loadComponent() { - const js = pathToFileURL(path.join(outDir, "dicom.js")).href; - const imported = await import(js); - return imported.instantiate; -} - async function play(name, dir, instantiate) { const files = collectDcm(dir); - if (files.length === 0) { - throw new Error(`no .dcm files under ${dir}`); - } + if (files.length === 0) throw new Error(`no .dcm files under ${dir}`); let memory; const memoryRef = () => { - if (!memory) { - throw new Error("guest memory is not available"); - } + if (!memory) throw new Error("guest memory is not available"); return memory; }; let instance = instantiate( (filename) => new WebAssembly.Module(fs.readFileSync(path.join(outDir, filename))), { - "foxglove:loader/console": { - log(message) { - console.log(`[${name}] ${message}`); - }, - }, - "foxglove:loader/reader": { - Reader, - open(filePath) { - return new Reader(filePath, memoryRef); - }, - }, + "foxglove:loader/console": { log: (message) => console.log(`[${name}] ${message}`) }, + "foxglove:loader/reader": { Reader, open: (filePath) => new Reader(filePath, memoryRef) }, }, (module, importObject) => { const created = new WebAssembly.Instance(module, importObject); const exported = created.exports.memory; - if (exported && (!memory || exported.buffer.byteLength >= memory.buffer.byteLength)) { + if (exported && (!memory || exported.buffer.byteLength >= memory.buffer.byteLength)) memory = exported; - } return created; }, ); - if (instance instanceof Promise) { - instance = await instance; - } + if (instance instanceof Promise) instance = await instance; const loader = new instance.loader.DataLoader({ paths: files }); const init = loader.initialize(); const memoryBytes = memory.buffer.byteLength; - console.log(`[${name}] wasm memory after initialize: ${memoryBytes} bytes (${files.length} files)`); - const errors = init.problems.filter((problem) => problem.severity === "error"); - if (errors.length > 0) { - throw new Error(`${name} initialize errors: ${errors.map((problem) => problem.message).join("; ")}`); - } - if (init.problems.length > 0) { - throw new Error(`${name} unexpected problems: ${JSON.stringify(init.problems)}`); - } + console.log( + `[${name}] wasm memory after initialize: ${memoryBytes} bytes (${files.length} files)`, + ); + if (init.problems.length > 0) throw new Error(`${name}: ${JSON.stringify(init.problems)}`); const { startTime, endTime } = init.timeRange; - if (startTime <= 0n || endTime < startTime) { + if (!(startTime > 0n && endTime >= startTime)) throw new Error(`${name} time range ${startTime}..${endTime}`); - } const ids = init.channels.map((channel) => channel.id); const iterator = loader.createIterator({ channels: Uint16Array.from(ids) }); const counts = new Map(); @@ -252,105 +200,68 @@ async function play(name, dir, instantiate) { let total = 0; while (true) { const next = iterator.next(); - if (next == null) { - break; - } - if (next.tag !== "ok") { - throw new Error(`${name} iterator error: ${next.val}`); - } + if (next == null) break; + if (next.tag !== "ok") throw new Error(`${name} iterator error: ${next.val}`); const message = next.val; - if (previous != null && message.logTime < previous) { + if (previous != null && message.logTime < previous) throw new Error(`${name} log time went backwards`); - } - if (message.logTime < startTime || message.logTime > endTime) { - throw new Error(`${name} log time ${message.logTime} outside ${startTime}..${endTime}`); - } + if (message.logTime < startTime || message.logTime > endTime) + throw new Error(`${name} log time out of range`); previous = message.logTime; counts.set(message.channelId, (counts.get(message.channelId) ?? 0) + 1); - const topic = init.channels.find((channel) => channel.id === message.channelId)?.topicName; - if (topic === "/dicom/axial" || topic === "/dicom/coronal" || topic === "/dicom/sagittal") { - const list = images.get(topic) ?? []; - list.push(message.data); - images.set(topic, list); + if (previewDir) { + const topic = init.channels.find((channel) => channel.id === message.channelId)?.topicName; + if (topic === "/dicom/axial" || topic === "/dicom/coronal" || topic === "/dicom/sagittal") { + const list = images.get(topic) ?? []; + list.push(message.data); + images.set(topic, list); + } } total += 1; } - if (previous !== endTime) { - throw new Error(`${name} last log time ${previous} != end ${endTime}`); - } + if (previous !== endTime) throw new Error(`${name} last log time ${previous} != end ${endTime}`); let expected = 0n; for (const channel of init.channels) { const got = BigInt(counts.get(channel.id) ?? 0); - if (got !== channel.messageCount) { - throw new Error(`${name} ${channel.topicName}: ${got} messages, expected ${channel.messageCount}`); - } + if (got !== channel.messageCount) + throw new Error(`${name} ${channel.topicName}: ${got} != ${channel.messageCount}`); expected += channel.messageCount ?? 0n; } - if (BigInt(total) !== expected) { + if (BigInt(total) !== expected) throw new Error(`${name} iterated ${total}, expected ${expected}`); - } const backfill = loader.getBackfill({ time: startTime, channels: Uint16Array.from(ids) }); - if (backfill.length !== ids.length) { - throw new Error(`${name} backfill returned ${backfill.length} messages for ${ids.length} channels`); - } - return { images, memoryBytes, channels: init.channels }; + if (backfill.length !== ids.length) + throw new Error(`${name} backfill ${backfill.length} != ${ids.length}`); + console.log(`[${name}] wasm memory after playback: ${memory.buffer.byteLength} bytes`); + return { images, memoryBytes: Math.max(memoryBytes, memory.buffer.byteLength) }; } -function expectImage(images, topic, index, label) { - const list = images.get(topic); - if (!list || !list[index]) { - throw new Error(`missing ${topic} frame ${index}`); - } - const image = decodeRawImage(list[index]); - if (image.encoding !== "mono8") { - throw new Error(`${label} encoding ${image.encoding}`); - } - if (image.width !== 512 || image.data.length !== image.width * image.height) { - throw new Error(`${label} is ${image.width}x${image.height} (${image.data.length} bytes)`); - } +function save(images, topic, index, label) { + const image = rawImage(images.get(topic)[index]); writePng(path.join(previewDir, `${label}.png`), image); - console.log(`[preview] ${label}.png ${image.width}x${image.height} frame=${image.frameId}`); + console.log(`[preview] ${label}.png ${image.width}x${image.height}`); return image.data; } transpile(); -const instantiate = await loadComponent(); -fs.mkdirSync(previewDir, { recursive: true }); - -const chest = await play("lidc", path.join(dataRoot, "lidc"), instantiate); -expectImage(chest.images, "/dicom/axial", 66, "lidc-axial"); -expectImage(chest.images, "/dicom/coronal", 0, "lidc-coronal"); -expectImage(chest.images, "/dicom/sagittal", 0, "lidc-sagittal"); -if (chest.images.get("/dicom/axial")?.length !== 133) { - throw new Error(`lidc axial frames ${chest.images.get("/dicom/axial")?.length}`); -} - -const breathing = await play("4dlung", path.join(dataRoot, "4dlung"), instantiate); -const phase0 = expectImage(breathing.images, "/dicom/coronal", 0, "4dlung-coronal-0"); -const phase50 = expectImage(breathing.images, "/dicom/coronal", 5, "4dlung-coronal-50"); -const phase50Cycle2 = breathing.images.get("/dicom/coronal")?.[15]; -if (!phase50Cycle2) { - throw new Error("missing 4D coronal frame 15"); -} -if (Buffer.compare(phase0, phase50) === 0) { - throw new Error("4D coronal phase 0% and 50% are identical"); -} -if (Buffer.compare(phase50, decodeRawImage(phase50Cycle2).data) !== 0) { - throw new Error("4D coronal phase 50% was not reused on the next cycle"); -} -expectImage(breathing.images, "/dicom/axial", 0, "4dlung-axial"); -expectImage(breathing.images, "/dicom/sagittal", 0, "4dlung-sagittal"); -if (breathing.images.get("/dicom/coronal")?.length !== 30) { - throw new Error(`4dlung coronal frames ${breathing.images.get("/dicom/coronal")?.length}`); +const imported = await import(pathToFileURL(path.join(outDir, "dicom.js")).href); +if (previewDir) fs.mkdirSync(previewDir, { recursive: true }); +const chest = await play("chest", path.join(dataRoot, "chest"), imported.instantiate); +const breathing = await play("breathing", path.join(dataRoot, "breathing"), imported.instantiate); +if (previewDir) { + save(chest.images, "/dicom/axial", 66, "lidc-axial"); + save(chest.images, "/dicom/coronal", 0, "lidc-coronal"); + save(chest.images, "/dicom/sagittal", 0, "lidc-sagittal"); + const phase0 = save(breathing.images, "/dicom/coronal", 0, "4dlung-coronal-0"); + const phase50 = save(breathing.images, "/dicom/coronal", 5, "4dlung-coronal-50"); + save(breathing.images, "/dicom/axial", 0, "4dlung-axial"); + save(breathing.images, "/dicom/sagittal", 0, "4dlung-sagittal"); + if (Buffer.compare(phase0, phase50) === 0) + throw new Error("breathing coronal phases are identical"); } - console.log( JSON.stringify( - { - lidcMemoryBytes: chest.memoryBytes, - lung4dMemoryBytes: breathing.memoryBytes, - previewDir, - }, + { chestMemoryBytes: chest.memoryBytes, breathingMemoryBytes: breathing.memoryBytes }, null, 2, ), From 00b79513d7cbdae3b4624786ba311ed484f17a6c Mon Sep 17 00:00:00 2001 From: Mat Sadowski Date: Mon, 5 Oct 2026 14:10:58 +0200 Subject: [PATCH 3/3] Use UV instead of conda for examples --- foxglove_sdk/dicom_data_loader/.gitignore | 1 + foxglove_sdk/dicom_data_loader/README.md | 11 +- .../dicom_data_loader/package-lock.json | 14 + .../dicom_data_loader/scripts/pyproject.toml | 11 + .../scripts/requirements.txt | 1 - .../dicom_data_loader/scripts/uv.lock | 864 ++++++++++++++++++ 6 files changed, 896 insertions(+), 6 deletions(-) create mode 100644 foxglove_sdk/dicom_data_loader/scripts/pyproject.toml delete mode 100644 foxglove_sdk/dicom_data_loader/scripts/requirements.txt create mode 100644 foxglove_sdk/dicom_data_loader/scripts/uv.lock diff --git a/foxglove_sdk/dicom_data_loader/.gitignore b/foxglove_sdk/dicom_data_loader/.gitignore index a718cb4..16011cf 100644 --- a/foxglove_sdk/dicom_data_loader/.gitignore +++ b/foxglove_sdk/dicom_data_loader/.gitignore @@ -6,3 +6,4 @@ data/* !data/README.md *.foxe scripts/.smoke +.venv diff --git a/foxglove_sdk/dicom_data_loader/README.md b/foxglove_sdk/dicom_data_loader/README.md index 7f62f47..5ea953c 100644 --- a/foxglove_sdk/dicom_data_loader/README.md +++ b/foxglove_sdk/dicom_data_loader/README.md @@ -13,17 +13,18 @@ The loader publishes lung-window images, a 3D point cloud of bone and lung, a pl - Rust stable, with the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`) - Node.js 18 or newer -- Python 3, only for downloading the sample series +- [uv](https://docs.astral.sh/uv/getting-started/installation/), only for downloading the sample series ## Download sample data Both series are public, CC BY 3.0, and hosted by the NCI Imaging Data Commons. The script uses the `idc-index` package. Nothing is committed to the repository. +From this directory: + ```bash -pip install -r scripts/requirements.txt -python scripts/download_sample_data.py --dataset chest # one 133-slice chest CT, ~70 MB -python scripts/download_sample_data.py --dataset breathing # ten phases, ~750 MB -python scripts/download_sample_data.py --dataset all +uv run --directory scripts download_sample_data.py --dataset chest # one 133-slice chest CT, ~70 MB +uv run --directory scripts download_sample_data.py --dataset breathing # ten phases, ~750 MB +uv run --directory scripts download_sample_data.py --dataset all ``` Files are written under `data/chest` and `data/breathing`. The breathing study is about 750 MB (ten phases, 142 slices each). From `rust/`, `cargo test --release` reads those two folders and skips a dataset when its folder is missing. `DICOM_DATA_DIR` overrides the parent directory. diff --git a/foxglove_sdk/dicom_data_loader/package-lock.json b/foxglove_sdk/dicom_data_loader/package-lock.json index 36ac7b1..e9e42ed 100644 --- a/foxglove_sdk/dicom_data_loader/package-lock.json +++ b/foxglove_sdk/dicom_data_loader/package-lock.json @@ -470,6 +470,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -558,6 +559,7 @@ "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.70.1", @@ -597,6 +599,7 @@ "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.70.1", "@typescript-eslint/types": "8.70.1", @@ -976,6 +979,7 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -999,6 +1003,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1340,6 +1345,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.25", "caniuse-lite": "^1.0.30001810", @@ -1972,6 +1978,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -2032,6 +2039,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4272,6 +4280,7 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4305,6 +4314,7 @@ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4363,6 +4373,7 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5147,6 +5158,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5425,6 +5437,7 @@ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5564,6 +5577,7 @@ "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", diff --git a/foxglove_sdk/dicom_data_loader/scripts/pyproject.toml b/foxglove_sdk/dicom_data_loader/scripts/pyproject.toml new file mode 100644 index 0000000..60d97cd --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/scripts/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "dicom-sample-data" +version = "0.1.0" +description = "Download the public CT series used by the DICOM data loader tutorial" +requires-python = ">=3.10" +dependencies = [ + "idc-index", +] + +[tool.uv] +package = false diff --git a/foxglove_sdk/dicom_data_loader/scripts/requirements.txt b/foxglove_sdk/dicom_data_loader/scripts/requirements.txt deleted file mode 100644 index 88edce7..0000000 --- a/foxglove_sdk/dicom_data_loader/scripts/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -idc-index diff --git a/foxglove_sdk/dicom_data_loader/scripts/uv.lock b/foxglove_sdk/dicom_data_loader/scripts/uv.lock new file mode 100644 index 0000000..b238459 --- /dev/null +++ b/foxglove_sdk/dicom_data_loader/scripts/uv.lock @@ -0,0 +1,864 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11'", +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dicom-sample-data" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "idc-index" }, +] + +[package.metadata] +requires-dist = [{ name = "idc-index" }] + +[[package]] +name = "duckdb" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/19/e57151753576373c6696a12022648546cca6038e8833fda2908ee2342d9b/duckdb-1.5.5.tar.gz", hash = "sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238", size = 18066741, upload-time = "2026-07-22T10:55:17.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/d4/298acf9331a80b3ce6ac64dd940e7e13f4058fb69d18914445f02e3c7bfe/duckdb-1.5.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f", size = 32702934, upload-time = "2026-07-22T10:53:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/c489fb63d64b2e7ee109ce8460bdede003a0f256e5b41a03a2a1c4764058/duckdb-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6", size = 17343604, upload-time = "2026-07-22T10:53:22.767Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/effa80a15b1f0c61c235622f797868485359e8c9ad6a8e358e7a0c479151/duckdb-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079", size = 15488179, upload-time = "2026-07-22T10:53:25.945Z" }, + { url = "https://files.pythonhosted.org/packages/5d/07/21212345c8d24ba62dceaa20be3b21f5c46f1510b1b42ce93bb058afe0c4/duckdb-1.5.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c", size = 19367323, upload-time = "2026-07-22T10:53:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/10371ae875fb4b5ef61bb892743b4b2e90c512b371fdf29317deb744857d/duckdb-1.5.5-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf", size = 21476568, upload-time = "2026-07-22T10:53:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/09568ce617dd7bc0757b3d7b6a981660b9e4f0b7594de8ed776755eae740/duckdb-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb", size = 13156129, upload-time = "2026-07-22T10:53:37.55Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c2/b62ec24d57bb8df4e24b0b58f7f8facb32f5fdb9f1895aed9e9fcdded168/duckdb-1.5.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528", size = 32708371, upload-time = "2026-07-22T10:53:41.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ce/769171ba45f0b73632dc3bc3108d891e81dd6c6bbfba630a34a75b4dcc0f/duckdb-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a", size = 17343979, upload-time = "2026-07-22T10:53:44.951Z" }, + { url = "https://files.pythonhosted.org/packages/46/59/a8e3384ee916e00d5dcf985194c1511d61978540778a1e96fa47f9fb3e0d/duckdb-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4", size = 15493704, upload-time = "2026-07-22T10:53:47.912Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1d/9840179c2607b90523a2884a129c4d4e6dbdc1178ba62a976c1043beba88/duckdb-1.5.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f", size = 19366574, upload-time = "2026-07-22T10:53:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/b5/55/f9641a4eebcc2f4df631287d6c3b9ed2eea3b92644f93acbad825e3972b6/duckdb-1.5.5-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe", size = 21477952, upload-time = "2026-07-22T10:53:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3a/07c3556e37a5c97b95917b029c8fdde4a25fbd76a660bacdac195cf20dcb/duckdb-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc", size = 13156986, upload-time = "2026-07-22T10:53:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ff/07b48eef2078ca033847e9caa46cc7633b714c5f91ad1ce091c8ca89d792/duckdb-1.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c", size = 14001317, upload-time = "2026-07-22T10:54:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/40/2e05d324400fdaa5656c9f48d6298da421cb034d85e509fa0e6e325cf04b/duckdb-1.5.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e", size = 32753858, upload-time = "2026-07-22T10:54:05.514Z" }, + { url = "https://files.pythonhosted.org/packages/79/15/5ceb58ffb5bb8a62b3fd7abb39c41467cdf94850ece02e6d88664dfc75ce/duckdb-1.5.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7", size = 17368293, upload-time = "2026-07-22T10:54:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5c/bf02da0b354fe83cca4f95a4fbf762181af466f7d551ab2a093f7698882a/duckdb-1.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae", size = 15509131, upload-time = "2026-07-22T10:54:12.228Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a9/5f1f09da421d8e930e0b063d11c1b3f90363f40ede74438cd188afdd13a2/duckdb-1.5.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6", size = 19391959, upload-time = "2026-07-22T10:54:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/6549769f158126fa64fd6c1ac2eb59a18282146c939867a3eb31b7c1db07/duckdb-1.5.5-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1", size = 21510909, upload-time = "2026-07-22T10:54:19.681Z" }, + { url = "https://files.pythonhosted.org/packages/af/b7/5753b41d3124838f868f9f523362812d9fc45409e9e4dd70dcbb0a25826e/duckdb-1.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f", size = 13168544, upload-time = "2026-07-22T10:54:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/44b679c7d46245f8398feae7edac959d1b83d4eb143e25b3fce0630b78bd/duckdb-1.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282", size = 13988684, upload-time = "2026-07-22T10:54:26.003Z" }, + { url = "https://files.pythonhosted.org/packages/47/37/4a38116e7700720fd152c666292214fd3abdf916496991296d8d1f66efbf/duckdb-1.5.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0", size = 32754294, upload-time = "2026-07-22T10:54:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/7d392f1ba1eee0eaf4ab4c8c7a604bfe3536cd63f979cf5c98798664f807/duckdb-1.5.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366", size = 17368211, upload-time = "2026-07-22T10:54:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a5/0a6f4fa60562faa615e55e15bd1953a2f2b17a8edd8105e5cda215e43457/duckdb-1.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c", size = 15509136, upload-time = "2026-07-22T10:54:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/023c89f51978545b9fab318581bba0c457a58e7530d2d933e54ae7d8647c/duckdb-1.5.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a", size = 19392147, upload-time = "2026-07-22T10:54:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/41bef391fb8b23dbc133c9f2ba016e7a7a8124513d2cc1b430f1897d87e4/duckdb-1.5.5-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053", size = 21511060, upload-time = "2026-07-22T10:54:42.788Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/c44dfc1f924ac29b3252dc1b91393c01d009dbfe9f8ed33f10b986151bd1/duckdb-1.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28", size = 13168028, upload-time = "2026-07-22T10:54:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/ca/88/591384b2cd59abddd6f5dc175e60374f9abae6064429f0c4402854c10f44/duckdb-1.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de", size = 13989955, upload-time = "2026-07-22T10:54:49.262Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/12c65bfa2d2605b81981b264788891bcf11ec72227889554cead5d8d13b9/duckdb-1.5.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2", size = 32761946, upload-time = "2026-07-22T10:54:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/b9/46/682ce155f17e0d2822d4f13ee3db9ca4b5b7c2da61b841b2629035e1f4bc/duckdb-1.5.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8", size = 17375069, upload-time = "2026-07-22T10:54:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/39/ce/a24bcbd3289c8f305a430759c5fc12242740b4af3e17f7593f3a34e333d2/duckdb-1.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151", size = 15519791, upload-time = "2026-07-22T10:55:00.681Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/3a01afbc615c1d418c0de58a6b68ac5ce2a8563232c0464bfbc2ce552398/duckdb-1.5.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece", size = 19398251, upload-time = "2026-07-22T10:55:04.168Z" }, + { url = "https://files.pythonhosted.org/packages/a1/43/3a5e81d1728f4d234c79bfe385808ee7c04834f7c37a4b5c257459c25614/duckdb-1.5.5-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe", size = 21513851, upload-time = "2026-07-22T10:55:07.864Z" }, + { url = "https://files.pythonhosted.org/packages/91/41/fc7c829172c60ca22485251eab285f4f1a0d87b486a024c726f21471d86e/duckdb-1.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353", size = 13691858, upload-time = "2026-07-22T10:55:11.181Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/95d9216b79e9273689d7ebce125a54503ed0c9bd7da931f0265888e99779/duckdb-1.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433", size = 14470180, upload-time = "2026-07-22T10:55:14.445Z" }, +] + +[[package]] +name = "idc-index" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "duckdb" }, + { name = "idc-index-data" }, + { name = "packaging" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "platformdirs" }, + { name = "psutil" }, + { name = "pyarrow" }, + { name = "requests" }, + { name = "s5cmd" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/df/56887e3c382ad29e2ebfd96b412eaa6ee95ac58536508de0e453d8c0136a/idc_index-0.12.5.tar.gz", hash = "sha256:ae3a276f7b0e26b573493541a6dd4f43c7c664ed7572fae61ce8e54430945ddc", size = 61341, upload-time = "2026-07-31T17:22:36.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/8c/2e5c469306b8fc661ce8feec01ae9dafc8bdb965279e6660266bc5ea3d06/idc_index-0.12.5-py3-none-any.whl", hash = "sha256:01818520b8e986e3f80cdb9f72c4a230eb38d51b1ad6979ebed6e468758b10d3", size = 29731, upload-time = "2026-07-31T17:22:35.235Z" }, +] + +[[package]] +name = "idc-index-data" +version = "24.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/a1/6e0e4a7efb1567d8ac6d03133d2e35863cc9dc92765794b3d0a948e2e248/idc_index_data-24.2.2.tar.gz", hash = "sha256:ca94c5f524a9bc6832556227c2a3bc955037c9ba9125cfc77d94f7a1d92de528", size = 77362846, upload-time = "2026-07-11T17:34:27.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b3/3adf736fa7db08e64dbbc04084a48054c18cc08822bee75ae5d6f7832f1c/idc_index_data-24.2.2-py3-none-any.whl", hash = "sha256:79ac3a43665077b16cc09e16875c9762ce646257ed0c75900a8c636a54456c62", size = 77341561, upload-time = "2026-07-11T17:34:22.461Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/17/d7b106e05bfa642e8694451e7d3d759c6a241c5386a5d962e4f66c047e06/pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10", size = 4667686, upload-time = "2026-09-17T23:23:18.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/48/88e8d250d28efa8163294f6809a71683c7ee67f63ac7c33021c0503b3547/pandas-3.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:085e3786ae6b2e82b406266bce36690f72b9dc1421903ba9296b2981a9fcf586", size = 10391798, upload-time = "2026-09-17T23:20:20.96Z" }, + { url = "https://files.pythonhosted.org/packages/55/a6/39db5d41f3eb5d7846626312cb30e0cea48e988fc47a3725698bc931ad3c/pandas-3.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7564d86a94c2eb8ab290b07f63ddaae5c032fa53897c29a2ff2197d43aee8af", size = 10023718, upload-time = "2026-09-17T23:20:25.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/b7/707e966129f77ee8a39d41a41bbd989b790b3fef581c6e26b821315cba6b/pandas-3.0.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7c0afdcaf6661d795fcefc2f647ddd1136f62cdc153fba177c685d97a87808", size = 10612800, upload-time = "2026-09-17T23:20:27.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/be/dfb6cc9329d0bbe76dadda8a1113d026bb996ec76c509221368dc68349f4/pandas-3.0.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47121f9571503f724c9b93e297ab6254ac99c77adf5e9ed085ea419fd585c258", size = 11108900, upload-time = "2026-09-17T23:20:30.65Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/3d58f15bbe972f3d7bfa13ee06d731785a76fe8d232c92b2655a8de14127/pandas-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:994a79608263fe1c14cc48ffa7300e2b834b7d1cb406ffe96a08828cb0cdd79b", size = 11630226, upload-time = "2026-09-17T23:20:33.582Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/9b494de4a3dd92fc6eb19187db1b21afb50fdc21962f32ea1aca9f270cb2/pandas-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3a22e07fe75347eaacc75b0e85297947af4fba6b4aae23916bd8b6828d0bba3", size = 12146942, upload-time = "2026-09-17T23:20:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/d2df02854aec5d47659acfb2be352eecc691845b2f86e99c84f1010a8671/pandas-3.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:2e5fa32ff162dfdbc280157d664f44d23049ae414725af9676df339c501d82cd", size = 9859246, upload-time = "2026-09-17T23:20:43.976Z" }, + { url = "https://files.pythonhosted.org/packages/79/1e/2a30df0d7dede5c195300a1820b0bc21cea3aa24e4c8b6c4431565ecc79a/pandas-3.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:5e75072773c1b2f7cb63faa3a6f562aede11f3976f68ed34cb538bc091a28171", size = 9106923, upload-time = "2026-09-17T23:20:46.611Z" }, + { url = "https://files.pythonhosted.org/packages/77/4c/597d588c055d4373cff19cbbc32d4dd046c7be8fadee957585b5ba9e5b24/pandas-3.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dac2d65e9087e8e7b5a45fe15c4920911a221df061ab629943ce016489145c7", size = 10397411, upload-time = "2026-09-17T23:20:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/48907c7c707b61a8e5018c32e1a3f70623209bfb59020a2f6196159d3aa7/pandas-3.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9dab635a549e58a053c7b0fa054dc0bd7be22f0ed9a720f4a85d5fb993276172", size = 10051279, upload-time = "2026-09-17T23:20:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/67/fa/613d867c3d9554a61bafdec6f79565c8a3e73235feb52cc4a72ad2e0fa6a/pandas-3.0.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3dccb584123b399c07562ac4d62543e90ede49ddf8ce3c13ffc64cbe828c281", size = 10303429, upload-time = "2026-09-17T23:20:55.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/67/0c0f18e38d7f2d2af8c24b3315bc4046e73bbdd4a5540405506671ad0c0d/pandas-3.0.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0704044b676496b8350e023b09f174a26772456c974a2b11c36bebb558c9490d", size = 10788193, upload-time = "2026-09-17T23:20:58.617Z" }, + { url = "https://files.pythonhosted.org/packages/39/53/1b57f3162501fe36687e4870e1b918a6458ca7386b173af75663ace95857/pandas-3.0.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e7c1905ef02c3d6d43d9dbd5b6ccb4da4870a0b0c821bbc103fbdb6f3ad2707b", size = 11323501, upload-time = "2026-09-17T23:21:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/b1182e8d39100369d7f13f4a125a3fb6b096fef112c46d0566c25781ff68/pandas-3.0.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:569e114072b24fc4970c12e2b4bab252671668a40b324318903380cab0254c0c", size = 11838173, upload-time = "2026-09-17T23:21:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5b717af24d2f27995e51e0875a269dddd373045216e34cf62e3aa764eaa1/pandas-3.0.6-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:2a8fc94be2ee5f1d86f97aacd8cc566f81680b6498e76f3007421bb5d98151bf", size = 7118499, upload-time = "2026-09-17T23:21:07.661Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/14b3b17cd75cef4a1ee1af4234eb41cc1afe4103b98c2d80e8916abfd42b/pandas-3.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:3ef908d28590b3f42d7070e7ad8f9b34b442b260b7f3c1afb57e0040c58cdb1b", size = 9661439, upload-time = "2026-09-17T23:21:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/3b/11/3d580a604a1e35d69f6676847bd12db7d14344bc677b717f4413e79c5d0d/pandas-3.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:f4e7c52eb108d752e7592268108fd3e98efd76d83a3125cdd06c621c2e44359b", size = 8959806, upload-time = "2026-09-17T23:21:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/143605a1f6443ad50ebda78a31e5a3a10147fec2590e931584aaa5ff0a09/pandas-3.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2", size = 10418900, upload-time = "2026-09-17T23:21:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/87f8548f73d452aab35e4a90f8b39ae303295e0f2ef0b4055c44d6b3f1be/pandas-3.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa", size = 10064785, upload-time = "2026-09-17T23:21:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/d951442e5607c6e3b2462eff8f420797d428aa74b87c6ecfe4f48553626e/pandas-3.0.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c", size = 10245290, upload-time = "2026-09-17T23:21:22.797Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/96d50e1e6cd0b08b5e2b7c838f65ae644940f75a124063380b5ef73b6866/pandas-3.0.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658", size = 10757657, upload-time = "2026-09-17T23:21:25.673Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/f82d13a2cb703e1a8acee7e01fdc2b898d9cd0c00f07d1dfce63af43e350/pandas-3.0.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2", size = 11249114, upload-time = "2026-09-17T23:21:28.898Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ce/8aef2e561a2f2c8b38c913c67373c65ba6748174e763d27c80271b24bd17/pandas-3.0.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d", size = 11820511, upload-time = "2026-09-17T23:21:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/63cb67e6903ef6d9c2871916dbcbc09d254da0fe8b870cf62e16b21945f2/pandas-3.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd", size = 9638092, upload-time = "2026-09-17T23:21:34.883Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/e7b35b712edb068d382ddc8b2bea8a04974100515ba2daa22b478b265842/pandas-3.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f", size = 8952032, upload-time = "2026-09-17T23:21:37.729Z" }, + { url = "https://files.pythonhosted.org/packages/75/55/1a8875395b05ccd572cbca0b9255dcd2db6e6508e632a558c1a6884b39ad/pandas-3.0.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1", size = 10492213, upload-time = "2026-09-17T23:21:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/35/61/47ae13476995cc8a40cd609e93e7cf11f273d8692925c2903cb6d38aa0d1/pandas-3.0.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729", size = 10156618, upload-time = "2026-09-17T23:21:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f2/cc5f2adb8d6e86a85d9fb5128f8cf205a61189336f70d1f7faf0d1b53ec9/pandas-3.0.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34", size = 10375479, upload-time = "2026-09-17T23:21:47.159Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/ffdcb19be4ff6bfe7d969e7cef2c567c633df5a3a1cc1053394ad053bca8/pandas-3.0.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1", size = 10783244, upload-time = "2026-09-17T23:21:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/e150075b2c6eb69fae896f2d9239bc6ed07db97735971d53d66de6553460/pandas-3.0.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de", size = 11382223, upload-time = "2026-09-17T23:21:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/b441c587dc7355bf6e1f68a91b4f76a6c29740f0c23be3acc5d4ebbeea6d/pandas-3.0.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c", size = 11854881, upload-time = "2026-09-17T23:21:56.342Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e9/f43410fada510b43fec09993c08f552086c3d247d3ee801a678f3cb10ea5/pandas-3.0.6-cp314-cp314-win_amd64.whl", hash = "sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553", size = 9791672, upload-time = "2026-09-17T23:21:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/db14c059c21f9baa1907d436f8bf30e0c76c6288225c5e8b79a08ba8b2c5/pandas-3.0.6-cp314-cp314-win_arm64.whl", hash = "sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c", size = 9121246, upload-time = "2026-09-17T23:22:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/67/ba/bad0f8dac020ab38a8637fddab01a57a82da7a496a6e6f19590aad53ab62/pandas-3.0.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514", size = 10920612, upload-time = "2026-09-17T23:22:05.404Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a9/b500982e9aac6d52a58da4ad3f11e14168a315b06906b3f397c427878065/pandas-3.0.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60", size = 10570213, upload-time = "2026-09-17T23:22:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/e6ecd0073c98be8f840ac3125b955272835d7d9fd69f5944383b164deb5e/pandas-3.0.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541", size = 10252356, upload-time = "2026-09-17T23:22:11.302Z" }, + { url = "https://files.pythonhosted.org/packages/04/f5/001e230a7a7803590d9275a1a3f7e1bb605e3a495cfe5e8d3a532090621b/pandas-3.0.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965", size = 10655952, upload-time = "2026-09-17T23:22:14.283Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/bab587148a3852c96aae26c4b5f9e04ce2221801ad94e166b4fbf969ede0/pandas-3.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7", size = 11274177, upload-time = "2026-09-17T23:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/74b48d87df2b80892d713c149abfe36d5db4de41b4eccb042a2bc07dafc1/pandas-3.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44", size = 11719041, upload-time = "2026-09-17T23:22:20.227Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c6/d64b72d64d7eb0fad9fe424d34138e45dee70ddbea1360dcd0adf30e28f6/pandas-3.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630", size = 10191388, upload-time = "2026-09-17T23:22:23.524Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/5e5b2ccabeca73ae2b03fc82bca3eabb7466cf43737f05ac08d591665d47/pandas-3.0.6-cp314-cp314t-win_arm64.whl", hash = "sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a", size = 9387796, upload-time = "2026-09-17T23:22:26.64Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/47c5fb0be8bdd00116814c2c40d9ec42dbeb943865ef95130fe58a898226/pandas-3.0.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:50c44cbf5820b6b91a5f74aae04972472aefadd3cd9fbd1010409d85528bd570", size = 10483960, upload-time = "2026-09-17T23:22:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/09/08/a310cb2fefe6d2b4623ab2150da818d93d4e73e33b58e4d163bb74243c5a/pandas-3.0.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:eb6900de08ac85f93ac4948aa6b80842eba555875337b8359035ac9c43e92d34", size = 10149432, upload-time = "2026-09-17T23:22:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/54/36/6af478ec3a26d7754555cd62c3101c589c0931b1d85398e4fa5403910a1c/pandas-3.0.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e25e2e1adee99ddfada6f7206a79ae8e9c8a8861b0e3eaaba165006d3eef18e", size = 10360981, upload-time = "2026-09-17T23:22:35.621Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/5ece0e9cb79a6473216620db6a90546f578001c2bd857b77c444b27acad1/pandas-3.0.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ff44b2cb51cbd691c91f92c4ea6c71e34003f239ebd67c2e857dc898466b49c", size = 10776585, upload-time = "2026-09-17T23:22:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b6/cd3038f31ade5e8d2b4e1c9549d4b31e4d598469562f47142b2ad9171c0a/pandas-3.0.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5edd0a7abb0986ecce1ac81f56d99b6763f86aa6946dceb6c661224f90af5a19", size = 11385249, upload-time = "2026-09-17T23:22:41.18Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/05bb3003737f80375d7311774916a24d161e2e591abe8c672aed7813defc/pandas-3.0.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1bcb3e9ed29e74a7439cedff9e2aefd3ea65de84d7de9ccb6c194192541bd60e", size = 11848595, upload-time = "2026-09-17T23:22:44.207Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/1c0d46acf236d19ef975e9cdd5d1e0dd54924b03f0ed8287096ad4a18152/pandas-3.0.6-cp315-cp315-win_amd64.whl", hash = "sha256:253e12cb9081b0afbac607920f6142975966bc315135e09de275fdbaa415d2de", size = 9783810, upload-time = "2026-09-17T23:22:47.097Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/df3304a9c2833c4810e7f1c887b04105b24731f9deef8b5d5d04522a375b/pandas-3.0.6-cp315-cp315-win_arm64.whl", hash = "sha256:97274c9adf6255bb48c620cd6959805efa7f09ea2167f0e0ae006a448cd2fca7", size = 9116820, upload-time = "2026-09-17T23:22:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/47/25/d5f6cce5efa17c38e4f752a875b4a26d66cfadd529cb8c81672f0376d6a6/pandas-3.0.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:265f562fdd1079f69f3de96dd425c3405224038c0af4f920c54bd240ee2c4640", size = 10894614, upload-time = "2026-09-17T23:22:53.223Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/e1876bfdc1df06bafc022d33202b5663bd86c81df2a6140aacafd0344669/pandas-3.0.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c6e4aae3e9bea26c6c9a20d88d96c86ec4a99b4db5fd516bcb4e829ab2c0ee36", size = 10546973, upload-time = "2026-09-17T23:22:56.155Z" }, + { url = "https://files.pythonhosted.org/packages/e9/27/e0a27a5a5c27f7db66657b44def9e93121fd0ad4f0808355b9898fcbf204/pandas-3.0.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a77a1a44e4d88f1c6a2a64d3eb12efec8420875722e14279800b173a7c7c2804", size = 10239717, upload-time = "2026-09-17T23:22:59.603Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/4eadb7d86a921c1e8bc70916cfe601ed667c4169118d17d69958611c21f8/pandas-3.0.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86fa853a12e0b70927e2b1ee00d56d2224ec9cbb4b9d58348b5ad52d2f21150e", size = 10641275, upload-time = "2026-09-17T23:23:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/eba72e9d905e84e79aefeefdcc6bc1abe15d9077973566643f4211636966/pandas-3.0.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c826e9babb7790142c399f58599d8de679bea059d7b39c5b6efa2096fac37266", size = 11262919, upload-time = "2026-09-17T23:23:06.038Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/1c5bd2642b450e374b6191189f47c49538fe41f82348a66de6f647e6ab59/pandas-3.0.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8fe77b408d82e2615674dfed62533b95e18a03610573877422aada4f625d4947", size = 11709148, upload-time = "2026-09-17T23:23:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/73/3d/1b142bd0d0f1326a5d98c91c955b923cd1e06b5eecb428cd9d8817fa01c0/pandas-3.0.6-cp315-cp315t-win_amd64.whl", hash = "sha256:83e91d15738d7783c050197cef2f2cf82fc6353dae9865aa87ed1fa16aa4d55a", size = 10161518, upload-time = "2026-09-17T23:23:12.365Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/6419c14da2adc1f09a6a183b8f91d7494d325b287f4ca984ac04f663638a/pandas-3.0.6-cp315-cp315t-win_arm64.whl", hash = "sha256:963ca21199097a84c7827c4678b04e30833084fbf8ef44fde3fa7180a29f8fa0", size = 9364529, upload-time = "2026-09-17T23:23:15.274Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/8a/84ef03c1c83eacd7cc4540b05428a93b5cd4e42f62fb0b98ac2cb6ed3a6d/platformdirs-4.12.1.tar.gz", hash = "sha256:38da801a4af303033cbffccb39030db22bf0473e6414309b02acebeee7ca8bf1", size = 59427, upload-time = "2026-09-28T05:05:16.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/31/1521c38d175108ac395941adbcb9f11e085340bed81ad166e8ad8c21c17b/platformdirs-4.12.1-py3-none-any.whl", hash = "sha256:7f251621befae78b2cb74616f9fc99d3fa3db0afba5035987731f8dcd430337d", size = 33011, upload-time = "2026-09-28T05:05:15.06Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/3e/5cd70becb51e1d044c54ba5e627424a6e87df5b98008cbd22cc6abd409ca/pyarrow-25.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0b1edbb2f385a6a65e9711b62ba86ac54a7816a3f8d17bb3e8a5929d65fb2485", size = 35954271, upload-time = "2026-08-10T12:36:33.857Z" }, + { url = "https://files.pythonhosted.org/packages/64/be/17599e086df264ea7dc221d1101e3131e181e00da428a2f9bd0358f0d06b/pyarrow-25.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:a4dd8bf99a8fac133efc0ed6a92f5fddbe2adba0d0f6dd720e39ba9855cea85c", size = 37647543, upload-time = "2026-08-10T12:36:39.486Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/e138b451fd3970a6eda4599f68ae3b2b32b661bc958de3239d54a0bf6575/pyarrow-25.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bddd0c4f7630c2a3ddf6347c1bdaa79d97bcf6bd445f9e60c816b7d77c85a5ae", size = 46837120, upload-time = "2026-08-10T12:36:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/57/5c/f8fc0eb2de03464a557d5a4d0c15e972d73362414696618833b771f7eddd/pyarrow-25.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a4d6d5e9a3d1879a97c08ded0c797579b7965eafd0f0c26c30b45ccc06db939b", size = 50066460, upload-time = "2026-08-10T12:36:53.702Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d1/0dd64fd06de0333b808a02f60981635f067b71aad3a30698a9a104fae778/pyarrow-25.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:514ddb60285631af068875550c90eddc181db3e8e63a032b1559be189e82f056", size = 49937892, upload-time = "2026-08-10T12:37:00.349Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/f89d1bd76d5f3284c2a44d7d7ebbd8204535e5ae2b41f4077069b4ff2ec6/pyarrow-25.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cab40b1edfef0262e0e5251aa2c58d75630f24d06dd7794480243acc001a1d7d", size = 53107240, upload-time = "2026-08-10T12:37:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/b554a8e09f3f3decccf405eb8fbe86696321cbcb5b62d18b4a5057a4c113/pyarrow-25.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:60e89d8f13861a1f7f8d950fa54aebb8023b30734d0ac51ffa80beabe2df4bba", size = 27848683, upload-time = "2026-08-10T12:37:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8b/0d23b47702fcfe8b3618d5292035099675c5a1c48258932350c08020f7b5/pyarrow-25.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:51093dd9e10325fbdb3c10a2ae7c4806e5c822d94e74ae4938b26524a3323fee", size = 35946180, upload-time = "2026-08-10T12:37:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/707d17a5476c55a9541fde0db8213ac30979a792864d72415f176ba50c45/pyarrow-25.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:eb6203482ff3746a5632303a7279ae0b5a304c46985b49ed1378cb350ea6728d", size = 37644787, upload-time = "2026-08-10T12:37:25.795Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b2/cdc98ecf1a6408280bc3a6a07054cdd99a3f4670acc0545d383ce113e87d/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:880523be3d29efcf83d3998835d206118ccf35e3871dbd2fb60408cf6b007a80", size = 46834633, upload-time = "2026-08-10T12:37:33.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6e/d3fafc41f378b2c65be43b827798c0fae42049a641c8526633ed3eb573e2/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:25f8720bf6387d5dc2ebd2622112de630760419e4b66134405dd24110d15f37e", size = 50065507, upload-time = "2026-08-10T12:37:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/d5/12/8d0698954b8c3001844a898e0a6900bebe83d7ee40c11195174c5122f324/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4facd65742a024a4a366328a1d2292062d72d6e023c1b7dda8d4c37544933a25", size = 49955690, upload-time = "2026-08-10T12:37:46.644Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/1ecb936ac6409e90a34d58eea1c7cec09a9ae6d2141b9e49ad01a2b1ea47/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa0559502e1cd6254d6814614085dd9c5a3dd0419362978a936a3f68a9e5c3df", size = 53128198, upload-time = "2026-08-10T12:37:52.531Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/5236033550633c9b7377b2a53660b2bbb06cb06dc09c4356332d67643ca1/pyarrow-25.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:62cd0d785b8aa6675ee355f9fc02252a340f4441257c42674937826fd7594325", size = 27857263, upload-time = "2026-08-10T12:37:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size = 35861559, upload-time = "2026-08-10T12:38:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size = 37628383, upload-time = "2026-08-10T12:38:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size = 46820190, upload-time = "2026-08-10T12:38:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/60/c9/711ca85d79f1ec98f29a5eae2b051e25b4ecec5de3e3c0e2d5c5dcb15664/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5389cdf79447ed1515c9e31620e6e1e2302249564d603f2ad727d4f6d313e4c3", size = 50102437, upload-time = "2026-08-10T12:38:22.487Z" }, + { url = "https://files.pythonhosted.org/packages/80/53/8fb8359ff17cfb6263a1cf3ebf7caec9fe197de118719e84fcb1d0618026/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d51592cb7561e87877c506113e7adbf1342ab579e6c21f0ef44b8ba41cb74c80", size = 49942424, upload-time = "2026-08-10T12:38:28.755Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/4e5ae02a9341571b18a6fca380ac7a58ce6ddae7ab3c060208c0a1e79f02/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6109c94d8b9f3b17a041daca16cacb2f651ad8f1ef70a4232c2c0f37a23da2a8", size = 53144206, upload-time = "2026-08-10T12:38:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/65/ee/197cbf47e49f83e6ebeb946a5259a48a638dea27ac774db42fe78022179d/pyarrow-25.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8858d7bfc22e3f51529aeaa4077225029724623e4595dc9eff8c793935c34140", size = 27953934, upload-time = "2026-08-10T12:38:39.808Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8d/8f271a7a034c834910ec925d56fa4b29733b1380f5289419f5aaa3b02777/pyarrow-25.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c7c534ec03c358a76ea3e505e74c1b6aef290af90c444dfd092dbfe23e755b85", size = 35855328, upload-time = "2026-08-10T12:38:45.489Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/5bac242f4e841b9971d5eb94fdfe2577e2b70be983e27401e72055786037/pyarrow-25.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dda9470024204d7bbf2042b47c6e8a0e47a3eeb8e34405882dfaea6577e0c153", size = 37622415, upload-time = "2026-08-10T12:38:51.107Z" }, + { url = "https://files.pythonhosted.org/packages/63/1f/96d03b4e1506524f7087adb0fd6b2f69f0c9c7aaff1ec36d8030082e15a5/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:44a9120ce5bd81936b8ab9a88076e3fd47c2c6838e0e43630fed83626aca81d9", size = 46813813, upload-time = "2026-08-10T12:38:57.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/d6/33a411115b61dbfc16ad6ad73e71730f6fea654ee3667673bc53ab0e2fe7/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0befcf816e45a1af33ac775a9970b749e4868a230c7372f0ae5e932bee27039f", size = 50104452, upload-time = "2026-08-10T12:39:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size = 49951343, upload-time = "2026-08-10T12:39:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size = 53144784, upload-time = "2026-08-10T12:39:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size = 27870159, upload-time = "2026-08-10T12:39:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ed/fa23d28713004418bbf2407127f80f385bfb0bf4e677bef02c25c27b00ee/pytz-2026.4.tar.gz", hash = "sha256:464303645bafafd72418898368b2429458f709cf1eb6a15372fbcc396b64da63", size = 318444, upload-time = "2026-09-24T03:58:40.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/11/e3df59614bebe0ca50c905442603eda64ae7ac19811d4e4f74aa2d51ca85/pytz-2026.4-py2.py3-none-any.whl", hash = "sha256:9d514388fbc89ca0833203464272ac485b8828568ab73532f5020f17e892a0ff", size = 506747, upload-time = "2026-09-24T03:58:38.629Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "s5cmd" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/20/4c5087622e2149ce3ab2a933b649e2119d407976f77592ddc18be997ec96/s5cmd-0.3.3.tar.gz", hash = "sha256:1c94d2bdff2937ac1610b52aef9219f446675f8cfc820c9d607cffe80daf2e57", size = 16901, upload-time = "2025-09-09T02:22:18.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/f9/767be6152403df4c727266995055fd463b6ab14af40522132255afc66020/s5cmd-0.3.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d341fc8cba5361d2efdeb0c6f1c8528dab8bfce7db9590d87a5b257ac8c2e50f", size = 4753239, upload-time = "2025-09-09T02:22:00.187Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6d/a319f73bde136496fd6a864f12a303262c0047dad18423ee4d8aa937f689/s5cmd-0.3.3-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c3c8b567bba82efc4b88192f30bb54a8b7fd81b701a90b73d3fccb6e5d44a6", size = 4952367, upload-time = "2025-09-09T02:22:02.223Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/a045684a7f818faf2b971e4ceca8793e7ff1bae4514058b205692d4660c9/s5cmd-0.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04821d75b725e8c39f313ebd47243be14e069e6d21b5525d7ddbecc3766cfd6f", size = 4513241, upload-time = "2025-09-09T02:22:04.257Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6b/f2e4d26ace767de4b396ca8b85a556bcf130e5e9c10d8729b1f09f91e2bd/s5cmd-0.3.3-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e32ed9e828c00aea7db2d06446461933eb5c477c5d3c9a0df6199c8271f9aca0", size = 4439452, upload-time = "2025-09-09T02:22:06.053Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/e83dbdaf7b5acac2124f07d172433e48d67287392ecf97ffd2f6258080ba/s5cmd-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:21004047781675e48b3a75271bd1bf9d9b6232f736c416e1644f07f6c44f9f31", size = 4513224, upload-time = "2025-09-09T02:22:07.523Z" }, + { url = "https://files.pythonhosted.org/packages/cf/30/c55d12a26ed22a54dba5fc261f87fb4c097935707cfa29deb654493e1e43/s5cmd-0.3.3-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:bfab2079bfa928a258ace5e140b82a1091099a128699ef55de83d4d001922150", size = 4439438, upload-time = "2025-09-09T02:22:10.363Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/2cfa4868a788ade0ca016b1a8305d69fc36113c94a6878467b2772fafdec/s5cmd-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e72608dbdb60e322c18706d673fafcc268382b2db9be53a08c6eee52574278d", size = 4952352, upload-time = "2025-09-09T02:22:11.897Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e3/c209fe93e8915516e4ff8b5298831d4f86f6d825b2322f963331323745a4/s5cmd-0.3.3-py3-none-win32.whl", hash = "sha256:1d6b429102bdd21f4db3778a43db7fa1112ad8adb88989fbbfc369b911f25252", size = 5113547, upload-time = "2025-09-09T02:22:13.438Z" }, + { url = "https://files.pythonhosted.org/packages/01/03/2092f3cdc317f704f0ebeb67ad046ca98b012d837f26af2a98e044a31995/s5cmd-0.3.3-py3-none-win_amd64.whl", hash = "sha256:db79dbffc027a2838a35f272c144b8747073e23f3d76ee92f50a6a97708acfab", size = 5113553, upload-time = "2025-09-09T02:22:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/590e067e6b09acad431bbcd1d79717ab5878456d9d7c6500bd4fd52a8eb1/s5cmd-0.3.3-py3-none-win_arm64.whl", hash = "sha256:2b2c1eabd1a7e96ea7b5907aa0494acb1efe31335b3d02b4d15a86d2e813abf7", size = 4595065, upload-time = "2025-09-09T02:22:16.898Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +]