diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0191686..e4092b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: run: pip install uv - name: Install dependencies - run: uv sync --extra dev + run: uv sync --extra dev --extra web - name: Run linter run: uv run ruff check . @@ -62,10 +62,28 @@ jobs: with: python-version: '3.13' + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web-ui/package-lock.json + - name: Install uv run: pip install uv - - name: Build package + - name: Install Node.js dependencies + run: cd web-ui && npm ci + + - name: Build frontend + run: cd web-ui && npm run build + + - name: Bundle frontend into package + run: | + rm -rf src/tablesleuth/web + cp -r web-ui/out src/tablesleuth/web + + - name: Build Python package run: uv build - name: Check package diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d165a84..7ad83a1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,9 +24,27 @@ jobs: with: python-version: '3.13' + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web-ui/package-lock.json + - name: Install uv run: pip install uv + - name: Install Node.js dependencies + run: cd web-ui && npm ci + + - name: Build frontend + run: cd web-ui && npm run build + + - name: Bundle frontend into package + run: | + rm -rf src/tablesleuth/web + cp -r web-ui/out src/tablesleuth/web + - name: Build package run: uv build diff --git a/.gitignore b/.gitignore index 2a4b997..583ac48 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +/lib/ +/lib64/ parts/ sdist/ var/ @@ -88,3 +88,14 @@ docker-compose.override.yml *.tar.gz backups/ ssl/ + +# Web UI built artifacts (built by make build-release; placeholder index.html is committed) +# Ignore everything in web/ EXCEPT the placeholder index.html +src/tablesleuth/web/* +!src/tablesleuth/web/index.html + +# Next.js dev artifacts (web-ui/ source is committed, build output is not) +web-ui/.next/ +web-ui/out/ +web-ui/node_modules/ +web-ui/next-env.d.ts diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18109cc..8d0423c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,7 @@ repos: - botocore>=1.34.0 - fsspec>=2023.0.0 - s3fs>=2023.0.0 + - fastavro>=1.9.0 args: [--config-file=pyproject.toml, src/] pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 8461dc5..394df54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,80 @@ All notable changes to this project will be documented in this file. +## [0.6.0] - 2026-02-23 + +### Added + +- **Browser-Based Web UI** - New `tablesleuth web` command launches a FastAPI + Next.js interface + - Full Parquet, Iceberg, Delta Lake, and GizmoSQL analysis in the browser + - Optional install: `pip install tablesleuth[web]`; requires `fastapi`, `uvicorn[standard]`, `python-multipart`, `fastavro` + - Pre-built Next.js static export bundled in the wheel via Hatchling force-include (no Node.js needed for end users) + - Configurable host/port and optional auto-browser-open; CORS origins configurable via `TABLESLEUTH_CORS_ORIGINS` + - `TABLESLEUTH_WEB_UI_DIR` env var overrides static file path for custom deployments + +- **FastAPI REST Backend** (`src/tablesleuth/api/`) + - `main.py` — FastAPI app with CORS, exception handlers, health endpoint (`/api/health`), and static file mount + - `routers/parquet.py` — Parquet file inspection endpoints + - `routers/iceberg.py` — Iceberg snapshot browsing and comparison endpoints; snapshot IDs serialized as strings to prevent JavaScript integer precision loss + - `routers/delta.py` — Delta Lake version history and forensics endpoints + - `routers/config.py` — Configuration read and validation endpoints; includes `.pyiceberg.yaml` upload support + - `routers/gizmosql.py` — Column profiling and `/gizmosql/compare` snapshot comparison endpoint + +- **GizmoSQL Snapshot Comparison** (`/gizmosql/compare`) + - Side-by-side Iceberg snapshot or Delta version query performance comparison + - Metadata-based scan stats sourced directly from Iceberg snapshot summary fields (`total-data-files`, `total-delete-files`, `total-records`, `total-position-deletes`, `total-equality-deletes`, `total-files-size`) via `_iceberg_snapshot_scan_stats()` — DuckDB's `EXPLAIN ANALYZE` is not reliably parseable over Arrow Flight SQL + - MOR breakdown fields on `QueryPerformanceMetrics`: `data_files_scanned`, `delete_files_scanned`, `data_rows_scanned`, `delete_rows_scanned` + - `rows_scanned` definition: `total-records + total-position-deletes + total-equality-deletes` (physical reads before merge apply) + - Web UI shows MOR sub-rows only when at least one snapshot has delete files + - Scan stats registered with `profiler.register_iceberg_scan_stats()` before query execution + +- **Iceberg Metadata Patching** (`src/tablesleuth/services/iceberg_manifest_patch.py`) + - `patched_iceberg_metadata(native_table, snapshot_id)` context manager — always writes a temporary local `metadata.json` before passing a table to DuckDB + - Fixes DuckDB `current-snapshot-id` delete-file bleed: DuckDB's `iceberg_scan()` applies delete files based on the metadata's `current-snapshot-id` field, not the `version =>` argument; the patch overwrites it with the target snapshot ID + - Fixes DuckDB rejection of uppercase `"PARQUET"` format strings in delete manifest entries; re-encodes affected manifests via fastavro with lowercased value + - Handles local, S3, and `file://` URIs transparently; never yields the original path + +- **API Test Suite** (`tests/api/`) + - Smoke tests for all five routers using `fastapi.testclient.TestClient` + - `test_main.py`, `test_parquet_router.py`, `test_iceberg_router.py`, `test_delta_router.py`, `test_config_router.py`, `test_gizmosql_router.py` + - Requires `--extra web` / `uv sync --extra web` + +- **New Makefile Targets** + - `dev-web-install-npm` — installs Node.js dependencies in `web-ui/` (run once after checkout) + - `dev-api` — starts FastAPI with hot-reload at `localhost:8000` + - `dev-web` — starts Next.js dev server at `localhost:3000` + - `build-web` — runs `npm run build` in `web-ui/` + - `build-release` — runs `build-web` then copies `web-ui/out/` into `src/tablesleuth/web/` + - `start-web` — runs `build-release` then launches `tablesleuth web` + +### Changed + +- **Dependency Upgrades** — all core libraries updated to latest versions + - `pyiceberg` → 0.11.0+ + - `deltalake` → 1.4.2+ + - `textual` → 0.86.2+ + - `pyarrow` → 23.0.0+ + - `pandas` → 3.0.1+ + - `ruff` → 0.14.4+ + - `mypy` → 1.18.2+ + - `pytest` → 8.4.2+ +- **Python version range** — now `>=3.13,<3.15` (added 3.14 upper bound) +- **Default catalog** — `tablesleuth.toml` default changed from `"local"` to `"glue"` to reflect typical production usage +- **Makefile** — replaced POSIX-only shell commands with Python equivalents for cross-platform (Windows) compatibility + +### Fixed + +- **Windows path handling** — `iceberg_manifest_patch.py` and `api/routers/iceberg.py` now correctly normalize Windows paths before passing to PyIceberg and DuckDB +- **Iceberg catalog serialization** — fixed `api/routers/iceberg.py` errors when serializing catalog objects with non-JSON-serializable fields +- **SPA fallback route** — removed FastAPI catch-all route that was intercepting `_next/static/*` asset requests and returning 404 for frontend JS/CSS bundles +- **Snapshot ID JavaScript precision** — Iceberg snapshot IDs (int64) are now serialized as strings in API responses to prevent silent precision loss in JavaScript (`Number.MAX_SAFE_INTEGER` is 2⁵³−1) +- **PyIceberg Windows path patch** — fixed path normalization for Windows-style absolute paths in the metadata patch context manager +- **npm ReDoS vulnerability** — resolved `minimatch` ReDoS security advisory in `web-ui/` dependencies; added `autoprefixer` for CSS compatibility + +### Dependencies + +- **New optional group `[web]`**: `fastapi>=0.131.0`, `uvicorn[standard]>=0.32.0`, `python-multipart>=0.0.12`, `fastavro>=1.9.0` + ## [0.5.3] - 2026-01-25 ### Changed diff --git a/DEVELOPMENT_SETUP.md b/DEVELOPMENT_SETUP.md index 3c0380e..9490e56 100644 --- a/DEVELOPMENT_SETUP.md +++ b/DEVELOPMENT_SETUP.md @@ -1,13 +1,13 @@ -# Table Sleuth Development Setup +# TableSleuth Development Setup -This guide covers setting up Table Sleuth for development, testing, and contributing. +This guide covers setting up TableSleuth for development, testing, and contributing. ## Prerequisites - Python 3.13+ - `uv` package manager - Git -- Docker (for integration tests) +- Node.js 20+ and npm (required to rebuild the web UI frontend) - AWS CLI (for AWS-related development) ## Quick Start @@ -17,7 +17,7 @@ This guide covers setting up Table Sleuth for development, testing, and contribu git clone https://github.com/jamesbconner/TableSleuth.git cd TableSleuth -# Install dependencies with dev tools +# Install dependencies with dev tools (includes web extras) make install-dev # Install pre-commit hooks @@ -54,10 +54,20 @@ make check # Run all quality checks ### Build & Run ```bash -make build # Build distribution packages +make build # Build wheel + sdist (for releases) make run # Run tablesleuth CLI ``` +### Web UI Development (v0.6.0+) +```bash +make dev-web-install-npm # Install Node.js dependencies (run once after checkout) +make dev-api # Start FastAPI server at localhost:8000 (hot-reload) +make dev-web # Start Next.js dev server at localhost:3000 (hot-reload) +make build-web # Build Next.js static export only +make build-release # Build frontend and copy into src/tablesleuth/web/ +make start-web # build-release then launch tablesleuth web +``` + ### Cleanup ```bash make clean # Remove build artifacts and cache @@ -149,16 +159,58 @@ gizmosql_server -U test_user -P test_password -Q \ ### Running from Source ```bash -# Run from source (development mode) -python -m tablesleuth.cli inspect data/sample.parquet - -# Or use the installed command -tablesleuth inspect data/sample.parquet +# Run TUI from source +uv run tablesleuth parquet data/sample.parquet +uv run tablesleuth iceberg --catalog local --table db.table +uv run tablesleuth delta path/to/table # Run with verbose logging for debugging -tablesleuth inspect data/sample.parquet -v +uv run tablesleuth parquet data/sample.parquet -v +``` + +## Web UI Development + +The web UI consists of a FastAPI backend and a Next.js frontend. During development you run them separately for hot-reload. + +### First-Time Setup + +```bash +# Install Node.js dependencies (once after checkout) +make dev-web-install-npm + +# Install Python web extras +uv sync --extra web +``` + +### Hot-Reload Development (two terminals) + +**Terminal 1 — FastAPI backend:** +```bash +make dev-api # http://localhost:8000/api/... ``` +**Terminal 2 — Next.js frontend:** +```bash +make dev-web # http://localhost:3000 +``` + +The Next.js dev server proxies API calls to `localhost:8000`. Edit files in `web-ui/src/` and Python source normally; both servers reload automatically. + +### Building the Frontend for Inclusion in the Wheel + +```bash +make build-release # npm run build → copies web-ui/out/ to src/tablesleuth/web/ +``` + +This is required before `uv build` so the compiled static export is bundled in the wheel. The GitHub Actions publish workflow runs this step automatically; you only need it locally when verifying the built package or committing an updated `src/tablesleuth/web/index.html`. + +### Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `TABLESLEUTH_WEB_UI_DIR` | package `web/` dir | Override path to static Next.js export | +| `TABLESLEUTH_CORS_ORIGINS` | `http://localhost:3000` | Comma-separated allowed CORS origins | + ## Testing ### Unit Tests @@ -174,6 +226,16 @@ pytest tests/test_parquet_service.py::test_inspect_file -v pytest --cov=src/tablesleuth --cov-report=html --cov-report=term-missing ``` +### API Tests (v0.6.0+) + +```bash +# Requires web extras installed +uv sync --extra web --extra dev + +# Run API smoke tests +pytest tests/api/ -v +``` + ### Integration Tests ```bash @@ -183,7 +245,7 @@ export TEST_GIZMOSQL_USERNAME="test_user" export TEST_GIZMOSQL_PASSWORD="test_password" # Run integration tests -pytest tests/integration/ -v +pytest -m integration -v # Run end-to-end tests pytest tests/test_end_to_end.py -v @@ -333,3 +395,4 @@ After development setup: 2. Check [ARCHITECTURE.md](docs/ARCHITECTURE.md) for system design 3. Read [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines 4. See [QUICKSTART.md](QUICKSTART.md) for usage examples +5. See [DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) for API reference and component interfaces diff --git a/Makefile b/Makefile index 69f9632..7eccb15 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install install-dev sync clean test test-cov lint format type-check security pre-commit check build run zip +.PHONY: help install install-dev sync clean test test-cov lint format type-check security pre-commit check build run zip dev-api dev-web dev-web-install-npm build-web build-release start-web # Default target help: @@ -21,6 +21,14 @@ help: @echo "Quality (runs all checks):" @echo " make check Run all quality checks" @echo "" + @echo "Web UI Development:" + @echo " make dev-api Start FastAPI dev server (localhost:8000, hot-reload)" + @echo " make dev-web-install-npm Install Node.js dependencies (run once after checkout)" + @echo " make dev-web Start Next.js dev server (localhost:3000)" + @echo " make build-web Build Next.js static export" + @echo " make build-release Build frontend and bundle into Python package" + @echo " make start-web Build release and launch web UI" + @echo "" @echo "Build & Run:" @echo " make build Build distribution packages" @echo " make run Run tablesleuth CLI" @@ -77,22 +85,28 @@ run: # Create source archive (excludes .gitignore files and untracked files) zip: - @echo "Creating source code archive..." - @VERSION=$$(grep '^version = ' pyproject.toml | cut -d'"' -f2); \ - ARCHIVE_NAME="tablesleuth-$$VERSION-src.zip"; \ - git archive --format=zip --prefix=tablesleuth/ -o $$ARCHIVE_NAME HEAD; \ - echo "Created $$ARCHIVE_NAME (excludes .gitignore patterns)" + uv run python -c "import subprocess, tomllib, pathlib; v=tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version']; n=f'tablesleuth-{v}-src.zip'; subprocess.run(['git','archive','--format=zip','--prefix=tablesleuth/','-o',n,'HEAD'],check=True); print(f'Created {n} (excludes .gitignore patterns)')" + +# Web UI development +dev-api: + uv run uvicorn tablesleuth.api.main:app --host localhost --port 8000 --reload + +dev-web-install-npm: + cd web-ui && npm install + +dev-web: + cd web-ui && npm run dev + +build-web: + cd web-ui && npm run build + +build-release: build-web + uv run python -c "import shutil; shutil.rmtree('src/tablesleuth/web', ignore_errors=True); shutil.copytree('web-ui/out', 'src/tablesleuth/web')" + +start-web: build-release + uv run tablesleuth web # Cleanup clean: - rm -rf build/ - rm -rf dist/ - rm -rf *.egg-info - rm -rf .pytest_cache/ - rm -rf .mypy_cache/ - rm -rf .ruff_cache/ - rm -rf htmlcov/ - rm -rf .coverage - rm -rf *.zip - find . -type f -name "*.pyc" -delete - find . -type d -name __pycache__ -delete + uv run python -c "import shutil, pathlib; [shutil.rmtree(d, ignore_errors=True) for d in ['build', 'dist', '.pytest_cache', '.mypy_cache', '.ruff_cache', 'htmlcov', 'src/tablesleuth/web', 'web-ui/out', 'web-ui/.next']]; [p.unlink(missing_ok=True) for p in [*pathlib.Path('.').glob('.coverage'), *pathlib.Path('.').glob('*.zip')]]; [shutil.rmtree(p, ignore_errors=True) for p in pathlib.Path('src').glob('**/*.egg-info')]; [p.unlink() for p in pathlib.Path('.').rglob('*.pyc')]; [shutil.rmtree(p, ignore_errors=True) for p in pathlib.Path('.').rglob('__pycache__')]" + -git restore src/tablesleuth/web/index.html diff --git a/QUICKSTART.md b/QUICKSTART.md index d285bbe..c2fd836 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,9 +1,10 @@ -# Table Sleuth Quick Start Guide +# TableSleuth Quick Start Guide -Get up and running with Table Sleuth for Parquet forensics and Iceberg snapshot analysis. +Get up and running with TableSleuth for Parquet forensics, Iceberg snapshot analysis, and Delta Lake inspection. ## Table of Contents - [Local Installation](#local-installation) +- [Web UI Quick Start](#web-ui-quick-start) - [AWS EC2 Deployment](#aws-ec2-deployment) - [Basic Usage](#basic-usage) - [Iceberg Snapshot Analysis](#iceberg-snapshot-analysis) @@ -25,7 +26,7 @@ Get up and running with Table Sleuth for Parquet forensics and Iceberg snapshot git clone https://github.com/jamesbconner/TableSleuth.git cd TableSleuth -# Install dependencies +# Install TUI dependencies (+ web UI and dev extras) uv sync --all-extras # Activate virtual environment @@ -38,6 +39,13 @@ tablesleuth init tablesleuth config-check ``` +Or install directly from PyPI: + +```bash +pip install tablesleuth # TUI only +pip install tablesleuth[web] # TUI + browser web UI (v0.6.0+) +``` + ### Configure AWS Credentials (if using S3) ```bash @@ -78,6 +86,46 @@ catalog: --- +## Web UI Quick Start + +The browser-based web UI (v0.6.0+) provides the same analysis features as the TUI in a browser. + +### Install and Launch + +```bash +# Install with web extras +pip install tablesleuth[web] +# or from source: +uv sync --extra web + +# Launch (opens http://localhost:8000 automatically) +tablesleuth web +``` + +### What You Can Do + +| Page | Features | +|---|---| +| **Parquet** | Browse files, inspect schema, row groups, data sample, column profiling | +| **Iceberg** | Browse snapshots, view files/schema/deletes, compare two snapshots | +| **Delta** | Browse versions, forensics, recommendations | +| **GizmoSQL** | Run queries, compare snapshot performance with MOR breakdown | +| **Settings** | View and validate configuration | + +### Development Mode + +If you have Node.js 20+ installed and cloned the repo: + +```bash +# Terminal 1: FastAPI hot-reload +make dev-api # http://localhost:8000/api/... + +# Terminal 2: Next.js hot-reload +make dev-web # http://localhost:3000 +``` + +--- + ## AWS EC2 Deployment For production use with large datasets in S3, deploy to EC2 with pre-configured environment using AWS CDK. @@ -479,9 +527,9 @@ tmux source-file ~/.tmux.conf This is normal for older snapshots that don't record operation type. TableSleuth infers the operation from file changes. -### Files Scanned Shows 0 +### Files Scanned Shows 0 or Doesn't Match Expectations -This can happen if DuckDB's EXPLAIN ANALYZE doesn't expose file counts. The fallback reads from Iceberg metadata, but may not always be available. +File/row/byte counts in snapshot comparison are sourced from Iceberg snapshot metadata summary fields, not from DuckDB's query plan (which is not reliably parseable over Arrow Flight SQL). Counts reflect the **full snapshot**, not partition-pruned results. Only `rows_returned` is accurate when predicates prune at runtime. --- @@ -489,14 +537,14 @@ This can happen if DuckDB's EXPLAIN ANALYZE doesn't expose file counts. The fall - Read [USER_GUIDE.md](docs/USER_GUIDE.md) for detailed features - See [ARCHITECTURE.md](docs/ARCHITECTURE.md) for system design -- Check [gizmosql-deployment.md](docs/gizmosql-deployment.md) for GizmoSQL setup -- Review [s3_tables_guide.md](docs/s3_tables_guide.md) for S3 Tables configuration +- Check [GIZMOSQL_DEPLOYMENT_GUIDE.md](docs/GIZMOSQL_DEPLOYMENT_GUIDE.md) for GizmoSQL setup +- See [TABLESLEUTH_SETUP.md](TABLESLEUTH_SETUP.md) for full setup including catalog and AWS config --- ## Quick Reference -### Keyboard Shortcuts +### Keyboard Shortcuts (TUI) | Key | Action | |-----|--------| @@ -504,7 +552,7 @@ This can happen if DuckDB's EXPLAIN ANALYZE doesn't expose file counts. The fall | Tab | Switch tabs | | Enter | Select | | q | Quit | -| r | Refresh (Iceberg view) | +| r | Refresh | | c | Toggle Compare mode | | t | Run performance test | | x | Cleanup test tables | @@ -524,6 +572,12 @@ tablesleuth iceberg --catalog my_catalog --table my_database.my_table # Iceberg table (S3 Tables) tablesleuth iceberg --catalog my_s3tables --table my_database.my_table +# Delta Lake table +tablesleuth delta path/to/delta/table + +# Web UI (v0.6.0+) +tablesleuth web + # Verbose logging tablesleuth iceberg --catalog my_catalog --table my_database.my_table -v ``` diff --git a/README.md b/README.md index 0a803e0..df2c6fb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Publish to PyPI](https://github.com/jamesbconner/TableSleuth/actions/workflows/publish.yml/badge.svg)](https://github.com/jamesbconner/TableSleuth/actions/workflows/publish.yml) [![codecov](https://codecov.io/gh/jamesbconner/TableSleuth/graph/badge.svg?token=SXREVJC93E)](https://codecov.io/gh/jamesbconner/TableSleuth) -A powerful terminal-based tool for deep inspection of Parquet files, Apache Iceberg tables, and Delta Lake tables. Analyze file structure, metadata, row groups, column statistics, and table evolution with an intuitive TUI interface. +A powerful forensic analysis tool for Parquet files, Apache Iceberg tables, and Delta Lake tables. Available as both a terminal TUI and a browser-based web interface — inspect file structure, metadata, row groups, column statistics, and table evolution. ## Key Features @@ -41,6 +41,7 @@ A powerful terminal-based tool for deep inspection of Parquet files, Apache Iceb ### Interface - **Interactive TUI** - Keyboard-driven navigation with rich visualizations +- **Browser-Based Web UI** - FastAPI + Next.js interface launched with `tablesleuth web` (v0.6.0+) - **Multi-Source Support** - Local files, S3, Iceberg catalogs, and Delta tables - **Performance Optimized** - Async operations, caching, and lazy loading @@ -118,15 +119,19 @@ A powerful terminal-based tool for deep inspection of Parquet files, Apache Iceb # Install with uv (recommended) uv sync -# Inspect a Parquet file +# Inspect a Parquet file (TUI) tablesleuth parquet data/file.parquet # Inspect a directory (recursive) tablesleuth parquet data/warehouse/ -# Inspect an Iceberg table +# Inspect an Iceberg table (TUI) tablesleuth iceberg --catalog local --table db.table +# Launch the browser-based web UI (v0.6.0+) +pip install tablesleuth[web] +tablesleuth web # opens http://localhost:8000 + # Inspect AWS S3 Tables (using ARN with parquet command) tablesleuth parquet "arn:aws:s3tables:us-east-2:123456789012:bucket/my-bucket/table/db.table" ``` @@ -141,13 +146,17 @@ tablesleuth parquet "arn:aws:s3tables:us-east-2:123456789012:bucket/my-bucket/ta **Requirements:** Python 3.13+ and [uv](https://docs.astral.sh/uv/) ```bash -# Install from PyPI +# Install from PyPI (TUI only) pip install tablesleuth +# Install with web UI support (v0.6.0+) +pip install tablesleuth[web] + # Or install from source git clone https://github.com/jamesbconner/TableSleuth cd TableSleuth -uv sync +uv sync # TUI only +uv sync --extra web # include web UI dependencies # Verify installation tablesleuth --version @@ -237,6 +246,10 @@ tablesleuth config-check # Validate configuration tablesleuth config-check -v # Detailed validation tablesleuth config-check --with-gizmosql # Include GizmoSQL connection test +# Web UI (v0.6.0+, requires tablesleuth[web]) +tablesleuth web # Launch browser UI at localhost:8000 +tablesleuth web --host 0.0.0.0 --port 9000 # Custom host/port + # Inspect Parquet files tablesleuth parquet file.parquet tablesleuth parquet directory/ @@ -336,7 +349,9 @@ See [GizmoSQL Deployment Guide](docs/GIZMOSQL_DEPLOYMENT_GUIDE.md) for complete TableSleuth uses a layered architecture: +- **CLI Layer** - Click-based commands with auto-discovery; includes `tablesleuth web` (v0.6.0+) - **TUI Layer** - Textual-based terminal interface with rich visualizations +- **Web API Layer** - FastAPI REST backend serving a Next.js static frontend (v0.6.0+) - **Service Layer** - Business logic for file inspection, profiling, and discovery - **Integration Layer** - PyArrow for Parquet, PyIceberg for tables, GizmoSQL for profiling @@ -356,6 +371,10 @@ uv run pre-commit run --all-files # Type checking mypy src/ + +# Web UI development (two terminals) +make dev-api # FastAPI at localhost:8000 +make dev-web # Next.js dev server at localhost:3000 ``` See [Development Setup](DEVELOPMENT_SETUP.md) for complete development environment setup. @@ -374,6 +393,7 @@ See [Development Setup](DEVELOPMENT_SETUP.md) for complete development environme ### Advanced Topics - **[Performance Profiling](docs/PERFORMANCE_PROFILING.md)** - Query performance analysis - **[GizmoSQL Deployment](docs/GIZMOSQL_DEPLOYMENT_GUIDE.md)** - Profiling backend setup +- **[Web UI Development](DEVELOPMENT_SETUP.md#web-ui-development)** - Building and running the browser interface ### Development - **[Development Setup](DEVELOPMENT_SETUP.md)** - Dev environment and workflows @@ -382,21 +402,23 @@ See [Development Setup](DEVELOPMENT_SETUP.md) for complete development environme ## What's New -### v0.5.3 (Latest) -- 🏗️ **CLI Architecture Refactored** - Modular command structure with auto-loading - - Split monolithic CLI into focused command modules (80% code reduction per module) - - Dynamic command discovery - new commands auto-register by convention - - Significantly improved maintainability and extensibility -- 🔧 **Service Layer Improvements** - Enhanced abstractions and reduced coupling - - **DeltaLogFileSystem** - Unified filesystem interface eliminating ~250 lines of duplication - - **SnapshotPerformanceAnalyzer** - Explicit interface validation with fail-fast error handling - - Reduced complexity by 40-50% across refactored methods -- 📊 **Code Quality: A (96/100)** - Upgraded from A (94/100) - - Eliminated 11 developer-days of technical debt - - All 165+ tests passing - - Production-ready architecture - -### v0.5.2 +### v0.6.0 (Latest) +- 🌐 **Browser-Based Web UI** - New `tablesleuth web` command launches a FastAPI + Next.js interface + - Full Parquet, Iceberg, Delta Lake, and GizmoSQL analysis in the browser + - Optional install: `pip install tablesleuth[web]` + - Hot-reload development mode (`make dev-api` / `make dev-web`) + - Pre-built static export bundled in the wheel (no Node.js needed for end users) +- 📊 **GizmoSQL Snapshot Comparison API** - `/gizmosql/compare` endpoint for head-to-head snapshot analysis + - MOR breakdown: per-type file counts, row counts, and bytes (data vs. delete files) + - Metadata-based scan stats sourced directly from Iceberg snapshot summary fields + - `rows_scanned` definition: total-records + position-deletes + equality-deletes (physical reads) +- 🔧 **Iceberg Metadata Patching** - New `patched_iceberg_metadata()` context manager + - Fixes DuckDB `current-snapshot-id` delete-file bleed when querying older snapshots + - Fixes DuckDB rejection of uppercase `PARQUET` format strings in delete manifests +- 📦 **Dependency Upgrades** - All core libraries updated to latest versions + - pyiceberg 0.11.0+, deltalake 1.4.2+, textual 0.86.2+, pyarrow 23.0.0+ + +### v0.5.3 - 🚀 **AWS CDK Infrastructure** - Production-ready CDK implementation for EC2 deployment - Replaces legacy boto3 scripts with infrastructure-as-code approach - Follows AWS CDK best practices (least-privilege IAM, EBS encryption, VPC Flow Logs) @@ -432,7 +454,7 @@ See [Development Setup](DEVELOPMENT_SETUP.md) for complete development environme - 🔒 **Enhanced Security** - Improved IAM permissions and encryption - 📚 **Consolidated Documentation** - Streamlined deployment guides and removed legacy content -### v0.5.0 (Current) +### v0.5.0 - 🎉 **Delta Lake Support** - Full Delta table inspection and forensics - Version history navigation and time travel - File size analysis and small file detection @@ -480,7 +502,7 @@ Contributions welcome! See [Developer Guide](docs/DEVELOPER_GUIDE.md) and [Devel ## License -MIT License - See [LICENSE](LICENSE) for details. +Apache 2.0 License - See [LICENSE](LICENSE) for details. ## Support diff --git a/TABLESLEUTH_SETUP.md b/TABLESLEUTH_SETUP.md index 7dd8bd2..98c8bf2 100644 --- a/TABLESLEUTH_SETUP.md +++ b/TABLESLEUTH_SETUP.md @@ -1,12 +1,14 @@ -# Table Sleuth Setup Guide +# TableSleuth Setup Guide -Complete setup guide for Table Sleuth with different catalog configurations and deployment options. +Complete setup guide for TableSleuth with different catalog configurations and deployment options. ## Table of Contents - [Prerequisites](#prerequisites) +- [Installation](#installation) - [Local Development Setup](#local-development-setup) - [Catalog Configuration](#catalog-configuration) - [GizmoSQL Setup](#gizmosql-setup) +- [Web UI Setup](#web-ui-setup) - [AWS EC2 Production Setup](#aws-ec2-production-setup) - [Verification](#verification) - [Troubleshooting](#troubleshooting) @@ -31,6 +33,33 @@ Complete setup guide for Table Sleuth with different catalog configurations and --- +## Installation + +### From PyPI + +```bash +# TUI only (terminal interface) +pip install tablesleuth + +# TUI + Web UI (browser interface, v0.6.0+) +pip install tablesleuth[web] +``` + +### From Source + +```bash +git clone https://github.com/jamesbconner/TableSleuth.git +cd TableSleuth + +# TUI only +uv sync + +# TUI + Web UI +uv sync --extra web +``` + +--- + ## Local Development Setup ### 1. Clone and Install @@ -270,6 +299,44 @@ gizmosql_client --command Execute --use-tls --tls-skip-verify --username gizmosq --- +## Web UI Setup + +The browser-based web UI (v0.6.0+) is an optional feature that launches a FastAPI server serving a Next.js frontend. + +### Install Web Dependencies + +```bash +# From PyPI +pip install tablesleuth[web] + +# From source +uv sync --extra web +``` + +### Launch the Web UI + +```bash +# Start at http://localhost:8000 (opens browser automatically) +tablesleuth web + +# Custom host and port +tablesleuth web --host 0.0.0.0 --port 9000 + +# Suppress auto-open +tablesleuth web --no-browser +``` + +### Configuration + +The web UI reads the same `tablesleuth.toml` and `.pyiceberg.yaml` as the TUI. Additional environment variables: + +| Variable | Default | Description | +|---|---|---| +| `TABLESLEUTH_WEB_UI_DIR` | built-in `web/` | Override path to static Next.js export | +| `TABLESLEUTH_CORS_ORIGINS` | `http://localhost:3000` | Comma-separated CORS origins | + +--- + ## AWS EC2 Production Setup For production deployments with large datasets, use the automated EC2 setup. @@ -493,7 +560,7 @@ After setup: 1. **Read the [QUICKSTART.md](QUICKSTART.md)** for usage examples 2. **Review [USER_GUIDE.md](docs/USER_GUIDE.md)** for detailed features 3. **Check [ARCHITECTURE.md](docs/ARCHITECTURE.md)** for system design -4. **See [gizmosql-deployment.md](docs/gizmosql-deployment.md)** for advanced GizmoSQL setup +4. **See [GIZMOSQL_DEPLOYMENT_GUIDE.md](docs/GIZMOSQL_DEPLOYMENT_GUIDE.md)** for advanced GizmoSQL setup --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c0eb614..cd3a516 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,10 +1,10 @@ -# Table Sleuth Architecture +# TableSleuth Architecture ## Overview -Table Sleuth is a Python-based Parquet file forensics and Iceberg table analysis tool built with a layered architecture that separates concerns between presentation, business logic, and data access. The system provides comprehensive inspection of Parquet files and Iceberg tables with support for multiple catalog types (local SQL, AWS Glue, AWS S3 Tables), column profiling via GizmoSQL/DuckDB, and performance testing across Iceberg snapshots. +TableSleuth is a Python-based tool for forensic analysis of Parquet files, Apache Iceberg tables, and Delta Lake tables. It exposes two interfaces — a Textual terminal TUI and a FastAPI + Next.js browser-based web UI (v0.6.0+) — built on a shared layered architecture that separates concerns between presentation, business logic, and data access. The system supports multiple catalog types (local SQL, AWS Glue, AWS S3 Tables), column profiling via GizmoSQL/DuckDB, and performance testing across Iceberg snapshots. -**Current Version**: 0.5.3 +**Current Version**: 0.6.0 This document provides a comprehensive overview of the system architecture, design patterns, and key technical decisions. @@ -21,15 +21,37 @@ This document provides a comprehensive overview of the system architecture, desi │ │ - Directories │ │ - Comparison │ │ - Forensics │ │ │ │ - Iceberg tables │ │ - Performance │ │ - Optimization │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ │ +│ │ web command │ (v0.6.0+) │ +│ │ - FastAPI server│ │ +│ │ - Next.js UI │ │ +│ └──────────────────┘ │ │ │ │ Modular CLI Structure (v0.5.3+): │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ cli/__init__.py - Auto-loading command discovery │ │ │ │ cli/helpers.py - Shared utilities │ │ -│ │ cli/init.py, config_check.py, parquet.py, iceberg.py, delta.py│ │ +│ │ cli/init.py, config_check.py, parquet.py, iceberg.py, │ │ +│ │ delta.py, web.py │ │ │ └──────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ + ┌─────────┴─────────┐ + │ │ + ▼ ▼ +┌──────────────────────────────┐ ┌───────────────────────────────────────┐ +│ Presentation Layer (TUI) │ │ Web API Layer (v0.6.0+) │ +│ Textual terminal interface │ │ FastAPI + uvicorn │ +│ │ │ ┌─────────────────────────────────┐ │ +│ │ │ │ api/routers/ │ │ +│ │ │ │ parquet.py, iceberg.py, │ │ +│ │ │ │ delta.py, config.py, │ │ +│ │ │ │ gizmosql.py │ │ +│ │ │ └─────────────────────────────────┘ │ +│ │ │ Serves Next.js static export at / │ +└──────────────────────────────┘ └───────────────────────────────────────┘ + │ │ + └─────────┬─────────┘ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Presentation Layer (Textual TUI) │ @@ -104,6 +126,13 @@ This document provides a comprehensive overview of the system architecture, desi │ │ - File discovery │ │ - Metadata parse │ │ - Overhead calc │ │ │ │ - S3 Tables ARN │ │ - Table info │ │ │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ │ +│ │ iceberg_manifest │ (v0.6.0+) │ +│ │ _patch │ │ +│ │ - DuckDB compat │ │ +│ │ - Snapshot patch │ │ +│ │ - Format fix │ │ +│ └──────────────────┘ │ │ │ │ Performance Testing: │ │ ┌──────────────────┐ ┌──────────────────┐ │ @@ -179,7 +208,7 @@ This document provides a comprehensive overview of the system architecture, desi ## CLI Commands -Table Sleuth provides a **modular CLI architecture** (v0.5.3+) with auto-loading command discovery. Each command is in its own focused module, following a format-oriented design pattern. +TableSleuth provides a **modular CLI architecture** (v0.5.3+) with auto-loading command discovery. Each command is in its own focused module, following a format-oriented design pattern. ### CLI Architecture (v0.5.3+) @@ -192,7 +221,8 @@ src/tablesleuth/cli/ ├── config_check.py # Config validation ├── parquet.py # Parquet inspection ├── iceberg.py # Iceberg analysis -└── delta.py # Delta Lake inspection +├── delta.py # Delta Lake inspection +└── web.py # Web UI server (v0.6.0+) ``` **Auto-Loading Pattern**: @@ -261,6 +291,28 @@ tablesleuth iceberg --catalog ratebeer --table ratebeer.reviews -v - Query performance testing between snapshots - Predefined query templates +### 4. `web` Command (v0.6.0+) + +**Purpose**: Launch browser-based web UI (requires `tablesleuth[web]`) + +**Usage**: +```bash +# Start on default host/port (localhost:8000) +tablesleuth web + +# Custom host and port +tablesleuth web --host 0.0.0.0 --port 9000 + +# Suppress auto-opening the browser +tablesleuth web --no-browser +``` + +**Features**: +- Serves pre-built Next.js static export from the installed package +- Provides REST API for all analysis features (Parquet, Iceberg, Delta, GizmoSQL) +- CORS configured for development (`http://localhost:3000` by default) +- Environment variable overrides: `TABLESLEUTH_WEB_UI_DIR`, `TABLESLEUTH_CORS_ORIGINS` + ### 3. `delta` Command **Purpose**: Analyze Delta Lake tables with forensic analysis @@ -1243,7 +1295,7 @@ tests/ 3. Add CLI option 4. Add tests -## Current Features (v0.4.2) +## Current Features (v0.6.0) ### Parquet Inspection - **File Discovery**: @@ -1307,6 +1359,14 @@ tests/ - Custom SQL query support - Metrics collection and visualization +### Web UI (v0.6.0+) + +- **Browser Interface**: + - `tablesleuth web` launches FastAPI + Next.js at `localhost:8000` + - Full Parquet, Iceberg, Delta Lake, and GizmoSQL analysis + - Pre-built static export bundled in the wheel (`pip install tablesleuth[web]`) + - Developers: `make dev-api` + `make dev-web` for hot-reload + ### Deployment Options - **Local Development**: @@ -1315,8 +1375,8 @@ tests/ - Local or S3-based data - **AWS EC2 Production**: - - Automated EC2 setup script - - Python 3.13.9 pre-installed + - AWS CDK stack (`resources/aws-cdk/`) + - Python 3.13+ pre-installed - GizmoSQL with TLS certificates - S3 and S3 Tables access - IAM role-based authentication @@ -1338,7 +1398,18 @@ tablesleuth/ │ │ ├── config_check.py # Config validation │ │ ├── parquet.py # Parquet inspection │ │ ├── iceberg.py # Iceberg analysis -│ │ └── delta.py # Delta Lake inspection +│ │ ├── delta.py # Delta Lake inspection +│ │ └── web.py # Web UI server (v0.6.0+) +│ │ +│ ├── api/ # FastAPI web backend (v0.6.0+) +│ │ ├── __init__.py +│ │ ├── main.py # FastAPI app, CORS, static mount +│ │ └── routers/ +│ │ ├── parquet.py # Parquet REST endpoints +│ │ ├── iceberg.py # Iceberg REST endpoints +│ │ ├── delta.py # Delta REST endpoints +│ │ ├── config.py # Config REST endpoints +│ │ └── gizmosql.py # GizmoSQL/comparison endpoints │ │ │ ├── models/ # Data models │ │ ├── __init__.py @@ -1355,6 +1426,7 @@ tablesleuth/ │ │ ├── delta_forensics.py # Delta Lake forensics (v0.5.0+) │ │ ├── file_discovery.py # File discovery service │ │ ├── filesystem.py # Filesystem abstraction (S3/local) +│ │ ├── iceberg_manifest_patch.py # DuckDB compat patch (v0.6.0+) │ │ ├── iceberg_metadata_service.py # Iceberg metadata loading │ │ ├── mor_service.py # Merge-on-read analysis │ │ ├── parquet_service.py # Parquet inspection @@ -1405,8 +1477,23 @@ tablesleuth/ │ └── utils/ # Utility functions │ └── __init__.py │ +├── web-ui/ # Next.js 15 source (developers only) +│ ├── src/ +│ │ ├── app/ # Next.js app router pages +│ │ ├── components/ # React components +│ │ └── lib/ # TypeScript types and API client +│ ├── package.json +│ └── next.config.ts +│ ├── tests/ # Test suite │ ├── conftest.py # Shared fixtures +│ ├── api/ # API smoke tests (v0.6.0+) +│ │ ├── test_main.py +│ │ ├── test_parquet_router.py +│ │ ├── test_iceberg_router.py +│ │ ├── test_delta_router.py +│ │ ├── test_config_router.py +│ │ └── test_gizmosql_router.py │ ├── test_parquet_service.py │ ├── test_file_discovery.py │ ├── test_profiling_backend.py @@ -1456,7 +1543,7 @@ tablesleuth/ ### Core Dependencies -- **Python 3.13+**: Latest Python features and performance +- **Python 3.13–3.14**: Latest Python features and performance - **Textual**: Terminal UI framework - **PyArrow**: Parquet file access and Arrow data structures - **PyIceberg**: Iceberg catalog and table API @@ -1464,16 +1551,23 @@ tablesleuth/ - **boto3**: AWS SDK for S3 and Glue access - **fsspec/s3fs**: Unified filesystem interface - **click**: CLI framework -- **tomli**: TOML configuration parsing +- **pydantic**: Data validation for API models + +### Optional Dependencies (`tablesleuth[web]`, v0.6.0+) + +- **FastAPI**: REST API framework +- **uvicorn**: ASGI server +- **fastavro**: Avro serialization for Iceberg manifest patching ### Development Dependencies -- **pytest**: Testing framework +- **pytest / pytest-asyncio**: Testing framework - **pytest-cov**: Code coverage - **mypy**: Static type checking - **ruff**: Linting and formatting - **pre-commit**: Git hooks for code quality - **uv**: Fast dependency management +- **Node.js 20+ / npm**: Required to rebuild the Next.js frontend (developers only) ### External Systems @@ -1489,37 +1583,24 @@ tablesleuth/ 1. **Advanced Snapshot Analysis** - Schema evolution visualization - Partition evolution tracking - - Automated compaction recommendations - Historical performance trends -2. **Performance Optimization** - - Query result caching - - Batch performance testing - - Historical performance tracking - - Benchmark suite - -3. **Export Capabilities** +2. **Export Capabilities** - JSON export for metadata - Markdown reports - - HTML reports with charts - - Performance dashboards - CSV export for statistics -4. **Advanced Filtering** +3. **Advanced Filtering** - Partition-aware filtering - - Time-travel queries - Custom query builder UI - Saved query templates -5. **Additional Table Formats** - - Delta Lake support +4. **Additional Table Formats** - Apache Hudi support - - Unified table format interface -6. **Enhanced Profiling** +5. **Enhanced Profiling** - PySpark profiling backend - Trino profiling backend - - Custom profiling queries - Profile comparison across snapshots ## References diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 2a2f4d1..7803a5e 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -1,6 +1,6 @@ -# Table Sleuth Developer Guide +# TableSleuth Developer Guide -**Version**: 0.5.3 +**Version**: 0.6.0 ## Overview @@ -39,7 +39,7 @@ See [DEVELOPMENT_SETUP.md](../DEVELOPMENT_SETUP.md) for complete setup instructi ## Architecture Overview -Table Sleuth follows a layered architecture with clear separation of concerns: +TableSleuth follows a layered architecture with clear separation of concerns: ``` ┌─────────────────────────────────────────────────────────────┐ @@ -48,16 +48,21 @@ Table Sleuth follows a layered architecture with clear separation of concerns: │ - parquet: Parquet file analysis │ │ - iceberg: Snapshot analysis and comparison │ │ - delta: Delta Lake forensics and optimization │ +│ - web: Browser-based UI server (v0.6.0+) │ └─────────────────────────────────────────────────────────────┘ │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer (TUI) │ -│ - Views: File list, schema, row groups, snapshots │ -│ - Widgets: Notifications, loading indicators, modals │ -│ - Event handling and user interactions │ -└─────────────────────────────────────────────────────────────┘ - │ + ┌──────────┴──────────┐ + │ │ + ▼ ▼ +┌────────────────────────┐ ┌──────────────────────────────────┐ +│ Presentation (TUI) │ │ Web API Layer (v0.6.0+) │ +│ - Views, Widgets │ │ - FastAPI routers │ +│ - Event handling │ │ - parquet, iceberg, delta, │ +│ │ │ config, gizmosql endpoints │ +│ │ │ - Next.js static frontend │ +└────────────────────────┘ └──────────────────────────────────┘ + │ │ + └──────────┬──────────┘ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Service Layer │ @@ -69,6 +74,7 @@ Table Sleuth follows a layered architecture with clear separation of concerns: │ - DeltaLogFileSystem: Unified FS API (v0.5.3+) │ │ - IcebergAdapter: Catalog and table management │ │ - IcebergMetadataService: Snapshot loading │ +│ - iceberg_manifest_patch: DuckDB compat patch (v0.6.0+) │ │ - MORService: Merge-on-read analysis │ │ - ProfilingBackend: Abstract profiling interface │ │ - SnapshotTestManager: Performance test setup │ @@ -185,7 +191,19 @@ tablesleuth/ │ │ ├── config_check.py # Config validation │ │ ├── parquet.py # Parquet inspection │ │ ├── iceberg.py # Iceberg analysis -│ │ └── delta.py # Delta Lake inspection +│ │ ├── delta.py # Delta Lake inspection +│ │ └── web.py # Web UI server (v0.6.0+) +│ │ +│ ├── api/ # FastAPI REST backend (v0.6.0+) +│ │ ├── __init__.py +│ │ ├── main.py # FastAPI app, CORS, static file mount +│ │ └── routers/ +│ │ ├── __init__.py +│ │ ├── parquet.py # Parquet endpoints +│ │ ├── iceberg.py # Iceberg endpoints +│ │ ├── delta.py # Delta endpoints +│ │ ├── config.py # Config endpoints +│ │ └── gizmosql.py # GizmoSQL + comparison endpoints │ │ │ ├── models/ # Data models and types │ │ ├── __init__.py @@ -203,6 +221,7 @@ tablesleuth/ │ │ ├── file_discovery.py # File discovery service │ │ ├── filesystem.py # S3/local filesystem abstraction │ │ ├── delta_forensics.py # Delta Lake forensics (v0.5.0+) +│ │ ├── iceberg_manifest_patch.py # DuckDB compat context manager (v0.6.0+) │ │ ├── iceberg_metadata_service.py # Iceberg metadata loading │ │ ├── mor_service.py # Merge-on-read analysis │ │ ├── snapshot_test_manager.py # Snapshot registration for testing @@ -253,9 +272,23 @@ tablesleuth/ │ ├── __init__.py │ └── formatting.py # Display formatting helpers │ +├── web-ui/ # Next.js 15 source (developers only) +│ ├── src/ +│ │ ├── app/ # Next.js app router pages +│ │ ├── components/ # React components per format +│ │ └── lib/ # TypeScript types + API client +│ └── package.json +│ ├── tests/ # Test suite │ ├── __init__.py │ ├── conftest.py # Pytest fixtures +│ ├── api/ # FastAPI smoke tests (v0.6.0+) +│ │ ├── test_main.py +│ │ ├── test_parquet_router.py +│ │ ├── test_iceberg_router.py +│ │ ├── test_delta_router.py +│ │ ├── test_config_router.py +│ │ └── test_gizmosql_router.py │ ├── test_parquet_service.py │ ├── test_file_discovery.py │ ├── test_profiling_backend.py @@ -1174,7 +1207,7 @@ Fixes #456 - [ ] Performance impact considered - [ ] Security implications reviewed -## Current Status (v0.4.2) +## Current Status (v0.6.0) ### Completed Features @@ -1193,16 +1226,31 @@ Fixes #456 - Snapshot comparison - Multiple catalog types (SQL, Glue, S3 Tables) - S3 Tables ARN support - -3. **Performance Testing** ✅ + - DuckDB metadata patching for correct snapshot scans + +3. **Delta Lake Support** ✅ + - Version history navigation and time travel + - File size analysis + - Storage waste tracking + - DML forensics + - Z-Order effectiveness monitoring + - Checkpoint health assessment + - Optimization recommendations + +4. **Performance Testing** ✅ - Query performance analysis across snapshots - Predefined query templates - Custom SQL query support - - Metrics collection (time, files, bytes) + - Metadata-based scan stats (MOR breakdown) -4. **Deployment** ✅ +5. **Web UI** ✅ (v0.6.0+) + - Browser-based interface via `tablesleuth web` + - Full Parquet, Iceberg, Delta, and GizmoSQL analysis + - GizmoSQL snapshot comparison with MOR metrics + +6. **Deployment** ✅ - Local development setup - - Automated EC2 deployment + - AWS CDK EC2 deployment - GizmoSQL integration - S3 and S3 Tables access @@ -1211,35 +1259,27 @@ Fixes #456 1. **Advanced Snapshot Analysis** - Schema evolution visualization - Partition evolution tracking - - Automated compaction recommendations - Historical performance trends 2. **Export Capabilities** - JSON export for metadata - - Markdown reports - - HTML reports with charts + - Markdown/HTML reports - CSV export for statistics - - Performance dashboards - -3. **Advanced Filtering** - - Partition-aware filtering - - Time-travel queries - - Custom query builder UI - - Saved query templates - -4. **Query History** - - Save profiling queries - - Bookmark files and tables - - Recent files list - - Query performance history - -5. **Additional Table Formats** - - Delta Lake support + +3. **Additional Table Formats** - Apache Hudi support - - Unified table format interface ### Extension Points +**New CLI Commands**: +1. Create `src/tablesleuth/cli/.py` with a function matching the filename +2. The auto-loader registers it automatically — no manual registration + +**New API Endpoints**: +1. Create a router in `src/tablesleuth/api/routers/` +2. Mount it in `src/tablesleuth/api/main.py` +3. Add corresponding smoke tests in `tests/api/` + **New Table Formats**: 1. Create adapter class (similar to `IcebergAdapter`) 2. Implement file discovery method @@ -1258,12 +1298,6 @@ Fixes #456 3. Add authentication handling 4. Test with real catalog -**New Export Formats**: -1. Create exporter class -2. Implement export method -3. Add CLI option -4. Add tests - ## Resources ### Documentation diff --git a/docs/PERFORMANCE_PROFILING.md b/docs/PERFORMANCE_PROFILING.md index 366cc19..f23d165 100644 --- a/docs/PERFORMANCE_PROFILING.md +++ b/docs/PERFORMANCE_PROFILING.md @@ -1,6 +1,6 @@ # Performance Profiling and Snapshot Comparison -**Version**: 0.4.2 +**Version**: 0.6.0 ## Overview @@ -62,14 +62,24 @@ Captures metrics for a single query execution: ```python @dataclass class QueryPerformanceMetrics: - query: str # The SQL query executed - execution_time_seconds: float # Total execution time in seconds - files_scanned: int # Number of data files scanned - bytes_read: int # Total bytes read from storage - rows_returned: int # Rows returned by query - snapshot_id: int | None # Snapshot ID queried + query: str # The SQL query executed + execution_time_seconds: float # Total execution time in seconds + rows_scanned: int # Physical rows read (data + delete rows for MOR) + rows_returned: int # Net rows returned by the query + bytes_scanned: int # Total bytes read from storage + files_scanned: int # Total files read (data + delete files) + snapshot_id: int | None # Snapshot ID queried + # MOR breakdown (v0.6.0+) — populated when delete files exist + data_files_scanned: int # Data files only + delete_files_scanned: int # Delete files only + data_rows_scanned: int # Rows from data files + delete_rows_scanned: int # Delete rows (position + equality deletes) ``` +**MOR `rows_scanned` definition**: For MOR snapshots, `rows_scanned = total-records + total-position-deletes + total-equality-deletes` (physical I/O before apply). `rows_returned` is the net count after merging deletes. + +**Metadata-based scan stats**: File counts, row counts, and bytes scanned are sourced directly from Iceberg snapshot summary fields (`total-data-files`, `total-delete-files`, `total-records`, `total-files-size`, etc.) because DuckDB's `EXPLAIN ANALYZE` output is not reliably parseable over Arrow Flight SQL. As a result, scan stats reflect the **full snapshot** and do not account for partition pruning. Only `rows_returned` is accurate when predicates prune at runtime. + #### PerformanceComparison Compares performance between two snapshots: @@ -101,6 +111,22 @@ class PerformanceComparison: """Which snapshot was faster ('A', 'B', or 'Equal')""" ``` +## Usage via Web UI (v0.6.0+) + +The web UI exposes snapshot comparison through the GizmoSQL page at `/gizmosql`. + +1. Navigate to the **GizmoSQL** page +2. Select an Iceberg table and two snapshots (A and B) +3. Enter a SQL query (use `{table}` placeholder) +4. Click **Compare** — the API calls `/gizmosql/compare` +5. View side-by-side results: + - Execution time for each snapshot + - Files scanned (data vs. delete, if MOR) + - Rows scanned vs. rows returned + - Bytes scanned + +The MOR breakdown sub-rows appear automatically when at least one snapshot has delete files. + ## Usage via TUI ### Step 1: Open Iceberg Table @@ -295,12 +321,13 @@ SELECT COUNT(*) FROM snapshot_tests.table_name_snapshot_a; ### Metrics Collection -Metrics are collected from DuckDB's query execution: +Metrics are collected as follows: - **Execution Time**: Measured using Python's `time.perf_counter()` -- **Files Scanned**: Extracted from DuckDB query plan or metadata -- **Bytes Read**: Calculated from file sizes in snapshot manifest -- **Rows Returned**: Result of query execution +- **Files Scanned / Bytes Scanned / Rows Scanned**: Read directly from Iceberg snapshot summary fields before the query runs, then registered with `profiler.register_iceberg_scan_stats()`. DuckDB's `EXPLAIN ANALYZE` is not used because its output is not reliably parseable over Arrow Flight SQL transport. +- **Rows Returned**: Result count from the actual `SELECT` query execution (accurate; reflects predicate/partition pruning) + +**Important limitation**: Because scan stats come from snapshot metadata, they represent the full snapshot and do not reflect partition pruning. A query with a highly selective predicate may return far fewer `rows_returned` than `rows_scanned`. ### Catalog Requirements @@ -574,7 +601,8 @@ Potential improvements for future versions: - [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture and design - [USER_GUIDE.md](USER_GUIDE.md) - Complete user documentation - [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md) - Developer guide and API reference -- [EC2_DEPLOYMENT_GUIDE.md](EC2_DEPLOYMENT_GUIDE.md) - AWS EC2 deployment +- [GIZMOSQL_DEPLOYMENT_GUIDE.md](GIZMOSQL_DEPLOYMENT_GUIDE.md) - GizmoSQL server setup - [Snapshot Performance Analyzer](../src/tablesleuth/services/snapshot_performance_analyzer.py) - Source code - [Snapshot Test Manager](../src/tablesleuth/services/snapshot_test_manager.py) - Source code - [Performance Models](../src/tablesleuth/models/performance.py) - Data models +- [GizmoSQL Router](../src/tablesleuth/api/routers/gizmosql.py) - Web API comparison endpoint (v0.6.0+) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 76da7e0..475f42f 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -1,32 +1,51 @@ -# Table Sleuth User Guide +# TableSleuth User Guide ## Overview -Table Sleuth is a comprehensive tool for analyzing Parquet files, Apache Iceberg tables, and Delta Lake tables with a powerful terminal user interface (TUI). It provides: +TableSleuth is a comprehensive tool for analyzing Parquet files, Apache Iceberg tables, and Delta Lake tables. It provides two interfaces: + +- **Terminal TUI** — keyboard-driven Textual interface (`tablesleuth parquet`, `tablesleuth iceberg`, `tablesleuth delta`) +- **Browser Web UI** — FastAPI + Next.js interface (`tablesleuth web`, v0.6.0+) + +Feature highlights: - **Parquet Inspection**: Deep metadata analysis, schema viewing, row group inspection - **Column Profiling**: Statistical analysis via local GizmoSQL - **Iceberg Support**: Table browsing, snapshot navigation, and comparison - **Delta Lake Support**: Version history, forensic analysis, and optimization recommendations - **Performance Testing**: Measure merge-on-read overhead across snapshots +- **Snapshot Comparison API**: Side-by-side GizmoSQL query metrics with MOR breakdown (v0.6.0+) ## Installation ### Prerequisites -- Python 3.12 or higher +- Python 3.13 or higher - uv for dependency management (recommended) +### Install from PyPI + +```bash +# TUI only +pip install tablesleuth + +# TUI + browser web UI (v0.6.0+) +pip install tablesleuth[web] +``` + ### Install from Source ```bash # Clone the repository -git clone +git clone https://github.com/jamesbconner/TableSleuth.git cd TableSleuth -# Install dependencies with uv +# Install TUI dependencies uv sync +# Install with web UI support +uv sync --extra web + # Activate virtual environment source .venv/bin/activate # On macOS/Linux .venv\Scripts\activate # On Windows @@ -42,11 +61,11 @@ tablesleuth --version ### Configuration File -Create a `tablesleuth.toml` file in your project directory or `~/.config/tablesleuth.toml`: +Create a `tablesleuth.toml` file in your project directory or `~/.tablesleuth.toml`: ```toml [catalog] -default = "local" +default = "glue" # change to your catalog name [gizmosql] uri = "grpc+tls://localhost:31337" @@ -64,6 +83,10 @@ export TABLESLEUTH_CATALOG_NAME="local" export TABLESLEUTH_GIZMO_URI="grpc+tls://localhost:31337" export TABLESLEUTH_GIZMO_USERNAME="gizmosql_username" export TABLESLEUTH_GIZMO_PASSWORD="gizmosql_password" + +# Web UI (v0.6.0+) +export TABLESLEUTH_WEB_UI_DIR="/path/to/custom/web" # override static files +export TABLESLEUTH_CORS_ORIGINS="http://localhost:3000,http://myhost:8080" ``` ### PyIceberg Configuration (Required for Iceberg Features) @@ -91,6 +114,10 @@ tablesleuth parquet --help # Show version tablesleuth --version + +# Launch browser web UI (requires tablesleuth[web]) +tablesleuth web +tablesleuth web --host 0.0.0.0 --port 9000 ``` ### Inspect a Single File @@ -287,23 +314,25 @@ tablesleuth parquet data/file.parquet --verbose GizmoSQL is a DuckDB instance exposed via Arrow Flight SQL that enables fast column profiling and Iceberg performance testing. It runs as a local process with direct filesystem access. +See [GIZMOSQL_DEPLOYMENT_GUIDE.md](GIZMOSQL_DEPLOYMENT_GUIDE.md) for full installation and configuration. + ### Installation **macOS (ARM64):** ```bash -curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.10/gizmosql_cli_macos_arm64.zip \ +curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.13/gizmosql_cli_macos_arm64.zip \ | sudo unzip -o -d /usr/local/bin - ``` **macOS (Intel):** ```bash -curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.10/gizmosql_cli_macos_amd64.zip \ +curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.13/gizmosql_cli_macos_amd64.zip \ | sudo unzip -o -d /usr/local/bin - ``` **Linux:** ```bash -curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.10/gizmosql_cli_linux_amd64.zip \ +curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.13/gizmosql_cli_linux_amd64.zip \ | sudo unzip -o -d /usr/local/bin - ``` @@ -659,8 +688,8 @@ Delta Lake forensics provides deep insights into table health: ## Current Limitations - **Read-only**: No write operations (safe for production) -- **No data preview**: Metadata analysis only (no actual data displayed) -- **Profiling requires GizmoSQL**: Column profiling needs local GizmoSQL server +- **Profiling requires GizmoSQL**: Column profiling needs a running GizmoSQL server +- **Web UI scan stats**: File/row/byte counts in snapshot comparison reflect the full snapshot (not partition-pruned). Only `rows_returned` is accurate when predicates prune at runtime. - **Iceberg features**: - ✅ Snapshot navigation and comparison - ✅ Performance testing across snapshots @@ -933,8 +962,8 @@ if info.file_size_bytes > 1_000_000_000: ## Next Steps - See [QUICKSTART.md](../QUICKSTART.md) for quick examples +- See [TABLESLEUTH_SETUP.md](../TABLESLEUTH_SETUP.md) for full setup including GizmoSQL and AWS - See [PERFORMANCE_PROFILING.md](PERFORMANCE_PROFILING.md) for performance analysis - See [CHANGELOG.md](../CHANGELOG.md) for version history - See [ARCHITECTURE.md](ARCHITECTURE.md) for system architecture and design - See [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md) for developer documentation -- See `.kiro/specs/` for detailed feature specifications diff --git a/pyproject.toml b/pyproject.toml index 19b52df..9e1991e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "tablesleuth" -version = "0.5.3" +version = "0.6.0" description = "TableSleuth - a Textual TUI for Open Table Format forensics (Iceberg, Delta Lake) with data profiling." readme = "README.md" requires-python = ">=3.13,<3.15" @@ -59,26 +59,34 @@ classifiers = [ # Table format support dependencies = [ - "pyiceberg[s3fs,sql-sqlite,glue]>=0.9.1", - "deltalake>=0.22.0", + "pyiceberg[s3fs,sql-sqlite,glue]>=0.11.0", + "deltalake>=1.4.2", "textual>=0.86.2", - "pyarrow>=22.0.0", - "pandas>=2.3.0", - "adbc-driver-flightsql>=1.7.0", - "click>=8.1.0", - "pydantic>=2.11.0", + "pyarrow>=23.0.0", + "pandas>=3.0.1", + "adbc-driver-flightsql>=1.10.0", + "click>=8.3.1", + "pydantic>=2.12.5", "pyyaml>=6.0.0", - "rich>=13.0.0", - "sqlalchemy>=2.0.0", - "duckdb>=1.1.0", + "rich>=14.3.3", + "sqlalchemy>=2.0.46", + "duckdb>=1.4.4", "boto3>=1.35.0", - "pip>=24.0", - "uv>=0.5.0", + "pip>=25.0", + "uv>=0.10.4", ] [project.optional-dependencies] +web = [ + "fastapi>=0.131.0", + "uvicorn[standard]>=0.32.0", + "python-multipart>=0.0.12", + "fastavro>=1.9.0", +] + dev = [ "pytest>=8.4.2,<9.0.0", + "httpx>=0.27.0", "pytest-asyncio>=0.26.0,<1.0.0", "pytest-cov>=6.0.0,<7.0.0", "hypothesis>=6.100.0,<7.0.0", @@ -106,9 +114,13 @@ Changelog = "https://github.com/jamesbconner/TableSleuth/blob/main/CHANGELOG.md" # Hatchling configuration for src layout [tool.hatch.build.targets.wheel] packages = ["src/tablesleuth"] +# Exclude web/ from VCS-based discovery so force-include is the sole +# mechanism that adds it. This prevents duplicate entries for files +# (e.g. index.html) that are both git-tracked and listed in force-include. +exclude = ["src/tablesleuth/web/**"] -# Hatchling configuration for src layout -# Removed to let hatchling auto determine the package in src +[tool.hatch.build.targets.wheel.force-include] +"src/tablesleuth/web" = "tablesleuth/web" # ----------------------- # Ruff configuration @@ -186,8 +198,11 @@ module = [ "boto3.*", "botocore.*", "pyarrow.*", + "fastavro.*", "fsspec.*", "s3fs.*", + "fastapi.*", + "uvicorn.*", ] ignore_missing_imports = true diff --git a/resources/aws-cdk/tablesleuth_cdk/tablesleuth_stack.py b/resources/aws-cdk/tablesleuth_cdk/tablesleuth_stack.py index 7eb4313..b739382 100644 --- a/resources/aws-cdk/tablesleuth_cdk/tablesleuth_stack.py +++ b/resources/aws-cdk/tablesleuth_cdk/tablesleuth_stack.py @@ -461,7 +461,7 @@ def _get_user_data(self) -> str: echo "Installing GizmoSQL CLI..." cd /tmp -wget https://github.com/gizmodata/gizmosql/releases/download/v1.12.13/gizmosql_cli_linux_amd64.zip +wget https://github.com/gizmodata/gizmosql/releases/download/v1.18.4/gizmosql_cli_linux_amd64.zip unzip -o gizmosql_cli_linux_amd64.zip -d /usr/local/bin/ chmod +x /usr/local/bin/gizmosql* diff --git a/src/tablesleuth/__init__.py b/src/tablesleuth/__init__.py index 5d0563b..f503ce3 100644 --- a/src/tablesleuth/__init__.py +++ b/src/tablesleuth/__init__.py @@ -1,4 +1,4 @@ """TableSleuth - open table format forensics.""" __all__ = ["__version__"] -__version__ = "0.5.3" +__version__ = "0.6.0" diff --git a/src/tablesleuth/api/__init__.py b/src/tablesleuth/api/__init__.py new file mode 100644 index 0000000..6967eff --- /dev/null +++ b/src/tablesleuth/api/__init__.py @@ -0,0 +1 @@ +"""TableSleuth FastAPI web backend.""" diff --git a/src/tablesleuth/api/main.py b/src/tablesleuth/api/main.py new file mode 100644 index 0000000..53092f7 --- /dev/null +++ b/src/tablesleuth/api/main.py @@ -0,0 +1,113 @@ +"""TableSleuth FastAPI application.""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path +from typing import Any + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles + +from tablesleuth import __version__ +from tablesleuth.api.routers import config, delta, gizmosql, iceberg, parquet +from tablesleuth.utils.web_utils import resolve_web_dir + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Application factory +# --------------------------------------------------------------------------- + +app = FastAPI( + title="TableSleuth", + description="REST API for Parquet, Iceberg, and Delta Lake forensic analysis.", + version=__version__, + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json", +) + +# --------------------------------------------------------------------------- +# CORS +# --------------------------------------------------------------------------- + +_cors_origins_raw = os.getenv("TABLESLEUTH_CORS_ORIGINS", "http://localhost:3000") +_cors_origins = [o.strip() for o in _cors_origins_raw.split(",") if o.strip()] + +app.add_middleware( + CORSMiddleware, + allow_origins=_cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --------------------------------------------------------------------------- +# Exception handlers +# --------------------------------------------------------------------------- + + +@app.exception_handler(FileNotFoundError) +async def file_not_found_handler(request: Request, exc: FileNotFoundError) -> JSONResponse: + """Return 404 for file-not-found errors.""" + return JSONResponse(status_code=404, content={"detail": str(exc)}) + + +@app.exception_handler(ValueError) +async def value_error_handler(request: Request, exc: ValueError) -> JSONResponse: + """Return 422 for value errors (bad input).""" + return JSONResponse(status_code=422, content={"detail": str(exc)}) + + +@app.exception_handler(Exception) +async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """Return 500 for unexpected errors.""" + logger.exception("Unhandled exception for %s %s", request.method, request.url.path) + return JSONResponse(status_code=500, content={"detail": "Internal server error"}) + + +# --------------------------------------------------------------------------- +# Routers (all under /api prefix) +# --------------------------------------------------------------------------- + +app.include_router(parquet.router, prefix="/api") +app.include_router(iceberg.router, prefix="/api") +app.include_router(delta.router, prefix="/api") +app.include_router(config.router, prefix="/api") +app.include_router(gizmosql.router, prefix="/api") + + +# --------------------------------------------------------------------------- +# Health endpoint +# --------------------------------------------------------------------------- + + +@app.get("/api/health", tags=["health"]) +def health() -> dict[str, Any]: + """Return server health status.""" + return {"status": "ok", "version": __version__} + + +# --------------------------------------------------------------------------- +# Static file serving (Next.js static export) — mounted LAST +# --------------------------------------------------------------------------- + +_web_dir = resolve_web_dir() + +if _web_dir: + logger.info("Serving web UI from: %s", _web_dir) + # Next.js static export with trailingSlash:true generates a dedicated + # index.html per route (e.g. settings/index.html, parquet/index.html). + # StaticFiles(html=True) serves those automatically — no custom SPA + # fallback needed, and adding one would intercept /_next/static/* asset + # requests before they reach the file server. + app.mount("/", StaticFiles(directory=str(_web_dir), html=True), name="static") +else: + logger.warning( + "Web UI directory not found. " + "Run 'make build-release' or set TABLESLEUTH_WEB_UI_DIR to serve the frontend." + ) diff --git a/src/tablesleuth/api/routers/__init__.py b/src/tablesleuth/api/routers/__init__.py new file mode 100644 index 0000000..7ccc298 --- /dev/null +++ b/src/tablesleuth/api/routers/__init__.py @@ -0,0 +1 @@ +"""TableSleuth API routers.""" diff --git a/src/tablesleuth/api/routers/config.py b/src/tablesleuth/api/routers/config.py new file mode 100644 index 0000000..42e153a --- /dev/null +++ b/src/tablesleuth/api/routers/config.py @@ -0,0 +1,255 @@ +"""Config API router.""" + +from __future__ import annotations + +import dataclasses +import logging +import os +from pathlib import Path +from typing import Any + +import yaml +from fastapi import APIRouter, HTTPException, UploadFile +from pydantic import BaseModel + +from tablesleuth.config import ( + AppConfig, + CatalogConfig, + GizmoConfig, + get_config_file_path, + load_config, +) + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/config", tags=["config"]) + +_PYICEBERG_HOME = Path(os.getenv("PYICEBERG_HOME", Path.home())) +_PYICEBERG_YAML = _PYICEBERG_HOME / ".pyiceberg.yaml" + + +def _config_to_dict(cfg: AppConfig) -> dict[str, Any]: + """Convert AppConfig to a serializable dict.""" + return { + "catalog": dataclasses.asdict(cfg.catalog), + "gizmosql": dataclasses.asdict(cfg.gizmosql), + } + + +class ConfigUpdate(BaseModel): + """Request body for PUT /config/.""" + + catalog: dict[str, Any] | None = None + gizmosql: dict[str, Any] | None = None + + +@router.get("/") +def get_config() -> dict[str, Any]: + """Return the current AppConfig as JSON. + + Returns: + Current configuration as a dictionary. + """ + try: + cfg = load_config() + return _config_to_dict(cfg) + except Exception as exc: + logger.exception("Error loading config") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.put("/") +def save_config(update: ConfigUpdate) -> dict[str, Any]: + """Save updated configuration to tablesleuth.toml. + + Writes to the local (cwd) config file. + + Args: + update: Partial config with catalog and/or gizmosql sections. + + Returns: + Saved configuration as a dictionary. + """ + try: + # Load current config + cfg = load_config() + + # Apply updates + if update.catalog: + cfg = AppConfig( + catalog=CatalogConfig(default=update.catalog.get("default", cfg.catalog.default)), + gizmosql=cfg.gizmosql, + ) + if update.gizmosql: + g = update.gizmosql + cfg = AppConfig( + catalog=cfg.catalog, + gizmosql=GizmoConfig( + uri=g.get("uri", cfg.gizmosql.uri), + username=g.get("username", cfg.gizmosql.username), + password=g.get("password", cfg.gizmosql.password), + tls_skip_verify=g.get("tls_skip_verify", cfg.gizmosql.tls_skip_verify), + ), + ) + + # Write to local config file + config_path = Path.cwd() / "tablesleuth.toml" + _write_toml(config_path, cfg) + + return {"saved": True, "path": str(config_path), "config": _config_to_dict(cfg)} + except Exception as exc: + logger.exception("Error saving config") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/upload") +async def upload_config(file: UploadFile) -> dict[str, Any]: + """Upload a tablesleuth.toml file. + + Args: + file: Uploaded TOML config file. + + Returns: + Parsed configuration from uploaded file. + """ + try: + import tomllib + + content = await file.read() + raw = tomllib.loads(content.decode("utf-8")) + config_path = Path.cwd() / "tablesleuth.toml" + config_path.write_bytes(content) + return {"saved": True, "path": str(config_path), "raw": raw} + except Exception as exc: + logger.exception("Error uploading config") + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +@router.get("/pyiceberg") +def get_pyiceberg_config() -> dict[str, Any]: + """Return the .pyiceberg.yaml contents. + + Returns: + PyIceberg YAML as a dictionary, or empty dict if not found. + """ + try: + if _PYICEBERG_YAML.exists(): + with _PYICEBERG_YAML.open() as f: + data = yaml.safe_load(f) or {} + return {"exists": True, "path": str(_PYICEBERG_YAML), "config": data} + return {"exists": False, "path": str(_PYICEBERG_YAML), "config": {}} + except Exception as exc: + logger.exception("Error reading .pyiceberg.yaml") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/pyiceberg/upload") +async def upload_pyiceberg_config(file: UploadFile) -> dict[str, Any]: + """Upload a .pyiceberg.yaml file. + + Args: + file: Uploaded YAML file. + + Returns: + Confirmation with path written. + """ + try: + content = await file.read() + # Validate YAML before saving + parsed = yaml.safe_load(content.decode("utf-8")) + if parsed is None: + parsed = {} + _PYICEBERG_YAML.parent.mkdir(parents=True, exist_ok=True) + _PYICEBERG_YAML.write_bytes(content) + return {"saved": True, "path": str(_PYICEBERG_YAML), "config": parsed} + except yaml.YAMLError as exc: + raise HTTPException(status_code=422, detail=f"Invalid YAML: {exc}") from exc + except Exception as exc: + logger.exception("Error uploading .pyiceberg.yaml") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.put("/pyiceberg") +def save_pyiceberg_config(config: dict[str, Any]) -> dict[str, Any]: + """Save updated .pyiceberg.yaml. + + Args: + config: Full PyIceberg config dict to write. + + Returns: + Confirmation with path written. + """ + try: + _PYICEBERG_YAML.parent.mkdir(parents=True, exist_ok=True) + with _PYICEBERG_YAML.open("w") as f: + yaml.dump(config, f, default_flow_style=False) + return {"saved": True, "path": str(_PYICEBERG_YAML)} + except Exception as exc: + logger.exception("Error saving .pyiceberg.yaml") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.get("/status") +def get_config_status() -> dict[str, Any]: + """Return active config file path and env var override status. + + Returns: + Dictionary with config file path, env var overrides, and pyiceberg status. + """ + try: + config_path = get_config_file_path() + env_overrides = { + k: bool(os.getenv(k)) + for k in [ + "TABLESLEUTH_CONFIG", + "TABLESLEUTH_CATALOG_NAME", + "TABLESLEUTH_GIZMO_URI", + "TABLESLEUTH_GIZMO_USERNAME", + "TABLESLEUTH_GIZMO_PASSWORD", + "TABLESLEUTH_CORS_ORIGINS", + ] + } + return { + "config_file": str(config_path) if config_path else None, + "env_overrides": env_overrides, + "pyiceberg_yaml_exists": _PYICEBERG_YAML.exists(), + "pyiceberg_yaml_path": str(_PYICEBERG_YAML), + } + except Exception as exc: + logger.exception("Error getting config status") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +def _write_toml(path: Path, cfg: AppConfig) -> None: + """Write AppConfig to a TOML file. + + Args: + path: Destination path. + cfg: AppConfig to serialize. + """ + + def _escape_toml_string(value: str) -> str: + """Escape special characters for TOML basic strings. + + Args: + value: String to escape. + + Returns: + Escaped string safe for TOML basic (double-quoted) strings. + """ + # Escape backslash first, then double quote + return value.replace("\\", "\\\\").replace('"', '\\"') + + lines = [ + "[catalog]", + f'default = "{_escape_toml_string(cfg.catalog.default)}"' + if cfg.catalog.default + else "# default = ", + "", + "[gizmosql]", + f'uri = "{_escape_toml_string(cfg.gizmosql.uri)}"', + f'username = "{_escape_toml_string(cfg.gizmosql.username)}"', + f'password = "{_escape_toml_string(cfg.gizmosql.password)}"', + f"tls_skip_verify = {str(cfg.gizmosql.tls_skip_verify).lower()}", + ] + path.write_text("\n".join(lines) + "\n", encoding="utf-8") diff --git a/src/tablesleuth/api/routers/delta.py b/src/tablesleuth/api/routers/delta.py new file mode 100644 index 0000000..7e941ea --- /dev/null +++ b/src/tablesleuth/api/routers/delta.py @@ -0,0 +1,182 @@ +"""Delta Lake API router.""" + +from __future__ import annotations + +import logging +from typing import Any + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from tablesleuth.api.serialization import to_dict +from tablesleuth.services.delta_forensics import DeltaForensics +from tablesleuth.services.formats.delta import DeltaAdapter + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/delta", tags=["delta"]) + + +class LoadRequest(BaseModel): + """Request body for /delta/load.""" + + path: str + version: int | None = None + storage_options: dict[str, str] | None = None + + +@router.post("/load") +def load_table(req: LoadRequest) -> dict[str, Any]: + """Load a Delta table and return current snapshot info. + + Args: + req: Request with table path, optional version, and storage options. + + Returns: + SnapshotInfo as a dictionary. + """ + try: + adapter = DeltaAdapter(storage_options=req.storage_options) + handle = adapter.open_table(req.path) + snapshot = adapter.load_snapshot(handle, req.version) + result = to_dict(snapshot) + assert isinstance(result, dict) + # Add native table version info + result["current_version"] = handle.native.version() + return result + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error loading Delta table: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/versions") +def list_versions(req: LoadRequest) -> dict[str, Any]: + """List all versions of a Delta table. + + Args: + req: Request with table path and optional storage options. + + Returns: + Dictionary with list of version snapshots. + """ + try: + adapter = DeltaAdapter(storage_options=req.storage_options) + handle = adapter.open_table(req.path) + snapshots = adapter.list_snapshots(handle) + return { + "versions": [to_dict(s) for s in snapshots], + "count": len(snapshots), + "current_version": handle.native.version(), + } + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error listing Delta versions: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/forensics") +def get_forensics(req: LoadRequest) -> dict[str, Any]: + """Run storage waste and file analysis on a Delta table. + + Args: + req: Request with table path and optional storage options. + + Returns: + Dictionary with forensics analysis results. + """ + try: + adapter = DeltaAdapter(storage_options=req.storage_options) + handle = adapter.open_table(req.path) + dt = handle.native + snapshot = adapter.load_snapshot(handle, req.version) + + file_sizes = DeltaForensics.analyze_file_sizes(snapshot) + storage_waste = DeltaForensics.analyze_storage_waste( + dt, dt.version(), storage_options=req.storage_options + ) + recommendations = DeltaForensics.generate_recommendations( + dt, snapshot, storage_options=req.storage_options + ) + checkpoint_health = DeltaForensics.analyze_checkpoint_health( + dt, storage_options=req.storage_options + ) + + return { + "path": req.path, + "current_version": dt.version(), + "file_size_analysis": file_sizes, + "storage_waste": storage_waste, + "checkpoint_health": checkpoint_health, + "recommendations": recommendations, + } + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error running Delta forensics: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/schema") +def get_schema(req: LoadRequest) -> dict[str, Any]: + """Get schema for a Delta table at a specific version. + + Args: + req: Request with table path and optional version. + + Returns: + Dictionary with list of schema fields. + """ + try: + from deltalake import DeltaTable as _DT + + kwargs: dict[str, Any] = {} + if req.version is not None: + kwargs["version"] = req.version + if req.storage_options: + kwargs["storage_options"] = req.storage_options + dt = _DT(req.path, **kwargs) + schema = dt.schema() + fields = [ + {"name": f.name, "type": str(f.type), "nullable": f.nullable} for f in schema.fields + ] + return {"fields": fields, "count": len(fields)} + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error getting Delta schema: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/schema-evolution") +def get_schema_evolution(req: LoadRequest) -> dict[str, Any]: + """Get schema evolution history for a Delta table. + + Args: + req: Request with table path and optional storage options. + + Returns: + Dictionary with list of schema changes per version. + """ + try: + adapter = DeltaAdapter(storage_options=req.storage_options) + handle = adapter.open_table(req.path) + evolution = adapter.get_schema_evolution(handle) + return {"evolution": evolution, "count": len(evolution)} + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error getting Delta schema evolution: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/tablesleuth/api/routers/gizmosql.py b/src/tablesleuth/api/routers/gizmosql.py new file mode 100644 index 0000000..0249c76 --- /dev/null +++ b/src/tablesleuth/api/routers/gizmosql.py @@ -0,0 +1,368 @@ +"""GizmoSQL API router.""" + +from __future__ import annotations + +import logging +import time +from pathlib import Path +from typing import Any, Literal + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from tablesleuth.config import load_config +from tablesleuth.models.iceberg import PerformanceComparison, QueryPerformanceMetrics + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/gizmosql", tags=["gizmosql"]) + + +class QueryRequest(BaseModel): + """Request body for /gizmosql/query.""" + + sql: str + + +class ProfileRequest(BaseModel): + """Request body for /gizmosql/profile.""" + + table_ref: str + metadata_location: str | None = None + snapshot_id: int | None = None + columns: list[str] | None = None + + +def _get_profiler() -> Any: + """Instantiate a GizmoDuckDbProfiler from current config. + + Returns: + GizmoDuckDbProfiler instance. + + Raises: + HTTPException: If GizmoSQL is not configured or not importable. + """ + try: + from tablesleuth.services.profiling.gizmo_duckdb import GizmoDuckDbProfiler + except ImportError as exc: + raise HTTPException( + status_code=503, + detail="GizmoSQL profiling backend not available. Install adbc-driver-flightsql.", + ) from exc + + cfg = load_config() + return GizmoDuckDbProfiler( + uri=cfg.gizmosql.uri, + username=cfg.gizmosql.username, + password=cfg.gizmosql.password, + tls_skip_verify=cfg.gizmosql.tls_skip_verify, + ) + + +@router.get("/status") +def get_status() -> dict[str, Any]: + """Test GizmoSQL connection and return status. + + Returns: + Dictionary with connected flag, version (if connected), or error message. + """ + try: + profiler = _get_profiler() + conn = profiler._connect() + with conn, conn.cursor() as cur: + cur.execute("SELECT version()") + row = cur.fetchone() + version = row[0] if row else "unknown" + return {"connected": True, "version": version} + except HTTPException: + raise + except Exception as exc: + return {"connected": False, "error": str(exc)} + + +@router.post("/query") +def execute_query(req: QueryRequest) -> dict[str, Any]: + """Execute a SQL query against GizmoSQL. + + Args: + req: Request with the SQL statement to execute. + + Returns: + Dictionary with columns, rows, and elapsed_ms. + """ + if not req.sql or not req.sql.strip(): + raise HTTPException(status_code=422, detail="SQL query cannot be empty") + + try: + profiler = _get_profiler() + start = time.perf_counter() + conn = profiler._connect() + with conn, conn.cursor() as cur: + cur.execute(req.sql) + rows = cur.fetchall() + columns = [desc[0] for desc in cur.description] if cur.description else [] + elapsed_ms = (time.perf_counter() - start) * 1000 + + # Serialize rows (convert any non-JSON-safe types to str) + serialized_rows = [] + for row in rows: + serialized_rows.append( + [ + str(v) if v is not None and not isinstance(v, int | float | bool | str) else v + for v in row + ] + ) + + return { + "columns": columns, + "rows": serialized_rows, + "row_count": len(rows), + "elapsed_ms": round(elapsed_ms, 2), + } + except HTTPException: + raise + except Exception as exc: + logger.exception("GizmoSQL query error") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/profile") +def profile_table(req: ProfileRequest) -> dict[str, Any]: + """Profile columns of an Iceberg table via GizmoSQL. + + Args: + req: Request with table_ref, optional metadata_location, snapshot_id, columns. + + Returns: + Dictionary mapping column names to ColumnProfile dicts. + """ + try: + profiler = _get_profiler() + + if req.metadata_location: + # Use a fixed internal name so profiling always uses the registered + # Iceberg table. profile_columns/profile_single_column resolve this + # via _replace_iceberg_tables; the client's table_ref may be an + # invalid identifier (e.g. "catalog.db.table") for the profiler. + _PROFILE_ICEBERG_VIEW = "profile_iceberg_view" + profiler.register_iceberg_table_with_snapshot( + _PROFILE_ICEBERG_VIEW, req.metadata_location, req.snapshot_id + ) + view_name = _PROFILE_ICEBERG_VIEW + else: + view_name = req.table_ref + + if req.columns: + profiles = profiler.profile_columns(view_name, req.columns) + else: + raise HTTPException( + status_code=422, detail="columns list is required for profile endpoint" + ) + + return {col: prof.model_dump() for col, prof in profiles.items()} + except HTTPException: + raise + except Exception as exc: + logger.exception("GizmoSQL profile error") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +class CompareRequest(BaseModel): + """Request body for /gizmosql/compare.""" + + format: Literal["iceberg", "delta"] + # Iceberg table reference (one of metadata_path or catalog_name+table_identifier) + metadata_path: str | None = None + catalog_name: str | None = None + table_identifier: str | None = None + # Delta table reference + path: str | None = None + storage_options: dict[str, str] | None = None + # Snapshot / version IDs — strings to handle int64 Iceberg snapshot IDs safely + id_a: str + id_b: str + # Query template; {table} is replaced at runtime + query: str + + +def _serialize_metrics(m: QueryPerformanceMetrics) -> dict[str, Any]: + return { + "execution_time_ms": m.execution_time_ms, + "files_scanned": m.files_scanned, + "bytes_scanned": m.bytes_scanned, + "rows_scanned": m.rows_scanned, + "rows_returned": m.rows_returned, + "memory_peak_mb": m.memory_peak_mb, + "scan_efficiency": m.scan_efficiency, + "data_files_scanned": m.data_files_scanned, + "delete_files_scanned": m.delete_files_scanned, + "data_rows_scanned": m.data_rows_scanned, + "delete_rows_scanned": m.delete_rows_scanned, + } + + +def _serialize_comparison(c: PerformanceComparison) -> dict[str, Any]: + return { + "query": c.query, + "table_a_name": c.table_a_name, + "table_b_name": c.table_b_name, + "metrics_a": _serialize_metrics(c.metrics_a), + "metrics_b": _serialize_metrics(c.metrics_b), + "execution_time_delta_pct": c.execution_time_delta_pct, + "files_scanned_delta_pct": c.files_scanned_delta_pct, + "analysis": c.analysis, + } + + +def _iceberg_snapshot_scan_stats(native_table: Any, snapshot_id: int) -> dict[str, int]: + """Return scan statistics derived from a PyIceberg snapshot summary. + + Keys returned: + - ``files_scanned``: data files + delete files + - ``bytes_scanned``: total file sizes from ``total-files-size`` + - ``rows_scanned``: data-file records + delete records (physical rows DuckDB reads) + - ``data_files_scanned``, ``delete_files_scanned``: per-type file counts + - ``data_rows_scanned``, ``delete_rows_scanned``: per-type row counts + + Returns a dict of zeros on any error so callers degrade gracefully. + + Args: + native_table: PyIceberg ``Table`` instance. + snapshot_id: Iceberg snapshot ID to look up. + + Returns: + Dict mapping stat name to integer value. + """ + _zero: dict[str, int] = { + "files_scanned": 0, + "bytes_scanned": 0, + "rows_scanned": 0, + "data_files_scanned": 0, + "delete_files_scanned": 0, + "data_rows_scanned": 0, + "delete_rows_scanned": 0, + } + try: + snap = native_table.snapshot_by_id(snapshot_id) + if snap is None: + return _zero + summary = snap.summary + + def _get_int(key: str) -> int: + try: + val = summary.get(key) + except (AttributeError, TypeError): + val = None + if val is None: + try: + val = summary.additional_properties.get(key) + except AttributeError: + pass + try: + return int(val or 0) + except (TypeError, ValueError): + return 0 + + data_files = _get_int("total-data-files") + delete_files = _get_int("total-delete-files") + total_records = _get_int("total-records") + pos_deletes = _get_int("total-position-deletes") + eq_deletes = _get_int("total-equality-deletes") + total_bytes = _get_int("total-files-size") + + return { + "files_scanned": data_files + delete_files, + "bytes_scanned": total_bytes, + "rows_scanned": total_records + pos_deletes + eq_deletes, + "data_files_scanned": data_files, + "delete_files_scanned": delete_files, + "data_rows_scanned": total_records, + "delete_rows_scanned": pos_deletes + eq_deletes, + } + except Exception: + logger.debug( + "Could not get snapshot scan stats for snapshot %d", snapshot_id, exc_info=True + ) + return _zero + + +@router.post("/compare") +def compare_performance(req: CompareRequest) -> dict[str, Any]: + """Compare query performance between two Iceberg snapshots or Delta versions. + + Registers both table versions with the GizmoDuckDbProfiler, executes the + query template against each, and returns side-by-side metrics with analysis. + + Args: + req: Request with format, table location, two IDs, and query template. + + Returns: + Serialized PerformanceComparison with metrics and analysis text. + """ + try: + from tablesleuth.services.snapshot_performance_analyzer import SnapshotPerformanceAnalyzer + + profiler = _get_profiler() + + if req.format == "iceberg": + from tablesleuth.services.iceberg_manifest_patch import patched_iceberg_metadata + from tablesleuth.services.iceberg_metadata_service import IcebergMetadataService + + service = IcebergMetadataService() + table = service.load_table( + metadata_path=req.metadata_path, + catalog_name=req.catalog_name, + table_identifier=req.table_identifier, + ) + native = table.native_table + id_a, id_b = int(req.id_a), int(req.id_b) + + # Use iceberg_scan() so that delete files (positional / equality) are + # applied during the query — this is the whole point of MOR comparison. + # DuckDB's iceberg extension rejects delete-file entries whose file_format + # is stored as 'PARQUET' (uppercase) in the manifest avro files. + # patched_iceberg_metadata() creates a lightweight local copy of the + # metadata chain with that string lowercased, redirecting only the affected + # delete manifests; all data files remain at their original S3/local paths. + # Derive file/row counts from snapshot summaries for metrics fallback. + stats_a = _iceberg_snapshot_scan_stats(native, id_a) + stats_b = _iceberg_snapshot_scan_stats(native, id_b) + + with ( + patched_iceberg_metadata(native, id_a) as meta_a, + patched_iceberg_metadata(native, id_b) as meta_b, + ): + profiler.register_iceberg_table_with_snapshot("snap_a", meta_a, id_a) + profiler.register_iceberg_table_with_snapshot("snap_b", meta_b, id_b) + profiler.register_iceberg_scan_stats("snap_a", **stats_a) + profiler.register_iceberg_scan_stats("snap_b", **stats_b) + + analyzer = SnapshotPerformanceAnalyzer(profiler) + comparison = analyzer.compare_query_performance("snap_a", "snap_b", req.query) + return _serialize_comparison(comparison) + + elif req.format == "delta": + if not req.path: + raise ValueError("path is required for delta format") + profiler.register_delta_table_with_version( + "ver_a", req.path, int(req.id_a), storage_options=req.storage_options + ) + profiler.register_delta_table_with_version( + "ver_b", req.path, int(req.id_b), storage_options=req.storage_options + ) + label_a, label_b = "ver_a", "ver_b" + + else: + raise ValueError(f"Unsupported format: {req.format}") + + analyzer = SnapshotPerformanceAnalyzer(profiler) + comparison = analyzer.compare_query_performance(label_a, label_b, req.query) + return _serialize_comparison(comparison) + + except HTTPException: + raise + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("GizmoSQL comparison error") + raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/tablesleuth/api/routers/iceberg.py b/src/tablesleuth/api/routers/iceberg.py new file mode 100644 index 0000000..b5bda57 --- /dev/null +++ b/src/tablesleuth/api/routers/iceberg.py @@ -0,0 +1,266 @@ +"""Iceberg API router.""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path +from typing import Any + +import yaml +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from tablesleuth.api.serialization import JS_MAX_SAFE_INT, to_dict +from tablesleuth.exceptions import MetadataError, SnapshotNotFoundError, TableLoadError +from tablesleuth.services.iceberg_metadata_service import IcebergMetadataService + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/iceberg", tags=["iceberg"]) + +_service = IcebergMetadataService() + + +def _to_dict_iceberg(obj: Any) -> Any: + """Convert Iceberg objects to dicts with special handling. + + - Skips native_table field (non-serializable PyIceberg Table object) + - Includes @property values (computed metrics like delete_ratio) + - Converts large integers to strings for JavaScript safety + """ + return to_dict( + obj, + skip_fields={"native_table"}, + include_properties=True, + safe_int_threshold=JS_MAX_SAFE_INT, + ) + + +class LoadRequest(BaseModel): + """Request body for /iceberg/load.""" + + metadata_path: str | None = None + catalog_name: str | None = None + table_identifier: str | None = None + + +class CompareRequest(BaseModel): + """Request body for /iceberg/compare.""" + + metadata_path: str | None = None + catalog_name: str | None = None + table_identifier: str | None = None + # Accept str or int — the frontend sends strings for int64 IDs to avoid + # JavaScript float64 precision loss. + snapshot_a_id: str | int + snapshot_b_id: str | int + + +@router.post("/load") +def load_table(req: LoadRequest) -> dict[str, Any]: + """Load an Iceberg table and return metadata. + + Args: + req: Request with metadata_path or catalog_name + table_identifier. + + Returns: + IcebergTableInfo as a dictionary (excluding native_table). + """ + try: + table = _service.load_table( + metadata_path=req.metadata_path, + catalog_name=req.catalog_name, + table_identifier=req.table_identifier, + ) + result = _to_dict_iceberg(table) + assert isinstance(result, dict) + return result + except TableLoadError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error loading Iceberg table") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/snapshots") +def list_snapshots(req: LoadRequest) -> dict[str, Any]: + """List all snapshots for an Iceberg table. + + Args: + req: Request with metadata_path or catalog_name + table_identifier. + + Returns: + Dictionary with list of IcebergSnapshotInfo objects. + """ + try: + table = _service.load_table( + metadata_path=req.metadata_path, + catalog_name=req.catalog_name, + table_identifier=req.table_identifier, + ) + snapshots = _service.list_snapshots(table) + return {"snapshots": [_to_dict_iceberg(s) for s in snapshots], "count": len(snapshots)} + except TableLoadError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error listing Iceberg snapshots") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/snapshot/{snapshot_id}") +def get_snapshot_details(snapshot_id: int, req: LoadRequest) -> dict[str, Any]: + """Get detailed information for a specific snapshot. + + Args: + snapshot_id: Snapshot ID to retrieve. + req: Request with table location info. + + Returns: + IcebergSnapshotDetails as a dictionary. + """ + try: + table = _service.load_table( + metadata_path=req.metadata_path, + catalog_name=req.catalog_name, + table_identifier=req.table_identifier, + ) + details = _service.get_snapshot_details(table, snapshot_id) + result = _to_dict_iceberg(details) + assert isinstance(result, dict) + return result + except SnapshotNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except MetadataError as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc + except TableLoadError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error getting Iceberg snapshot details") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/compare") +def compare_snapshots(req: CompareRequest) -> dict[str, Any]: + """Compare two snapshots and return differences. + + Args: + req: Request with table location and two snapshot IDs. + + Returns: + SnapshotComparison as a dictionary. + """ + try: + table = _service.load_table( + metadata_path=req.metadata_path, + catalog_name=req.catalog_name, + table_identifier=req.table_identifier, + ) + comparison = _service.compare_snapshots( + table, int(req.snapshot_a_id), int(req.snapshot_b_id) + ) + result = _to_dict_iceberg(comparison) + assert isinstance(result, dict) + return result + except SnapshotNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except TableLoadError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error comparing Iceberg snapshots") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +_PYICEBERG_HOME = Path(os.getenv("PYICEBERG_HOME", str(Path.home()))) +_PYICEBERG_YAML = _PYICEBERG_HOME / ".pyiceberg.yaml" + + +@router.get("/catalogs") +def list_catalogs() -> dict[str, Any]: + """List catalog names defined in .pyiceberg.yaml. + + Returns: + Dictionary with list of catalog names and the config file path. + """ + try: + if not _PYICEBERG_YAML.exists(): + return {"catalogs": [], "path": str(_PYICEBERG_YAML), "exists": False} + with _PYICEBERG_YAML.open() as f: + data = yaml.safe_load(f) or {} + catalogs = list((data.get("catalog") or {}).keys()) + return {"catalogs": catalogs, "path": str(_PYICEBERG_YAML), "exists": True} + except Exception as exc: + logger.exception("Error reading catalog names from .pyiceberg.yaml") + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +class CatalogTablesRequest(BaseModel): + """Request body for /iceberg/catalog-tables.""" + + catalog_name: str + + +@router.post("/catalog-tables") +def list_catalog_tables(req: CatalogTablesRequest) -> dict[str, Any]: + """List all tables available in a PyIceberg catalog. + + Enumerates namespaces then tables within each namespace. + + Args: + req: Request with catalog_name matching a .pyiceberg.yaml entry. + + Returns: + Dictionary with list of fully-qualified table identifiers. + """ + try: + from pyiceberg.catalog import load_catalog + + catalog = load_catalog(req.catalog_name) + namespaces = catalog.list_namespaces() + tables: list[str] = [] + for ns in namespaces: + ns_str = ".".join(ns) if isinstance(ns, list | tuple) else str(ns) + try: + for tbl in catalog.list_tables(ns): + tables.append(".".join(tbl)) + except Exception as exc: + # Some catalogs may have namespaces that cannot be listed + logger.debug("Failed to list tables in namespace %s: %s", ns_str, exc) + continue + return {"tables": sorted(tables), "count": len(tables), "catalog": req.catalog_name} + except TableLoadError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error listing tables in catalog %s", req.catalog_name) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/schema-evolution") +def get_schema_evolution(req: LoadRequest) -> dict[str, Any]: + """Get schema evolution history for a table. + + Args: + req: Request with table location info. + + Returns: + Dictionary with list of SchemaInfo objects. + """ + try: + table = _service.load_table( + metadata_path=req.metadata_path, + catalog_name=req.catalog_name, + table_identifier=req.table_identifier, + ) + schemas = _service.get_schema_evolution(table) + return {"schemas": [_to_dict_iceberg(s) for s in schemas], "count": len(schemas)} + except TableLoadError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error getting Iceberg schema evolution") + raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/tablesleuth/api/routers/parquet.py b/src/tablesleuth/api/routers/parquet.py new file mode 100644 index 0000000..6f786ea --- /dev/null +++ b/src/tablesleuth/api/routers/parquet.py @@ -0,0 +1,161 @@ +"""Parquet API router.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from tablesleuth.api.serialization import to_dict +from tablesleuth.services.file_discovery import FileDiscoveryService +from tablesleuth.services.formats.iceberg import IcebergAdapter +from tablesleuth.services.parquet_service import ParquetInspector + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/parquet", tags=["parquet"]) + + +class AnalyzeRequest(BaseModel): + """Request body for /parquet/analyze.""" + + path: str + catalog_name: str | None = None + region: str | None = None + + +class FileInfoRequest(BaseModel): + """Request body for /parquet/file-info.""" + + path: str + region: str | None = None + + +class SampleRequest(BaseModel): + """Request body for /parquet/sample.""" + + path: str + num_rows: int = 100 + region: str | None = None + + +@router.post("/analyze") +def analyze_parquet(req: AnalyzeRequest) -> dict[str, Any]: + """Discover and inspect Parquet files at the given path. + + Args: + req: Request containing path, optional catalog_name and region. + + Returns: + Dictionary with discovered file refs and their basic metadata. + """ + try: + iceberg_adapter = IcebergAdapter(default_catalog=req.catalog_name) + discovery = FileDiscoveryService(iceberg_adapter=iceberg_adapter, region=req.region) + + if req.catalog_name: + file_refs = discovery.discover_from_table( + table_identifier=req.path, catalog_name=req.catalog_name + ) + else: + file_refs = discovery.discover_from_path(req.path) + + return {"files": [to_dict(f) for f in file_refs], "count": len(file_refs)} + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error analyzing parquet path: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/file-info") +def get_file_info(req: FileInfoRequest) -> dict[str, Any]: + """Get detailed metadata for a single Parquet file. + + Args: + req: Request containing the file path and optional region. + + Returns: + ParquetFileInfo as a dictionary. + """ + try: + inspector = ParquetInspector(region=req.region) + info = inspector.inspect_file(Path(req.path)) + result = to_dict(info) + assert isinstance(result, dict) + return result + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error inspecting parquet file: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@router.post("/sample") +def get_sample(req: SampleRequest) -> dict[str, Any]: + """Read a data sample from a Parquet file. + + Args: + req: Request containing path, optional num_rows, and region. + + Returns: + Dictionary with columns list and rows as list-of-lists. + """ + try: + import pyarrow.parquet as pq + + from tablesleuth.services.filesystem import FileSystem + from tablesleuth.utils.path_utils import is_s3_path + + fs = FileSystem(region=req.region) + path = req.path + + if is_s3_path(path): + filesystem = fs.get_filesystem(path) + normalized = fs.normalize_s3_path(path) + pf = pq.ParquetFile(normalized, filesystem=filesystem) + else: + pf = pq.ParquetFile(path) + + # Read only the first batch to avoid loading entire file into memory + batch_reader = pf.iter_batches(batch_size=req.num_rows, use_threads=False) + try: + table = next(batch_reader) + columns = table.schema.names + rows = table.to_pydict() + # Serialize rows to handle non-JSON-safe types (bytes, Decimal, etc.) + rows_as_lists = [ + [ + str(rows[c][i]) + if rows[c][i] is not None + and not isinstance(rows[c][i], int | float | bool | str) + else rows[c][i] + for c in columns + ] + for i in range(len(table)) + ] + except StopIteration: + # Empty file or no readable rows + columns = pf.schema.names + rows_as_lists = [] + + return { + "columns": columns, + "rows": rows_as_lists, + "total_rows_in_file": pf.metadata.num_rows, + "sampled_rows": len(rows_as_lists), + } + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except Exception as exc: + logger.exception("Error reading parquet sample: %s", req.path) + raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/tablesleuth/api/serialization.py b/src/tablesleuth/api/serialization.py new file mode 100644 index 0000000..363bcaf --- /dev/null +++ b/src/tablesleuth/api/serialization.py @@ -0,0 +1,104 @@ +"""Serialization utilities for API responses.""" + +from __future__ import annotations + +import dataclasses +from typing import Any + +# JavaScript's Number.MAX_SAFE_INTEGER = 2^53 - 1 +_JS_MAX_SAFE_INT = 9007199254740991 + + +def to_dict( + obj: Any, + *, + skip_fields: set[str] | None = None, + include_properties: bool = False, + safe_int_threshold: int | None = None, +) -> Any: + """Recursively convert dataclasses and nested objects to serializable dicts. + + Args: + obj: Object to convert (dataclass, list, dict, or primitive). + skip_fields: Set of field names to skip when serializing dataclasses. + include_properties: If True, include @property values from dataclasses. + safe_int_threshold: If set, integers exceeding this value (positive or negative) + are serialized as strings to prevent precision loss in JavaScript. + Use _JS_MAX_SAFE_INT for JavaScript compatibility. + + Returns: + Serializable dictionary, list, or primitive value. + + Examples: + Basic usage: + >>> to_dict(my_dataclass) + + Skip non-serializable fields: + >>> to_dict(obj, skip_fields={"native_table"}) + + Include computed properties: + >>> to_dict(obj, include_properties=True) + + JavaScript-safe integers: + >>> to_dict(obj, safe_int_threshold=_JS_MAX_SAFE_INT) + """ + if dataclasses.is_dataclass(obj) and not isinstance(obj, type): + d = {} + for field in dataclasses.fields(obj): + if skip_fields and field.name in skip_fields: + continue + # Use getattr rather than dataclasses.asdict() to avoid deep-copying + # all fields before we can skip them, which can cause pickle errors + # on non-serializable objects. + d[field.name] = to_dict( + getattr(obj, field.name), + skip_fields=skip_fields, + include_properties=include_properties, + safe_int_threshold=safe_int_threshold, + ) + + # Include @property values if requested + # (dataclasses.fields() only returns declared fields, not computed properties) + if include_properties: + for name, val in vars(type(obj)).items(): + if isinstance(val, property) and name not in d: + d[name] = to_dict( + getattr(obj, name), + skip_fields=skip_fields, + include_properties=include_properties, + safe_int_threshold=safe_int_threshold, + ) + return d + + if isinstance(obj, list): + return [ + to_dict( + i, + skip_fields=skip_fields, + include_properties=include_properties, + safe_int_threshold=safe_int_threshold, + ) + for i in obj + ] + + if isinstance(obj, dict): + return { + k: to_dict( + v, + skip_fields=skip_fields, + include_properties=include_properties, + safe_int_threshold=safe_int_threshold, + ) + for k, v in obj.items() + } + + # Serialize large integers as strings to avoid silent float64 rounding in JavaScript + if safe_int_threshold is not None and isinstance(obj, int) and not isinstance(obj, bool): + if obj > safe_int_threshold or obj < -safe_int_threshold: + return str(obj) + + return obj + + +# Convenience constants for common use cases +JS_MAX_SAFE_INT = _JS_MAX_SAFE_INT diff --git a/src/tablesleuth/cli/web.py b/src/tablesleuth/cli/web.py new file mode 100644 index 0000000..530be70 --- /dev/null +++ b/src/tablesleuth/cli/web.py @@ -0,0 +1,87 @@ +"""Launch the TableSleuth web UI (FastAPI + Next.js).""" + +from __future__ import annotations + +import logging +import threading +import time +import webbrowser + +import click + +from tablesleuth.utils.web_utils import resolve_web_dir + +logger = logging.getLogger(__name__) + + +@click.command("web") +@click.option("--host", default="localhost", show_default=True, help="Host to bind the server to.") +@click.option("--port", default=8000, show_default=True, type=int, help="Port to listen on.") +@click.option( + "--no-browser", is_flag=True, default=False, help="Do not open browser automatically." +) +@click.option( + "--log-level", + default="warning", + show_default=True, + type=click.Choice(["debug", "info", "warning", "error"], case_sensitive=False), + help="Uvicorn log level.", +) +def web(host: str, port: int, no_browser: bool, log_level: str) -> None: + """Launch the TableSleuth web UI. + + Starts a FastAPI server and optionally opens the browser. The frontend is + served from the bundled Next.js static export (or the dev build at web-ui/out). + + To install web dependencies: + pip install tablesleuth[web] + + To rebuild the frontend (developers): + make build-release + """ + # Check uvicorn is importable + try: + import uvicorn # noqa: F401 + except ImportError as err: + raise click.ClickException( + "uvicorn is not installed. Install web dependencies with:\n" + " pip install tablesleuth[web]\n" + "or:\n" + " uv sync --extra web" + ) from err + + # Resolve web UI directory + web_dir = resolve_web_dir() + if web_dir is None: + click.echo( + "Warning: Web UI static files not found. The API will start but no UI will be served.\n" + " - For production: pip install tablesleuth[web] (includes pre-built UI)\n" + " - For development: run 'make build-release' first, or 'make dev-web' separately.", + err=True, + ) + else: + click.echo(f"Serving web UI from: {web_dir}") + + url = f"http://{host}:{port}" + click.echo(f"Starting TableSleuth web UI at {url}") + + # Open browser after 1.2s delay in daemon thread + if not no_browser: + + def _open_browser() -> None: + time.sleep(1.2) + webbrowser.open(url) + + t = threading.Thread(target=_open_browser, daemon=True) + t.start() + + # Launch uvicorn + import uvicorn + + uvicorn.run( + "tablesleuth.api.main:app", + host=host, + port=port, + log_level=log_level.lower(), + reload=False, + ) diff --git a/src/tablesleuth/models/iceberg.py b/src/tablesleuth/models/iceberg.py index 8672933..660ccad 100644 --- a/src/tablesleuth/models/iceberg.py +++ b/src/tablesleuth/models/iceberg.py @@ -295,11 +295,16 @@ class QueryPerformanceMetrics: Attributes: execution_time_ms: Total query execution time in milliseconds - files_scanned: Number of files scanned - bytes_scanned: Total bytes scanned - rows_scanned: Total rows scanned (before filtering) - rows_returned: Rows returned (after filtering) + files_scanned: Total files scanned (data + delete files) + bytes_scanned: Total bytes scanned (sum of all scanned file sizes) + rows_scanned: Total rows scanned before delete application + (data-file records + delete records) + rows_returned: Rows returned after all filtering and delete application memory_peak_mb: Peak memory usage in megabytes + data_files_scanned: Data files only (subset of files_scanned) + delete_files_scanned: Delete files only (subset of files_scanned) + data_rows_scanned: Records in data files (subset of rows_scanned) + delete_rows_scanned: Records in delete files (subset of rows_scanned) """ execution_time_ms: float @@ -308,6 +313,10 @@ class QueryPerformanceMetrics: rows_scanned: int rows_returned: int memory_peak_mb: float + data_files_scanned: int = 0 + delete_files_scanned: int = 0 + data_rows_scanned: int = 0 + delete_rows_scanned: int = 0 @property def scan_efficiency(self) -> float: diff --git a/src/tablesleuth/services/iceberg_manifest_patch.py b/src/tablesleuth/services/iceberg_manifest_patch.py new file mode 100644 index 0000000..0945b2a --- /dev/null +++ b/src/tablesleuth/services/iceberg_manifest_patch.py @@ -0,0 +1,330 @@ +"""Patch Iceberg snapshot metadata to work around DuckDB's uppercase file_format bug. + +DuckDB's iceberg_scan() rejects delete files whose file_format is stored as 'PARQUET' +(uppercase) in the manifest avro — it only accepts 'parquet' (lowercase). This module +creates a lightweight, temporary, locally-patched copy of the metadata chain: + + metadata.json → patched temp copy (JSON rewrite, trivial) + manifest-list.avro → patched temp copy (avro rewrite via fastavro, redirects + delete-manifest paths to temp copies) + delete manifests → fastavro-rewritten temp copies (file_format lowercased, + handles null/Snappy/deflate codecs transparently) + +Data manifests and all actual data / delete Parquet files are NOT copied or modified; +they are referenced by their original S3 / local paths. + +Usage (context manager — cleans up temp files on exit):: + + with patched_iceberg_metadata(native_table, snapshot_id) as meta_uri: + # meta_uri is a file:// URI pointing to the patched metadata.json + profiler.register_iceberg_table_with_snapshot("snap_a", meta_uri, snapshot_id) +""" + +from __future__ import annotations + +import io +import json +import logging +import tempfile +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Generator + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Low-level helpers +# --------------------------------------------------------------------------- + + +def _read_bytes(uri: str) -> bytes: + """Read raw bytes from a local path, file:// URI, or s3:// URI.""" + if uri.startswith(("s3://", "s3a://")): + from urllib.parse import urlparse + + import boto3 # already a project dependency via pyiceberg + + parsed = urlparse(uri) + bucket = parsed.netloc + key = parsed.path.lstrip("/") + s3 = boto3.client("s3") + body = s3.get_object(Bucket=bucket, Key=key)["Body"].read() + return bytes(body) + + # file:// URI or plain local path + if uri.startswith("file://"): + # Path.from_uri() handles Windows drive letters correctly: + # file:///D:/path → D:\path (available in Python 3.13+) + return Path.from_uri(uri).read_bytes() + + return Path(uri).read_bytes() + + +def _patch_file_format_in_record(obj: Any) -> tuple[Any, bool]: + """Recursively lowercase any file_format == 'PARQUET' field in a deserialized Avro object. + + Handles any nesting depth so it works regardless of whether the manifest uses + the v1 or v2 Iceberg schema layout (direct record vs union-wrapped struct). + + Returns: + Tuple of (possibly-modified object, changed_flag). + """ + if isinstance(obj, dict): + changed = False + new_obj: dict[str, Any] = {} + for k, v in obj.items(): + if k == "file_format" and isinstance(v, str) and v == "PARQUET": + new_obj[k] = "parquet" + changed = True + else: + new_v, sub_changed = _patch_file_format_in_record(v) + new_obj[k] = new_v + if sub_changed: + changed = True + return new_obj, changed + elif isinstance(obj, list): + changed = False + new_list: list[Any] = [] + for item in obj: + new_item, sub_changed = _patch_file_format_in_record(item) + new_list.append(new_item) + if sub_changed: + changed = True + return new_list, changed + else: + return obj, False + + +def _patch_delete_manifest(content: bytes) -> bytes: + """Rewrite a delete manifest avro, lowercasing file_format 'PARQUET' → 'parquet'. + + Uses fastavro for proper Avro round-tripping so it works with null, Snappy, + and deflate codecs. Falls back to binary substitution if fastavro fails. + + Returns the original bytes object unchanged if no patching was needed. + Caller uses identity comparison (patched is content) to detect this. + """ + import fastavro + + try: + reader = fastavro.reader(io.BytesIO(content)) + schema = reader.writer_schema + records: list[dict] = [] + changed = False + + for record in reader: + new_record, was_changed = _patch_file_format_in_record(record) + records.append(new_record) + if was_changed: + changed = True + + if not changed: + return content # Same object — identity check in caller will skip this manifest + + out = io.BytesIO() + fastavro.writer(out, fastavro.parse_schema(schema), records) + patched = out.getvalue() + logger.warning( + "Patched delete manifest: lowercased %d file_format field(s)", + sum( + 1 + for r in records + if isinstance(r.get("data_file"), dict) + and r["data_file"].get("file_format") == "parquet" + ), + ) + return patched + + except Exception as exc: + logger.warning("fastavro round-trip failed (%s), falling back to binary patch", exc) + if b"PARQUET" not in content: + return content + return content.replace(b"PARQUET", b"parquet") + + +def _rewrite_manifest_list(content: bytes, path_map: dict[str, str]) -> bytes: + """Rewrite a manifest-list avro file, updating manifest_path values per path_map. + + Uses fastavro for proper avro round-tripping (path strings change length so + binary substitution is not safe here). + """ + import fastavro + + reader = fastavro.reader(io.BytesIO(content)) + schema = reader.writer_schema + records: list[dict] = [] + for record in reader: + orig = record.get("manifest_path", "") # type: ignore[union-attr] + if orig in path_map: + record = dict(record) # type: ignore[arg-type] + record["manifest_path"] = path_map[orig] + records.append(record) # type: ignore[arg-type] + + out = io.BytesIO() + fastavro.writer(out, fastavro.parse_schema(schema), records) + return out.getvalue() + + +# --------------------------------------------------------------------------- +# Public context manager +# --------------------------------------------------------------------------- + + +@contextmanager +def patched_iceberg_metadata( + native_table: Any, # pyiceberg.table.Table + snapshot_id: int, +) -> Generator[str, None, None]: # noqa: UP043 + """Yield a ``file://`` URI for a locally-patched copy of Iceberg snapshot metadata. + + Two problems are corrected: + + 1. **DuckDB delete-file format bug** — DuckDB's ``iceberg_scan()`` rejects delete + manifests whose ``file_format`` is ``'PARQUET'`` (uppercase). Affected avro + files are re-encoded with the value lowercased. + + 2. **Stale current-snapshot-id** — DuckDB applies delete files based on the + table's ``current-snapshot-id`` rather than the ``version =>`` argument. + When comparing an older snapshot against a newer one that introduced deletes, + the older snapshot would incorrectly inherit those deletes. The patched + metadata sets ``current-snapshot-id`` to the target snapshot so DuckDB only + sees the delete files that belong to that particular snapshot. + + A temporary local ``metadata.json`` is always written (even when no avro + re-encoding is needed) to carry the corrected ``current-snapshot-id``. + + Args: + native_table: PyIceberg ``Table`` instance (from ``IcebergTableInfo.native_table``). + snapshot_id: Snapshot ID to patch. + + Yields: + A ``file://`` URI pointing to the patched local ``metadata.json``, suitable + for passing to ``iceberg_scan(uri, version => snapshot_id)``. + """ + metadata_location: str = native_table.metadata_location + logger.debug( + "patched_iceberg_metadata: metadata_location=%r snapshot_id=%s", + metadata_location, + snapshot_id, + ) + + # Read the metadata JSON (always local-accessible since IcebergMetadataService + # already resolved it; for S3 tables PyIceberg caches/fetches it). + try: + meta_bytes = _read_bytes(metadata_location) + except Exception as exc: + logger.warning("Cannot read metadata for patching, using original: %s", exc) + yield metadata_location + return + + metadata: dict = json.loads(meta_bytes) + + # Locate the target snapshot entry. + target_snap: dict | None = None + for snap in metadata.get("snapshots", []): + if snap.get("snapshot-id") == snapshot_id: + target_snap = snap + break + + if target_snap is None: + logger.warning("Snapshot %s not found in metadata; using original", snapshot_id) + yield metadata_location + return + + manifest_list_uri: str = target_snap["manifest-list"] + logger.debug("patched_iceberg_metadata: manifest_list_uri=%r", manifest_list_uri) + + # Read the manifest-list avro to discover which manifests are DELETE manifests. + try: + ml_bytes = _read_bytes(manifest_list_uri) + except Exception as exc: + logger.warning("Cannot read manifest-list for patching, using original: %s", exc) + yield metadata_location + return + + import fastavro + + ml_reader = fastavro.reader(io.BytesIO(ml_bytes)) + ml_records: list[dict] = list(ml_reader) # type: ignore[arg-type] + + # Identify DELETE manifests (content == 1 in the manifest-list schema). + delete_manifest_uris = [r["manifest_path"] for r in ml_records if r.get("content", 0) == 1] + + logger.debug( + "patched_iceberg_metadata: found %d delete manifest(s): %r", + len(delete_manifest_uris), + delete_manifest_uris, + ) + + # Always create a patched metadata copy even when no delete manifests need format + # fixing. DuckDB's iceberg_scan() applies delete files based on the table's + # *current-snapshot-id* rather than on the version specified via ``version =>``. + # Setting current-snapshot-id to the target snapshot in a local copy ensures + # DuckDB only sees delete files that belong to that snapshot, preventing older + # snapshots from inheriting delete records added by a newer current snapshot. + with tempfile.TemporaryDirectory(prefix="tablesleuth_iceberg_patch_") as tmpdir: + tmp = Path(tmpdir) + path_map: dict[str, str] = {} # original URI → local posix path + + for idx, del_uri in enumerate(delete_manifest_uris): + try: + raw = _read_bytes(del_uri) + except Exception as exc: + logger.warning("Cannot read delete manifest %r: %s", del_uri, exc) + continue + + patched = _patch_delete_manifest(raw) + if patched is raw: + # No uppercase PARQUET found — no format patch needed for this manifest. + logger.debug( + "patched_iceberg_metadata: delete manifest %r needed no patching", del_uri + ) + continue + + local_name = f"delete_manifest_{idx}.avro" + local_path = tmp / local_name + local_path.write_bytes(patched) + # Use posix path (no file:// prefix) so DuckDB can open it directly. + # file:///C:/... URIs get mangled by DuckDB's internal path stripping + # (file:// stripped → /C:/... which is invalid on Windows). + local_posix = local_path.as_posix() + path_map[del_uri] = local_posix + logger.debug( + "patched_iceberg_metadata: patched delete manifest %r → %s", del_uri, local_posix + ) + + # Rewrite the manifest-list only when some delete manifests were re-encoded. + new_ml_posix: str | None = None + if path_map: + patched_ml = _rewrite_manifest_list(ml_bytes, path_map) + ml_path = tmp / "manifest_list.avro" + ml_path.write_bytes(patched_ml) + new_ml_posix = ml_path.as_posix() + logger.debug("patched_iceberg_metadata: rewrote manifest-list → %s", new_ml_posix) + else: + logger.debug( + "patched_iceberg_metadata: %d delete manifest(s) found; none needed format patching", + len(delete_manifest_uris), + ) + + # Always rewrite the metadata JSON so that: + # 1. current-snapshot-id points to the target snapshot (not the table's + # current HEAD), preventing DuckDB from applying newer delete files. + # 2. If delete manifests were re-encoded, the manifest-list path is updated. + patched_meta = json.loads(json.dumps(metadata)) # deep copy + patched_meta["current-snapshot-id"] = snapshot_id + + if new_ml_posix is not None: + for snap in patched_meta.get("snapshots", []): + if snap.get("snapshot-id") == snapshot_id: + snap["manifest-list"] = new_ml_posix + break + + meta_path = tmp / "metadata.json" + meta_path.write_text(json.dumps(patched_meta), encoding="utf-8") + logger.debug( + "patched_iceberg_metadata: yielding patched metadata URI: %s", meta_path.as_uri() + ) + + yield meta_path.as_uri() diff --git a/src/tablesleuth/services/iceberg_metadata_service.py b/src/tablesleuth/services/iceberg_metadata_service.py index 293018b..b766fc0 100644 --- a/src/tablesleuth/services/iceberg_metadata_service.py +++ b/src/tablesleuth/services/iceberg_metadata_service.py @@ -3,12 +3,50 @@ from __future__ import annotations import logging +import re +import sys from pathlib import Path from typing import Any from pyiceberg.catalog import load_catalog from pyiceberg.table import StaticTable, Table +# --------------------------------------------------------------------------- +# Windows compatibility: fix PyIceberg's URI path parsing for local files. +# +# On Windows, urlparse("file:///D:/path/file.json").path == "/D:/path/file.json" +# (leading slash before drive letter). PyArrow's LocalFileSystem rejects this +# with WinError 123. Patch parse_location to strip the spurious slash so that +# "file:///D:/path/file.json" resolves to "D:/path/file.json" for ALL file +# operations (metadata read, manifest-list, manifest, data files). +# --------------------------------------------------------------------------- +if sys.platform == "win32": + try: + from pyiceberg.io.pyarrow import PyArrowFileIO as _PAFIO + + # Accessing a staticmethod via the class gives the raw function directly — + # no .__func__ needed. + _orig_parse = _PAFIO.parse_location + _WIN_DRIVE_PATH = re.compile(r"^/([A-Za-z]:/.*)") + + def _win_parse_location(location: str, properties: dict | None = None) -> tuple: + if properties is None: + properties = {} + scheme, netloc, path = _orig_parse(location, properties) + if scheme == "file": + m = _WIN_DRIVE_PATH.match(path) + if m: + path = m.group(1) # /D:/path → D:/path + return scheme, netloc, path + + # Must re-wrap as staticmethod so self.parse_location(...) doesn't + # receive self as the first argument. + _PAFIO.parse_location = staticmethod(_win_parse_location) # type: ignore[method-assign] + except Exception as _patch_err: + logging.getLogger(__name__).warning( + "Failed to apply Windows PyArrowFileIO path patch: %s", _patch_err + ) + from tablesleuth.exceptions import ( MetadataError, SnapshotNotFoundError, @@ -66,7 +104,20 @@ def load_table( raise TableLoadError(f"Metadata file not found: {metadata_path}") try: - table: Table = StaticTable.from_metadata(metadata_path) + # PyIceberg parses the path as a URI. On Windows, a drive + # letter like "D:" is mistaken for a URI scheme, causing + # "Unrecognized filesystem type in URI: d". Convert any + # absolute local path to a file:// URI first. + path_obj = Path(metadata_path) + if not path_obj.is_absolute(): + path_obj = path_obj.resolve() + if not metadata_path.startswith( + ("s3://", "gs://", "abfs://", "file://", "hdfs://") + ): + metadata_uri = path_obj.as_uri() + else: + metadata_uri = metadata_path + table: Table = StaticTable.from_metadata(metadata_uri) location = metadata_path except Exception as e: logger.exception(f"Failed to load table from metadata file: {metadata_path}") diff --git a/src/tablesleuth/services/profiling/gizmo_duckdb.py b/src/tablesleuth/services/profiling/gizmo_duckdb.py index 546d93d..a794cb7 100644 --- a/src/tablesleuth/services/profiling/gizmo_duckdb.py +++ b/src/tablesleuth/services/profiling/gizmo_duckdb.py @@ -3,6 +3,7 @@ import logging import re from collections.abc import Sequence +from pathlib import Path from typing import Any, Optional from adbc_driver_flightsql import DatabaseOptions @@ -110,16 +111,27 @@ def _validate_filter_expression(filters: str) -> None: def _clean_file_path(path: str) -> str: - """Remove file:// prefix from paths if present. + """Convert a file:// URI to a local path, or return path unchanged. + + Handles both Windows (file:///C:/path) and Unix (file:///path) style URIs. + Falls back to manual parsing if Path.from_uri() fails. Args: - path: File path that may include file:// prefix + path: File path or file:// URI Returns: - Cleaned path without file:// prefix + Local path with forward slashes (suitable for DuckDB on any platform) """ if path.startswith("file://"): - return path[7:] + try: + # Try Path.from_uri() first (Python 3.13+) + # Works for proper absolute URIs like file:///C:/path on Windows + return Path.from_uri(path).as_posix() + except ValueError: + # Fallback for URIs that aren't considered absolute on this platform + # (e.g., file:///path/to/file on Windows) + # Simply strip the file:// prefix + return path[7:] # Remove "file://" return path @@ -136,6 +148,17 @@ def __init__( self._password = password self._tls_skip_verify = tls_skip_verify self._registered_catalogs: dict[str, str] = {} # catalog_name -> catalog_path + self._view_paths: dict[str, list[str]] = {} # view_name -> cleaned file paths + self._iceberg_tables: dict[ + str, tuple[str, int | None] + ] = {} # table_id -> (metadata_path, snapshot_id) + self._delta_tables: dict[str, list[str]] = {} # table_id -> file URIs + self._delta_table_stats: dict[ + str, tuple[int, int, int] + ] = {} # table_id -> (files, bytes, rows) + self._iceberg_scan_stats: dict[ + str, dict[str, int] + ] = {} # table_id -> scan stat keys/values def _connect(self) -> Any: """Create a FlightSQL connection. @@ -234,8 +257,6 @@ def register_file_view(self, file_paths: list[str], view_name: str | None = None cleaned_paths = [_clean_file_path(path) for path in file_paths] # Store the cleaned file paths mapping for this view name - if not hasattr(self, "_view_paths"): - self._view_paths = {} self._view_paths[safe_view_name] = cleaned_paths return safe_view_name @@ -260,7 +281,7 @@ def profile_single_column( where_clause = "" # Get the file paths for this view name - if hasattr(self, "_view_paths") and safe_view_name in self._view_paths: + if safe_view_name in self._view_paths: file_paths = self._view_paths[safe_view_name] # Build read_parquet() expression if len(file_paths) == 1: @@ -271,8 +292,23 @@ def profile_single_column( paths_list = ", ".join(f"'{p}'" for p in escaped_paths) from_clause = f"read_parquet([{paths_list}])" else: - # Fallback: assume view_name is a table/view name - from_clause = safe_view_name + # Check if this is a registered Iceberg or Delta table + # Use a dummy query to trigger table replacement logic + # safe_view_name is sanitized via _sanitize_identifier() + dummy_query = f"SELECT * FROM {safe_view_name}" # nosec B608 + replaced_query = self._replace_iceberg_tables(dummy_query) + + if replaced_query != dummy_query: + # Extract the scan call from the replaced query + # Pattern: SELECT * FROM + match = re.search(r"FROM\s+(.+)$", replaced_query, re.IGNORECASE) + if match: + from_clause = match.group(1).strip() + else: + from_clause = safe_view_name + else: + # Fallback: assume view_name is a table/view name + from_clause = safe_view_name # First, detect if column is numeric by checking its type type_check_sql = f""" @@ -282,8 +318,11 @@ def profile_single_column( LIMIT 1 """ # nosec B608 + use_iceberg = "iceberg_scan" in from_clause is_numeric = False with self._connect() as conn, conn.cursor() as cur: + if use_iceberg: + self._ensure_iceberg_loaded(cur) cur.execute(type_check_sql) type_row = cur.fetchone() if type_row: @@ -334,6 +373,8 @@ def profile_single_column( """ # nosec B608 with self._connect() as conn, conn.cursor() as cur: + if use_iceberg: + self._ensure_iceberg_loaded(cur) cur.execute(sql) row = cur.fetchone() @@ -352,6 +393,8 @@ def profile_single_column( """ # nosec B608 with self._connect() as conn, conn.cursor() as cur: + if use_iceberg: + self._ensure_iceberg_loaded(cur) cur.execute(mode_sql) mode_row = cur.fetchone() if mode_row: @@ -409,8 +452,7 @@ def clear_views(self) -> None: This removes all stored file path mappings, forcing views to be re-registered on next use. Useful when refreshing or invalidating caches. """ - if hasattr(self, "_view_paths"): - self._view_paths.clear() + self._view_paths.clear() def register_iceberg_table(self, table_identifier: str, metadata_location: str) -> None: """Register an Iceberg table for querying. @@ -450,9 +492,6 @@ def register_iceberg_table_with_snapshot( clean_metadata_location = _clean_file_path(metadata_location) # Store the mapping with snapshot info - if not hasattr(self, "_iceberg_tables"): - self._iceberg_tables: dict[str, tuple[str, int | None]] = {} - self._iceberg_tables[table_identifier] = (clean_metadata_location, snapshot_id) logger.debug( f"Registered Iceberg table {table_identifier} -> {clean_metadata_location}" @@ -477,17 +516,13 @@ def execute_query_with_metrics(self, query: str) -> tuple[Any, QueryPerformanceM import time try: - # Replace Iceberg table references with iceberg_scan() calls + # Replace Iceberg/Delta table references with scan function calls. + # Keep the original so we can later look up which file tables were used. + original_query = query modified_query = self._replace_iceberg_tables(query) with self._connect() as conn, conn.cursor() as cur: - # Install and load Iceberg extension - try: - cur.execute("INSTALL iceberg") - cur.execute("LOAD iceberg") - except Exception as e: - logger.warning(f"Failed to install/load Iceberg extension: {e}") - + self._ensure_iceberg_loaded(cur) # Execute query and measure time start_time = time.time() cur.execute(modified_query) @@ -502,60 +537,187 @@ def execute_query_with_metrics(self, query: str) -> tuple[Any, QueryPerformanceM # Debug: Log the explain output to see what we're getting logger.debug(f"EXPLAIN ANALYZE output: {explain_output}") - # Parse metrics from EXPLAIN ANALYZE output + # Parse metrics from EXPLAIN ANALYZE output, then fill in any + # zeros from the stats we captured at file-table registration time. metrics = self._parse_explain_analyze( explain_output, execution_time_ms, modified_query ) + metrics = self._supplement_metrics(metrics, original_query, results) return results, metrics except Exception as e: raise RuntimeError(f"Query execution failed: {e}") from e + def register_delta_table_with_version( + self, + table_identifier: str, + path: str, + version: int | None = None, + storage_options: dict[str, str] | None = None, + ) -> None: + """Register a Delta table at a specific version for query rewriting. + + Uses deltalake to resolve the active Parquet file URIs at the given version. + At query time, references to table_identifier are rewritten to + read_parquet([uri1, uri2, ...]) so GizmoSQL can scan the exact snapshot. + + Args: + table_identifier: Alias to use in SQL queries (e.g. "ver_a") + path: Local or cloud path to the Delta table root + version: Optional version number to pin to; None means latest + storage_options: Optional storage configuration for cloud backends + """ + if not table_identifier or not path: + raise ValueError("table_identifier and path are required") + + from deltalake import DeltaTable + + kwargs: dict[str, Any] = {} + if version is not None: + kwargs["version"] = version + if storage_options: + kwargs["storage_options"] = storage_options + + dt = DeltaTable(path, **kwargs) + file_uris = dt.file_uris() # absolute URIs for all active files at this version + + # Collect stats for metrics fallback (best-effort; ignore errors). + total_bytes = 0 + total_rows = 0 + try: + actions = dt.get_add_actions(flatten=True) + total_bytes = int(sum(v or 0 for v in actions.column("size_bytes").to_pylist())) + total_rows = int(sum(v or 0 for v in actions.column("num_records").to_pylist())) + except Exception as exc: + logger.debug(f"Could not read Delta add-actions stats: {exc}") + + self._delta_tables[table_identifier] = file_uris + self._delta_table_stats[table_identifier] = (len(file_uris), total_bytes, total_rows) + logger.debug( + f"Registered Delta table {table_identifier} -> {len(file_uris)} files" + + (f" at version {version}" if version is not None else " at latest version") + ) + + def register_iceberg_scan_stats( + self, + table_identifier: str, + files_scanned: int = 0, + rows_scanned: int = 0, + bytes_scanned: int = 0, + data_files_scanned: int = 0, + delete_files_scanned: int = 0, + data_rows_scanned: int = 0, + delete_rows_scanned: int = 0, + ) -> None: + """Register known scan statistics for an Iceberg snapshot. + + Used to supplement metrics when EXPLAIN ANALYZE (via Arrow Flight SQL) does + not return reliable file or row counts. Call this after + ``register_iceberg_table_with_snapshot`` with values derived from the + PyIceberg snapshot summary. + + Args: + table_identifier: Alias used in SQL queries (e.g. ``"snap_a"``). + files_scanned: Total files (data + delete). + rows_scanned: Total physical rows (data-file records + delete records). + bytes_scanned: Total file size in bytes (from snapshot summary). + data_files_scanned: Data files only. + delete_files_scanned: Delete files only. + data_rows_scanned: Records in data files only. + delete_rows_scanned: Records in delete files only. + """ + self._iceberg_scan_stats[table_identifier] = { + "files_scanned": files_scanned, + "bytes_scanned": bytes_scanned, + "rows_scanned": rows_scanned, + "data_files_scanned": data_files_scanned, + "delete_files_scanned": delete_files_scanned, + "data_rows_scanned": data_rows_scanned, + "delete_rows_scanned": delete_rows_scanned, + } + logger.debug( + "Registered Iceberg scan stats for %s: %d files (%d data + %d delete), " + "%d rows_scanned, %d bytes", + table_identifier, + files_scanned, + data_files_scanned, + delete_files_scanned, + rows_scanned, + bytes_scanned, + ) + + def _ensure_iceberg_loaded(self, cur: Any) -> None: + """Ensure the Iceberg extension is installed and loaded on the given cursor.""" + try: + cur.execute("INSTALL iceberg") + cur.execute("LOAD iceberg") + except Exception as e: + logger.warning("Failed to install/load Iceberg extension: %s", e) + + @staticmethod + def _replace_table_ref(query: str, table_identifier: str, scan_call: str) -> str: + """Replace all references to a table identifier with a scan function call. + + Uses word-boundary matching which handles bare identifiers and quoted + identifiers uniformly (quotes are non-word characters, so \\b matches + at the quote boundary). + + Args: + query: SQL query to modify. + table_identifier: Table name/alias to replace. + scan_call: Scan function call to substitute (e.g., "iceberg_scan(...)"). + + Returns: + Modified query with table references replaced. + """ + # Word boundary pattern handles both bare and quoted identifiers + # because quotes (", ') are non-word characters + pattern = rf"\b{re.escape(table_identifier)}\b" + return re.sub(pattern, lambda m: scan_call, query, flags=re.IGNORECASE) + def _replace_iceberg_tables(self, query: str) -> str: - """Replace Iceberg table references with iceberg_scan() calls. + """Replace table references with iceberg_scan() or read_parquet() calls. Args: query: Original SQL query Returns: - Modified query with iceberg_scan() calls + Modified query with scan function calls substituted """ - if not hasattr(self, "_iceberg_tables") or not self._iceberg_tables: - return query - modified_query = query - for table_id, table_info in self._iceberg_tables.items(): - # Unpack table info tuple - metadata_loc, snapshot_id = table_info - - # Escape single quotes in metadata location to prevent SQL injection - # SQL standard: escape single quotes by doubling them - escaped_metadata_loc = metadata_loc.replace("'", "''") - - # Build iceberg_scan() call with optional snapshot parameter - # Validate snapshot_id is actually an integer to prevent SQL injection - if snapshot_id is not None: - if not isinstance(snapshot_id, int): - raise ValueError(f"snapshot_id must be an integer, got {type(snapshot_id)}") - scan_call = f"iceberg_scan('{escaped_metadata_loc}', version => {snapshot_id})" - else: - scan_call = f"iceberg_scan('{escaped_metadata_loc}')" - # Replace table references with iceberg_scan() - # Handle both quoted and unquoted table names - patterns = [ - rf"\b{re.escape(table_id)}\b", # Unquoted - rf'"{re.escape(table_id)}"', # Double quoted - rf"'{re.escape(table_id)}'", # Single quoted - ] + # --- Iceberg tables --- + if self._iceberg_tables: + for table_id, table_info in self._iceberg_tables.items(): + metadata_loc, snapshot_id = table_info + escaped_metadata_loc = metadata_loc.replace("'", "''") - for pattern in patterns: - modified_query = re.sub(pattern, scan_call, modified_query, flags=re.IGNORECASE) + if snapshot_id is not None: + if not isinstance(snapshot_id, int): + raise ValueError(f"snapshot_id must be an integer, got {type(snapshot_id)}") + scan_call = f"iceberg_scan('{escaped_metadata_loc}', version => {snapshot_id})" + else: + scan_call = f"iceberg_scan('{escaped_metadata_loc}')" + + modified_query = self._replace_table_ref(modified_query, table_id, scan_call) + + # --- Delta tables --- + # delta_scan() does not support version travel; instead we resolve the active + # Parquet file URIs at registration time and use read_parquet([...]) here. + if self._delta_tables: + for table_id, file_uris in self._delta_tables.items(): + if not file_uris: + continue + + escaped_uris = ", ".join(f"'{u.replace(chr(39), chr(39) * 2)}'" for u in file_uris) + scan_call = f"read_parquet([{escaped_uris}])" + + modified_query = self._replace_table_ref(modified_query, table_id, scan_call) if modified_query != query: logger.debug( - f"Replaced Iceberg tables in query:\nOriginal: {query}\nModified: {modified_query}" + f"Replaced table refs in query:\nOriginal: {query}\nModified: {modified_query}" ) return modified_query @@ -584,6 +746,116 @@ def explain_analyze(self, query: str) -> str: except Exception as e: raise RuntimeError(f"EXPLAIN ANALYZE failed: {e}") from e + def _supplement_metrics( + self, + metrics: QueryPerformanceMetrics, + original_query: str, + results: list, + ) -> QueryPerformanceMetrics: + """Fill zero-valued metrics using stats stored at file-table registration time. + + EXPLAIN ANALYZE output format varies by DuckDB build / transport (Arrow Flight SQL + vs local). When using read_parquet([...]) the plan text may not contain the + file/row counts in the expected format, leaving those metrics as 0. Since we + already know the file list, total bytes, and row counts from file metadata, we + use those as authoritative values whenever EXPLAIN ANALYZE returns zeros. + + rows_returned is derived from the actual query results when the parsed value is 0. + + Args: + metrics: Partially-populated metrics from EXPLAIN ANALYZE parsing + original_query: Query before table-alias substitution (used to identify + which registered file table was referenced) + results: Raw query results rows (to derive rows_returned for COUNT queries) + + Returns: + Updated QueryPerformanceMetrics with zeros replaced by known values + """ + import re as _re + + files_scanned = metrics.files_scanned + bytes_scanned = metrics.bytes_scanned + rows_scanned = metrics.rows_scanned + rows_returned = metrics.rows_returned + data_files_scanned = metrics.data_files_scanned + delete_files_scanned = metrics.delete_files_scanned + data_rows_scanned = metrics.data_rows_scanned + delete_rows_scanned = metrics.delete_rows_scanned + + # 1. Iceberg snapshot stats (files = data+delete; rows_scanned = data records + # + delete records, i.e. physical rows read before applying deletes). + for table_id, ic in self._iceberg_scan_stats.items(): + if _re.search(rf"\b{_re.escape(table_id)}\b", original_query, _re.IGNORECASE): + if files_scanned == 0 and ic.get("files_scanned", 0) > 0: + files_scanned = ic["files_scanned"] + if bytes_scanned == 0 and ic.get("bytes_scanned", 0) > 0: + bytes_scanned = ic["bytes_scanned"] + if rows_scanned == 0 and ic.get("rows_scanned", 0) > 0: + rows_scanned = ic["rows_scanned"] + if data_files_scanned == 0 and ic.get("data_files_scanned", 0) > 0: + data_files_scanned = ic["data_files_scanned"] + if delete_files_scanned == 0 and ic.get("delete_files_scanned", 0) > 0: + delete_files_scanned = ic["delete_files_scanned"] + if data_rows_scanned == 0 and ic.get("data_rows_scanned", 0) > 0: + data_rows_scanned = ic["data_rows_scanned"] + if delete_rows_scanned == 0 and ic.get("delete_rows_scanned", 0) > 0: + delete_rows_scanned = ic["delete_rows_scanned"] + + # 2. Collect stats for any registered Delta tables referenced. + for table_id, (fc, tb, tr) in self._delta_table_stats.items(): + if _re.search(rf"\b{_re.escape(table_id)}\b", original_query, _re.IGNORECASE): + if files_scanned == 0: + files_scanned = fc + if bytes_scanned == 0: + bytes_scanned = tb + if rows_scanned == 0: + rows_scanned = tr + + # 3. Derive rows_returned from actual query results (most accurate for COUNT + # queries, where the result already reflects any applied delete records). + if rows_returned == 0 and results: + if len(results) == 1 and len(results[0]) == 1: + # Single scalar result — could be COUNT(*) or another aggregate (SUM, AVG, etc.) + val = results[0][0] + # Only treat as row count if query contains COUNT. + # This heuristic prevents misinterpreting SUM(price)=5000000 as 5M rows. + if _re.search(r"\bCOUNT\s*\(", original_query, _re.IGNORECASE): + try: + count_val = int(val) + # Accept any non-negative count value, including >1B for large + # data warehouse tables (Iceberg, Delta Lake routinely exceed 1B rows) + if count_val >= 0: + rows_returned = count_val + else: + # Negative counts are invalid + rows_returned = 1 + except (ValueError, TypeError): + # If conversion fails, default to 1 (single result row) + rows_returned = 1 + else: + # Non-COUNT aggregate (SUM, AVG, MAX, etc.) — just 1 result row + rows_returned = 1 + else: + rows_returned = len(results) + + # 4. Last-resort fallback: if rows_scanned is still unknown, assume it equals + # rows_returned (valid for tables with no delete files). + if rows_scanned == 0 and rows_returned > 0: + rows_scanned = rows_returned + + return QueryPerformanceMetrics( + execution_time_ms=metrics.execution_time_ms, + files_scanned=files_scanned, + bytes_scanned=bytes_scanned, + rows_scanned=rows_scanned, + rows_returned=rows_returned, + memory_peak_mb=metrics.memory_peak_mb, + data_files_scanned=data_files_scanned, + delete_files_scanned=delete_files_scanned, + data_rows_scanned=data_rows_scanned, + delete_rows_scanned=delete_rows_scanned, + ) + def _parse_explain_analyze( self, explain_output: list, execution_time_ms: float, query: str ) -> QueryPerformanceMetrics: @@ -750,11 +1022,12 @@ def _get_iceberg_file_count(self, metadata_location: str, snapshot_id: str | Non if not manifest_list_path: return 0 - # For now, return summary stats if available + # Return total files (data + delete) from snapshot summary when available. summary = snapshot.get("summary", {}) - total_data_files = summary.get("total-data-files") - if total_data_files: - return int(total_data_files) + total_data_files = int(summary.get("total-data-files", 0) or 0) + total_delete_files = int(summary.get("total-delete-files", 0) or 0) + if total_data_files or total_delete_files: + return total_data_files + total_delete_files # If summary not available, would need to read manifest list # This is a simplified version - full implementation would parse manifests diff --git a/src/tablesleuth/utils/web_utils.py b/src/tablesleuth/utils/web_utils.py new file mode 100644 index 0000000..b255638 --- /dev/null +++ b/src/tablesleuth/utils/web_utils.py @@ -0,0 +1,39 @@ +"""Web UI utilities.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +def resolve_web_dir() -> Path | None: + """Resolve the web UI directory in priority order. + + Priority: + 1. TABLESLEUTH_WEB_UI_DIR env var + 2. Installed package: /web + 3. Dev build: /web-ui/out + + Returns: + Path to web directory if found, else None. + """ + # 1. Env var override + env_dir = os.getenv("TABLESLEUTH_WEB_UI_DIR") + if env_dir: + p = Path(env_dir) + if p.is_dir(): + return p + + # 2. Installed package location + # Navigate from utils/web_utils.py -> utils -> tablesleuth -> web + pkg_web = Path(__file__).parent.parent / "web" + if pkg_web.is_dir() and (pkg_web / "index.html").exists(): + return pkg_web + + # 3. Dev build output (repo checkout) + # Navigate from utils/web_utils.py -> utils -> tablesleuth -> src -> repo_root -> web-ui/out + dev_web = Path(__file__).parent.parent.parent.parent / "web-ui" / "out" + if dev_web.is_dir() and (dev_web / "index.html").exists(): + return dev_web + + return None diff --git a/src/tablesleuth/web/index.html b/src/tablesleuth/web/index.html new file mode 100644 index 0000000..a5333a5 --- /dev/null +++ b/src/tablesleuth/web/index.html @@ -0,0 +1 @@ +TableSleuth
diff --git a/tablesleuth.toml b/tablesleuth.toml index 8753e99..7d01663 100644 --- a/tablesleuth.toml +++ b/tablesleuth.toml @@ -1,37 +1,8 @@ -# Table Sleuth Configuration - [catalog] -# Default catalog for Iceberg tables. default = "glue" [gizmosql] -# GizmoSQL connection settings for column profiling and Iceberg performance testing -# -# IMPORTANT: These credentials should match those in resources/config.json -# used for EC2 deployment. Update both files if you change the username or password. - uri = "grpc+tls://localhost:31337" username = "gizmosql_username" password = "gizmosql_password" tls_skip_verify = true - -# Note: Do NOT configure local_data_path or docker_data_path -# These are legacy settings for Docker deployments which are no longer recommended - -# To start GizmoSQL: -# If using the AWS CDK deployment, GizmoSQL is installed automatically -# and an alias is created that allows for a single command to startup the server with -# the correct params. -# -# $ gizmosvr -# -# Alternatively, the gizmosql_server can be started manually after installation: -# $ gizmosql_server -P gizmosql_password -Q -T ~/.certs/cert0.pem ~/.certs/cert0.key -# (Default port is 31337, -Q enables query printing, -T enables TLS with self-signed certs) -# -# Installation (can use more recent versions): -# macOS (ARM64): curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.10/gizmosql_cli_macos_arm64.zip | sudo unzip -o -d /usr/local/bin - -# macOS (Intel): curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.10/gizmosql_cli_macos_amd64.zip | sudo unzip -o -d /usr/local/bin - -# Linux: curl -L https://github.com/gizmodata/gizmosql/releases/download/v1.12.10/gizmosql_cli_linux_amd64.zip | sudo unzip -o -d /usr/local/bin - -# -# See docs/gizmosql-deployment.md for detailed setup instructions diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..38e8307 --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1 @@ +"""API smoke tests.""" diff --git a/tests/api/test_config_router.py b/tests/api/test_config_router.py new file mode 100644 index 0000000..5ce2cfb --- /dev/null +++ b/tests/api/test_config_router.py @@ -0,0 +1,41 @@ +"""Smoke tests for the Config API router.""" + +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_get_config() -> None: + """GET /api/config/ returns 200 with catalog and gizmosql keys.""" + response = client.get("/api/config/") + assert response.status_code == 200 + data = response.json() + assert "catalog" in data + assert "gizmosql" in data + assert "uri" in data["gizmosql"] + + +def test_config_status() -> None: + """GET /api/config/status returns 200 with required keys.""" + response = client.get("/api/config/status") + assert response.status_code == 200 + data = response.json() + assert "config_file" in data + assert "env_overrides" in data + assert "pyiceberg_yaml_exists" in data + + +def test_get_pyiceberg() -> None: + """GET /api/config/pyiceberg returns 200.""" + response = client.get("/api/config/pyiceberg") + assert response.status_code == 200 + data = response.json() + assert "exists" in data + assert "config" in data diff --git a/tests/api/test_delta_router.py b/tests/api/test_delta_router.py new file mode 100644 index 0000000..3e4bcbb --- /dev/null +++ b/tests/api/test_delta_router.py @@ -0,0 +1,45 @@ +"""Smoke tests for the Delta API router.""" + +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_load_nonexistent_table() -> None: + """POST /api/delta/load with nonexistent path returns 404 or 422.""" + response = client.post( + "/api/delta/load", + json={"path": "/nonexistent/delta/table"}, + ) + assert response.status_code in (404, 422, 500) + + +def test_load_no_body() -> None: + """POST /api/delta/load without required field returns 422.""" + response = client.post("/api/delta/load", json={}) + assert response.status_code == 422 + + +def test_versions_nonexistent() -> None: + """POST /api/delta/versions with nonexistent path returns error.""" + response = client.post( + "/api/delta/versions", + json={"path": "/nonexistent/delta/table"}, + ) + assert response.status_code in (404, 422, 500) + + +def test_forensics_nonexistent() -> None: + """POST /api/delta/forensics with nonexistent path returns error.""" + response = client.post( + "/api/delta/forensics", + json={"path": "/nonexistent/delta/table"}, + ) + assert response.status_code in (404, 422, 500) diff --git a/tests/api/test_gizmosql_router.py b/tests/api/test_gizmosql_router.py new file mode 100644 index 0000000..cc91a9a --- /dev/null +++ b/tests/api/test_gizmosql_router.py @@ -0,0 +1,42 @@ +"""Smoke tests for the GizmoSQL API router.""" + +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_status_returns_200() -> None: + """GET /api/gizmosql/status always returns 200 (connected or not).""" + response = client.get("/api/gizmosql/status") + assert response.status_code == 200 + data = response.json() + assert "connected" in data + + +def test_query_empty_sql() -> None: + """POST /api/gizmosql/query with empty SQL returns 422.""" + response = client.post("/api/gizmosql/query", json={"sql": ""}) + assert response.status_code == 422 + + +def test_query_no_body() -> None: + """POST /api/gizmosql/query without body returns 422.""" + response = client.post("/api/gizmosql/query", json={}) + assert response.status_code == 422 + + +def test_profile_missing_columns() -> None: + """POST /api/gizmosql/profile without columns returns 422.""" + response = client.post( + "/api/gizmosql/profile", + json={"table_ref": "test_table"}, + ) + # columns is required — should return 422 + assert response.status_code in (422, 500) diff --git a/tests/api/test_iceberg_router.py b/tests/api/test_iceberg_router.py new file mode 100644 index 0000000..c55509c --- /dev/null +++ b/tests/api/test_iceberg_router.py @@ -0,0 +1,43 @@ +"""Smoke tests for the Iceberg API router.""" + +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_load_no_args() -> None: + """POST /api/iceberg/load without required args returns 404 or 422.""" + response = client.post("/api/iceberg/load", json={}) + # ValueError → 422 via exception handler + assert response.status_code in (404, 422, 500) + + +def test_load_missing_metadata() -> None: + """POST /api/iceberg/load with nonexistent metadata file returns 404.""" + response = client.post( + "/api/iceberg/load", + json={"metadata_path": "/nonexistent/metadata.json"}, + ) + assert response.status_code in (404, 422, 500) + + +def test_snapshots_no_args() -> None: + """POST /api/iceberg/snapshots without args returns error.""" + response = client.post("/api/iceberg/snapshots", json={}) + assert response.status_code in (404, 422, 500) + + +def test_compare_no_table() -> None: + """POST /api/iceberg/compare without table info returns error.""" + response = client.post( + "/api/iceberg/compare", + json={"snapshot_a_id": 1, "snapshot_b_id": 2}, + ) + assert response.status_code in (404, 422, 500) diff --git a/tests/api/test_main.py b/tests/api/test_main.py new file mode 100644 index 0000000..149e8d3 --- /dev/null +++ b/tests/api/test_main.py @@ -0,0 +1,31 @@ +"""Smoke tests for the TableSleuth FastAPI application.""" + +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_health() -> None: + """GET /api/health returns 200 with status ok.""" + response = client.get("/api/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" + assert "version" in data + assert data["version"] == "0.6.0" + + +def test_openapi_schema() -> None: + """GET /api/openapi.json returns 200.""" + response = client.get("/api/openapi.json") + assert response.status_code == 200 + schema = response.json() + assert "openapi" in schema + assert schema["info"]["title"] == "TableSleuth" diff --git a/tests/api/test_parquet_empty_file.py b/tests/api/test_parquet_empty_file.py new file mode 100644 index 0000000..5e2fdd0 --- /dev/null +++ b/tests/api/test_parquet_empty_file.py @@ -0,0 +1,95 @@ +"""Tests for Parquet sample endpoint with empty files.""" + +from pathlib import Path +from tempfile import TemporaryDirectory + +import pyarrow as pa +import pyarrow.parquet as pq +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_sample_empty_parquet_file() -> None: + """Test that /parquet/sample handles empty Parquet files gracefully.""" + with TemporaryDirectory() as tmpdir: + # Create an empty Parquet file with schema but no rows + schema = pa.schema([("id", pa.int64()), ("name", pa.string())]) + empty_table = pa.Table.from_pydict({"id": [], "name": []}, schema=schema) + + file_path = Path(tmpdir) / "empty.parquet" + pq.write_table(empty_table, file_path) + + # Request sample from empty file + response = client.post( + "/api/parquet/sample", json={"path": str(file_path), "num_rows": 100} + ) + + assert response.status_code == 200 + data = response.json() + + # Should return empty sample with schema + assert data["columns"] == ["id", "name"] + assert data["rows"] == [] + assert data["total_rows_in_file"] == 0 + assert data["sampled_rows"] == 0 + + +def test_sample_parquet_file_with_rows() -> None: + """Test that /parquet/sample still works correctly with non-empty files.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with data + schema = pa.schema([("id", pa.int64()), ("name", pa.string())]) + table = pa.Table.from_pydict( + {"id": [1, 2, 3, 4, 5], "name": ["a", "b", "c", "d", "e"]}, schema=schema + ) + + file_path = Path(tmpdir) / "data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 3}) + + assert response.status_code == 200 + data = response.json() + + # Should return first 3 rows + assert data["columns"] == ["id", "name"] + assert len(data["rows"]) == 3 + assert data["rows"][0] == [1, "a"] + assert data["rows"][1] == [2, "b"] + assert data["rows"][2] == [3, "c"] + assert data["total_rows_in_file"] == 5 + assert data["sampled_rows"] == 3 + + +def test_sample_parquet_file_fewer_rows_than_requested() -> None: + """Test sampling when file has fewer rows than requested.""" + with TemporaryDirectory() as tmpdir: + # Create a small Parquet file + schema = pa.schema([("value", pa.int64())]) + table = pa.Table.from_pydict({"value": [1, 2]}, schema=schema) + + file_path = Path(tmpdir) / "small.parquet" + pq.write_table(table, file_path) + + # Request more rows than available + response = client.post( + "/api/parquet/sample", json={"path": str(file_path), "num_rows": 100} + ) + + assert response.status_code == 200 + data = response.json() + + # Should return all available rows + assert data["columns"] == ["value"] + assert len(data["rows"]) == 2 + assert data["total_rows_in_file"] == 2 + assert data["sampled_rows"] == 2 diff --git a/tests/api/test_parquet_router.py b/tests/api/test_parquet_router.py new file mode 100644 index 0000000..b5f15e5 --- /dev/null +++ b/tests/api/test_parquet_router.py @@ -0,0 +1,45 @@ +"""Smoke tests for the Parquet API router.""" + +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_analyze_missing_path() -> None: + """POST /api/parquet/analyze with nonexistent path returns 404 or 500.""" + response = client.post( + "/api/parquet/analyze", + json={"path": "/nonexistent/path/that/does/not/exist"}, + ) + assert response.status_code in (404, 422, 500) + + +def test_analyze_no_body() -> None: + """POST /api/parquet/analyze without required field returns 422.""" + response = client.post("/api/parquet/analyze", json={}) + assert response.status_code == 422 + + +def test_file_info_missing() -> None: + """POST /api/parquet/file-info with nonexistent file returns 404 or 422.""" + response = client.post( + "/api/parquet/file-info", + json={"path": "/nonexistent/file.parquet"}, + ) + assert response.status_code in (404, 422, 500) + + +def test_sample_missing() -> None: + """POST /api/parquet/sample with nonexistent file returns non-200.""" + response = client.post( + "/api/parquet/sample", + json={"path": "/nonexistent/file.parquet"}, + ) + assert response.status_code != 200 diff --git a/tests/api/test_parquet_serialization.py b/tests/api/test_parquet_serialization.py new file mode 100644 index 0000000..001fbff --- /dev/null +++ b/tests/api/test_parquet_serialization.py @@ -0,0 +1,202 @@ +"""Tests for Parquet sample endpoint serialization of complex types.""" + +from decimal import Decimal +from pathlib import Path +from tempfile import TemporaryDirectory + +import pyarrow as pa +import pyarrow.parquet as pq +import pytest + +pytest.importorskip("fastapi", reason="fastapi not installed; run with --extra web") +pytest.importorskip("httpx", reason="httpx not installed; run with --extra web") + +from fastapi.testclient import TestClient # noqa: E402 + +from tablesleuth.api.main import app # noqa: E402 + +client = TestClient(app, raise_server_exceptions=False) + + +def test_sample_parquet_with_decimal_columns() -> None: + """Test that Parquet files with Decimal columns are serialized correctly.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with Decimal column + schema = pa.schema([("id", pa.int64()), ("price", pa.decimal128(10, 2))]) + table = pa.Table.from_pydict( + { + "id": [1, 2, 3], + "price": [Decimal("19.99"), Decimal("29.99"), Decimal("39.99")], + }, + schema=schema, + ) + + file_path = Path(tmpdir) / "decimal_data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 10}) + + assert response.status_code == 200 + data = response.json() + + # Decimals should be serialized as strings + assert data["columns"] == ["id", "price"] + assert len(data["rows"]) == 3 + assert data["rows"][0] == [1, "19.99"] + assert data["rows"][1] == [2, "29.99"] + assert data["rows"][2] == [3, "39.99"] + + +def test_sample_parquet_with_binary_columns() -> None: + """Test that Parquet files with binary columns are serialized correctly.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with binary column + schema = pa.schema([("id", pa.int64()), ("data", pa.binary())]) + table = pa.Table.from_pydict({"id": [1, 2], "data": [b"hello", b"world"]}, schema=schema) + + file_path = Path(tmpdir) / "binary_data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 10}) + + assert response.status_code == 200 + data = response.json() + + # Binary data should be serialized as strings + assert data["columns"] == ["id", "data"] + assert len(data["rows"]) == 2 + # Binary values are converted to string representation + assert isinstance(data["rows"][0][1], str) + assert isinstance(data["rows"][1][1], str) + + +def test_sample_parquet_with_date_columns() -> None: + """Test that Parquet files with date columns are serialized correctly.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with date column + from datetime import date + + schema = pa.schema([("id", pa.int64()), ("created_date", pa.date32())]) + table = pa.Table.from_pydict( + {"id": [1, 2], "created_date": [date(2024, 1, 1), date(2024, 12, 31)]}, schema=schema + ) + + file_path = Path(tmpdir) / "date_data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 10}) + + assert response.status_code == 200 + data = response.json() + + # Dates should be serialized as strings + assert data["columns"] == ["id", "created_date"] + assert len(data["rows"]) == 2 + assert isinstance(data["rows"][0][1], str) + assert isinstance(data["rows"][1][1], str) + + +def test_sample_parquet_with_timestamp_columns() -> None: + """Test that Parquet files with timestamp columns are serialized correctly.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with timestamp column + from datetime import datetime + + schema = pa.schema([("id", pa.int64()), ("timestamp", pa.timestamp("us"))]) + table = pa.Table.from_pydict( + { + "id": [1, 2], + "timestamp": [datetime(2024, 1, 1, 12, 0, 0), datetime(2024, 12, 31, 23, 59, 59)], + }, + schema=schema, + ) + + file_path = Path(tmpdir) / "timestamp_data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 10}) + + assert response.status_code == 200 + data = response.json() + + # Timestamps should be serialized as strings + assert data["columns"] == ["id", "timestamp"] + assert len(data["rows"]) == 2 + assert isinstance(data["rows"][0][1], str) + assert isinstance(data["rows"][1][1], str) + + +def test_sample_parquet_with_mixed_types() -> None: + """Test that Parquet files with mixed column types are serialized correctly.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with various column types + schema = pa.schema( + [ + ("int_col", pa.int64()), + ("float_col", pa.float64()), + ("string_col", pa.string()), + ("bool_col", pa.bool_()), + ("decimal_col", pa.decimal128(10, 2)), + ] + ) + table = pa.Table.from_pydict( + { + "int_col": [1, 2], + "float_col": [1.5, 2.5], + "string_col": ["a", "b"], + "bool_col": [True, False], + "decimal_col": [Decimal("10.50"), Decimal("20.75")], + }, + schema=schema, + ) + + file_path = Path(tmpdir) / "mixed_data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 10}) + + assert response.status_code == 200 + data = response.json() + + # Check that all types are properly serialized + assert data["columns"] == ["int_col", "float_col", "string_col", "bool_col", "decimal_col"] + assert len(data["rows"]) == 2 + + # Primitives should remain as-is + assert data["rows"][0][0] == 1 # int + assert data["rows"][0][1] == 1.5 # float + assert data["rows"][0][2] == "a" # string + assert data["rows"][0][3] is True # bool + + # Decimal should be serialized as string + assert data["rows"][0][4] == "10.50" + assert data["rows"][1][4] == "20.75" + + +def test_sample_parquet_with_null_values() -> None: + """Test that null values are preserved correctly.""" + with TemporaryDirectory() as tmpdir: + # Create a Parquet file with null values + schema = pa.schema([("id", pa.int64()), ("value", pa.decimal128(10, 2))]) + table = pa.Table.from_pydict( + {"id": [1, 2, 3], "value": [Decimal("10.00"), None, Decimal("30.00")]}, schema=schema + ) + + file_path = Path(tmpdir) / "null_data.parquet" + pq.write_table(table, file_path) + + # Request sample + response = client.post("/api/parquet/sample", json={"path": str(file_path), "num_rows": 10}) + + assert response.status_code == 200 + data = response.json() + + # Null values should remain as None (null in JSON) + assert data["rows"][0] == [1, "10.00"] + assert data["rows"][1] == [2, None] + assert data["rows"][2] == [3, "30.00"] diff --git a/tests/unit/api/test_config_toml_escaping.py b/tests/unit/api/test_config_toml_escaping.py new file mode 100644 index 0000000..abc2161 --- /dev/null +++ b/tests/unit/api/test_config_toml_escaping.py @@ -0,0 +1,110 @@ +"""Tests for TOML escaping in config router.""" + +import tomllib +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + +from tablesleuth.api.routers.config import _write_toml +from tablesleuth.config import AppConfig, CatalogConfig, GizmoConfig + + +def test_write_toml_escapes_quotes() -> None: + """Test that double quotes in values are properly escaped.""" + cfg = AppConfig( + catalog=CatalogConfig(default="test_catalog"), + gizmosql=GizmoConfig( + uri="grpc://localhost:31337", + username='user"with"quotes', + password='pass"word', + tls_skip_verify=False, + ), + ) + + with TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "test.toml" + _write_toml(path, cfg) + + # Verify the file can be parsed back + content = path.read_text() + parsed = tomllib.loads(content) + + assert parsed["gizmosql"]["username"] == 'user"with"quotes' + assert parsed["gizmosql"]["password"] == 'pass"word' + + +def test_write_toml_escapes_backslashes() -> None: + """Test that backslashes in values are properly escaped.""" + cfg = AppConfig( + catalog=CatalogConfig(default="test_catalog"), + gizmosql=GizmoConfig( + uri="grpc://localhost:31337", + username=r"domain\user", + password=r"pass\word", + tls_skip_verify=False, + ), + ) + + with TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "test.toml" + _write_toml(path, cfg) + + # Verify the file can be parsed back + content = path.read_text() + parsed = tomllib.loads(content) + + assert parsed["gizmosql"]["username"] == r"domain\user" + assert parsed["gizmosql"]["password"] == r"pass\word" + + +def test_write_toml_escapes_combined_special_chars() -> None: + """Test that combinations of special characters are properly escaped.""" + cfg = AppConfig( + catalog=CatalogConfig(default="test_catalog"), + gizmosql=GizmoConfig( + uri="grpc://localhost:31337", + username=r'user\"with\both"', + password=r'complex\"pass"word\\', + tls_skip_verify=True, + ), + ) + + with TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "test.toml" + _write_toml(path, cfg) + + # Verify the file can be parsed back + content = path.read_text() + parsed = tomllib.loads(content) + + assert parsed["gizmosql"]["username"] == r'user\"with\both"' + assert parsed["gizmosql"]["password"] == r'complex\"pass"word\\' + assert parsed["gizmosql"]["tls_skip_verify"] is True + + +def test_write_toml_normal_values() -> None: + """Test that normal values without special characters work correctly.""" + cfg = AppConfig( + catalog=CatalogConfig(default="my_catalog"), + gizmosql=GizmoConfig( + uri="grpc+tls://example.com:31337", + username="normal_user", + password="normal_password", + tls_skip_verify=False, + ), + ) + + with TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "test.toml" + _write_toml(path, cfg) + + # Verify the file can be parsed back + content = path.read_text() + parsed = tomllib.loads(content) + + assert parsed["catalog"]["default"] == "my_catalog" + assert parsed["gizmosql"]["uri"] == "grpc+tls://example.com:31337" + assert parsed["gizmosql"]["username"] == "normal_user" + assert parsed["gizmosql"]["password"] == "normal_password" + assert parsed["gizmosql"]["tls_skip_verify"] is False diff --git a/tests/unit/api/test_serialization.py b/tests/unit/api/test_serialization.py new file mode 100644 index 0000000..f874830 --- /dev/null +++ b/tests/unit/api/test_serialization.py @@ -0,0 +1,241 @@ +"""Tests for API serialization utilities.""" + +from dataclasses import dataclass + +import pytest + +from tablesleuth.api.serialization import JS_MAX_SAFE_INT, to_dict + + +@dataclass +class SimpleDataclass: + """Simple dataclass for testing.""" + + name: str + value: int + + +@dataclass +class NestedDataclass: + """Nested dataclass for testing.""" + + simple: SimpleDataclass + items: list[int] + metadata: dict[str, str] + + +@dataclass +class DataclassWithProperty: + """Dataclass with computed property.""" + + base_value: int + + @property + def computed_value(self) -> int: + """Computed property.""" + return self.base_value * 2 + + +@dataclass +class DataclassWithSkippableField: + """Dataclass with field that should be skipped.""" + + name: str + native_object: object # Non-serializable field + + +def test_to_dict_simple_dataclass() -> None: + """Test basic dataclass serialization.""" + obj = SimpleDataclass(name="test", value=42) + result = to_dict(obj) + + assert result == {"name": "test", "value": 42} + assert isinstance(result, dict) + + +def test_to_dict_nested_dataclass() -> None: + """Test nested dataclass serialization.""" + obj = NestedDataclass( + simple=SimpleDataclass(name="nested", value=100), + items=[1, 2, 3], + metadata={"key": "value"}, + ) + result = to_dict(obj) + + assert result == { + "simple": {"name": "nested", "value": 100}, + "items": [1, 2, 3], + "metadata": {"key": "value"}, + } + + +def test_to_dict_list() -> None: + """Test list serialization.""" + objs = [ + SimpleDataclass(name="first", value=1), + SimpleDataclass(name="second", value=2), + ] + result = to_dict(objs) + + assert result == [ + {"name": "first", "value": 1}, + {"name": "second", "value": 2}, + ] + + +def test_to_dict_dict() -> None: + """Test dict serialization.""" + obj = { + "a": SimpleDataclass(name="test", value=1), + "b": [1, 2, 3], + } + result = to_dict(obj) + + assert result == { + "a": {"name": "test", "value": 1}, + "b": [1, 2, 3], + } + + +def test_to_dict_primitives() -> None: + """Test that primitives pass through unchanged.""" + assert to_dict("string") == "string" + assert to_dict(42) == 42 + assert to_dict(3.14) == 3.14 + assert to_dict(True) is True + assert to_dict(None) is None + + +def test_to_dict_skip_fields() -> None: + """Test skipping specific fields.""" + obj = DataclassWithSkippableField(name="test", native_object=object()) + result = to_dict(obj, skip_fields={"native_object"}) + + assert result == {"name": "test"} + assert "native_object" not in result + + +def test_to_dict_include_properties() -> None: + """Test including @property values.""" + obj = DataclassWithProperty(base_value=10) + + # Without include_properties + result_without = to_dict(obj, include_properties=False) + assert result_without == {"base_value": 10} + assert "computed_value" not in result_without + + # With include_properties + result_with = to_dict(obj, include_properties=True) + assert result_with == {"base_value": 10, "computed_value": 20} + + +def test_to_dict_safe_int_threshold() -> None: + """Test converting large integers to strings.""" + obj = SimpleDataclass(name="test", value=JS_MAX_SAFE_INT + 1) + + # Without threshold + result_without = to_dict(obj) + assert result_without["value"] == JS_MAX_SAFE_INT + 1 + assert isinstance(result_without["value"], int) + + # With threshold + result_with = to_dict(obj, safe_int_threshold=JS_MAX_SAFE_INT) + assert result_with["value"] == str(JS_MAX_SAFE_INT + 1) + assert isinstance(result_with["value"], str) + + +def test_to_dict_safe_int_threshold_negative() -> None: + """Test converting large negative integers to strings.""" + obj = SimpleDataclass(name="test", value=-(JS_MAX_SAFE_INT + 1)) + + result = to_dict(obj, safe_int_threshold=JS_MAX_SAFE_INT) + assert result["value"] == str(-(JS_MAX_SAFE_INT + 1)) + assert isinstance(result["value"], str) + + +def test_to_dict_safe_int_threshold_within_range() -> None: + """Test that integers within threshold remain as integers.""" + obj = SimpleDataclass(name="test", value=JS_MAX_SAFE_INT) + + result = to_dict(obj, safe_int_threshold=JS_MAX_SAFE_INT) + assert result["value"] == JS_MAX_SAFE_INT + assert isinstance(result["value"], int) + + +def test_to_dict_safe_int_threshold_boolean() -> None: + """Test that booleans are not converted to strings.""" + + # Booleans are instances of int in Python, but should not be converted + @dataclass + class BoolDataclass: + flag: bool + + obj = BoolDataclass(flag=True) + result = to_dict(obj, safe_int_threshold=0) # Very low threshold + + assert result["flag"] is True + assert isinstance(result["flag"], bool) + + +def test_to_dict_combined_options() -> None: + """Test using multiple options together.""" + + @dataclass + class ComplexDataclass: + name: str + large_id: int + native_obj: object + + @property + def computed(self) -> str: + return f"{self.name}_computed" + + obj = ComplexDataclass( + name="test", + large_id=JS_MAX_SAFE_INT + 100, + native_obj=object(), + ) + + result = to_dict( + obj, + skip_fields={"native_obj"}, + include_properties=True, + safe_int_threshold=JS_MAX_SAFE_INT, + ) + + assert result == { + "name": "test", + "large_id": str(JS_MAX_SAFE_INT + 100), + "computed": "test_computed", + } + assert "native_obj" not in result + + +def test_to_dict_nested_with_options() -> None: + """Test that options propagate through nested structures.""" + + @dataclass + class Inner: + value: int + + @dataclass + class Outer: + inner: Inner + items: list[Inner] + + obj = Outer( + inner=Inner(value=JS_MAX_SAFE_INT + 1), + items=[Inner(value=JS_MAX_SAFE_INT + 2), Inner(value=100)], + ) + + result = to_dict(obj, safe_int_threshold=JS_MAX_SAFE_INT) + + assert result["inner"]["value"] == str(JS_MAX_SAFE_INT + 1) + assert result["items"][0]["value"] == str(JS_MAX_SAFE_INT + 2) + assert result["items"][1]["value"] == 100 # Within threshold + + +def test_js_max_safe_int_constant() -> None: + """Test that JS_MAX_SAFE_INT constant is correct.""" + assert JS_MAX_SAFE_INT == 9007199254740991 + assert JS_MAX_SAFE_INT == (1 << 53) - 1 diff --git a/tests/unit/profiling/test_gizmo_delta_storage_options.py b/tests/unit/profiling/test_gizmo_delta_storage_options.py new file mode 100644 index 0000000..60e9e9e --- /dev/null +++ b/tests/unit/profiling/test_gizmo_delta_storage_options.py @@ -0,0 +1,110 @@ +"""Tests for Delta table registration with storage options.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from tablesleuth.services.profiling.gizmo_duckdb import GizmoDuckDbProfiler + + +class TestDeltaStorageOptions: + """Tests for register_delta_table_with_version with storage_options.""" + + @patch("deltalake.DeltaTable") + def test_register_delta_without_storage_options(self, mock_delta_table: MagicMock) -> None: + """Test registering Delta table without storage options.""" + mock_dt = MagicMock() + mock_dt.file_uris.return_value = ["s3://bucket/file1.parquet", "s3://bucket/file2.parquet"] + mock_dt.get_add_actions.return_value.column.return_value.to_pylist.return_value = [ + 100, + 200, + ] + mock_delta_table.return_value = mock_dt + + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + profiler.register_delta_table_with_version("test_table", "/path/to/delta", version=1) + + # Verify DeltaTable was called with correct arguments + mock_delta_table.assert_called_once_with("/path/to/delta", version=1) + assert "test_table" in profiler._delta_tables + assert len(profiler._delta_tables["test_table"]) == 2 + + @patch("deltalake.DeltaTable") + def test_register_delta_with_storage_options(self, mock_delta_table: MagicMock) -> None: + """Test registering Delta table with storage options.""" + mock_dt = MagicMock() + mock_dt.file_uris.return_value = ["s3://bucket/file1.parquet"] + mock_dt.get_add_actions.return_value.column.return_value.to_pylist.return_value = [100] + mock_delta_table.return_value = mock_dt + + storage_opts = { + "AWS_ACCESS_KEY_ID": "test_key", + "AWS_SECRET_ACCESS_KEY": "test_secret", + "AWS_REGION": "us-west-2", + } + + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + profiler.register_delta_table_with_version( + "test_table", "s3://bucket/delta", version=2, storage_options=storage_opts + ) + + # Verify DeltaTable was called with storage_options + mock_delta_table.assert_called_once_with( + "s3://bucket/delta", version=2, storage_options=storage_opts + ) + assert "test_table" in profiler._delta_tables + + @patch("deltalake.DeltaTable") + def test_register_delta_latest_version_with_storage_options( + self, mock_delta_table: MagicMock + ) -> None: + """Test registering Delta table at latest version with storage options.""" + mock_dt = MagicMock() + mock_dt.file_uris.return_value = ["s3://bucket/file1.parquet"] + mock_dt.get_add_actions.return_value.column.return_value.to_pylist.return_value = [100] + mock_delta_table.return_value = mock_dt + + storage_opts = {"AWS_REGION": "eu-west-1"} + + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + profiler.register_delta_table_with_version( + "test_table", "s3://bucket/delta", storage_options=storage_opts + ) + + # Verify DeltaTable was called without version but with storage_options + mock_delta_table.assert_called_once_with("s3://bucket/delta", storage_options=storage_opts) + assert "test_table" in profiler._delta_tables + + @patch("deltalake.DeltaTable") + def test_register_delta_collects_stats(self, mock_delta_table: MagicMock) -> None: + """Test that stats are collected from Delta table.""" + mock_dt = MagicMock() + mock_dt.file_uris.return_value = ["file1.parquet", "file2.parquet", "file3.parquet"] + + # Mock get_add_actions to return stats + mock_actions = MagicMock() + mock_actions.column.side_effect = lambda col: ( + MagicMock(to_pylist=lambda: [1000, 2000, 3000]) + if col == "size_bytes" + else MagicMock(to_pylist=lambda: [100, 200, 300]) + ) + mock_dt.get_add_actions.return_value = mock_actions + mock_delta_table.return_value = mock_dt + + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + profiler.register_delta_table_with_version("test_table", "/path/to/delta", version=5) + + # Verify stats were collected + assert "test_table" in profiler._delta_table_stats + file_count, total_bytes, total_rows = profiler._delta_table_stats["test_table"] + assert file_count == 3 + assert total_bytes == 6000 # 1000 + 2000 + 3000 + assert total_rows == 600 # 100 + 200 + 300 diff --git a/tests/unit/profiling/test_gizmo_duckdb_unit.py b/tests/unit/profiling/test_gizmo_duckdb_unit.py index e934212..d7f0ae2 100644 --- a/tests/unit/profiling/test_gizmo_duckdb_unit.py +++ b/tests/unit/profiling/test_gizmo_duckdb_unit.py @@ -1,5 +1,6 @@ """Unit tests for GizmoDuckDbProfiler (mocked, no server required).""" +import sys from unittest.mock import Mock, patch import pytest @@ -19,9 +20,13 @@ def test_clean_file_uri(self): """Test removing file:// prefix.""" assert _clean_file_path("file:///path/to/file.parquet") == "/path/to/file.parquet" + @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific path handling") def test_clean_file_uri_windows(self): - """Test removing file:// prefix from Windows path.""" - assert _clean_file_path("file:///C:/path/to/file.parquet") == "/C:/path/to/file.parquet" + """Test removing file:// prefix from Windows path. + + Path.from_uri() on Windows converts file:///C:/path to C:/path (no leading slash). + """ + assert _clean_file_path("file:///C:/path/to/file.parquet") == "C:/path/to/file.parquet" def test_preserve_s3_path(self): """Test S3 paths are preserved.""" diff --git a/tests/unit/profiling/test_gizmo_iceberg_profile.py b/tests/unit/profiling/test_gizmo_iceberg_profile.py new file mode 100644 index 0000000..3b1f947 --- /dev/null +++ b/tests/unit/profiling/test_gizmo_iceberg_profile.py @@ -0,0 +1,205 @@ +"""Tests for Iceberg table profiling via GizmoDuckDB. + +Verifies that profile_single_column correctly uses registered Iceberg tables +by calling _replace_iceberg_tables to generate iceberg_scan() calls. +""" + +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from tablesleuth.services.profiling.gizmo_duckdb import GizmoDuckDbProfiler + + +@pytest.fixture +def profiler(): + """Create a GizmoDuckDbProfiler instance.""" + return GizmoDuckDbProfiler( + uri="motherduck://test_db", + username="test_user", + password="test_pass", + tls_skip_verify=True, + ) + + +def test_profile_single_column_uses_iceberg_registration(profiler): + """Test that profile_single_column uses registered Iceberg tables.""" + # Register an Iceberg table + table_id = "my_iceberg_table" + metadata_loc = "/path/to/metadata.json" + snapshot_id = 12345 + + profiler.register_iceberg_table_with_snapshot(table_id, metadata_loc, snapshot_id) + + # Mock the connection to verify the generated SQL + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=mock_conn) + mock_conn.__exit__ = MagicMock(return_value=False) + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) + + # First query checks column type + mock_cursor.fetchone.side_effect = [ + ("INTEGER",), # Type check result + (100, 95, 5, 50, 1, 100, 50.5, 50, 25, 75, 28.87, 833.33), # Stats result + (42, 10), # Mode result + ] + + with patch.object(profiler, "_connect", return_value=mock_conn): + result = profiler.profile_single_column(table_id, "my_column") + + # Verify that iceberg_scan was used in the SQL queries (type check, stats, mode). + # INSTALL/LOAD iceberg run first per connection block, so filter to profiling queries only. + calls = mock_cursor.execute.call_args_list + iceberg_queries = [call[0][0] for call in calls if "iceberg_scan" in call[0][0]] + assert len(iceberg_queries) == 3, f"Expected 3 iceberg_scan queries, got {len(iceberg_queries)}" + + for sql in iceberg_queries: + assert metadata_loc.replace("'", "''") in sql + assert f"version => {snapshot_id}" in sql + + +def test_profile_single_column_uses_delta_registration(profiler): + """Test that profile_single_column uses registered Delta tables.""" + # Register a Delta table + table_id = "my_delta_table" + file_uris = ["/path/to/file1.parquet", "/path/to/file2.parquet"] + + # Manually set up Delta table registration (since we don't have the full method here) + profiler._delta_tables[table_id] = file_uris + + # Mock the connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=mock_conn) + mock_conn.__exit__ = MagicMock(return_value=False) + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) + + mock_cursor.fetchone.side_effect = [ + ("VARCHAR",), # Type check result + (100, 95, 5, 50, "a", "z"), # Stats result (non-numeric) + ("mode_val", 10), # Mode result + ] + + with patch.object(profiler, "_connect", return_value=mock_conn): + result = profiler.profile_single_column(table_id, "my_column") + + # Verify that read_parquet was used with the file URIs + calls = mock_cursor.execute.call_args_list + assert len(calls) == 3 + + for call in calls: + sql = call[0][0] + assert "read_parquet" in sql + # Check that file URIs are present + assert any(uri.replace("'", "''") in sql for uri in file_uris) + + +def test_profile_single_column_fallback_to_bare_table_name(profiler): + """Test that profile_single_column falls back to bare table name when no registration exists.""" + table_name = "regular_table" + + # Mock the connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=mock_conn) + mock_conn.__exit__ = MagicMock(return_value=False) + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) + + mock_cursor.fetchone.side_effect = [ + ("INTEGER",), + (100, 95, 5, 50, 1, 100, 50.5, 50, 25, 75, 28.87, 833.33), + (42, 10), + ] + + with patch.object(profiler, "_connect", return_value=mock_conn): + result = profiler.profile_single_column(table_name, "my_column") + + # Verify that the bare table name is used (no scan functions) + calls = mock_cursor.execute.call_args_list + for call in calls: + sql = call[0][0] + assert "iceberg_scan" not in sql + assert table_name in sql + + +def test_profile_single_column_with_view_paths(profiler): + """Test that profile_single_column prefers _view_paths over Iceberg registration.""" + view_name = "my_view" + file_paths = ["/path/to/file.parquet"] + + # Set up view paths + profiler._view_paths = {view_name: file_paths} + + # Also register as Iceberg (should be ignored in favor of _view_paths) + profiler.register_iceberg_table_with_snapshot(view_name, "/metadata.json", 123) + + # Mock the connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=mock_conn) + mock_conn.__exit__ = MagicMock(return_value=False) + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) + + mock_cursor.fetchone.side_effect = [ + ("INTEGER",), + (100, 95, 5, 50, 1, 100, 50.5, 50, 25, 75, 28.87, 833.33), + (42, 10), + ] + + with patch.object(profiler, "_connect", return_value=mock_conn): + result = profiler.profile_single_column(view_name, "my_column") + + # Verify that read_parquet with view paths is used, NOT iceberg_scan + calls = mock_cursor.execute.call_args_list + for call in calls: + sql = call[0][0] + assert "read_parquet" in sql + assert "iceberg_scan" not in sql + assert file_paths[0].replace("'", "''") in sql + + +def test_profile_columns_uses_iceberg_registration(profiler): + """Test that profile_columns correctly delegates to profile_single_column with Iceberg tables.""" + table_id = "iceberg_table" + profiler.register_iceberg_table_with_snapshot(table_id, "/metadata.json", 999) + + # Mock the connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=mock_conn) + mock_conn.__exit__ = MagicMock(return_value=False) + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) + + # Return results for two columns + mock_cursor.fetchone.side_effect = [ + # Column 1 + ("INTEGER",), + (100, 95, 5, 50, 1, 100, 50.5, 50, 25, 75, 28.87, 833.33), + (42, 10), + # Column 2 + ("VARCHAR",), + (100, 90, 10, 40, "a", "z"), + ("mode", 5), + ] + + with patch.object(profiler, "_connect", return_value=mock_conn): + results = profiler.profile_columns(table_id, ["col1", "col2"]) + + assert len(results) == 2 + assert "col1" in results + assert "col2" in results + + # Verify iceberg_scan was used in profiling queries (INSTALL/LOAD run first per block) + calls = mock_cursor.execute.call_args_list + iceberg_queries = [call[0][0] for call in calls if "iceberg_scan" in call[0][0]] + assert len(iceberg_queries) >= 3, ( + f"Expected at least 3 iceberg_scan queries, got {len(iceberg_queries)}" + ) diff --git a/tests/unit/profiling/test_gizmo_pattern_simplification.py b/tests/unit/profiling/test_gizmo_pattern_simplification.py new file mode 100644 index 0000000..d084fae --- /dev/null +++ b/tests/unit/profiling/test_gizmo_pattern_simplification.py @@ -0,0 +1,120 @@ +"""Tests for simplified table reference replacement pattern. + +Verifies that the single word-boundary pattern correctly handles both +bare and quoted table identifiers. +""" + +import pytest + +from tablesleuth.services.profiling.gizmo_duckdb import GizmoDuckDbProfiler + + +def test_replace_table_ref_bare_identifier(): + """Test replacement of bare table identifier.""" + query = "SELECT * FROM my_table WHERE x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT * FROM iceberg_scan('/path/to/metadata.json') WHERE x > 5" + + +def test_replace_table_ref_double_quoted(): + """Test replacement of double-quoted table identifier.""" + query = 'SELECT * FROM "my_table" WHERE x > 5' + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT * FROM \"iceberg_scan('/path/to/metadata.json')\" WHERE x > 5" + + +def test_replace_table_ref_single_quoted(): + """Test replacement of single-quoted table identifier.""" + query = "SELECT * FROM 'my_table' WHERE x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT * FROM 'iceberg_scan('/path/to/metadata.json')' WHERE x > 5" + + +def test_replace_table_ref_case_insensitive(): + """Test that replacement is case-insensitive.""" + query = "SELECT * FROM MY_TABLE WHERE x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT * FROM iceberg_scan('/path/to/metadata.json') WHERE x > 5" + + +def test_replace_table_ref_multiple_occurrences(): + """Test replacement of multiple occurrences of the same table.""" + query = "SELECT * FROM my_table t1 JOIN my_table t2 ON t1.id = t2.id" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + expected = "SELECT * FROM iceberg_scan('/path/to/metadata.json') t1 JOIN iceberg_scan('/path/to/metadata.json') t2 ON t1.id = t2.id" + assert result == expected + + +def test_replace_table_ref_no_partial_match(): + """Test that partial matches are not replaced (word boundary protection).""" + query = "SELECT * FROM my_table_extended WHERE x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + # Should NOT replace because my_table_extended contains my_table but is a different identifier + assert result == query + + +def test_replace_table_ref_with_special_chars(): + """Test replacement with table names containing special regex characters.""" + query = "SELECT * FROM my.table WHERE x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my.table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT * FROM iceberg_scan('/path/to/metadata.json') WHERE x > 5" + + +def test_replace_table_ref_mixed_quotes(): + """Test replacement with mixed quote styles in the same query.""" + query = 'SELECT * FROM "my_table" t1 JOIN my_table t2 ON t1.id = t2.id' + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + expected = "SELECT * FROM \"iceberg_scan('/path/to/metadata.json')\" t1 JOIN iceberg_scan('/path/to/metadata.json') t2 ON t1.id = t2.id" + assert result == expected + + +def test_replace_table_ref_in_subquery(): + """Test replacement in subqueries.""" + query = "SELECT * FROM (SELECT * FROM my_table) AS sub" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT * FROM (SELECT * FROM iceberg_scan('/path/to/metadata.json')) AS sub" + + +def test_replace_table_ref_with_alias(): + """Test replacement when table has an alias.""" + query = "SELECT t.* FROM my_table AS t WHERE t.x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "SELECT t.* FROM iceberg_scan('/path/to/metadata.json') AS t WHERE t.x > 5" + + +def test_replace_table_ref_empty_query(): + """Test replacement with empty query.""" + query = "" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == "" + + +def test_replace_table_ref_no_match(): + """Test replacement when table name is not in query.""" + query = "SELECT * FROM other_table WHERE x > 5" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('/path/to/metadata.json')" + ) + assert result == query diff --git a/tests/unit/profiling/test_gizmo_scalar_conversion.py b/tests/unit/profiling/test_gizmo_scalar_conversion.py new file mode 100644 index 0000000..8e57afe --- /dev/null +++ b/tests/unit/profiling/test_gizmo_scalar_conversion.py @@ -0,0 +1,438 @@ +"""Tests for scalar query result conversion in _supplement_metrics.""" + +from decimal import Decimal + +import pytest + +from tablesleuth.models.iceberg import QueryPerformanceMetrics +from tablesleuth.services.profiling.gizmo_duckdb import GizmoDuckDbProfiler + + +class TestScalarConversion: + """Tests for rows_returned inference from scalar query results.""" + + def test_supplement_metrics_with_int_result(self) -> None: + """Test that integer COUNT results are handled correctly.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + # Mock metrics with zero rows_returned + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result with int from COUNT query + results = [[42]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + assert metrics.rows_returned == 42 + + def test_supplement_metrics_with_sum_aggregate(self) -> None: + """Test that SUM aggregates are not misinterpreted as row counts.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result with large value from SUM query + results = [[5000000]] + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT SUM(price) FROM table", results + ) + + # Should be 1 (one result row), not 5000000 + assert metrics.rows_returned == 1 + + def test_supplement_metrics_with_avg_aggregate(self) -> None: + """Test that AVG aggregates are not misinterpreted as row counts.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result from AVG query + results = [[42.5]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT AVG(age) FROM table", results) + + # Should be 1 (one result row) + assert metrics.rows_returned == 1 + + def test_supplement_metrics_with_max_aggregate(self) -> None: + """Test that MAX aggregates are not misinterpreted as row counts.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result from MAX query + results = [[999999]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT MAX(id) FROM table", results) + + # Should be 1 (one result row) + assert metrics.rows_returned == 1 + + def test_supplement_metrics_with_unreasonably_large_count(self) -> None: + """Test that large COUNT values (>1B) are accepted for data warehouse tables.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Large value (5B rows) - valid for data warehouse tables (Iceberg, Delta Lake) + results = [[5_000_000_000]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + # Should accept the large count value + assert metrics.rows_returned == 5_000_000_000 + + def test_supplement_metrics_with_float_result(self) -> None: + """Test that float COUNT results are converted to int.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result with float + results = [[123.0]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + assert metrics.rows_returned == 123 + + def test_supplement_metrics_with_decimal_result(self) -> None: + """Test that Decimal COUNT results are converted to int.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result with Decimal (common from some SQL engines) + results = [[Decimal("999")]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + assert metrics.rows_returned == 999 + + def test_supplement_metrics_with_string_numeric_result(self) -> None: + """Test that string numeric results are converted to int.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result with string number + results = [["456"]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + assert metrics.rows_returned == 456 + + def test_supplement_metrics_with_non_numeric_result(self) -> None: + """Test that non-numeric scalar results default to 1.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single-cell result with non-numeric value + results = [["some_string"]] + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT name FROM table LIMIT 1", results + ) + + # Should default to 1 for non-numeric scalar + assert metrics.rows_returned == 1 + + def test_supplement_metrics_with_multiple_rows(self) -> None: + """Test that multi-row results use row count.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Multiple rows + results = [[1, "a"], [2, "b"], [3, "c"]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT id, name FROM table", results) + + assert metrics.rows_returned == 3 + + def test_supplement_metrics_with_multiple_columns(self) -> None: + """Test that single row with multiple columns uses row count.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Single row, multiple columns + results = [[1, "a", 100.5]] + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT id, name, value FROM table LIMIT 1", results + ) + + # Should use row count (1) not try to convert first cell + assert metrics.rows_returned == 1 + + def test_supplement_metrics_with_empty_results(self) -> None: + """Test that empty results don't crash.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Empty results + results = [] + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT * FROM table WHERE 1=0", results + ) + + # Should remain 0 + assert metrics.rows_returned == 0 + + def test_supplement_metrics_preserves_existing_rows_returned(self) -> None: + """Test that existing rows_returned is not overwritten.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=50, # Already set + memory_peak_mb=0, + ) + + # Results that would suggest different value + results = [[100]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + # Should preserve existing value + assert metrics.rows_returned == 50 + + def test_supplement_metrics_count_over_one_billion(self) -> None: + """Test that COUNT results over 1 billion are accepted for data warehouse tables.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # COUNT result of 5 billion rows (common for large Iceberg/Delta tables) + results = [[5_000_000_000]] + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT COUNT(*) FROM large_table", results + ) + + assert metrics.rows_returned == 5_000_000_000 + + def test_supplement_metrics_count_exactly_one_billion(self) -> None: + """Test that COUNT result of exactly 1 billion is accepted.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + results = [[1_000_000_000]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + assert metrics.rows_returned == 1_000_000_000 + + def test_supplement_metrics_count_ten_billion(self) -> None: + """Test that COUNT result of 10 billion is accepted.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + results = [[10_000_000_000]] + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT COUNT(*) FROM huge_table", results + ) + + assert metrics.rows_returned == 10_000_000_000 + + def test_supplement_metrics_count_negative_rejected(self) -> None: + """Test that negative COUNT results are rejected (invalid).""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=0, + bytes_scanned=0, + rows_scanned=0, + rows_returned=0, + memory_peak_mb=0, + ) + + # Negative count is invalid + results = [[-100]] + + metrics = profiler._supplement_metrics(mock_metrics, "SELECT COUNT(*) FROM table", results) + + # Should default to 1 (single result row) for invalid counts + assert metrics.rows_returned == 1 + + def test_supplement_metrics_scan_efficiency_with_large_count(self) -> None: + """Test that scan_efficiency is calculated correctly with large COUNT results.""" + profiler = GizmoDuckDbProfiler( + uri="grpc://localhost:31337", username="test", password="test" + ) + + # Simulate a full table scan with 5B rows + mock_metrics = QueryPerformanceMetrics( + execution_time_ms=100, + files_scanned=1000, + bytes_scanned=500_000_000_000, # 500 GB + rows_scanned=5_000_000_000, # 5B rows scanned + rows_returned=0, # Will be filled from results + memory_peak_mb=1024, + ) + + results = [[5_000_000_000]] # COUNT(*) returns 5B + + metrics = profiler._supplement_metrics( + mock_metrics, "SELECT COUNT(*) FROM large_table", results + ) + + assert metrics.rows_returned == 5_000_000_000 + assert metrics.rows_scanned == 5_000_000_000 + + # Scan efficiency should be 100% (all scanned rows returned) + # scan_efficiency = rows_returned / rows_scanned = 5B / 5B = 1.0 = 100% + # This is calculated elsewhere, but the metrics should support it + if metrics.rows_scanned > 0: + scan_efficiency = metrics.rows_returned / metrics.rows_scanned + assert scan_efficiency == 1.0 diff --git a/tests/unit/profiling/test_gizmo_table_replacement.py b/tests/unit/profiling/test_gizmo_table_replacement.py new file mode 100644 index 0000000..60ad0a6 --- /dev/null +++ b/tests/unit/profiling/test_gizmo_table_replacement.py @@ -0,0 +1,120 @@ +"""Tests for table reference replacement logic in GizmoDuckDbProfiler.""" + +import pytest + +from tablesleuth.services.profiling.gizmo_duckdb import GizmoDuckDbProfiler + + +class TestReplaceTableRef: + """Tests for the _replace_table_ref helper method.""" + + def test_replace_bare_identifier(self) -> None: + """Test replacing bare table identifier.""" + query = "SELECT * FROM my_table WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + assert result == "SELECT * FROM iceberg_scan('s3://bucket/metadata.json') WHERE id = 1" + + def test_replace_double_quoted_identifier(self) -> None: + """Test replacing double-quoted table identifier.""" + query = 'SELECT * FROM "my_table" WHERE id = 1' + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + # Quoted identifiers are replaced with the scan call wrapped in the same quotes + assert result == "SELECT * FROM \"iceberg_scan('s3://bucket/metadata.json')\" WHERE id = 1" + + def test_replace_single_quoted_identifier(self) -> None: + """Test replacing single-quoted table identifier.""" + query = "SELECT * FROM 'my_table' WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + # Quoted identifiers are replaced with the scan call wrapped in the same quotes + assert result == "SELECT * FROM 'iceberg_scan('s3://bucket/metadata.json')' WHERE id = 1" + + def test_replace_case_insensitive(self) -> None: + """Test that replacement is case-insensitive.""" + query = "SELECT * FROM MY_TABLE WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + assert result == "SELECT * FROM iceberg_scan('s3://bucket/metadata.json') WHERE id = 1" + + def test_replace_multiple_occurrences(self) -> None: + """Test replacing multiple occurrences of the same table.""" + query = "SELECT * FROM my_table t1 JOIN my_table t2 ON t1.id = t2.parent_id" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + expected = ( + "SELECT * FROM iceberg_scan('s3://bucket/metadata.json') t1 " + "JOIN iceberg_scan('s3://bucket/metadata.json') t2 ON t1.id = t2.parent_id" + ) + assert result == expected + + def test_replace_with_special_chars_in_table_name(self) -> None: + """Test replacing table names with special regex characters.""" + query = "SELECT * FROM my.table WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my.table", "iceberg_scan('s3://bucket/metadata.json')" + ) + assert result == "SELECT * FROM iceberg_scan('s3://bucket/metadata.json') WHERE id = 1" + + def test_no_replacement_for_partial_match(self) -> None: + """Test that partial matches are not replaced.""" + query = "SELECT * FROM my_table_extended WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + # Should not replace because my_table_extended is not an exact match + assert result == "SELECT * FROM my_table_extended WHERE id = 1" + + def test_replace_with_read_parquet(self) -> None: + """Test replacing with read_parquet function.""" + query = "SELECT * FROM delta_table WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, + "delta_table", + "read_parquet(['s3://bucket/file1.parquet', 's3://bucket/file2.parquet'])", + ) + assert ( + result + == "SELECT * FROM read_parquet(['s3://bucket/file1.parquet', 's3://bucket/file2.parquet']) WHERE id = 1" + ) + + def test_replace_with_snapshot_version(self) -> None: + """Test replacing with iceberg_scan with version parameter.""" + query = "SELECT COUNT(*) FROM snapshot_a" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "snapshot_a", "iceberg_scan('s3://bucket/metadata.json', version => 123)" + ) + assert ( + result + == "SELECT COUNT(*) FROM iceberg_scan('s3://bucket/metadata.json', version => 123)" + ) + + def test_replace_in_complex_query(self) -> None: + """Test replacement in a complex query with multiple clauses.""" + query = """ + SELECT t1.id, t2.name + FROM my_table t1 + LEFT JOIN my_table t2 ON t1.parent_id = t2.id + WHERE t1.status = 'active' + ORDER BY t1.id + """ + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + assert "iceberg_scan('s3://bucket/metadata.json') t1" in result + assert "iceberg_scan('s3://bucket/metadata.json') t2" in result + assert "my_table" not in result + + def test_no_replacement_when_table_not_present(self) -> None: + """Test that query is unchanged when table is not present.""" + query = "SELECT * FROM other_table WHERE id = 1" + result = GizmoDuckDbProfiler._replace_table_ref( + query, "my_table", "iceberg_scan('s3://bucket/metadata.json')" + ) + assert result == query diff --git a/tests/unit/utils/test_web_utils.py b/tests/unit/utils/test_web_utils.py new file mode 100644 index 0000000..ed70072 --- /dev/null +++ b/tests/unit/utils/test_web_utils.py @@ -0,0 +1,48 @@ +"""Tests for web utilities.""" + +import os +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from tablesleuth.utils.web_utils import resolve_web_dir + + +def test_resolve_web_dir_env_var() -> None: + """Test that TABLESLEUTH_WEB_UI_DIR env var takes priority.""" + with TemporaryDirectory() as tmpdir: + web_dir = Path(tmpdir) / "web" + web_dir.mkdir() + (web_dir / "index.html").write_text("") + + with patch.dict(os.environ, {"TABLESLEUTH_WEB_UI_DIR": str(web_dir)}): + result = resolve_web_dir() + assert result == web_dir + + +def test_resolve_web_dir_env_var_nonexistent() -> None: + """Test that nonexistent env var path is skipped.""" + with patch.dict(os.environ, {"TABLESLEUTH_WEB_UI_DIR": "/nonexistent/path"}): + # Should fall through to other resolution methods + result = resolve_web_dir() + # Result depends on whether package or dev build exists + assert result is None or result.exists() + + +def test_resolve_web_dir_no_env_var() -> None: + """Test resolution without env var set.""" + with patch.dict(os.environ, {}, clear=True): + result = resolve_web_dir() + # Result depends on whether package or dev build exists + # Just verify it returns a Path or None + assert result is None or isinstance(result, Path) + + +def test_resolve_web_dir_returns_none_when_not_found() -> None: + """Test that None is returned when no web directory is found.""" + # Set env var to nonexistent path and ensure package/dev paths don't exist + with patch.dict(os.environ, {"TABLESLEUTH_WEB_UI_DIR": "/absolutely/nonexistent/path"}): + # Mock Path.is_dir to always return False + with patch("tablesleuth.utils.web_utils.Path.is_dir", return_value=False): + result = resolve_web_dir() + assert result is None diff --git a/uv.lock b/uv.lock index 16ac330..e49cfa8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,22 +1,30 @@ version = 1 revision = 3 requires-python = ">=3.13, <3.15" +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.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'", +] [[package]] name = "adbc-driver-flightsql" -version = "1.9.0" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "adbc-driver-manager" }, { name = "importlib-resources" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/8a/4d0c84b15eefe6576d431a18103005e356bca44b6a45fbc631cb6e8bb17a/adbc_driver_flightsql-1.9.0.tar.gz", hash = "sha256:534e125194b6e835245eb705246c6525a8d4046d23489140deeededdd6f848a9", size = 21215, upload-time = "2025-11-07T01:46:54.749Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/c3/a97fba4960b76b4b3a3055d8c33f6915e7d40a9c67f065fe760d9a17514a/adbc_driver_flightsql-1.10.0.tar.gz", hash = "sha256:aab737ee7c16d0ec89928ef2297c92f815756e91773085d55cc5eabbebcb9338", size = 24516, upload-time = "2026-01-09T07:13:44.793Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/ee/08baaea64b123ef1c904c6a25f63b1c95208c040d794425047cbcb6c2c3a/adbc_driver_flightsql-1.9.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:49c15fec4e03732bf478922035563be0665686b950efd8efb325b6ce84e05dd5", size = 7832201, upload-time = "2025-11-07T01:44:54.695Z" }, - { url = "https://files.pythonhosted.org/packages/2e/67/b301f1ff18e03124c62a6d29cabd01d1b122bea94d94035233316b44d03f/adbc_driver_flightsql-1.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:edee43183625acb2fd2e2b8331f07ac516b6958ef1f27663f6308334db7f7272", size = 7362111, upload-time = "2025-11-07T01:44:57.395Z" }, - { url = "https://files.pythonhosted.org/packages/04/e5/a2826ac6e8cb135d9a9f24d46e60563353fe387c542ee19b42dc4d043402/adbc_driver_flightsql-1.9.0-py3-none-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a79a4225de366f8c1f622f4ab92ad7cc09e874a89748c6d21003641a96de8482", size = 14609578, upload-time = "2025-11-07T01:45:00.995Z" }, - { url = "https://files.pythonhosted.org/packages/94/69/34cf90a32fe3925be6806ad7bf8c1d46d40ad8e16a7a3a36adb02ae65cfb/adbc_driver_flightsql-1.9.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c6ca16f2963297abb4816e1c0a0dc07852993e5a2ac1a3232536833153e6df8", size = 13583150, upload-time = "2025-11-07T01:45:06.276Z" }, - { url = "https://files.pythonhosted.org/packages/62/1a/bc7ec4153b1d00c0a7fbde9cf0a836600d954b362e19a994365710ade713/adbc_driver_flightsql-1.9.0-py3-none-win_amd64.whl", hash = "sha256:ccd438a02c1638db9131ba46eafb7b860df76756ecc7585317e72dd83162b712", size = 14435609, upload-time = "2025-11-07T01:45:10.361Z" }, + { url = "https://files.pythonhosted.org/packages/94/fb/c0d48ded0e75b61bbaff24ef52c89b97ba9b2fbc5caaeb9a102ab17f8f1d/adbc_driver_flightsql-1.10.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:a520579be3194e315f35c749afc9cb2ae9b9b7b852c8c2ac5fb9cafa31cdc0c6", size = 7922165, upload-time = "2026-01-09T07:11:35.077Z" }, + { url = "https://files.pythonhosted.org/packages/c8/55/c8bc08ea1e0ba3a35f6307528efa23f11745c5acb90981d2632f5d416659/adbc_driver_flightsql-1.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c6d6f5e93adcc87f41e70adc07a470f865f36f8dd1e6e9ab2b05855bc44274ca", size = 7366098, upload-time = "2026-01-09T07:11:37.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/ed/2cc8683b1f59d5c9c82aaf8f5992b41d19e5abc90393af6b882eab072773/adbc_driver_flightsql-1.10.0-py3-none-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dfee1c767281c9add95fcf29eac84338107a7610b2d9b19d1169e67083a3eaa", size = 14446811, upload-time = "2026-01-09T07:11:41.532Z" }, + { url = "https://files.pythonhosted.org/packages/b3/5d/1d4c235a04b349d8d5c89e6ca42e11a6b21e6959f48b5847523e87926b4b/adbc_driver_flightsql-1.10.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e6212fba5a2a59d7a2a71db1b036730908eb85457df3cc3c90563e4ddadaa923", size = 13257586, upload-time = "2026-01-09T07:11:45.63Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4d/55b96339f18c3932c61bd503c477fabfb7f6c95d4fc4ceb31a1e4275d4fd/adbc_driver_flightsql-1.10.0-py3-none-win_amd64.whl", hash = "sha256:6750c1def8c782469cc33dd883f5c9598086a875432a257adf0522bb1e3b95ca", size = 14231290, upload-time = "2026-01-09T07:11:49.683Z" }, ] [[package]] @@ -177,6 +185,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -186,6 +203,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + [[package]] name = "arro3-core" version = "0.6.5" @@ -309,7 +338,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -386,14 +415,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -471,7 +500,7 @@ name = "cryptography" version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } wheels = [ @@ -512,20 +541,20 @@ wheels = [ [[package]] name = "deltalake" -version = "1.3.2" +version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arro3-core" }, { name = "deprecated" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/54/6a0537aaba348d64eb88e72a84045494cb6a9970ae0b8ee65966859f9528/deltalake-1.3.2.tar.gz", hash = "sha256:ba3c569a6ac6e319487e47b6689c58ac2d51f74aab9c961e9b9d249b10a2fb6a", size = 5204561, upload-time = "2026-01-14T10:37:12.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/5f/095796d6e175103924989157bfab537efe41806b2b706b76df86051ded2c/deltalake-1.4.2.tar.gz", hash = "sha256:957e52624e1dcee35f0920868e3d15a1feab40fcd0fcd4682231a33da3d442da", size = 5246550, upload-time = "2026-02-09T03:15:57.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/30/4e218bf1c6b335d2e020442662d0f6596c3d15a1dd79d4e6402675599031/deltalake-1.3.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6dcf29c6cbbbd15ecabe0013c99586f88b517988f5a2c385494b1477f5c62fad", size = 36977633, upload-time = "2026-01-14T10:49:08.295Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7e/3349ac5d254c1507bf447e6f090f18fe499408cc1c9e837867355ebe1d93/deltalake-1.3.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5881372d4588e936d2a0b38e02d120f18633de2fbaf7188a26cfe8caf679ce73", size = 33846723, upload-time = "2026-01-14T10:52:33.952Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f9/45971281b33ec18227a7a5a7768e6b2ba60ccf3cbe732cba2e1777fda4e1/deltalake-1.3.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95048c15cc414c2d0eb06716839489b91989ec9b9bc68551ea83f2e10944b2d", size = 37738744, upload-time = "2026-01-14T10:37:09.751Z" }, - { url = "https://files.pythonhosted.org/packages/2c/94/7c237171296d40a60b82e948f7cb67162f7a4fed8b9ba4b7f606b2772031/deltalake-1.3.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:88ced2bbd7b08bc674d5e2fe1a8774465910c3d178a1756bfffd5065e73b22a5", size = 36424838, upload-time = "2026-01-14T10:28:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3e/65df78f786df2ac83fa92e68fbdc0ba05216ac894abc5b213d0b643a4375/deltalake-1.3.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dc7b5837b7d2ad0b59b5272eef09c4c0aaa4b10e62d290ab6ee3dc808b9be5d2", size = 37710838, upload-time = "2026-01-14T10:38:02.225Z" }, - { url = "https://files.pythonhosted.org/packages/ab/48/94b37fa3b4cf24c6ae5a171a08e6c531a3d014402eb1dbdc5f13bf63db08/deltalake-1.3.2-cp310-abi3-win_amd64.whl", hash = "sha256:f72131d7ddd8e25f84a53ac26028fbed143c66035639644d50603871cf606871", size = 40109156, upload-time = "2026-01-14T11:00:17.709Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/c2835de7bfbf810849cc6d48720574dd7f5ce308e5d11ab5d9bbaa8afb4e/deltalake-1.4.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5c88b856a74517abfc91ca667594e984648447ba96e65914db2190467caad9b9", size = 37692216, upload-time = "2026-02-09T03:36:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9b/ddb4adaec71b42c172905764d945b0ed01db1167cb59ec347f08d9456e31/deltalake-1.4.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4d4044a83e13a861c1d94eb5211dc40f41d089c9d6db9c08445d0438d346fe3", size = 34600691, upload-time = "2026-02-09T03:36:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ec/9aa7d0c52355d4664fcc1552a0be7535111f15003d2cd1cd8f225a256622/deltalake-1.4.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b6817e8beae1408fc90827c5c3687d5acf35ae9e43a0e71e5c517a6ec77272a", size = 38490480, upload-time = "2026-02-09T03:15:53.185Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/d70be4042a4818b906e9e1f55504b205eb94f48696b568b7e33a989d3cf1/deltalake-1.4.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c6e4797bb0b3e9fd62cbe41806f52f68658891528ee4b6a2073d16c2002bdcc8", size = 37146247, upload-time = "2026-02-09T03:06:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/ba/74/bc63508d0ea80b9cb52db9844ba044fd11a7dff235093eae74a13cc0ff25/deltalake-1.4.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f515fd0ecfcd87ba7dfa3fd94247e1fd8f0ce147e1bd78e2d7a32197ce5dbf17", size = 38486927, upload-time = "2026-02-09T03:16:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e0/e15a10c860e9a773bf0ac951a8bb8491a115a77e22ae52a5cbc1d13d9594/deltalake-1.4.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e3e6f858ddf843a3f6936d7261866a765408c253e31483172f8c28600cc55ff", size = 40774527, upload-time = "2026-02-09T03:39:48.583Z" }, ] [[package]] @@ -560,22 +589,69 @@ wheels = [ [[package]] name = "duckdb" -version = "1.4.2" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/99/ac6c105118751cc3ccae980b12e44847273f3402e647ec3197aff2251e23/duckdb-1.4.2.tar.gz", hash = "sha256:df81acee3b15ecb2c72eb8f8579fb5922f6f56c71f5c8892ea3bc6fab39aa2c4", size = 18469786, upload-time = "2025-11-12T13:18:04.203Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9d/ab66a06e416d71b7bdcb9904cdf8d4db3379ef632bb8e9495646702d9718/duckdb-1.4.4.tar.gz", hash = "sha256:8bba52fd2acb67668a4615ee17ee51814124223de836d9e2fdcbc4c9021b3d3c", size = 18419763, upload-time = "2026-01-26T11:50:37.68Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/c4/5054dbe79cf570b0c97db0c2eba7eb541cc561037360479059a3b57e4a32/duckdb-1.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de646227fc2c53101ac84e86e444e7561aa077387aca8b37052f3803ee690a17", size = 29015784, upload-time = "2025-11-12T13:17:14.409Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b8/97f4f07d9459f5d262751cccfb2f4256debb8fe5ca92370cebe21aab1ee2/duckdb-1.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1fac31babda2045d4cdefe6d0fd2ebdd8d4c2a333fbcc11607cfeaec202d18d", size = 15403788, upload-time = "2025-11-12T13:17:16.864Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ea/112f33ace03682bafd4aaf0a3336da689b9834663e7032b3d678fd2902c9/duckdb-1.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:43ac632f40ab1aede9b4ce3c09ea043f26f3db97b83c07c632c84ebd7f7c0f4a", size = 13733603, upload-time = "2025-11-12T13:17:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/34/83/8d6f845a9a946e8b47b6253b9edb084c45670763e815feed6cfefc957e89/duckdb-1.4.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77db030b48321bf785767b7b1800bf657dd2584f6df0a77e05201ecd22017da2", size = 18473725, upload-time = "2025-11-12T13:17:23.074Z" }, - { url = "https://files.pythonhosted.org/packages/82/29/153d1b4fc14c68e6766d7712d35a7ab6272a801c52160126ac7df681f758/duckdb-1.4.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a456adbc3459c9dcd99052fad20bd5f8ef642be5b04d09590376b2eb3eb84f5c", size = 20481971, upload-time = "2025-11-12T13:17:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/58/b7/8d3a58b5ebfb9e79ed4030a0f2fbd7e404c52602e977b1e7ab51651816c7/duckdb-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f7c61617d2b1da3da5d7e215be616ad45aa3221c4b9e2c4d1c28ed09bc3c1c4", size = 12330535, upload-time = "2025-11-12T13:17:29.175Z" }, - { url = "https://files.pythonhosted.org/packages/25/46/0f316e4d0d6bada350b9da06691a2537c329c8948c78e8b5e0c4874bc5e2/duckdb-1.4.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:422be8c6bdc98366c97f464b204b81b892bf962abeae6b0184104b8233da4f19", size = 29028616, upload-time = "2025-11-12T13:17:31.599Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/e04a8f97865251b544aee9501088d4f0cb8e8b37339bd465c0d33857d411/duckdb-1.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:459b1855bd06a226a2838da4f14c8863fd87a62e63d414a7f7f682a7c616511a", size = 15410382, upload-time = "2025-11-12T13:17:34.14Z" }, - { url = "https://files.pythonhosted.org/packages/47/ec/b8229517c2f9fe88a38bb1a172a2da4d0ff34996d319d74554fda80b6358/duckdb-1.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20c45b4ead1ea4d23a1be1cd4f1dfc635e58b55f0dd11e38781369be6c549903", size = 13737588, upload-time = "2025-11-12T13:17:36.515Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9a/63d26da9011890a5b893e0c21845c0c0b43c634bf263af3bbca64be0db76/duckdb-1.4.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e552451054534970dc999e69ca5ae5c606458548c43fb66d772117760485096", size = 18477886, upload-time = "2025-11-12T13:17:39.136Z" }, - { url = "https://files.pythonhosted.org/packages/23/35/b1fae4c5245697837f6f63e407fa81e7ccc7948f6ef2b124cd38736f4d1d/duckdb-1.4.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:128c97dab574a438d7c8d020670b21c68792267d88e65a7773667b556541fa9b", size = 20483292, upload-time = "2025-11-12T13:17:41.501Z" }, - { url = "https://files.pythonhosted.org/packages/25/5e/6f5ebaabc12c6db62f471f86b5c9c8debd57f11aa1b2acbbcc4c68683238/duckdb-1.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:dfcc56a83420c0dec0b83e97a6b33addac1b7554b8828894f9d203955591218c", size = 12830520, upload-time = "2025-11-12T13:17:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fe/64810fee20030f2bf96ce28b527060564864ce5b934b50888eda2cbf99dd/duckdb-1.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:337f8b24e89bc2e12dadcfe87b4eb1c00fd920f68ab07bc9b70960d6523b8bc3", size = 28899349, upload-time = "2026-01-26T11:49:40.294Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9b/3c7c5e48456b69365d952ac201666053de2700f5b0144a699a4dc6854507/duckdb-1.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0509b39ea7af8cff0198a99d206dca753c62844adab54e545984c2e2c1381616", size = 15350691, upload-time = "2026-01-26T11:49:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7b/64e68a7b857ed0340045501535a0da99ea5d9d5ea3708fec0afb8663eb27/duckdb-1.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fb94de6d023de9d79b7edc1ae07ee1d0b4f5fa8a9dcec799650b5befdf7aafec", size = 13672311, upload-time = "2026-01-26T11:49:46.069Z" }, + { url = "https://files.pythonhosted.org/packages/09/5b/3e7aa490841784d223de61beb2ae64e82331501bf5a415dc87a0e27b4663/duckdb-1.4.4-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d636ceda422e7babd5e2f7275f6a0d1a3405e6a01873f00d38b72118d30c10b", size = 18422740, upload-time = "2026-01-26T11:49:49.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/32/256df3dbaa198c58539ad94f9a41e98c2c8ff23f126b8f5f52c7dcd0a738/duckdb-1.4.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df7351328ffb812a4a289732f500d621e7de9942a3a2c9b6d4afcf4c0e72526", size = 20435578, upload-time = "2026-01-26T11:49:51.946Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/620323fd87062ea43e527a2d5ed9e55b525e0847c17d3b307094ddab98a2/duckdb-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:6fb1225a9ea5877421481d59a6c556a9532c32c16c7ae6ca8d127e2b878c9389", size = 12268083, upload-time = "2026-01-26T11:49:54.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/a397fdb7c95388ba9c055b9a3d38dfee92093f4427bc6946cf9543b1d216/duckdb-1.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:f28a18cc790217e5b347bb91b2cab27aafc557c58d3d8382e04b4fe55d0c3f66", size = 13006123, upload-time = "2026-01-26T11:49:57.092Z" }, + { url = "https://files.pythonhosted.org/packages/97/a6/f19e2864e651b0bd8e4db2b0c455e7e0d71e0d4cd2cd9cc052f518e43eb3/duckdb-1.4.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25874f8b1355e96178079e37312c3ba6d61a2354f51319dae860cf21335c3a20", size = 28909554, upload-time = "2026-01-26T11:50:00.107Z" }, + { url = "https://files.pythonhosted.org/packages/0e/93/8a24e932c67414fd2c45bed83218e62b73348996bf859eda020c224774b2/duckdb-1.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:452c5b5d6c349dc5d1154eb2062ee547296fcbd0c20e9df1ed00b5e1809089da", size = 15353804, upload-time = "2026-01-26T11:50:03.382Z" }, + { url = "https://files.pythonhosted.org/packages/62/13/e5378ff5bb1d4397655d840b34b642b1b23cdd82ae19599e62dc4b9461c9/duckdb-1.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e5c2d8a0452df55e092959c0bfc8ab8897ac3ea0f754cb3b0ab3e165cd79aff", size = 13676157, upload-time = "2026-01-26T11:50:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/24364da564b27aeebe44481f15bd0197a0b535ec93f188a6b1b98c22f082/duckdb-1.4.4-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af6e76fe8bd24875dc56dd8e38300d64dc708cd2e772f67b9fbc635cc3066a3", size = 18426882, upload-time = "2026-01-26T11:50:08.97Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/6ae31b2914b4dc34243279b2301554bcbc5f1a09ccc82600486c49ab71d1/duckdb-1.4.4-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0440f59e0cd9936a9ebfcf7a13312eda480c79214ffed3878d75947fc3b7d6d", size = 20435641, upload-time = "2026-01-26T11:50:12.188Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b1/fd5c37c53d45efe979f67e9bd49aaceef640147bb18f0699a19edd1874d6/duckdb-1.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:59c8d76016dde854beab844935b1ec31de358d4053e792988108e995b18c08e7", size = 12762360, upload-time = "2026-01-26T11:50:14.76Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2d/13e6024e613679d8a489dd922f199ef4b1d08a456a58eadd96dc2f05171f/duckdb-1.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:53cd6423136ab44383ec9955aefe7599b3fb3dd1fe006161e6396d8167e0e0d4", size = 13458633, upload-time = "2026-01-26T11:50:17.657Z" }, +] + +[[package]] +name = "fastapi" +version = "0.131.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/32/158cbf685b7d5a26f87131069da286bf10fc9fbf7fc968d169d48a45d689/fastapi-0.131.0.tar.gz", hash = "sha256:6531155e52bee2899a932c746c9a8250f210e3c3303a5f7b9f8a808bfe0548ff", size = 369612, upload-time = "2026-02-22T16:38:11.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/b58ec24c321acc2ad1327f69b033cadc005e0f26df9a73828c9e9c7db7ce/fastapi-0.131.0-py3-none-any.whl", hash = "sha256:ed0e53decccf4459de78837ce1b867cd04fa9ce4579497b842579755d20b405a", size = 103854, upload-time = "2026-02-22T16:38:09.814Z" }, +] + +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, ] [[package]] @@ -678,7 +754,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -689,7 +764,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -697,6 +771,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "hypothesis" version = "6.150.2" @@ -1268,42 +1401,46 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -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/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" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, ] [[package]] @@ -1438,38 +1575,38 @@ wheels = [ [[package]] name = "pyarrow" -version = "22.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, ] [[package]] @@ -1483,7 +1620,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1491,9 +1628,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] @@ -1560,7 +1697,7 @@ wheels = [ [[package]] name = "pyiceberg" -version = "0.10.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -1572,11 +1709,20 @@ dependencies = [ { name = "pyroaring" }, { name = "requests" }, { name = "rich" }, - { name = "sortedcontainers" }, { name = "strictyaml" }, { name = "tenacity" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/22/3d02ad39710bf51834d108e6d548cee9c1916850460ccba80db47a982567/pyiceberg-0.11.0.tar.gz", hash = "sha256:095bbafc87d204cf8d3ffc1c434e07cf9a67a709192ac0b11dcb0f8251f7ad4e", size = 1074873, upload-time = "2026-02-10T02:28:20.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/72/ef1e816d79d703eec1182398947a6b72f502eefeee01c4484bd5e1493b07/pyiceberg-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c707f4463dd9c1ca664d41d5ddd38babadf1bf5fa1946cb591c033a6a2827eb4", size = 532359, upload-time = "2026-02-10T02:28:11.473Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/ec85279b1b8ed57d0d27d4675203d314b8f5d69383e1df68f615f45e9dda/pyiceberg-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f1c944969fda799a2d26dc6f57448ace44ee07e334306ba6f5110df1aadeeef1", size = 532496, upload-time = "2026-02-10T02:28:13.19Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/02861c450057c9a6e2f2e1eb0ef735c2e28473cff60b2747c50d0427ec1c/pyiceberg-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1be075b9ecc175b8dd76822b081b379ce33cda33d6403eaf607268f6061f3275", size = 721917, upload-time = "2026-02-10T02:28:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/16/cf/924b7b14267d47f5055bb5d032c7d24eb9542ac3631b460e1398fe9935ea/pyiceberg-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3507d079d43d724bffb80e75201f2995822af844b674642dcf73c19d5303994", size = 723754, upload-time = "2026-02-10T02:28:15.77Z" }, + { url = "https://files.pythonhosted.org/packages/24/a1/df2d73af6dc3ee301e727d0bef4421c57de02b5030cf38e39ed25ef36154/pyiceberg-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb3719cd61a0512596b4306283072de443d84ec7b68654f565b0d7c2d7cdeeeb", size = 715749, upload-time = "2026-02-10T02:28:17.034Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0a/c3cdcd5ed417aceb2f73e8463d97e8dd7e3f7021015d0c8d51394a5c5a63/pyiceberg-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9a71fd6b1c3c625ed2a9ca2cecf0dc8713acc5814e78c9becde3b1f42315c35", size = 720600, upload-time = "2026-02-10T02:28:18.275Z" }, + { url = "https://files.pythonhosted.org/packages/01/b8/29ec7281fb831ab983f953b00924c1cc3ebc21e9f67a1466af9b63767ba4/pyiceberg-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:bed2df9eb7e1496af22fa2307dbd13f29865b98ba5851695ffd1f4436edc05f9", size = 530631, upload-time = "2026-02-10T02:28:19.561Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/0e/90e61c38504f4fbd5ed79631f85da7d5ea5e5bf997bdeaa65b28ebf04cab/pyiceberg-0.10.0.tar.gz", hash = "sha256:2525afa5e7e5fc4e72b291f8e1cc219e982d2bda5ff17e62cd05b8d91c4139f5", size = 842633, upload-time = "2025-09-11T14:59:34.044Z" } [package.optional-dependencies] glue = [ @@ -1684,12 +1830,21 @@ wheels = [ ] [[package]] -name = "pytz" -version = "2025.2" +name = "python-dotenv" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -1789,15 +1944,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] @@ -1857,8 +2012,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -1885,23 +2040,49 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.44" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, - { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, - { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, - { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, - { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, - { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] @@ -1927,7 +2108,7 @@ wheels = [ [[package]] name = "tablesleuth" -version = "0.5.3" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "adbc-driver-flightsql" }, @@ -1951,6 +2132,7 @@ dependencies = [ dev = [ { name = "bandit" }, { name = "build" }, + { name = "httpx" }, { name = "hypothesis" }, { name = "mypy" }, { name = "pre-commit" }, @@ -1963,39 +2145,50 @@ dev = [ { name = "types-pyyaml" }, { name = "types-toml" }, ] +web = [ + { name = "fastapi" }, + { name = "fastavro" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] [package.metadata] requires-dist = [ - { name = "adbc-driver-flightsql", specifier = ">=1.7.0" }, + { name = "adbc-driver-flightsql", specifier = ">=1.10.0" }, { name = "bandit", extras = ["toml"], marker = "extra == 'dev'", specifier = ">=1.8.6,<2.0.0" }, { name = "boto3", specifier = ">=1.35.0" }, { name = "build", marker = "extra == 'dev'", specifier = ">=1.0.0,<2.0.0" }, - { name = "click", specifier = ">=8.1.0" }, - { name = "deltalake", specifier = ">=0.22.0" }, - { name = "duckdb", specifier = ">=1.1.0" }, + { name = "click", specifier = ">=8.3.1" }, + { name = "deltalake", specifier = ">=1.4.2" }, + { name = "duckdb", specifier = ">=1.4.4" }, + { name = "fastapi", marker = "extra == 'web'", specifier = ">=0.131.0" }, + { name = "fastavro", marker = "extra == 'web'", specifier = ">=1.9.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100.0,<7.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.18.2,<2.0.0" }, - { name = "pandas", specifier = ">=2.3.0" }, - { name = "pip", specifier = ">=24.0" }, + { name = "pandas", specifier = ">=3.0.1" }, + { name = "pip", specifier = ">=25.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.4.0,<5.0.0" }, - { name = "pyarrow", specifier = ">=22.0.0" }, - { name = "pydantic", specifier = ">=2.11.0" }, - { name = "pyiceberg", extras = ["s3fs", "sql-sqlite", "glue"], specifier = ">=0.9.1" }, + { name = "pyarrow", specifier = ">=23.0.0" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pyiceberg", extras = ["s3fs", "sql-sqlite", "glue"], specifier = ">=0.11.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2,<9.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.26.0,<1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0,<7.0.0" }, + { name = "python-multipart", marker = "extra == 'web'", specifier = ">=0.0.12" }, { name = "pyyaml", specifier = ">=6.0.0" }, - { name = "rich", specifier = ">=13.0.0" }, + { name = "rich", specifier = ">=14.3.3" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.4,<0.15.0" }, - { name = "sqlalchemy", specifier = ">=2.0.0" }, + { name = "sqlalchemy", specifier = ">=2.0.46" }, { name = "textual", specifier = ">=0.86.2" }, { name = "textual-dev", marker = "extra == 'dev'", specifier = ">=1.7.0,<2.0.0" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0.0,<6.0.0" }, { name = "types-pyyaml", marker = "extra == 'dev'" }, { name = "types-toml", marker = "extra == 'dev'" }, - { name = "uv", specifier = ">=0.5.0" }, + { name = "uv", specifier = ">=0.10.4" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = ">=0.32.0" }, ] -provides-extras = ["dev"] +provides-extras = ["web", "dev"] [[package]] name = "tenacity" @@ -2144,28 +2337,77 @@ wheels = [ [[package]] name = "uv" -version = "0.9.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/10/ad3dc22d0cabe7c335a1d7fc079ceda73236c0984da8d8446de3d2d30c9b/uv-0.9.13.tar.gz", hash = "sha256:105a6f4ff91480425d1b61917e89ac5635b8e58a79267e2be103338ab448ccd6", size = 3761269, upload-time = "2025-11-26T16:17:30.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/ae/94ec7111b006bc7212bf727907a35510a37928c15302ecc3757cfd7d6d7f/uv-0.9.13-py3-none-linux_armv6l.whl", hash = "sha256:7be41bdeb82c246f8ef1421cf4d1dd6ab3e5f46e4235eb22c8f5bf095debc069", size = 20830010, upload-time = "2025-11-26T16:17:13.147Z" }, - { url = "https://files.pythonhosted.org/packages/8a/53/5eb0eb0ca7ed41c10447d6c859b4d81efc5b76de14d01fd900af7d7bd1be/uv-0.9.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1d4c624bb2b81f885b7182d99ebdd5c2842219d2ac355626a4a2b6c1e3e6f8c1", size = 19961915, upload-time = "2025-11-26T16:17:15.587Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d1/0f0c8dc2125709a8e072b73e5e89da9f016d492ca88b909b23b3006c2b51/uv-0.9.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:318d0b9a39fa26f95a428a551d44cbefdfd58178954a831669248a42f39d3c75", size = 18426731, upload-time = "2025-11-26T16:17:31.855Z" }, - { url = "https://files.pythonhosted.org/packages/36/ee/f9db8cb69d584b8326b3e0e60e5a639469cdebac76e7f4ff5ba7c2c6fe6c/uv-0.9.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6a641ed7bcc8d317d22a7cb1ad0dfa41078c8e392f6f9248b11451abff0ccf50", size = 20315156, upload-time = "2025-11-26T16:17:08.125Z" }, - { url = "https://files.pythonhosted.org/packages/8a/49/045bbfe264fc1add3e238e0e11dec62725c931946dbcda3780d15ca3591b/uv-0.9.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e797ae9d283ee129f33157d84742607547939ca243d7a8c17710dc857a7808bd", size = 20430487, upload-time = "2025-11-26T16:17:28.143Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/18a14dbaedfd2492de5cca50b46a238d5199e9f0291f027f63a03f2ebdd4/uv-0.9.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48fa9cf568c481c150f957a2f9285d1c3ad2c1d50c904b03bcebd5c9669c5668", size = 21378284, upload-time = "2025-11-26T16:16:48.696Z" }, - { url = "https://files.pythonhosted.org/packages/08/04/d0fc5fb25e3f90740913b1c028e1556515e4e1fea91e1f58e7c18c1712a3/uv-0.9.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a66817a416c1c79303fd5e40c319ed9c8e59b46fb04cf3eac4447e95b9ec8763", size = 23016232, upload-time = "2025-11-26T16:16:46.149Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bc/cef461a47cddeb99c2a3b31f3946d38cbca7923b0f2fb6666756ba63a84a/uv-0.9.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05eb7e941c54666e8c52519a79ff46d15b5206967645652d3dfb2901fd982493", size = 22657140, upload-time = "2025-11-26T16:17:03.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/5c9de65279480b1922c51aae409bbfa1d90ff108f8b81688022499f2c3e2/uv-0.9.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fe5ac5b0a98a876da8f4c08e03217589a89ea96883cfdc9c4b397bf381ef7b9", size = 21644453, upload-time = "2025-11-26T16:16:43.228Z" }, - { url = "https://files.pythonhosted.org/packages/da/e5/148ab5edb339f5833d04f0bcb8380a53e8b19bd5f091ae67222ed188b393/uv-0.9.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6627d0abbaf58f9ff6e07e3f8522d65421230969aa2d7b10421339f0cb30dec4", size = 21655007, upload-time = "2025-11-26T16:16:51.36Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d8/a77587e4608af6efc5a72d3a937573eb5d08052550a3f248821b50898626/uv-0.9.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6cca7671efacf6e2950eb86273ecce4a9a3f8bfa6ac04e8a17be9499bb3bb882", size = 20448163, upload-time = "2025-11-26T16:16:53.768Z" }, - { url = "https://files.pythonhosted.org/packages/81/ad/e3bb28d175f22edf1779a81b76910e842dcde012859556b28e9f4b630f26/uv-0.9.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2e00a4f8404000e86074d7d2fe5734078126a65aefed1e9a39f10c390c4c15dc", size = 21477072, upload-time = "2025-11-26T16:16:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/32/b6/9231365ab2495107a9e23aa36bb5400a4b697baaa0e5367f009072e88752/uv-0.9.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:3a4c16906e78f148c295a2e4f2414b843326a0f48ae68f7742149fd2d5dafbf7", size = 20421263, upload-time = "2025-11-26T16:17:10.552Z" }, - { url = "https://files.pythonhosted.org/packages/8c/83/d83eeee9cea21b9a9e053d4a2ec752a3b872e22116851317da04681cc27e/uv-0.9.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f254cb60576a3ae17f8824381f0554120b46e2d31a1c06fc61432c55d976892d", size = 20855418, upload-time = "2025-11-26T16:17:05.552Z" }, - { url = "https://files.pythonhosted.org/packages/6e/88/70102f374cfbbb284c6fe385e35978bff25a70b8e6afa871886af8963595/uv-0.9.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d50cea327b994786866b2d12c073097b9c8d883d42f0c0408b2d968492f571a4", size = 21871073, upload-time = "2025-11-26T16:17:00.213Z" }, - { url = "https://files.pythonhosted.org/packages/01/05/00c90367db0c81379c9d2b1fb458a09a0704ecd89821c071cb0d8a917752/uv-0.9.13-py3-none-win32.whl", hash = "sha256:a80296b1feb61bac36aee23ea79be33cd9aa545236d0780fbffaac113a17a090", size = 19607949, upload-time = "2025-11-26T16:17:23.337Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e0/718b433acf811388e309936524be5786b8e0cc8ff23128f9cc29a34c075b/uv-0.9.13-py3-none-win_amd64.whl", hash = "sha256:5732cd0fe09365fa5ad2c0a2d0c007bb152a2aa3c48e79f570eec13fc235d59d", size = 21722341, upload-time = "2025-11-26T16:17:20.764Z" }, - { url = "https://files.pythonhosted.org/packages/9f/31/142457b7c9d5edcdd8d4853c740c397ec83e3688b69d0ef55da60f7ab5b5/uv-0.9.13-py3-none-win_arm64.whl", hash = "sha256:edfc3d53b6adefae766a67672e533d7282431f0deb2570186d1c3dd0d0e3c0a3", size = 20042030, upload-time = "2025-11-26T16:17:18.058Z" }, +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/bb/dfd872ab6515e5609dc899acb65ccaf8cbedddefa3e34e8da0a5b3e13070/uv-0.10.4.tar.gz", hash = "sha256:b9ecf9f9145b95ddd6627b106e2e74f4204393b41bea2488079872699c03612e", size = 3875347, upload-time = "2026-02-17T22:01:22.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/a3/565e5e45b5006c108ccd596682768c00be988421a83be92193c90bd889e4/uv-0.10.4-py3-none-linux_armv6l.whl", hash = "sha256:97cd6856145dec1d50821468bb6a10c14f3d71015eb97bb657163c837b5ffe79", size = 22352134, upload-time = "2026-02-17T22:01:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c6/b86f3fdcde9f270e6dc1ff631a4fe73971bf4162c4dd169c7621110361b8/uv-0.10.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:44dd91ef224cfce2203716ecf244c3d3641269d1c99996aab852248caf2aeba4", size = 21417697, upload-time = "2026-02-17T22:01:51.162Z" }, + { url = "https://files.pythonhosted.org/packages/63/91/c4ddf7e55e05394967615050cc364a999157a44c008d0e1e9db2ed49a11c/uv-0.10.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:751959135a62f006ef51f3fcc5d02ec67986defa0424d470cce0918eede36a55", size = 20082236, upload-time = "2026-02-17T22:01:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/25/92/606701b147d421ba2afe327d25f1ec5f59e519157b7e530d09cf61781d22/uv-0.10.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c184891b496c5fa04a7e1396d7f1953f52c97a5635636330854ab68f9e8ec212", size = 21921200, upload-time = "2026-02-17T22:01:24.131Z" }, + { url = "https://files.pythonhosted.org/packages/c3/79/942e75d0920a9e4cac76257cd3e2c238f1963d7e45423793f92e84eaa480/uv-0.10.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:5b8a2170ecc700d82ed322fa056789ae2281353fef094e44f563c2f32ab8f438", size = 21974822, upload-time = "2026-02-17T22:01:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/71/e5b1140c5c7296f935037a967717a82591522bbc93b4e67c4554dfbb4380/uv-0.10.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:decaf620106efa0d09ca27a8301dd83b8a5371e42649cd2704cfd11fe31af7d7", size = 21953309, upload-time = "2026-02-17T22:01:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/70/a3/03ac1ff2058413c2c7d347f3b3396f291e192b096d2625a201c00bd962c6/uv-0.10.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d1035db05ac5b94387395428bdcbfce685f6c8eb2b711b66a5a1b397111913", size = 23217053, upload-time = "2026-02-17T22:01:09.278Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/9b02140e8ff29d9b575335662288493cdcde5f123337613c04613017cf23/uv-0.10.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e754f9c8fd7532a28da7deaa6e400de5e7b459f7846bd5320db215a074fa8664", size = 24053086, upload-time = "2026-02-17T22:01:32.722Z" }, + { url = "https://files.pythonhosted.org/packages/f8/80/7023e1b0f9180226f8c3aa3e207383671cb524eb8bbd8a8eecf1c0cfe867/uv-0.10.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d419ef8d4fbd5be0af952a60c76d4f6183acb827cc729095d11c63e7dfaec24c", size = 23121689, upload-time = "2026-02-17T22:01:26.835Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b3/4b9580d62e1245df52e8516cf3e404ff39cc72634d2d749d47b1dada4161/uv-0.10.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82978155e571f2ac3dd57077bd746bfe41b65fa19accc3c92d1f09632cd36c63", size = 23136767, upload-time = "2026-02-17T22:01:40.729Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/058976e2a5513f11954e09595a1821d5db1819e96e00bafded19c6a470e9/uv-0.10.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8437e56a7d0f8ecd7421e8b84024dd8153179b8f1371ca1bd66b79fa7fb4c2c1", size = 22003202, upload-time = "2026-02-17T22:01:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/41/c5/da0fc5b732f7dd1f99116ce19e3c1cae7dfa7d04528a0c38268f20643edf/uv-0.10.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ff1c6a465ec035dfe2dfd745b2e85061f47ab3c5cc626eead491994c028eacc6", size = 22720004, upload-time = "2026-02-17T22:01:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/13c24dd56c135553645c2c62543eba928e88479fdd2d8356fdf35a0113bc/uv-0.10.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:525dc49a02b78fcd77431f013f2c48b2a152e31808e792c0d1aee4600495a320", size = 22401692, upload-time = "2026-02-17T22:01:35.368Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/7a5fdbc0bfd8364e6290457794127d5e766dbc6d44bb15d1a9e318bc356b/uv-0.10.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:7d514b30877fda6e83874ccbd1379e0249cfa064511c5858433edcf697d0d4e3", size = 23330968, upload-time = "2026-02-17T22:01:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/d1/df/004e32be4cd24338422842dd93383f2df0be4554efb6872fef37997ff3ca/uv-0.10.4-py3-none-win32.whl", hash = "sha256:4aed1237847dbd694475c06e8608f2f5f6509181ac148ee35694400d382a3784", size = 21373394, upload-time = "2026-02-17T22:01:20.362Z" }, + { url = "https://files.pythonhosted.org/packages/31/dd/1900452678d46f6a649ab8167bededb02500b0561fc9f69e1f52607895c7/uv-0.10.4-py3-none-win_amd64.whl", hash = "sha256:4a1c595cf692fa611019a7ad9bf4b0757fccd0a3f838ca05e53db82912ddaa39", size = 23813606, upload-time = "2026-02-17T22:01:17.733Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/c6ba7ceee3ec58d21156b4968449e6a12af15eea8d26308b3b3ffeef2baf/uv-0.10.4-py3-none-win_arm64.whl", hash = "sha256:28c59a02d7a648b75a9c2ea735773d9d357a1eee773b78593c275b0bef1a4b73", size = 22180241, upload-time = "2026-02-17T22:01:56.305Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] [[package]] @@ -2182,6 +2424,99 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "wrapt" version = "1.17.3" @@ -2307,3 +2642,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] diff --git a/web-ui/components.json b/web-ui/components.json new file mode 100644 index 0000000..8a44063 --- /dev/null +++ b/web-ui/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/web-ui/next.config.ts b/web-ui/next.config.ts new file mode 100644 index 0000000..8a7e2d2 --- /dev/null +++ b/web-ui/next.config.ts @@ -0,0 +1,9 @@ +import type { NextConfig } from "next"; + +const config: NextConfig = { + output: "export", + trailingSlash: true, + images: { unoptimized: true }, +}; + +export default config; diff --git a/web-ui/package-lock.json b/web-ui/package-lock.json new file mode 100644 index 0000000..5e3ca41 --- /dev/null +++ b/web-ui/package-lock.json @@ -0,0 +1,6630 @@ +{ + "name": "tablesleuth-web", + "version": "0.6.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tablesleuth-web", + "version": "0.6.0", + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.468.0", + "next": "^16.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "autoprefixer": "^10.4.24", + "eslint": "^9", + "eslint-config-next": "^16.1.0", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/eslint-utils/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-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/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "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/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "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.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", + "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "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/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.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/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.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.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "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/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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/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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "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/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.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/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "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/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "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/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "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/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001772", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", + "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "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/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "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.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "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-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.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "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", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "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.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.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-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.1.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "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-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "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-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/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-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "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": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "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/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "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/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-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/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/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/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-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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==", + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "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/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/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/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-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "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==", + "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==", + "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": "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/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/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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-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": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/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/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "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.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/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": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "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/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "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-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "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/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-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==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "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/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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "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/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "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/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/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/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "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-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "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.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "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.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/tailwindcss/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.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/tsconfig-paths/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "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.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.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.0.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": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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-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.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "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": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "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" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/web-ui/package.json b/web-ui/package.json new file mode 100644 index 0000000..a032473 --- /dev/null +++ b/web-ui/package.json @@ -0,0 +1,37 @@ +{ + "name": "tablesleuth-web", + "version": "0.6.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.468.0", + "next": "^16.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "autoprefixer": "^10.4.24", + "eslint": "^9", + "eslint-config-next": "^16.1.0", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + }, + "overrides": { + "minimatch": "^10.2.1" + } +} diff --git a/web-ui/postcss.config.mjs b/web-ui/postcss.config.mjs new file mode 100644 index 0000000..73a0f54 --- /dev/null +++ b/web-ui/postcss.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('postcss').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/web-ui/src/app/delta/page.tsx b/web-ui/src/app/delta/page.tsx new file mode 100644 index 0000000..87f5156 --- /dev/null +++ b/web-ui/src/app/delta/page.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState } from "react"; +import { DeltaTableLoader } from "@/components/delta/table-loader"; +import { VersionHistory } from "@/components/delta/version-history"; +import { VersionDetail } from "@/components/delta/version-detail"; +import { ForensicsPanel } from "@/components/delta/forensics-panel"; +import { DataSample } from "@/components/shared/data-sample"; +import { ComparisonPanel } from "@/components/shared/comparison-panel"; +import { delta as api } from "@/lib/api"; +import type { DeltaForensicsResponse, DeltaLoadResponse, DeltaSchemaField, SnapshotInfo } from "@/lib/types"; + +type RightTab = "details" | "forensics" | "sample" | "compare"; + +export default function DeltaPage() { + const [tablePath, setTablePath] = useState(null); + const [currentSnapshot, setCurrentSnapshot] = useState(null); + const [versions, setVersions] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [schema, setSchema] = useState(null); + const [loadingSchema, setLoadingSchema] = useState(false); + const [forensics, setForensics] = useState(null); + const [loadingForensics, setLoadingForensics] = useState(false); + const [rightTab, setRightTab] = useState("details"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const selectedVersion = versions.find((v) => v.snapshot_id === selectedId) ?? null; + + const loadSchema = async (path: string, version: number) => { + setLoadingSchema(true); + setSchema(null); + try { + const res = await api.schema({ path, version }); + setSchema(res.fields); + } catch { + setSchema(null); + } finally { + setLoadingSchema(false); + } + }; + + const handleLoad = async (path: string, version?: number) => { + setLoading(true); + setError(null); + setVersions([]); + setForensics(null); + setSchema(null); + setRightTab("details"); + try { + const [snap, vers] = await Promise.all([ + api.load({ path, version }), + api.versions({ path }), + ]); + setTablePath(path); + setCurrentSnapshot(snap); + setVersions(vers.versions); + setSelectedId(snap.snapshot_id); + loadSchema(path, snap.snapshot_id); + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + }; + + const handleSelect = (id: number) => { + setSelectedId(id); + setRightTab("details"); + if (tablePath) { + loadSchema(tablePath, id); + } + }; + + const handleLoadForensics = async () => { + if (!tablePath) return; + setLoadingForensics(true); + try { + const f = await api.forensics({ path: tablePath }); + setForensics(f); + } catch (e) { + setError(String(e)); + } finally { + setLoadingForensics(false); + } + }; + + return ( +
+
+

Delta Lake Analyzer

+ + {error && ( +
+ {error} +
+ )} + {currentSnapshot && ( +
+ v{currentSnapshot.current_version} · {versions.length} versions +
+ )} +
+ +
+ {versions.length > 0 && ( +
+
+ {versions.length} versions +
+ +
+ )} + +
+ {selectedVersion ? ( + <> +
+ {(["details", "forensics", "sample", "compare"] as RightTab[]).map((t) => ( + + ))} +
+ +
+ {rightTab === "details" && ( + + )} + {rightTab === "forensics" && ( +
+ {!forensics && ( + + )} + {loadingForensics && ( +
Running forensic analysis…
+ )} + {forensics && } +
+ )} + {rightTab === "sample" && ( + + )} + {rightTab === "compare" && tablePath && ( + b.timestamp_ms - a.timestamp_ms).map((v) => ({ + id: String(v.snapshot_id), + label: `v${v.snapshot_id} · ${v.operation} · ${new Date(v.timestamp_ms).toLocaleString()}`, + }))} + path={tablePath} + /> + )} +
+ + ) : ( +
+ {versions.length > 0 ? "Select a version to inspect" : "Enter a Delta table path above"} +
+ )} +
+
+
+ ); +} diff --git a/web-ui/src/app/gizmosql/page.tsx b/web-ui/src/app/gizmosql/page.tsx new file mode 100644 index 0000000..808b05e --- /dev/null +++ b/web-ui/src/app/gizmosql/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SqlEditor } from "@/components/gizmosql/sql-editor"; +import { ResultsGrid } from "@/components/gizmosql/results-grid"; +import { gizmosql as api } from "@/lib/api"; +import type { GizmoStatus, QueryResult } from "@/lib/types"; + +export default function GizmoSQLPage() { + const [status, setStatus] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + api.status().then(setStatus).catch(() => setStatus({ connected: false, error: "Could not reach server" })); + }, []); + + const handleExecute = async (sql: string) => { + setLoading(true); + setError(null); + setResult(null); + try { + const r = await api.query(sql); + setResult(r); + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + }; + + return ( +
+

GizmoSQL Query Console

+ + {/* Connection status */} + {status && ( +
+ {status.connected ? ( + <>Connected · {status.version} + ) : ( + <>Not connected · {status.error} + )} +
+ )} + + + + {error && ( +
+ {error} +
+ )} + + {result && ( +
+ +
+ )} +
+ ); +} diff --git a/web-ui/src/app/globals.css b/web-ui/src/app/globals.css new file mode 100644 index 0000000..00b08e3 --- /dev/null +++ b/web-ui/src/app/globals.css @@ -0,0 +1,59 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/web-ui/src/app/iceberg/page.tsx b/web-ui/src/app/iceberg/page.tsx new file mode 100644 index 0000000..aa08f45 --- /dev/null +++ b/web-ui/src/app/iceberg/page.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { useState } from "react"; +import { IcebergTableLoader } from "@/components/iceberg/table-loader"; +import { SnapshotList } from "@/components/iceberg/snapshot-list"; +import { SnapshotDetail } from "@/components/iceberg/snapshot-detail"; +import { DataSample } from "@/components/shared/data-sample"; +import { ComparisonPanel } from "@/components/shared/comparison-panel"; +import { iceberg as api } from "@/lib/api"; +import type { + IcebergSnapshotDetails, + IcebergSnapshotInfo, + IcebergTableInfo, + SchemaInfo, +} from "@/lib/types"; + +interface TableRef { + metadata_path?: string; + catalog_name?: string; + table_identifier?: string; + snapshot_id?: string; +} + +type RightTab = "details" | "forensics" | "sample" | "compare"; + +export default function IcebergPage() { + const [tableRef, setTableRef] = useState(null); + const [tableInfo, setTableInfo] = useState(null); + const [snapshots, setSnapshots] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [details, setDetails] = useState(null); + const [schemas, setSchemas] = useState([]); + const [loadingSchemas, setLoadingSchemas] = useState(false); + const [rightTab, setRightTab] = useState("details"); + const [loading, setLoading] = useState(false); + const [loadingDetails, setLoadingDetails] = useState(false); + const [error, setError] = useState(null); + + const handleLoad = async (ref: TableRef) => { + setLoading(true); + setError(null); + setSnapshots([]); + setSelectedId(null); + setDetails(null); + setSchemas([]); + setRightTab("details"); + const { snapshot_id, ...loadRef } = ref; + try { + const [info, snaps] = await Promise.all([ + api.load(loadRef), + api.snapshots(loadRef), + ]); + setTableInfo(info); + setTableRef(loadRef); + setSnapshots(snaps.snapshots); + // Auto-select: snapshot_id if provided (and found), otherwise current snapshot + const targetId = snapshot_id ?? info.current_snapshot_id ?? null; + if (targetId && snaps.snapshots.some((s) => s.snapshot_id === targetId)) { + setSelectedId(targetId); + setLoadingDetails(true); + api.snapshotDetails(targetId, loadRef) + .then(setDetails) + .catch((e) => setError(String(e))) + .finally(() => setLoadingDetails(false)); + } + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + }; + + const handleSelectSnapshot = async (id: string) => { + if (!tableRef) return; + setSelectedId(id); + setDetails(null); + setRightTab("details"); + setLoadingDetails(true); + try { + const d = await api.snapshotDetails(id, tableRef); + setDetails(d); + } catch (e) { + setError(String(e)); + } finally { + setLoadingDetails(false); + } + }; + + const handleRightTab = async (tab: RightTab) => { + setRightTab(tab); + if (tab === "forensics" && tableRef && schemas.length === 0 && !loadingSchemas) { + setLoadingSchemas(true); + try { + const res = await api.schemaEvolution(tableRef); + setSchemas(res.schemas); + } catch { + // ignore — show empty state + } finally { + setLoadingSchemas(false); + } + } + }; + + const sampleFilePath = details?.data_files[0]?.file_path ?? null; + + return ( +
+
+

Iceberg Snapshot Analyzer

+ + {error && ( +
+ {error} +
+ )} + {tableInfo && ( +
+ Table UUID: {tableInfo.table_uuid} · Format v{tableInfo.format_version} ·{" "} + {snapshots.length} snapshots +
+ )} +
+ +
+ {snapshots.length > 0 && ( +
+
+ {snapshots.length} snapshot{snapshots.length !== 1 ? "s" : ""} +
+ +
+ )} + +
+ {selectedId ? ( + <> +
+ {(["details", "forensics", "sample", "compare"] as RightTab[]).map((t) => ( + + ))} +
+ +
+ {rightTab === "details" && ( + loadingDetails ? ( +
+ Loading snapshot details… +
+ ) : details ? ( + + ) : ( +
+ Loading… +
+ ) + )} + {rightTab === "forensics" && ( +
+ {loadingSchemas ? ( +

Loading schema evolution…

+ ) : schemas.length === 0 ? ( +

No schema evolution history found.

+ ) : ( +
+

Schema Evolution ({schemas.length} versions)

+ {schemas.map((s) => ( +
+

+ Schema ID {s.schema_id} · {s.fields.length} fields +

+ + + + + + + + + + {s.fields.map((f) => ( + + + + + + ))} + +
FieldTypeRequired
{f.name}{f.field_type}{f.required ? "Yes" : "No"}
+
+ ))} +
+ )} +
+ )} + {rightTab === "sample" && ( + + )} + {rightTab === "compare" && ( + b.timestamp_ms - a.timestamp_ms).map((s) => ({ + id: s.snapshot_id, + label: `${s.snapshot_id.slice(-8)} · ${s.operation} · ${new Date(s.timestamp_ms).toLocaleString()}`, + }))} + metadata_path={tableRef?.metadata_path} + catalog_name={tableRef?.catalog_name} + table_identifier={tableRef?.table_identifier} + /> + )} +
+ + ) : ( +
+ {snapshots.length > 0 + ? "Select a snapshot to inspect" + : "Enter a metadata path or catalog info above"} +
+ )} +
+
+
+ ); +} diff --git a/web-ui/src/app/layout.tsx b/web-ui/src/app/layout.tsx new file mode 100644 index 0000000..1f31669 --- /dev/null +++ b/web-ui/src/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { NavBar } from "@/components/layout/navbar"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "TableSleuth", + description: "Forensic analysis for Parquet, Iceberg, and Delta Lake tables", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + +
{children}
+ + + ); +} diff --git a/web-ui/src/app/page.tsx b/web-ui/src/app/page.tsx new file mode 100644 index 0000000..fd6cf53 --- /dev/null +++ b/web-ui/src/app/page.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import { Database, FileText, GitBranch, Terminal } from "lucide-react"; + +const formats = [ + { + href: "/parquet", + label: "Parquet", + icon: FileText, + description: + "Inspect Parquet files and directories. Explore schema, row groups, column stats, and data samples.", + color: "text-blue-600", + bg: "bg-blue-50 hover:bg-blue-100", + }, + { + href: "/iceberg", + label: "Apache Iceberg", + icon: Database, + description: + "Analyze Iceberg table snapshots, schema evolution, MOR overhead, and compare snapshot deltas.", + color: "text-purple-600", + bg: "bg-purple-50 hover:bg-purple-100", + }, + { + href: "/delta", + label: "Delta Lake", + icon: GitBranch, + description: + "Examine Delta table version history, storage waste, checkpoint health, and optimization recommendations.", + color: "text-green-600", + bg: "bg-green-50 hover:bg-green-100", + }, + { + href: "/gizmosql", + label: "GizmoSQL", + icon: Terminal, + description: + "Run SQL queries and column profiling against Parquet/Iceberg data via GizmoSQL DuckDB Flight SQL.", + color: "text-orange-600", + bg: "bg-orange-50 hover:bg-orange-100", + }, +]; + +export default function HomePage() { + return ( +
+
+

TableSleuth

+

+ Forensic analysis for open table formats — Parquet, Apache Iceberg, Delta Lake. +

+
+ +
+ {formats.map(({ href, label, icon: Icon, description, color, bg }) => ( + +
+ + {label} +
+

{description}

+ + ))} +
+
+ ); +} diff --git a/web-ui/src/app/parquet/page.tsx b/web-ui/src/app/parquet/page.tsx new file mode 100644 index 0000000..2a614bd --- /dev/null +++ b/web-ui/src/app/parquet/page.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useState } from "react"; +import { PathInput } from "@/components/parquet/path-input"; +import { FileList } from "@/components/parquet/file-list"; +import { FileDetailTabs } from "@/components/parquet/file-detail-tabs"; +import { parquet as api } from "@/lib/api"; +import type { FileRef, ParquetFileInfo, SampleResponse } from "@/lib/types"; + +export default function ParquetPage() { + const [files, setFiles] = useState([]); + const [selectedPath, setSelectedPath] = useState(null); + const [fileInfo, setFileInfo] = useState(null); + const [sample, setSample] = useState(null); + const [loading, setLoading] = useState(false); + const [loadingInfo, setLoadingInfo] = useState(false); + const [loadingSample, setLoadingSample] = useState(false); + const [error, setError] = useState(null); + + const handleAnalyze = async (path: string, catalog?: string, region?: string) => { + setLoading(true); + setError(null); + setFiles([]); + setSelectedPath(null); + setFileInfo(null); + setSample(null); + try { + const result = await api.analyze(path, catalog, region); + setFiles(result.files); + if (result.files.length === 1) { + await handleSelectFile(result.files[0].path, region); + } + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + }; + + const handleSelectFile = async (path: string, region?: string) => { + setSelectedPath(path); + setFileInfo(null); + setSample(null); + setLoadingInfo(true); + try { + const info = await api.fileInfo(path, region); + setFileInfo(info); + } catch (e) { + setError(String(e)); + } finally { + setLoadingInfo(false); + } + }; + + const handleLoadSample = async () => { + if (!selectedPath) return; + setLoadingSample(true); + try { + const s = await api.sample(selectedPath, 100); + setSample(s); + } catch (e) { + setError(String(e)); + } finally { + setLoadingSample(false); + } + }; + + return ( +
+
+

Parquet Inspector

+ + {error && ( +
+ {error} +
+ )} +
+ +
+ {/* File list sidebar */} + {files.length > 0 && ( +
+
+ {files.length} file{files.length !== 1 ? "s" : ""} found +
+ handleSelectFile(path)} + /> +
+ )} + + {/* Detail panel */} +
+ {loadingInfo ? ( +
+ Loading file metadata... +
+ ) : fileInfo ? ( + + ) : ( +
+ {files.length > 0 + ? "Select a file to inspect" + : "Enter a path above to discover Parquet files"} +
+ )} +
+
+
+ ); +} diff --git a/web-ui/src/app/settings/page.tsx b/web-ui/src/app/settings/page.tsx new file mode 100644 index 0000000..430dc74 --- /dev/null +++ b/web-ui/src/app/settings/page.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { config as api } from "@/lib/api"; +import type { AppConfig, ConfigStatus } from "@/lib/types"; + +export default function SettingsPage() { + const [cfg, setCfg] = useState(null); + const [status, setStatus] = useState(null); + const [pyiceberg, setPyiceberg] = useState | null>(null); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(null); + const [uploadingPyiceberg, setUploadingPyiceberg] = useState(false); + const [pyicebergUploadStatus, setPyicebergUploadStatus] = useState(null); + + useEffect(() => { + Promise.all([api.get(), api.status(), api.getPyiceberg()]) + .then(([c, s, p]) => { + setCfg(c); + setStatus(s); + setPyiceberg(p.config); + }) + .catch((e) => setError(String(e))); + }, []); + + const handlePyicebergUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setUploadingPyiceberg(true); + setPyicebergUploadStatus(null); + try { + const result = await api.uploadPyiceberg(file); + setPyiceberg(result.config); + setPyicebergUploadStatus(`Saved to ${result.path}`); + } catch (err) { + setPyicebergUploadStatus(`Error: ${String(err)}`); + } finally { + setUploadingPyiceberg(false); + e.target.value = ""; + } + }; + + const handleSave = async () => { + if (!cfg) return; + setSaving(true); + setError(null); + try { + await api.save(cfg); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + } catch (e) { + setError(String(e)); + } finally { + setSaving(false); + } + }; + + if (!cfg) { + return ( +
+ Loading settings... +
+ ); + } + + return ( +
+

Settings

+ + {status && ( +
+

Active Configuration

+

+ File: {status.config_file ?? "defaults (no file found)"} +

+ {Object.entries(status.env_overrides) + .filter(([, v]) => v) + .map(([k]) => ( +

+ ENV override: {k} +

+ ))} +
+ )} + + {error && ( +
+ {error} +
+ )} + + {/* Catalog settings */} +
+

Catalog

+ + setCfg({ ...cfg, catalog: { default: e.target.value || null } })} + placeholder="e.g. glue, local_sqlite" + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + /> +
+ + {/* GizmoSQL settings */} +
+

GizmoSQL

+
+ {[ + { label: "URI", key: "uri", placeholder: "grpc+tls://localhost:31337" }, + { label: "Username", key: "username", placeholder: "gizmosql_username" }, + { label: "Password", key: "password", placeholder: "gizmosql_password", type: "password" }, + ].map(({ label, key, placeholder, type }) => ( +
+ + + setCfg({ ...cfg, gizmosql: { ...cfg.gizmosql, [key]: e.target.value } }) + } + placeholder={placeholder} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + /> +
+ ))} +
+ + setCfg({ + ...cfg, + gizmosql: { ...cfg.gizmosql, tls_skip_verify: e.target.checked }, + }) + } + /> + +
+
+
+ + {/* PyIceberg config */} +
+

PyIceberg Config (.pyiceberg.yaml)

+

+ {status?.pyiceberg_yaml_exists + ? `Active: ${status.pyiceberg_yaml_path}` + : `Not found at ${status?.pyiceberg_yaml_path ?? "~/.pyiceberg.yaml"} — upload a file to create it`} +

+ {pyiceberg && Object.keys(pyiceberg).length > 0 && ( +
+            {JSON.stringify(pyiceberg, null, 2)}
+          
+ )} +
+ + {pyicebergUploadStatus && ( + + {pyicebergUploadStatus} + + )} +
+
+ + +
+ ); +} diff --git a/web-ui/src/components/delta/forensics-panel.tsx b/web-ui/src/components/delta/forensics-panel.tsx new file mode 100644 index 0000000..9c21a41 --- /dev/null +++ b/web-ui/src/components/delta/forensics-panel.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { formatBytes, formatNumber } from "@/lib/utils"; +import type { DeltaForensicsResponse } from "@/lib/types"; + +interface ForensicsPanelProps { + forensics: DeltaForensicsResponse; +} + +const priorityColors = { + high: "bg-red-100 text-red-800 border-red-200", + medium: "bg-yellow-100 text-yellow-800 border-yellow-200", + low: "bg-blue-100 text-blue-800 border-blue-200", +}; + +const healthColors = { + healthy: "text-green-600", + degraded: "text-yellow-600", + critical: "text-red-600", +}; + +export function ForensicsPanel({ forensics }: ForensicsPanelProps) { + const { file_size_analysis: fsa, storage_waste: sw, checkpoint_health: ch, recommendations } = + forensics; + + return ( +
+ {/* File size distribution */} +
+

File Size Distribution

+
+ {Object.entries(fsa.histogram).map(([bucket, count]) => ( +
+

{count}

+

{bucket}

+
+ ))} +
+
+
+

Small Files (<10MB)

+

+ {fsa.small_file_count} ({fsa.small_file_percentage.toFixed(1)}%) +

+
+
+

Optimization Opportunity

+

~{fsa.optimization_opportunity} files reducible

+
+
+

Total Size

+

{formatBytes(fsa.total_size_bytes)}

+
+
+
+ + {/* Storage waste */} +
+

Storage Waste

+
+
+

Active Files

+

+ {sw.active_files.count} ({formatBytes(sw.active_files.total_size_bytes)}) +

+
+
+

Tombstoned Files

+

+ {sw.tombstone_files.count} ({formatBytes(sw.tombstone_files.total_size_bytes)}) +

+
+
+

Waste %

+

30 ? "text-red-600" : sw.waste_percentage > 15 ? "text-yellow-600" : "text-green-600"}`} + > + {sw.waste_percentage.toFixed(1)}% +

+
+
+

Reclaimable

+

{formatBytes(sw.reclaimable_bytes)}

+
+
+
+ + {/* Checkpoint health */} +
+

Checkpoint Health

+
+
+

Status

+

+ {ch.health_status.toUpperCase()} +

+
+
+

Last Checkpoint

+

+ {ch.last_checkpoint_version != null ? `v${ch.last_checkpoint_version}` : "None"} +

+
+
+

Log Tail

+

{ch.log_tail_length} files

+
+
+ {ch.issues.length > 0 && ( +
    + {ch.issues.map((issue, i) => ( +
  • {issue}
  • + ))} +
+ )} +
+ + {/* Recommendations */} + {recommendations.length > 0 && ( +
+

Recommendations

+
+ {recommendations.map((rec, i) => ( +
+
+ {rec.type} + {rec.priority} +
+

{rec.reason}

+

{rec.estimated_impact}

+ + {rec.command} + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/web-ui/src/components/delta/table-loader.tsx b/web-ui/src/components/delta/table-loader.tsx new file mode 100644 index 0000000..0ff1b2d --- /dev/null +++ b/web-ui/src/components/delta/table-loader.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useState } from "react"; + +interface DeltaTableLoaderProps { + onLoad: (path: string, version?: number, storage_options?: Record) => void; + loading?: boolean; +} + +export function DeltaTableLoader({ onLoad, loading }: DeltaTableLoaderProps) { + const [path, setPath] = useState(""); + const [version, setVersion] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (path.trim()) { + onLoad(path.trim(), version ? parseInt(version) : undefined); + } + }; + + return ( +
+ setPath(e.target.value)} + placeholder="Path to Delta table (local or s3://...)" + className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" + /> + setVersion(e.target.value)} + placeholder="Version (optional)" + className="w-32 rounded-md border border-input bg-background px-3 py-2 text-sm" + /> + +
+ ); +} diff --git a/web-ui/src/components/delta/version-detail.tsx b/web-ui/src/components/delta/version-detail.tsx new file mode 100644 index 0000000..dc77e11 --- /dev/null +++ b/web-ui/src/components/delta/version-detail.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { formatBytes, formatNumber, formatTimestamp } from "@/lib/utils"; +import type { DeltaSchemaField, SnapshotInfo } from "@/lib/types"; + +interface VersionDetailProps { + version: SnapshotInfo; + schema?: DeltaSchemaField[] | null; + loadingSchema?: boolean; +} + +export function VersionDetail({ version, schema, loadingSchema }: VersionDetailProps) { + const totalSize = version.data_files.reduce((s, f) => s + f.file_size_bytes, 0); + const totalRecords = version.data_files.reduce( + (s, f) => s + (f.record_count ?? 0), + 0 + ); + + return ( +
+ {/* Summary */} +
+

Version {version.snapshot_id}

+
+ {[ + ["Timestamp", formatTimestamp(version.timestamp_ms)], + ["Operation", version.operation], + ["Data Files", formatNumber(version.data_files.length)], + ["Delete Files", formatNumber(version.delete_files.length)], + ["Total Records", formatNumber(totalRecords)], + ["Total Size", formatBytes(totalSize)], + ].map(([label, value]) => ( +
+

{label}

+

{value}

+
+ ))} +
+
+ + {/* Operation summary */} + {Object.keys(version.summary).length > 0 && ( +
+

Commit Summary

+
+ {Object.entries(version.summary).map(([k, v]) => ( +
+ {k}: + {v} +
+ ))} +
+
+ )} + + {/* Schema */} +
+

Schema

+ {loadingSchema ? ( +

Loading schema…

+ ) : schema && schema.length > 0 ? ( + + + + + + + + + + {schema.map((f) => ( + + + + + + ))} + +
FieldTypeNullable
{f.name}{f.type}{f.nullable ? "Yes" : "No"}
+ ) : ( +

No schema available.

+ )} +
+ + {/* Data files */} +
+

+ Data Files ({version.data_files.length}) +

+ {version.data_files.length === 0 ? ( +

No data files in this version.

+ ) : ( +
+ + + + + + + + + + {version.data_files.map((f, i) => ( + + + + + + ))} + +
PathSizeRecords
{f.path}{formatBytes(f.file_size_bytes)} + {f.record_count != null ? formatNumber(f.record_count) : "—"} +
+
+ )} +
+ + {/* Delete files */} + {version.delete_files.length > 0 && ( +
+

+ Delete Files ({version.delete_files.length}) +

+
+ + + + + + + + + {version.delete_files.map((f, i) => ( + + + + + ))} + +
PathSize
{f.path}{formatBytes(f.file_size_bytes)}
+
+
+ )} +
+ ); +} diff --git a/web-ui/src/components/delta/version-history.tsx b/web-ui/src/components/delta/version-history.tsx new file mode 100644 index 0000000..beb4554 --- /dev/null +++ b/web-ui/src/components/delta/version-history.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { formatBytes, formatNumber, formatTimestamp } from "@/lib/utils"; +import type { SnapshotInfo } from "@/lib/types"; + +interface VersionHistoryProps { + versions: SnapshotInfo[]; + selectedId: number | null; + onSelect: (id: number) => void; +} + +export function VersionHistory({ versions, selectedId, onSelect }: VersionHistoryProps) { + return ( +
+ {[...versions].reverse().map((v) => ( + + ))} +
+ ); +} diff --git a/web-ui/src/components/gizmosql/results-grid.tsx b/web-ui/src/components/gizmosql/results-grid.tsx new file mode 100644 index 0000000..e066650 --- /dev/null +++ b/web-ui/src/components/gizmosql/results-grid.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { formatDuration, formatNumber } from "@/lib/utils"; +import type { QueryResult } from "@/lib/types"; + +interface ResultsGridProps { + result: QueryResult; +} + +export function ResultsGrid({ result }: ResultsGridProps) { + return ( +
+
+ {formatNumber(result.row_count)} rows + · + {formatDuration(result.elapsed_ms)} +
+
+ + + + {result.columns.map((col) => ( + + ))} + + + + {result.rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {col} +
+ {cell == null ? ( + null + ) : ( + String(cell) + )} +
+
+
+ ); +} diff --git a/web-ui/src/components/gizmosql/sql-editor.tsx b/web-ui/src/components/gizmosql/sql-editor.tsx new file mode 100644 index 0000000..0d17cf3 --- /dev/null +++ b/web-ui/src/components/gizmosql/sql-editor.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useState } from "react"; +import { Play } from "lucide-react"; + +interface SqlEditorProps { + onExecute: (sql: string) => void; + loading?: boolean; +} + +export function SqlEditor({ onExecute, loading }: SqlEditorProps) { + const [sql, setSql] = useState("SELECT 1 AS test"); + + const handleExecute = () => { + if (sql.trim()) onExecute(sql.trim()); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { + handleExecute(); + } + }; + + return ( +
+