diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..865d0e2 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + root: true, + env: { + browser: true, + es2022: true, + node: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + plugins: ['@typescript-eslint', 'react', 'react-hooks'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + ], + settings: { + react: { version: 'detect' }, + }, + ignorePatterns: ['dist', 'dist-electron', 'node_modules'], + rules: { + // Vite's automatic JSX runtime makes React imports unnecessary + 'react/react-in-jsx-scope': 'off', + // TypeScript types make prop-types redundant + 'react/prop-types': 'off', + // Downgraded: existing code uses `any` in a few places; flag without failing the build + '@typescript-eslint/no-explicit-any': 'warn', + // Downgraded: existing code has unused vars; `_`-prefix is the in-repo convention + // for intentionally unused bindings, so ignore those entirely + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + // Stylistic: literal quotes in JSX prose are fine in this app + 'react/no-unescaped-entities': 'off', + }, +}; diff --git a/.gitignore b/.gitignore index a9eaad0..dca4d71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /.DS_Store +.DS_Store +._* .idea/ __pycache__/ files/loaded-data/ @@ -13,3 +15,54 @@ files/data/SE1_*.evt.gz files/data/data_small*.hdf5 files/data/data_smaller*.hdf5 files/data/test.rmf + +# Large NICER event file (2.4 GB) - download from https://zenodo.org/record/6785435 +files/data/ni1200120106_0mpu7_cl_bary.evt.gz + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm +.pnpm-debug.log* + +# Electron +dist/ +dist-electron/ +out/ +*.asar +*.snap + +# Build output +*.tsbuildinfo +*.log + +# Python virtual environment +.venv/ +venv/ +*.pyc +*.pyo +*.egg-info/ +*.egg +.eggs/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +*.swp +*.swo +*~ + +# Test coverage +coverage/ +.nyc_output/ + +# Pixi +.pixi/ + +# AI reference documentation (local-only) +AI_DOCS/ diff --git a/docs/superpowers/plans/2026-06-10-quicklook-core-pages.md b/docs/superpowers/plans/2026-06-10-quicklook-core-pages.md new file mode 100644 index 0000000..9063141 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-quicklook-core-pages.md @@ -0,0 +1,4163 @@ +# QuickLook Core Pages Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the eleven QuickLook placeholder pages (EventList, LightCurve, PowerSpectrum, AvgPowerSpectrum, CrossSpectrum, AvgCrossSpectrum, DynamicalPowerSpectrum, Bispectrum, Coherence, TimeLags, PowerColors) with working analysis UIs wired to the existing FastAPI endpoints, fixing the four backend correctness bugs that block them. + +**Architecture:** Each page is a self-contained React component: a parameters card (left) driving one POST to the backend, and a Plotly result card (right). Shared machinery is built once — a theme-aware `PlotlyChart`, an `EventListSelector` fed by a TanStack Query hook, and a `useAnalysisRunner` hook that owns the request lifecycle and notifications. Analysis results live in backend memory (StateManager); pages display the create-response payload directly. Backend fixes land first (TDD with pytest) because three endpoints currently return unserializable or scientifically wrong data. + +**Tech Stack:** React 18 + TypeScript + MUI v5 + TanStack Query v5 + Zustand + react-plotly.js (lazy-loaded), FastAPI + Stingray 2.2.10, vitest + @testing-library/react (frontend), pytest via pixi `dev` env (backend). + +**Testing strategy (read before executing):** Backend fixes and all shared frontend logic (hooks, helpers, selector, chart wrapper) are TDD'd. The EventList and LightCurve pages get component tests establishing the display-page and runner-page patterns. The remaining nine pages are declarative wiring of already-tested pieces; their gates are `npm run typecheck`, `npm run lint`, and a manual verification step each — jsdom tests for Plotly-heavy pages would test mocks, not behavior. + +**Git note (user preference):** Commits use conventional format, **no Claude co-authorship**. The user requires confirmation before git commands — at execution start, ask the user for blanket approval of the commit steps in this plan, or pause at each commit step. + +--- + +## Context for an engineer with zero prior knowledge + +Repo root: `/Volumes/Mac Projects/StingrayExplorer`, branch `electron-migration`. + +- Run the app: `npm run dev` (Electron spawns the Python backend itself from `.pixi/envs/default/bin/python`). First cold start can take ~60 s. +- The renderer talks to FastAPI at `http://127.0.0.1:` via `apiClient` ([src/api/client.ts](../../../src/api/client.ts)). Every response has shape `ApiResponse = { success, data, message, error }`. **A failed analysis still returns HTTP 200 with `success: false`** — always branch on `res.success`. +- Frontend API modules already exist and match the backend: `src/api/dataApi.ts`, `lightcurveApi.ts`, `spectrumApi.ts`, `timingApi.ts`. +- Notifications: `useUIStore.getState().addNotification({ type, title, message })` where `type: 'info' | 'success' | 'warning' | 'error'` (`src/store/uiStore.ts:81`). +- Pages live at `src/pages/QuickLook//index.tsx`, currently rendering `PageTemplate` with `status="coming-soon"`. `PageTemplate` (`src/components/common/PageTemplate.tsx`) accepts `status="ready"` and `children`. +- Routes: `src/App.tsx:648-665` (hash router). Sidebar nav: `src/components/layout/Sidebar.tsx:79-98` (submenu items) and `:157-181` (category groupings). **There is no route/page/nav entry yet for Time Lags or Power Colors — Tasks 20-21 add them.** +- Path alias `@/` → `src/` (configured in `electron.vite.config.ts`; vitest config in Task 1 must mirror it). +- Backend services return plain dicts via `BaseService.create_result(success, data, message, error)`. Services are constructed per-request: `LightcurveService(state_manager=request.app.state.state_manager, performance_monitor=...)` — the second arg is optional. +- Python style: 4-space indent. TS style: 2-space indent. + +**Known backend bugs this plan fixes (verified by reading code):** +1. `spectrum_service.py:203,286` — `cs.power.tolist()` on a **complex** array (Stingray cross spectra) → not JSON-serializable; both cross-spectrum endpoints fail. +2. `timing_service.py:305` — coherence computed as `np.abs(cs.unnorm_power)**2` (just |cross power|², values ≫ 1). Must use Stingray's `cs.coherence()`. +3. `timing_service.py:217` — time lags hand-rolled as `np.angle(unnorm_power)/(2πf)`, no uncertainties. Must use `cs.time_lag()`. +4. `lightcurve_routes.py` / `spectrum_routes.py` / `timing_routes.py` — `async def` handlers call synchronous services directly, **blocking the event loop** during computation (freezes SSE job/log streams). `data_routes.py` already shows the fix pattern: `await asyncio.to_thread(...)`. +5. Light curves return full `time`/`counts` arrays — a NICER file at dt=1 ms is tens of millions of bins → renderer death. Add server-side stride decimation for plotting. + +--- + +## File structure + +**Created:** +- `vitest.config.ts`, `src/test/setup.ts`, `src/test/testUtils.tsx` +- `python-backend/tests/__init__.py`, `conftest.py`, `test_spectrum_service.py`, `test_timing_service.py`, `test_lightcurve_service.py`, `test_route_concurrency.py` +- `src/components/plots/PlotlyChart.tsx` (+ test) +- `src/components/analysis/EventListSelector.tsx` (+ test) +- `src/hooks/useEventLists.ts` (+ test), `src/hooks/useAnalysisRunner.ts` (+ test) +- `src/utils/numbers.ts`, `src/utils/powerColors.ts` (+ tests) +- `src/pages/QuickLook/TimeLags/index.tsx`, `src/pages/QuickLook/PowerColors/index.tsx` +- Page tests: `src/pages/QuickLook/EventList/index.test.tsx`, `src/pages/QuickLook/LightCurve/index.test.tsx` + +**Modified:** +- `python-backend/services/spectrum_service.py`, `timing_service.py`, `lightcurve_service.py` +- `python-backend/routes/lightcurve_routes.py`, `spectrum_routes.py`, `timing_routes.py` +- `src/api/lightcurveApi.ts`, `spectrumApi.ts`, `timingApi.ts` +- `src/pages/QuickLook/{EventList,LightCurve,PowerSpectrum,AvgPowerSpectrum,CrossSpectrum,AvgCrossSpectrum,DynamicalPowerSpectrum,Bispectrum,Coherence}/index.tsx` +- `src/App.tsx` (2 imports + 2 routes), `src/components/layout/Sidebar.tsx` (2 nav items + 2 category entries) +- `package.json` (test devDeps) + +--- + +### Task 1: Frontend test infrastructure + +**Files:** +- Create: `vitest.config.ts`, `src/test/setup.ts`, `src/test/testUtils.tsx`, `src/utils/numbers.ts`, `src/utils/numbers.test.ts` +- Modify: `package.json` (devDependencies via npm) + +- [x] **Step 1: Install test dependencies** + +Run: `npm install --save-dev jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event` +Expected: exits 0, package.json devDependencies updated. + +- [x] **Step 2: Create vitest config and setup** + +`vitest.config.ts`: +```ts +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['src/test/setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + }, +}); +``` + +`src/test/setup.ts`: +```ts +import '@testing-library/jest-dom/vitest'; + +// MUI useMediaQuery requires matchMedia, absent in jsdom +if (!window.matchMedia) { + window.matchMedia = (query: string): MediaQueryList => + ({ + matches: false, + media: query, + onchange: null, + addListener: () => undefined, + removeListener: () => undefined, + addEventListener: () => undefined, + removeEventListener: () => undefined, + dispatchEvent: () => false, + }) as MediaQueryList; +} +``` + +`src/test/testUtils.tsx`: +```tsx +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export function renderWithProviders(ui: React.ReactElement): RenderResult { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: 0 } }, + }); + return render( + + {ui} + + ); +} +``` + +- [x] **Step 3: Write a first real test (number parsing helpers used by every page)** + +`src/utils/numbers.test.ts`: +```ts +import { describe, expect, it } from 'vitest'; +import { parseNumber, parsePositiveNumber } from './numbers'; + +describe('parsePositiveNumber', () => { + it('parses valid positive numbers', () => { + expect(parsePositiveNumber('0.0625')).toBe(0.0625); + expect(parsePositiveNumber('32')).toBe(32); + }); + + it('rejects zero, negatives, and junk', () => { + expect(parsePositiveNumber('0')).toBeNull(); + expect(parsePositiveNumber('-1')).toBeNull(); + expect(parsePositiveNumber('abc')).toBeNull(); + expect(parsePositiveNumber('')).toBeNull(); + }); +}); + +describe('parseNumber', () => { + it('parses any finite number', () => { + expect(parseNumber('-2.5')).toBe(-2.5); + expect(parseNumber('0')).toBe(0); + }); + + it('rejects non-numeric input', () => { + expect(parseNumber('1e999')).toBeNull(); + expect(parseNumber('x')).toBeNull(); + expect(parseNumber('')).toBeNull(); + }); +}); +``` + +- [x] **Step 4: Run to verify it fails** + +Run: `npm test -- --run` +Expected: FAIL — `Cannot find module './numbers'` (or equivalent resolve error). + +- [x] **Step 5: Implement the helpers** + +`src/utils/numbers.ts`: +```ts +/** Parse a text-field value into a finite positive number, or null if invalid. */ +export function parsePositiveNumber(value: string): number | null { + if (value.trim() === '') return null; + const n = Number(value); + return Number.isFinite(n) && n > 0 ? n : null; +} + +/** Parse a text-field value into any finite number, or null if invalid. */ +export function parseNumber(value: string): number | null { + if (value.trim() === '') return null; + const n = Number(value); + return Number.isFinite(n) ? n : null; +} +``` + +- [x] **Step 6: Run to verify it passes** + +Run: `npm test -- --run` +Expected: PASS (4 tests). + +- [x] **Step 7: Commit** + +```bash +git add vitest.config.ts src/test/ src/utils/numbers.ts src/utils/numbers.test.ts package.json package-lock.json +git commit -m "chore: add vitest + testing-library infrastructure" +``` + +--- + +### Task 2: Python test infrastructure + +**Files:** +- Create: `python-backend/tests/__init__.py`, `python-backend/tests/conftest.py`, `python-backend/tests/test_smoke.py` + +- [x] **Step 1: Ensure the pixi dev environment exists** + +Run: `pixi install -e dev` +Expected: exits 0 (installs pytest, pytest-asyncio into `.pixi/envs/dev`). May take a few minutes the first time. + +- [x] **Step 2: Create the test package and fixtures** + +`python-backend/tests/__init__.py`: empty file. + +`python-backend/tests/conftest.py`: +```python +"""Shared fixtures for backend service tests. + +python-backend is not an installable package (hyphenated dir name), so tests +add it to sys.path and import the same way main.py does (cwd=python-backend). +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import numpy as np +import pytest +from stingray import EventList + +from services.state_manager import StateManager + + +def make_event_list(seed: int, n_events: int = 20000, length: float = 64.0) -> EventList: + """Deterministic synthetic event list spanning [0, length] seconds.""" + rng = np.random.default_rng(seed) + times = np.sort(rng.uniform(0.0, length, n_events)) + energy = rng.uniform(0.5, 10.0, n_events) + return EventList(time=times, energy=energy, gti=[[0.0, length]]) + + +@pytest.fixture() +def state_manager() -> StateManager: + return StateManager() + + +@pytest.fixture() +def loaded_state(state_manager: StateManager) -> StateManager: + state_manager.add_event_data("ev1", make_event_list(1)) + state_manager.add_event_data("ev2", make_event_list(2)) + return state_manager +``` + +`python-backend/tests/test_smoke.py`: +```python +def test_services_import_and_state_works(loaded_state): + assert loaded_state.has_event_data("ev1") + assert loaded_state.has_event_data("ev2") + assert len(loaded_state.get_event_data("ev1").time) == 20000 +``` + +- [x] **Step 3: Run the smoke test** + +Run: `pixi run -e dev pytest python-backend/tests -v` +Expected: PASS (1 test). If `add_event_data` has a different name, check `python-backend/services/state_manager.py:48` — it is `add_event_data(name, event_list)`. + +- [x] **Step 4: Commit** + +```bash +git add python-backend/tests/ +git commit -m "chore: add pytest scaffolding for python backend" +``` + +--- + +### Task 3: Fix cross-spectrum complex power serialization (backend) + +**Files:** +- Create: `python-backend/tests/test_spectrum_service.py` +- Modify: `python-backend/services/spectrum_service.py` + +- [x] **Step 1: Write the failing tests** + +`python-backend/tests/test_spectrum_service.py`: +```python +import json + +from services.spectrum_service import SpectrumService + + +def test_cross_spectrum_is_strict_json_serializable(loaded_state): + svc = SpectrumService(loaded_state) + result = svc.create_cross_spectrum("ev1", "ev2", dt=0.0625) + assert result["success"], result + json.dumps(result, allow_nan=False) # complex or NaN values raise here + data = result["data"] + assert all(isinstance(p, float) for p in data["power"][:10]) + assert data["power_phase"] is not None + assert len(data["power_phase"]) == len(data["power"]) + + +def test_averaged_cross_spectrum_is_strict_json_serializable(loaded_state): + svc = SpectrumService(loaded_state) + result = svc.create_averaged_cross_spectrum( + "ev1", "ev2", dt=0.0625, segment_size=8.0 + ) + assert result["success"], result + json.dumps(result, allow_nan=False) + assert result["data"]["power_phase"] is not None + + +def test_power_spectrum_has_null_phase(loaded_state): + svc = SpectrumService(loaded_state) + result = svc.create_power_spectrum("ev1", dt=0.0625) + assert result["success"], result + json.dumps(result, allow_nan=False) + assert result["data"]["power_phase"] is None + + +def test_rebin_of_stored_cross_spectrum_serializes(loaded_state): + svc = SpectrumService(loaded_state) + created = svc.create_cross_spectrum("ev1", "ev2", dt=0.0625, output_name="cs1") + assert created["success"], created + rebinned = svc.rebin_spectrum("cs1", rebin_factor=0.1, log=True) + assert rebinned["success"], rebinned + json.dumps(rebinned, allow_nan=False) +``` + +- [x] **Step 2: Run to verify they fail** + +Run: `pixi run -e dev pytest python-backend/tests/test_spectrum_service.py -v` +Expected: FAIL — `TypeError: Object of type complex is not JSON serializable` (cross-spectrum tests) and `KeyError: 'power_phase'` (power-spectrum test). + +- [x] **Step 3: Implement the fix** + +In `python-backend/services/spectrum_service.py`, add after the imports (below line 18): + +```python +def _power_to_lists(power) -> tuple: + """Split a (possibly complex) power array into JSON-safe magnitude and phase lists. + + Returns (power_list, phase_list_or_None). Non-finite values become None so + strict JSON (and JS JSON.parse) never sees NaN/Infinity. + """ + arr = np.asarray(power) + if np.iscomplexobj(arr): + mag = np.abs(arr) + phase = np.angle(arr) + return _finite_list(mag), _finite_list(phase) + return _finite_list(arr.astype(float)), None + + +def _finite_list(arr) -> list: + """Convert a float array to a list, replacing non-finite values with None.""" + values = np.asarray(arr, dtype=float) + return [float(v) if np.isfinite(v) else None for v in values] +``` + +Replace the four response dict constructions: + +1. `create_power_spectrum` (lines 64-72) — replace with: +```python + power_list, phase_list = _power_to_lists(ps.power) + ps_data = { + "name": output_name, + "freq": ps.freq.tolist(), + "power": power_list, + "power_phase": phase_list, + "norm": norm, + "n_freq": len(ps.freq), + "df": float(ps.df), + "freq_range": [float(ps.freq[0]), float(ps.freq[-1])], + } +``` + +2. `create_averaged_power_spectrum` (lines 122-131) — replace with: +```python + power_list, phase_list = _power_to_lists(ps.power) + ps_data = { + "name": output_name, + "freq": ps.freq.tolist(), + "power": power_list, + "power_phase": phase_list, + "norm": norm, + "n_freq": len(ps.freq), + "df": float(ps.df), + "segment_size": segment_size, + "n_segments": int(ps.m) if hasattr(ps, "m") else None, + } +``` + +3. `create_cross_spectrum` (lines 200-207) — replace with: +```python + power_list, phase_list = _power_to_lists(cs.power) + cs_data = { + "name": output_name, + "freq": cs.freq.tolist(), + "power": power_list, + "power_phase": phase_list, + "norm": norm, + "n_freq": len(cs.freq), + "df": float(cs.df), + } +``` + +4. `create_averaged_cross_spectrum` (lines 281-289) — replace with: +```python + power_list, phase_list = _power_to_lists(cs.power) + cs_data = { + "name": output_name, + "freq": cs.freq.tolist(), + "power": power_list, + "power_phase": phase_list, + "norm": norm, + "n_freq": len(cs.freq), + "df": float(cs.df), + "segment_size": segment_size, + } +``` + +5. `rebin_spectrum` (lines 409-414) — replace with: +```python + power_list, phase_list = _power_to_lists(rebinned.power) + data = { + "name": output_name, + "freq": rebinned.freq.tolist(), + "power": power_list, + "power_phase": phase_list, + "n_freq": len(rebinned.freq), + } +``` + +- [x] **Step 4: Run to verify they pass** + +Run: `pixi run -e dev pytest python-backend/tests/test_spectrum_service.py -v` +Expected: PASS (4 tests). + +- [x] **Step 5: Commit** + +```bash +git add python-backend/services/spectrum_service.py python-backend/tests/test_spectrum_service.py +git commit -m "fix: serialize complex cross-spectrum power as magnitude and phase" +``` + +--- + +### Task 4: Fix coherence and time lags (backend) + +**Files:** +- Create: `python-backend/tests/test_timing_service.py` +- Modify: `python-backend/services/timing_service.py` + +- [x] **Step 1: Write the failing tests** + +`python-backend/tests/test_timing_service.py`: +```python +import json + +import numpy as np + +from services.timing_service import TimingService + + +def test_coherence_of_identical_signals_is_one(loaded_state): + svc = TimingService(loaded_state) + # An event list crossed with itself has coherence == 1 at all frequencies. + result = svc.calculate_coherence("ev1", "ev1", dt=0.0625, segment_size=8.0) + assert result["success"], result + json.dumps(result, allow_nan=False) + coh = np.asarray(result["data"]["coherence"], dtype=float) + assert np.all(coh <= 1.0 + 1e-6) + assert np.median(coh) > 0.9 + + +def test_coherence_includes_uncertainty(loaded_state): + svc = TimingService(loaded_state) + result = svc.calculate_coherence("ev1", "ev2", dt=0.0625, segment_size=8.0) + assert result["success"], result + data = result["data"] + assert "coherence_err" in data + if data["coherence_err"] is not None: + assert len(data["coherence_err"]) == len(data["coherence"]) + + +def test_time_lags_include_errors_and_serialize(loaded_state): + svc = TimingService(loaded_state) + result = svc.calculate_time_lags("ev1", "ev2", dt=0.0625, segment_size=8.0) + assert result["success"], result + json.dumps(result, allow_nan=False) + data = result["data"] + assert "time_lags_err" in data + assert len(data["freq"]) == len(data["time_lags"]) + if data["time_lags_err"] is not None: + assert len(data["time_lags_err"]) == len(data["time_lags"]) + + +def test_time_lags_freq_range_filters_all_arrays(loaded_state): + svc = TimingService(loaded_state) + full = svc.calculate_time_lags("ev1", "ev2", dt=0.0625, segment_size=8.0) + sub = svc.calculate_time_lags( + "ev1", "ev2", dt=0.0625, segment_size=8.0, freq_range=(0.5, 2.0) + ) + assert sub["success"], sub + freqs = np.asarray(sub["data"]["freq"], dtype=float) + assert freqs.min() >= 0.5 + assert freqs.max() <= 2.0 + assert len(sub["data"]["freq"]) < len(full["data"]["freq"]) + assert len(sub["data"]["time_lags"]) == len(sub["data"]["freq"]) +``` + +- [x] **Step 2: Run to verify they fail** + +Run: `pixi run -e dev pytest python-backend/tests/test_timing_service.py -v` +Expected: FAIL — coherence values ≫ 1 (first test), `KeyError`/missing `coherence_err` and `time_lags_err`. + +- [x] **Step 3: Implement the fix** + +In `python-backend/services/timing_service.py`: + +Add after the imports (below line 12): +```python +def _finite_list(arr) -> list: + """Convert a float array to a list, replacing non-finite values with None.""" + values = np.asarray(arr, dtype=float) + return [float(v) if np.isfinite(v) else None for v in values] +``` + +In `calculate_time_lags`, replace lines 215-230 (from `# Calculate time lags` through the `result_data = {...}` block) with: +```python + # Stingray's time_lag() returns (lag, lag_err) for averaged spectra. + lag_result = cs.time_lag() + if isinstance(lag_result, tuple): + time_lags, time_lags_err = lag_result + else: + time_lags, time_lags_err = lag_result, None + + freq = np.asarray(cs.freq, dtype=float) + time_lags = np.real(np.asarray(time_lags)) + if time_lags_err is not None: + time_lags_err = np.real(np.asarray(time_lags_err)) + + if freq_range: + mask = (freq >= freq_range[0]) & (freq <= freq_range[1]) + freq = freq[mask] + time_lags = time_lags[mask] + if time_lags_err is not None: + time_lags_err = time_lags_err[mask] + + result_data = { + "name": output_name, + "freq": freq.tolist(), + "time_lags": _finite_list(time_lags), + "time_lags_err": _finite_list(time_lags_err) if time_lags_err is not None else None, + "freq_range": freq_range, + } +``` + +In `calculate_coherence`, replace lines 304-311 (from `# Calculate coherence` through the `result_data = {...}` block) with: +```python + # Stingray's coherence() returns (coherence, uncertainty) for + # averaged cross spectra (Vaughan & Nowak 1997). + coh_result = cs.coherence() + if isinstance(coh_result, tuple): + coherence_vals, coherence_err = coh_result + else: + coherence_vals, coherence_err = coh_result, None + + coherence_vals = np.real(np.asarray(coherence_vals)) + result_data = { + "name": output_name, + "freq": cs.freq.tolist(), + "coherence": _finite_list(coherence_vals), + "coherence_err": _finite_list(np.real(np.asarray(coherence_err))) + if coherence_err is not None + else None, + "segment_size": segment_size, + "n_segments": int(cs.m) if hasattr(cs, "m") else None, + } +``` + +- [x] **Step 4: Run to verify they pass** + +Run: `pixi run -e dev pytest python-backend/tests/test_timing_service.py -v` +Expected: PASS (4 tests). Also run the full suite: `pixi run -e dev pytest python-backend/tests -v` — all green. + +- [x] **Step 5: Commit** + +```bash +git add python-backend/services/timing_service.py python-backend/tests/test_timing_service.py +git commit -m "fix: use stingray coherence() and time_lag() with uncertainties" +``` + +--- + +### Task 5: Light-curve plot decimation (backend) + +**Files:** +- Create: `python-backend/tests/test_lightcurve_service.py` +- Modify: `python-backend/services/lightcurve_service.py`, `python-backend/routes/lightcurve_routes.py` + +- [x] **Step 1: Write the failing tests** + +`python-backend/tests/test_lightcurve_service.py`: +```python +import json + +from services.lightcurve_service import LightcurveService + + +def test_decimation_caps_returned_points(loaded_state): + svc = LightcurveService(loaded_state) + # 64 s span at dt=0.001 -> 64000 bins; cap at 5000 plot points. + result = svc.create_lightcurve_from_event_list( + "ev1", dt=0.001, output_name="lc_fine", max_points=5000 + ) + assert result["success"], result + json.dumps(result, allow_nan=False) + data = result["data"] + assert data["n_bins"] == 64000 # true resolution is reported + assert len(data["time"]) <= 5000 # transferred arrays are capped + assert data["plot_stride"] == 13 # ceil(64000 / 5000) + assert len(data["time"]) == len(data["counts"]) + + +def test_no_decimation_below_cap(loaded_state): + svc = LightcurveService(loaded_state) + result = svc.create_lightcurve_from_event_list("ev1", dt=1.0, output_name="lc_coarse") + assert result["success"], result + data = result["data"] + assert data["plot_stride"] == 1 + assert len(data["time"]) == data["n_bins"] + + +def test_get_lightcurve_data_decimates(loaded_state): + svc = LightcurveService(loaded_state) + svc.create_lightcurve_from_event_list("ev1", dt=0.001, output_name="lc_fine2") + result = svc.get_lightcurve_data("lc_fine2", max_points=1000) + assert result["success"], result + assert len(result["data"]["time"]) <= 1000 + assert result["data"]["plot_stride"] == 64 +``` + +- [x] **Step 2: Run to verify they fail** + +Run: `pixi run -e dev pytest python-backend/tests/test_lightcurve_service.py -v` +Expected: FAIL — `TypeError: ... unexpected keyword argument 'max_points'`. + +- [x] **Step 3: Implement decimation in the service** + +In `python-backend/services/lightcurve_service.py`: + +Add after the imports (below line 12): +```python +# Cap on points transferred for plotting. The full-resolution Lightcurve stays +# in StateManager; only the JSON payload is strided. +DEFAULT_MAX_PLOT_POINTS = 200_000 + + +def _decimate_for_plot(time, counts, max_points): + """Stride-decimate arrays for display. Returns (time, counts, stride).""" + n = len(time) + if not max_points or n <= max_points: + return time, counts, 1 + stride = int(np.ceil(n / max_points)) + return time[::stride], counts[::stride], stride +``` + +Change `create_lightcurve_from_event_list` signature (line 22-28) to: +```python + def create_lightcurve_from_event_list( + self, + event_list_name: str, + dt: float, + output_name: str, + gti: Optional[List[List[float]]] = None, + max_points: Optional[int] = DEFAULT_MAX_PLOT_POINTS, + ) -> Dict[str, Any]: +``` +and replace its `lc_data = {...}` block (lines 64-72) with: +```python + plot_time, plot_counts, stride = _decimate_for_plot(lc.time, lc.counts, max_points) + lc_data = { + "name": output_name, + "time": plot_time.tolist(), + "counts": plot_counts.tolist(), + "dt": float(lc.dt), + "n_bins": len(lc.time), + "plot_stride": stride, + "time_range": [float(lc.time.min()), float(lc.time.max())], + "count_rate_mean": float(np.mean(lc.counts / lc.dt)), + } +``` + +Change `rebin_lightcurve` signature (lines 130-135) to add `max_points: Optional[int] = DEFAULT_MAX_PLOT_POINTS,` after `output_name: str,` and replace its `lc_data = {...}` block (lines 162-168) with: +```python + plot_time, plot_counts, stride = _decimate_for_plot( + rebinned_lc.time, rebinned_lc.counts, max_points + ) + lc_data = { + "name": output_name, + "time": plot_time.tolist(), + "counts": plot_counts.tolist(), + "dt": float(rebinned_lc.dt), + "n_bins": len(rebinned_lc.time), + "plot_stride": stride, + } +``` + +Change `get_lightcurve_data` signature (line 181) to: +```python + def get_lightcurve_data( + self, name: str, max_points: Optional[int] = DEFAULT_MAX_PLOT_POINTS + ) -> Dict[str, Any]: +``` +and replace its `lc_data = {...}` block (lines 202-215) with: +```python + plot_time, plot_counts, stride = _decimate_for_plot(lc.time, lc.counts, max_points) + lc_data = { + "name": name, + "time": plot_time.tolist(), + "counts": plot_counts.tolist(), + "dt": float(lc.dt), + "n_bins": len(lc.time), + "plot_stride": stride, + "time_range": [float(lc.time.min()), float(lc.time.max())], + "count_stats": { + "mean": float(np.mean(lc.counts)), + "std": float(np.std(lc.counts)), + "min": float(np.min(lc.counts)), + "max": float(np.max(lc.counts)), + }, + } +``` + +- [x] **Step 4: Plumb max_points through the routes** + +In `python-backend/routes/lightcurve_routes.py`: + +`CreateLightcurveFromEventListRequest` (lines 24-28) — add field: +```python +class CreateLightcurveFromEventListRequest(BaseModel): + event_list_name: str + dt: float + output_name: str + gti: Optional[List[List[float]]] = None + max_points: Optional[int] = 200000 +``` + +`RebinLightcurveRequest` (lines 38-41) — add field: +```python +class RebinLightcurveRequest(BaseModel): + name: str + rebin_factor: float + output_name: str + max_points: Optional[int] = 200000 +``` + +Pass them through in the handlers: in `create_lightcurve_from_event_list` add `max_points=request.max_points,` to the service call; in `rebin_lightcurve` add `max_points=request.max_points,`; change `get_lightcurve_data` (lines 86-92) to: +```python +@router.get("/{name}") +async def get_lightcurve_data( + name: str, + max_points: int = 200000, + service: LightcurveService = Depends(get_lightcurve_service), +): + """Get lightcurve data for plotting.""" + return service.get_lightcurve_data(name, max_points=max_points) +``` + +- [x] **Step 5: Run to verify they pass** + +Run: `pixi run -e dev pytest python-backend/tests -v` +Expected: PASS (all tests, including previous tasks'). + +- [x] **Step 6: Commit** + +```bash +git add python-backend/services/lightcurve_service.py python-backend/routes/lightcurve_routes.py python-backend/tests/test_lightcurve_service.py +git commit -m "feat: add server-side plot decimation for large lightcurves" +``` + +--- + +### Task 6: Unblock the event loop in analysis routes + +**Files:** +- Create: `python-backend/tests/test_route_concurrency.py` +- Modify: `python-backend/routes/lightcurve_routes.py`, `python-backend/routes/spectrum_routes.py`, `python-backend/routes/timing_routes.py` + +- [x] **Step 1: Write the failing test** + +`python-backend/tests/test_route_concurrency.py`: +```python +"""Verify analysis routes run blocking work off the event loop. + +A handler that calls the synchronous service directly blocks the loop, so a +concurrent "/" request cannot complete until the slow call finishes. With +asyncio.to_thread, the probe returns immediately. +""" + +import asyncio +import time + +import httpx +import pytest + +from services.state_manager import StateManager +from utils.performance_monitor import PerformanceMonitor + + +@pytest.mark.asyncio +async def test_lightcurve_create_does_not_block_event_loop(monkeypatch): + import services.lightcurve_service as lcs_mod + from main import create_app + + def slow_create(self, **kwargs): + time.sleep(0.6) + return {"success": True, "data": None, "message": "ok", "error": None} + + monkeypatch.setattr( + lcs_mod.LightcurveService, "create_lightcurve_from_event_list", slow_create + ) + + app = create_app() + # ASGITransport does not run the lifespan; provide state manually. + app.state.state_manager = StateManager() + app.state.performance_monitor = PerformanceMonitor() + + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + slow_task = asyncio.create_task( + client.post( + "/api/lightcurve/from-event-list", + json={"event_list_name": "x", "dt": 0.1, "output_name": "y"}, + ) + ) + await asyncio.sleep(0.05) # let the slow handler start + + t0 = time.monotonic() + probe = await client.get("/") + elapsed = time.monotonic() - t0 + + slow_response = await slow_task + assert probe.status_code == 200 + assert slow_response.status_code == 200 + # Without to_thread the probe waits ~0.55s for the loop to free up. + assert elapsed < 0.4, f"event loop was blocked for {elapsed:.2f}s" +``` + +- [x] **Step 2: Run to verify it fails** + +Run: `pixi run -e dev pytest python-backend/tests/test_route_concurrency.py -v` +Expected: FAIL — `event loop was blocked for ~0.55s`. (If it fails with an import error on `create_app`, check `python-backend/main.py:86` for the factory name.) + +- [x] **Step 3: Wrap service calls in asyncio.to_thread** + +Pattern (matches `data_routes.py`): add `import asyncio` to the imports of each file, and change every handler that calls a service method doing computation from `return service.method(...)` to `return await asyncio.to_thread(service.method, ...)` with the same keyword arguments. + +`python-backend/routes/lightcurve_routes.py` — wrap all 6 handlers. Example for the first: +```python +import asyncio +``` +```python +@router.post("/from-event-list") +async def create_lightcurve_from_event_list( + request: CreateLightcurveFromEventListRequest, + service: LightcurveService = Depends(get_lightcurve_service), +): + """Create a Lightcurve from an EventList.""" + return await asyncio.to_thread( + service.create_lightcurve_from_event_list, + event_list_name=request.event_list_name, + dt=request.dt, + output_name=request.output_name, + gti=request.gti, + max_points=request.max_points, + ) +``` +Apply the same transformation to `create_lightcurve_from_arrays`, `rebin_lightcurve`, `get_lightcurve_data` (`await asyncio.to_thread(service.get_lightcurve_data, name, max_points=max_points)`), `list_lightcurves`, and `delete_lightcurve`. + +`python-backend/routes/spectrum_routes.py` — same for all 8 handlers (`create_power_spectrum`, `create_averaged_power_spectrum`, `create_cross_spectrum`, `create_averaged_cross_spectrum`, `create_dynamical_power_spectrum`, `rebin_spectrum`, `list_spectra`, `delete_spectrum`), preserving each handler's existing keyword arguments. + +`python-backend/routes/timing_routes.py` — same for all 4 handlers (`create_bispectrum`, `calculate_power_colors`, `calculate_time_lags`, `calculate_coherence`). + +- [x] **Step 4: Run to verify it passes** + +Run: `pixi run -e dev pytest python-backend/tests -v` +Expected: PASS (all backend tests). + +- [x] **Step 5: Commit** + +```bash +git add python-backend/routes/lightcurve_routes.py python-backend/routes/spectrum_routes.py python-backend/routes/timing_routes.py python-backend/tests/test_route_concurrency.py +git commit -m "fix: run analysis routes in worker threads to keep event loop responsive" +``` + +--- + +### Task 7: Frontend API type updates + +**Files:** +- Modify: `src/api/lightcurveApi.ts`, `src/api/spectrumApi.ts`, `src/api/timingApi.ts` + +- [x] **Step 1: Update the types and params** + +`src/api/spectrumApi.ts` — in `PowerSpectrumData` (lines 8-18), add after `power: number[];`: +```ts + power_phase?: Array | null; +``` + +`src/api/timingApi.ts` — replace `TimeLagsData` and `CoherenceData` (lines 27-38) with: +```ts +export interface TimeLagsData { + name: string | null; + freq: number[]; + time_lags: Array; + time_lags_err?: Array | null; + freq_range: [number, number] | null; +} + +export interface CoherenceData { + name: string | null; + freq: number[]; + coherence: Array; + coherence_err?: Array | null; + segment_size?: number; + n_segments?: number | null; +} +``` + +`src/api/lightcurveApi.ts`: +- In `LightcurveData` (lines 8-22), add after `n_bins: number;`: +```ts + plot_stride?: number; +``` +- In `createFromEventList`, add `max_points?: number;` to the params type and `max_points: params.max_points,` to the POST body. +- In `rebin`, add `max_points?: number;` to the params type and `max_points: params.max_points,` to the POST body. +- Replace `getLightcurveData` with: +```ts + /** + * Get lightcurve data for plotting + */ + async getLightcurveData( + name: string, + maxPoints?: number + ): Promise> { + const query = maxPoints ? `?max_points=${maxPoints}` : ''; + return apiClient.get(`/api/lightcurve/${name}${query}`); + }, +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: both exit 0. + +- [x] **Step 3: Commit** + +```bash +git add src/api/lightcurveApi.ts src/api/spectrumApi.ts src/api/timingApi.ts +git commit -m "feat: extend api types for phase, uncertainties, and plot decimation" +``` + +--- + +### Task 8: PlotlyChart shared component + +**Files:** +- Create: `src/components/plots/PlotlyChart.tsx`, `src/components/plots/PlotlyChart.test.tsx` + +- [x] **Step 1: Write the failing test** + +`src/components/plots/PlotlyChart.test.tsx`: +```tsx +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('react-plotly.js', () => ({ + default: ({ data, layout }: { data: unknown[]; layout: Record }) => ( +
+ ), +})); + +import PlotlyChart from './PlotlyChart'; + +describe('PlotlyChart', () => { + it('renders traces and merges page layout over theme defaults', async () => { + render( + + ); + await waitFor(() => expect(screen.getByTestId('plotly-mock')).toBeInTheDocument()); + expect(screen.getByTestId('plotly-mock').dataset.traces).toBe('1'); + expect(screen.getByTestId('plotly-mock').dataset.xtype).toBe('log'); + }); +}); +``` + +- [x] **Step 2: Run to verify it fails** + +Run: `npm test -- --run src/components/plots` +Expected: FAIL — cannot resolve `./PlotlyChart`. + +- [x] **Step 3: Implement** + +`src/components/plots/PlotlyChart.tsx`: +```tsx +import React, { Suspense } from 'react'; +import { Box, CircularProgress, useTheme } from '@mui/material'; +import type { Config, Data, Layout } from 'plotly.js'; + +// plotly.js is ~3 MB; load it only when a page actually renders a chart. +const Plot = React.lazy(() => import('react-plotly.js')); + +export interface PlotlyChartProps { + data: Data[]; + layout?: Partial; + height?: number | string; +} + +const PlotlyChart: React.FC = ({ data, layout = {}, height = 440 }) => { + const theme = useTheme(); + const isDark = theme.palette.mode === 'dark'; + const gridColor = isDark ? 'rgba(148, 163, 184, 0.12)' : 'rgba(100, 116, 139, 0.2)'; + + const mergedLayout: Partial = { + autosize: true, + paper_bgcolor: 'rgba(0,0,0,0)', + plot_bgcolor: 'rgba(0,0,0,0)', + font: { + family: '"IBM Plex Sans", sans-serif', + size: 12, + color: theme.palette.text.primary, + }, + margin: { l: 64, r: 24, t: 24, b: 52 }, + showlegend: false, + ...layout, + xaxis: { gridcolor: gridColor, zeroline: false, ...layout.xaxis }, + yaxis: { gridcolor: gridColor, zeroline: false, ...layout.yaxis }, + }; + + const config: Partial = { + responsive: true, + displaylogo: false, + modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'], + }; + + return ( + + + + } + > + + + ); +}; + +export default PlotlyChart; +``` + +- [x] **Step 4: Run to verify it passes** + +Run: `npm test -- --run src/components/plots && npm run typecheck` +Expected: PASS. + +- [x] **Step 5: Commit** + +```bash +git add src/components/plots/ +git commit -m "feat: add theme-aware lazy-loaded PlotlyChart component" +``` + +--- + +### Task 9: useEventLists hook + EventListSelector + +**Files:** +- Create: `src/hooks/useEventLists.ts`, `src/hooks/useEventLists.test.tsx`, `src/components/analysis/EventListSelector.tsx`, `src/components/analysis/EventListSelector.test.tsx` + +- [x] **Step 1: Write the failing hook test** + +`src/hooks/useEventLists.test.tsx`: +```tsx +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const listEventLists = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { listEventLists: (...args: unknown[]) => listEventLists(...args) }, +})); + +import { useEventLists } from './useEventLists'; + +const wrapper = ({ children }: { children: React.ReactNode }): React.ReactElement => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return {children}; +}; + +describe('useEventLists', () => { + beforeEach(() => listEventLists.mockReset()); + + it('returns event list summaries on success', async () => { + listEventLists.mockResolvedValue({ + success: true, + data: [{ name: 'ev1', n_events: 10, time_range: [0, 1] }], + message: '', + error: null, + }); + const { result } = renderHook(() => useEventLists(), { wrapper }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.[0].name).toBe('ev1'); + }); + + it('surfaces a success:false response as a query error', async () => { + listEventLists.mockResolvedValue({ success: false, data: null, message: 'boom', error: 'boom' }); + const { result } = renderHook(() => useEventLists(), { wrapper }); + await waitFor(() => expect(result.current.isError).toBe(true)); + expect((result.current.error as Error).message).toBe('boom'); + }); +}); +``` + +- [x] **Step 2: Run to verify it fails, then implement the hook** + +Run: `npm test -- --run src/hooks/useEventLists` → FAIL (module not found). + +`src/hooks/useEventLists.ts`: +```ts +import { useQuery } from '@tanstack/react-query'; +import { dataApi, EventListSummary } from '@/api/dataApi'; + +export const EVENT_LISTS_QUERY_KEY = ['eventLists'] as const; + +/** + * Loaded event lists from the backend. The ApiClient re-resolves the backend + * port on every request, so this works without gating on backend readiness; + * failures surface as query errors with a retry affordance in the UI. + */ +export function useEventLists() { + return useQuery({ + queryKey: EVENT_LISTS_QUERY_KEY, + queryFn: async (): Promise => { + const res = await dataApi.listEventLists(); + if (!res.success) { + throw new Error(res.error || res.message || 'Failed to list event lists'); + } + return res.data ?? []; + }, + staleTime: 5_000, + }); +} +``` + +Run: `npm test -- --run src/hooks/useEventLists` → PASS. + +- [x] **Step 3: Write the failing selector test** + +`src/components/analysis/EventListSelector.test.tsx`: +```tsx +import React, { useState } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '@/test/testUtils'; + +const listEventLists = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { listEventLists: (...args: unknown[]) => listEventLists(...args) }, +})); + +import EventListSelector from './EventListSelector'; + +const Harness: React.FC = () => { + const [value, setValue] = useState(''); + return ; +}; + +describe('EventListSelector', () => { + beforeEach(() => listEventLists.mockReset()); + + it('lists loaded event lists and selects one', async () => { + listEventLists.mockResolvedValue({ + success: true, + data: [ + { name: 'obs1', n_events: 1000, time_range: [0, 10] }, + { name: 'obs2', n_events: 2000, time_range: [0, 20] }, + ], + message: '', + error: null, + }); + renderWithProviders(); + const select = await screen.findByLabelText('Event list'); + await userEvent.click(select); + await userEvent.click(await screen.findByText(/obs2/)); + await waitFor(() => expect(screen.getByLabelText('Event list')).toHaveTextContent('obs2')); + }); + + it('shows an empty-state prompt linking to data ingestion', async () => { + listEventLists.mockResolvedValue({ success: true, data: [], message: '', error: null }); + renderWithProviders(); + expect(await screen.findByText(/No event lists loaded/)).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /Load data/ })).toHaveAttribute( + 'href', + '/data-ingestion' + ); + }); +}); +``` + +- [x] **Step 4: Run to verify it fails, then implement the selector** + +Run: `npm test -- --run src/components/analysis` → FAIL. + +`src/components/analysis/EventListSelector.tsx`: +```tsx +import React from 'react'; +import { + Alert, + Box, + CircularProgress, + FormControl, + IconButton, + InputLabel, + Link, + MenuItem, + Select, + Tooltip, +} from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { Link as RouterLink } from 'react-router-dom'; +import { useEventLists } from '@/hooks/useEventLists'; + +interface EventListSelectorProps { + label: string; + value: string; + onChange: (name: string) => void; +} + +/** Dropdown of event lists currently loaded in the backend. */ +const EventListSelector: React.FC = ({ label, value, onChange }) => { + const { data, isLoading, isError, error, refetch, isFetching } = useEventLists(); + + const refreshButton = ( + + + refetch()} disabled={isFetching}> + {isFetching ? : } + + + + ); + + if (isError) { + return ( + + Failed to load event lists: {error instanceof Error ? error.message : 'unknown error'} + + ); + } + + if (!isLoading && (data?.length ?? 0) === 0) { + return ( + + No event lists loaded.{' '} + + Load data + {' '} + first. + + ); + } + + const labelId = `event-list-selector-${label.replace(/\s+/g, '-').toLowerCase()}`; + + return ( + + + {label} + + + {refreshButton} + + ); +}; + +export default EventListSelector; +``` + +Run: `npm test -- --run src/components/analysis src/hooks` → PASS. + +- [x] **Step 5: Commit** + +```bash +git add src/hooks/useEventLists.ts src/hooks/useEventLists.test.tsx src/components/analysis/ +git commit -m "feat: add event list query hook and selector component" +``` + +--- + +### Task 10: useAnalysisRunner hook + +**Files:** +- Create: `src/hooks/useAnalysisRunner.ts`, `src/hooks/useAnalysisRunner.test.tsx` + +- [x] **Step 1: Write the failing test** + +`src/hooks/useAnalysisRunner.test.tsx`: +```tsx +import { beforeEach, describe, expect, it } from 'vitest'; +import { act, renderHook } from '@testing-library/react'; +import { useAnalysisRunner } from './useAnalysisRunner'; +import { useUIStore } from '@/store/uiStore'; + +describe('useAnalysisRunner', () => { + beforeEach(() => { + useUIStore.setState({ notifications: [], unreadNotificationCount: 0 }); + }); + + it('stores the result and pushes a success notification', async () => { + const { result } = renderHook(() => useAnalysisRunner<{ v: number }>('Test Op')); + await act(async () => { + await result.current.run(async () => ({ + success: true, + data: { v: 42 }, + message: 'computed', + error: null, + })); + }); + expect(result.current.result?.v).toBe(42); + expect(result.current.running).toBe(false); + expect(result.current.error).toBeNull(); + const notes = useUIStore.getState().notifications; + expect(notes[0].type).toBe('success'); + expect(notes[0].title).toBe('Test Op'); + }); + + it('captures success:false as an error and keeps the previous result', async () => { + const { result } = renderHook(() => useAnalysisRunner<{ v: number }>('Test Op')); + await act(async () => { + await result.current.run(async () => ({ + success: true, + data: { v: 1 }, + message: '', + error: null, + })); + }); + await act(async () => { + await result.current.run(async () => ({ + success: false, + data: null, + message: 'bad dt', + error: 'bad dt', + })); + }); + expect(result.current.error).toBe('bad dt'); + expect(result.current.result?.v).toBe(1); + expect(useUIStore.getState().notifications[0].type).toBe('error'); + }); + + it('captures thrown errors (network failures)', async () => { + const { result } = renderHook(() => useAnalysisRunner('Test Op')); + await act(async () => { + await result.current.run(async () => { + throw new Error('connection refused'); + }); + }); + expect(result.current.error).toBe('connection refused'); + }); +}); +``` + +- [x] **Step 2: Run to verify it fails, then implement** + +Run: `npm test -- --run src/hooks/useAnalysisRunner` → FAIL. + +`src/hooks/useAnalysisRunner.ts`: +```ts +import { useCallback, useState } from 'react'; +import { ApiResponse } from '@/api/client'; +import { useUIStore } from '@/store/uiStore'; + +interface AnalysisRunnerState { + result: T | null; + running: boolean; + error: string | null; +} + +/** + * Owns the lifecycle of a single analysis request: running flag, last + * successful result, last error, and success/error notifications. + * On failure the previous result is kept so the plot doesn't vanish. + */ +export function useAnalysisRunner(label: string) { + const addNotification = useUIStore((s) => s.addNotification); + const [state, setState] = useState>({ + result: null, + running: false, + error: null, + }); + + const run = useCallback( + async (call: () => Promise>): Promise => { + setState((s) => ({ ...s, running: true, error: null })); + try { + const res = await call(); + if (res.success && res.data !== null) { + setState({ result: res.data, running: false, error: null }); + addNotification({ type: 'success', title: label, message: res.message || 'Done' }); + } else { + const msg = res.error || res.message || 'Operation failed'; + setState((s) => ({ ...s, running: false, error: msg })); + addNotification({ type: 'error', title: label, message: msg }); + } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + setState((s) => ({ ...s, running: false, error: msg })); + addNotification({ type: 'error', title: label, message: msg }); + } + }, + [label, addNotification] + ); + + const reset = useCallback((): void => { + setState({ result: null, running: false, error: null }); + }, []); + + return { ...state, run, reset }; +} +``` + +Run: `npm test -- --run src/hooks/useAnalysisRunner` → PASS. + +- [x] **Step 3: Commit** + +```bash +git add src/hooks/useAnalysisRunner.ts src/hooks/useAnalysisRunner.test.tsx +git commit -m "feat: add useAnalysisRunner request-lifecycle hook" +``` + +--- + +### Task 11: EventList page + +**Files:** +- Modify: `src/pages/QuickLook/EventList/index.tsx` +- Create: `src/pages/QuickLook/EventList/index.test.tsx` + +- [x] **Step 1: Write the failing test** + +`src/pages/QuickLook/EventList/index.test.tsx`: +```tsx +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '@/test/testUtils'; + +const listEventLists = vi.fn(); +const getEventListInfo = vi.fn(); +const getEventListFullPreview = vi.fn(); +const deleteEventList = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { + listEventLists: (...a: unknown[]) => listEventLists(...a), + getEventListInfo: (...a: unknown[]) => getEventListInfo(...a), + getEventListFullPreview: (...a: unknown[]) => getEventListFullPreview(...a), + deleteEventList: (...a: unknown[]) => deleteEventList(...a), + }, +})); +vi.mock('@/components/plots/PlotlyChart', () => ({ + default: () =>
, +})); + +import EventListPage from './index'; + +describe('EventListPage', () => { + beforeEach(() => { + listEventLists.mockReset(); + getEventListInfo.mockReset(); + listEventLists.mockResolvedValue({ + success: true, + data: [{ name: 'obs1', n_events: 5000, time_range: [0, 100] }], + message: '', + error: null, + }); + getEventListInfo.mockResolvedValue({ + success: true, + data: { + name: 'obs1', + n_events: 5000, + time_range: [0, 100], + duration: 100, + mjdref: 56000, + gti_count: 2, + gti_list: [ + [0, 40], + [60, 100], + ], + mean_count_rate: 50, + }, + message: '', + error: null, + }); + }); + + it('lists event lists and shows details when one is selected', async () => { + renderWithProviders(); + await userEvent.click(await screen.findByText(/obs1/)); + expect(await screen.findByText('Duration (s)')).toBeInTheDocument(); + expect(getEventListInfo).toHaveBeenCalledWith('obs1'); + // GTI table rows + expect(await screen.findByText('Good Time Intervals')).toBeInTheDocument(); + }); +}); +``` + +- [x] **Step 2: Run to verify it fails** + +Run: `npm test -- --run src/pages/QuickLook/EventList` +Expected: FAIL — page still renders the coming-soon placeholder, `obs1` never appears. + +- [x] **Step 3: Implement the page** + +Replace `src/pages/QuickLook/EventList/index.tsx` entirely with: +```tsx +import React, { useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + IconButton, + List, + ListItemButton, + ListItemText, + Stack, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tabs, + Tooltip, + Typography, +} from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import DeleteIcon from '@mui/icons-material/Delete'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import { EVENT_LISTS_QUERY_KEY, useEventLists } from '@/hooks/useEventLists'; +import { dataApi, EventListFullPreview, EventListInfo } from '@/api/dataApi'; +import { useUIStore } from '@/store/uiStore'; + +const mono = { fontFamily: '"JetBrains Mono", monospace' }; + +const formatNum = (v: number | null | undefined, digits = 3): string => + v === null || v === undefined + ? '—' + : Number(v).toLocaleString(undefined, { maximumFractionDigits: digits }); + +const InfoRow: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( + + + {label} + + + {value} + + +); + +const EventListPage: React.FC = () => { + const [selected, setSelected] = useState(null); + const [tab, setTab] = useState(0); + const [deleteTarget, setDeleteTarget] = useState(null); + const addNotification = useUIStore((s) => s.addNotification); + const queryClient = useQueryClient(); + const { data: eventLists, isLoading, isError, error, refetch, isFetching } = useEventLists(); + + const infoQuery = useQuery({ + queryKey: ['eventListInfo', selected], + enabled: selected !== null, + queryFn: async (): Promise => { + const res = await dataApi.getEventListInfo(selected as string); + if (!res.success || !res.data) throw new Error(res.error || res.message); + return res.data; + }, + }); + + const previewQuery = useQuery({ + queryKey: ['eventListPreview', selected], + enabled: selected !== null && tab === 1, + queryFn: async (): Promise => { + const res = await dataApi.getEventListFullPreview(selected as string); + if (!res.success || !res.data) throw new Error(res.error || res.message); + return res.data; + }, + }); + + const handleDelete = async (): Promise => { + if (!deleteTarget) return; + const res = await dataApi.deleteEventList(deleteTarget); + if (res.success) { + addNotification({ type: 'success', title: 'Event List', message: `Deleted '${deleteTarget}'` }); + if (selected === deleteTarget) setSelected(null); + await queryClient.invalidateQueries({ queryKey: EVENT_LISTS_QUERY_KEY }); + } else { + addNotification({ + type: 'error', + title: 'Event List', + message: res.error || res.message || 'Delete failed', + }); + } + setDeleteTarget(null); + }; + + const info = infoQuery.data; + const preview = previewQuery.data; + + return ( + + + + + + + Loaded event lists + + + refetch()} disabled={isFetching}> + {isFetching ? : } + + + + + {isError && ( + + {error instanceof Error ? error.message : 'Failed to load'} + + )} + {isLoading && } + {!isLoading && (eventLists?.length ?? 0) === 0 && ( + + Nothing loaded yet — use Data Ingestion first. + + )} + + {(eventLists ?? []).map((ev) => ( + setSelected(ev.name)} + > + + { + e.stopPropagation(); + setDeleteTarget(ev.name); + }} + > + + + + ))} + + + + + + + + + {!selected && ( + + + Select an event list to inspect it. + + + )} + {selected && ( + <> + + + {selected} + + {info?.mission && } + {info?.instrument && } + + setTab(v)} sx={{ mb: 2 }}> + + + + + {infoQuery.isError && ( + + {infoQuery.error instanceof Error ? infoQuery.error.message : 'Failed to load info'} + + )} + {tab === 0 && infoQuery.isLoading && } + + {tab === 0 && info && ( + + + + + + + + + + + + + {(info.validation_issues ?? []) + .filter((v) => v.severity === 'error' || v.severity === 'warning') + .map((v, i) => ( + + {v.message} + + ))} + {info.notes && ( + + {info.notes} + + )} + + {(info.gti_list?.length ?? 0) > 0 && ( + + + + Good Time Intervals + + + + + + # + Start + Stop + Duration (s) + Rate (cts/s) + + + + {(info.gti_list ?? []).map((g, i) => { + const rate = info.per_gti_rates?.[i]; + return ( + + {i + 1} + {formatNum(g[0])} + {formatNum(g[1])} + {formatNum(g[1] - g[0])} + {formatNum(rate?.rate)} + + ); + })} + +
+
+
+ )} +
+ )} + + {tab === 1 && previewQuery.isLoading && } + {tab === 1 && previewQuery.isError && ( + + {previewQuery.error instanceof Error + ? previewQuery.error.message + : 'Failed to load preview'} + + )} + {tab === 1 && preview && ( + + + + Arrival time distribution (preview sample) + + + + {preview.has_energy && preview.energy_preview && ( + + + Energy distribution (preview sample) + + + + )} + + )} + + )} +
+
+
+
+ + setDeleteTarget(null)}> + Delete event list? + + + Remove '{deleteTarget}' from backend memory? This cannot be undone. + + + + + + + +
+ ); +}; + +export default EventListPage; +``` + +- [x] **Step 4: Run to verify it passes** + +Run: `npm test -- --run src/pages/QuickLook/EventList && npm run typecheck && npm run lint` +Expected: PASS, no type or lint errors. + +- [x] **Step 5: Manual verification** + +Run `npm run dev`, load a sample file from `files/data/` via Data Ingestion, open QuickLook → Event List. Confirm: list shows the file, Overview shows real numbers and GTI table, Distributions tab renders two histograms, Delete removes it. + +- [x] **Step 6: Commit** + +```bash +git add src/pages/QuickLook/EventList/ +git commit -m "feat: implement EventList QuickLook page" +``` + +--- + +### Task 12: Light Curve page + +**Files:** +- Modify: `src/pages/QuickLook/LightCurve/index.tsx` +- Create: `src/pages/QuickLook/LightCurve/index.test.tsx` + +- [x] **Step 1: Write the failing test** + +`src/pages/QuickLook/LightCurve/index.test.tsx`: +```tsx +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '@/test/testUtils'; +import { useUIStore } from '@/store/uiStore'; + +const listEventLists = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { listEventLists: (...a: unknown[]) => listEventLists(...a) }, +})); + +const createFromEventList = vi.fn(); +const listLightcurves = vi.fn(); +const getLightcurveData = vi.fn(); +const rebin = vi.fn(); +const deleteLightcurve = vi.fn(); +vi.mock('@/api/lightcurveApi', () => ({ + lightcurveApi: { + createFromEventList: (...a: unknown[]) => createFromEventList(...a), + listLightcurves: (...a: unknown[]) => listLightcurves(...a), + getLightcurveData: (...a: unknown[]) => getLightcurveData(...a), + rebin: (...a: unknown[]) => rebin(...a), + deleteLightcurve: (...a: unknown[]) => deleteLightcurve(...a), + }, +})); +vi.mock('@/components/plots/PlotlyChart', () => ({ + default: () =>
, +})); + +import LightCurvePage from './index'; + +describe('LightCurvePage', () => { + beforeEach(() => { + useUIStore.setState({ notifications: [], unreadNotificationCount: 0 }); + listEventLists.mockResolvedValue({ + success: true, + data: [{ name: 'obs1', n_events: 5000, time_range: [0, 100] }], + message: '', + error: null, + }); + listLightcurves.mockResolvedValue({ success: true, data: [], message: '', error: null }); + createFromEventList.mockResolvedValue({ + success: true, + data: { + name: 'obs1_lc', + time: [0.5, 1.5, 2.5], + counts: [10, 12, 9], + dt: 1, + n_bins: 3, + plot_stride: 1, + count_rate_mean: 10.3, + }, + message: 'created', + error: null, + }); + }); + + it('creates a light curve with parsed parameters and plots it', async () => { + renderWithProviders(); + await userEvent.click(await screen.findByLabelText('Event list')); + await userEvent.click(await screen.findByText(/obs1/)); + const dtField = screen.getByLabelText(/Time bin/); + await userEvent.clear(dtField); + await userEvent.type(dtField, '1.0'); + await userEvent.click(screen.getByRole('button', { name: /Generate/ })); + await waitFor(() => + expect(createFromEventList).toHaveBeenCalledWith( + expect.objectContaining({ event_list_name: 'obs1', dt: 1, output_name: 'obs1_lc' }) + ) + ); + expect(await screen.findByTestId('chart')).toBeInTheDocument(); + }); + + it('disables Generate until inputs are valid', async () => { + renderWithProviders(); + const button = await screen.findByRole('button', { name: /Generate/ }); + expect(button).toBeDisabled(); + }); +}); +``` + +- [x] **Step 2: Run to verify it fails** + +Run: `npm test -- --run src/pages/QuickLook/LightCurve` +Expected: FAIL (placeholder page). + +- [x] **Step 3: Implement the page** + +Replace `src/pages/QuickLook/LightCurve/index.tsx` entirely with: +```tsx +import React, { useEffect, useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + Stack, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import DeleteIcon from '@mui/icons-material/Delete'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { lightcurveApi, LightcurveData, LightcurveSummary } from '@/api/lightcurveApi'; +import { parsePositiveNumber } from '@/utils/numbers'; +import { useUIStore } from '@/store/uiStore'; + +const LIGHTCURVES_QUERY_KEY = ['lightcurves'] as const; + +const LightCurvePage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('1.0'); + const [outputName, setOutputName] = useState(''); + const [existingSelection, setExistingSelection] = useState(''); + const [rebinFactor, setRebinFactor] = useState('2'); + const addNotification = useUIStore((s) => s.addNotification); + const queryClient = useQueryClient(); + const { result, running, error, run } = useAnalysisRunner('Light Curve'); + + const existingQuery = useQuery({ + queryKey: LIGHTCURVES_QUERY_KEY, + queryFn: async (): Promise => { + const res = await lightcurveApi.listLightcurves(); + if (!res.success) throw new Error(res.error || res.message); + return res.data ?? []; + }, + }); + + // Any successful create/rebin changes the stored set — refresh the list. + useEffect(() => { + if (result) void queryClient.invalidateQueries({ queryKey: LIGHTCURVES_QUERY_KEY }); + }, [result, queryClient]); + + const dtNum = parsePositiveNumber(dt); + const canRun = eventList !== '' && dtNum !== null && !running; + const rebinNum = parsePositiveNumber(rebinFactor); + + const handleGenerate = (): void => { + if (!dtNum || !eventList) return; + const name = outputName.trim() || `${eventList}_lc`; + void run(() => + lightcurveApi.createFromEventList({ event_list_name: eventList, dt: dtNum, output_name: name }) + ); + }; + + const handleView = (): void => { + if (!existingSelection) return; + void run(() => lightcurveApi.getLightcurveData(existingSelection)); + }; + + const handleRebin = (): void => { + if (!result?.name || !rebinNum) return; + void run(() => + lightcurveApi.rebin({ + name: result.name, + rebin_factor: rebinNum, + output_name: `${result.name}_r${rebinNum}`, + }) + ); + }; + + const handleDelete = async (name: string): Promise => { + const res = await lightcurveApi.deleteLightcurve(name); + if (res.success) { + addNotification({ type: 'success', title: 'Light Curve', message: `Deleted '${name}'` }); + await queryClient.invalidateQueries({ queryKey: LIGHTCURVES_QUERY_KEY }); + } else { + addNotification({ + type: 'error', + title: 'Light Curve', + message: res.error || res.message || 'Delete failed', + }); + } + }; + + const plotData: Data[] = result + ? [ + { + x: result.time, + y: result.counts, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + return ( + + + + + + + Generate from event list + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setOutputName(e.target.value)} + placeholder={eventList ? `${eventList}_lc` : 'name'} + /> + + + + {result?.name && ( + <> + + + Rebin '{result.name}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && rebinNum === null} + helperText={ + rebinFactor !== '' && rebinNum === null ? 'Must be a positive number' : ' ' + } + /> + + + + )} + + + + Stored light curves + + Light curve + + + + + + + { + void handleDelete(existingSelection); + setExistingSelection(''); + }} + > + + + + + + + + + + + + + + + + {result?.name ? `Light curve: ${result.name}` : 'Result'} + + {result && } + {result && } + {result?.count_rate_mean !== undefined && ( + + )} + + {result?.plot_stride !== undefined && result.plot_stride > 1 && ( + + Showing every {result.plot_stride}th bin for display performance (full resolution + is stored in the backend). + + )} + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Generate a light curve or view a stored one. + + + )} + + + + + + ); +}; + +export default LightCurvePage; +``` + +- [x] **Step 4: Run to verify it passes** + +Run: `npm test -- --run src/pages/QuickLook/LightCurve && npm run typecheck && npm run lint` +Expected: PASS. + +- [x] **Step 5: Manual verification** + +In the running app: generate a light curve from a loaded event list (dt=1), confirm plot + chips; rebin ×2, confirm new plot and that the stored list now contains both; view and delete stored curves. + +- [x] **Step 6: Commit** + +```bash +git add src/pages/QuickLook/LightCurve/ +git commit -m "feat: implement Light Curve page with rebin and stored-curve viewer" +``` + +--- + +### Task 13: Power Spectrum page + +**Files:** +- Modify: `src/pages/QuickLook/PowerSpectrum/index.tsx` + +The pattern for all single-input spectrum pages. `lastStoredName` tracks the most recent result that was stored under a name, so Rebin always re-derives from the stored original (rebin responses themselves have `name: null` and are display-only). + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/PowerSpectrum/index.tsx` entirely with: +```tsx +import React, { useEffect, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, PowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const PowerSpectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logX, setLogX] = useState(true); + const [logY, setLogY] = useState(true); + const [rebinFactor, setRebinFactor] = useState('0.02'); + const [logRebin, setLogRebin] = useState(true); + const [lastStoredName, setLastStoredName] = useState(null); + const { result, running, error, run } = useAnalysisRunner('Power Spectrum'); + + useEffect(() => { + if (result?.name) setLastStoredName(result.name); + }, [result]); + + const dtNum = parsePositiveNumber(dt); + const rebinNum = parsePositiveNumber(rebinFactor); + const canRun = eventList !== '' && dtNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum) return; + void run(() => + spectrumApi.createPowerSpectrum({ + event_list_name: eventList, + dt: dtNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const handleRebin = (): void => { + if (!lastStoredName || !rebinNum) return; + void run(() => + spectrumApi.rebinSpectrum({ name: lastStoredName, rebin_factor: rebinNum, log: logRebin }) + ); + }; + + const plotData: Data[] = result + ? [ + { + x: result.freq, + y: result.power, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + placeholder={eventList ? `${eventList}_ps` : ''} + helperText="Required to enable rebinning" + /> + + + + {lastStoredName && ( + <> + + + Rebin '{lastStoredName}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && rebinNum === null} + helperText={logRebin ? 'Each bin grows by (1 + f)' : ' '} + /> + setLogRebin(e.target.checked)} />} + label="Logarithmic" + /> + + + + )} + + + + + + + + + + Result + + {result?.norm && } + {result && } + {result?.df !== undefined && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + setLogY(e.target.checked)} />} + label="log P" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Choose an event list and compute a power spectrum. + + + )} + + + + + + ); +}; + +export default PowerSpectrumPage; +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint && npm test -- --run` +Expected: all pass (existing tests unaffected). + +- [x] **Step 3: Manual verification** + +In the app: compute leahy PS at dt=0.0625 on a loaded event list — expect mean power ≈ 2 at high frequency for Poisson data. Store + rebin (log, f=0.02) and confirm the curve smooths. + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/PowerSpectrum/ +git commit -m "feat: implement Power Spectrum page" +``` + +--- + +### Task 14: Averaged Power Spectrum page + +**Files:** +- Modify: `src/pages/QuickLook/AvgPowerSpectrum/index.tsx` + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/AvgPowerSpectrum/index.tsx` entirely. It is the PowerSpectrum page (Task 13) with five deltas — apply them to a fresh copy of that component source: + +1. Component name and export: `AvgPowerSpectrumPage`. +2. Add state below `dt`: `const [segmentSize, setSegmentSize] = useState('16');` and parse it: `const segNum = parsePositiveNumber(segmentSize);` and extend `canRun`: `const canRun = eventList !== '' && dtNum !== null && segNum !== null && !running;` +3. `handleRun` calls the averaged endpoint: +```tsx + const handleRun = (): void => { + if (!dtNum || !segNum) return; + void run(() => + spectrumApi.createAveragedPowerSpectrum({ + event_list_name: eventList, + dt: dtNum, + segment_size: segNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; +``` +4. Add a segment-size field after the dt TextField: +```tsx + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> +``` +5. Add a segments chip in the result header (after the `n_freq` chip): +```tsx + {result?.n_segments != null && ( + + )} +``` +Also update `PageTemplate` props: `title="Averaged Power Spectrum"`, `description="Welch-style averaged power spectrum over fixed-length segments"`, label `'Averaged Power Spectrum'` in `useAnalysisRunner`, and placeholder `_aps` instead of `_ps`. + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: clean. + +- [x] **Step 3: Manual verification** + +Compute with dt=0.0625, segment=16 s: scatter should be visibly smaller than the single PS; segments chip shows a sensible count (~duration/16). + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/AvgPowerSpectrum/ +git commit -m "feat: implement Averaged Power Spectrum page" +``` + +--- + +### Task 15: Cross Spectrum page + +**Files:** +- Modify: `src/pages/QuickLook/CrossSpectrum/index.tsx` + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/CrossSpectrum/index.tsx` entirely with: +```tsx +import React, { useEffect, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, PowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const CrossSpectrumPage: React.FC = () => { + const [eventList1, setEventList1] = useState(''); + const [eventList2, setEventList2] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logX, setLogX] = useState(true); + const [logY, setLogY] = useState(true); + const [rebinFactor, setRebinFactor] = useState('0.02'); + const [logRebin, setLogRebin] = useState(true); + const [lastStoredName, setLastStoredName] = useState(null); + const { result, running, error, run } = useAnalysisRunner('Cross Spectrum'); + + useEffect(() => { + if (result?.name) setLastStoredName(result.name); + }, [result]); + + const dtNum = parsePositiveNumber(dt); + const rebinNum = parsePositiveNumber(rebinFactor); + const canRun = eventList1 !== '' && eventList2 !== '' && dtNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum) return; + void run(() => + spectrumApi.createCrossSpectrum({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const handleRebin = (): void => { + if (!lastStoredName || !rebinNum) return; + void run(() => + spectrumApi.rebinSpectrum({ name: lastStoredName, rebin_factor: rebinNum, log: logRebin }) + ); + }; + + const magnitudeTrace: Data[] = result + ? [ + { + x: result.freq, + y: result.power, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + const phaseTrace: Data[] = + result && result.power_phase + ? [ + { + x: result.freq, + y: result.power_phase, + type: 'scattergl', + mode: 'markers', + marker: { color: '#3b82f6', size: 3 }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + helperText="Required to enable rebinning" + /> + + + + {lastStoredName && ( + <> + + + Rebin '{lastStoredName}' + setRebinFactor(e.target.value)} + /> + setLogRebin(e.target.checked)} />} + label="Logarithmic" + /> + + + + )} + + + + + + + + + + Result + + {result?.norm && } + {result && } + setLogX(e.target.checked)} />} + label="log f" + /> + setLogY(e.target.checked)} />} + label="log |C|" + /> + + {error && ( + + {error} + + )} + {result ? ( + + + + Cross-power magnitude + + + + {phaseTrace.length > 0 && ( + + + Cross-spectrum phase + + + + )} + + ) : ( + + + Choose two event lists and compute their cross spectrum. + + + )} + + + + + + ); +}; + +export default CrossSpectrumPage; +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: clean. + +- [x] **Step 3: Manual verification** + +Load the same file twice under two names (or two different files), compute: magnitude plot renders, phase panel renders with values in [-π, π]. This exercises the Task 3 backend fix end-to-end. + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/CrossSpectrum/ +git commit -m "feat: implement Cross Spectrum page with magnitude and phase" +``` + +--- + +### Task 16: Averaged Cross Spectrum page + +**Files:** +- Modify: `src/pages/QuickLook/AvgCrossSpectrum/index.tsx` + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/AvgCrossSpectrum/index.tsx` entirely. It is the CrossSpectrum page (Task 15) with five deltas — apply them to a fresh copy of that component source: + +1. Component name and export: `AvgCrossSpectrumPage`; runner label `'Averaged Cross Spectrum'`. +2. Add state below `dt`: `const [segmentSize, setSegmentSize] = useState('16');` plus `const segNum = parsePositiveNumber(segmentSize);` and extend `canRun` with `&& segNum !== null`. +3. `handleRun` calls: +```tsx + spectrumApi.createAveragedCrossSpectrum({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + segment_size: segNum, + norm, + output_name: outputName.trim() || undefined, + }) +``` +(guard becomes `if (!dtNum || !segNum) return;`) +4. Add the same segment-size TextField as Task 14 delta 4, after the dt field. +5. `PageTemplate` props: `title="Averaged Cross Spectrum"`, `description="Segment-averaged cross spectrum between two event lists"`; add a `segment_size` chip in the result header: +```tsx + {result?.segment_size !== undefined && ( + + )} +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: clean. + +- [x] **Step 3: Manual verification** + +Compute with segment=16 s on two loaded lists; phase scatter should be visibly less noisy than the single cross spectrum. + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/AvgCrossSpectrum/ +git commit -m "feat: implement Averaged Cross Spectrum page" +``` + +--- + +### Task 17: Dynamical Power Spectrum page + +**Files:** +- Modify: `src/pages/QuickLook/DynamicalPowerSpectrum/index.tsx` + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/DynamicalPowerSpectrum/index.tsx` entirely with: +```tsx +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, DynamicalPowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const DynamicalPowerSpectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('8'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logZ, setLogZ] = useState(true); + const { result, running, error, run } = useAnalysisRunner( + 'Dynamical Power Spectrum' + ); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const canRun = eventList !== '' && dtNum !== null && segNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum || !segNum) return; + void run(() => + spectrumApi.createDynamicalPowerSpectrum({ + event_list_name: eventList, + dt: dtNum, + segment_size: segNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + // dyn_ps rows correspond to frequencies (n_freq x n_times) — matches + // Plotly's convention that z[i] pairs with y[i]. + const zValues: Array> | undefined = result + ? logZ + ? result.dyn_ps.map((row) => row.map((v) => (v > 0 ? Math.log10(v) : null))) + : result.dyn_ps + : undefined; + + const heatmap: Data[] = + result && zValues + ? [ + { + z: zValues, + x: result.time, + y: result.freq, + type: 'heatmap', + colorscale: 'Viridis', + colorbar: { title: { text: logZ ? 'log10 P' : 'Power' } }, + } as Data, + ] + : []; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + /> + + + + + + + + + + + + Result + + {result && ( + + )} + setLogZ(e.target.checked)} />} + label="log color" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Compute a dynamical power spectrum to see the time-frequency map. + + + )} + + + + + + ); +}; + +export default DynamicalPowerSpectrumPage; +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: clean. + +- [x] **Step 3: Manual verification** + +Compute with dt=0.0625, segment=8 s. Heatmap renders with time on x, frequency on y; toggling "log color" rescales. + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/DynamicalPowerSpectrum/ +git commit -m "feat: implement Dynamical Power Spectrum page" +``` + +--- + +### Task 18: Bispectrum page + +**Files:** +- Modify: `src/pages/QuickLook/Bispectrum/index.tsx` + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/Bispectrum/index.tsx` entirely with: +```tsx +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + Tab, + Tabs, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, BispectrumData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const SCALE_OPTIONS = ['biased', 'unbiased']; +const WINDOW_OPTIONS = ['uniform', 'parzen', 'hamming', 'hanning', 'triangular', 'welch', 'blackmann', 'flat-top']; + +const BispectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.1'); + const [maxlag, setMaxlag] = useState('25'); + const [scale, setScale] = useState('unbiased'); + const [windowFn, setWindowFn] = useState('uniform'); + const [outputName, setOutputName] = useState(''); + const [tab, setTab] = useState(0); + const [logZ, setLogZ] = useState(true); + const { result, running, error, run } = useAnalysisRunner('Bispectrum'); + + const dtNum = parsePositiveNumber(dt); + const maxlagNum = parsePositiveNumber(maxlag); + const canRun = eventList !== '' && dtNum !== null && maxlagNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum || !maxlagNum) return; + void run(() => + timingApi.createBispectrum({ + event_list_name: eventList, + dt: dtNum, + maxlag: Math.round(maxlagNum), + scale, + window: windowFn, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const buildHeatmap = (): Data[] => { + if (!result) return []; + if (tab === 0) { + const z = logZ + ? result.bispec_mag.map((row) => row.map((v) => (v > 0 ? Math.log10(v) : null))) + : result.bispec_mag; + return [ + { + z, + x: result.freq, + y: result.freq, + type: 'heatmap', + colorscale: 'Viridis', + colorbar: { title: { text: logZ ? 'log10 |B|' : '|B|' } }, + } as Data, + ]; + } + return [ + { + z: result.bispec_phase, + x: result.freq, + y: result.freq, + type: 'heatmap', + colorscale: 'RdBu', + zmid: 0, + colorbar: { title: { text: 'Phase (rad)' } }, + } as Data, + ]; + }; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setMaxlag(e.target.value)} + error={maxlag !== '' && maxlagNum === null} + helperText="Bispectrum size is (2·maxlag+1)²; keep ≤ 100" + /> + + Scale + + + + Window + + + setOutputName(e.target.value)} + /> + + + + + + + + + + + setTab(v)} sx={{ flexGrow: 1 }}> + + + + {result && } + {result && } + {tab === 0 && ( + setLogZ(e.target.checked)} />} + label="log color" + /> + )} + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Compute a bispectrum to see magnitude and phase maps. + + + )} + + + + + + ); +}; + +export default BispectrumPage; +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: clean. + +- [x] **Step 3: Manual verification** + +Compute with dt=0.1, maxlag=25: Magnitude tab shows a (51×51) heatmap, Phase tab a diverging map. Computation may take a while — confirm the log panel stays live (Task 6 fix). + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/Bispectrum/ +git commit -m "feat: implement Bispectrum page with magnitude/phase heatmaps" +``` + +--- + +### Task 19: Coherence page + +**Files:** +- Modify: `src/pages/QuickLook/Coherence/index.tsx` + +- [x] **Step 1: Implement the page** + +Replace `src/pages/QuickLook/Coherence/index.tsx` entirely with: +```tsx +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControlLabel, + Grid, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, CoherenceData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const CoherencePage: React.FC = () => { + const [eventList1, setEventList1] = useState(''); + const [eventList2, setEventList2] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('16'); + const [logX, setLogX] = useState(true); + const { result, running, error, run } = useAnalysisRunner('Coherence'); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const canRun = eventList1 !== '' && eventList2 !== '' && dtNum !== null && segNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum || !segNum) return; + void run(() => + timingApi.calculateCoherence({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + segment_size: segNum, + }) + ); + }; + + const traces: Data[] = result + ? [ + { + x: result.freq, + y: result.coherence, + type: 'scattergl', + mode: 'lines+markers', + marker: { size: 4, color: '#00d4aa' }, + line: { color: '#00d4aa', width: 1 }, + error_y: result.coherence_err + ? { type: 'data', array: result.coherence_err, visible: true, color: 'rgba(0, 212, 170, 0.35)' } + : undefined, + } as Data, + ] + : []; + + return ( + + + + + + + Parameters + + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + + + + + + + + + + + Result + + {result?.n_segments != null && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Choose two event lists and compute their coherence. + + + )} + + + + + + ); +}; + +export default CoherencePage; +``` + +- [x] **Step 2: Verify** + +Run: `npm run typecheck && npm run lint` +Expected: clean. + +- [x] **Step 3: Manual verification** + +Compute coherence of an event list with itself (load the same file under two names): values cluster at 1.0 (this directly validates the Task 4 fix). With two unrelated lists, values are low. + +- [x] **Step 4: Commit** + +```bash +git add src/pages/QuickLook/Coherence/ +git commit -m "feat: implement Coherence page with uncertainties" +``` + +--- + +### Task 20: Time Lags page (new route + nav) + +**Files:** +- Create: `src/pages/QuickLook/TimeLags/index.tsx` +- Modify: `src/App.tsx`, `src/components/layout/Sidebar.tsx` + +- [x] **Step 1: Implement the page** + +Create `src/pages/QuickLook/TimeLags/index.tsx`. It is the Coherence page (Task 19) with these deltas applied to a fresh copy of that component source: + +1. Component name/export `TimeLagsPage`; runner type `TimeLagsData`; label `'Time Lags'`; import `{ timingApi, TimeLagsData }` and additionally `{ parseNumber }` from `@/utils/numbers`. +2. Add optional frequency-range state after `segmentSize`: +```tsx + const [freqMin, setFreqMin] = useState(''); + const [freqMax, setFreqMax] = useState(''); +``` +and parse: `const fMin = parseNumber(freqMin); const fMax = parseNumber(freqMax);` +3. `handleRun` becomes: +```tsx + const handleRun = (): void => { + if (!dtNum || !segNum) return; + const freq_range: [number, number] | undefined = + fMin !== null && fMax !== null && fMax > fMin ? [fMin, fMax] : undefined; + void run(() => + timingApi.calculateTimeLags({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + segment_size: segNum, + freq_range, + }) + ); + }; +``` +4. Add two small fields after the segment-size TextField: +```tsx + + setFreqMin(e.target.value)} + /> + setFreqMax(e.target.value)} + /> + +``` +5. Replace the `traces` constant with: +```tsx + const traces: Data[] = result + ? [ + { + x: result.freq, + y: result.time_lags, + type: 'scattergl', + mode: 'lines+markers', + marker: { size: 4, color: '#00d4aa' }, + line: { color: '#00d4aa', width: 1 }, + error_y: result.time_lags_err + ? { type: 'data', array: result.time_lags_err, visible: true, color: 'rgba(0, 212, 170, 0.35)' } + : undefined, + } as Data, + ] + : []; +``` +and in the chart layout use `yaxis: { title: { text: 'Time lag (s)' } }` (no fixed range), with the dashed reference shape at `y0: 0, y1: 0` (zero lag) instead of 1. +6. `PageTemplate` props: `title="Time Lags"`, `description="Frequency-dependent time lags between two energy bands (positive = band 1 lags band 2)"`, `category="Frequency Domain"`. +7. Result header chip: `TimeLagsData` has no `n_segments` — replace that chip with one showing `result.freq_range` when set: +```tsx + {result?.freq_range && ( + + )} +``` + +- [x] **Step 2: Register route and navigation** + +`src/App.tsx`: +- After the `CoherencePage` import (line 33) add: +```tsx +import TimeLagsPage from '@/pages/QuickLook/TimeLags'; +``` +- After the coherence route (`{ path: 'quicklook/coherence', element: },`) add: +```tsx + { path: 'quicklook/time-lags', element: }, +``` + +`src/components/layout/Sidebar.tsx`: +- In `submenuItems` after the Coherence entry (line 87) add: +```tsx + { text: 'Time Lags', path: '/quicklook/time-lags' }, +``` +- In `quicklookCategories`, append `'Time Lags'` to the `'Frequency Domain'` array (after `'Coherence'`). + +- [x] **Step 3: Verify** + +Run: `npm run typecheck && npm run lint && npm test -- --run` +Expected: clean. + +- [x] **Step 4: Manual verification** + +Sidebar → QuickLook → Frequency Domain shows "Time Lags"; page computes and plots lags with error bars and a zero reference line; the f-range fields restrict the plotted band. + +- [x] **Step 5: Commit** + +```bash +git add src/pages/QuickLook/TimeLags/ src/App.tsx src/components/layout/Sidebar.tsx +git commit -m "feat: add Time Lags page with route and navigation" +``` + +--- + +### Task 21: Power Colors page (new route + nav) + +**Files:** +- Create: `src/utils/powerColors.ts`, `src/utils/powerColors.test.ts`, `src/pages/QuickLook/PowerColors/index.tsx` +- Modify: `src/App.tsx`, `src/components/layout/Sidebar.tsx` + +- [x] **Step 1: Write the failing helper test** + +`src/utils/powerColors.test.ts`: +```ts +import { describe, expect, it } from 'vitest'; +import { computePowerColorRatios } from './powerColors'; + +describe('computePowerColorRatios', () => { + const powerColors = { + A: [1, 2], + B: [10, 20], + C: [100, 200], + D: [5, 10], + }; + + it('computes PC1 = C/A and PC2 = B/D per segment', () => { + const result = computePowerColorRatios(powerColors, ['A', 'B', 'C', 'D']); + expect(result).not.toBeNull(); + expect(result?.pc1).toEqual([100, 100]); + expect(result?.pc2).toEqual([2, 2]); + }); + + it('returns null when a band is missing or count is not 4', () => { + expect(computePowerColorRatios(powerColors, ['A', 'B', 'C'])).toBeNull(); + expect(computePowerColorRatios({ A: [1] }, ['A', 'B', 'C', 'D'])).toBeNull(); + }); + + it('skips segments with non-positive denominators', () => { + const result = computePowerColorRatios( + { A: [0, 1], B: [1, 1], C: [1, 1], D: [1, 1] }, + ['A', 'B', 'C', 'D'] + ); + expect(result?.pc1).toEqual([1]); + expect(result?.pc2).toEqual([1]); + }); +}); +``` + +- [x] **Step 2: Run to verify it fails, then implement the helper** + +Run: `npm test -- --run src/utils/powerColors` → FAIL. + +`src/utils/powerColors.ts`: +```ts +/** + * Power-color ratios following the Heil et al. (2015) convention: with four + * frequency bands A < B < C < D (ascending f_min), + * PC1 = P(C) / P(A) and PC2 = P(B) / P(D) + * computed per dynamical-spectrum segment. + */ +export interface PowerColorRatios { + pc1: number[]; + pc2: number[]; +} + +export function computePowerColorRatios( + powerColors: Record, + bandOrder: string[] +): PowerColorRatios | null { + if (bandOrder.length !== 4) return null; + const [a, b, c, d] = bandOrder.map((key) => powerColors[key]); + if (!a || !b || !c || !d) return null; + + const n = Math.min(a.length, b.length, c.length, d.length); + const pc1: number[] = []; + const pc2: number[] = []; + for (let i = 0; i < n; i++) { + if (a[i] > 0 && d[i] > 0 && c[i] > 0 && b[i] > 0) { + pc1.push(c[i] / a[i]); + pc2.push(b[i] / d[i]); + } + } + return { pc1, pc2 }; +} +``` + +Run: `npm test -- --run src/utils/powerColors` → PASS. + +- [x] **Step 3: Implement the page** + +Create `src/pages/QuickLook/PowerColors/index.tsx`: +```tsx +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + CircularProgress, + Grid, + Stack, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, PowerColorsData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; +import { computePowerColorRatios } from '@/utils/powerColors'; + +interface BandInput { + label: string; + fmin: string; + fmax: string; +} + +// Heil et al. (2015) bands; require dt <= 1/(2*16) s for the top band. +const DEFAULT_BANDS: BandInput[] = [ + { label: 'A', fmin: '0.0039', fmax: '0.031' }, + { label: 'B', fmin: '0.031', fmax: '0.25' }, + { label: 'C', fmin: '0.25', fmax: '2.0' }, + { label: 'D', fmin: '2.0', fmax: '16.0' }, +]; + +const BAND_COLORS = ['#00d4aa', '#3b82f6', '#f59e0b', '#ef4444']; + +const PowerColorsPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.03125'); + const [segmentSize, setSegmentSize] = useState('64'); + const [bands, setBands] = useState(DEFAULT_BANDS); + const { result, running, error, run } = useAnalysisRunner('Power Colors'); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + + const parsedBands = bands.map((b) => ({ + label: b.label, + fmin: parsePositiveNumber(b.fmin), + fmax: parsePositiveNumber(b.fmax), + })); + const bandsValid = parsedBands.every( + (b) => b.fmin !== null && b.fmax !== null && b.fmax > b.fmin + ); + const canRun = eventList !== '' && dtNum !== null && segNum !== null && bandsValid && !running; + + const updateBand = (index: number, field: 'fmin' | 'fmax', value: string): void => { + setBands((prev) => prev.map((b, i) => (i === index ? { ...b, [field]: value } : b))); + }; + + const handleRun = (): void => { + if (!dtNum || !segNum || !bandsValid) return; + const freq_ranges: Record = {}; + for (const b of parsedBands) { + freq_ranges[b.label] = [b.fmin as number, b.fmax as number]; + } + void run(() => + timingApi.calculatePowerColors({ + event_list_name: eventList, + dt: dtNum, + segment_size: segNum, + freq_ranges, + }) + ); + }; + + const bandTraces: Data[] = result + ? Object.entries(result.power_colors).map(([label, values], i) => ({ + x: result.time, + y: values, + type: 'scattergl', + mode: 'lines+markers', + marker: { size: 4 }, + line: { width: 1, color: BAND_COLORS[i % BAND_COLORS.length] }, + name: label, + })) + : []; + + const ratios = result + ? computePowerColorRatios(result.power_colors, bands.map((b) => b.label)) + : null; + + const scatterTrace: Data[] = ratios + ? [ + { + x: ratios.pc1, + y: ratios.pc2, + type: 'scattergl', + mode: 'markers', + marker: { size: 6, color: '#00d4aa' }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText="Nyquist must cover the highest band" + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText="Must exceed 1/f_min of the lowest band" + /> + Frequency bands (Hz) + {bands.map((b, i) => ( + + + {b.label} + + updateBand(i, 'fmin', e.target.value)} + /> + updateBand(i, 'fmax', e.target.value)} + /> + + ))} + {!bandsValid && ( + Each band needs 0 < f min < f max. + )} + + + + + + + + + + {error && ( + + {error} + + )} + {result ? ( + + + + Band power vs time + + + + {ratios && ratios.pc1.length > 0 && ( + + + Power-color diagram (PC1 = C/A, PC2 = B/D) + + + + )} + + ) : ( + + + Compute band powers to populate the power-color diagram. + + + )} + + + + + + ); +}; + +export default PowerColorsPage; +``` + +- [x] **Step 4: Register route and navigation** + +`src/App.tsx`: +- After the `TimeLagsPage` import add: +```tsx +import PowerColorsPage from '@/pages/QuickLook/PowerColors'; +``` +- After the bispectrum route (`{ path: 'quicklook/bispectrum', element: },`) add: +```tsx + { path: 'quicklook/power-colors', element: }, +``` + +`src/components/layout/Sidebar.tsx`: +- In `submenuItems` after the Bispectrum entry add: +```tsx + { text: 'Power Colors', path: '/quicklook/power-colors' }, +``` +- In `quicklookCategories`, append `'Power Colors'` to the `'Advanced Analysis'` array (after `'Bispectrum'`). + +- [x] **Step 5: Verify** + +Run: `npm test -- --run && npm run typecheck && npm run lint` +Expected: all pass. + +- [x] **Step 6: Manual verification** + +Compute with the defaults on a loaded list (needs ≥ 64 s of data and dt=0.03125): four band traces render; PC diagram shows one point per segment. + +- [x] **Step 7: Commit** + +```bash +git add src/utils/powerColors.ts src/utils/powerColors.test.ts src/pages/QuickLook/PowerColors/ src/App.tsx src/components/layout/Sidebar.tsx +git commit -m "feat: add Power Colors page with band powers and PC diagram" +``` + +--- + +### Task 22: Full-suite verification and end-to-end pass + +- [x] **Step 1: Run every automated gate** + +```bash +npm test -- --run +npm run typecheck +npm run lint +pixi run -e dev pytest python-backend/tests -v +``` +Expected: all green. Fix anything that isn't before proceeding. + +- [ ] **Step 2: End-to-end manual checklist** + +Start `npm run dev`. Load one sample event file from `files/data/` twice under two names (`evA`, `evB`). Then walk every page: + +| Page | Action | Expect | +|---|---|---| +| Event List | select evA, both tabs | info + GTI table + 2 histograms | +| Light Curve | generate dt=1, rebin ×2 | plot, chips, stored list grows | +| Power Spectrum | dt=0.0625, leahy, store, rebin log | Poisson level ≈ 2 at high f | +| Avg Power Spectrum | segment=16 | smoother spectrum, segments chip | +| Cross Spectrum | evA × evB | magnitude + phase panels | +| Avg Cross Spectrum | segment=16 | less scatter | +| Dynamical PS | dt=0.0625, segment=8 | heatmap, log-color toggle | +| Bispectrum | dt=0.1, maxlag=25 | magnitude + phase tabs; UI stays responsive | +| Coherence | evA × evA | γ² ≈ 1 flat line at the reference | +| Time Lags | evA × evB, then f-range 0.5–2 | error bars, zero line, filtered range | +| Power Colors | defaults | 4 band traces + PC scatter | + +Also confirm: notifications fire on each success/failure; the log panel keeps streaming during a Bispectrum run; no `coming soon` banner remains on any of the eleven pages. + +- [x] **Step 3: Check off this plan** + +Mark all checkboxes in this document, note any deviations at the bottom, and commit the plan updates: +```bash +git add docs/superpowers/plans/2026-06-10-quicklook-core-pages.md +git commit -m "docs: record quicklook core implementation plan completion" +``` + +--- + +## Execution notes (2026-06-11) + +- **Rebin factor semantics fixed on both paths**: lightcurve rebin uses `f=` (fractional factor) not the positional `dt` arg; spectrum `rebin()` similarly uses `f=rebin_factor` to avoid passing df-in-Hz. +- **Power-colors axis bug fixed**: `dps.dyn_ps` has shape `(n_freq, n_time)` — mask and mean were applied along the wrong axis in earlier draft; corrected to `dps.dyn_ps[mask, :].mean(axis=0)`. +- **longdouble serialization**: `dps.time`, `lc.time`, and similar stingray arrays use numpy `longdouble`; serialization now coerces via `.astype(float).tolist()` or `float(v)` casts to avoid JSON failures on macOS/Linux where `longdouble` is not JSON-safe. +- **dyn_ps NaN sanitization**: `_finite_list` applied row-by-row on `dps.dyn_ps` to replace any NaN/Inf cells with `null` before JSON serialization. +- **Segment-size + overlap + empty-list guards added**: `_overlap_error` in both `spectrum_service.py` and `timing_service.py` now checks empty event lists first, then disjoint ranges, then optionally rejects if overlap duration < `segment_size`; the optional `segment_size` parameter is passed at all three averaged call sites (`create_averaged_cross_spectrum`, `calculate_time_lags`, `calculate_coherence`), NOT at plain `create_cross_spectrum`. +- **ESLint config created**: project had no `.eslintrc` / `eslint.config.js`; created `eslint.config.js` with `@typescript-eslint` and `react-hooks` rules to make `npm run lint` operational. +- **6 pre-existing typecheck errors cleared**: stray `any` types, missing return-type annotations, and an unused import in route handler stubs were fixed before the first typecheck gate. +- **maxlag capped at 500 + cum3 dropped from payload**: bispectrum `cum3` field is large and unused by the UI; dropped from the JSON response. `maxlag` is validated ≤ 500 server-side to prevent OOM on large lags. +- **Lag sign convention pinned**: positive lag = second list leads first list (stingray convention); documented in the Time Lags page UI and pinned by `test_time_lag_sign_convention_for_shifted_signal`. +- **Time Lags + Power Colors pages added with routes/nav**: Tasks 20–21 added `src/pages/QuickLook/TimeLags/index.tsx` and `PowerColors/index.tsx`, wired routes in `src/App.tsx`, and added sidebar entries in `src/components/layout/Sidebar.tsx`. +- **Decimation with longdouble coercion on lightcurve payloads**: stride-decimation helper also coerces `lc.time` (longdouble on some platforms) to `float64` before `tolist()`. +- **Band-mean (not integrated) power-colors convention documented**: power colors use `dps.dyn_ps[mask, :].mean(axis=0)` (mean over frequencies in band per segment) following the Heil et al. 2015 convention; "integrated" wording removed from UI descriptions to avoid confusion with flux integrals. diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..d977103 --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,59 @@ +import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'dist-electron', + emptyOutDir: false, + rollupOptions: { + input: resolve(__dirname, 'electron/main.ts'), + output: { + format: 'es', + entryFileNames: '[name].js', + }, + }, + }, + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'dist-electron', + emptyOutDir: false, + rollupOptions: { + input: resolve(__dirname, 'electron/preload.ts'), + output: { + format: 'cjs', + // Must be .cjs: the root package.json sets "type": "module", and + // Electron >= ~29 resolves unsandboxed preload module type Node-style, + // so a CommonJS preload named .js is parsed as ESM and crashes before + // contextBridge.exposeInMainWorld runs (electronAPI never appears). + entryFileNames: '[name].cjs', + }, + }, + }, + }, + renderer: { + root: '.', + build: { + outDir: 'dist', + rollupOptions: { + input: resolve(__dirname, 'index.html'), + }, + }, + plugins: [react()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + watch: { + // Ignore large directories to prevent ENOSPC errors on Linux + ignored: ['**/.pixi/**', '**/node_modules/**', '**/.git/**'], + }, + }, + }, +}); diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts new file mode 100644 index 0000000..1352298 --- /dev/null +++ b/electron/ipcHandlers.ts @@ -0,0 +1,295 @@ +import { ipcMain, dialog, app, shell, clipboard, BrowserWindow } from 'electron'; +import fs from 'fs/promises'; +import path from 'path'; +import { PythonManager } from './pythonManager'; + +type PythonManagerGetter = () => PythonManager | null; + +/** + * Set up all IPC handlers for communication between main and renderer processes + */ +export function setupIpcHandlers(getPythonManager: PythonManagerGetter): void { + // ============================================ + // File Dialog Handlers + // ============================================ + + ipcMain.handle( + 'dialog:openFile', + ( + event, + options?: { + title?: string; + filters?: { name: string; extensions: string[] }[]; + multiple?: boolean; + } + ) => { + const parentWindow = BrowserWindow.fromWebContents(event.sender); + + const dialogOptions = { + title: options?.title || 'Open File', + filters: options?.filters || [ + { name: 'FITS Files', extensions: ['fits', 'fit', 'fts'] }, + { name: 'HDF5 Files', extensions: ['hdf5', 'h5', 'hdf'] }, + { name: 'Text Files', extensions: ['txt', 'csv', 'dat', 'ascii'] }, + { name: 'All Files', extensions: ['*'] }, + ], + properties: options?.multiple ? (['openFile', 'multiSelections'] as ('openFile' | 'multiSelections')[]) : (['openFile'] as ('openFile')[]), + }; + + const result = parentWindow + ? dialog.showOpenDialogSync(parentWindow, dialogOptions) + : dialog.showOpenDialogSync(dialogOptions); + + if (!result || result.length === 0) { + return null; + } + + return result; + } + ); + + ipcMain.handle( + 'dialog:saveFile', + ( + event, + options?: { + title?: string; + defaultPath?: string; + filters?: { name: string; extensions: string[] }[]; + } + ) => { + const parentWindow = BrowserWindow.fromWebContents(event.sender); + + const dialogOptions = { + title: options?.title || 'Save File', + defaultPath: options?.defaultPath, + filters: options?.filters || [ + { name: 'FITS Files', extensions: ['fits'] }, + { name: 'HDF5 Files', extensions: ['hdf5'] }, + { name: 'CSV Files', extensions: ['csv'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }; + + const result = parentWindow + ? dialog.showSaveDialogSync(parentWindow, dialogOptions) + : dialog.showSaveDialogSync(dialogOptions); + + return result || null; + } + ); + + ipcMain.handle('dialog:openDirectory', (event) => { + const parentWindow = BrowserWindow.fromWebContents(event.sender); + + const dialogOptions = { + title: 'Select Directory', + properties: ['openDirectory'] as ('openDirectory')[], + }; + + const result = parentWindow + ? dialog.showOpenDialogSync(parentWindow, dialogOptions) + : dialog.showOpenDialogSync(dialogOptions); + + if (!result || result.length === 0) { + return null; + } + + return result[0]; + }); + + // ============================================ + // File System Handlers + // ============================================ + + ipcMain.handle('file:read', async (_event, filePath: string) => { + try { + const buffer = await fs.readFile(filePath); + return buffer.buffer; + } catch (error) { + throw new Error(`Failed to read file: ${error}`); + } + }); + + ipcMain.handle('file:write', async (_event, filePath: string, data: ArrayBuffer | string) => { + try { + const buffer = typeof data === 'string' ? data : Buffer.from(data); + await fs.writeFile(filePath, buffer); + } catch (error) { + throw new Error(`Failed to write file: ${error}`); + } + }); + + ipcMain.handle('file:exists', async (_event, filePath: string) => { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + }); + + // ============================================ + // Python Backend Handlers + // ============================================ + + ipcMain.handle('python:getPort', () => { + const pythonManager = getPythonManager(); + return pythonManager?.getPort() || 8765; + }); + + ipcMain.handle('python:isRunning', () => { + const pythonManager = getPythonManager(); + return pythonManager?.getIsRunning() || false; + }); + + ipcMain.handle('python:restart', async () => { + const pythonManager = getPythonManager(); + if (pythonManager) { + await pythonManager.restart(); + } + }); + + // ============================================ + // Application Info Handlers + // ============================================ + + ipcMain.handle('app:getVersion', () => { + return app.getVersion(); + }); + + ipcMain.handle('app:getName', () => { + return app.getName(); + }); + + ipcMain.handle('app:getPlatform', () => { + return process.platform; + }); + + ipcMain.handle('app:isDev', () => { + return process.env.NODE_ENV === 'development' || !app.isPackaged; + }); + + // ============================================ + // Window Control Handlers + // ============================================ + + ipcMain.on('window:minimize', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + window?.minimize(); + }); + + ipcMain.on('window:maximize', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (window?.isMaximized()) { + window.unmaximize(); + } else { + window?.maximize(); + } + }); + + ipcMain.on('window:close', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + window?.close(); + }); + + ipcMain.on('window:toggleFullscreen', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + window.setFullScreen(!window.isFullScreen()); + } + }); + + ipcMain.on('window:openDevTools', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + window.webContents.openDevTools(); + } + }); + + // ============================================ + // Shell Handlers + // ============================================ + + ipcMain.handle('shell:openExternal', async (_event, url: string) => { + await shell.openExternal(url); + }); + + ipcMain.on('shell:showItemInFolder', (_event, filePath: string) => { + shell.showItemInFolder(path.normalize(filePath)); + }); + + // ============================================ + // Clipboard Handlers + // ============================================ + + ipcMain.on('clipboard:copy', (_event, text: string) => { + clipboard.writeText(text); + }); + + ipcMain.handle('clipboard:read', () => { + return clipboard.readText(); + }); + + // ============================================ + // Resource Monitoring Handlers + // ============================================ + + ipcMain.handle('resources:getElectronUsage', (event) => { + // Get main process metrics + const mainProcessMemory = process.memoryUsage(); + const mainCpuUsage = process.cpuUsage(); + + // Get renderer process metrics + const window = BrowserWindow.fromWebContents(event.sender); + let rendererMetrics = null; + + if (window) { + // Get all app metrics which includes renderer processes + const appMetrics = app.getAppMetrics(); + + // Find the renderer process for this window + const rendererPid = window.webContents.getOSProcessId(); + const rendererProcess = appMetrics.find(m => m.pid === rendererPid); + + if (rendererProcess) { + rendererMetrics = { + memory_mb: rendererProcess.memory.workingSetSize / (1024 * 1024), + cpu_percent: rendererProcess.cpu.percentCPUUsage, + }; + } + } + + // Main process metrics + const mainMetrics: { + memory_mb: number; + heap_used_mb: number; + heap_total_mb: number; + cpu_user_ms: number; + cpu_system_ms: number; + cpu_percent?: number; + } = { + memory_mb: mainProcessMemory.rss / (1024 * 1024), + heap_used_mb: mainProcessMemory.heapUsed / (1024 * 1024), + heap_total_mb: mainProcessMemory.heapTotal / (1024 * 1024), + // CPU usage is cumulative, convert to approximate percent + // Note: This is microseconds since process start, not a percentage + cpu_user_ms: mainCpuUsage.user / 1000, + cpu_system_ms: mainCpuUsage.system / 1000, + }; + + // Get main process CPU percentage from app metrics + const appMetrics = app.getAppMetrics(); + const mainPid = process.pid; + const mainAppMetric = appMetrics.find(m => m.pid === mainPid); + if (mainAppMetric) { + mainMetrics['cpu_percent'] = mainAppMetric.cpu.percentCPUUsage; + } + + return { + main: mainMetrics, + renderer: rendererMetrics, + timestamp: Date.now(), + }; + }); +} diff --git a/electron/main.ts b/electron/main.ts new file mode 100644 index 0000000..a665506 --- /dev/null +++ b/electron/main.ts @@ -0,0 +1,234 @@ +import { app, BrowserWindow, dialog, shell, ipcMain } from 'electron'; +import path from 'path'; +import { PythonManager, LogLevel, LogSource, LogMessage } from './pythonManager'; +import { setupIpcHandlers } from './ipcHandlers'; +import { createAppMenu } from './menu'; + +let mainWindow: BrowserWindow | null = null; +let pythonManager: PythonManager | null = null; +let rendererReady = false; +const logHistory: LogMessage[] = []; // Persistent history for replay +const MAX_LOG_HISTORY = 100; + +const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; + +/** + * Send a log message to the renderer process + * Always stores in history for replay on new connections + */ +function sendLog(level: LogLevel, message: string, source: LogSource = 'electron'): void { + console.log(`[${source}] ${message}`); + const logMessage: LogMessage = { level, source, message }; + + // Always store in history for replay + logHistory.push(logMessage); + if (logHistory.length > MAX_LOG_HISTORY) { + logHistory.shift(); + } + + // Send immediately if renderer ready + if (rendererReady && mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('log:message', logMessage); + } +} + +async function createWindow(): Promise { + mainWindow = new BrowserWindow({ + width: 1400, + height: 900, + minWidth: 1024, + minHeight: 768, + show: false, // Don't show until ready + webPreferences: { + // .cjs extension is load-bearing: see the preload output comment in + // electron.vite.config.ts ("type": "module" + Electron >= ~29). + preload: path.join(__dirname, 'preload.cjs'), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', + // Pin the traffic lights (y:24 centers them in the 64px app header) and + // enable the Window Controls Overlay API so the renderer can size its + // titlebar inset from env(titlebar-area-x) — which tracks page zoom and + // fullscreen, unlike any fixed pixel offset. + trafficLightPosition: { x: 16, y: 24 }, + titleBarOverlay: true, + icon: isDev + ? path.join(__dirname, '../resources/icon.png') + : path.join(process.resourcesPath, 'icon.png'), + backgroundColor: '#ffffff', + }); + + // Set up the application menu + createAppMenu(mainWindow); + + // Show window when ready + mainWindow.once('ready-to-show', () => { + mainWindow?.show(); + // DevTools can be opened manually with Ctrl+Shift+I or View menu + }); + + // Handle external links + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + // Add right-click context menu for copy/paste + mainWindow.webContents.on('context-menu', (_event, params) => { + const { Menu, MenuItem } = require('electron'); + const menu = new Menu(); + + // Add "Copy" if text is selected + if (params.selectionText) { + menu.append(new MenuItem({ + label: 'Copy', + role: 'copy', + })); + } + + // Add "Paste" if in an editable field + if (params.isEditable) { + menu.append(new MenuItem({ + label: 'Paste', + role: 'paste', + })); + } + + // Add "Cut" if text is selected and in editable field + if (params.selectionText && params.isEditable) { + menu.insert(0, new MenuItem({ + label: 'Cut', + role: 'cut', + })); + } + + // Add "Select All" for editable fields + if (params.isEditable) { + menu.append(new MenuItem({ + label: 'Select All', + role: 'selectAll', + })); + } + + // Only show menu if it has items + if (menu.items.length > 0) { + menu.popup(); + } + }); + + // Load the app + if (isDev) { + await mainWindow.loadURL('http://localhost:5173'); + } else { + await mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +async function initializeApp(): Promise { + try { + // Start Python backend + pythonManager = new PythonManager(); + + // Set the log callback so pythonManager uses our buffered logging + pythonManager.setLogCallback((level, message, source) => { + sendLog(level, message, source); + }); + + // Notify renderer that we're starting Python + mainWindow?.webContents.send('python:starting'); + sendLog('info', 'Starting Python backend...'); + + await pythonManager.start(); + + // Notify renderer that Python is ready + mainWindow?.webContents.send('python:ready', pythonManager.getPort()); + + sendLog('info', `Python backend started successfully on port ${pythonManager.getPort()}`); + } catch (error) { + sendLog('error', `Failed to start Python backend: ${error}`); + mainWindow?.webContents.send('python:error', String(error)); + + // Show error dialog + dialog.showErrorBox( + 'Python Backend Error', + `Failed to start the Python backend. Please ensure Python and required dependencies are installed.\n\nError: ${error}` + ); + } +} + +// Handle renderer ready signal +ipcMain.on('log:rendererReady', () => { + rendererReady = true; + // Note: We no longer replay log history here to avoid duplicate logs. + // Early startup logs are visible in the terminal. + // Real-time logs come through the Python SSE stream once the backend is ready. +}); + +// App lifecycle +app.whenReady().then(async () => { + // Set the application name (important for Linux desktop integration) + app.setName('Stingray Explorer'); + + // Set up IPC handlers before creating window + setupIpcHandlers(() => pythonManager); + + await createWindow(); + await initializeApp(); + + app.on('activate', async () => { + // On macOS, re-create the window when the dock icon is clicked. + if (BrowserWindow.getAllWindows().length === 0) { + await createWindow(); + + // Make sure the freshly-loaded renderer ends up connected. Normally the + // backend is still running (see 'window-all-closed'), so just tell the new + // window its port — the renderer's mount-time check also reconnects on its + // own. If the backend somehow isn't running, (re)initialize it so we never + // get stuck showing "Starting..." with no backend. + if (pythonManager && pythonManager.getIsRunning()) { + mainWindow?.webContents.send('python:ready', pythonManager.getPort()); + } else { + await initializeApp(); + } + } + }); +}); + +app.on('window-all-closed', async () => { + // On macOS the app (and its Python backend) stays alive when all windows are + // closed — the user reopens via the dock and we want the backend still there + // to reconnect to. Tearing it down here meant a reopened window had no backend + // and got stuck on "Starting...". Only fully shut down on platforms where + // closing the last window means quitting; final cleanup lives in 'before-quit'. + if (process.platform !== 'darwin') { + if (pythonManager) { + await pythonManager.stop(); + pythonManager = null; + } + app.quit(); + } +}); + +app.on('before-quit', async () => { + // Ensure Python backend is stopped + if (pythonManager) { + await pythonManager.stop(); + pythonManager = null; + } +}); + +// Handle uncaught exceptions +process.on('uncaughtException', (error) => { + sendLog('error', `Uncaught exception: ${error.message}`); + dialog.showErrorBox('Unexpected Error', `An unexpected error occurred:\n\n${error.message}`); +}); + +process.on('unhandledRejection', (reason) => { + sendLog('error', `Unhandled rejection: ${reason}`); +}); diff --git a/electron/menu.ts b/electron/menu.ts new file mode 100644 index 0000000..a6d9986 --- /dev/null +++ b/electron/menu.ts @@ -0,0 +1,249 @@ +import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron'; + +const isMac = process.platform === 'darwin'; + +/** + * Create the application menu + */ +export function createAppMenu(mainWindow: BrowserWindow): void { + const template: MenuItemConstructorOptions[] = [ + // App menu (macOS only) + ...(isMac + ? [ + { + label: app.name, + submenu: [ + { role: 'about' as const }, + { type: 'separator' as const }, + { + label: 'Preferences...', + accelerator: 'CmdOrCtrl+,', + click: (): void => { + mainWindow.webContents.send('menu:preferences'); + }, + }, + { type: 'separator' as const }, + { role: 'services' as const }, + { type: 'separator' as const }, + { role: 'hide' as const }, + { role: 'hideOthers' as const }, + { role: 'unhide' as const }, + { type: 'separator' as const }, + { role: 'quit' as const }, + ], + }, + ] + : []), + + // File menu + { + label: 'File', + submenu: [ + { + label: 'Open File...', + accelerator: 'CmdOrCtrl+O', + click: (): void => { + mainWindow.webContents.send('menu:openFile'); + }, + }, + { + label: 'Open Recent', + role: 'recentDocuments' as const, + submenu: [ + { + label: 'Clear Recent', + role: 'clearRecentDocuments' as const, + }, + ], + }, + { type: 'separator' }, + { + label: 'Save', + accelerator: 'CmdOrCtrl+S', + click: (): void => { + mainWindow.webContents.send('menu:save'); + }, + }, + { + label: 'Save As...', + accelerator: 'CmdOrCtrl+Shift+S', + click: (): void => { + mainWindow.webContents.send('menu:saveAs'); + }, + }, + { + label: 'Export...', + accelerator: 'CmdOrCtrl+E', + click: (): void => { + mainWindow.webContents.send('menu:export'); + }, + }, + { type: 'separator' }, + isMac ? { role: 'close' as const } : { role: 'quit' as const }, + ], + }, + + // Edit menu + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + ...(isMac + ? [ + { role: 'pasteAndMatchStyle' as const }, + { role: 'delete' as const }, + { role: 'selectAll' as const }, + ] + : [{ role: 'delete' as const }, { type: 'separator' as const }, { role: 'selectAll' as const }]), + ], + }, + + // View menu + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + { type: 'separator' }, + { + label: 'Toggle Sidebar', + accelerator: 'CmdOrCtrl+B', + click: (): void => { + mainWindow.webContents.send('menu:toggleSidebar'); + }, + }, + ], + }, + + // Analysis menu + { + label: 'Analysis', + submenu: [ + { + label: 'QuickLook', + submenu: [ + { + label: 'Power Spectrum', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/quicklook/power-spectrum'); + }, + }, + { + label: 'Light Curve', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/quicklook/light-curve'); + }, + }, + { + label: 'Cross Spectrum', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/quicklook/cross-spectrum'); + }, + }, + ], + }, + { + label: 'Pulsar', + submenu: [ + { + label: 'Period Search', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/pulsar/search'); + }, + }, + { + label: 'Phase Folding', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/pulsar/folding'); + }, + }, + ], + }, + { + label: 'Modeling', + submenu: [ + { + label: 'Model Builder', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/modeling/builder'); + }, + }, + { + label: 'MCMC Fitting', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/modeling/mcmc'); + }, + }, + ], + }, + { type: 'separator' }, + { + label: 'Simulator', + click: (): void => { + mainWindow.webContents.send('menu:navigate', '/simulator'); + }, + }, + ], + }, + + // Window menu + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + ...(isMac + ? [{ type: 'separator' as const }, { role: 'front' as const }, { type: 'separator' as const }, { role: 'window' as const }] + : [{ role: 'close' as const }]), + ], + }, + + // Help menu + { + role: 'help', + submenu: [ + { + label: 'Stingray Documentation', + click: async (): Promise => { + await shell.openExternal('https://docs.stingray.science/'); + }, + }, + { + label: 'Stingray Explorer Wiki', + click: async (): Promise => { + await shell.openExternal('https://github.com/kartikmandar-GSOC24/StingrayExplorer/wiki'); + }, + }, + { type: 'separator' }, + { + label: 'Report Issue', + click: async (): Promise => { + await shell.openExternal('https://github.com/kartikmandar-GSOC24/StingrayExplorer/issues'); + }, + }, + { type: 'separator' }, + { + label: 'About Stingray', + click: async (): Promise => { + await shell.openExternal('https://stingray.science/'); + }, + }, + ], + }, + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} diff --git a/electron/preload.ts b/electron/preload.ts new file mode 100644 index 0000000..f467950 --- /dev/null +++ b/electron/preload.ts @@ -0,0 +1,236 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +/** + * Electron API exposed to the renderer process via context bridge + * All communication between renderer and main process goes through here + */ +const electronAPI = { + // ============================================ + // File System Operations + // ============================================ + + /** + * Open a file dialog and return selected file paths + */ + openFile: (options?: { + title?: string; + filters?: { name: string; extensions: string[] }[]; + multiple?: boolean; + }): Promise => ipcRenderer.invoke('dialog:openFile', options), + + /** + * Open a save dialog and return the selected path + */ + saveFile: (options?: { + title?: string; + defaultPath?: string; + filters?: { name: string; extensions: string[] }[]; + }): Promise => ipcRenderer.invoke('dialog:saveFile', options), + + /** + * Open a directory selection dialog + */ + openDirectory: (): Promise => ipcRenderer.invoke('dialog:openDirectory'), + + /** + * Read a file and return its contents + */ + readFile: (filePath: string): Promise => ipcRenderer.invoke('file:read', filePath), + + /** + * Write data to a file + */ + writeFile: (filePath: string, data: ArrayBuffer | string): Promise => + ipcRenderer.invoke('file:write', filePath, data), + + /** + * Check if a file exists + */ + fileExists: (filePath: string): Promise => ipcRenderer.invoke('file:exists', filePath), + + // ============================================ + // Python Backend Communication + // ============================================ + + /** + * Get the port the Python backend is running on + */ + getBackendPort: (): Promise => ipcRenderer.invoke('python:getPort'), + + /** + * Check if Python backend is running + */ + isPythonRunning: (): Promise => ipcRenderer.invoke('python:isRunning'), + + /** + * Restart the Python backend + */ + restartPython: (): Promise => ipcRenderer.invoke('python:restart'), + + /** + * Subscribe to Python backend ready event + */ + onPythonReady: (callback: (port: number) => void): (() => void) => { + const handler = (_event: Electron.IpcRendererEvent, port: number): void => callback(port); + ipcRenderer.on('python:ready', handler); + return () => ipcRenderer.removeListener('python:ready', handler); + }, + + /** + * Subscribe to Python backend starting event + */ + onPythonStarting: (callback: () => void): (() => void) => { + const handler = (): void => callback(); + ipcRenderer.on('python:starting', handler); + return () => ipcRenderer.removeListener('python:starting', handler); + }, + + /** + * Subscribe to Python backend error event + */ + onPythonError: (callback: (error: string) => void): (() => void) => { + const handler = (_event: Electron.IpcRendererEvent, error: string): void => callback(error); + ipcRenderer.on('python:error', handler); + return () => ipcRenderer.removeListener('python:error', handler); + }, + + // ============================================ + // Application Info + // ============================================ + + /** + * Get the application version + */ + getAppVersion: (): Promise => ipcRenderer.invoke('app:getVersion'), + + /** + * Get the application name + */ + getAppName: (): Promise => ipcRenderer.invoke('app:getName'), + + /** + * Get platform information + */ + getPlatform: (): Promise => ipcRenderer.invoke('app:getPlatform'), + + /** + * Check if running in development mode + */ + isDev: (): Promise => ipcRenderer.invoke('app:isDev'), + + // ============================================ + // Window Controls + // ============================================ + + /** + * Minimize the window + */ + minimizeWindow: (): void => ipcRenderer.send('window:minimize'), + + /** + * Maximize/restore the window + */ + maximizeWindow: (): void => ipcRenderer.send('window:maximize'), + + /** + * Close the window + */ + closeWindow: (): void => ipcRenderer.send('window:close'), + + /** + * Toggle fullscreen mode + */ + toggleFullscreen: (): void => ipcRenderer.send('window:toggleFullscreen'), + + /** + * Open Chrome DevTools + */ + openDevTools: (): void => ipcRenderer.send('window:openDevTools'), + + // ============================================ + // Shell Operations + // ============================================ + + /** + * Open a URL in the default browser + */ + openExternal: (url: string): Promise => ipcRenderer.invoke('shell:openExternal', url), + + /** + * Show an item in the file manager + */ + showItemInFolder: (path: string): void => ipcRenderer.send('shell:showItemInFolder', path), + + // ============================================ + // Clipboard Operations + // ============================================ + + /** + * Copy text to clipboard + */ + copyToClipboard: (text: string): void => ipcRenderer.send('clipboard:copy', text), + + /** + * Read text from clipboard + */ + readFromClipboard: (): Promise => ipcRenderer.invoke('clipboard:read'), + + // ============================================ + // Log Events + // ============================================ + + /** + * Subscribe to log messages from main process + */ + onLog: ( + callback: (log: { level: 'info' | 'warn' | 'error' | 'debug'; source: 'python' | 'electron'; message: string }) => void + ): (() => void) => { + const handler = ( + _event: Electron.IpcRendererEvent, + log: { level: 'info' | 'warn' | 'error' | 'debug'; source: 'python' | 'electron'; message: string } + ): void => callback(log); + ipcRenderer.on('log:message', handler); + return () => ipcRenderer.removeListener('log:message', handler); + }, + + /** + * Send a log message from renderer to main (for aggregation) + */ + sendLog: (log: { level: 'info' | 'warn' | 'error' | 'debug'; message: string }): void => { + ipcRenderer.send('log:fromRenderer', log); + }, + + /** + * Signal that the renderer is ready to receive logs + */ + signalLogReady: (): void => { + ipcRenderer.send('log:rendererReady'); + }, + + // ============================================ + // Resource Monitoring + // ============================================ + + /** + * Get Electron process resource usage (main + renderer) + */ + getElectronResources: (): Promise<{ + main: { + memory_mb: number; + heap_used_mb: number; + heap_total_mb: number; + cpu_percent?: number; + }; + renderer: { + memory_mb: number; + cpu_percent: number; + } | null; + timestamp: number; + }> => ipcRenderer.invoke('resources:getElectronUsage'), +}; + +// Expose the API to the renderer process +contextBridge.exposeInMainWorld('electronAPI', electronAPI); + +// Type declaration for the exposed API +export type ElectronAPI = typeof electronAPI; diff --git a/electron/pythonManager.ts b/electron/pythonManager.ts new file mode 100644 index 0000000..e1ae185 --- /dev/null +++ b/electron/pythonManager.ts @@ -0,0 +1,402 @@ +import { spawn, ChildProcess } from 'child_process'; +import path from 'path'; +import { app } from 'electron'; +import http from 'http'; + +export type LogLevel = 'info' | 'warn' | 'error' | 'debug'; +export type LogSource = 'python' | 'electron'; + +export interface LogMessage { + level: LogLevel; + source: LogSource; + message: string; +} + +export type LogCallback = (level: LogLevel, message: string, source: LogSource) => void; + +export class PythonManager { + private process: ChildProcess | null = null; + private port: number = 8765; + private retryInterval: number = 500; // ms between health checks + // Hard cap on how long we wait for the backend to answer /health. A cold + // first launch imports the full scientific stack (stingray, numba, astropy, + // scipy) and warms numba's compile cache, which can take ~60s — so this is + // deliberately generous. We fail sooner if the spawned process dies (see + // waitForReady), so a genuine startup crash still surfaces quickly. + private readyTimeoutMs: number = 180000; // 3 minutes + private progressLogIntervalMs: number = 15000; // emit a "still waiting" log every 15s + private isRunning: boolean = false; + private externalBackend: boolean = false; // True if backend was started externally + private logCallback: LogCallback | null = null; + + /** + * Set the log callback for sending logs to the renderer + */ + setLogCallback(callback: LogCallback): void { + this.logCallback = callback; + } + + /** + * Send a log message via the callback + */ + private sendLog(level: LogLevel, message: string): void { + if (this.logCallback) { + this.logCallback(level, message, 'python'); + } else { + console.log(`[Python] ${message}`); + } + } + + /** + * Start the Python backend process + */ + async start(): Promise { + if (this.isRunning) { + this.sendLog('info', 'Python backend is already running'); + return; + } + + // First check if backend is already running (started by dev.sh or externally) + const alreadyRunning = await this.checkHealth(); + if (alreadyRunning) { + this.sendLog('info', 'Python backend is already running externally, connecting to it...'); + this.isRunning = true; + this.externalBackend = true; + return; + } + + // Not running, start it ourselves + const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; + const { pythonPath, args } = this.getPythonCommand(); + + this.sendLog('info', `Starting Python backend: ${pythonPath} ${args.join(' ')}`); + + this.process = spawn(pythonPath, args, { + cwd: isDev ? path.join(app.getAppPath(), 'python-backend') : undefined, + env: { + ...process.env, + PYTHONUNBUFFERED: '1', + PYTHONDONTWRITEBYTECODE: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + // Handle stdout + this.process.stdout?.on('data', (data: Buffer) => { + const lines = data.toString().trim().split('\n'); + for (const line of lines) { + if (line) { + // Check for port announcement + const portMatch = line.match(/^BACKEND_PORT:(\d+)$/); + if (portMatch) { + this.port = parseInt(portMatch[1], 10); + this.sendLog('info', `Backend will use port ${this.port}`); + } + + // Detect log level from message content + const level = this.detectLogLevel(line); + this.sendLog(level, line); + } + } + }); + + // Handle stderr + this.process.stderr?.on('data', (data: Buffer) => { + const lines = data.toString().trim().split('\n'); + for (const line of lines) { + if (line) { + // Stderr messages are typically warnings or errors + const level = this.detectLogLevel(line, 'warn'); + this.sendLog(level, line); + } + } + }); + + // Handle process exit + this.process.on('exit', (code, signal) => { + const message = `Python backend exited with code ${code}, signal ${signal}`; + this.sendLog(code === 0 ? 'info' : 'error', message); + this.isRunning = false; + this.process = null; + }); + + // Handle process error + this.process.on('error', (error) => { + this.sendLog('error', `Failed to start Python backend: ${error.message}`); + this.isRunning = false; + }); + + // Wait for backend to be ready + await this.waitForReady(); + this.isRunning = true; + } + + /** + * Detect log level from message content + */ + private detectLogLevel(message: string, defaultLevel: LogLevel = 'info'): LogLevel { + const lowerMessage = message.toLowerCase(); + + // Check for explicit level prefixes (uvicorn style: "INFO:", "WARNING:", etc.) + if (lowerMessage.startsWith('info:') || lowerMessage.includes('info: ')) { + return 'info'; + } + if (lowerMessage.startsWith('debug:')) { + return 'debug'; + } + + // Check for error indicators + if (lowerMessage.startsWith('error:') || lowerMessage.includes('error') || + lowerMessage.includes('exception') || lowerMessage.includes('traceback')) { + return 'error'; + } + + // Check for warning indicators + if (lowerMessage.startsWith('warning:') || lowerMessage.startsWith('warn:') || + lowerMessage.includes('warning') || lowerMessage.includes('warn')) { + return 'warn'; + } + + return defaultLevel; + } + + /** + * Request the backend to shutdown via API + */ + private async requestShutdown(): Promise { + return new Promise((resolve) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: this.port, + path: '/api/shutdown', + method: 'POST', + timeout: 2000, + }, + (res) => { + resolve(res.statusCode === 200); + } + ); + + req.on('error', () => { + resolve(false); + }); + + req.on('timeout', () => { + req.destroy(); + resolve(false); + }); + + req.end(); + }); + } + + /** + * Wait for the backend to stop + */ + private async waitForStop(): Promise { + for (let i = 0; i < 20; i++) { + const isRunning = await this.checkHealth(); + if (!isRunning) { + return; + } + await this.sleep(250); + } + } + + /** + * Stop the Python backend process + */ + async stop(): Promise { + this.sendLog('info', 'Stopping Python backend...'); + + // If it was external, request shutdown via API + if (this.externalBackend) { + const shutdownRequested = await this.requestShutdown(); + if (shutdownRequested) { + await this.waitForStop(); + this.sendLog('info', 'External backend stopped via API'); + } + this.isRunning = false; + this.externalBackend = false; + return; + } + + // If we spawned it, kill the process + if (!this.process) { + this.isRunning = false; + return; + } + + return new Promise((resolve) => { + if (!this.process) { + resolve(); + return; + } + + // Try graceful shutdown first + this.process.once('exit', () => { + this.process = null; + this.isRunning = false; + this.sendLog('info', 'Python backend stopped'); + resolve(); + }); + + // Send SIGTERM for graceful shutdown + this.process.kill('SIGTERM'); + + // Force kill after 5 seconds if still running + setTimeout(() => { + if (this.process) { + this.sendLog('warn', 'Force killing Python backend...'); + this.process.kill('SIGKILL'); + } + }, 5000); + }); + } + + /** + * Get the Python command and arguments based on environment + */ + private getPythonCommand(): { pythonPath: string; args: string[] } { + const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; + + if (isDev) { + // Development: run main.py from python-backend directory + // The cwd is set to python-backend in the spawn call + // Use pixi environment Python if available, otherwise fall back to system python + const pixiPython = path.join(app.getAppPath(), '.pixi', 'envs', 'default', 'bin', 'python'); + return { + pythonPath: pixiPython, + args: ['main.py'], + }; + } else { + // Production: use bundled executable + const platform = process.platform; + let executableName = 'stingray-backend'; + + if (platform === 'win32') { + executableName = 'stingray-backend.exe'; + } + + const executablePath = path.join(process.resourcesPath, 'python-backend', executableName); + + return { + pythonPath: executablePath, + args: [], + }; + } + } + + /** + * Wait for the Python backend to be ready + */ + private async waitForReady(): Promise { + this.sendLog( + 'info', + 'Waiting for Python backend to be ready (first launch can take ~60s while the scientific stack and numba caches warm up)...' + ); + + const startTime = Date.now(); + let lastProgressLog = startTime; + + // Poll until the backend is healthy, the process dies, or we hit the hard + // cap. We deliberately keep waiting as long as the process is alive — a + // cold import of stingray/numba/astropy routinely exceeds the old 30s + // limit on first launch, which made Electron give up and show a spurious + // "backend failed to start" error even though it came up moments later. + while (Date.now() - startTime < this.readyTimeoutMs) { + // If we spawned the process and it has already exited, there's no point + // waiting out the full timeout — fail fast so the real error (crash, + // missing dependency, etc.) surfaces immediately. The exit handler in + // start() sets this.process to null when the child exits. + if (!this.process) { + throw new Error('Python backend process exited before becoming ready'); + } + + try { + if (await this.checkHealth()) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + this.sendLog('info', `Python backend is ready! (took ${elapsed}s)`); + return; + } + } catch { + // Ignore errors, keep trying + } + + // Periodic progress so a slow cold start doesn't look like a hang. + if (Date.now() - lastProgressLog >= this.progressLogIntervalMs) { + const elapsed = Math.round((Date.now() - startTime) / 1000); + this.sendLog('info', `Still waiting for Python backend... (${elapsed}s elapsed)`); + lastProgressLog = Date.now(); + } + + await this.sleep(this.retryInterval); + } + + const seconds = Math.round(this.readyTimeoutMs / 1000); + const errorMsg = `Python backend failed to become ready within ${seconds} seconds`; + this.sendLog('error', errorMsg); + throw new Error(errorMsg); + } + + /** + * Check if the backend is healthy + */ + private checkHealth(): Promise { + return new Promise((resolve) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: this.port, + path: '/health', + method: 'GET', + timeout: 1000, + }, + (res) => { + resolve(res.statusCode === 200); + } + ); + + req.on('error', () => { + resolve(false); + }); + + req.on('timeout', () => { + req.destroy(); + resolve(false); + }); + + req.end(); + }); + } + + /** + * Sleep for a specified number of milliseconds + */ + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Get the port the Python backend is running on + */ + getPort(): number { + return this.port; + } + + /** + * Check if the Python backend is running + */ + getIsRunning(): boolean { + return this.isRunning; + } + + /** + * Restart the Python backend + */ + async restart(): Promise { + await this.stop(); + await this.start(); + } +} diff --git a/files/data/DATA_README.txt b/files/data/DATA_README.txt new file mode 100644 index 0000000..4f32287 --- /dev/null +++ b/files/data/DATA_README.txt @@ -0,0 +1,143 @@ +================================================================================ + StingrayExplorer Sample Data Files +================================================================================ + +This folder contains sample X-ray astronomy event lists and light curves for +testing and demonstration purposes with StingrayExplorer. + +================================================================================ + EVENT LIST FILES +================================================================================ + +ni1200120106_0mpu7_cl_bary.evt.gz (2.4 GB compressed) +------------------------------------------------------------------------------ + Source: MAXI J1820+070 (accreting stellar-mass black hole) + Mission: NASA NICER (Neutron star Interior Composition Explorer) + ObsID: 1200120106 + Observation: 2018 outburst - famous for strong quasi-periodic oscillations + Processing: Barycentered using JPL DE 430 ephemeris + Authors: Matteo Bachetti, Daniela Huppenkothen + Download: https://zenodo.org/record/6785435 + Usage: Official Stingray tutorial dataset + Load with: EventList.read("ni1200120106_0mpu7_cl_bary.evt.gz", fmt="hea") + +monol_testA.evt (28 KB) +------------------------------------------------------------------------------ + Source: Simulated/test data + Mission: Generic test event list + Description: Basic event list for unit testing and quick demos + Load with: EventList.read("monol_testA.evt", fmt="hea") + +monol_testA_calib.evt (25 KB) +------------------------------------------------------------------------------ + Source: Simulated/test data + Mission: Generic test event list + Description: Calibrated version of monol_testA with energy information + Load with: EventList.read("monol_testA_calib.evt", fmt="hea") + +monol_testA_calib_unsrt.evt (25 KB) +------------------------------------------------------------------------------ + Source: Simulated/test data + Mission: Generic test event list + Description: Unsorted calibrated event list (for testing sorting functions) + Load with: EventList.read("monol_testA_calib_unsrt.evt", fmt="hea") + +nomission.evt (28 KB) +------------------------------------------------------------------------------ + Source: Simulated/test data + Mission: None (generic format) + Description: Event list without mission-specific metadata + Usage: Testing generic event list handling + Load with: EventList.read("nomission.evt", fmt="hea") + +xte_test.evt.gz (11 KB compressed) +------------------------------------------------------------------------------ + Source: Test data based on RXTE format + Mission: RXTE (Rossi X-ray Timing Explorer) + Description: Small RXTE-format event list for testing + Usage: Testing RXTE data loading and processing + Load with: EventList.read("xte_test.evt.gz", fmt="hea") + +xte_gx_test.evt.gz (34 KB compressed) +------------------------------------------------------------------------------ + Source: Test data based on RXTE format + Mission: RXTE (Rossi X-ray Timing Explorer) + Description: RXTE event list, likely from a GX source observation + Usage: Testing RXTE data with slightly more events + Load with: EventList.read("xte_gx_test.evt.gz", fmt="hea") + +================================================================================ + LIGHT CURVE FILES +================================================================================ + +lcurveA.fits (37 KB) +------------------------------------------------------------------------------ + Type: Pre-computed light curve + Format: FITS (OGIP standard) + Description: Sample light curve for testing light curve operations + Load with: Lightcurve.read("lcurveA.fits", fmt="ogip") + +lcurve_new.fits (54 KB) +------------------------------------------------------------------------------ + Type: Pre-computed light curve + Format: FITS (OGIP standard) + Description: Another sample light curve with different parameters + Load with: Lightcurve.read("lcurve_new.fits", fmt="ogip") + +LightCurve_bexvar.fits (416 KB) +------------------------------------------------------------------------------ + Type: Pre-computed light curve + Format: FITS + Description: Light curve for testing excess variance (bexvar) calculations + Usage: Testing variability and excess variance spectrum analysis + Load with: Lightcurve.read("LightCurve_bexvar.fits", fmt="ogip") + +================================================================================ + LOADING DATA IN PYTHON +================================================================================ + +Using Stingray directly: + + from stingray import EventList, Lightcurve + + # Load event list + evt = EventList.read("files/data/monol_testA.evt", fmt="hea") + print(f"Events: {len(evt.time)}, Time range: {evt.time.min()}-{evt.time.max()}") + + # Load light curve + lc = Lightcurve.read("files/data/lcurveA.fits", fmt="ogip") + print(f"Bins: {len(lc.time)}, Count rate: {lc.countrate.mean():.2f} cts/s") + +Using StingrayExplorer UI: + + 1. Go to "Data Ingestion" page + 2. Click "Browse Files" and select the file + 3. Choose format: "OGIP/FITS (recommended)" for .evt/.fits files + 4. Enter a name for the dataset + 5. Click "Load Event List" + +================================================================================ + DATA SOURCES +================================================================================ + +HEASARC (NASA): https://heasarc.gsfc.nasa.gov +NICER Archive: https://heasarc.gsfc.nasa.gov/docs/nicer/ +Stingray Docs: https://docs.stingray.science/ +Zenodo Dataset: https://zenodo.org/record/6785435 + +================================================================================ + FILE FORMATS +================================================================================ + +.evt / .evt.gz - Event list files (photon arrival times + metadata) +.fits - FITS format (Flexible Image Transport System) +.gz - Gzip compressed files (auto-detected by Stingray) + +Format parameter for EventList.read(): + - "hea" or "ogip" : HEASARC/OGIP standard FITS event files + - "hdf5" : HDF5 format + - "ascii.ecsv" : ASCII Enhanced CSV + +================================================================================ +Last updated: February 2025 +================================================================================ diff --git a/index.html b/index.html new file mode 100644 index 0000000..00d4f7f --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + Stingray Explorer + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..73bd8ec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15093 @@ +{ + "name": "stingray-explorer", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stingray-explorer", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/ibm-plex-sans": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@mui/icons-material": "^5.15.6", + "@mui/material": "^5.15.6", + "@tanstack/react-query": "^5.17.19", + "axios": "^1.6.7", + "plotly.js": "^2.29.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-plotly.js": "^2.6.0", + "react-router-dom": "^6.22.0", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^20.11.16", + "@types/plotly.js": "^2.35.14", + "@types/react": "^18.2.52", + "@types/react-dom": "^18.2.18", + "@types/react-plotly.js": "^2.6.3", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", + "@vitejs/plugin-react": "^5.2.0", + "electron": "^41.0.4", + "electron-builder": "^26.8.1", + "electron-vite": "^5.0.0", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jsdom": "^29.1.1", + "typescript": "^5.3.3", + "vite": "^7.3.1", + "vitest": "^4.1.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "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/core/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/@babel/core/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/@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==", + "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-compilation-targets/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/@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==", + "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==", + "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-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "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==", + "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.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@choojs/findup": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", + "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", + "license": "MIT", + "dependencies": { + "commander": "^2.15.1" + }, + "bin": { + "findup": "bin/findup.js" + } + }, + "node_modules/@choojs/findup/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/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/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz", + "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-sans": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.2.8.tgz", + "integrity": "sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-rewind/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", + "license": "ISC" + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", + "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", + "license": "BSD-3-Clause", + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", + "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz", + "integrity": "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", + "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", + "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.18.0", + "@mui/system": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz", + "integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz", + "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@plotly/d3": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.2.tgz", + "integrity": "sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", + "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "node_modules/@plotly/d3-sankey-circular": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", + "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", + "license": "MIT", + "dependencies": { + "d3-array": "^1.2.1", + "d3-collection": "^1.0.4", + "d3-shape": "^1.2.0", + "elementary-circuits-directed-graph": "^1.0.4" + } + }, + "node_modules/@plotly/mapbox-gl": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz", + "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/@plotly/point-cluster": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", + "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "binary-search-bounds": "^2.0.4", + "clamp": "^1.0.1", + "defined": "^1.0.0", + "dtype": "^2.0.0", + "flatten-vertex-data": "^1.0.2", + "is-obj": "^1.0.1", + "math-log2": "^1.0.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz", + "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", + "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.95.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@turf/area": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.3.4.tgz", + "integrity": "sha512-UEQQFw2XwHpozSBAMEtZI3jDsAad4NnHL/poF7/S6zeDCjEBCkt3MYd6DSGH/cvgcOozxH/ky3/rIVSMZdx4vA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.3.4.tgz", + "integrity": "sha512-D5ErVWtfQbEPh11yzI69uxqrcJmbPU/9Y59f1uTapgwAwQHQztDWgsYpnL3ns8r1GmPWLP8sGJLVTIk2TZSiYA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.3.4.tgz", + "integrity": "sha512-6c3kyTSKBrmiPMe75UkHw6MgedroZ6eR5usEvdlDhXgA3MudFPXIZkMFmMd1h9XeJ9xFfkmq+HPCdF0cOzvztA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz", + "integrity": "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.4.tgz", + "integrity": "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "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==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/plotly.js": { + "version": "2.35.14", + "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.35.14.tgz", + "integrity": "sha512-CcD/32JcK19+xWH4FFpmYez/5X9kOjUcBr8Hxh7gQ/3Z32gIoLLy/L9xvC7DG5YikPvJjq6QN05B9+MCRu/Ncw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-plotly.js": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/react-plotly.js/-/react-plotly.js-2.6.4.tgz", + "integrity": "sha512-AU6w1u3qEGM0NmBA69PaOgNc0KPFA/+qkH6Uu9EBTJ45/WYOUoXi9AF5O15PRM2klpHSiHAAs4WnlI+OZAFmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/plotly.js": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "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.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "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/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "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/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/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/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/almost-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-1.1.0.tgz", + "integrity": "sha512-0V/PkoculFl5+0Lp47JoxUcO0xSxhIBvm+BxHdD/OgXNmdRpRHCFnKVuUoWyS9EzQP+otSGv0m9Lb4yVkQBn2A==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/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/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-bounds": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-bounds/-/array-bounds-1.0.1.tgz", + "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==", + "license": "MIT" + }, + "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-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-normalize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array-normalize/-/array-normalize-1.1.4.tgz", + "integrity": "sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.0" + } + }, + "node_modules/array-range": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-range/-/array-range-1.0.1.tgz", + "integrity": "sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==", + "license": "MIT" + }, + "node_modules/array-rearrange": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/array-rearrange/-/array-rearrange-2.2.2.tgz", + "integrity": "sha512-UfobP5N12Qm4Qu4fwLDIi2v6+wZsSf6snYSxAMeKhrh37YGnNWZPRmVEKc/2wfms53TLQnzfpG8wCx2Y/6NG1w==", + "license": "MIT" + }, + "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.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/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.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/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "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/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "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/baseline-browser-mapping": { + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", + "license": "MIT" + }, + "node_modules/bit-twiddle": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz", + "integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==", + "license": "MIT" + }, + "node_modules/bitmap-sdf": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", + "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "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/canvas-fit": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/canvas-fit/-/canvas-fit-1.5.0.tgz", + "integrity": "sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==", + "license": "MIT", + "dependencies": { + "element-size": "^1.1.1" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clamp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", + "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-alpha": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.4.tgz", + "integrity": "sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==", + "license": "MIT", + "dependencies": { + "color-parse": "^1.3.8" + } + }, + "node_modules/color-alpha/node_modules/color-parse": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", + "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/color-id/-/color-id-1.1.0.tgz", + "integrity": "sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/color-normalize": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/color-normalize/-/color-normalize-1.5.0.tgz", + "integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1", + "color-rgba": "^2.1.1", + "dtype": "^2.0.0" + } + }, + "node_modules/color-parse": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-2.0.0.tgz", + "integrity": "sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-rgba": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-2.1.1.tgz", + "integrity": "sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1", + "color-parse": "^1.3.8", + "color-space": "^1.14.6" + } + }, + "node_modules/color-rgba/node_modules/color-parse": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", + "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-space": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/color-space/-/color-space-1.16.0.tgz", + "integrity": "sha512-A6WMiFzunQ8KEPFmj02OnnoUnqhmSaHaZ/0LVFcPTdlvm8+3aMJ5x1HRHy3bDHPkovkf4sS0f4wsVvwk71fKkg==", + "license": "MIT", + "dependencies": { + "hsluv": "^0.0.3", + "mumath": "^3.3.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/country-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz", + "integrity": "sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==", + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "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/cross-spawn/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/cross-spawn/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/css-font": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-font/-/css-font-1.2.0.tgz", + "integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==", + "license": "MIT", + "dependencies": { + "css-font-size-keywords": "^1.0.0", + "css-font-stretch-keywords": "^1.0.1", + "css-font-style-keywords": "^1.0.1", + "css-font-weight-keywords": "^1.0.0", + "css-global-keywords": "^1.0.1", + "css-system-font-keywords": "^1.0.0", + "pick-by-alias": "^1.2.0", + "string-split-by": "^1.0.0", + "unquote": "^1.1.0" + } + }, + "node_modules/css-font-size-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", + "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", + "license": "MIT" + }, + "node_modules/css-font-stretch-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", + "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", + "license": "MIT" + }, + "node_modules/css-font-style-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", + "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", + "license": "MIT" + }, + "node_modules/css-font-weight-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", + "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", + "license": "MIT" + }, + "node_modules/css-global-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz", + "integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==", + "license": "MIT" + }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-system-font-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", + "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "bin": { + "geo2svg": "bin/geo2svg", + "geograticule": "bin/geograticule", + "geoproject": "bin/geoproject", + "geoquantize": "bin/geoquantize", + "geostitch": "bin/geostitch" + } + }, + "node_modules/d3-geo-projection/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "license": "BSD-3-Clause" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-kerning": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-kerning/-/detect-kerning-2.1.2.tgz", + "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/draw-svg-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/draw-svg-path/-/draw-svg-path-1.0.0.tgz", + "integrity": "sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "~0.1.1", + "normalize-svg-path": "~0.1.0" + } + }, + "node_modules/dtype": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz", + "integrity": "sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/dup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dup/-/dup-1.0.0.tgz", + "integrity": "sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==", + "license": "MIT" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "41.0.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.0.4.tgz", + "integrity": "sha512-rO08CxnAsAkKPFj3OZnxFkKrlnpSL3OCOewMDj5kaohVo++7e8hIT5Sl+tNl9WkNKiLvfZSW180ueA9s5zh9dg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", + "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "cac": "^6.7.14", + "esbuild": "^0.25.11", + "magic-string": "^0.30.19", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/element-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/element-size/-/element-size-1.1.1.tgz", + "integrity": "sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==", + "license": "MIT" + }, + "node_modules/elementary-circuits-directed-graph": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/elementary-circuits-directed-graph/-/elementary-circuits-directed-graph-1.3.1.tgz", + "integrity": "sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==", + "license": "MIT", + "dependencies": { + "strongly-connected-components": "^1.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "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", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "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==", + "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==", + "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/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/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/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/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-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/falafel": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.5.tgz", + "integrity": "sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "isarray": "^2.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/falafel/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.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==", + "license": "MIT" + }, + "node_modules/fast-isnumeric": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-isnumeric/-/fast-isnumeric-1.1.4.tgz", + "integrity": "sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==", + "license": "MIT", + "dependencies": { + "is-string-blank": "^1.0.1" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/flatten-vertex-data": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz", + "integrity": "sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==", + "license": "MIT", + "dependencies": { + "dtype": "^2.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/font-atlas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/font-atlas/-/font-atlas-2.1.0.tgz", + "integrity": "sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==", + "license": "MIT", + "dependencies": { + "css-font": "^1.0.0" + } + }, + "node_modules/font-measure": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/font-measure/-/font-measure-1.2.2.tgz", + "integrity": "sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==", + "license": "MIT", + "dependencies": { + "css-font": "^1.2.0" + } + }, + "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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "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/geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", + "license": "ISC" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-canvas-context": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-canvas-context/-/get-canvas-context-1.0.2.tgz", + "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==", + "license": "MIT" + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/gl-mat4": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz", + "integrity": "sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==", + "license": "Zlib" + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/gl-text": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.4.0.tgz", + "integrity": "sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==", + "license": "MIT", + "dependencies": { + "bit-twiddle": "^1.0.2", + "color-normalize": "^1.5.0", + "css-font": "^1.2.0", + "detect-kerning": "^2.1.2", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "font-atlas": "^2.1.0", + "font-measure": "^1.2.2", + "gl-util": "^3.1.2", + "is-plain-obj": "^1.1.0", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "parse-unit": "^1.0.1", + "pick-by-alias": "^1.2.0", + "regl": "^2.0.0", + "to-px": "^1.0.1", + "typedarray-pool": "^1.1.0" + } + }, + "node_modules/gl-util": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/gl-util/-/gl-util-3.1.3.tgz", + "integrity": "sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1", + "is-firefox": "^1.0.3", + "is-plain-obj": "^1.1.0", + "number-is-integer": "^1.0.1", + "object-assign": "^4.1.0", + "pick-by-alias": "^1.2.0", + "weak-map": "^1.0.5" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "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/glsl-inject-defines": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", + "integrity": "sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==", + "license": "MIT", + "dependencies": { + "glsl-token-inject-block": "^1.0.0", + "glsl-token-string": "^1.0.1", + "glsl-tokenizer": "^2.0.2" + } + }, + "node_modules/glsl-resolve": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/glsl-resolve/-/glsl-resolve-0.0.1.tgz", + "integrity": "sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==", + "license": "MIT", + "dependencies": { + "resolve": "^0.6.1", + "xtend": "^2.1.2" + } + }, + "node_modules/glsl-resolve/node_modules/resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", + "license": "MIT" + }, + "node_modules/glsl-resolve/node_modules/xtend": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", + "integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/glsl-token-assignments": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", + "integrity": "sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==", + "license": "MIT" + }, + "node_modules/glsl-token-defines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", + "integrity": "sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==", + "license": "MIT", + "dependencies": { + "glsl-tokenizer": "^2.0.0" + } + }, + "node_modules/glsl-token-depth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", + "integrity": "sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==", + "license": "MIT" + }, + "node_modules/glsl-token-descope": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", + "integrity": "sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==", + "license": "MIT", + "dependencies": { + "glsl-token-assignments": "^2.0.0", + "glsl-token-depth": "^1.1.0", + "glsl-token-properties": "^1.0.0", + "glsl-token-scope": "^1.1.0" + } + }, + "node_modules/glsl-token-inject-block": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", + "integrity": "sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==", + "license": "MIT" + }, + "node_modules/glsl-token-properties": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", + "integrity": "sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==", + "license": "MIT" + }, + "node_modules/glsl-token-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", + "integrity": "sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==", + "license": "MIT" + }, + "node_modules/glsl-token-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glsl-token-string/-/glsl-token-string-1.0.1.tgz", + "integrity": "sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==", + "license": "MIT" + }, + "node_modules/glsl-token-whitespace-trim": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", + "integrity": "sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz", + "integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==", + "license": "MIT", + "dependencies": { + "through2": "^0.6.3" + } + }, + "node_modules/glsl-tokenizer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/glsl-tokenizer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "license": "MIT", + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/glslify": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", + "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", + "license": "MIT", + "dependencies": { + "bl": "^2.2.1", + "concat-stream": "^1.5.2", + "duplexify": "^3.4.5", + "falafel": "^2.1.0", + "from2": "^2.3.0", + "glsl-resolve": "0.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glslify-bundle": "^5.0.0", + "glslify-deps": "^1.2.5", + "minimist": "^1.2.5", + "resolve": "^1.1.5", + "stack-trace": "0.0.9", + "static-eval": "^2.0.5", + "through2": "^2.0.1", + "xtend": "^4.0.0" + }, + "bin": { + "glslify": "bin.js" + } + }, + "node_modules/glslify-bundle": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-5.1.1.tgz", + "integrity": "sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==", + "license": "MIT", + "dependencies": { + "glsl-inject-defines": "^1.0.1", + "glsl-token-defines": "^1.0.0", + "glsl-token-depth": "^1.1.1", + "glsl-token-descope": "^1.0.2", + "glsl-token-scope": "^1.1.1", + "glsl-token-string": "^1.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glsl-tokenizer": "^2.0.2", + "murmurhash-js": "^1.0.0", + "shallow-copy": "0.0.1" + } + }, + "node_modules/glslify-deps": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", + "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", + "license": "ISC", + "dependencies": { + "@choojs/findup": "^0.2.0", + "events": "^3.2.0", + "glsl-resolve": "0.0.1", + "glsl-tokenizer": "^2.0.0", + "graceful-fs": "^4.1.2", + "inherits": "^2.0.1", + "map-limit": "0.0.1", + "resolve": "^1.0.0" + } + }, + "node_modules/glslify/node_modules/bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/glslify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/glslify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/glslify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/glslify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-hover": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-hover/-/has-hover-1.0.1.tgz", + "integrity": "sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/has-passive-events": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-passive-events/-/has-passive-events-1.0.0.tgz", + "integrity": "sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "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==", + "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==", + "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/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/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==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/hsluv": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hsluv/-/hsluv-0.0.3.tgz", + "integrity": "sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-browser/-/is-browser-2.1.0.tgz", + "integrity": "sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-firefox": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-firefox/-/is-firefox-1.0.3.tgz", + "integrity": "sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-iexplorer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-iexplorer/-/is-iexplorer-1.0.0.tgz", + "integrity": "sha512-YeLzceuwg3K6O0MLM3UyUUjKAlyULetwryFp1mHy1I5PfArK0AEqlfa+MR4gkJjcbuJXoDJCvXbyqZVf5CR2Sg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-mobile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", + "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==", + "license": "MIT" + }, + "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-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-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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-string-blank": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-string-blank/-/is-string-blank-1.0.1.tgz", + "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==", + "license": "MIT" + }, + "node_modules/is-svg-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-svg-path/-/is-svg-path-1.0.2.tgz", + "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", + "license": "MIT" + }, + "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-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "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/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "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/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.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/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, + "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/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "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/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/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/map-limit": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", + "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/mapbox-gl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", + "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-log2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", + "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mouse-change": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", + "integrity": "sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==", + "license": "MIT", + "dependencies": { + "mouse-event": "^1.0.0" + } + }, + "node_modules/mouse-event": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/mouse-event/-/mouse-event-1.0.5.tgz", + "integrity": "sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==", + "license": "MIT" + }, + "node_modules/mouse-event-offset": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", + "integrity": "sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==", + "license": "MIT" + }, + "node_modules/mouse-wheel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mouse-wheel/-/mouse-wheel-1.2.0.tgz", + "integrity": "sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==", + "license": "MIT", + "dependencies": { + "right-now": "^1.0.0", + "signum": "^1.0.0", + "to-px": "^1.0.1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mumath": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/mumath/-/mumath-3.3.4.tgz", + "integrity": "sha512-VAFIOG6rsxoc7q/IaY3jdjmrsuX9f15KlRLYTHmixASBZkZEKC1IFqE2BC5CdhXmK6WLM1Re33z//AGmeRI6FA==", + "deprecated": "Redundant dependency in your project.", + "license": "Unlicense", + "dependencies": { + "almost-equal": "^1.1.0" + } + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, + "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/native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "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-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-svg-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", + "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/number-is-integer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-integer/-/number-is-integer-1.0.1.tgz", + "integrity": "sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==", + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.1" + }, + "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-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.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/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parse-rect/-/parse-rect-1.2.0.tgz", + "integrity": "sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==", + "license": "MIT", + "dependencies": { + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/parse-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-unit/-/parse-unit-1.0.1.tgz", + "integrity": "sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/pick-by-alias": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pick-by-alias/-/pick-by-alias-1.2.0.tgz", + "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==", + "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": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plotly.js": { + "version": "2.35.3", + "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.35.3.tgz", + "integrity": "sha512-7RaC6FxmCUhpD6H4MpD+QLUu3hCn76I11rotRefrh3m1iDvWqGnVqVk9dSaKmRAhFD3vsNsYea0OxnR1rc2IzQ==", + "license": "MIT", + "dependencies": { + "@plotly/d3": "3.8.2", + "@plotly/d3-sankey": "0.7.2", + "@plotly/d3-sankey-circular": "0.33.1", + "@plotly/mapbox-gl": "1.13.4", + "@turf/area": "^7.1.0", + "@turf/bbox": "^7.1.0", + "@turf/centroid": "^7.1.0", + "base64-arraybuffer": "^1.0.2", + "canvas-fit": "^1.5.0", + "color-alpha": "1.0.4", + "color-normalize": "1.5.0", + "color-parse": "2.0.0", + "color-rgba": "2.1.1", + "country-regex": "^1.1.0", + "css-loader": "^7.1.2", + "d3-force": "^1.2.1", + "d3-format": "^1.4.5", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", + "d3-hierarchy": "^1.1.9", + "d3-interpolate": "^3.0.1", + "d3-time": "^1.1.0", + "d3-time-format": "^2.2.3", + "fast-isnumeric": "^1.1.4", + "gl-mat4": "^1.2.0", + "gl-text": "^1.4.0", + "has-hover": "^1.0.1", + "has-passive-events": "^1.0.0", + "is-mobile": "^4.0.0", + "maplibre-gl": "^4.5.2", + "mouse-change": "^1.4.0", + "mouse-event-offset": "^3.0.2", + "mouse-wheel": "^1.2.0", + "native-promise-only": "^0.8.1", + "parse-svg-path": "^0.1.2", + "point-in-polygon": "^1.1.0", + "polybooljs": "^1.2.2", + "probe-image-size": "^7.2.3", + "regl": "npm:@plotly/regl@^2.1.2", + "regl-error2d": "^2.0.12", + "regl-line2d": "^3.1.3", + "regl-scatter2d": "^3.3.1", + "regl-splom": "^1.0.14", + "strongly-connected-components": "^1.0.1", + "style-loader": "^4.0.0", + "superscript-text": "^1.0.0", + "svg-path-sdf": "^1.1.3", + "tinycolor2": "^1.4.2", + "to-px": "1.0.1", + "topojson-client": "^3.1.0", + "webgl-context": "^2.2.0", + "world-calendars": "^1.0.3" + } + }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, + "node_modules/polybooljs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/polybooljs/-/polybooljs-1.2.2.tgz", + "integrity": "sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==", + "license": "MIT" + }, + "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.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "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/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "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/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/probe-image-size": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", + "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", + "license": "MIT", + "dependencies": { + "lodash.merge": "^4.6.2", + "needle": "^2.5.2", + "stream-parser": "~0.3.1" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/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==", + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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==", + "dev": true, + "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/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" + }, + "node_modules/react-plotly.js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz", + "integrity": "sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "plotly.js": ">1.34.0", + "react": ">0.13.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/regl": { + "name": "@plotly/regl", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@plotly/regl/-/regl-2.1.2.tgz", + "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", + "license": "MIT" + }, + "node_modules/regl-error2d": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/regl-error2d/-/regl-error2d-2.0.12.tgz", + "integrity": "sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-line2d": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.1.3.tgz", + "integrity": "sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "array-find-index": "^1.0.2", + "array-normalize": "^1.1.4", + "color-normalize": "^1.5.0", + "earcut": "^2.1.5", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0" + } + }, + "node_modules/regl-scatter2d": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.3.1.tgz", + "integrity": "sha512-seOmMIVwaCwemSYz/y4WE0dbSO9svNFSqtTh5RE57I7PjGo3tcUYKtH0MTSoshcAsreoqN8HoCtnn8wfHXXfKQ==", + "license": "MIT", + "dependencies": { + "@plotly/point-cluster": "^3.1.9", + "array-range": "^1.0.1", + "array-rearrange": "^2.2.2", + "clamp": "^1.0.1", + "color-id": "^1.1.0", + "color-normalize": "^1.5.0", + "color-rgba": "^2.1.1", + "flatten-vertex-data": "^1.0.2", + "glslify": "^7.0.0", + "is-iexplorer": "^1.0.0", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-splom": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.14.tgz", + "integrity": "sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "array-range": "^1.0.1", + "color-alpha": "^1.0.4", + "flatten-vertex-data": "^1.0.2", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "raf": "^3.4.1", + "regl-scatter2d": "^3.2.3" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "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-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/right-now": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz", + "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "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==", + "dev": true, + "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/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "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-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "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/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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/signum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/signum/-/signum-1.0.0.tgz", + "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", + "engines": { + "node": "*" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/static-eval": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", + "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", + "license": "MIT", + "dependencies": { + "escodegen": "^2.1.0" + } + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "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/stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", + "license": "MIT", + "dependencies": { + "debug": "2" + } + }, + "node_modules/stream-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stream-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "license": "MIT", + "dependencies": { + "parenthesis": "^3.1.5" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/strongly-connected-components": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strongly-connected-components/-/strongly-connected-components-1.0.1.tgz", + "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", + "license": "MIT" + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "license": "ISC", + "dependencies": { + "kdbush": "^3.0.0" + } + }, + "node_modules/supercluster/node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", + "license": "ISC" + }, + "node_modules/superscript-text": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/superscript-text/-/superscript-text-1.0.0.tgz", + "integrity": "sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==", + "license": "MIT" + }, + "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/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, + "node_modules/svg-path-bounds": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", + "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "^0.1.1", + "is-svg-path": "^1.0.1", + "normalize-svg-path": "^1.0.0", + "parse-svg-path": "^0.1.2" + } + }, + "node_modules/svg-path-bounds/node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "node_modules/svg-path-sdf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/svg-path-sdf/-/svg-path-sdf-1.1.3.tgz", + "integrity": "sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==", + "license": "MIT", + "dependencies": { + "bitmap-sdf": "^1.0.0", + "draw-svg-path": "^1.0.0", + "is-svg-path": "^1.0.1", + "parse-svg-path": "^0.1.2", + "svg-path-bounds": "^1.0.1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "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/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "license": "ISC" + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-float32": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-float32/-/to-float32-1.1.0.tgz", + "integrity": "sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==", + "license": "MIT" + }, + "node_modules/to-px": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz", + "integrity": "sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==", + "license": "MIT", + "dependencies": { + "parse-unit": "^1.0.1" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/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": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "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/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typedarray-pool": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typedarray-pool/-/typedarray-pool-1.2.0.tgz", + "integrity": "sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==", + "license": "MIT", + "dependencies": { + "bit-twiddle": "^1.0.0", + "dup": "^1.0.0" + } + }, + "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/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": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", + "integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "license": "MIT" + }, + "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==", + "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/update-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-diff/-/update-diff-1.1.0.tgz", + "integrity": "sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==", + "license": "MIT" + }, + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-map": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", + "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==", + "license": "Apache-2.0" + }, + "node_modules/webgl-context": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webgl-context/-/webgl-context-2.2.0.tgz", + "integrity": "sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==", + "license": "MIT", + "dependencies": { + "get-canvas-context": "^1.0.1" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/world-calendars": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.4.tgz", + "integrity": "sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "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/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0e4a141 --- /dev/null +++ b/package.json @@ -0,0 +1,183 @@ +{ + "name": "stingray-explorer", + "version": "2.0.0", + "description": "X-ray Timing Analysis Desktop Application - Next Generation Spectral Timing Made Easy", + "type": "module", + "main": "dist-electron/main.js", + "author": "Kartik Mandar ", + "license": "MIT", + "homepage": "https://github.com/kartikmandar-GSOC24/StingrayExplorer", + "repository": { + "type": "git", + "url": "https://github.com/kartikmandar-GSOC24/StingrayExplorer.git" + }, + "scripts": { + "dev": "electron-vite dev", + "dev:linux": "ELECTRON_DISABLE_SANDBOX=1 electron-vite dev", + "build": "electron-vite build", + "preview": "electron-vite preview", + "package": "npm run build && electron-builder", + "package:mac": "npm run build && electron-builder --mac", + "package:win": "npm run build && electron-builder --win", + "package:linux": "npm run build && electron-builder --linux", + "package:all": "npm run build && electron-builder -mwl", + "python:install": "cd python-backend && pip install -r requirements.txt", + "python:dev": "cd python-backend && python main.py 8765", + "python:build": "cd python-backend && pyinstaller --onefile --name stingray-backend main.py", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:coverage": "vitest --coverage" + }, + "build": { + "appId": "com.stingray.explorer", + "productName": "Stingray Explorer", + "copyright": "Copyright (c) 2024 Kartik Mandar", + "directories": { + "output": "release", + "buildResources": "resources" + }, + "files": [ + "dist/**/*", + "dist-electron/**/*" + ], + "extraResources": [ + { + "from": "python-backend/dist/", + "to": "python-backend", + "filter": [ + "**/*" + ] + }, + { + "from": "files/", + "to": "files", + "filter": [ + "**/*" + ] + }, + { + "from": "resources/icon.png", + "to": "icon.png" + } + ], + "mac": { + "target": [ + { + "target": "dmg", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "zip", + "arch": [ + "x64", + "arm64" + ] + } + ], + "icon": "resources/icon.png", + "category": "public.app-category.developer-tools", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.plist" + }, + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } + ], + "icon": "resources/icon.ico", + "publisherName": "Kartik Mandar" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": [ + "x64" + ] + }, + { + "target": "deb", + "arch": [ + "x64" + ] + }, + { + "target": "rpm", + "arch": [ + "x64" + ] + } + ], + "icon": "resources/icon.png", + "category": "Science", + "maintainer": "kartik4321mandar@gmail.com" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "installerIcon": "resources/icon.ico", + "uninstallerIcon": "resources/icon.ico", + "installerHeaderIcon": "resources/icon.ico" + } + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/ibm-plex-sans": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@mui/icons-material": "^5.15.6", + "@mui/material": "^5.15.6", + "@tanstack/react-query": "^5.17.19", + "axios": "^1.6.7", + "plotly.js": "^2.29.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-plotly.js": "^2.6.0", + "react-router-dom": "^6.22.0", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^20.11.16", + "@types/plotly.js": "^2.35.14", + "@types/react": "^18.2.52", + "@types/react-dom": "^18.2.18", + "@types/react-plotly.js": "^2.6.3", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", + "@vitejs/plugin-react": "^5.2.0", + "electron": "^41.0.4", + "electron-builder": "^26.8.1", + "electron-vite": "^5.0.0", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jsdom": "^29.1.1", + "typescript": "^5.3.3", + "vite": "^7.3.1", + "vitest": "^4.1.2" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..44aedcb --- /dev/null +++ b/pixi.lock @@ -0,0 +1,13080 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.14-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/astropy-base-7.2.0-py314hc02f841_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.4-h8824e59_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h20b40b1_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.1-h3a458e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.2-h3a5f585_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.15.0-h2a74896_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.11.0-h3d7a050_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.13.0-hf38f1be_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bottleneck-1.6.0-np2py314h56abb78_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.22.0-hc31b594_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py314h9891dd4_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-46.0.4-py314h7fe84b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hd9c7081_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.18-py314h42812f9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.3.1-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.15.1-nompi_py314hc32fe06_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-12.2.0-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h1b119a7_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.7.1-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyha804496_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py314h97ea11e_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-22.0.0-hb6ed5f4_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-22.0.0-h635bf11_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-22.0.0-h8c2c5c3_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-22.0.0-h635bf11_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-22.0.0-h3f74fd7_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-4_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-4_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp21.1-21.1.7-default_h99862b1_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.7-default_h746c552_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.2-h32235b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-hdb79228_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.73.1-h3288cfb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-4_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.7-hf7376ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-hb9b0907_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-22.0.0-h7376487_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.53-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.1-h5c52fec_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.2-hfe17d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-h5347b49_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.328.1-h5279c79_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.1-ha9997c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.1-h26afc86_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.46.0-py314h946fb2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py314hae3bed6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.10.8-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.8-py314h1194b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.63.1-py314h8169c2f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.1-py314heb044ea_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py314h2b28147_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.10-he970967_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.1-hd747db4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py314ha0b5721_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.0.0-py314h8ec4b1a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.3-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-22.0.0-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-22.0.0-py314h52d6ec5_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.41.5-py314h2e6c369_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyerfa-2.0.1.5-py310h32771cd_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.9.3-py314hf36963e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pytables-3.10.2-py314h5611b9a_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.2-h32b2ec7_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hfb55c3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.9.3-h5c1c036_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.4.1-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.3-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.0-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.1.1-py314ha5689aa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py314h31f8a6b_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.3-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.1-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.2-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/astropy-base-7.2.0-py314hd1ec8a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.9.3-hdff831d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.13-hea39f9f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.12.6-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.1-h901532c_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.5.7-ha05da6a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.10.7-h924c446_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.23.3-hf559bb5_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.13.3-ha72ff4e_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.11.3-he30762a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-h901532c_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.7-h901532c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.35.2-h7484968_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.606-hffd60a0_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.1-he2a98a9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.2-h0e8e1c8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.15.0-h388f2e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.11.0-h56a711b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.13.0-h1984e67_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/blosc-1.21.6-hd145fbb_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bottleneck-1.6.0-np2py314hfeef9c2_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py314h3262eb8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-blosc2-2.22.0-hedb7e5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.3.3-py314h00ed6fe_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.18-py314h3658963_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.1-h694c41f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/h5py-3.15.1-nompi_py314hf613b1f_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc1508a4_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.7.1-py314h6482030_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh534df25_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.9-py314hf3ac25a_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.17-h72f5680_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hcca01a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libaec-1.1.4-ha6bc127_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-22.0.0-hd1700fa_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-22.0.0-h2db2d7d_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-22.0.0-h7751554_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-22.0.0-h2db2d7d_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-22.0.0-h4653b8a_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-4_he492b99_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-4_h9b27e0a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.17.0-h7dd4100_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.7-h3d58e20_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.1-h694c41f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.1-h6912278_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-2.39.0-hed66dea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-2.39.0-h8ac052b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.73.1-h451496d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.2-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-4_h859234e_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.21.0-h7d3f41d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.21.0-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-22.0.0-habb56ca_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.53-h380d223_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h03562ea_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h554ac88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-h6cc646a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-h687e942_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.2-h7983711_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.1-ha1d9b0f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.1-h7b7ecba_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.7-h472b3d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.46.0-py314h85c3bf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.0.2-py314h787f955_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.10.8-py314hee6578b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.10.8-py314hd47142c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h53ec75d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.63.0-py314h385e359_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numexpr-2.14.1-py314h205861b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.3.5-py314hf08249b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.4-h87e8dc5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.2.1-hd1b02dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-2.3.3-py314hc4308db_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-12.0.0-py314hedf0282_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.3-py314hd1e8ddb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-22.0.0-py314hee6578b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-22.0.0-py314h35e0213_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.41.5-py314ha7b6dee_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyerfa-2.0.1.5-py310hcbffc5d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pytables-3.10.2-py314hb51f073_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.2-hf88997e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312hb7d603e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2025.11.05-h7df6414_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.16.3-py314h9d854bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.3-py314h6482030_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.0-py314h6482030_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uvloop-0.22.1-py314h6482030_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/watchfiles-1.1.1-py314hc9c287a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-15.0.1-py314hcfd16f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-1.17.3-py314h03d016b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.12-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.5-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h6c33b1e_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.1-hd23fc13_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.2-h53ec75d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/astropy-base-7.2.0-py314hdcf55e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.9.3-h1ddaa69_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.13-h6ee9776_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.12.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.1-h16f91aa_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.5.7-h9ae9c55_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.10.7-h5928ca5_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.23.3-hbe03c90_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.13.3-haf5c5c8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.11.3-h8da9771_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h16f91aa_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.7-h16f91aa_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.35.4-h74951b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.606-h4e1b0f7_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.1-h88fedcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.2-h853621b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.15.0-h10d327b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.11.0-h7e4aa5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.13.0-hb288d13_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bottleneck-1.6.0-np2py314hfa18b03_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-blosc2-2.22.0-hb83781b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py314h784bc60_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.18-py314hf820bb6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.15.1-nompi_py314h1c8d760_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_hd3baa01_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.7.1-py314h0612a62_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh534df25_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py314h42813c9_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-22.0.0-he6e817a_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-22.0.0-hc317990_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-22.0.0-h75845d1_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-22.0.0-hc317990_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-22.0.0-h144af7f_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-4_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-4_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-2.39.0-head0a95_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-2.39.0-hfa3a374_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.73.1-h3063b79_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-4_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.21.0-he15edb5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.21.0-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-22.0.0-h0ac143b_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.53-hfab5511_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h658db43_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h91c62da_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h9a5124b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h14a376c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.2-hd2415e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.1-h9329255_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.7-h4a912ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.46.0-py314ha398f32_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py314he05ef12_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-3.10.8-py314he55896b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.10.8-py314hd63e3f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h248ca61_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.63.1-py314h945de62_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numexpr-2.14.1-py314hc5bb990_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.5-py314h5b5928d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.2.1-h4fd0076_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.3-py314ha3d490a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.0.0-py314h57fbdfe_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.3-py314h9d33bd4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-22.0.0-py314he55896b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-22.0.0-py314hf20a12a_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.41.5-py314haad56a0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyerfa-2.0.1.5-py310hbb12772_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pytables-3.10.2-py314h8eb144a_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.2-h40d2674_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312hd65ceae_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-h64b956e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.3-py314h624bdf2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.3-py314h0612a62_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.0-py314h0612a62_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uvloop-0.22.1-py314h0612a62_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchfiles-1.1.1-py314h8d4a433_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-15.0.1-py314hf17b0b1_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.3-py314hb84d1df_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.2-h248ca61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/astropy-base-7.2.0-py314h2dcd201_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.9.3-h2970c50_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.9.13-h46f3b43_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.12.6-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.3.1-hcb3a2da_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.5.7-ha388e84_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.10.7-hc678f4a_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.23.3-h0d5b9f9_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.13.3-hfa314fa_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.11.3-ha659bf3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.2.4-hcb3a2da_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.2.7-hcb3a2da_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.35.4-hca034e6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.606-hac16450_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bottleneck-1.6.0-np2py314hea88fa1_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/c-ares-1.34.6-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/c-blosc2-2.22.0-h2af8807_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyha7b4d00_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py314h909e829_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.18-py314hb98de8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/double-conversion-3.3.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.15.1-nompi_py314hc249e69_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-12.2.0-h5f2951f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_h89f0904_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/httptools-0.7.1-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py314hf309875_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250512.1-cxx17_habfad5f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-22.0.0-h89d7da9_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-22.0.0-h7d8d6a5_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-compute-22.0.0-h2db994a_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-22.0.0-h7d8d6a5_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-substrait-22.0.0-hf865cc0_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-4_hf2e6a31_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-4_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-21.1.7-default_ha2db4b5_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcrc32c-1.1.2-h0e60522_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libevent-2.1.12-h3671451_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.2-hd9c3897_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-2.39.0-h19ee442_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-storage-2.39.0-he04ea4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgrpc-1.73.1-h317e13b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.2-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-4_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libparquet-22.0.0-h7051d1f_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.53-h7351971_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.31.1-hdcda5b4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libre2-11-2025.11.05-h0eb2380_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libthrift-0.22.0-h23985f6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h8f73337_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libutf8proc-2.11.2-hb980946_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.328.1-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h06f855e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-ha29bfb0_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.7-h4fa8253_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvmlite-0.46.0-py314hb492ee6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py314hcdb55d9_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.10.8-py314h86ab7b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.8-py314hfa45d96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_454.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numba-0.63.1-py314h36f8cf2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numexpr-2.14.1-mkl_py314h220b711_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.5-py314h06c3c77_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.2.1-h7414dfc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.3-py314hd8fd7ce_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-12.0.0-py314h61b30b5_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.3-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-22.0.0-py314h86ab7b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-core-22.0.0-py314hb5be3fa_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.41.5-py314h9f07db2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyerfa-2.0.1.5-py310h1f63838_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyside6-6.9.3-py314h2c9462b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pytables-3.10.2-py314h2bd12ea_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.2-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-ctypes-0.2.3-py314h86ab7b2_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312hbb5da91_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qt6-main-6.9.3-ha0de62e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/re2-2025.11.05-ha104f34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.3-py314h5798d8a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.3-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/unicodedata2-17.0.0-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/watchfiles-1.1.1-py314h170c82c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/websockets-15.0.1-py314h4667ab5_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.3-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-hba3369d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-hba3369d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.2-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + dev: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.14-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/astropy-base-7.2.0-py314hc02f841_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.4-h8824e59_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h20b40b1_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.1-h3a458e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.2-h3a5f585_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.15.0-h2a74896_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.11.0-h3d7a050_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.13.0-hf38f1be_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyh5ded981_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/black-25.12.0-pyh866005b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bottleneck-1.6.0-np2py314h56abb78_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.22.0-hc31b594_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py314h9891dd4_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-46.0.4-py314h7fe84b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hd9c7081_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.18-py314h42812f9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.3.1-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.15.1-nompi_py314hc32fe06_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-12.2.0-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h1b119a7_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.7.1-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyha804496_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py314h97ea11e_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-22.0.0-hb6ed5f4_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-22.0.0-h635bf11_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-22.0.0-h8c2c5c3_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-22.0.0-h635bf11_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-22.0.0-h3f74fd7_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-4_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-4_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp21.1-21.1.7-default_h99862b1_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.7-default_h746c552_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.2-h32235b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-hdb79228_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.73.1-h3288cfb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-4_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.7-hf7376ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-hb9b0907_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-22.0.0-h7376487_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.53-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.1-h5c52fec_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.2-hfe17d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-h5347b49_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.328.1-h5279c79_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.1-ha9997c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.1-h26afc86_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.46.0-py314h946fb2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py314hae3bed6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.10.8-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.8-py314h1194b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.63.1-py314h8169c2f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.1-py314heb044ea_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py314h2b28147_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.10-he970967_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.1-hd747db4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py314ha0b5721_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.0.0-py314h8ec4b1a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.3-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-22.0.0-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-22.0.0-py314h52d6ec5_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.41.5-py314h2e6c369_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyerfa-2.0.1.5-py310h32771cd_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.9.3-py314hf36963e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pytables-3.10.2-py314h5611b9a_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.2-h32b2ec7_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytokens-0.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hfb55c3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.9.3-h5c1c036_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.14.8-h813ae00_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.4.1-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.3-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.0-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.1.1-py314ha5689aa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py314h31f8a6b_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.3-py314h5bd0f2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.1-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.2-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/astropy-base-7.2.0-py314hd1ec8a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.9.3-hdff831d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.13-hea39f9f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.12.6-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.1-h901532c_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.5.7-ha05da6a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.10.7-h924c446_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.23.3-hf559bb5_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.13.3-ha72ff4e_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.11.3-he30762a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-h901532c_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.7-h901532c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.35.2-h7484968_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.606-hffd60a0_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.1-he2a98a9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.2-h0e8e1c8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.15.0-h388f2e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.11.0-h56a711b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.13.0-h1984e67_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyh5ded981_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/black-25.12.0-pyh866005b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/blosc-1.21.6-hd145fbb_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bottleneck-1.6.0-np2py314hfeef9c2_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py314h3262eb8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-blosc2-2.22.0-hedb7e5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.3.3-py314h00ed6fe_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.18-py314h3658963_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.1-h694c41f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/h5py-3.15.1-nompi_py314hf613b1f_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc1508a4_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.7.1-py314h6482030_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh534df25_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.9-py314hf3ac25a_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.17-h72f5680_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hcca01a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libaec-1.1.4-ha6bc127_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-22.0.0-hd1700fa_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-22.0.0-h2db2d7d_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-22.0.0-h7751554_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-22.0.0-h2db2d7d_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-22.0.0-h4653b8a_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-4_he492b99_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-4_h9b27e0a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.17.0-h7dd4100_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.7-h3d58e20_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.1-h694c41f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.1-h6912278_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-2.39.0-hed66dea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-2.39.0-h8ac052b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.73.1-h451496d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.2-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-4_h859234e_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.21.0-h7d3f41d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.21.0-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-22.0.0-habb56ca_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.53-h380d223_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h03562ea_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h554ac88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-h6cc646a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-h687e942_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.2-h7983711_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.1-ha1d9b0f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.1-h7b7ecba_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.7-h472b3d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.46.0-py314h85c3bf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.0.2-py314h787f955_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.10.8-py314hee6578b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.10.8-py314hd47142c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h53ec75d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.63.0-py314h385e359_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numexpr-2.14.1-py314h205861b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.3.5-py314hf08249b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.4-h87e8dc5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.2.1-hd1b02dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-2.3.3-py314hc4308db_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-12.0.0-py314hedf0282_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.3-py314hd1e8ddb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-22.0.0-py314hee6578b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-22.0.0-py314h35e0213_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.41.5-py314ha7b6dee_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyerfa-2.0.1.5-py310hcbffc5d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pytables-3.10.2-py314hb51f073_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.2-hf88997e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytokens-0.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312hb7d603e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2025.11.05-h7df6414_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.14.8-hd9f4cfa_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.16.3-py314h9d854bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.3-py314h6482030_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.0-py314h6482030_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uvloop-0.22.1-py314h6482030_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/watchfiles-1.1.1-py314hc9c287a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-15.0.1-py314hcfd16f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-1.17.3-py314h03d016b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.12-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.5-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h6c33b1e_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.1-hd23fc13_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.2-h53ec75d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/astropy-base-7.2.0-py314hdcf55e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.9.3-h1ddaa69_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.13-h6ee9776_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.12.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.1-h16f91aa_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.5.7-h9ae9c55_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.10.7-h5928ca5_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.23.3-hbe03c90_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.13.3-haf5c5c8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.11.3-h8da9771_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h16f91aa_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.7-h16f91aa_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.35.4-h74951b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.606-h4e1b0f7_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.1-h88fedcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.2-h853621b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.15.0-h10d327b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.11.0-h7e4aa5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.13.0-hb288d13_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyh5ded981_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/black-25.12.0-pyh866005b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bottleneck-1.6.0-np2py314hfa18b03_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-blosc2-2.22.0-hb83781b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py314h784bc60_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.18-py314hf820bb6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.15.1-nompi_py314h1c8d760_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_hd3baa01_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.7.1-py314h0612a62_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh534df25_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py314h42813c9_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-22.0.0-he6e817a_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-22.0.0-hc317990_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-22.0.0-h75845d1_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-22.0.0-hc317990_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-22.0.0-h144af7f_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-4_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-4_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-2.39.0-head0a95_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-2.39.0-hfa3a374_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.73.1-h3063b79_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-4_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.21.0-he15edb5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.21.0-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-22.0.0-h0ac143b_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.53-hfab5511_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h658db43_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h91c62da_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h9a5124b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h14a376c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.2-hd2415e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.1-h9329255_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.7-h4a912ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.46.0-py314ha398f32_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py314he05ef12_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-3.10.8-py314he55896b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.10.8-py314hd63e3f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h248ca61_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.63.1-py314h945de62_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numexpr-2.14.1-py314hc5bb990_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.5-py314h5b5928d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.2.1-h4fd0076_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.3-py314ha3d490a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.0.0-py314h57fbdfe_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.3-py314h9d33bd4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-22.0.0-py314he55896b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-22.0.0-py314hf20a12a_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.41.5-py314haad56a0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyerfa-2.0.1.5-py310hbb12772_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pytables-3.10.2-py314h8eb144a_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.2-h40d2674_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytokens-0.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312hd65ceae_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-h64b956e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.14.8-h382de68_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.3-py314h624bdf2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.3-py314h0612a62_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.0-py314h0612a62_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uvloop-0.22.1-py314h0612a62_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchfiles-1.1.1-py314h8d4a433_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-15.0.1-py314hf17b0b1_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.3-py314hb84d1df_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.2-h248ca61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/astropy-base-7.2.0-py314h2dcd201_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.9.3-h2970c50_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.9.13-h46f3b43_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.12.6-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.3.1-hcb3a2da_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.5.7-ha388e84_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.10.7-hc678f4a_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.23.3-h0d5b9f9_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.13.3-hfa314fa_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.11.3-ha659bf3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.2.4-hcb3a2da_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.2.7-hcb3a2da_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.35.4-hca034e6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.606-hac16450_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyh5ded981_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/black-25.12.0-pyh866005b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bottleneck-1.6.0-np2py314hea88fa1_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/c-ares-1.34.6-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/c-blosc2-2.22.0-h2af8807_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyha7b4d00_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py314h909e829_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.18-py314hb98de8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/double-conversion-3.3.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.15.1-nompi_py314hc249e69_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-12.2.0-h5f2951f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_h89f0904_104.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/httptools-0.7.1-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py314hf309875_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250512.1-cxx17_habfad5f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-22.0.0-h89d7da9_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-22.0.0-h7d8d6a5_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-compute-22.0.0-h2db994a_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-22.0.0-h7d8d6a5_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-substrait-22.0.0-hf865cc0_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-4_hf2e6a31_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-4_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-21.1.7-default_ha2db4b5_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcrc32c-1.1.2-h0e60522_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libevent-2.1.12-h3671451_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.2-hd9c3897_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-2.39.0-h19ee442_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-storage-2.39.0-he04ea4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgrpc-1.73.1-h317e13b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.2-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-4_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libparquet-22.0.0-h7051d1f_6_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.53-h7351971_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.31.1-hdcda5b4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libre2-11-2025.11.05-h0eb2380_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libthrift-0.22.0-h23985f6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h8f73337_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libutf8proc-2.11.2-hb980946_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.328.1-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h06f855e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-ha29bfb0_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.7-h4fa8253_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvmlite-0.46.0-py314hb492ee6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py314hcdb55d9_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.10.8-py314h86ab7b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.8-py314hfa45d96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_454.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numba-0.63.1-py314h36f8cf2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numexpr-2.14.1-mkl_py314h220b711_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.5-py314h06c3c77_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.2.1-h7414dfc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.3-py314hd8fd7ce_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-12.0.0-py314h61b30b5_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.3-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-22.0.0-py314h86ab7b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-core-22.0.0-py314hb5be3fa_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.41.5-py314h9f07db2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyerfa-2.0.1.5-py310h1f63838_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyside6-6.9.3-py314h2c9462b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pytables-3.10.2-py314h2bd12ea_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.2-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytokens-0.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-ctypes-0.2.3-py314h86ab7b2_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312hbb5da91_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qt6-main-6.9.3-ha0de62e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/re2-2025.11.05-ha104f34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.14.8-h15e3a1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.3-py314h5798d8a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.3-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/unicodedata2-17.0.0-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_33.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/watchfiles-1.1.1-py314h170c82c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/websockets-15.0.1-py314h4667ab5_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.3-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-hba3369d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-hba3369d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.2-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda + build_number: 7 + sha256: 30006902a9274de8abdad5a9f02ef7c8bb3d69a503486af0c1faee30b023e5b7 + md5: eaac87c21aff3ed21ad9656697bb8326 + depends: + - llvm-openmp >=9.0.1 + license: BSD-3-Clause + license_family: BSD + size: 8328 + timestamp: 1764092562779 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + build_number: 7 + sha256: 7acaa2e0782cad032bdaf756b536874346ac1375745fb250e9bdd6a48a7ab3cd + md5: a44032f282e7d2acdeb1c240308052dd + depends: + - llvm-openmp >=9.0.1 + license: BSD-3-Clause + license_family: BSD + size: 8325 + timestamp: 1764092507920 +- conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + build_number: 8 + sha256: 1a62cd1f215fe0902e7004089693a78347a30ad687781dfda2289cab000e652d + md5: 37e16618af5c4851a3f3d66dd0e11141 + depends: + - libgomp >=7.5.0 + - libwinpthread >=12.0.0.r2.ggc561118da + constrains: + - openmp_impl 9999 + - msys2-conda-epoch <0.0a0 + license: BSD-3-Clause + license_family: BSD + size: 49468 + timestamp: 1718213032772 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + size: 8191 + timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + sha256: 922fb146148449c6bc374a37fa9edb89b3af1c385392391339206d3ab3d571a3 + md5: 6e90a60dbb939d24a6295e19377cf0e6 + depends: + - python >=3.10 + - aiohttp >=3.9.2,<4.0.0 + - aioitertools >=0.5.1,<1.0.0 + - botocore >=1.40.46,<1.40.71 + - python-dateutil >=2.1,<3.0.0 + - jmespath >=0.7.1,<2.0.0 + - multidict >=6.0.0,<7.0.0 + - wrapt >=1.10.10,<2.0.0 + - python + license: Apache-2.0 + license_family: APACHE + size: 80900 + timestamp: 1762893423043 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhd8ed1ab_0.conda + sha256: 1d0dcbeaab76d87aa9f9fb07ec9ba07d30f0386019328aaa11a578266f324aaf + md5: 9b7781a926808f424434003f728ea7ab + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + size: 19145 + timestamp: 1760127109813 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda + sha256: 7842ddc678e77868ba7b92a726b437575b23aaec293bca0d40826f1026d90e27 + md5: 18fd895e0e775622906cdabfc3cf0fb4 + depends: + - python >=3.9 + license: PSF-2.0 + license_family: PSF + size: 19750 + timestamp: 1741775303303 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiohttp-3.13.2-pyh4ca1811_0.conda + sha256: 8af88a6daa5e30f347da7faee1ee17d920a1090c0e921431bf43adff02429b50 + md5: 9b7efc1b9351892fc1b0af3fb7e44280 + depends: + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - async-timeout >=4.0,<6.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.10 + - yarl >=1.17.0,<2.0 + track_features: + - aiohttp_no_compile + license: MIT AND Apache-2.0 + license_family: Apache + size: 474272 + timestamp: 1761726660058 +- conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.12.0-pyhd8ed1ab_1.conda + sha256: 7d56e547a819a03c058dd8793ca9df6ff9825812da52c214192edb61a7de1c95 + md5: 3eb47adbffac44483f59e580f8600a1e + depends: + - python >=3.9 + - typing_extensions >=4.0 + license: MIT + license_family: MIT + size: 25063 + timestamp: 1735329177103 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + sha256: 8dc149a6828d19bf104ea96382a9d04dae185d4a03cc6beb1bc7b84c428e3ca2 + md5: 421a865222cd0c9d83ff08bc78bf3a61 + depends: + - frozenlist >=1.1.0 + - python >=3.9 + - typing_extensions >=4.2 + license: Apache-2.0 + license_family: APACHE + size: 13688 + timestamp: 1751626573984 +- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.14-hb9d3cd8_0.conda + sha256: b9214bc17e89bf2b691fad50d952b7f029f6148f4ac4fe7c60c08f093efdf745 + md5: 76df83c2a9035c54df5d04ff81bcc02d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + license_family: GPL + size: 566531 + timestamp: 1744668655747 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda + sha256: cc9fbc50d4ee7ee04e49ee119243e6f1765750f0fd0b4d270d5ef35461b643b1 + md5: 52be5139047efadaeeb19c6a5103f92a + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 14222 + timestamp: 1762868213144 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 + md5: 2934f256a8acfe48f6ebb4fce6cde29c + depends: + - python >=3.9 + - typing-extensions >=4.0.0 + license: MIT + license_family: MIT + size: 18074 + timestamp: 1733247158254 +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda + sha256: 830fc81970cd9d19869909b9b16d241f4d557e4f201a1030aa6ed87c6aa8b930 + md5: 9958d4a1ee7e9c768fe8f4fb51bd07ea + depends: + - exceptiongroup >=1.0.2 + - idna >=2.8 + - python >=3.10 + - typing_extensions >=4.5 + - python + constrains: + - trio >=0.32.0 + - uvloop >=0.21 + license: MIT + license_family: MIT + size: 144702 + timestamp: 1764375386926 +- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf + md5: 54898d0f524c9dee622d44bbb081a8ab + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + size: 10076 + timestamp: 1733332433806 +- conda: https://conda.anaconda.org/conda-forge/noarch/astropy-7.2.0-pyhd8ed1ab_0.conda + sha256: 41ca79f6c5c6e5dd2ebbc96dae8420c914161b261d437d3de7dce0071de55318 + md5: 8f873669e83f0d7fcbaf0b221f6963d5 + depends: + - aiohttp + - astropy-base >=7.2.0,<7.2.1.0a0 + - beautifulsoup4 >=4.9.3 + - bleach >=3.2.1 + - bottleneck >=1.3.3 + - certifi >=2022.6.15.1 + - dask-core >=2024.8.0 + - fsspec >=2023.4.0 + - h5py >=3.9.0 + - html5lib >=1.1 + - ipydatagrid >=1.1.13 + - ipykernel >=6.16.0 + - ipython >=8.0.0 + - ipywidgets >=7.7.3 + - jplephem >=2.17.0 + - matplotlib-base >=3.8.0 + - mpmath >=1.2.1 + - narwhals >=1.42.0 + - pandas >=2.0 + - pyarrow >=14.0.2 + - python >=3.11 + - pytz >=2016.10 + - s3fs >=2023.4.0 + - scipy >=1.9.2 + - sortedcontainers >=2.1.0 + - uncompresspy >=0.4.0 + license: BSD-3-Clause + license_family: BSD + size: 9060 + timestamp: 1764120734938 +- conda: https://conda.anaconda.org/conda-forge/linux-64/astropy-base-7.2.0-py314hc02f841_0.conda + sha256: c430dec69f18dd013fb8d7a930cb1f4b3781ba0d150bb18dc3a221eaeca57ca8 + md5: 8c7d00a0b82d5399c242be5c80a5a0d4 + depends: + - __glibc >=2.17,<3.0.a0 + - astropy-iers-data >=0.2025.10.27.0.39.10 + - libgcc >=14 + - numpy >=1.23,<3 + - numpy >=1.24 + - packaging >=22.0.0 + - pyerfa >=2.0.1.1 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - pyyaml >=6.0.0 + constrains: + - astropy >=7.0.0 + license: BSD-3-Clause + license_family: BSD + size: 9880758 + timestamp: 1764120697264 +- conda: https://conda.anaconda.org/conda-forge/osx-64/astropy-base-7.2.0-py314hd1ec8a2_0.conda + sha256: b8baffed419ab2cd755d1ec84ba770fae31bb48d978e409847c57d4b84cd122d + md5: f227079dec8a611007e0ab0e6776600b + depends: + - __osx >=10.13 + - astropy-iers-data >=0.2025.10.27.0.39.10 + - numpy >=1.23,<3 + - numpy >=1.24 + - packaging >=22.0.0 + - pyerfa >=2.0.1.1 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - pyyaml >=6.0.0 + constrains: + - astropy >=7.0.0 + license: BSD-3-Clause + license_family: BSD + size: 9632932 + timestamp: 1764121177780 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/astropy-base-7.2.0-py314hdcf55e8_0.conda + sha256: bbc0210239ef80bd80e5981b192f03c5a237a9a066945025585234648db19d11 + md5: 59d305b4aac5f4b79d5c8a5c9383f6fe + depends: + - __osx >=11.0 + - astropy-iers-data >=0.2025.10.27.0.39.10 + - numpy >=1.23,<3 + - numpy >=1.24 + - packaging >=22.0.0 + - pyerfa >=2.0.1.1 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - pyyaml >=6.0.0 + constrains: + - astropy >=7.0.0 + license: BSD-3-Clause + license_family: BSD + size: 9696751 + timestamp: 1764121149314 +- conda: https://conda.anaconda.org/conda-forge/win-64/astropy-base-7.2.0-py314h2dcd201_0.conda + sha256: 9b5726688adef7b2fe970a33ccb3d60ae7826ca3cb7ebcb04c051f7249ba5757 + md5: 535a85525e40a1cc8e5d2fe7f7b85c53 + depends: + - astropy-iers-data >=0.2025.10.27.0.39.10 + - numpy >=1.23,<3 + - numpy >=1.24 + - packaging >=22.0.0 + - pyerfa >=2.0.1.1 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - pyyaml >=6.0.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - astropy >=7.0.0 + license: BSD-3-Clause + license_family: BSD + size: 9647766 + timestamp: 1764120824948 +- conda: https://conda.anaconda.org/conda-forge/noarch/astropy-iers-data-0.2025.12.8.0.38.44-pyhd8ed1ab_0.conda + sha256: 621bc2dcbb165587067c68d4ff87ce6ddbb92cbe86161f35a44c81f20f4d08aa + md5: 62be984ecacca909a0f8c4009347fdd8 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 1237257 + timestamp: 1765160444930 +- conda: https://conda.anaconda.org/conda-forge/noarch/astroquery-0.4.11-pyhd8ed1ab_0.conda + sha256: 47b686df04b1dc8bf2a6dd6c4ebfa7afa80eaf00974a00c2976804e31369692d + md5: f6d7a43c5fb5cbeadaa0e2815f96fd36 + depends: + - astropy-base >=5.0 + - beautifulsoup4 >=4.8 + - html5lib >=0.999 + - keyring >=15.0 + - numpy >=1.20.0 + - python >=3.9 + - pyvo >=1.5 + - requests >=2.19 + license: BSD-3-Clause + license_family: BSD + size: 9855118 + timestamp: 1758344073086 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 + md5: 9673a61a297b00016442e022d689faa6 + depends: + - python >=3.10 + constrains: + - astroid >=2,<5 + license: Apache-2.0 + license_family: Apache + size: 28797 + timestamp: 1763410017955 +- conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + sha256: 33d12250c870e06c9a313c6663cfbf1c50380b73dfbbb6006688c3134b29b45a + md5: 5d842988b11a8c3ab57fb70840c83d24 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: Apache + size: 11763 + timestamp: 1733235428203 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + sha256: c13d5e42d187b1d0255f591b7ce91201d4ed8a5370f0d986707a802c20c9d32f + md5: 537296d57ea995666c68c821b00e360b + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 64759 + timestamp: 1764875182184 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda + sha256: d9c5babed03371448bb0dc91a1573c80d278d1222a3b0accef079ed112e584f9 + md5: bdd464b33f6540ed70845b946c11a7b8 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 133443 + timestamp: 1764765235190 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.9.3-hdff831d_0.conda + sha256: aaadae39675911059bf0caa072c9d0cab622278365f6c3ceb6a63a2e9e57df03 + md5: a04fb222805ce5697065036ae1676436 + depends: + - __osx >=10.13 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 119662 + timestamp: 1764765258455 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.9.3-h1ddaa69_0.conda + sha256: 491576e1ef8640e0cc345705c2028aebb98e015d51471395fe595f60a3b33884 + md5: f0cc47ecd2058f2dd65fde1a5f6528ec + depends: + - __osx >=11.0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 114473 + timestamp: 1764765266429 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.9.3-h2970c50_0.conda + sha256: 1ca3be8873335aff46da2d613c0e9e0c27b9878e402548e3cf31cd378a2f9342 + md5: 6f42aac88a3b880dd3a4e0fe61f418bc + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 125616 + timestamp: 1764765271198 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda + sha256: f21d648349a318f4ae457ea5403d542ba6c0e0343b8642038523dd612b2a5064 + md5: 3c3d02681058c3d206b562b2e3bc337f + depends: + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - libgcc >=14 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: Apache + size: 56230 + timestamp: 1764593147526 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.13-hea39f9f_1.conda + sha256: c085b749572ca7c137dfbf8a2a4fd505657f8f7f8a7b374d5f41bf4eb2dd9214 + md5: cbf7be9e03e8b5e38ec60b6dbdf3a649 + depends: + - __osx >=10.13 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: Apache + size: 45262 + timestamp: 1764593359925 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.13-h6ee9776_1.conda + sha256: 13c42cb54619df0a1c3e5e5b0f7c8e575460b689084024fd23abeb443aac391b + md5: 8baab664c541d6f059e83423d9fc5e30 + depends: + - __osx >=11.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: Apache + size: 45233 + timestamp: 1764593742187 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.9.13-h46f3b43_1.conda + sha256: 5f61082caea9fbdd6ba02702935e9dea9997459a7e6c06fd47f21b81aac882fb + md5: 7cc4953d504d4e8f3d6f4facb8549465 + depends: + - aws-c-common >=0.12.6,<0.12.7.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 53613 + timestamp: 1764593604081 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda + sha256: 926a5b9de0a586e88669d81de717c8dd3218c51ce55658e8a16af7e7fe87c833 + md5: e36ad70a7e0b48f091ed6902f04c23b8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + size: 239605 + timestamp: 1763585595898 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.12.6-h8616949_0.conda + sha256: 66fb2710898bb3e25cb4af52ee88a0559dcde5e56e6bd09b31b98a346a89b2e3 + md5: c7f2d588a6d50d170b343f3ae0b72e62 + depends: + - __osx >=10.13 + license: Apache-2.0 + license_family: Apache + size: 230785 + timestamp: 1763585852531 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.12.6-hc919400_0.conda + sha256: cd3817c82470826167b1d8008485676862640cff65750c34062e6c20aeac419b + md5: b759f02a7fa946ea9fd9fb035422c848 + depends: + - __osx >=11.0 + license: Apache-2.0 + license_family: Apache + size: 224116 + timestamp: 1763585987935 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.12.6-hfd05255_0.conda + sha256: 0627691c34eb3d9fcd18c71346d9f16f83e8e58f9983e792138a2cccf387d18a + md5: b1465f33b05b9af02ad0887c01837831 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 236441 + timestamp: 1763586152571 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda + sha256: 96edccb326b8c653c8eb95a356e01d4aba159da1a97999577b7dd74461b040b4 + md5: f7ec84186dfe7a9e3a9f9e5a4d023e75 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 22272 + timestamp: 1764593718823 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.1-h901532c_9.conda + sha256: b99ddb6654ca12b9f530ca4cbe4d2063335d4ac43f9d97092c4076ccaf9b89e7 + md5: abb79371a321d47da8f7ddca128533de + depends: + - __osx >=10.13 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 21423 + timestamp: 1764593738902 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.1-h16f91aa_9.conda + sha256: 988f2251c5ddb91a93a3893e52eccb4fdd8b755af80bbc2bf739aabc25c5cfdf + md5: 8dc111381c4c73deb8b9a529b3abee4a + depends: + - __osx >=11.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 21372 + timestamp: 1764593773975 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.3.1-hcb3a2da_9.conda + sha256: ff1046d67709960859adfa5793391a2d233bb432ec7429069fcfab5b643827df + md5: 0888dbe9e883582d138ec6221f5482d6 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 23136 + timestamp: 1764593733263 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda + sha256: a5b151db1c8373b6ca2dacea65bc8bda02791a43685eebfa4ea987bb1a758ca9 + md5: 7b8e3f846353b75db163ad93248e5f9d + depends: + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 58806 + timestamp: 1764675439822 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.5.7-ha05da6a_1.conda + sha256: 56f7aebd59d5527830ef7cf6e91f63ee4c5cf510af56529276affe8e2dc9eb24 + md5: e0d71662f35b21fb993484238b4861d9 + depends: + - __osx >=10.13 + - libcxx >=19 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 52911 + timestamp: 1764675471218 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.5.7-h9ae9c55_1.conda + sha256: c336b71a356d9b39fa6e9769d475dea6fd0cfe25ad81dcecac3102ef30f8b753 + md5: 53c59e7f68bbd3754de6c8dcd4c27f86 + depends: + - libcxx >=19 + - __osx >=11.0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 52221 + timestamp: 1764675514267 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.5.7-ha388e84_1.conda + sha256: 5fbbfd835831dace087064d08c38eb279b7db3231fbd0db32fad86fe9273c10c + md5: 34e3b065b76c8a144c92e224cc3f5672 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 57054 + timestamp: 1764675494741 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda + sha256: 5527224d6e0813e37426557d38cb04fed3753d6b1e544026cfbe2654f5e556be + md5: 3028f20dacafc00b22b88b324c8956cc + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-compression >=0.3.1,<0.3.2.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 224580 + timestamp: 1764675497060 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.10.7-h924c446_5.conda + sha256: 53ee041db79f6cbff62179b2f693e50e484d163b9a843a3dbbb80dbc36220c7e + md5: acff093ebb711857fb78fae3b656631c + depends: + - __osx >=10.13 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-compression >=0.3.1,<0.3.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 192149 + timestamp: 1764675489248 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.10.7-h5928ca5_5.conda + sha256: 29e180b61155279a2e64011b95957fbe38385113c60467b8d34fce47bc29c728 + md5: f12bd6066c693efba2e5886e2c70d7ba + depends: + - __osx >=11.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-compression >=0.3.1,<0.3.2.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 171020 + timestamp: 1764675515369 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.10.7-hc678f4a_5.conda + sha256: 4f41b922ce01c983f98898208d49af5f3d6b0d8f3e8dcb44bd13d8183287b19a + md5: 3427460b0654d317e72a0ba959bb3a23 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-compression >=0.3.1,<0.3.2.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 206709 + timestamp: 1764675527860 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda + sha256: 07d7f2a4493ada676084c3f4313da1fab586cf0a7302572c5d8dde6606113bf4 + md5: 132e8f8f40f0ffc0bbde12bb4e8dd1a1 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - s2n >=1.6.2,<1.6.3.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 181361 + timestamp: 1765168239856 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.23.3-hf559bb5_5.conda + sha256: 734496fb5a33a4d13ff0a27c5bc4a0f4e7fe9ed15ec099722d5be82b456b9502 + md5: d9cc056da3a1ee0a2da750d10a5496f3 + depends: + - __osx >=10.15 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 182572 + timestamp: 1765168277462 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.23.3-hbe03c90_5.conda + sha256: bf1c7cf7997d28922283e6612e5ea6a9409fcfc2749cd4acfafd1bf6e0c57c08 + md5: c249aa1a151e319d7acd05a2e1f165d2 + depends: + - __osx >=11.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 176451 + timestamp: 1765168273313 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.23.3-h0d5b9f9_5.conda + sha256: 2d726ffd67fb387dbebf63c9b9965b476b9d670f683e71c3dca1feb6365ddc7c + md5: 400792109e426730ac9047fd6c9537ef + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 182053 + timestamp: 1765168273517 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda + sha256: fb102b0346a1f5c4f3bb680ec863c529b0333fa4119d78768c3e8a5d1cc2c812 + md5: 6a653aefdc5d83a4f959869d1759e6e3 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 216454 + timestamp: 1764681745427 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.13.3-ha72ff4e_11.conda + sha256: c05215c85f90a0caba1202f4c852d6e3a2ad93b4a25f286435a8e855db4237ae + md5: 96f22c912f1cf3493d9113b9fd04c912 + depends: + - __osx >=10.13 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 188230 + timestamp: 1764681760102 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.13.3-haf5c5c8_11.conda + sha256: 880996ae8c792eb15fcbca0a452d8b3508dba16ed7384bdb73fb7ed6c075c125 + md5: 3fcd02361ce1427ae5968fcd532a85b4 + depends: + - __osx >=11.0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 150454 + timestamp: 1764681796127 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.13.3-hfa314fa_11.conda + sha256: 9b241397ef436dcf67e8e6cde15ff9c0d03ea942ad11e27c77caecce0d51b5be + md5: 6c043365f1d3f89c0b68238c6f5b8cce + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 206357 + timestamp: 1764681793150 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda + sha256: 8de2292329dce2fd512413d83988584d616582442a07990f67670f9bc793a98b + md5: 3689a4290319587e3b54a4f9e68f70c8 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - openssl >=3.5.4,<4.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 151382 + timestamp: 1765174166541 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.11.3-he30762a_1.conda + sha256: 9c989a5f0b35ff5cee91b74bcba0d540ce5684450dc072ba0bb5299783cdf9cd + md5: 33c653401dc7b016b0011cb4d16de458 + depends: + - __osx >=10.13 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 133827 + timestamp: 1765174162875 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.11.3-h8da9771_1.conda + sha256: 31f432d1a0f7dacbe80b476c3236c22a71f4018e840ae6974e843d38d5763335 + md5: 06417cb45f131cf503d3483446cedbc3 + depends: + - __osx >=11.0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 129384 + timestamp: 1765174183548 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.11.3-ha659bf3_1.conda + sha256: cda138c03683e85f29eafc680b043a40f304ac8759138dc141a42878eb17a90f + md5: dcfc08ccd8e332411c454e38110ea915 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + size: 141805 + timestamp: 1765174184168 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda + sha256: 9d62c5029f6f8219368a8665f0a549da572dc777f52413b7d75609cacdbc02cc + md5: c7e3e08b7b1b285524ab9d74162ce40b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 59383 + timestamp: 1764610113765 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-h901532c_4.conda + sha256: 468629dbf52fee6dcabda1fcb0c0f2f29941b9001dcc75a57ebfbe38d0bde713 + md5: b384fb05730f549a55cdb13c484861eb + depends: + - __osx >=10.13 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 55664 + timestamp: 1764610141049 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h16f91aa_4.conda + sha256: 8a4ee03ea6e14d5a498657e5fe96875a133b4263b910c5b60176db1a1a0aaa27 + md5: 658a8236f3f1ebecaaa937b5ccd5d730 + depends: + - __osx >=11.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 53430 + timestamp: 1764755714246 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.2.4-hcb3a2da_4.conda + sha256: c86c30edba7457e04d905c959328142603b62d7d1888aed893b2e21cca9c302c + md5: 3c97faee5be6fd0069410cf2bca71c85 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 56509 + timestamp: 1764610148907 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda + sha256: a8693d2e06903a09e98fe724ed5ec32e7cd1b25c405d754f0ab7efb299046f19 + md5: 68da5b56dde41e172b7b24f071c4b392 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 76915 + timestamp: 1764593731486 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.7-h901532c_5.conda + sha256: 0f67c453829592277f90d520f7855e260cf0565a3dc59fe90c55293996b7fbe9 + md5: cccf553ce36da9ae739206b69c1a4d28 + depends: + - __osx >=10.13 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 75646 + timestamp: 1764593751665 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.7-h16f91aa_5.conda + sha256: c630ece8c0fe99cdf03774bb0b048cfd72daec0458dbc825be5de0106431087e + md5: ee9ebfd7b6fdf61dd632e4fea6287c47 + depends: + - __osx >=11.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 74377 + timestamp: 1764593734393 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.2.7-hcb3a2da_5.conda + sha256: ca5e0719b7ca257462a4aa7d3b99fde756afaf579ee1472cac91c04c7bf3a725 + md5: 38f1501fc55f833a4567c83581a2d2ed + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 93142 + timestamp: 1764593765744 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.4-h8824e59_0.conda + sha256: 524fc8aa2645e5701308b865bf5c523257feabc6dfa7000cb8207ccfbb1452a1 + md5: 113b9d9913280474c0868b0e290c0326 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-mqtt >=0.13.3,<0.13.4.0a0 + - aws-c-s3 >=0.11.3,<0.11.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 408804 + timestamp: 1765200263609 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.35.2-h7484968_6.conda + sha256: 199db73ed3d3c7503b4cdfaef2e18bd7b2e67c2464d64c37f250833897a65d84 + md5: 1c3916576404e725bb46c8393e90dab5 + depends: + - libcxx >=19 + - __osx >=10.13 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + - aws-c-mqtt >=0.13.3,<0.13.4.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-s3 >=0.11.3,<0.11.4.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + license: Apache-2.0 + license_family: APACHE + size: 344127 + timestamp: 1765193382465 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.35.4-h74951b9_0.conda + sha256: 465527f414c2399ab70503d9d4e891658e7698439ba7f22d723f2ca8c03bb3e8 + md5: 87351fb3a08425237b701c582773be1a + depends: + - __osx >=11.0 + - libcxx >=19 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-s3 >=0.11.3,<0.11.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-c-mqtt >=0.13.3,<0.13.4.0a0 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + size: 266862 + timestamp: 1765200345049 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.35.4-hca034e6_0.conda + sha256: 7b4aef9e1823207a5f91e8b5b95853bdfafcfea306cd62b99fd53c38aa5c3da0 + md5: ce1a20b5c406727e32222ac91e5848c4 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-mqtt >=0.13.3,<0.13.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-c-s3 >=0.11.3,<0.11.4.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 302247 + timestamp: 1765200336894 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h20b40b1_10.conda + sha256: e0d81b7dd6d054d457a1c54d17733d430d96dc5ca9b2ca69a72eb41c3fc8c9bf + md5: 937d1d4c233adc6eeb2ac3d6e9a73e53 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.17.0,<9.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - libzlib >=1.3.1,<2.0a0 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 3472674 + timestamp: 1765257107074 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.606-hffd60a0_9.conda + sha256: a58e471c09ffc63bafa4a2833a1d8f175693852763d840c446092898fa635b31 + md5: a76d9ef0a4417a6f418207b62ca3c796 + depends: + - libcxx >=19 + - __osx >=10.13 + - libcurl >=8.17.0,<9.0a0 + - aws-crt-cpp >=0.35.2,<0.35.3.0a0 + - libzlib >=1.3.1,<2.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 3313038 + timestamp: 1765199752667 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.606-h4e1b0f7_10.conda + sha256: 87660413df6c49984a897544c8ace8461cd4ed69301ede5a793d00530985f702 + md5: a392fe9e9a3c6e0b65161533aca39be9 + depends: + - __osx >=11.0 + - libcxx >=19 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + - libzlib >=1.3.1,<2.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - libcurl >=8.17.0,<9.0a0 + license: Apache-2.0 + license_family: APACHE + size: 3121951 + timestamp: 1765257130593 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.606-hac16450_10.conda + sha256: 8a12c4f6774ecb3641048b74133ff5e6c2b560469fe5ac1d7515631b84e63059 + md5: d9b942bede589d0ad1e8e360e970efd0 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + license: Apache-2.0 + license_family: APACHE + size: 3438133 + timestamp: 1765257127502 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.1-h3a458e0_0.conda + sha256: cba633571e7368953520a4f66dc74c3942cc12f735e0afa8d3d5fc3edf35c866 + md5: 1d4e0d37da5f3c22ecd44033f673feba + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.14.1,<9.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 348231 + timestamp: 1760926677260 +- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.1-he2a98a9_0.conda + sha256: 923a0f9fab0c922e17f8bb27c8210d8978111390ff4e0cf6c1adff3c1a4d13bc + md5: 9f39c22aad61e76bfb73bb7d4114efac + depends: + - __osx >=10.13 + - libcurl >=8.14.1,<9.0a0 + - libcxx >=19 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 297681 + timestamp: 1760927174036 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.1-h88fedcc_0.conda + sha256: d995413e4daf19ee3120f3ab9f0c9e330771787f33cbd4a33d8e5445f52022e3 + md5: fbe485a39b05090c0b5f8bb4febcd343 + depends: + - __osx >=11.0 + - libcurl >=8.14.1,<9.0a0 + - libcxx >=19 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 289984 + timestamp: 1760927117177 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.2-h3a5f585_1.conda + sha256: fc1df5ea2595f4f16d0da9f7713ce5fed20cb1bfc7fb098eda7925c7d23f0c45 + md5: 4e921d9c85e6559c60215497978b3cdb + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 249684 + timestamp: 1761066654684 +- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.2-h0e8e1c8_1.conda + sha256: 555e9c9262b996f8c688598760b4cddf4d16ae1cb2f0fd0a31cb76c2fdc7d628 + md5: 32eb613f88ae1530ca78481bdce41cdd + depends: + - __osx >=10.13 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - libcxx >=19 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 174582 + timestamp: 1761067038720 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.2-h853621b_1.conda + sha256: a4ed52062025035d9c1b3d8c70af39496fc5153cc741420139a770bc1312cfd6 + md5: fac63edc393d7035ab23fbccdeda34f4 + depends: + - __osx >=11.0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - libcxx >=19 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 167268 + timestamp: 1761066827371 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.15.0-h2a74896_1.conda + sha256: 58879f33cd62c30a4d6a19fd5ebc59bd0c4560f575bd02645d93d342b6f881d2 + md5: ffd553ff98ce5d74d3d89ac269153149 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-storage-common-cpp >=12.11.0,<12.11.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + size: 576406 + timestamp: 1761080005291 +- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.15.0-h388f2e7_1.conda + sha256: 0a736f04c9778b87884422ebb6b549495430652204d964ff161efb719362baee + md5: 6b5f36e610295f4f859dd9cf680bbf7d + depends: + - __osx >=10.13 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-storage-common-cpp >=12.11.0,<12.11.1.0a0 + - libcxx >=19 + license: MIT + license_family: MIT + size: 432811 + timestamp: 1761080273088 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.15.0-h10d327b_1.conda + sha256: 274267b458ed51f4b71113fe615121fabd6f1d7b62ebfefdad946f8436a5db8e + md5: 443b74cf38c6b0f4b675c0517879ce69 + depends: + - __osx >=11.0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-storage-common-cpp >=12.11.0,<12.11.1.0a0 + - libcxx >=19 + license: MIT + license_family: MIT + size: 425175 + timestamp: 1761080947110 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.11.0-h3d7a050_1.conda + sha256: eb590e5c47ee8e6f8cc77e9c759da860ae243eed56aceb67ce51db75f45c9a50 + md5: 89985ba2a3742f34be6aafd6a8f3af8c + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 149620 + timestamp: 1761066643066 +- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.11.0-h56a711b_1.conda + sha256: 322919e9842ddf5c9d0286667420a76774e1e42ae0520445d65726f8a2565823 + md5: 278ccb9a3616d4342731130287c3ba79 + depends: + - __osx >=10.13 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - libcxx >=19 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 126230 + timestamp: 1761066840950 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.11.0-h7e4aa5d_1.conda + sha256: 74803bd26983b599ea54ff1267a0c857ff37ccf6f849604a72eb63d8d30e4425 + md5: ac9113ea0b7ed5ecf452503f82bf2956 + depends: + - __osx >=11.0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - libcxx >=19 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + size: 121744 + timestamp: 1761066874537 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.13.0-hf38f1be_1.conda + sha256: 9f3d0f484e97cef5f019b7faef0c07fb7ee6c584e3a6e2954980f440978a365e + md5: f10b9303c7239fbce3580a60a92bcf97 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-storage-blobs-cpp >=12.15.0,<12.15.1.0a0 + - azure-storage-common-cpp >=12.11.0,<12.11.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + size: 299198 + timestamp: 1761094654852 +- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.13.0-h1984e67_1.conda + sha256: 268175ab07f1917eff35e4c38a17a2b71c5f9b86e38e5c0b313da477600a82df + md5: ef5701f2da108d432e7872d58e8ac64e + depends: + - __osx >=10.13 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-storage-blobs-cpp >=12.15.0,<12.15.1.0a0 + - azure-storage-common-cpp >=12.11.0,<12.11.1.0a0 + - libcxx >=19 + license: MIT + license_family: MIT + size: 203298 + timestamp: 1761095036240 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.13.0-hb288d13_1.conda + sha256: 2205e24d587453a04b075f86c59e3e72ad524c447fc5be61d7d1beb3cf2d7661 + md5: 595091ae43974e5059d6eabf0a6a7aa5 + depends: + - __osx >=11.0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-storage-blobs-cpp >=12.15.0,<12.15.1.0a0 + - azure-storage-common-cpp >=12.11.0,<12.11.1.0a0 + - libcxx >=19 + license: MIT + license_family: MIT + size: 197152 + timestamp: 1761094913245 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda + sha256: e1c3dc8b5aa6e12145423fed262b4754d70fec601339896b9ccf483178f690a6 + md5: 767d508c1a67e02ae8f50e44cacfadb2 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 7069 + timestamp: 1733218168786 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyh5ded981_2.conda + sha256: 2ade43752e8494f110a2cfb9e4d5b1ea29e3dcb037fba63395442d00371e8bf9 + md5: 0fd7e45c862b3305226a992f9f7b204a + depends: + - python >=3.11 + - python + constrains: + - python >=3.11 + license: PSF-2.0 + license_family: PSF + size: 10186 + timestamp: 1753456386827 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda + sha256: 25abdb37e186f0d6ac3b774a63c81c5bc4bf554b5096b51343fa5e7c381193b1 + md5: bea46844deb274b2cc2a3a941745fa73 + depends: + - python >=3.10 + - backports + - python + license: MIT + license_family: MIT + size: 35739 + timestamp: 1767290467820 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.2.0-py314h680f03e_0.conda + noarch: generic + sha256: de90f762aecfa4b8680ae7299398bd4a1634870a01db8351e5e22affc6bbf313 + md5: 25e227ee028a17c2f2ef6eaf97e86734 + depends: + - python >=3.14 + license: BSD-3-Clause AND MIT AND EPL-2.0 + size: 7512 + timestamp: 1765057691766 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 + md5: 5267bef8efea4127aacd1f4e1f149b6e + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + size: 90399 + timestamp: 1764520638652 +- conda: https://conda.anaconda.org/conda-forge/noarch/black-25.12.0-pyh866005b_0.conda + sha256: b7d00a8b682f650ac547d8d70c6cd65f303011313b3d3608d3704f20b1dad5b6 + md5: 7b658ed81f14384c83f4c4f01959fdc2 + depends: + - click >=8.0.0 + - mypy_extensions >=0.4.3 + - packaging >=22.0 + - pathspec >=0.9 + - platformdirs >=2 + - python >=3.11 + - pytokens >=0.3 + license: MIT + license_family: MIT + size: 169740 + timestamp: 1765222747417 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_0.conda + sha256: e03ba1a2b93fe0383c57920a9dc6b4e0c2c7972a3f214d531ed3c21dc8f8c717 + md5: b1a27250d70881943cca0dd6b4ba0956 + depends: + - python >=3.10 + - webencodings + - python + constrains: + - tinycss >=1.1.0,<1.5 + license: Apache-2.0 AND MIT + size: 141952 + timestamp: 1763589981635 +- conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + sha256: e7af5d1183b06a206192ff440e08db1c4e8b2ca1f8376ee45fb2f3a85d4ee45d + md5: 2c2fae981fd2afd00812c92ac47d023d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.1,<1.3.0a0 + - zstd >=1.5.6,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 48427 + timestamp: 1733513201413 +- conda: https://conda.anaconda.org/conda-forge/osx-64/blosc-1.21.6-hd145fbb_1.conda + sha256: 876bdb1947644b4408f498ac91c61f1f4987d2c57eb47c0aba0d5ee822cd7da9 + md5: 717852102c68a082992ce13a53403f9d + depends: + - __osx >=10.13 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.1,<1.3.0a0 + - zstd >=1.5.6,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 46990 + timestamp: 1733513422834 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + sha256: c3fe902114b9a3ac837e1a32408cc2142c147ec054c1038d37aec6814343f48a + md5: 925acfb50a750aa178f7a0aced77f351 + depends: + - __osx >=11.0 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.1,<1.3.0a0 + - zstd >=1.5.6,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 33602 + timestamp: 1733513285902 +- conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + sha256: 9303a7a0e03cf118eab3691013f6d6cbd1cbac66efbc70d89b20f5d0145257c0 + md5: 357d7be4146d5fec543bfaa96a8a40de + depends: + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.1,<1.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zstd >=1.5.6,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 49840 + timestamp: 1733513605730 +- conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + sha256: 92e3b65d162600eec4c858a870e2b7593886d837c965ca51bf8bd1ed0e6f1e27 + md5: 280a8a31bface0a6b1cf49ea85004128 + depends: + - jmespath >=0.7.1,<2.0.0 + - python >=3.10 + - python-dateutil >=2.1,<3.0.0 + - urllib3 >=1.25.4,!=2.2.0,<3 + license: Apache-2.0 + license_family: Apache + size: 8150945 + timestamp: 1762813779810 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bottleneck-1.6.0-np2py314h56abb78_3.conda + sha256: 58cc4ecb796ec8093863d13264aca2746fa833461b30fd24b620d1acee0efd08 + md5: 48b137fb9317635b90c335348518d0a6 + depends: + - numpy + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - numpy >=1.23,<3 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + size: 158983 + timestamp: 1762775788892 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bottleneck-1.6.0-np2py314hfeef9c2_3.conda + sha256: b75b8e766102cac6fa01ae63f94f81841a041f8f2dba554be8095bd2e3f02d19 + md5: 5088e82d7776efb203ff2ef560d0dc52 + depends: + - numpy + - python + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + - numpy >=1.23,<3 + license: BSD-2-Clause + license_family: BSD + size: 158336 + timestamp: 1762775903695 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bottleneck-1.6.0-np2py314hfa18b03_3.conda + sha256: 377dd23a6ebc813a6f3e9f54ef6152bd0dc447527aad6b37638822916b4fd484 + md5: f48af87bb77ab96c244e5105c4a9434b + depends: + - numpy + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - numpy >=1.23,<3 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + size: 140095 + timestamp: 1762775905428 +- conda: https://conda.anaconda.org/conda-forge/win-64/bottleneck-1.6.0-np2py314hea88fa1_3.conda + sha256: 480b5a3f635e6cbefceb29adb448b83e580fede49022894d0939bce0ebd1cfe7 + md5: 9f8dae835389010da7ad59bc673dd06b + depends: + - numpy + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - numpy >=1.23,<3 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + size: 141911 + timestamp: 1762775771443 +- conda: https://conda.anaconda.org/conda-forge/noarch/bqplot-0.12.45-pyhe01879c_0.conda + sha256: 2248c46491d6cc11692d7fbc5bb61c1b6177fd50654a296c13e31434e30b8994 + md5: 3cedf673ae6d0e272807bcb9929df40e + depends: + - ipywidgets >=7.6.0,<9 + - numpy >=1.10.4 + - pandas >=1.0.0,<3.0.0 + - python >=3.9 + - traitlets >=4.3.0,<6.0 + - traittypes >=0.0.6 + - python + license: Apache-2.0 + license_family: APACHE + size: 966021 + timestamp: 1756830785696 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + sha256: e511644d691f05eb12ebe1e971fd6dc3ae55a4df5c253b4e1788b789bdf2dfa6 + md5: 8ccf913aaba749a5496c17629d859ed1 + depends: + - __glibc >=2.17,<3.0.a0 + - brotli-bin 1.2.0 hb03c661_1 + - libbrotlidec 1.2.0 hb03c661_1 + - libbrotlienc 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + size: 20103 + timestamp: 1764017231353 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda + sha256: c838c71ded28ada251589f6462fc0f7c09132396799eea2701277566a1a863bf + md5: 149d8ee7d6541a02a6117d8814fd9413 + depends: + - __osx >=10.13 + - brotli-bin 1.2.0 h8616949_1 + - libbrotlidec 1.2.0 h8616949_1 + - libbrotlienc 1.2.0 h8616949_1 + license: MIT + license_family: MIT + size: 20194 + timestamp: 1764017661405 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + sha256: 422ac5c91f8ef07017c594d9135b7ae068157393d2a119b1908c7e350938579d + md5: 48ece20aa479be6ac9a284772827d00c + depends: + - __osx >=11.0 + - brotli-bin 1.2.0 hc919400_1 + - libbrotlidec 1.2.0 hc919400_1 + - libbrotlienc 1.2.0 hc919400_1 + license: MIT + license_family: MIT + size: 20237 + timestamp: 1764018058424 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + sha256: a4fffdf1c9b9d3d0d787e20c724cff3a284dfa3773f9ce609c93b1cfd0ce8933 + md5: bc58fdbced45bb096364de0fba1637af + depends: + - brotli-bin 1.2.0 hfd05255_1 + - libbrotlidec 1.2.0 hfd05255_1 + - libbrotlienc 1.2.0 hfd05255_1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 20342 + timestamp: 1764017988883 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + sha256: 64b137f30b83b1dd61db6c946ae7511657eead59fdf74e84ef0ded219605aa94 + md5: af39b9a8711d4a8d437b52c1d78eb6a1 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlidec 1.2.0 hb03c661_1 + - libbrotlienc 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + size: 21021 + timestamp: 1764017221344 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda + sha256: dcb5a2b29244b82af2545efad13dfdf8dddb86f88ce64ff415be9e7a10cc0383 + md5: 34803b20dfec7af32ba675c5ccdbedbf + depends: + - __osx >=10.13 + - libbrotlidec 1.2.0 h8616949_1 + - libbrotlienc 1.2.0 h8616949_1 + license: MIT + license_family: MIT + size: 18589 + timestamp: 1764017635544 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + sha256: e2d142052a83ff2e8eab3fe68b9079cad80d109696dc063a3f92275802341640 + md5: 377d015c103ad7f3371be1777f8b584c + depends: + - __osx >=11.0 + - libbrotlidec 1.2.0 hc919400_1 + - libbrotlienc 1.2.0 hc919400_1 + license: MIT + license_family: MIT + size: 18628 + timestamp: 1764018033635 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + sha256: e76966232ef9612de33c2087e3c92c2dc42ea5f300050735a3c646f33bce0429 + md5: 6abd7089eb3f0c790235fe469558d190 + depends: + - libbrotlidec 1.2.0 hfd05255_1 + - libbrotlienc 1.2.0 hfd05255_1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 22714 + timestamp: 1764017952449 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 + md5: 8910d2c46f7e7b519129f486e0fe927a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + size: 367376 + timestamp: 1764017265553 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py314h3262eb8_1.conda + sha256: 2e34922abda4ac5726c547887161327b97c3bbd39f1204a5db162526b8b04300 + md5: 389d75a294091e0d7fa5a6fc683c4d50 + depends: + - __osx >=10.13 + - libcxx >=19 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT + size: 390153 + timestamp: 1764017784596 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + sha256: 5c2e471fd262fcc3c5a9d5ea4dae5917b885e0e9b02763dbd0f0d9635ed4cb99 + md5: f9501812fe7c66b6548c7fcaa1c1f252 + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + size: 359854 + timestamp: 1764018178608 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + sha256: 6854ee7675135c57c73a04849c29cbebc2fb6a3a3bfee1f308e64bf23074719b + md5: 1302b74b93c44791403cbeee6a0f62a3 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libbrotlicommon 1.2.0 hfd05255_1 + license: MIT + license_family: MIT + size: 335782 + timestamp: 1764018443683 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 + md5: 51a19bba1b8ebfb60df25cde030b7ebc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + size: 260341 + timestamp: 1757437258798 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + sha256: 8f50b58efb29c710f3cecf2027a8d7325ba769ab10c746eff75cea3ac050b10c + md5: 97c4b3bd8a90722104798175a1bdddbf + depends: + - __osx >=10.13 + license: bzip2-1.0.6 + license_family: BSD + size: 132607 + timestamp: 1757437730085 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 + md5: 58fd217444c2a5701a44244faf518206 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + size: 125061 + timestamp: 1757437486465 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 + md5: 1077e9333c41ff0be8edd1a5ec0ddace + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + size: 55977 + timestamp: 1757437738856 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 207882 + timestamp: 1765214722852 +- conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda + sha256: 2f5bc0292d595399df0d168355b4e9820affc8036792d6984bd751fdda2bcaea + md5: fc9a153c57c9f070bebaa7eef30a8f17 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 186122 + timestamp: 1765215100384 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196 + md5: bcb3cba70cf1eec964a03b4ba7775f01 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 180327 + timestamp: 1765215064054 +- conda: https://conda.anaconda.org/conda-forge/win-64/c-ares-1.34.6-hfd05255_0.conda + sha256: 5e1e2e24ce279f77e421fcc0e5846c944a8a75f7cf6158427c7302b02984291a + md5: 7c6da34e5b6e60b414592c74582e28bf + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 193550 + timestamp: 1765215100218 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.22.0-hc31b594_1.conda + sha256: efe06a982fe7f4e483a2043c4b43fc3598a538a66ed11364ee5b25d3400ef415 + md5: 52019609422a72ec80c32bbc16a889d8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - lz4-c >=1.10.0,<1.11.0a0 + - zlib-ng >=2.3.1,<2.4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 352332 + timestamp: 1764291444176 +- conda: https://conda.anaconda.org/conda-forge/osx-64/c-blosc2-2.22.0-hedb7e5f_1.conda + sha256: f529640f28822172017b8159c5d1f149ceda2c44707bcf8732b812e806cff669 + md5: 13038523111830630683530ea54eb503 + depends: + - __osx >=10.13 + - libcxx >=19 + - lz4-c >=1.10.0,<1.11.0a0 + - zlib-ng >=2.3.1,<2.4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 287057 + timestamp: 1764291903510 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-blosc2-2.22.0-hb83781b_1.conda + sha256: 4c1afcc78418a5d171f94238bae8b798c288deb8ba454113cf11f10d72b09ff6 + md5: 5e4bdded23f6d61d8351223db98bc8f3 + depends: + - __osx >=11.0 + - libcxx >=19 + - lz4-c >=1.10.0,<1.11.0a0 + - zlib-ng >=2.3.1,<2.4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 253671 + timestamp: 1764291734763 +- conda: https://conda.anaconda.org/conda-forge/win-64/c-blosc2-2.22.0-h2af8807_1.conda + sha256: fb27b61b4c969e1761c2d02c12854a3e809c9db2b4097bdef77e0aaa3f7ee33a + md5: eb7c33dcf2ff0cea48cd13f0ebba44f5 + depends: + - lz4-c >=1.10.0,<1.11.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zlib-ng >=2.3.1,<2.4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + size: 225534 + timestamp: 1764291826235 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda + sha256: 686a13bd2d4024fc99a22c1e0e68a7356af3ed3304a8d3ff6bb56249ad4e82f0 + md5: f98fb7db808b94bc1ec5b0e62f9f1069 + depends: + - __win + license: ISC + size: 152827 + timestamp: 1762967310929 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + sha256: b986ba796d42c9d3265602bc038f6f5264095702dd546c14bc684e60c385e773 + md5: f0991f0f84902f6b6009b4d2350a83aa + depends: + - __unix + license: ISC + size: 152432 + timestamp: 1762967197890 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + noarch: python + sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 + md5: 9b347a7ec10940d3f7941ff6c460b551 + depends: + - cached_property >=1.5.2,<1.5.3.0a0 + license: BSD-3-Clause + license_family: BSD + size: 4134 + timestamp: 1615209571450 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 + md5: 576d629e47797577ab0f1b351297ef4a + depends: + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + size: 11065 + timestamp: 1615209567874 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7 + md5: 09262e66b19567aff4f592fb53b28760 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.5,<2.0a0 + - xorg-libx11 >=1.8.11,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.1-only or MPL-1.1 + size: 978114 + timestamp: 1741554591855 +- conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + sha256: b9f577bddb033dba4533e851853924bfe7b7c1623d0697df382eef177308a917 + md5: 20e32ced54300292aff690a69c5e7b97 + depends: + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.1-only or MPL-1.1 + size: 1524254 + timestamp: 1741555212198 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + sha256: 083a2bdad892ccf02b352ecab38ee86c3e610ba9a4b11b073ea769d55a115d32 + md5: 96a02a5c1a65470a7e4eedb644c872fd + depends: + - python >=3.10 + license: ISC + size: 157131 + timestamp: 1762976260320 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + sha256: c6339858a0aaf5d939e00d345c98b99e4558f285942b27232ac098ad17ac7f8e + md5: cf45f4278afd6f4e6d03eda0f435d527 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 300271 + timestamp: 1761203085220 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + sha256: b32f8362e885f1b8417bac2b3da4db7323faa12d5db62b7fd6691c02d60d6f59 + md5: a22d1fd9bf98827e280a02875d9a007a + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 50965 + timestamp: 1760437331772 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + sha256: 38cfe1ee75b21a8361c8824f5544c3866f303af1762693a178266d7f198e8715 + md5: ea8a6c3256897cc31263de9f455e25d9 + depends: + - python >=3.10 + - __unix + - python + license: BSD-3-Clause + license_family: BSD + size: 97676 + timestamp: 1764518652276 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyha7b4d00_1.conda + sha256: c3bc9a49930fa1c3383a1485948b914823290efac859a2587ca57a270a652e08 + md5: 6cd3ccc98bacfcc92b2bd7f236f01a7e + depends: + - python >=3.10 + - colorama + - __win + - python + license: BSD-3-Clause + license_family: BSD + size: 96620 + timestamp: 1764518654675 +- conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + sha256: 4c287c2721d8a34c94928be8fe0e9a85754e90189dd4384a31b1806856b50a67 + md5: 61b8078a0905b12529abc622406cb62c + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + size: 27353 + timestamp: 1765303462831 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + size: 14690 + timestamp: 1753453984907 +- conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py314h9891dd4_3.conda + sha256: 54c79736927c787e535db184bb7f3bce13217cb7d755c50666cfc0da7c6c86f3 + md5: 72d57382d0f63c20a16b1d514fcde6ff + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - numpy >=1.25 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 299226 + timestamp: 1762525516589 +- conda: https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.3.3-py314h00ed6fe_3.conda + sha256: 1ffeead3cedb5990d17c077b0943d6ded6b5d8c148becb01caaaa7920be122a4 + md5: 761aa19f97a0dd5dedb9a0a6003707c1 + depends: + - __osx >=10.13 + - libcxx >=19 + - numpy >=1.25 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 272746 + timestamp: 1762525900749 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py314h784bc60_3.conda + sha256: e5ca7f079f9bd49a9fce837dfe9014d96603600a29e5575cce19895d3639182c + md5: d75fae59fe0c8863de391e95959b2c65 + depends: + - __osx >=11.0 + - libcxx >=19 + - numpy >=1.25 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 262199 + timestamp: 1762525837746 +- conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py314h909e829_3.conda + sha256: f014eb687eb8dd25cec124594f4e48cf85803ff1db85a2a1f95719f9ec6434d2 + md5: 3647d90eea49efc6076729ef0ae81075 + depends: + - numpy >=1.25 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 227536 + timestamp: 1762525688384 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.2-py314hd8ed1ab_100.conda + noarch: generic + sha256: 9e345f306446500956ffb1414b773f5476f497d7a2b5335a59edd2c335209dbb + md5: 30f999d06f347b0116f0434624b6e559 + depends: + - python >=3.14,<3.15.0a0 + - python_abi * *_cp314 + license: Python-2.0 + size: 49298 + timestamp: 1765020324943 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-46.0.4-py314h7fe84b3_0.conda + sha256: 90738c26981732357d71b97df1994a1a74f87701468d61e19755af7d9e35edf8 + md5: afabda22fe5163200fc59f31b58d9e6a + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.14 + - libgcc >=14 + - openssl >=3.5.5,<4.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + size: 1719239 + timestamp: 1769650654007 +- conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + sha256: bb47aec5338695ff8efbddbc669064a3b10fe34ad881fb8ad5d64fbfa6910ed1 + md5: 4c2a8fef270f6c69591889b93f9f55c1 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + size: 14778 + timestamp: 1764466758386 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hd9c7081_0.conda + sha256: ee09ad7610c12c7008262d713416d0b58bf365bc38584dce48950025850bdf3f + md5: cae723309a49399d2949362f4ab5c9e4 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libntlm >=1.8,<2.0a0 + - libstdcxx >=13 + - libxcrypt >=4.4.36 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause-Attribution + license_family: BSD + size: 209774 + timestamp: 1750239039316 +- conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.12.0-pyhcf101f3_1.conda + sha256: f02b63259e8f927a7e38e818a8dd251a06bce3f3f853235b8886a3cb89e0dded + md5: cc7b371edd70319942c802c7d828a428 + depends: + - python >=3.10 + - click >=8.1 + - cloudpickle >=3.0.0 + - fsspec >=2021.9.0 + - packaging >=20.0 + - partd >=1.4.0 + - pyyaml >=5.3.1 + - toolz >=0.12.0 + - importlib-metadata >=4.13.0 + - python + license: BSD-3-Clause + size: 1062442 + timestamp: 1765558272352 +- conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda + sha256: 8bb557af1b2b7983cf56292336a1a1853f26555d9c6cecf1e5b2b96838c9da87 + md5: ce96f2f470d39bd96ce03945af92e280 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - libglib >=2.86.2,<3.0a0 + - libexpat >=2.7.3,<3.0a0 + license: AFL-2.1 OR GPL-2.0-or-later + size: 447649 + timestamp: 1764536047944 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.18-py314h42812f9_0.conda + sha256: 2803e9285da433a5d704a63ac9c64c87b5df9aaa1e2d48cc333e65d5a945912e + md5: 69635aa34b45d84c2599ff8b48094978 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - python_abi 3.14.* *_cp314 + license: MIT + size: 2888322 + timestamp: 1765704065377 +- conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.18-py314h3658963_0.conda + sha256: 7f9ace95f4a1ba6c6e212ee0d7d05aa6bf0f44adaf1388ca35348308962958d1 + md5: 42fe9cfc2ab30e60ad4641b42e93615a + depends: + - python + - libcxx >=19 + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + license: MIT + size: 2784382 + timestamp: 1765704065500 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.18-py314hf820bb6_0.conda + sha256: 9cbd840be5ac5304b28dd422552ac6a42b45606b94ba140579c0799f3802998f + md5: e12de4b9087624d63dba226c297a8d7f + depends: + - python + - python 3.14.* *_cp314 + - libcxx >=19 + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + license: MIT + size: 2776113 + timestamp: 1765704076173 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.18-py314hb98de8c_0.conda + sha256: d60cf14462bc4d4f0c851a24e268b688b6ae7918d89e8648028e8a141257e28d + md5: c0647965c420ce856b7d7a6d077afe55 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: MIT + size: 4021632 + timestamp: 1765704089964 +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 + md5: 9ce473d1d1be1cc3810856a48b3fab32 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + size: 14129 + timestamp: 1740385067843 +- conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + sha256: ef1e7b8405997ed3d6e2b6722bd7088d4a8adf215e7c88335582e65651fb4e05 + md5: d73fdc05f10693b518f52c994d748c19 + depends: + - python >=3.10,<4.0.0 + - sniffio + - python + constrains: + - aioquic >=1.2.0 + - cryptography >=45 + - httpcore >=1.0.0 + - httpx >=0.28.0 + - h2 >=4.2.0 + - idna >=3.10 + - trio >=0.30 + - wmi >=1.5.1 + license: ISC + size: 196500 + timestamp: 1757292856922 +- conda: https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.3.1-h5888daf_0.conda + sha256: 1bcc132fbcc13f9ad69da7aa87f60ea41de7ed4d09f3a00ff6e0e70e1c690bc2 + md5: bfd56492d8346d669010eccafe0ba058 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-3-Clause + license_family: BSD + size: 69544 + timestamp: 1739569648873 +- conda: https://conda.anaconda.org/conda-forge/win-64/double-conversion-3.3.1-he0c23c2_0.conda + sha256: b1fee32ef36a98159f0a2a96c4e734dfc9adff73acd444940831b22c1fb6d5c0 + md5: e9a1402439c18a4e3c7a52e4246e9e1c + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + size: 71355 + timestamp: 1739570178995 +- conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + sha256: c37320864c35ef996b0e02e289df6ee89582d6c8e233e18dc9983375803c46bb + md5: 3bc0ac31178387e8ed34094d9481bfe8 + depends: + - dnspython >=2.0.0 + - idna >=2.0.0 + - python >=3.10 + license: Unlicense + size: 46767 + timestamp: 1756221480106 +- conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + sha256: 6a518e00d040fcad016fb2dde29672aa3476cd9ae33ea5b7b257222e66037d89 + md5: 2452e434747a6b742adc5045f2182a8e + depends: + - email-validator >=2.3.0,<2.3.1.0a0 + license: Unlicense + size: 7077 + timestamp: 1756221480651 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 30753 + timestamp: 1756729456476 +- conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.124.4-hd122799_0.conda + sha256: 750d5c9a2f3b5887d3d4ae390544295ece610b75f9276eafc926f99afe7ee2d8 + md5: de74823e1b48db18c446d8b123a5391b + depends: + - fastapi-core ==0.124.4 pyhcf101f3_0 + - email_validator + - fastapi-cli + - httpx + - jinja2 + - python-multipart + - uvicorn-standard + license: MIT + size: 4785 + timestamp: 1765682147035 +- conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.16-pyhcf101f3_1.conda + sha256: 4136b0c277188b205332983278c7b278ea946dc1c78a381e0f5bc79204b8ac97 + md5: 4f82a266e2d5b199db16cdb42341d785 + depends: + - python >=3.10 + - rich-toolkit >=0.14.8 + - tomli >=2.0.0 + - typer >=0.15.1 + - uvicorn-standard >=0.15.0 + - python + license: MIT + license_family: MIT + size: 19029 + timestamp: 1763068963965 +- conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.124.4-pyhcf101f3_0.conda + sha256: f38b786d4b2012d629ecacbb6c090205245590dc464ed5500fe31e9e58f0c4d8 + md5: a3d7236ab2f52f893e667ab551cac180 + depends: + - python >=3.10 + - annotated-doc >=0.0.2 + - starlette >=0.40.0,<0.51.0 + - typing_extensions >=4.8.0 + - pydantic >=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0 + - python + constrains: + - email_validator >=2.0.0 + - fastapi-cli >=0.0.8 + - httpx >=0.23.0,<1.0.0 + - jinja2 >=3.1.5 + - python-multipart >=0.0.18 + - uvicorn-standard >=0.12.0 + license: MIT + size: 89596 + timestamp: 1765682147034 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b + md5: 0c96522c6bdaed4b1566d11387caaf45 + license: BSD-3-Clause + license_family: BSD + size: 397370 + timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c + md5: 34893075a5c9e55cdafac56607368fc6 + license: OFL-1.1 + license_family: Other + size: 96530 + timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 + md5: 4d59c254e01d9cde7957100457e2d5fb + license: OFL-1.1 + license_family: Other + size: 700814 + timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 + md5: 49023d73832ef61042f6a237cb2687e7 + license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 + license_family: Other + size: 1620504 + timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + sha256: 7093aa19d6df5ccb6ca50329ef8510c6acb6b0d8001191909397368b65b02113 + md5: 8f5b0b297b59e1ac160ad4beec99dbee + depends: + - __glibc >=2.17,<3.0.a0 + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + size: 265599 + timestamp: 1730283881107 +- conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda + sha256: ed122fc858fb95768ca9ca77e73c8d9ddc21d4b2e13aaab5281e27593e840691 + md5: 9bb0026a2131b09404c59c4290c697cd + depends: + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libiconv >=1.17,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 192355 + timestamp: 1730284147944 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 + md5: fee5683a3f04bd15cbd8318b096a27ab + depends: + - fonts-conda-forge + license: BSD-3-Clause + license_family: BSD + size: 3667 + timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 + md5: a7970cd949a077b7cb9696379d338681 + depends: + - font-ttf-ubuntu + - font-ttf-inconsolata + - font-ttf-dejavu-sans-mono + - font-ttf-source-code-pro + license: BSD-3-Clause + license_family: BSD + size: 4059 + timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonttools-4.61.1-pyh7db6752_0.conda + sha256: bb74f1732065eb95c3ea4ae7f7ab29d6ddaafe6da32f009106bf9a335147cb77 + md5: d5da976e963e70364b9e3ff270842b9f + depends: + - brotli + - munkres + - python >=3.10 + - unicodedata2 >=15.1.0 + track_features: + - fonttools_no_compile + license: MIT + license_family: MIT + size: 834764 + timestamp: 1765632669874 +- conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + sha256: bf8e4dffe46f7d25dc06f31038cacb01672c47b9f45201f065b0f4d00ab0a83e + md5: 4afc585cd97ba8a23809406cd8a9eda8 + depends: + - libfreetype 2.14.1 ha770c72_0 + - libfreetype6 2.14.1 h73754d4_0 + license: GPL-2.0-only OR FTL + size: 173114 + timestamp: 1757945422243 +- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.1-h694c41f_0.conda + sha256: 9f8282510db291496e89618fc66a58a1124fe7a6276fbd57ed18c602ce2576e9 + md5: ca641fdf8b7803f4b7212b6d66375930 + depends: + - libfreetype 2.14.1 h694c41f_0 + - libfreetype6 2.14.1 h6912278_0 + license: GPL-2.0-only OR FTL + size: 173969 + timestamp: 1757945973505 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + sha256: 14427aecd72e973a73d5f9dfd0e40b6bc3791d253de09b7bf233f6a9a190fd17 + md5: 1ec9a1ee7a2c9339774ad9bb6fe6caec + depends: + - libfreetype 2.14.1 hce30654_0 + - libfreetype6 2.14.1 h6da58f4_0 + license: GPL-2.0-only OR FTL + size: 173399 + timestamp: 1757947175403 +- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda + sha256: a9b3313edea0bf14ea6147ea43a1059d0bf78771a1336d2c8282891efc57709a + md5: d69c21967f35eb2ce7f1f85d6b6022d3 + depends: + - libfreetype 2.14.1 h57928b3_0 + - libfreetype6 2.14.1 hdbac1cb_0 + license: GPL-2.0-only OR FTL + size: 184553 + timestamp: 1757946164012 +- conda: https://conda.anaconda.org/conda-forge/noarch/frozenlist-1.7.0-pyhf298e5d_0.conda + sha256: d065c6c76ba07c148b07102f89fd14e39e4f0b2c022ad671bbef8fda9431ba1b + md5: 3998c9592e3db2f6809e4585280415f4 + depends: + - python >=3.9 + track_features: + - frozenlist_no_compile + license: Apache-2.0 + license_family: APACHE + size: 18952 + timestamp: 1752167260183 +- conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.12.0-pyhd8ed1ab_0.conda + sha256: 64a4ed910e39d96cd590d297982b229c57a08e70450d489faa34fd2bec36dbcc + md5: a3b9510e2491c20c7fc0f5e730227fbb + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 147391 + timestamp: 1764784920938 +- conda: https://conda.anaconda.org/conda-forge/noarch/gast-0.4.0-pyh9f0ad1d_0.tar.bz2 + sha256: 0f7eff1aab91ec3ac2eb3bbace1297fd71c16d235503222c3da89428ac562a63 + md5: 42323c77b73462199fca93bc8ac9279d + depends: + - python + license: BSD-3-Clause + license_family: BSD + size: 12325 + timestamp: 1596839771978 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + sha256: 6c33bf0c4d8f418546ba9c250db4e4221040936aef8956353bc764d4877bc39a + md5: d411fc29e338efb48c5fd4576d71d881 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-3-Clause + license_family: BSD + size: 119654 + timestamp: 1726600001928 +- conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda + sha256: c0bea66f71a6f4baa8d4f0248e17f65033d558d9e882c0af571b38bcca3e4b46 + md5: a26de8814083a6971f14f9c8c3cb36c2 + depends: + - __osx >=10.13 + - libcxx >=17 + license: BSD-3-Clause + license_family: BSD + size: 84946 + timestamp: 1726600054963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda + sha256: fd56ed8a1dab72ab90d8a8929b6f916a6d9220ca297ff077f8f04c5ed3408e20 + md5: 57a511a5905caa37540eb914dfcbf1fb + depends: + - __osx >=11.0 + - libcxx >=17 + license: BSD-3-Clause + license_family: BSD + size: 82090 + timestamp: 1726600145480 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2 + md5: ff862eebdfeb2fd048ae9dc92510baca + depends: + - gflags >=2.2.2,<2.3.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-3-Clause + license_family: BSD + size: 143452 + timestamp: 1718284177264 +- conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda + sha256: dd56547db8625eb5c91bb0a9fbe8bd6f5c7fbf5b6059d46365e94472c46b24f9 + md5: 06cf91665775b0da395229cd4331b27d + depends: + - __osx >=10.13 + - gflags >=2.2.2,<2.3.0a0 + - libcxx >=16 + license: BSD-3-Clause + license_family: BSD + size: 117017 + timestamp: 1718284325443 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda + sha256: 9fc77de416953aa959039db72bc41bfa4600ae3ff84acad04a7d0c1ab9552602 + md5: fef68d0a95aa5b84b5c1a4f6f3bf40e1 + depends: + - __osx >=11.0 + - gflags >=2.2.2,<2.3.0a0 + - libcxx >=16 + license: BSD-3-Clause + license_family: BSD + size: 112215 + timestamp: 1718284365403 +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c + md5: 2cd94587f3a401ae05e03a6caf09539d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: LGPL-2.0-or-later + license_family: LGPL + size: 99596 + timestamp: 1755102025473 +- conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda + sha256: 5f1714b07252f885a62521b625898326ade6ca25fbc20727cfe9a88f68a54bfd + md5: b785694dd3ec77a011ccf0c24725382b + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.0-or-later + license_family: LGPL + size: 96336 + timestamp: 1755102441729 +- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + sha256: f64b68148c478c3bfc8f8d519541de7d2616bf59d44485a5271041d40c061887 + md5: 4b69232755285701bc86a5afe4d9933a + depends: + - python >=3.9 + - typing_extensions + license: MIT + license_family: MIT + size: 37697 + timestamp: 1745526482242 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.15.1-nompi_py314hc32fe06_101.conda + sha256: 36f836d9212fda38e09e3d7c1e694996112456c1b1da1b1bb6c0072321559082 + md5: d5f709371311de1343675757978a50d5 + depends: + - __glibc >=2.17,<3.0.a0 + - cached-property + - hdf5 >=1.14.6,<1.14.7.0a0 + - libgcc >=14 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 1291384 + timestamp: 1764016672412 +- conda: https://conda.anaconda.org/conda-forge/osx-64/h5py-3.15.1-nompi_py314hf613b1f_101.conda + sha256: 7df694dadfe5dae733617d27f31b392148b42f0068766c4d4c3dc6d8dd1d709d + md5: 60a46376d9f6bc9f84b7327a200d6753 + depends: + - __osx >=10.13 + - cached-property + - hdf5 >=1.14.6,<1.14.7.0a0 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 1162048 + timestamp: 1764016999757 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.15.1-nompi_py314h1c8d760_101.conda + sha256: 1add46ebafbab228bbb2db615740b5763f139f65aa110a2996f08695b5fed7d3 + md5: 81e42cd3fcea0984435a3c21857e0d50 + depends: + - __osx >=11.0 + - cached-property + - hdf5 >=1.14.6,<1.14.7.0a0 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 1157833 + timestamp: 1764017977683 +- conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.15.1-nompi_py314hc249e69_101.conda + sha256: 7a05562f2cf290b50de67eefef6ea704ec2356551a2683b767c511680562eeaa + md5: 4019722f94eac6540faf77d20cc4190d + depends: + - cached-property + - hdf5 >=1.14.6,<1.14.7.0a0 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 1059478 + timestamp: 1764017347777 +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-12.2.0-h15599e2_0.conda + sha256: 6bd8b22beb7d40562b2889dc68232c589ff0d11a5ad3addd41a8570d11f039d9 + md5: b8690f53007e9b5ee2c2178dd4ac778c + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libgcc >=14 + - libglib >=2.86.1,<3.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + size: 2411408 + timestamp: 1762372726141 +- conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-12.2.0-h5f2951f_0.conda + sha256: db73714c7f7e0c47b3b9db9302a83f2deb6f8d6081716d35710ef3c6756af6c3 + md5: e798ef748fc564e42f381d3d276850f0 + depends: + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libglib >=2.86.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 1138900 + timestamp: 1762373626704 +- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h1b119a7_104.conda + sha256: 454e9724b322cee277abd7acf4f8d688e9c4ded006b6d5bc9fcc2a1ff907d27a + md5: 0857f4d157820dcd5625f61fdfefb780 + depends: + - __glibc >=2.17,<3.0.a0 + - libaec >=1.1.4,<2.0a0 + - libcurl >=8.17.0,<9.0a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 3720961 + timestamp: 1764771748126 +- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc1508a4_104.conda + sha256: aed322f0e8936960332305fbc213831a3cd301db5ea22c06e1293d953ddec563 + md5: 9425a5c53febdf71696aed291586d038 + depends: + - __osx >=10.13 + - libaec >=1.1.4,<2.0a0 + - libcurl >=8.17.0,<9.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 3528765 + timestamp: 1764773824647 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_hd3baa01_104.conda + sha256: 3cd591334a838b127dfe8a626f38241892063eac8873abb93255962c71155533 + md5: 5a1cbaf2349dd2e6dd6cfaab378de51b + depends: + - __osx >=11.0 + - libaec >=1.1.4,<2.0a0 + - libcurl >=8.17.0,<9.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 3292042 + timestamp: 1764771887501 +- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_h89f0904_104.conda + sha256: cc948149f700033ff85ce4a1854edf6adcb5881391a3df5c40cbe2a793dd9f81 + md5: 9cc4a5567d46c7fcde99563e86522882 + depends: + - libaec >=1.1.4,<2.0a0 + - libcurl >=8.17.0,<9.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 2028777 + timestamp: 1764771527382 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 30731 + timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda + sha256: 8027e436ad59e2a7392f6036392ef9d6c223798d8a1f4f12d5926362def02367 + md5: cf25bfddbd3bc275f3d3f9936cee1dd3 + depends: + - python >=3.9 + - six >=1.9 + - webencodings + license: MIT + license_family: MIT + size: 94853 + timestamp: 1734075276288 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 4f14640d58e2cc0aa0819d9d8ba125bb + depends: + - python >=3.9 + - h11 >=0.16 + - h2 >=3,<5 + - sniffio 1.* + - anyio >=4.0,<5.0 + - certifi + - python + license: BSD-3-Clause + license_family: BSD + size: 49483 + timestamp: 1745602916758 +- conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.7.1-py314h5bd0f2a_1.conda + sha256: 91bfdf1dad0fa57efc2404ca00f5fee8745ad9b56ec1d0df298fd2882ad39806 + md5: 067a52c66f453b97771650bbb131e2b5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 99037 + timestamp: 1762504051423 +- conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.7.1-py314h6482030_1.conda + sha256: 28f63c3a15b60d11f81b8b291776a804ff0d5b7cb2d56a3e8cd9c2c6f21258f3 + md5: defa8d782c5ae29fedab67e33a10df68 + depends: + - __osx >=10.13 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 90328 + timestamp: 1762504303446 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.7.1-py314h0612a62_1.conda + sha256: 042343211aafabab79120d0deda73358ddd3cb61b9ad55307108a275976fccfa + md5: 0ca03669a236fee8ce414e166d0bbf23 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 90384 + timestamp: 1762504632522 +- conda: https://conda.anaconda.org/conda-forge/win-64/httptools-0.7.1-py314h5a2d7ad_1.conda + sha256: 8377e165207fcd24844b7e62ed68b9da3573c0a7b1c9998736d50cf1d6324afc + md5: edde0f16d9733e829901f0c9755e5d22 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 75701 + timestamp: 1762504456801 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 + md5: d6989ead454181f4f9bc987d3dc4e285 + depends: + - anyio + - certifi + - httpcore 1.* + - idna + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 63082 + timestamp: 1733663449209 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + md5: 8b189310083baabfb622af68fd9d3ae3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + size: 12129203 + timestamp: 1720853576813 +- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + sha256: 2e64307532f482a0929412976c8450c719d558ba20c0962832132fd0d07ba7a7 + md5: d68d48a3060eb5abdc1cdc8e2a3a5966 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 11761697 + timestamp: 1720853679409 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 + md5: 5eb22c1d7b3fc4abb50d92d621583137 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 11857802 + timestamp: 1720853997952 +- conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + sha256: 1d04369a1860a1e9e371b9fc82dd0092b616adcf057d6c88371856669280e920 + md5: 8579b6bb8d18be7c0b27fb08adeeeb40 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 14544252 + timestamp: 1720853966338 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + sha256: ae89d0299ada2a3162c2614a9d26557a92aa6a77120ce142f8e0109bbf0342b0 + md5: 53abe63df7e10a6ba605dc5f9f961d36 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 50721 + timestamp: 1760286526795 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745 + md5: 63ccfdc3a3ce25b027b8767eb722fca8 + depends: + - python >=3.9 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + size: 34641 + timestamp: 1747934053147 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80 + md5: c85c76dc67d75619a92f51dfbce06992 + depends: + - python >=3.9 + - zipp >=3.1.0 + constrains: + - importlib-resources >=6.5.2,<6.5.3.0a0 + license: Apache-2.0 + license_family: APACHE + size: 33781 + timestamp: 1736252433366 +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 13387 + timestamp: 1760831448842 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipydatagrid-1.4.0-pyhcf101f3_2.conda + sha256: 05d121a997a7911e2644f5a58a62d24c8ae87d0e715f00ac537895fbc5c895d4 + md5: 12234484af2c95fca5911cd4b90ba30a + depends: + - bqplot >=0.11.6 + - ipywidgets >=7.6,<9 + - pandas >=1.3.5 + - py2vega >=0.5 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + size: 681089 + timestamp: 1755954998991 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda + sha256: b5f7eaba3bb109be49d00a0a8bda267ddf8fa66cc1b54fc5944529ed6f3e8503 + md5: 1849eec35b60082d2bd66b4e36dec2b6 + depends: + - appnope + - __osx + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.0.0 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.2 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + size: 132289 + timestamp: 1761567969884 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda + sha256: 75e42103bc3350422896f727041e24767795b214a20f50bf39c371626b8aae8b + md5: f22cb16c5ad68fd33d0f65c8739b6a06 + depends: + - python + - __win + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.0.0 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.2 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + size: 132418 + timestamp: 1761567966860 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda + sha256: a9d6b74115dbd62e19017ff8fa4885b07b5164427f262cc15b5307e5aaf3ee73 + md5: c6f63cfe66adaa5650788e3106b6683a + depends: + - python + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.0.0 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.2 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + size: 133820 + timestamp: 1761567932044 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda + sha256: 8a72c9945dc4726ee639a9652b622ae6b03f3eba0e16a21d1c6e5bfb562f5a3f + md5: fd77b1039118a3e8ce1070ac8ed45bae + depends: + - __unix + - pexpect >4.3 + - decorator >=4.3.2 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.1 + - matplotlib-inline >=0.1.5 + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.11.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + size: 645145 + timestamp: 1764766793792 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyhe2676ad_0.conda + sha256: 7c6974866caaccb7eb827bb70523205601c10b8e89d724b193cb4e818f4db2bd + md5: 1bc380b3fd0ea85afdfe0aba5b6b7398 + depends: + - __win + - colorama >=0.4.4 + - decorator >=4.3.2 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.1 + - matplotlib-inline >=0.1.5 + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.11.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + size: 644388 + timestamp: 1764766840112 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 + md5: bd80ba060603cc228d9d81c257093119 + depends: + - pygments + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 13993 + timestamp: 1737123723464 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + sha256: 6bb58afb7eabc8b4ac0c7e92707fb498313cc0164cf04e7ba1090dbf49af514b + md5: d68e3f70d1f068f1b66d94822fdc644e + depends: + - comm >=0.1.3 + - ipython >=6.1.0 + - jupyterlab_widgets >=3.0.15,<3.1.0 + - python >=3.10 + - traitlets >=4.3.1 + - widgetsnbextension >=4.0.14,<4.1.0 + license: BSD-3-Clause + license_family: BSD + size: 114376 + timestamp: 1762040524661 +- conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda + sha256: 3cc991f0f09dfd00d2626e745ba68da03e4f1dcbb7b36dd20f7a7373643cd5d5 + md5: d59568bad316413c89831456e691de29 + depends: + - python >=3.10 + - more-itertools + - python + license: MIT + license_family: MIT + size: 14831 + timestamp: 1767294269456 +- conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.1.0-pyhcf101f3_0.conda + sha256: 04c9f919dcc9edd18f748c47d809479812429af27c43c5562a861df22d5bda6a + md5: f34ec3aa0ea911a038d973d97603faf3 + depends: + - python >=3.10 + - backports.tarfile + - python + license: MIT + license_family: MIT + size: 15566 + timestamp: 1768299702258 +- conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda + sha256: 6a91447b3bb4d7ae94cc0d77ed12617796629aee11111efe7ea43cbd0e113bda + md5: aa83cc08626bf6b613a3103942be8951 + depends: + - python >=3.10 + - more-itertools + - python + license: MIT + license_family: MIT + size: 18744 + timestamp: 1767294193246 +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + size: 843646 + timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda + sha256: 00d37d85ca856431c67c8f6e890251e7cc9e5ef3724a0302b8d4a101f22aa27f + md5: b4b91eb14fbe2f850dd2c5fc20676c0d + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 40015 + timestamp: 1740828380668 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + sha256: 3d2f20ee7fd731e3ff55c189db9c43231bc8bde957875817a609c227bcb295c6 + md5: 972bdca8f30147135f951847b30399ea + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 23708 + timestamp: 1733229244590 +- conda: https://conda.anaconda.org/conda-forge/noarch/jplephem-2.23-pyha4b2019_0.conda + sha256: 396678bcf99f925380e90b6ec4f0a8b3c6dc4c06a8e89ce777375ae44016f38e + md5: c778493b6112f330d4aa9569954119d3 + depends: + - numpy + - python >=3.9 + license: MIT + license_family: MIT + size: 40807 + timestamp: 1750675277409 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.7.0-pyhcf101f3_0.conda + sha256: 6aa61417547b925de64905b7a4da7c98e0b355f48a7b21bdbef438f8950ee74e + md5: 1b0397a7b1fbffa031feb690b5fd0277 + depends: + - jupyter_core >=5.1 + - python >=3.10 + - python-dateutil >=2.8.2 + - pyzmq >=25.0 + - tornado >=6.4.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + size: 111367 + timestamp: 1765375773813 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc + md5: a8db462b01221e9f5135be466faeb3e0 + depends: + - __win + - pywin32 + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + size: 64679 + timestamp: 1760643889625 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 + depends: + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + sha256: 5c03de243d7ae6247f39a402f4785d95e61c3be79ef18738e8f17155585d31a8 + md5: dbf8b81974504fa51d34e436ca7ef389 + depends: + - python >=3.10 + - python + constrains: + - jupyterlab >=3,<5 + license: BSD-3-Clause + license_family: BSD + size: 216779 + timestamp: 1762267481404 +- conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh534df25_0.conda + sha256: 9def5c6fb3b3b4952a4f6b55a019b5c7065b592682b84710229de5a0b73f6364 + md5: c88f9579d08eb4031159f03640714ce3 + depends: + - __osx + - importlib-metadata >=4.11.4 + - importlib_resources + - jaraco.classes + - jaraco.context + - jaraco.functools + - python >=3.10 + license: MIT + license_family: MIT + size: 37924 + timestamp: 1763320995459 +- conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyh7428d3b_0.conda + sha256: ed76a29fd1dbaf1bb24058191386618315ab9e35da9ef9a76da232cd6885165b + md5: e91b0f2040c580527ccc54665aa7cdba + depends: + - __win + - importlib-metadata >=4.11.4 + - importlib_resources + - jaraco.classes + - jaraco.context + - jaraco.functools + - python >=3.10 + - pywin32-ctypes >=0.2.0 + license: MIT + license_family: MIT + size: 38153 + timestamp: 1763320939579 +- conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.7.0-pyha804496_0.conda + sha256: 010718b1b1a35ce72782d38e6d6b9495d8d7d0dbea9a3e42901d030ff2189545 + md5: 9eeb0eaf04fa934808d3e070eebbe630 + depends: + - __linux + - importlib-metadata >=4.11.4 + - importlib_resources + - jaraco.classes + - jaraco.context + - jaraco.functools + - jeepney >=0.4.2 + - python >=3.10 + - secretstorage >=3.2 + license: MIT + license_family: MIT + size: 37717 + timestamp: 1763320674488 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py314h97ea11e_2.conda + sha256: a707d08c095d02148201f2da9fba465054fb750e33117e215892a4fefcc1b54a + md5: 57f1ce4f7ba6bcd460be8f83c8f04c69 + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 78071 + timestamp: 1762488742381 +- conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.9-py314hf3ac25a_2.conda + sha256: a9d220022002611515de26be256a08abcf046bf8e66a7d95d22cdef0842b0f84 + md5: 28a77c52c425fa9c6d914c609c626b1a + depends: + - python + - libcxx >=19 + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 69742 + timestamp: 1762488879086 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py314h42813c9_2.conda + sha256: c4d7e6653d343e768110ec77ac1c6c89f313f77a19a1f2cd60b7c7b8b0758bdf + md5: 9aa431bf603c231e8c77a1b0842a85ed + depends: + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - libcxx >=19 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 68534 + timestamp: 1762489024029 +- conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py314hf309875_2.conda + sha256: ded907ab1ce24abcff20bc239e770ae7ef4cff6fdcfb8cc24ca59ebe736a1d3f + md5: e9d93271b021332f5492ff5478601614 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 73670 + timestamp: 1762488752873 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 + md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + depends: + - keyutils >=1.6.1,<2.0a0 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + size: 1370023 + timestamp: 1719463201255 +- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda + sha256: 83b52685a4ce542772f0892a0f05764ac69d57187975579a0835ff255ae3ef9c + md5: d4765c524b1d91567886bde656fb514b + depends: + - __osx >=10.13 + - libcxx >=16 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + size: 1185323 + timestamp: 1719463492984 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b + md5: c6dc8a0fdec13a0565936655c33069a1 + depends: + - __osx >=11.0 + - libcxx >=16 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + size: 1155530 + timestamp: 1719463474401 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 + md5: 31aec030344e962fbd7dbbbbd68e60a9 + depends: + - openssl >=3.3.1,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 712034 + timestamp: 1719463874284 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda + sha256: d6a61830a354da022eae93fa896d0991385a875c6bba53c82263a289deda9db8 + md5: 000e85703f0fd9594c81710dd5066471 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libtiff >=4.7.0,<4.8.0a0 + license: MIT + license_family: MIT + size: 248046 + timestamp: 1739160907615 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.17-h72f5680_0.conda + sha256: bcb81543e49ff23e18dea79ef322ab44b8189fb11141b1af99d058503233a5fc + md5: bf210d0c63f2afb9e414a858b79f0eaa + depends: + - __osx >=10.13 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libtiff >=4.7.0,<4.8.0a0 + license: MIT + license_family: MIT + size: 226001 + timestamp: 1739161050843 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda + sha256: 310a62c2f074ebd5aa43b3cd4b00d46385ce680fa2132ecee255a200e2d2f15f + md5: 92a61fd30b19ebd5c1621a5bfe6d8b5f + depends: + - __osx >=11.0 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libtiff >=4.7.0,<4.8.0a0 + license: MIT + license_family: MIT + size: 212125 + timestamp: 1739161108467 +- conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda + sha256: 7712eab5f1a35ca3ea6db48ead49e0d6ac7f96f8560da8023e61b3dbe4f3b25d + md5: 3538827f77b82a837fa681a4579e37a1 + depends: + - libjpeg-turbo >=3.0.0,<4.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 510641 + timestamp: 1739161381270 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda + sha256: 9e191baf2426a19507f1d0a17be0fdb7aa155cdf0f61d5a09c808e0a69464312 + md5: a6abd2796fc332536735f68ba23f7901 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45 + license: GPL-3.0-only + license_family: GPL + size: 725545 + timestamp: 1764007826689 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + sha256: 412381a43d5ff9bbed82cd52a0bbca5b90623f62e41007c9c42d3870c60945ff + md5: 9344155d33912347b37f0ae6c410a835 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: Apache-2.0 + license_family: Apache + size: 264243 + timestamp: 1745264221534 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hcca01a6_1.conda + sha256: cc1f1d7c30aa29da4474ec84026ec1032a8df1d7ec93f4af3b98bb793d01184e + md5: 21f765ced1a0ef4070df53cb425e1967 + depends: + - __osx >=10.13 + - libcxx >=18 + license: Apache-2.0 + license_family: Apache + size: 248882 + timestamp: 1745264331196 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + sha256: 12361697f8ffc9968907d1a7b5830e34c670e4a59b638117a2cdfed8f63a38f8 + md5: a74332d9b60b62905e3d30709df08bf1 + depends: + - __osx >=11.0 + - libcxx >=18 + license: Apache-2.0 + license_family: Apache + size: 188306 + timestamp: 1745264362794 +- conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + sha256: 868a3dff758cc676fa1286d3f36c3e0101cca56730f7be531ab84dc91ec58e9d + md5: c1b81da6d29a14b542da14a36c9fbf3f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: Apache + size: 164701 + timestamp: 1745264384716 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda + sha256: dcd1429a1782864c452057a6c5bc1860f2b637dc20a2b7e6eacd57395bbceff8 + md5: 83b160d4da3e1e847bf044997621ed63 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + constrains: + - libabseil-static =20250512.1=cxx17* + - abseil-cpp =20250512.1 + license: Apache-2.0 + license_family: Apache + size: 1310612 + timestamp: 1750194198254 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + sha256: a878efebf62f039a1f1733c1e150a75a99c7029ece24e34efdf23d56256585b1 + md5: ddf1acaed2276c7eb9d3c76b49699a11 + depends: + - __osx >=10.13 + - libcxx >=18 + constrains: + - abseil-cpp =20250512.1 + - libabseil-static =20250512.1=cxx17* + license: Apache-2.0 + license_family: Apache + size: 1162435 + timestamp: 1750194293086 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda + sha256: 7f0ee9ae7fa2cf7ac92b0acf8047c8bac965389e48be61bf1d463e057af2ea6a + md5: 360dbb413ee2c170a0a684a33c4fc6b8 + depends: + - __osx >=11.0 + - libcxx >=18 + constrains: + - libabseil-static =20250512.1=cxx17* + - abseil-cpp =20250512.1 + license: Apache-2.0 + license_family: Apache + size: 1174081 + timestamp: 1750194620012 +- conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250512.1-cxx17_habfad5f_0.conda + sha256: 78790771f44e146396d9ae92efbe1022168295afd8d174f653a1fa16f0f0fa32 + md5: d6a4cd236fc1c69a1cfc9698fb5e391f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.42.34438 + constrains: + - libabseil-static =20250512.1=cxx17* + - abseil-cpp =20250512.1 + license: Apache-2.0 + license_family: Apache + size: 1615210 + timestamp: 1750194549591 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + sha256: 410ab78fe89bc869d435de04c9ffa189598ac15bb0fe1ea8ace8fb1b860a2aa3 + md5: 01ba04e414e47f95c03d6ddd81fd37be + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD + size: 36825 + timestamp: 1749993532943 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libaec-1.1.4-ha6bc127_0.conda + sha256: f4fe00ef0df58b670696c62f2ec3f6484431acbf366ecfbcb71141c81439e331 + md5: 1a768b826dfc68e07786788d98babfc3 + depends: + - __osx >=10.13 + - libcxx >=18 + license: BSD-2-Clause + license_family: BSD + size: 30034 + timestamp: 1749993664561 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda + sha256: 0ea6b73b3fb1511615d9648186a7409e73b7a8d9b3d890d39df797730e3d1dbb + md5: 8ed0f86b7a5529b98ec73b43a53ce800 + depends: + - __osx >=11.0 + - libcxx >=18 + license: BSD-2-Clause + license_family: BSD + size: 30173 + timestamp: 1749993648288 +- conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda + sha256: 0be89085effce9fdcbb6aea7acdb157b18793162f68266ee0a75acf615d4929b + md5: 85a2bed45827d77d5b308cb2b165404f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + size: 33847 + timestamp: 1749993666162 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-22.0.0-hb6ed5f4_6_cpu.conda + build_number: 6 + sha256: bab5fcb86cf28a3de65127fbe61ed9194affc1cf2d9b60a9e09af8a8b96b93e3 + md5: fbaa3742ccca0f7096216c0832137b72 + depends: + - __glibc >=2.17,<3.0.a0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - aws-sdk-cpp >=1.11.606,<1.11.607.0a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-identity-cpp >=1.13.2,<1.13.3.0a0 + - azure-storage-blobs-cpp >=12.15.0,<12.15.1.0a0 + - azure-storage-files-datalake-cpp >=12.13.0,<12.13.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libgcc >=14 + - libgoogle-cloud >=2.39.0,<2.40.0a0 + - libgoogle-cloud-storage >=2.39.0,<2.40.0a0 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.2.1,<2.2.2.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - arrow-cpp <0.0a0 + - apache-arrow-proc =*=cpu + - parquet-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + size: 6324546 + timestamp: 1765381265473 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-22.0.0-hd1700fa_4_cpu.conda + build_number: 4 + sha256: 82d764b803ed198123c77ec954770deec0e477e003d3d906eb8eda5260f88e24 + md5: 9c95de09ac58d37d8cfbaa54b7174ee5 + depends: + - __osx >=11.0 + - aws-crt-cpp >=0.35.2,<0.35.3.0a0 + - aws-sdk-cpp >=1.11.606,<1.11.607.0a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-identity-cpp >=1.13.2,<1.13.3.0a0 + - azure-storage-blobs-cpp >=12.15.0,<12.15.1.0a0 + - azure-storage-files-datalake-cpp >=12.13.0,<12.13.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcxx >=19 + - libgoogle-cloud >=2.39.0,<2.40.0a0 + - libgoogle-cloud-storage >=2.39.0,<2.40.0a0 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.2.1,<2.2.2.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - apache-arrow-proc =*=cpu + - parquet-cpp <0.0a0 + - arrow-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + size: 4266919 + timestamp: 1763229988804 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-22.0.0-he6e817a_6_cpu.conda + build_number: 6 + sha256: 77d82f2d6787ec0300da0ad683d30eccc71723665c5dc4e7c6e4ca9b7955f599 + md5: b972d880c503c30ee178489ec76bbd6d + depends: + - __osx >=11.0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - aws-sdk-cpp >=1.11.606,<1.11.607.0a0 + - azure-core-cpp >=1.16.1,<1.16.2.0a0 + - azure-identity-cpp >=1.13.2,<1.13.3.0a0 + - azure-storage-blobs-cpp >=12.15.0,<12.15.1.0a0 + - azure-storage-files-datalake-cpp >=12.13.0,<12.13.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcxx >=19 + - libgoogle-cloud >=2.39.0,<2.40.0a0 + - libgoogle-cloud-storage >=2.39.0,<2.40.0a0 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.2.1,<2.2.2.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - parquet-cpp <0.0a0 + - arrow-cpp <0.0a0 + - apache-arrow-proc =*=cpu + license: Apache-2.0 + license_family: APACHE + size: 4160249 + timestamp: 1765382560379 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-22.0.0-h89d7da9_6_cpu.conda + build_number: 6 + sha256: 5469cd02381c6760893fc2bcfda9cfb7a2c248527132964d36740e5789648133 + md5: e9fe1ee5e997417347e1ee312af94092 + depends: + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - aws-sdk-cpp >=1.11.606,<1.11.607.0a0 + - bzip2 >=1.0.8,<2.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl >=8.17.0,<9.0a0 + - libgoogle-cloud >=2.39.0,<2.40.0a0 + - libgoogle-cloud-storage >=2.39.0,<2.40.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.2.1,<2.2.2.0a0 + - snappy >=1.2.2,<1.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - parquet-cpp <0.0a0 + - apache-arrow-proc =*=cpu + - arrow-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + size: 3965279 + timestamp: 1765381971425 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-22.0.0-h635bf11_6_cpu.conda + build_number: 6 + sha256: b7e013502eb6dbb59bf58c34b83ed4e7bbcc32ee37600016d862f0bb21a6dc5a + md5: 5a8f878ca313083960ab819a009848b3 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 22.0.0 hb6ed5f4_6_cpu + - libarrow-compute 22.0.0 h8c2c5c3_6_cpu + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + size: 585860 + timestamp: 1765381484672 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-22.0.0-h2db2d7d_4_cpu.conda + build_number: 4 + sha256: ebc47c938c7e3af8af35cb6ad92a2ff4fcaba3776bf3f0dc8690e3c168035b0b + md5: f9e754e716ed279c88f25d17fc6b5764 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 hd1700fa_4_cpu + - libarrow-compute 22.0.0 h7751554_4_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 551790 + timestamp: 1763230587607 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-22.0.0-hc317990_6_cpu.conda + build_number: 6 + sha256: 3250653194b95fc30785f7fc394381318ecc3afb500884967b6d736349b135fe + md5: f17f28aba732a290919eecdec17677d9 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 he6e817a_6_cpu + - libarrow-compute 22.0.0 h75845d1_6_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 523683 + timestamp: 1765383066107 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-22.0.0-h7d8d6a5_6_cpu.conda + build_number: 6 + sha256: bea322b50e5db84ba1de28a70e0da9ebb44a8d525a0ffb5facc2fa0b8332c3e5 + md5: bbef682dd3d8f686faad9f1a94b3d9ae + depends: + - libarrow 22.0.0 h89d7da9_6_cpu + - libarrow-compute 22.0.0 h2db994a_6_cpu + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 451321 + timestamp: 1765382291986 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-22.0.0-h8c2c5c3_6_cpu.conda + build_number: 6 + sha256: 0cd08dd11263105e2bf45514e08f8e4a59fac41a80a82f17540e047242835872 + md5: d2cd924b5f451a7c258001cb1c14155d + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 22.0.0 hb6ed5f4_6_cpu + - libgcc >=14 + - libre2-11 >=2025.8.12 + - libstdcxx >=14 + - libutf8proc >=2.11.2,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE + size: 2973397 + timestamp: 1765381343806 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-22.0.0-h7751554_4_cpu.conda + build_number: 4 + sha256: 0a27101d20f8e47dea60fb489d8904472e92da913660605d61f318352ac79163 + md5: 673d1c37cbbf9a99235337cbb6969dff + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 hd1700fa_4_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libre2-11 >=2025.8.12 + - libutf8proc >=2.11.0,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE + size: 2394943 + timestamp: 1763230184019 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-22.0.0-h75845d1_6_cpu.conda + build_number: 6 + sha256: 053d096e77464ea8da7c35ab167864bacac3590af304aa3368d09aba8cdf8af8 + md5: 51b139c330f194379c4271c91c9cd1c7 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 he6e817a_6_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libre2-11 >=2025.8.12 + - libutf8proc >=2.11.2,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE + size: 2155806 + timestamp: 1765382724366 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-compute-22.0.0-h2db994a_6_cpu.conda + build_number: 6 + sha256: f26d1d4752f847c11ed3202b1314b1729a52f1468b17dfd3174885db7e3e2dfe + md5: 922c36699625c3f49940337feeba8291 + depends: + - libarrow 22.0.0 h89d7da9_6_cpu + - libre2-11 >=2025.8.12 + - libutf8proc >=2.11.2,<2.12.0a0 + - re2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 1685242 + timestamp: 1765382093115 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-22.0.0-h635bf11_6_cpu.conda + build_number: 6 + sha256: d0321d8d82ccc55557ccb3119174179de3f282df68a6efe60f9c523bbf242a1f + md5: 579bdb829ab093d048e49a289d3c9883 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 22.0.0 hb6ed5f4_6_cpu + - libarrow-acero 22.0.0 h635bf11_6_cpu + - libarrow-compute 22.0.0 h8c2c5c3_6_cpu + - libgcc >=14 + - libparquet 22.0.0 h7376487_6_cpu + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + size: 584952 + timestamp: 1765381575560 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-22.0.0-h2db2d7d_4_cpu.conda + build_number: 4 + sha256: 31c410ca02fb477d8c19792ebb6b5f50144e510ed50a41be268fba6cca6a3417 + md5: 64d5722c982b89dabf848feb7edf97f9 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 hd1700fa_4_cpu + - libarrow-acero 22.0.0 h2db2d7d_4_cpu + - libarrow-compute 22.0.0 h7751554_4_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libparquet 22.0.0 habb56ca_4_cpu + - libprotobuf >=6.31.1,<6.31.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 533092 + timestamp: 1763230993273 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-22.0.0-hc317990_6_cpu.conda + build_number: 6 + sha256: ab07545a7f99cb8026b3bfe0f7f2c33d3204972fe1d5eb011adf2eb002277989 + md5: cf0d62de81a3a2b7afb723b4b629879a + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 he6e817a_6_cpu + - libarrow-acero 22.0.0 hc317990_6_cpu + - libarrow-compute 22.0.0 h75845d1_6_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libparquet 22.0.0 h0ac143b_6_cpu + - libprotobuf >=6.31.1,<6.31.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 520397 + timestamp: 1765383321028 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-22.0.0-h7d8d6a5_6_cpu.conda + build_number: 6 + sha256: 147e9f2092443bf4facda44323097d8a494b4930c2865996aa54e2d19a454d93 + md5: 974630001cbf61d4d94a7c7c142eade4 + depends: + - libarrow 22.0.0 h89d7da9_6_cpu + - libarrow-acero 22.0.0 h7d8d6a5_6_cpu + - libarrow-compute 22.0.0 h2db994a_6_cpu + - libparquet 22.0.0 h7051d1f_6_cpu + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 435881 + timestamp: 1765382430115 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-22.0.0-h3f74fd7_6_cpu.conda + build_number: 6 + sha256: a343378e20aaa27e955c1f84394f00668458b69f6eaf7efcf4b21a3f8f10e02a + md5: cfc7d2c5a81eb6de3100661a69de5f3d + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 hb6ed5f4_6_cpu + - libarrow-acero 22.0.0 h635bf11_6_cpu + - libarrow-dataset 22.0.0 h635bf11_6_cpu + - libgcc >=14 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + size: 487167 + timestamp: 1765381605708 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-22.0.0-h4653b8a_4_cpu.conda + build_number: 4 + sha256: ecc37e1e0faa308f7bed2e346a1b3aa2bae7155f6df2312f11ad25fe30731bd4 + md5: c2f7a8fec7db2ce12d5aaabeed2cedea + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 hd1700fa_4_cpu + - libarrow-acero 22.0.0 h2db2d7d_4_cpu + - libarrow-dataset 22.0.0 h2db2d7d_4_cpu + - libcxx >=19 + - libprotobuf >=6.31.1,<6.31.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 447573 + timestamp: 1763231087749 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-22.0.0-h144af7f_6_cpu.conda + build_number: 6 + sha256: f2181c286af7d0d4cf381976f100daf1ac84b9661975130adce4ce7a03025696 + md5: 58a5b39bc7d23fa938affe1bfc43c241 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 he6e817a_6_cpu + - libarrow-acero 22.0.0 hc317990_6_cpu + - libarrow-dataset 22.0.0 hc317990_6_cpu + - libcxx >=19 + - libprotobuf >=6.31.1,<6.31.2.0a0 + license: Apache-2.0 + license_family: APACHE + size: 458819 + timestamp: 1765383438751 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-substrait-22.0.0-hf865cc0_6_cpu.conda + build_number: 6 + sha256: 393a9bedc2424ea2335364de0be0de69f6dbcc456c893b70a9776975acd749d0 + md5: 01d0606bf4202d358a71545759223202 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 h89d7da9_6_cpu + - libarrow-acero 22.0.0 h7d8d6a5_6_cpu + - libarrow-dataset 22.0.0 h7d8d6a5_6_cpu + - libprotobuf >=6.31.1,<6.31.2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 364040 + timestamp: 1765382475732 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-4_h4a7cf45_openblas.conda + build_number: 4 + sha256: f35fee1eb3fe1a80b2c8473f145a830cf6f98c3b15b232b256b93d44bd9c93b3 + md5: 14ff9fdfbd8bd590fca383b995470711 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - liblapack 3.11.0 4*_openblas + - blas 2.304 openblas + - mkl <2026 + - libcblas 3.11.0 4*_openblas + - liblapacke 3.11.0 4*_openblas + license: BSD-3-Clause + license_family: BSD + size: 18529 + timestamp: 1764823833499 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-4_he492b99_openblas.conda + build_number: 4 + sha256: 293e5290eee6d9be5a817ba4e1830ba18b04be9d619c2bdffeacf8ba3b0bef8d + md5: fa78d175db3b07d8eb963558e1bd9228 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - mkl <2026 + - liblapack 3.11.0 4*_openblas + - libcblas 3.11.0 4*_openblas + - liblapacke 3.11.0 4*_openblas + - blas 2.304 openblas + license: BSD-3-Clause + license_family: BSD + size: 18702 + timestamp: 1764824607451 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-4_h51639a9_openblas.conda + build_number: 4 + sha256: db31cdcd24b9f4be562c37a780d6a665f5eddc88a97d59997e293d91c522ffc1 + md5: f5c7d8c3256cd95d5ec31afc24c9dd30 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - libcblas 3.11.0 4*_openblas + - blas 2.304 openblas + - liblapack 3.11.0 4*_openblas + - liblapacke 3.11.0 4*_openblas + - mkl <2026 + license: BSD-3-Clause + license_family: BSD + size: 18767 + timestamp: 1764824430403 +- conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-4_hf2e6a31_mkl.conda + build_number: 4 + sha256: 0c6ecdabcd3c5b92c7be68a65c30c29983040dd81f502d2e9ad3763fdbbabdef + md5: 97ec87aab53fb310e6c19cde2eec1de2 + depends: + - mkl >=2025.3.0,<2026.0a0 + constrains: + - liblapacke 3.11.0 4*_mkl + - libcblas 3.11.0 4*_mkl + - liblapack 3.11.0 4*_mkl + - blas 2.304 mkl + license: BSD-3-Clause + license_family: BSD + size: 67784 + timestamp: 1764824188313 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + sha256: 4c19b211b3095f541426d5a9abac63e96a5045e509b3d11d4f9482de53efe43b + md5: f157c098841474579569c85a60ece586 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 78854 + timestamp: 1764017554982 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 + md5: 006e7ddd8a110771134fcc4e1e3a6ffa + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 79443 + timestamp: 1764017945924 +- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + sha256: 5097303c2fc8ebf9f9ea9731520aa5ce4847d0be41764edd7f6dee2100b82986 + md5: 444b0a45bbd1cb24f82eedb56721b9c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 82042 + timestamp: 1764017799966 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + sha256: 729158be90ae655a4e0427fe4079767734af1f9b69ff58cf94ca6e8d4b3eb4b7 + md5: 63186ac7a8a24b3528b4b14f21c03f54 + depends: + - __osx >=10.13 + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT + size: 30835 + timestamp: 1764017584474 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf + md5: 079e88933963f3f149054eec2c487bc2 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + size: 29452 + timestamp: 1764017979099 +- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + sha256: 3239ce545cf1c32af6fffb7fc7c75cb1ef5b6ea8221c66c85416bb2d46f5cccb + md5: 450e3ae947fc46b60f1d8f8f318b40d4 + depends: + - libbrotlicommon 1.2.0 hfd05255_1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 34449 + timestamp: 1764017851337 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + sha256: 8ece7b41b6548d6601ac2c2cd605cf2261268fc4443227cc284477ed23fbd401 + md5: 12a58fd3fc285ce20cf20edf21a0ff8f + depends: + - __osx >=10.13 + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT + size: 310355 + timestamp: 1764017609985 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 + md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + size: 290754 + timestamp: 1764018009077 +- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda + sha256: 3226df6b7df98734440739f75527d585d42ca2bfe912fbe8d1954c512f75341a + md5: ccd93cfa8e54fd9df4e83dbe55ff6e8c + depends: + - libbrotlicommon 1.2.0 hfd05255_1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 252903 + timestamp: 1764017901735 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-4_h0358290_openblas.conda + build_number: 4 + sha256: 7abc88e2fdccddab27d2a889b9c9063df84a05766cc24828c9b5ca879f25c92c + md5: 25f5e5af61cee1ffedd9b4c9947d3af8 + depends: + - libblas 3.11.0 4_h4a7cf45_openblas + constrains: + - liblapack 3.11.0 4*_openblas + - blas 2.304 openblas + - liblapacke 3.11.0 4*_openblas + license: BSD-3-Clause + license_family: BSD + size: 18521 + timestamp: 1764823852735 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-4_h9b27e0a_openblas.conda + build_number: 4 + sha256: 2412cc96eda9455cdddc6221b023df738f4daef269007379d06cfe79cfd065be + md5: 4ebb29d020eb3c2c8ac9674d8cfa4a31 + depends: + - libblas 3.11.0 4_he492b99_openblas + constrains: + - liblapacke 3.11.0 4*_openblas + - liblapack 3.11.0 4*_openblas + - blas 2.304 openblas + license: BSD-3-Clause + license_family: BSD + size: 18690 + timestamp: 1764824633990 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-4_hb0561ab_openblas.conda + build_number: 4 + sha256: fd57f4c8863ac78f42c55ee68351c963fe14fb3d46575c6f236082076690dd0f + md5: be77be52a6f01b46b1eb9aa5270023cc + depends: + - libblas 3.11.0 4_h51639a9_openblas + constrains: + - liblapack 3.11.0 4*_openblas + - blas 2.304 openblas + - liblapacke 3.11.0 4*_openblas + license: BSD-3-Clause + license_family: BSD + size: 18722 + timestamp: 1764824449333 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-4_h2a3cdd5_mkl.conda + build_number: 4 + sha256: 4cd0f2ec9823995a74b73c0119201dcf9a28444bdc2f0a824dfa938b5bdd5601 + md5: 64410b46ecf6fdfd19eb1d124d9eb450 + depends: + - libblas 3.11.0 4_hf2e6a31_mkl + constrains: + - liblapacke 3.11.0 4*_mkl + - liblapack 3.11.0 4*_mkl + - blas 2.304 mkl + license: BSD-3-Clause + license_family: BSD + size: 68001 + timestamp: 1764824219221 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp21.1-21.1.7-default_h99862b1_1.conda + sha256: ce8b8464b1230dd93d2b5a2646d2c80639774c9e781097f041581c07b83d4795 + md5: d3042ebdaacc689fd1daa701885fc96c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libllvm21 >=21.1.7,<21.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 21055642 + timestamp: 1764816319608 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.7-default_h746c552_1.conda + sha256: a9bcd5fc463ddf088077eceaf314d560af347d10c4d92ca3177fa313a79a6e46 + md5: 66508e5f84c3dc9af1a0a62694325ef2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libllvm21 >=21.1.7,<21.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 12347100 + timestamp: 1764816644936 +- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-21.1.7-default_ha2db4b5_1.conda + sha256: 9153b722591aac572b2384daac7f5071d59b746239e6d5b74b06844e49339ec7 + md5: 065bcc5d1a29de06d4566b7b9ac89882 + depends: + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 28995533 + timestamp: 1764820055107 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5 + md5: c965a5aa0d5c1c37ffc62dff36e28400 + depends: + - libgcc-ng >=9.4.0 + - libstdcxx-ng >=9.4.0 + license: BSD-3-Clause + license_family: BSD + size: 20440 + timestamp: 1633683576494 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2 + sha256: 3043869ac1ee84554f177695e92f2f3c2c507b260edad38a0bf3981fce1632ff + md5: 23d6d5a69918a438355d7cbc4c3d54c9 + depends: + - libcxx >=11.1.0 + license: BSD-3-Clause + license_family: BSD + size: 20128 + timestamp: 1633683906221 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + sha256: 58477b67cc719060b5b069ba57161e20ba69b8695d154a719cb4b60caf577929 + md5: 32bd82a6a625ea6ce090a81c3d34edeb + depends: + - libcxx >=11.1.0 + license: BSD-3-Clause + license_family: BSD + size: 18765 + timestamp: 1633683992603 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcrc32c-1.1.2-h0e60522_0.tar.bz2 + sha256: 75e60fbe436ba8a11c170c89af5213e8bec0418f88b7771ab7e3d9710b70c54e + md5: cd4cc2d0c610c8cb5419ccc979f2d6ce + depends: + - vc >=14.1,<15.0a0 + - vs2015_runtime >=14.16.27012 + license: BSD-3-Clause + license_family: BSD + size: 25694 + timestamp: 1633684287072 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + sha256: cb83980c57e311783ee831832eb2c20ecb41e7dee6e86e8b70b8cef0e43eab55 + md5: d4a250da4737ee127fb1fa6452a9002e + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: Apache-2.0 + license_family: Apache + size: 4523621 + timestamp: 1749905341688 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda + sha256: 2d7be2fe0f58a0945692abee7bb909f8b19284b518d958747e5ff51d0655c303 + md5: 117499f93e892ea1e57fdca16c2e8351 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=14 + - libnghttp2 >=1.67.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + size: 459417 + timestamp: 1765379027010 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.17.0-h7dd4100_1.conda + sha256: 80c7c8ff76eb699ec8d096dce80642b527fd8fc9dd72779bccec8d140c5b997a + md5: 9ddfaeed0eafce233ae8f4a430816aa5 + depends: + - __osx >=10.13 + - krb5 >=1.21.3,<1.22.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + size: 413119 + timestamp: 1765379670120 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda + sha256: 1a8a958448610ca3f8facddfe261fdbb010e7029a1571b84052ec9770fc0a36e + md5: 1d6e791c6e264ae139d469ce011aab51 + depends: + - __osx >=11.0 + - krb5 >=1.21.3,<1.22.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + size: 394471 + timestamp: 1765379821294 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda + sha256: 5ebab5c980c09d31b35a25095b295124d89fd8bdffdb3487604218ad56512885 + md5: c02248f96a0073904bb085a437143895 + depends: + - krb5 >=1.21.3,<1.22.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: curl + license_family: MIT + size: 379189 + timestamp: 1765379273605 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.7-h3d58e20_0.conda + sha256: 0ac1b1d1072a14fe8fd3a871c8ca0b411f0fdf30de70e5c95365a149bd923ac8 + md5: 67c086bf0efc67b54a235dd9184bd7a2 + depends: + - __osx >=10.13 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 571564 + timestamp: 1764676139160 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda + sha256: 4bdbef0241b52e7a8552e8af7425f0b56d5621dd69df46c816546fefa17d77ab + md5: 0de94f39727c31c0447e408c5a210a56 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 568715 + timestamp: 1764676451068 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 + md5: 6c77a605a7a689d17d4819c0f8ac9a00 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 73490 + timestamp: 1761979956660 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + sha256: 025f8b1e85dd8254e0ca65f011919fb1753070eb507f03bca317871a884d24de + md5: 31aa65919a729dc48180893f62c25221 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 70840 + timestamp: 1761980008502 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + sha256: 5e0b6961be3304a5f027a8c00bd0967fc46ae162cffb7553ff45c70f51b8314c + md5: a6130c709305cd9828b4e1bd9ba0000c + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 55420 + timestamp: 1761980066242 +- conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + sha256: 834e4881a18b690d5ec36f44852facd38e13afe599e369be62d29bd675f107ee + md5: e77030e67343e28b084fabd7db0ce43e + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 156818 + timestamp: 1761979842440 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda + sha256: c076a213bd3676cc1ef22eeff91588826273513ccc6040d9bea68bccdc849501 + md5: 9314bc5a1fe7d1044dc9dfd3ef400535 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpciaccess >=0.18,<0.19.0a0 + license: MIT + license_family: MIT + size: 310785 + timestamp: 1757212153962 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + sha256: 6cc49785940a99e6a6b8c6edbb15f44c2dd6c789d9c283e5ee7bdfedd50b4cd6 + md5: 1f4ed31220402fcddc083b4bff406868 + depends: + - ncurses + - __osx >=10.13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + size: 115563 + timestamp: 1738479554273 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + size: 107691 + timestamp: 1738479560845 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda + sha256: 7fd5408d359d05a969133e47af580183fbf38e2235b562193d427bb9dad79723 + md5: c151d5eb730e9b7480e6d48c0fc44048 + depends: + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_2 + license: LicenseRef-libglvnd + size: 44840 + timestamp: 1731330973553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + sha256: 0d238488564a7992942aa165ff994eca540f687753b4f0998b29b4e4d030ff43 + md5: 899db79329439820b7e8f8de41bca902 + license: BSD-2-Clause + license_family: BSD + size: 106663 + timestamp: 1702146352558 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + md5: 36d33e440c31857372a72137f78bacf5 + license: BSD-2-Clause + license_family: BSD + size: 107458 + timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 + md5: a1cfcc585f0c42bf8d5546bb1dfb668d + depends: + - libgcc-ng >=12 + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 427426 + timestamp: 1685725977222 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda + sha256: e0bd9af2a29f8dd74309c0ae4f17a7c2b8c4b89f875ff1d6540c941eefbd07fb + md5: e38e467e577bd193a7d5de7c2c540b04 + depends: + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 372661 + timestamp: 1685726378869 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda + sha256: 8c136d7586259bb5c0d2b913aaadc5b9737787ae4f40e3ad1beaf96c80b919b7 + md5: 1a109764bff3bdc7bdd84088347d71dc + depends: + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 368167 + timestamp: 1685726248899 +- conda: https://conda.anaconda.org/conda-forge/win-64/libevent-2.1.12-h3671451_1.conda + sha256: af03882afb7a7135288becf340c2f0cf8aa8221138a9a7b108aaeb308a486da1 + md5: 25efbd786caceef438be46da78a7b5ef + depends: + - openssl >=3.1.1,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + size: 410555 + timestamp: 1685726568668 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f + md5: 8b09ae86839581147ef2e5c5e229d164 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + size: 76643 + timestamp: 1763549731408 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda + sha256: d11b3a6ce5b2e832f430fd112084533a01220597221bee16d6c7dc3947dffba6 + md5: 222e0732a1d0780a622926265bee14ef + depends: + - __osx >=10.13 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + size: 74058 + timestamp: 1763549886493 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98 + md5: b79875dbb5b1db9a4a22a4520f918e1a + depends: + - __osx >=11.0 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + size: 67800 + timestamp: 1763549994166 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda + sha256: 844ab708594bdfbd7b35e1a67c379861bcd180d6efe57b654f482ae2f7f5c21e + md5: 8c9e4f1a0e688eef2e95711178061a0f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + size: 70137 + timestamp: 1763550049107 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda + sha256: 25cbdfa65580cfab1b8d15ee90b4c9f1e0d72128f1661449c9a999d341377d54 + md5: 35f29eec58405aaf55e01cb470d8c26a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 57821 + timestamp: 1760295480630 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda + sha256: 277dc89950f5d97f1683f26e362d6dca3c2efa16cb2f6fdb73d109effa1cd3d0 + md5: d214916b24c625bcc459b245d509f22e + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 52573 + timestamp: 1760295626449 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f + md5: 411ff7cd5d1472bba0f55c0faf04453b + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 40251 + timestamp: 1760295839166 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda + sha256: ddff25aaa4f0aa535413f5d831b04073789522890a4d8626366e43ecde1534a3 + md5: ba4ad812d2afc22b9a34ce8327a0930f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 44866 + timestamp: 1760295760649 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + sha256: 4641d37faeb97cf8a121efafd6afd040904d4bca8c46798122f417c31d5dfbec + md5: f4084e4e6577797150f9b04a4560ceb0 + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + size: 7664 + timestamp: 1757945417134 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.1-h694c41f_0.conda + sha256: 035e23ef87759a245d51890aedba0b494a26636784910c3730d76f3dc4482b1d + md5: e0e2edaf5e0c71b843e25a7ecc451cc9 + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + size: 7780 + timestamp: 1757945952392 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + sha256: 9de25a86066f078822d8dd95a83048d7dc2897d5d655c0e04a8a54fca13ef1ef + md5: f35fb38e89e2776994131fbf961fa44b + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + size: 7810 + timestamp: 1757947168537 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda + sha256: 2029702ec55e968ce18ec38cc8cf29f4c8c4989a0d51797164dab4f794349a64 + md5: 3235024fe48d4087721797ebd6c9d28c + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + size: 8109 + timestamp: 1757946135015 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + sha256: 4a7af818a3179fafb6c91111752954e29d3a2a950259c14a2fc7ba40a8b03652 + md5: 8e7251989bca326a28f4a5ffbd74557a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + size: 386739 + timestamp: 1757945416744 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.1-h6912278_0.conda + sha256: f5f28092e368efc773bcd1c381d123f8b211528385a9353e36f8808d00d11655 + md5: dfbdc8fd781dc3111541e4234c19fdbd + depends: + - __osx >=10.13 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + size: 374993 + timestamp: 1757945949585 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + sha256: cc4aec4c490123c0f248c1acd1aeab592afb6a44b1536734e20937cda748f7cd + md5: 6d4ede03e2a8e20eb51f7f681d2a2550 + depends: + - __osx >=11.0 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + size: 346703 + timestamp: 1757947166116 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda + sha256: 223710600b1a5567163f7d66545817f2f144e4ef8f84e99e90f6b8a4e19cb7ad + md5: 6e7c5c5ab485057b5d07fd8188ba5c28 + depends: + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + size: 340264 + timestamp: 1757946133889 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda + sha256: 6eed58051c2e12b804d53ceff5994a350c61baf117ec83f5f10c953a3f311451 + md5: 6d0363467e6ed84f11435eb309f2ff06 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_16 + - libgomp 15.2.0 he0feb66_16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 1042798 + timestamp: 1765256792743 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda + sha256: e04b115ae32f8cbf95905971856ff557b296511735f4e1587b88abf519ff6fb8 + md5: c816665789d1e47cdfd6da8a81e1af64 + depends: + - _openmp_mutex + constrains: + - libgomp 15.2.0 15 + - libgcc-ng ==15.2.0=*_15 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 422960 + timestamp: 1764839601296 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda + sha256: 646c91dbc422fe92a5f8a3a5409c9aac66549f4ce8f8d1cab7c2aa5db789bb69 + md5: 8b216bac0de7a9d60f3ddeba2515545c + depends: + - _openmp_mutex + constrains: + - libgcc-ng ==15.2.0=*_16 + - libgomp 15.2.0 16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 402197 + timestamp: 1765258985740 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_16.conda + sha256: 24984e1e768440ba73021f08a1da0c1ec957b30d7071b9a89b877a273d17cae8 + md5: 1edb8bd8e093ebd31558008e9cb23b47 + depends: + - _openmp_mutex >=4.5 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + constrains: + - libgomp 15.2.0 h8ee18e1_16 + - libgcc-ng ==15.2.0=*_16 + - msys2-conda-epoch <0.0a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 819696 + timestamp: 1765260437409 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda + sha256: 5f07f9317f596a201cc6e095e5fc92621afca64829785e483738d935f8cab361 + md5: 5a68259fac2da8f2ee6f7bfe49c9eb8b + depends: + - libgcc 15.2.0 he0feb66_16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 27256 + timestamp: 1765256804124 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda + sha256: 8a7b01e1ee1c462ad243524d76099e7174ebdd94ff045fe3e9b1e58db196463b + md5: 40d9b534410403c821ff64f00d0adc22 + depends: + - libgfortran5 15.2.0 h68bc16d_16 + constrains: + - libgfortran-ng ==15.2.0=*_16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 27215 + timestamp: 1765256845586 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda + sha256: 7bb4d51348e8f7c1a565df95f4fc2a2021229d42300aab8366eda0ea1af90587 + md5: a089323fefeeaba2ae60e1ccebf86ddc + depends: + - libgfortran5 15.2.0 hd16e46c_15 + constrains: + - libgfortran-ng ==15.2.0=*_15 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 139002 + timestamp: 1764839892631 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda + sha256: 68a6c1384d209f8654112c4c57c68c540540dd8e09e17dd1facf6cf3467798b5 + md5: 11e09edf0dde4c288508501fe621bab4 + depends: + - libgfortran5 15.2.0 hdae7583_16 + constrains: + - libgfortran-ng ==15.2.0=*_16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 138630 + timestamp: 1765259217400 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda + sha256: d0e974ebc937c67ae37f07a28edace978e01dc0f44ee02f29ab8a16004b8148b + md5: 39183d4e0c05609fd65f130633194e37 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 2480559 + timestamp: 1765256819588 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda + sha256: 456385a7d3357d5fdfc8e11bf18dcdf71753c4016c440f92a2486057524dd59a + md5: c2a6149bf7f82774a0118b9efef966dd + depends: + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 1061950 + timestamp: 1764839609607 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda + sha256: 9fb7f4ff219e3fb5decbd0ee90a950f4078c90a86f5d8d61ca608c913062f9b0 + md5: 265a9d03461da24884ecc8eb58396d57 + depends: + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 598291 + timestamp: 1765258993165 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda + sha256: dc2752241fa3d9e40ce552c1942d0a4b5eeb93740c9723873f6fcf8d39ef8d2d + md5: 928b8be80851f5d8ffb016f9c81dae7a + depends: + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_2 + - libglx 1.7.0 ha4b6fd6_2 + license: LicenseRef-libglvnd + size: 134712 + timestamp: 1731330998354 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.2-h32235b2_0.conda + sha256: 918306d6ed211ab483e4e19368e5748b265d24e75c88a1c66a61f72b9fa30b29 + md5: 0cb0612bc9cb30c62baf41f9d600611b + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.46,<10.47.0a0 + constrains: + - glib 2.86.2 *_0 + license: LGPL-2.1-or-later + size: 3974801 + timestamp: 1763672326986 +- conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.2-hd9c3897_0.conda + sha256: 60fa317d11a6f5d4bc76be5ff89b9ac608171a00b206c688e3cc4f65c73b1bc4 + md5: fbd144e60009d93f129f0014a76512d3 + depends: + - libffi >=3.5.2,<3.6.0a0 + - libiconv >=1.18,<2.0a0 + - libintl >=0.22.5,<1.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.46,<10.47.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - glib 2.86.2 *_0 + license: LGPL-2.1-or-later + size: 3793396 + timestamp: 1763672587079 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda + sha256: 1175f8a7a0c68b7f81962699751bb6574e6f07db4c9f72825f978e3016f46850 + md5: 434ca7e50e40f4918ab701e3facd59a0 + depends: + - __glibc >=2.17,<3.0.a0 + license: LicenseRef-libglvnd + size: 132463 + timestamp: 1731330968309 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda + sha256: 2d35a679624a93ce5b3e9dd301fff92343db609b79f0363e6d0ceb3a6478bfa7 + md5: c8013e438185f33b13814c5c488acd5c + depends: + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_2 + - xorg-libx11 >=1.8.10,<2.0a0 + license: LicenseRef-libglvnd + size: 75504 + timestamp: 1731330988898 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda + sha256: 5b3e5e4e9270ecfcd48f47e3a68f037f5ab0f529ccb223e8e5d5ac75a58fc687 + md5: 26c46f90d0e727e95c6c9498a33a09f3 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 603284 + timestamp: 1765256703881 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_16.conda + sha256: 9c86aadc1bd9740f2aca291da8052152c32dd1c617d5d4fd0f334214960649bb + md5: ab8189163748f95d4cb18ea1952943c3 + depends: + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + constrains: + - msys2-conda-epoch <0.0a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 663567 + timestamp: 1765260367147 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-hdb79228_0.conda + sha256: d3341cf69cb02c07bbd1837968f993da01b7bd467e816b1559a3ca26c1ff14c5 + md5: a2e30ccd49f753fd30de0d30b1569789 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libgcc >=14 + - libgrpc >=1.73.1,<1.74.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + - openssl >=3.5.1,<4.0a0 + constrains: + - libgoogle-cloud 2.39.0 *_0 + license: Apache-2.0 + license_family: Apache + size: 1307909 + timestamp: 1752048413383 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-2.39.0-hed66dea_0.conda + sha256: 9b50362bafd60c4a3eb6c37e6dbf7e200562dab7ae1b282b1ebd633d4d77d4bd + md5: 06564befaabd2760dfa742e47074bad2 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libcxx >=19 + - libgrpc >=1.73.1,<1.74.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - openssl >=3.5.1,<4.0a0 + constrains: + - libgoogle-cloud 2.39.0 *_0 + license: Apache-2.0 + license_family: Apache + size: 899629 + timestamp: 1752048034356 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-2.39.0-head0a95_0.conda + sha256: 209facdb8ea5b68163f146525720768fa3191cef86c82b2538e8c3cafa1e9dd4 + md5: ad7272a081abe0966d0297691154eda5 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libcxx >=19 + - libgrpc >=1.73.1,<1.74.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - openssl >=3.5.1,<4.0a0 + constrains: + - libgoogle-cloud 2.39.0 *_0 + license: Apache-2.0 + license_family: Apache + size: 876283 + timestamp: 1752047598741 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-2.39.0-h19ee442_0.conda + sha256: 8f5b26e9ea985c819a67e41664da82219534f9b9c8ba190f7d3c440361e5accb + md5: c2c512f98c5c666782779439356a1713 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libgrpc >=1.73.1,<1.74.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libgoogle-cloud 2.39.0 *_0 + license: Apache-2.0 + license_family: Apache + size: 14952 + timestamp: 1752049549178 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_0.conda + sha256: 59eb8365f0aee384f2f3b2a64dcd454f1a43093311aa5f21a8bb4bd3c79a6db8 + md5: bd21962ff8a9d1ce4720d42a35a4af40 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgcc >=14 + - libgoogle-cloud 2.39.0 hdb79228_0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + size: 804189 + timestamp: 1752048589800 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-2.39.0-h8ac052b_0.conda + sha256: fe790fc9ed8ffa468d27e886735fe11844369caee406d98f1da2c0d8aed0401e + md5: 7600fb1377c8eb5a161e4a2520933daa + depends: + - __osx >=11.0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libcxx >=19 + - libgoogle-cloud 2.39.0 hed66dea_0 + - libzlib >=1.3.1,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + size: 543323 + timestamp: 1752048443047 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-2.39.0-hfa3a374_0.conda + sha256: a5160c23b8b231b88d0ff738c7f52b0ee703c4c0517b044b18f4d176e729dfd8 + md5: 147a468b9b6c3ced1fccd69b864ae289 + depends: + - __osx >=11.0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libcxx >=19 + - libgoogle-cloud 2.39.0 head0a95_0 + - libzlib >=1.3.1,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + size: 525153 + timestamp: 1752047915306 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-storage-2.39.0-he04ea4c_0.conda + sha256: 51c29942d9bb856081605352ac74c45cad4fedbaac89de07c74efb69a3be9ab3 + md5: 26198e3dc20bbcbea8dd6fa5ab7ea1e0 + depends: + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgoogle-cloud 2.39.0 h19ee442_0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 14904 + timestamp: 1752049852815 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.73.1-h3288cfb_1.conda + sha256: bc9d32af6167b1f5bcda216dc44eddcb27f3492440571ab12f6e577472a05e34 + md5: ff63bb12ac31c176ff257e3289f20770 + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.5,<2.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libre2-11 >=2025.8.12 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.73.1 + license: Apache-2.0 + license_family: APACHE + size: 8349777 + timestamp: 1761058442526 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.73.1-h451496d_1.conda + sha256: 30378f4c9055224fecd1da8b9a65e2c0293cde68edca0f8a306fd9e92fd6ee1f + md5: d6ea2acfae86b523b54938c6bc30e378 + depends: + - __osx >=11.0 + - c-ares >=1.34.5,<2.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libre2-11 >=2025.8.12 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.73.1 + license: Apache-2.0 + license_family: APACHE + size: 5468625 + timestamp: 1761060387315 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.73.1-h3063b79_1.conda + sha256: c2099872b1aa06bf8153e35e5b706d2000c1fc16f4dde2735ccd77a0643a4683 + md5: f5856b3b9dae4463348a7ec23c1301f2 + depends: + - __osx >=11.0 + - c-ares >=1.34.5,<2.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libre2-11 >=2025.8.12 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.73.1 + license: Apache-2.0 + license_family: APACHE + size: 5377798 + timestamp: 1761053602943 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgrpc-1.73.1-h317e13b_1.conda + sha256: 95a83e98c35b8ec03d84f0714eefb2630078d9224360a93dbef6f2403414f76f + md5: 855b10d858d6c078a28d670cf32baa67 + depends: + - c-ares >=1.34.5,<2.0a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libre2-11 >=2025.8.12 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - re2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - grpc-cpp =1.73.1 + license: Apache-2.0 + license_family: APACHE + size: 14433486 + timestamp: 1761053760632 +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda + sha256: 2d534c09f92966b885acb3f4a838f7055cea043165a03079a539b06c54e20a49 + md5: d1699ce4fe195a9f61264a1c29b87035 + depends: + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - libxml2 + - libxml2-16 >=2.14.6 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 2412642 + timestamp: 1765090345611 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-only + size: 790176 + timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + sha256: a1c8cecdf9966921e13f0ae921309a1f415dfbd2b791f2117cf7e8f5e61a48b6 + md5: 210a85a1119f97ea7887188d176db135 + depends: + - __osx >=10.13 + license: LGPL-2.1-only + size: 737846 + timestamp: 1754908900138 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + sha256: de0336e800b2af9a40bdd694b03870ac4a848161b35c8a2325704f123f185f03 + md5: 4d5a7445f0b25b6a3ddbb56e790f5251 + depends: + - __osx >=11.0 + license: LGPL-2.1-only + size: 750379 + timestamp: 1754909073836 +- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 + md5: 64571d1dd6cdcfa25d0664a5950fdaa2 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-only + size: 696926 + timestamp: 1754909290005 +- conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + sha256: c7e4600f28bcada8ea81456a6530c2329312519efcf0c886030ada38976b0511 + md5: 2cf0cf76cc15d360dfa2f17fd6cf9772 + depends: + - libiconv >=1.17,<2.0a0 + license: LGPL-2.1-or-later + size: 95568 + timestamp: 1723629479451 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda + sha256: cc9aba923eea0af8e30e0f94f2ad7156e2984d80d1e8e7fe6be5a1f257f0eb32 + md5: 8397539e3a0bbd1695584fb4f927485a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + size: 633710 + timestamp: 1762094827865 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.2-h8616949_0.conda + sha256: ebe2877abc046688d6ea299e80d8322d10c69763f13a102010f90f7168cc5f54 + md5: 48dda187f169f5a8f1e5e07701d5cdd9 + depends: + - __osx >=10.13 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + size: 586189 + timestamp: 1762095332781 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda + sha256: 6c061c56058bb10374daaef50e81b39cf43e8aee21f0037022c0c39c4f31872f + md5: f0695fbecf1006f27f4395d64bd0c4b8 + depends: + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + size: 551197 + timestamp: 1762095054358 +- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.2-hfd05255_0.conda + sha256: 795e2d4feb2f7fc4a2c6e921871575feb32b8082b5760726791f080d1e2c2597 + md5: 56a686f92ac0273c0f6af58858a3f013 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + size: 841783 + timestamp: 1762094814336 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-4_h47877c9_openblas.conda + build_number: 4 + sha256: 5a6ed95bf093d709c8ba8373890773b912767eafdd2e8e4ad0fa6413d13ae3c9 + md5: 8ba8431802764597f400ee3e99026367 + depends: + - libblas 3.11.0 4_h4a7cf45_openblas + constrains: + - blas 2.304 openblas + - libcblas 3.11.0 4*_openblas + - liblapacke 3.11.0 4*_openblas + license: BSD-3-Clause + license_family: BSD + size: 18533 + timestamp: 1764823871307 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-4_h859234e_openblas.conda + build_number: 4 + sha256: cd490682199bd61c8db56cb72e71c154d91e8bf652cb28327690fa38246085d5 + md5: ebce74f166fc65413f751b8a125d4be3 + depends: + - libblas 3.11.0 4_he492b99_openblas + constrains: + - liblapacke 3.11.0 4*_openblas + - libcblas 3.11.0 4*_openblas + - blas 2.304 openblas + license: BSD-3-Clause + license_family: BSD + size: 18692 + timestamp: 1764824659093 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-4_hd9741b5_openblas.conda + build_number: 4 + sha256: 63c9ac0c44c99fdf8de038b66f549d29a7b71e51223ad3fac1b4ba79080581c1 + md5: 3b949d8c584bc30932e41c755507bdc1 + depends: + - libblas 3.11.0 4_h51639a9_openblas + constrains: + - libcblas 3.11.0 4*_openblas + - blas 2.304 openblas + - liblapacke 3.11.0 4*_openblas + license: BSD-3-Clause + license_family: BSD + size: 18764 + timestamp: 1764824468301 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-4_hf9ab0e9_mkl.conda + build_number: 4 + sha256: d820333e9bac8381fb69e857d673c12d034bb45d0fe4818a1d12e1ec7a39e7df + md5: 67298727e96b60068a316d2f627e1e35 + depends: + - libblas 3.11.0 4_hf2e6a31_mkl + constrains: + - liblapacke 3.11.0 4*_mkl + - libcblas 3.11.0 4*_mkl + - blas 2.304 mkl + license: BSD-3-Clause + license_family: BSD + size: 80387 + timestamp: 1764824249543 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.7-hf7376ad_0.conda + sha256: afe5c5cfc90dc8b5b394e21cf02188394e36766119ad5d78a1d8619d011bbfb1 + md5: 27dc1a582b442f24979f2a28641fe478 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 44320825 + timestamp: 1764711528746 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda + sha256: 7e22fd1bdb8bf4c2be93de2d4e718db5c548aa082af47a7430eb23192de6bb36 + md5: 8468beea04b9065b9807fc8b9cdc5894 + depends: + - __osx >=10.13 + constrains: + - xz 5.8.1.* + license: 0BSD + size: 104826 + timestamp: 1749230155443 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 + md5: d6df911d4564d77c4374b02552cb17d1 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.1.* + license: 0BSD + size: 92286 + timestamp: 1749230283517 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc + md5: c15148b2e18da456f5108ccb5e411446 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - xz 5.8.1.* + license: 0BSD + size: 104935 + timestamp: 1749230611612 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee + md5: c7e925f37e3b40d893459e625f6a53f1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + size: 91183 + timestamp: 1748393666725 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda + sha256: 98299c73c7a93cd4f5ff8bb7f43cd80389f08b5a27a296d806bdef7841cc9b9e + md5: 18b81186a6adb43f000ad19ed7b70381 + depends: + - __osx >=10.13 + license: BSD-2-Clause + license_family: BSD + size: 77667 + timestamp: 1748393757154 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + sha256: 0a1875fc1642324ebd6c4ac864604f3f18f57fbcf558a8264f6ced028a3c75b2 + md5: 85ccccb47823dd9f7a99d2c7f530342f + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + size: 71829 + timestamp: 1748393749336 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + sha256: fc529fc82c7caf51202cc5cec5bb1c2e8d90edbac6d0a4602c966366efe3c7bf + md5: 74860100b2029e2523cf480804c76b9b + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + size: 88657 + timestamp: 1723861474602 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 + md5: b499ce4b026493a13774bcf0f4c33849 + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.5,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.2,<4.0a0 + license: MIT + license_family: MIT + size: 666600 + timestamp: 1756834976695 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda + sha256: c48d7e1cc927aef83ff9c48ae34dd1d7495c6ccc1edc4a3a6ba6aff1624be9ac + md5: e7630cef881b1174d40f3e69a883e55f + depends: + - __osx >=10.13 + - c-ares >=1.34.5,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.2,<4.0a0 + license: MIT + license_family: MIT + size: 605680 + timestamp: 1756835898134 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + sha256: a07cb53b5ffa2d5a18afc6fd5a526a5a53dd9523fbc022148bd2f9395697c46d + md5: a4b4dd73c67df470d091312ab87bf6ae + depends: + - __osx >=11.0 + - c-ares >=1.34.5,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.2,<4.0a0 + license: MIT + license_family: MIT + size: 575454 + timestamp: 1756835746393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda + sha256: 3b3f19ced060013c2dd99d9d46403be6d319d4601814c772a3472fe2955612b0 + md5: 7c7927b404672409d9917d49bff5f2d6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + size: 33418 + timestamp: 1734670021371 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + sha256: 199d79c237afb0d4780ccd2fbf829cea80743df60df4705202558675e07dd2c5 + md5: be43915efc66345cccb3c310b6ed0374 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + size: 5927939 + timestamp: 1763114673331 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda + sha256: ba642353f7f41ab2d2eb6410fbe522238f0f4483bcd07df30b3222b4454ee7cd + md5: 9241a65e6e9605e4581a2a8005d7f789 + depends: + - __osx >=10.13 + - libgfortran + - libgfortran5 >=14.3.0 + - llvm-openmp >=19.1.7 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + size: 6268795 + timestamp: 1763117623665 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda + sha256: dcc626c7103503d1dfc0371687ad553cb948b8ed0249c2a721147bdeb8db4a73 + md5: a18a7f471c517062ee71b843ef95eb8a + depends: + - __osx >=11.0 + - libgfortran + - libgfortran5 >=14.3.0 + - llvm-openmp >=19.1.7 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + size: 4285762 + timestamp: 1761749506256 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda + sha256: 215086c108d80349e96051ad14131b751d17af3ed2cb5a34edd62fa89bfe8ead + md5: 7df50d44d4a14d6c31a2c54f2cd92157 + depends: + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_2 + license: LicenseRef-libglvnd + size: 50757 + timestamp: 1731330993524 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-hb9b0907_1.conda + sha256: ba9b09066f9abae9b4c98ffedef444bbbf4c068a094f6c77d70ef6f006574563 + md5: 1c0320794855f457dea27d35c4c71e23 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libgrpc >=1.73.1,<1.74.0a0 + - libopentelemetry-cpp-headers 1.21.0 ha770c72_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.21.0 + license: Apache-2.0 + license_family: APACHE + size: 885397 + timestamp: 1751782709380 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.21.0-h7d3f41d_1.conda + sha256: 94df4129f94dbb17998a60bff0b53c700e6124a6cb67f3047fe7059ebaa7d357 + md5: 952dd64cff4a72cadf5e81572a7a81c8 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libgrpc >=1.73.1,<1.74.0a0 + - libopentelemetry-cpp-headers 1.21.0 h694c41f_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.21.0 + license: Apache-2.0 + license_family: APACHE + size: 585875 + timestamp: 1751782877386 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.21.0-he15edb5_1.conda + sha256: 4bf8f703ddd140fe54d4c8464ac96b28520fbc1083cce52c136a85a854745d5c + md5: cbcea547d6d831863ab0a4e164099062 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcurl >=8.14.1,<9.0a0 + - libgrpc >=1.73.1,<1.74.0a0 + - libopentelemetry-cpp-headers 1.21.0 hce30654_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.21.0 + license: Apache-2.0 + license_family: APACHE + size: 564609 + timestamp: 1751782939921 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_1.conda + sha256: b3a1b36d5f92fbbfd7b6426982a99561bdbd7e4adbafca1b7f127c9a5ab0a60f + md5: 9e298d76f543deb06eb0f3413675e13a + license: Apache-2.0 + license_family: APACHE + size: 363444 + timestamp: 1751782679053 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.21.0-h694c41f_1.conda + sha256: 5b43ec55305a6fabd8eb37cee06bc3260d3641f260435194837d0b64faa0b355 + md5: 62636543478d53b28c1fc5efce346622 + license: Apache-2.0 + license_family: APACHE + size: 362175 + timestamp: 1751782820895 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.21.0-hce30654_1.conda + sha256: ce74278453dec1e3c11158ec368c8f1b03862e279b63f79ed01f38567a1174e6 + md5: c7df4b2d612208f3a27486c113b6aefc + license: Apache-2.0 + license_family: APACHE + size: 363213 + timestamp: 1751782889359 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-22.0.0-h7376487_6_cpu.conda + build_number: 6 + sha256: c6cc2a73091e5c460c3cbd606927d5ed85d3706e19459073e1ea023d1e754d13 + md5: 83fd8f55f38ac972947c9eca12dc4657 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 22.0.0 hb6ed5f4_6_cpu + - libgcc >=14 + - libstdcxx >=14 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 1350396 + timestamp: 1765381452093 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-22.0.0-habb56ca_4_cpu.conda + build_number: 4 + sha256: f195841bde46a049fe449cf59b8e42db7f83e2459ffd1de4dad2bd192db86b84 + md5: 67ff6ca0e1fdec92bdc20fa593390ba1 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 hd1700fa_4_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 1073343 + timestamp: 1763230480681 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-22.0.0-h0ac143b_6_cpu.conda + build_number: 6 + sha256: 329c6cd1fbeef6e91f8bc7a2e8bd28c50b72bc42e0a028d990e2281966f57ef5 + md5: 4939c8e3ca5f98f229be9f318df740e2 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libarrow 22.0.0 he6e817a_6_cpu + - libcxx >=19 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 1048992 + timestamp: 1765382997871 +- conda: https://conda.anaconda.org/conda-forge/win-64/libparquet-22.0.0-h7051d1f_6_cpu.conda + build_number: 6 + sha256: c30839adc47e3ccd6f717c33632d9b482e83f7e087a24211416246f8f05e9a54 + md5: d840a2b45e737bb768ec4e0d5bf36c90 + depends: + - libarrow 22.0.0 h89d7da9_6_cpu + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.4,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 927228 + timestamp: 1765382245972 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda + sha256: 0bd91de9b447a2991e666f284ae8c722ffb1d84acb594dbd0c031bd656fa32b2 + md5: 70e3400cbbfa03e96dcde7fc13e38c7b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + size: 28424 + timestamp: 1749901812541 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.53-h421ea60_0.conda + sha256: 8acdeb9a7e3d2630176ba8e947caf6bf4985a5148dec69b801e5eb797856688b + md5: 00d4e66b1f746cb14944cad23fffb405 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + size: 317748 + timestamp: 1764981060755 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.53-h380d223_0.conda + sha256: 62a861e407bf0d0a2a983d0b0167ed263ae035cae7061976e9994f9963e6c68d + md5: 0cdbbd56f660997cfe5d33e516afac2f + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + size: 298397 + timestamp: 1764981064303 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.53-hfab5511_0.conda + sha256: 6793e7284e175c515fc6453be45c7c0febdea853657d246d8136fbda791dd0ad + md5: 62b6111feeffe607c3ecc8ca5bd1514b + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + size: 288210 + timestamp: 1764981075326 +- conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.53-h7351971_0.conda + sha256: e5d061e7bdb2b97227b6955d1aa700a58a5703b5150ab0467cc37de609f277b6 + md5: fb6f43f6f08ca100cb24cff125ab0d9e + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + size: 383702 + timestamp: 1764981078732 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.1-h5c52fec_2.conda + sha256: bbab2c3e6f650f2bd1bc84d88e6a20fefa6a401fa445bb4b97c509c1b3a89fa8 + md5: a8ac9a6342569d1714ae1b53ae2fcadb + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=14 + - openldap >=2.6.10,<2.7.0a0 + - openssl >=3.5.4,<4.0a0 + license: PostgreSQL + size: 2711480 + timestamp: 1764345810429 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda + sha256: 1679f16c593d769f3dab219adb1117cbaaddb019080c5a59f79393dc9f45b84f + md5: 94cb88daa0892171457d9fdc69f43eca + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 4645876 + timestamp: 1760550892361 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h03562ea_2.conda + sha256: 40a32a77cdb7f7b49187a4c9faf5c7812d95233288ab96b06e0dd9978ecd8e6d + md5: 39b7711c03a0d0533e832e734641e56e + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 3550823 + timestamp: 1760550860606 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h658db43_2.conda + sha256: a01c3829eb0e3c1354ee7d61c5cde9a79dcebe6ccc7114c2feadf30aecbc7425 + md5: 155d3d17eaaf49ddddfe6c73842bc671 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 2982875 + timestamp: 1760550241203 +- conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.31.1-hdcda5b4_2.conda + sha256: bb28909aef3777c5e950b769b30fe4bf02e0a7fb5322e583042a5cdc76bb15d0 + md5: 0e44c704760bbe4b696d981c3313f665 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 7787239 + timestamp: 1760550955606 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h7b12aa8_0.conda + sha256: eb5d5ef4d12cdf744e0f728b35bca910843c8cf1249f758cf15488ca04a21dbb + md5: a30848ebf39327ea078cf26d114cff53 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + size: 211099 + timestamp: 1762397758105 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h554ac88_0.conda + sha256: 901fb4cfdabf1495e7f080f8e8e218d1ad182c9bcd3cea2862481fef0e9d534f + md5: a0237623ed85308cb816c3dcced23db2 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + size: 180107 + timestamp: 1762398117273 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h91c62da_0.conda + sha256: 7b525313ab16415c4a3191ccf59157c3a4520ed762c8ec61fcfb81d27daa4723 + md5: 060f099756e6baf2ed51b9065e44eda8 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + size: 165593 + timestamp: 1762398300610 +- conda: https://conda.anaconda.org/conda-forge/win-64/libre2-11-2025.11.05-h0eb2380_0.conda + sha256: 8eb2c205588e6d751fe387e90f1321ac8bbaef0a12d125a1dd898e925327f8ae + md5: 960713477ad3d7f82e5199fa1b940495 + depends: + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + size: 263996 + timestamp: 1762397947932 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 + md5: a587892d3c13b6621a6091be690dbca2 + depends: + - libgcc-ng >=12 + license: ISC + size: 205978 + timestamp: 1716828628198 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda + sha256: d3975cfe60e81072666da8c76b993af018cf2e73fe55acba2b5ba0928efaccf5 + md5: 6af4b059e26492da6013e79cbcb4d069 + depends: + - __osx >=10.13 + license: ISC + size: 210249 + timestamp: 1716828641383 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + sha256: fade8223e1e1004367d7101dd17261003b60aa576df6d7802191f8972f7470b1 + md5: a7ce36e284c5faaf93c220dfc39e3abd + depends: + - __osx >=11.0 + license: ISC + size: 164972 + timestamp: 1716828607917 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + sha256: 7bcb3edccea30f711b6be9601e083ecf4f435b9407d70fc48fbcf9e5d69a0fc6 + md5: 198bb594f202b205c7d18b936fa4524f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: ISC + size: 202344 + timestamp: 1716828757533 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_0.conda + sha256: 6f0e8a812e8e33a4d8b7a0e595efe28373080d27b78ee4828aa4f6649a088454 + md5: 2e1b84d273b01835256e53fd938de355 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + size: 938979 + timestamp: 1764359444435 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-h6cc646a_0.conda + sha256: 8460901daff15749354f0de143e766febf0682fe9201bf307ea84837707644d1 + md5: f71213ed0c51030cb17a77fc60a757f1 + depends: + - __osx >=10.13 + - icu >=75.1,<76.0a0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + size: 991350 + timestamp: 1764359781222 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h9a5124b_0.conda + sha256: a46b167447e2a9e38586320c30b29e3b68b6f7e6b873c18d6b1aa2efd2626917 + md5: 67e50e5bd4e5e2310d66b88c4da50096 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + size: 906292 + timestamp: 1764359907797 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_0.conda + sha256: a976c8b455d9023b83878609bd68c3b035b9839d592bd6c7be7552c523773b62 + md5: f92bef2f8e523bb0eabe60099683617a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + size: 1291059 + timestamp: 1764359545703 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 + md5: eecce068c7e4eddeb169591baac20ac4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 304790 + timestamp: 1745608545575 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + sha256: 00654ba9e5f73aa1f75c1f69db34a19029e970a4aeb0fa8615934d8e9c369c3c + md5: a6cb15db1c2dc4d3a5f6cf3772e09e81 + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 284216 + timestamp: 1745608575796 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + sha256: 8bfe837221390ffc6f111ecca24fa12d4a6325da0c8d131333d63d6c37f27e0a + md5: b68e8f66b94b44aaa8de4583d3d4cc40 + depends: + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 279193 + timestamp: 1745608793272 +- conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + sha256: cbdf93898f2e27cefca5f3fe46519335d1fab25c4ea2a11b11502ff63e602c09 + md5: 9dce2f112bfd3400f4f432b3d0ac07b2 + depends: + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + size: 292785 + timestamp: 1745608759342 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda + sha256: 813427918316a00c904723f1dfc3da1bbc1974c5cfe1ed1e704c6f4e0798cbc6 + md5: 68f68355000ec3f1d6f26ea13e8f525f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_16 + constrains: + - libstdcxx-ng ==15.2.0=*_16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 5856456 + timestamp: 1765256838573 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda + sha256: 81f2f246c7533b41c5e0c274172d607829019621c4a0823b5c0b4a8c7028ee84 + md5: 1b3152694d236cf233b76b8c56bf0eae + depends: + - libstdcxx 15.2.0 h934c35e_16 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 27300 + timestamp: 1765256885128 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda + sha256: 4888b9ea2593c36ca587a5ebe38d0a56a0e6d6a9e4bb7da7d9a326aaaca7c336 + md5: 8ed82d90e6b1686f5e98f8b7825a15ef + depends: + - __glibc >=2.17,<3.0.a0 + - libevent >=2.1.12,<2.1.13.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.1,<4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 424208 + timestamp: 1753277183984 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-h687e942_1.conda + sha256: a0f9fdc663db089fde4136a0bd6c819d7f8daf869fc3ca8582201412e47f298c + md5: 69251ed374b31a5664bf5ba58626f3b7 + depends: + - __osx >=10.13 + - libcxx >=19 + - libevent >=2.1.12,<2.1.13.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.1,<4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 331822 + timestamp: 1753277335578 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h14a376c_1.conda + sha256: 8b703f2c6e47ed5886d7298601b9416b59e823fc8d1a8fa867192c94c5911aac + md5: 3161023bb2f8c152e4c9aa59bdd40975 + depends: + - __osx >=11.0 + - libcxx >=19 + - libevent >=2.1.12,<2.1.13.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.1,<4.0a0 + license: Apache-2.0 + license_family: APACHE + size: 323360 + timestamp: 1753277264380 +- conda: https://conda.anaconda.org/conda-forge/win-64/libthrift-0.22.0-h23985f6_1.conda + sha256: 87516b128ffa497fc607d5da0cc0366dbee1dbcc14c962bf9ea951d480c7698b + md5: 556d49ad5c2ad553c2844cc570bb71c7 + depends: + - libevent >=2.1.12,<2.1.13.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.1,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 636513 + timestamp: 1753277481158 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda + sha256: e5f8c38625aa6d567809733ae04bb71c161a42e44a9fa8227abe61fa5c60ebe0 + md5: cd5a90476766d53e901500df9215e927 + depends: + - __glibc >=2.17,<3.0.a0 + - lerc >=4.0.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + size: 435273 + timestamp: 1762022005702 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda + sha256: e53424c34147301beae2cd9223ebf593720d94c038b3f03cacd0535e12c9668e + md5: 9d4344f94de4ab1330cdc41c40152ea6 + depends: + - __osx >=10.13 + - lerc >=4.0.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + size: 404591 + timestamp: 1762022511178 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda + sha256: e9248077b3fa63db94caca42c8dbc6949c6f32f94d1cafad127f9005d9b1507f + md5: e2a72ab2fa54ecb6abab2b26cde93500 + depends: + - __osx >=11.0 + - lerc >=4.0.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + size: 373892 + timestamp: 1762022345545 +- conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h8f73337_1.conda + sha256: f1b8cccaaeea38a28b9cd496694b2e3d372bb5be0e9377c9e3d14b330d1cba8a + md5: 549845d5133100142452812feb9ba2e8 + depends: + - lerc >=4.0.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + size: 993166 + timestamp: 1762022118895 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.2-hfe17d71_0.conda + sha256: 98812901f52df746f89e1fda2a65494dd30de9e826f89b49ebad5d53e5fc424d + md5: 5641725dfad698909ec71dac80d16736 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 85985 + timestamp: 1764062044259 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.2-h7983711_0.conda + sha256: 83f2799e28643c7793730aa32e007832ffb520c5d77714d2097c227424f33ef1 + md5: e630b1baa02a5eeb0ef351c6125865c4 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 84943 + timestamp: 1764062312835 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.2-hd2415e0_0.conda + sha256: 5c7d4268a1bd02f3cbba6d8a8f9bd47829a46dbc81690a39b1c05e698c180570 + md5: 1ae98806b064c48f184d7c6e0ac506b6 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 88014 + timestamp: 1764062565080 +- conda: https://conda.anaconda.org/conda-forge/win-64/libutf8proc-2.11.2-hb980946_0.conda + sha256: ff63a5e402fb5007174ea9796a210617da898a43d00b4e8a3192537cad0bd403 + md5: 405c392813b74f3df06276e99c0e2841 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 89116 + timestamp: 1764062179403 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-h5347b49_1.conda + sha256: 030447cf827c471abd37092ab9714fde82b8222106f22fde94bc7a64e2704c40 + md5: 41f5c09a211985c3ce642d60721e7c3e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + size: 40235 + timestamp: 1764790744114 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b + md5: 0f03292cc56bf91a077a134ea8747118 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 895108 + timestamp: 1753948278280 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda + sha256: d90dd0eee6f195a5bd14edab4c5b33be3635b674b0b6c010fb942b956aa2254c + md5: fbfc6cf607ae1e1e498734e256561dc3 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 422612 + timestamp: 1753948458902 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + sha256: 042c7488ad97a5629ec0a991a8b2a3345599401ecc75ad6a5af73b60e6db9689 + md5: c0d87c3c8e075daf1daf6c31b53e8083 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 421195 + timestamp: 1753948426421 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.328.1-h5279c79_0.conda + sha256: bbabc5c48b63ff03f440940a11d4648296f5af81bb7630d98485405cd32ac1ce + md5: 372a62464d47d9e966b630ffae3abe73 + depends: + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxrandr >=1.5.4,<2.0a0 + constrains: + - libvulkan-headers 1.4.328.1.* + license: Apache-2.0 + license_family: APACHE + size: 197672 + timestamp: 1759972155030 +- conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.328.1-h477610d_0.conda + sha256: 934d676c445c1ea010753dfa98680b36a72f28bec87d15652f013c91a1d8d171 + md5: 4403eae6c81f448d63a7f66c0b330536 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + constrains: + - libvulkan-headers 1.4.328.1.* + license: Apache-2.0 + license_family: APACHE + size: 280488 + timestamp: 1759972163692 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b + md5: aea31d2e5b1091feca96fcfe945c3cf9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + size: 429011 + timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + sha256: 00dbfe574b5d9b9b2b519acb07545380a6bc98d1f76a02695be4995d4ec91391 + md5: 7bb6608cf1f83578587297a158a6630b + depends: + - __osx >=10.13 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + size: 365086 + timestamp: 1752159528504 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd + md5: e5e7d467f80da752be17796b87fe6385 + depends: + - __osx >=11.0 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + size: 294974 + timestamp: 1752159906788 +- conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + sha256: 7b6316abfea1007e100922760e9b8c820d6fc19df3f42fb5aca684cfacb31843 + md5: f9bbae5e2537e3b06e0f7310ba76c893 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + size: 279176 + timestamp: 1752159543911 +- conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + sha256: 0fccf2d17026255b6e10ace1f191d0a2a18f2d65088fd02430be17c701f8ffe0 + md5: 8a86073cf3b343b87d03f41790d8b4e5 + depends: + - ucrt + constrains: + - pthreads-win32 <0.0a0 + - msys2-conda-epoch <0.0a0 + license: MIT AND BSD-3-Clause-Clear + size: 36621 + timestamp: 1759768399557 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa + md5: 92ed62436b625154323d40d5f2f11dd7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + size: 395888 + timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda + sha256: 8896cd5deff6f57d102734f3e672bc17120613647288f9122bec69098e839af7 + md5: bbeca862892e2898bdb45792a61c4afc + depends: + - __osx >=10.13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + size: 323770 + timestamp: 1727278927545 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + sha256: bd3816218924b1e43b275863e21a3e13a5db4a6da74cca8e60bc3c213eb62f71 + md5: af523aae2eca6dfa1c8eec693f5b9a79 + depends: + - __osx >=11.0 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + size: 323658 + timestamp: 1727278733917 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + sha256: 08dec73df0e161c96765468847298a420933a36bc4f09b50e062df8793290737 + md5: a69bbf778a462da324489976c84cfc8c + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - pthread-stubs + - ucrt >=10.0.20348.0 + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + size: 1208687 + timestamp: 1727279378819 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda + sha256: d2195b5fbcb0af1ff7b345efdf89290c279b8d1d74f325ae0ac98148c375863c + md5: 2bca1fbb221d9c3c8e3a155784bbc2e9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libxcb >=1.17.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - xkeyboard-config + - xorg-libxau >=1.0.12,<2.0a0 + license: MIT/X11 Derivative + license_family: MIT + size: 837922 + timestamp: 1764794163823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.1-h26afc86_0.conda + sha256: ec0735ae56c3549149eebd7dc22c0bed91fd50c02eaa77ff418613ddda190aa8 + md5: e512be7dc1f84966d50959e900ca121f + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2-16 2.15.1 ha9997c6_0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + size: 45283 + timestamp: 1761015644057 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.1-h7b7ecba_0.conda + sha256: ddf87bf05955d7870a41ca6f0e9fbd7b896b5a26ec1a98cd990883ac0b4f99bb + md5: e7ed73b34f9d43d80b7e80eba9bce9f3 + depends: + - __osx >=10.13 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2-16 2.15.1 ha1d9b0f_0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + size: 39985 + timestamp: 1761015935429 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.1-h9329255_0.conda + sha256: c409e384ddf5976a42959265100d6b2c652017d250171eb10bae47ef8166193f + md5: fb5ce61da27ee937751162f86beba6d1 + depends: + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2-16 2.15.1 h0ff4647_0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + size: 40607 + timestamp: 1761016108361 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-ha29bfb0_0.conda + sha256: fb51b91a01eac9ee5e26c67f4e081f09f970c18a3da5231b8172919a1e1b3b6b + md5: 87116b9de9c1825c3fd4ef92c984877b + depends: + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2-16 2.15.1 h06f855e_0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 43042 + timestamp: 1761016261024 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.1-ha9997c6_0.conda + sha256: 71436e72a286ef8b57d6f4287626ff91991eb03c7bdbe835280521791efd1434 + md5: e7733bc6785ec009e47a224a71917e84 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + size: 556302 + timestamp: 1761015637262 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.1-ha1d9b0f_0.conda + sha256: e23c5ac1da7b9b65bd18bf32b68717cd9da0387941178cb4d8cc5513eb69a0a9 + md5: 453807a4b94005e7148f89f9327eb1b7 + depends: + - __osx >=10.13 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + size: 494318 + timestamp: 1761015899881 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + sha256: ebe2dd9da94280ad43da936efa7127d329b559f510670772debc87602b49b06d + md5: 438c97d1e9648dd7342f86049dd44638 + depends: + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + size: 464952 + timestamp: 1761016087733 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h06f855e_0.conda + sha256: 3f65ea0f04c7738116e74ca87d6e40f8ba55b3df31ef42b8cb4d78dd96645e90 + md5: 4a5ea6ec2055ab0dfd09fd0c498f834a + depends: + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + size: 518616 + timestamp: 1761016240185 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + sha256: 0694760a3e62bdc659d90a14ae9c6e132b525a7900e59785b18a08bb52a5d7e5 + md5: 87e6096ec6d542d1c1f8b33245fe8300 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxml2 + - libxml2-16 >=2.14.6 + license: MIT + license_family: MIT + size: 245434 + timestamp: 1757963724977 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + sha256: 00d6b5e92fc1c5d86e095b9b6840f793d9fc4c9b4a7753fa0f8197ab11d5eb90 + md5: 367b8029352f3899fb76cc20f4d144b9 + depends: + - __osx >=10.13 + - libxml2 + - libxml2-16 >=2.14.6 + license: MIT + license_family: MIT + size: 225660 + timestamp: 1757964032926 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + sha256: 7a4d0676ab1407fecb24d4ada7fe31a98c8889f61f04612ea533599c22b8c472 + md5: 90f7ed12bb3c164c758131b3d3c2ab0c + depends: + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + license: MIT + license_family: MIT + size: 220345 + timestamp: 1757964000982 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + sha256: 13da38939c2c20e7112d683ab6c9f304bfaf06230a2c6a7cf00359da1a003ec7 + md5: 46034d9d983edc21e84c0b36f1b4ba61 + depends: + - libxml2 + - libxml2-16 >=2.14.6 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 420223 + timestamp: 1757963935611 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda + sha256: 8412f96504fc5993a63edf1e211d042a1fd5b1d51dedec755d2058948fcced09 + md5: 003a54a4e32b02f7355b50a837e699da + depends: + - __osx >=10.13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 57133 + timestamp: 1727963183990 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 55476 + timestamp: 1727963768015 +- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.7-h472b3d1_0.conda + sha256: 5ae51ca08ac19ce5504b8201820ba6387365662033f20af2150ae7949f3f308a + md5: c9f0fc88c8f46637392b95bef78dc036 + depends: + - __osx >=10.13 + constrains: + - openmp 21.1.7|21.1.7.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + size: 311027 + timestamp: 1764721464764 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.7-h4a912ad_0.conda + sha256: 002695e79b0e4c2d117a8bd190ffd62ef3d74a4cae002afa580bd1f98f9560a3 + md5: 05d475f50ddcc2173a6beece9960c6cb + depends: + - __osx >=11.0 + constrains: + - openmp 21.1.7|21.1.7.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + size: 286129 + timestamp: 1764721670250 +- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.7-h4fa8253_0.conda + sha256: 79121242419bf8b485c313fa28697c5c61ec207afa674eac997b3cb2fd1ff892 + md5: 5823741f7af732cd56036ae392396ec6 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - intel-openmp <0.0a0 + - openmp 21.1.7|21.1.7.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + size: 347969 + timestamp: 1764722187332 +- conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.46.0-py314h946fb2a_0.conda + sha256: 99f15d69f059aa9c7d06cc45a6519a2375cc7a93ca85127964d6325a89a2b519 + md5: 7ee180b967506bbd108ca9d5ff45eace + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + size: 34123266 + timestamp: 1765279959565 +- conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.46.0-py314h85c3bf0_0.conda + sha256: 468f68ddfad77e92de45a9023e92c8cea13df253bd27861de7cd594bc13f5569 + md5: babaf455ce9be7d7001cf048eb80508b + depends: + - __osx >=10.13 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + size: 26019299 + timestamp: 1765280661650 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.46.0-py314ha398f32_0.conda + sha256: 10ee25664d790b117d84701506b60caba147f7bf599215cbd688037aaa42ff81 + md5: b9eefe6197dafc779b784731fa507f60 + depends: + - __osx >=11.0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + size: 24330524 + timestamp: 1765280789928 +- conda: https://conda.anaconda.org/conda-forge/win-64/llvmlite-0.46.0-py314hb492ee6_0.conda + sha256: 8f8bb4cd5a93aaf576e6861846f09dcff8f37032b02704e830d9afd3e6676d6b + md5: de5f7e2de23118d72f43c99fe7f2a942 + depends: + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + size: 22926897 + timestamp: 1765280131964 +- conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + sha256: 9afe0b5cfa418e8bdb30d8917c5a6cec10372b037924916f1f85b9f4899a67a6 + md5: 91e27ef3d05cc772ce627e51cff111c4 + depends: + - python >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* + license: BSD-2-Clause + license_family: BSD + size: 8250 + timestamp: 1650660473123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py314hae3bed6_2.conda + sha256: 4871db69d62586fa264373cb1fee0fd2e3bbed2cddeca66bc423ccc9836c2e45 + md5: ddd2ee75713129777aa3e3339f899008 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause and MIT-CMU + size: 1616783 + timestamp: 1762506575980 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.0.2-py314h787f955_2.conda + sha256: b76af19826dfe9fc7101fdeb3d96852d091a430b73fdf084fbd4bacd1e1a3210 + md5: 5bedcfaa028960dc42499bbd8be83c16 + depends: + - __osx >=10.13 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause and MIT-CMU + size: 1428805 + timestamp: 1762506862335 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py314he05ef12_2.conda + sha256: b13cb4b556e1973c81aefef5709059251084f5c80fe9fe94981141d7932ceb74 + md5: b53627cd79341966e489b2f8bc82f486 + depends: + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause and MIT-CMU + size: 1388732 + timestamp: 1762506892440 +- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py314hcdb55d9_2.conda + sha256: 6231fe0751c1174ddfba98267b58bbcc0cccc7cfd0c68163f29d0b46f1851085 + md5: d1feee0dad2ed550bbb59969d4d3457f + depends: + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause and MIT-CMU + size: 1238889 + timestamp: 1762506487891 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 + md5: 9de5350a85c4a20c685259b889aa6393 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD + size: 167055 + timestamp: 1733741040117 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda + sha256: 8da3c9d4b596e481750440c0250a7e18521e7f69a47e1c8415d568c847c08a1c + md5: d6b9bd7e356abd7e3a633d59b753495a + depends: + - __osx >=10.13 + - libcxx >=18 + license: BSD-2-Clause + license_family: BSD + size: 159500 + timestamp: 1733741074747 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + sha256: 94d3e2a485dab8bdfdd4837880bde3dd0d701e2b97d6134b8806b7c8e69c8652 + md5: 01511afc6cc1909c5303cf31be17b44f + depends: + - __osx >=11.0 + - libcxx >=18 + license: BSD-2-Clause + license_family: BSD + size: 148824 + timestamp: 1733741047892 +- conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + sha256: 632cf3bdaf7a7aeb846de310b6044d90917728c73c77f138f08aa9438fc4d6b5 + md5: 0b69331897a92fac3d8923549d48d092 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + size: 139891 + timestamp: 1733741168264 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + sha256: 7b1da4b5c40385791dbc3cc85ceea9fad5da680a27d5d3cb8bfaa185e304a89e + md5: 5b5203189eb668f042ac2b0826244964 + depends: + - mdurl >=0.1,<1 + - python >=3.10 + license: MIT + license_family: MIT + size: 64736 + timestamp: 1754951288511 +- conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + sha256: e0cbfea51a19b3055ca19428bd9233a25adca956c208abb9d00b21e7259c7e03 + md5: fab1be106a50e20f10fe5228fd1d1651 + depends: + - python >=3.10 + constrains: + - jinja2 >=3.0.0 + track_features: + - markupsafe_no_compile + license: BSD-3-Clause + license_family: BSD + size: 15499 + timestamp: 1759055275624 +- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.10.8-py314hdafbbf9_0.conda + sha256: 0c9417291ada8df3415ad13d52db38707adaba42584246264294e0faaaa54f77 + md5: 8286e3966eac286d5ac7c7a4afbac812 + depends: + - matplotlib-base >=3.10.8,<3.10.9.0a0 + - pyside6 >=6.7.2 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tornado >=5 + license: PSF-2.0 + license_family: PSF + size: 17473 + timestamp: 1763055464987 +- conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.10.8-py314hee6578b_0.conda + sha256: f32e8313e154db7b41c8147cb11f20c666e16b85abbc06ffebf7920c393aad0f + md5: 7fdf446de012e1750bf465b76412928d + depends: + - matplotlib-base >=3.10.8,<3.10.9.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tornado >=5 + license: PSF-2.0 + license_family: PSF + size: 17466 + timestamp: 1763055821938 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-3.10.8-py314he55896b_0.conda + sha256: 070b99e48cd6dda06086116626203c100e6f34af771b34384848ce5abeaf683e + md5: ad9a3f773f13989b92b41c0eabed5a38 + depends: + - matplotlib-base >=3.10.8,<3.10.9.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tornado >=5 + license: PSF-2.0 + license_family: PSF + size: 17538 + timestamp: 1763055987021 +- conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.10.8-py314h86ab7b2_0.conda + sha256: e7b6349b12f7d98ab7b595e01e486d3544083c694e8ee2c45a0b8f17016a7a0a + md5: e786fc5fefad7779cb2d954dd214fa37 + depends: + - matplotlib-base >=3.10.8,<3.10.9.0a0 + - pyside6 >=6.7.2 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tornado >=5 + license: PSF-2.0 + license_family: PSF + size: 18016 + timestamp: 1763056036732 +- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.8-py314h1194b4b_0.conda + sha256: ee773261fbd6c76fc8174b0e4e1ce272b0bbaa56610f130e9d3d1f575106f04f + md5: b8683e6068099b69c10dbfcf7204203f + depends: + - __glibc >=2.17,<3.0.a0 + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libgcc >=14 + - libstdcxx >=14 + - numpy >=1.23 + - numpy >=1.23,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.7 + - python_abi 3.14.* *_cp314 + - qhull >=2020.2,<2020.3.0a0 + - tk >=8.6.13,<8.7.0a0 + license: PSF-2.0 + license_family: PSF + size: 8473358 + timestamp: 1763055439346 +- conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.10.8-py314hd47142c_0.conda + sha256: 912302723c6be178ccf47386ed2cd70ef7a8604e52e957a2e8d3807abe938da5 + md5: 91d76a5937b47f7f0894857ce88feb9f + depends: + - __osx >=10.13 + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libcxx >=19 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - numpy >=1.23 + - numpy >=1.23,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.7 + - python_abi 3.14.* *_cp314 + - qhull >=2020.2,<2020.3.0a0 + license: PSF-2.0 + license_family: PSF + size: 8224527 + timestamp: 1763055779683 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.10.8-py314hd63e3f0_0.conda + sha256: 198dcc0ed83e78bc7bf48e6ef8d4ecd220e9cf1f07db98508251b2bc0be067f9 + md5: c84152e510d41378b8758826655b6ed7 + depends: + - __osx >=11.0 + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libcxx >=19 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - numpy >=1.23 + - numpy >=1.23,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python-dateutil >=2.7 + - python_abi 3.14.* *_cp314 + - qhull >=2020.2,<2020.3.0a0 + license: PSF-2.0 + license_family: PSF + size: 8286510 + timestamp: 1763055937766 +- conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.8-py314hfa45d96_0.conda + sha256: 82a50284275e8a1818cd3323846f3032dc89bd23a3f80dcf44e34a62b016256b + md5: 9d491a60700e0e90e92607fcc4e2566c + depends: + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - numpy >=1.23 + - numpy >=1.23,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.7 + - python_abi 3.14.* *_cp314 + - qhull >=2020.2,<2020.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: PSF-2.0 + license_family: PSF + size: 8185296 + timestamp: 1763055983613 +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + sha256: 9d690334de0cd1d22c51bc28420663f4277cfa60d34fa5cad1ce284a13f1d603 + md5: 00e120ce3e40bad7bfc78861ce3c4a25 + depends: + - python >=3.10 + - traitlets + license: BSD-3-Clause + license_family: BSD + size: 15175 + timestamp: 1761214578417 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 + md5: 592132998493b3ff25fd7479396e8351 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 14465 + timestamp: 1733255681319 +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_454.conda + sha256: 3c432e77720726c6bd83e9ee37ac8d0e3dd7c4cf9b4c5805e1d384025f9e9ab6 + md5: c83ec81713512467dfe1b496a8292544 + depends: + - llvm-openmp >=21.1.4 + - tbb >=2022.2.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + size: 99909095 + timestamp: 1761668703167 +- conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhcf101f3_1.conda + sha256: 449609f0d250607a300754474350a3b61faf45da183d3071e9720e453c765b8a + md5: 32f78e9d06e8593bc4bbf1338da06f5f + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 69210 + timestamp: 1764487059562 +- conda: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_1.conda + sha256: 7d7aa3fcd6f42b76bd711182f3776a02bef09a68c5f117d66b712a6d81368692 + md5: 3585aa87c43ab15b167b574cd73b057b + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 439705 + timestamp: 1733302781386 +- conda: https://conda.anaconda.org/conda-forge/noarch/multidict-6.7.0-pyh62beb40_0.conda + sha256: 1edb22a6cf563a24fcdd1185e9fd9b98b1571233460de1eefe903edd28ac8321 + md5: cf7c106c72e6fd92fee6ded0bd76d343 + depends: + - python >=3.10 + - typing-extensions >=4.1.0 + track_features: + - multidict_no_compile + license: Apache-2.0 + license_family: APACHE + size: 37469 + timestamp: 1765460459538 +- conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + sha256: d09c47c2cf456de5c09fa66d2c3c5035aa1fa228a1983a433c47b876aa16ce90 + md5: 37293a85a0f4f77bbd9cf7aaefc62609 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: Apache + size: 15851 + timestamp: 1749895533014 +- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 + md5: e9c622e0d00fa24a6292279af3ab6d06 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 11766 + timestamp: 1745776666688 +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.13.0-pyhcf101f3_0.conda + sha256: 03220ba0560de1d81b8b122e8ff6313238dbb1ed621db39f4b81f767904ed475 + md5: 0129bb97a81c2ca0f57031673424387a + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 268700 + timestamp: 1764604454148 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + sha256: ea4a5d27ded18443749aefa49dc79f6356da8506d508b5296f60b8d51e0c4bd9 + md5: ced34dd9929f491ca6dab6a2927aff25 + depends: + - __osx >=10.13 + license: X11 AND BSD-3-Clause + size: 822259 + timestamp: 1738196181298 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 + md5: 598fd7d4d0de2455fb74f56063969a97 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + size: 11543 + timestamp: 1733325673691 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + sha256: fd2cbd8dfc006c72f45843672664a8e4b99b2f8137654eaae8c3d46dca776f63 + md5: 16c2a0e9c4a166e53632cfca4f68d020 + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + size: 136216 + timestamp: 1758194284857 +- conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h53ec75d_1.conda + sha256: 186edb5fe84bddf12b5593377a527542f6ba42486ca5f49cd9dfeda378fb0fbe + md5: 5e9bee5fa11d91e1621e477c3cb9b9ba + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + size: 136667 + timestamp: 1758194361656 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h248ca61_1.conda + sha256: f6aa432b073778c3970d3115d291267f32ae85adfa99d80ff1abdf0b806aa249 + md5: 3ba9d0c21af2150cb92b2ab8bdad3090 + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + size: 136912 + timestamp: 1758194464430 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda + sha256: 6516f99fe400181ebe27cba29180ca0c7425c15d7392f74220a028ad0e0064a2 + md5: d8005b3a90515c952b51026f6b7d005d + depends: + - __glibc >=2.28,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - zstd >=1.5.7,<1.6.0a0 + - c-ares >=1.34.6,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libsqlite >=3.51.1,<4.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - libabseil >=20250512.1,<20250513.0a0 + - libabseil * cxx17* + - libzlib >=1.3.1,<2.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - icu >=75.1,<76.0a0 + license: MIT + license_family: MIT + size: 17246248 + timestamp: 1765444698486 +- conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda + sha256: 25ade898cb9e6f26622cc563dab89810f59e898e37ec4ffabd079f9f9a068998 + md5: 18ce8107e5d71b65aaa585c238a9e90d + depends: + - __osx >=11.0 + - libcxx >=19 + - libsqlite >=3.51.1,<4.0a0 + - libabseil >=20250512.1,<20250513.0a0 + - libabseil * cxx17* + - zstd >=1.5.7,<1.6.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - libuv >=1.51.0,<2.0a0 + - c-ares >=1.34.6,<2.0a0 + - icu >=75.1,<76.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + size: 16923801 + timestamp: 1765444650323 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda + sha256: acb4a33a096fa89d0ec0eea5d5f19988594d4e5c8d482ac60d2b0365d16dd984 + md5: 0b6dfe96bcfb469afe82885b3fecbd56 + depends: + - __osx >=11.0 + - libcxx >=19 + - libsqlite >=3.51.1,<4.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - openssl >=3.5.4,<4.0a0 + - c-ares >=1.34.6,<2.0a0 + - icu >=75.1,<76.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libabseil >=20250512.1,<20250513.0a0 + - libabseil * cxx17* + - libnghttp2 >=1.67.0,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + size: 16202237 + timestamp: 1765482731453 +- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda + sha256: 9742d28cf4a171dc9898bfb3c8512858f1ed46aa3cbc26d8839003d879564beb + md5: 461d47b472740c68ec0771c8b759868b + license: MIT + license_family: MIT + size: 30449097 + timestamp: 1765444649904 +- conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 + sha256: d38542a151a90417065c1a234866f97fd1ea82a81de75ecb725955ab78f88b4b + md5: 9a66894dfd07c4510beb6b3f9672ccc0 + constrains: + - mkl <0.a0 + license: BSD-3-Clause + license_family: BSD + size: 3843 + timestamp: 1582593857545 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.63.1-py314h8169c2f_0.conda + sha256: 6ab91790aeee336cc4526b02b477eb0f261df6bd9645f44a138b1e8a3ccc5e60 + md5: 9dfbe6bd11b1c77f618b347ec654b37b + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + - libgcc >=14 + - libstdcxx >=14 + - llvmlite >=0.46.0,<0.47.0a0 + - numpy >=1.22.3,<2.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - tbb >=2021.6.0 + - libopenblas !=0.3.6 + - cuda-version >=11.2 + - cudatoolkit >=11.2 + - cuda-python >=11.6 + - scipy >=1.0 + license: BSD-2-Clause + license_family: BSD + size: 5797268 + timestamp: 1765466862046 +- conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.63.0-py314h385e359_0.conda + sha256: 3c67a020a87a6cd378159bb9d08f998d808bfad29e6f9e147b0b183312e6baf0 + md5: 165bd22e3dc74ccd73e2f061ffecd870 + depends: + - __osx >=10.13 + - libcxx >=19 + - llvm-openmp >=19.1.7 + - llvm-openmp >=21.1.7 + - llvmlite >=0.46.0,<0.47.0a0 + - numpy >=1.22.3,<2.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - cuda-python >=11.6 + - libopenblas !=0.3.6 + - tbb >=2021.6.0 + - cuda-version >=11.2 + - scipy >=1.0 + - cudatoolkit >=11.2 + license: BSD-2-Clause + license_family: BSD + size: 5787861 + timestamp: 1765321991941 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.63.1-py314h945de62_0.conda + sha256: 4e6acf20fafec2b390e73c54bb348f71ef2fd0092e179e370fdf4ad4c2862baa + md5: 4f9128c2986d86725aa0dd5a5dfff168 + depends: + - __osx >=11.0 + - libcxx >=19 + - llvm-openmp >=19.1.7 + - llvm-openmp >=21.1.7 + - llvmlite >=0.46.0,<0.47.0a0 + - numpy >=1.22.3,<2.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - scipy >=1.0 + - libopenblas >=0.3.18,!=0.3.20 + - cudatoolkit >=11.2 + - cuda-version >=11.2 + - cuda-python >=11.6 + - tbb >=2021.6.0 + license: BSD-2-Clause + license_family: BSD + size: 5780959 + timestamp: 1765466926700 +- conda: https://conda.anaconda.org/conda-forge/win-64/numba-0.63.1-py314h36f8cf2_0.conda + sha256: 1bbfc2793e04aaac5d289e6e5bec8b020b4419c4af1e161ab409c6995d1cc89d + md5: a77827229f4dfdbae9d503707d41a277 + depends: + - llvmlite >=0.46.0,<0.47.0a0 + - numpy >=1.22.3,<2.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - tbb >=2021.6.0 + - libopenblas !=0.3.6 + - cuda-version >=11.2 + - cudatoolkit >=11.2 + - scipy >=1.0 + - cuda-python >=11.6 + license: BSD-2-Clause + license_family: BSD + size: 5775759 + timestamp: 1765466860567 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.1-py314heb044ea_101.conda + sha256: d9911d3d54c8fe25e4506c3171fee107a2222b60b7916ba9e8aa10e0b39153ea + md5: 9b1f7d691ba516ec40fa43fc28fcf5be + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - nomkl + - numpy >=1.23,<3 + - numpy >=1.23.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 217238 + timestamp: 1762594968114 +- conda: https://conda.anaconda.org/conda-forge/osx-64/numexpr-2.14.1-py314h205861b_1.conda + sha256: 68d602e1fea2626e802ba541aa8620032c9f7a5cab0ef73193429a57f56fc19d + md5: 9bfbdd8222dc1cffa8fda9000e5edd60 + depends: + - __osx >=10.13 + - libcxx >=19 + - numpy >=1.23,<3 + - numpy >=1.23.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 209762 + timestamp: 1762595270088 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numexpr-2.14.1-py314hc5bb990_1.conda + sha256: 36fec9e03675c08ebcba1a85dd8d1de0962bf433ee8ea65e832805466537741f + md5: 4dcec6227b059dae2fc56a5f58ddda48 + depends: + - __osx >=11.0 + - libcxx >=19 + - numpy >=1.23,<3 + - numpy >=1.23.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 202180 + timestamp: 1762595578484 +- conda: https://conda.anaconda.org/conda-forge/win-64/numexpr-2.14.1-mkl_py314h220b711_1.conda + sha256: 421d316bd2d3bc3e9ccd16bf4e937481292dcb20aa03f6b11c101c892f5f120b + md5: 3ee35b3d4e12cbb427bde41eb4a2c174 + depends: + - libblas * *mkl + - mkl >=2025.3.0,<2026.0a0 + - numpy >=1.23,<3 + - numpy >=1.23.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 210520 + timestamp: 1764766861691 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py314h2b28147_0.conda + sha256: 4fa3b8b80dd848a70f679b31d74d6fb28f9c4de9cd81086aa8e10256e9de20d1 + md5: 6d2cff81447b8fe424645d7dd3bde8bf + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + size: 8983459 + timestamp: 1763350996398 +- conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.3.5-py314hf08249b_0.conda + sha256: 77e0b2ddb433ac23ca9d587c37a8f6da9baee3888c34d19e530fe8cbaaf49bdc + md5: 5c9e4bc0c170115fd3602d7377c9e8da + depends: + - python + - libcxx >=19 + - __osx >=10.13 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - python_abi 3.14.* *_cp314 + - libblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + size: 8140127 + timestamp: 1763350902772 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.5-py314h5b5928d_0.conda + sha256: a8731e3e31013be69cb585dbc57cb225437bb0c945ddce9a550c1cd10b6fad37 + md5: e126981f973ddc2510d7a249c5b69533 + depends: + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - libcxx >=19 + - libcblas >=3.9.0,<4.0a0 + - libblas >=3.9.0,<4.0a0 + - python_abi 3.14.* *_cp314 + - liblapack >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + size: 6861174 + timestamp: 1763350930747 +- conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.5-py314h06c3c77_0.conda + sha256: e64d4c049c9c69ef02d924ac1750b32e08f57732cbc6a3fe11794f3169b59d14 + md5: ddc6687a8f402695bd22229aaf69fb26 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - liblapack >=3.9.0,<4.0a0 + - python_abi 3.14.* *_cp314 + - libcblas >=3.9.0,<4.0a0 + - libblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + size: 7588219 + timestamp: 1763350950306 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + sha256: 3900f9f2dbbf4129cf3ad6acf4e4b6f7101390b53843591c53b00f034343bc4d + md5: 11b3379b191f63139e29c0d19dee24cd + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpng >=1.6.50,<1.7.0a0 + - libstdcxx >=14 + - libtiff >=4.7.1,<4.8.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-2-Clause + license_family: BSD + size: 355400 + timestamp: 1758489294972 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.4-h87e8dc5_0.conda + sha256: fdf4708a4e45b5fd9868646dd0c0a78429f4c0b8be490196c975e06403a841d0 + md5: a67d3517ebbf615b91ef9fdc99934e0c + depends: + - __osx >=10.13 + - libcxx >=19 + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-2-Clause + license_family: BSD + size: 334875 + timestamp: 1758489493148 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + sha256: dd73e8f1da7dd6a5494c5586b835cbe2ec68bace55610b1c4bf927400fe9c0d7 + md5: 6bf3d24692c157a41c01ce0bd17daeea + depends: + - __osx >=11.0 + - libcxx >=19 + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-2-Clause + license_family: BSD + size: 319967 + timestamp: 1758489514651 +- conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda + sha256: 226c270a7e3644448954c47959c00a9bf7845f6d600c2a643db187118d028eee + md5: 5af852046226bb3cb15c7f61c2ac020a + depends: + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + size: 244860 + timestamp: 1758489556249 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.10-he970967_0.conda + sha256: cb0b07db15e303e6f0a19646807715d28f1264c6350309a559702f4f34f37892 + md5: 2e5bf4f1da39c0b32778561c3c4e5878 + depends: + - __glibc >=2.17,<3.0.a0 + - cyrus-sasl >=2.1.27,<3.0a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libstdcxx >=13 + - openssl >=3.5.0,<4.0a0 + license: OLDAP-2.8 + license_family: BSD + size: 780253 + timestamp: 1748010165522 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + sha256: a47271202f4518a484956968335b2521409c8173e123ab381e775c358c67fe6d + md5: 9ee58d5c534af06558933af3c845a780 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + size: 3165399 + timestamp: 1762839186699 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + sha256: 36fe9fb316be22fcfb46d5fa3e2e85eec5ef84f908b7745f68f768917235b2d5 + md5: 3f50cdf9a97d0280655758b735781096 + depends: + - __osx >=10.13 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2778996 + timestamp: 1762840724922 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988 + md5: b34dc4172653c13dcf453862f251af2b + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 3108371 + timestamp: 1762839712322 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda + sha256: 6d72d6f766293d4f2aa60c28c244c8efed6946c430814175f959ffe8cab899b3 + md5: 84f8fb4afd1157f59098f618cd2437e4 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 9440812 + timestamp: 1762841722179 +- conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.1-hd747db4_0.conda + sha256: 8d91d6398fc63a94d238e64e4983d38f6f9555460f11bed00abb2da04dbadf7c + md5: ddab8b2af55b88d63469c040377bd37e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.2,<1.3.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 + license_family: Apache + size: 1316445 + timestamp: 1759424644934 +- conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.2.1-hd1b02dc_0.conda + sha256: a00d48750d2140ea97d92b32c171480b76b2632dbb9d19d1ae423999efcc825f + md5: b4646b6ddcbcb3b10e9879900c66ed48 + depends: + - __osx >=11.0 + - libcxx >=19 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.2,<1.3.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 + license_family: Apache + size: 521463 + timestamp: 1759424838652 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.2.1-h4fd0076_0.conda + sha256: f0a31625a647cb8d55a7016950c11f8fabc394df5054d630e9c9b526bf573210 + md5: b5dea50c77ab3cc18df48bdc9994ac44 + depends: + - __osx >=11.0 + - libcxx >=19 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.2,<1.3.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 + license_family: Apache + size: 487298 + timestamp: 1759424875005 +- conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.2.1-h7414dfc_0.conda + sha256: f28f8f2d743c2091f76161b8d59f82c4ba4970d03cb9900c52fb908fe5e8a7c4 + md5: a9b6ebf475194b0e5ad43168e9b936a7 + depends: + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.2,<1.3.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 + license_family: Apache + size: 1064397 + timestamp: 1759424869069 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + sha256: 289861ed0c13a15d7bbb408796af4de72c2fe67e2bcb0de98f4c3fce259d7991 + md5: 58335b26c38bf4a20f399384c33cbcf9 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + size: 62477 + timestamp: 1745345660407 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py314ha0b5721_2.conda + sha256: 0a86a582b906d9cfd4d2c59180898fe9d714b55eea7ced71630a1fedae206c62 + md5: fe3a5c8be07a7b82058bdeb39d33d93b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.14.* *_cp314 + - pytz >=2020.1 + constrains: + - pyarrow >=10.0.1 + - numba >=0.56.4 + - odfpy >=1.4.1 + - xlsxwriter >=3.0.5 + - tabulate >=0.9.0 + - html5lib >=1.1 + - lxml >=4.9.2 + - blosc >=1.21.3 + - s3fs >=2022.11.0 + - fsspec >=2022.11.0 + - psycopg2 >=2.9.6 + - pandas-gbq >=0.19.0 + - openpyxl >=3.1.0 + - qtpy >=2.3.0 + - python-calamine >=0.1.7 + - sqlalchemy >=2.0.0 + - pyqt5 >=5.15.9 + - bottleneck >=1.3.6 + - zstandard >=0.19.0 + - numexpr >=2.8.4 + - tzdata >=2022.7 + - scipy >=1.10.0 + - gcsfs >=2022.11.0 + - pyxlsb >=1.0.10 + - matplotlib >=3.6.3 + - pytables >=3.8.0 + - beautifulsoup4 >=4.11.2 + - pyreadstat >=1.2.0 + - fastparquet >=2022.12.0 + - xlrd >=2.0.1 + - xarray >=2022.12.0 + license: BSD-3-Clause + license_family: BSD + size: 15178918 + timestamp: 1764615084415 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-2.3.3-py314hc4308db_2.conda + sha256: 66df07b283018490ca7e75fd869a4ad8e542e61bf916f17463c8ad022cce7ffd + md5: b082e18eb2696625aa09c80e0fbd1997 + depends: + - __osx >=10.13 + - libcxx >=19 + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.14.* *_cp314 + - pytz >=2020.1 + constrains: + - openpyxl >=3.1.0 + - lxml >=4.9.2 + - tzdata >=2022.7 + - blosc >=1.21.3 + - pandas-gbq >=0.19.0 + - pyarrow >=10.0.1 + - odfpy >=1.4.1 + - sqlalchemy >=2.0.0 + - bottleneck >=1.3.6 + - gcsfs >=2022.11.0 + - beautifulsoup4 >=4.11.2 + - fsspec >=2022.11.0 + - numba >=0.56.4 + - pyxlsb >=1.0.10 + - scipy >=1.10.0 + - pyqt5 >=5.15.9 + - xarray >=2022.12.0 + - qtpy >=2.3.0 + - numexpr >=2.8.4 + - tabulate >=0.9.0 + - pyreadstat >=1.2.0 + - zstandard >=0.19.0 + - html5lib >=1.1 + - matplotlib >=3.6.3 + - xlsxwriter >=3.0.5 + - fastparquet >=2022.12.0 + - python-calamine >=0.1.7 + - xlrd >=2.0.1 + - pytables >=3.8.0 + - psycopg2 >=2.9.6 + - s3fs >=2022.11.0 + license: BSD-3-Clause + license_family: BSD + size: 14362288 + timestamp: 1764615196689 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.3-py314ha3d490a_2.conda + sha256: f71fc63904d80ef7bf4e882b420426e167e02cf68b9bd71ea6beb0a9d0c37430 + md5: 6e2f31aca92c525a884c509738aca93a + depends: + - __osx >=11.0 + - libcxx >=19 + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.14.* *_cp314 + - pytz >=2020.1 + constrains: + - odfpy >=1.4.1 + - zstandard >=0.19.0 + - blosc >=1.21.3 + - html5lib >=1.1 + - numexpr >=2.8.4 + - gcsfs >=2022.11.0 + - sqlalchemy >=2.0.0 + - numba >=0.56.4 + - pyqt5 >=5.15.9 + - fastparquet >=2022.12.0 + - pandas-gbq >=0.19.0 + - pytables >=3.8.0 + - qtpy >=2.3.0 + - fsspec >=2022.11.0 + - s3fs >=2022.11.0 + - pyreadstat >=1.2.0 + - pyxlsb >=1.0.10 + - pyarrow >=10.0.1 + - xlrd >=2.0.1 + - xarray >=2022.12.0 + - beautifulsoup4 >=4.11.2 + - tabulate >=0.9.0 + - psycopg2 >=2.9.6 + - bottleneck >=1.3.6 + - matplotlib >=3.6.3 + - python-calamine >=0.1.7 + - lxml >=4.9.2 + - openpyxl >=3.1.0 + - scipy >=1.10.0 + - xlsxwriter >=3.0.5 + - tzdata >=2022.7 + license: BSD-3-Clause + license_family: BSD + size: 14130201 + timestamp: 1764615862386 +- conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.3-py314hd8fd7ce_2.conda + sha256: a1c87d34f72d6ae3f78203c60cf1b1adfb8d5cf55a3fc90f47e9f9ed50eb8b91 + md5: 95cf7fc22f898b6faeb1d62ce2f5b82c + depends: + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.14,<3.15.0a0 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.14.* *_cp314 + - pytz >=2020.1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - scipy >=1.10.0 + - sqlalchemy >=2.0.0 + - fsspec >=2022.11.0 + - pyreadstat >=1.2.0 + - gcsfs >=2022.11.0 + - tabulate >=0.9.0 + - openpyxl >=3.1.0 + - pytables >=3.8.0 + - qtpy >=2.3.0 + - matplotlib >=3.6.3 + - bottleneck >=1.3.6 + - python-calamine >=0.1.7 + - numba >=0.56.4 + - beautifulsoup4 >=4.11.2 + - tzdata >=2022.7 + - xarray >=2022.12.0 + - pyqt5 >=5.15.9 + - odfpy >=1.4.1 + - xlrd >=2.0.1 + - pyarrow >=10.0.1 + - s3fs >=2022.11.0 + - psycopg2 >=2.9.6 + - pandas-gbq >=0.19.0 + - xlsxwriter >=3.0.5 + - fastparquet >=2022.12.0 + - numexpr >=2.8.4 + - zstandard >=0.19.0 + - lxml >=4.9.2 + - pyxlsb >=1.0.10 + - html5lib >=1.1 + - blosc >=1.21.3 + license: BSD-3-Clause + license_family: BSD + size: 14046781 + timestamp: 1764615388271 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + sha256: 30de7b4d15fbe53ffe052feccde31223a236dae0495bab54ab2479de30b2990f + md5: a110716cdb11cf51482ff4000dc253d7 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 81562 + timestamp: 1755974222274 +- conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + sha256: 472fc587c63ec4f6eba0cc0b06008a6371e0a08a5986de3cf4e8024a47b4fe6c + md5: 0badf9c54e24cecfb0ad2f99d680c163 + depends: + - locket + - python >=3.9 + - toolz + license: BSD-3-Clause + license_family: BSD + size: 20884 + timestamp: 1715026639309 +- conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + sha256: 9f64009cdf5b8e529995f18e03665b03f5d07c0b17445b8badef45bde76249ee + md5: 617f15191456cc6a13db418a275435e5 + depends: + - python >=3.9 + license: MPL-2.0 + license_family: MOZILLA + size: 41075 + timestamp: 1733233471940 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + sha256: 5c7380c8fd3ad5fc0f8039069a45586aa452cf165264bc5a437ad80397b32934 + md5: 7fa07cb0fb1b625a089ccc01218ee5b1 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 1209177 + timestamp: 1756742976157 +- conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda + sha256: 29c2ed44a8534d27faad96bdce16efe29c2788f556f4c5409d4ae8ae074681ec + md5: 889053e920d15353c2665fa6310d7a7a + depends: + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 1034703 + timestamp: 1756743085974 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + size: 53561 + timestamp: 1733302019362 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.0.0-py314h8ec4b1a_2.conda + sha256: e08f64a5df6ced2a5a366d82377857d7e71ff7b74a3dd1db5b6ddbca39cbe6e1 + md5: 8cad8a4569a55fe71631eaaea27fe451 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxcb >=1.17.0,<2.0a0 + - tk >=8.6.13,<8.7.0a0 + - openjpeg >=2.5.4,<3.0a0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - lcms2 >=2.17,<3.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - python_abi 3.14.* *_cp314 + - zlib-ng >=2.3.1,<2.4.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + license: HPND + size: 1071517 + timestamp: 1764330106864 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-12.0.0-py314hedf0282_2.conda + sha256: becb686065e95a5ab8acd1ea7894a01dc7f0a736413bb4a7f9fbaad7c96cb2f1 + md5: 399177697c7225b64edeaeb373a8c98b + depends: + - python + - __osx >=10.13 + - libwebp-base >=1.6.0,<2.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libxcb >=1.17.0,<2.0a0 + - zlib-ng >=2.3.1,<2.4.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - lcms2 >=2.17,<3.0a0 + - python_abi 3.14.* *_cp314 + - openjpeg >=2.5.4,<3.0a0 + - tk >=8.6.13,<8.7.0a0 + license: HPND + size: 1002287 + timestamp: 1764330319004 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.0.0-py314h57fbdfe_2.conda + sha256: 2cf1346e3aa8ab9f73d533fb55d753dd3a8d64b50f86d1f0e4f3ff8669c3b0d9 + md5: 8c10435c6b30aaa4c376106d68298f6f + depends: + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - tk >=8.6.13,<8.7.0a0 + - openjpeg >=2.5.4,<3.0a0 + - zlib-ng >=2.3.1,<2.4.0a0 + - python_abi 3.14.* *_cp314 + - libxcb >=1.17.0,<2.0a0 + - lcms2 >=2.17,<3.0a0 + - libwebp-base >=1.6.0,<2.0a0 + license: HPND + size: 993019 + timestamp: 1764330196019 +- conda: https://conda.anaconda.org/conda-forge/win-64/pillow-12.0.0-py314h61b30b5_2.conda + sha256: a428b9d5c64d1ab9eac878755e72301003efe618d1430e13a18ebdf5f332dfd8 + md5: 27562522f8d26e6fad29e234f6ae48be + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + - libxcb >=1.17.0,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - zlib-ng >=2.3.1,<2.4.0a0 + - lcms2 >=2.17,<3.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - openjpeg >=2.5.4,<3.0a0 + license: HPND + size: 971941 + timestamp: 1764330112083 +- conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda + sha256: 4d5e2faca810459724f11f78d19a0feee27a7be2b3fc5f7abbbec4c9fdcae93d + md5: bf47878473e5ab9fdb4115735230e191 + depends: + - python >=3.13.0a0 + license: MIT + license_family: MIT + size: 1177084 + timestamp: 1762776338614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + sha256: 43d37bc9ca3b257c5dd7bf76a8426addbdec381f6786ff441dc90b1a49143b6a + md5: c01af13bdc553d1a8fbfff6e8db075f0 + depends: + - libgcc >=14 + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + size: 450960 + timestamp: 1754665235234 +- conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda + sha256: 246fce4706b3f8b247a7d6142ba8d732c95263d3c96e212b9d63d6a4ab4aff35 + md5: 08c8fa3b419df480d985e304f7884d35 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 542795 + timestamp: 1754665193489 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + sha256: 04c64fb78c520e5c396b6e07bc9082735a5cc28175dbe23138201d0a9441800b + md5: 1bd2e65c8c7ef24f4639ae6e850dacc2 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 23922 + timestamp: 1764950726246 +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + size: 25877 + timestamp: 1764896838868 +- conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + sha256: 013669433eb447548f21c3c6b16b2ed64356f726b5f77c1b39d5ba17a8a4b8bc + md5: a83f6a2fdc079e643237887a37460668 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.10.1,<9.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + size: 199544 + timestamp: 1730769112346 +- conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda + sha256: af754a477ee2681cb7d5d77c621bd590d25fe1caf16741841fc2d176815fc7de + md5: f36107fa2557e63421a46676371c4226 + depends: + - __osx >=10.13 + - libcurl >=8.10.1,<9.0a0 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + size: 179103 + timestamp: 1730769223221 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda + sha256: 851a77ae1a8e90db9b9f3c4466abea7afb52713c3d98ceb0d37ba6ff27df2eff + md5: 7172339b49c94275ba42fec3eaeda34f + depends: + - __osx >=11.0 + - libcurl >=8.10.1,<9.0a0 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + size: 173220 + timestamp: 1730769371051 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae + md5: edb16f14d920fb3faf17f5ce582942d6 + depends: + - python >=3.10 + - wcwidth + constrains: + - prompt_toolkit 3.0.52 + license: BSD-3-Clause + license_family: BSD + size: 273927 + timestamp: 1756321848365 +- conda: https://conda.anaconda.org/conda-forge/noarch/propcache-0.3.1-pyhe1237c8_0.conda + sha256: d8927d64b35e1fb82285791444673e47d3729853be962c7045e75fc0fd715cec + md5: b1cda654f58d74578ac9786909af84cd + depends: + - python >=3.9 + track_features: + - propcache_no_compile + license: Apache-2.0 + license_family: APACHE + size: 17693 + timestamp: 1744525054494 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.3-py314h0f05182_0.conda + sha256: 7c5d69ad61fe4e0d3657185f51302075ef5b9e34686238c6b3bde102344d4390 + md5: aee1c9aecc66339ea6fd89e6a143a282 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 509226 + timestamp: 1762092897605 +- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.3-py314hd1e8ddb_0.conda + sha256: 444a73838eff6d7d35e22a684c1774dacd191500c3e27a828ec1ed0f96d5f70d + md5: 3156552ec761b34da86aeb273e725a25 + depends: + - python + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 520432 + timestamp: 1762093042719 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.3-py314h9d33bd4_0.conda + sha256: e69d9bdc482596abb10a7d54094e3f6a80ccba5b710353e9bda7d3313158985f + md5: 7259e501bb4288143582312017bb1e44 + depends: + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 523325 + timestamp: 1762093068430 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.3-py314hc5dbbe4_0.conda + sha256: 1cdcd27f34682414d2481835ff13797e532f28e518bd451256c34952cf37c34c + md5: c96a29c38696f7dcaf486c4a33cd1063 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 527946 + timestamp: 1762092943903 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 + md5: b3c17d95b5a10c6e64a21fa17573e70e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + size: 8252 + timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda + sha256: 05944ca3445f31614f8c674c560bca02ff05cb51637a96f665cb2bbe496099e5 + md5: 8bcf980d2c6b17094961198284b8e862 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 8364 + timestamp: 1726802331537 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + sha256: 8ed65e17fbb0ca944bfb8093b60086e3f9dd678c3448b5de212017394c247ee3 + md5: 415816daf82e0b23a736a069a75e9da7 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 8381 + timestamp: 1726802424786 +- conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + sha256: 7e446bafb4d692792310ed022fe284e848c6a868c861655a92435af7368bae7b + md5: 3c8f2573569bb816483e5cf57efbbe29 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 9389 + timestamp: 1726802555076 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + size: 19457 + timestamp: 1733302371990 +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 16668 + timestamp: 1733569518868 +- conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda + sha256: 6d8f03c13d085a569fde931892cded813474acbef2e03381a1a87f420c7da035 + md5: 46830ee16925d5ed250850503b5dc3a8 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 25766 + timestamp: 1733236452235 +- conda: https://conda.anaconda.org/conda-forge/noarch/py2vega-0.6.1-pyhd8ed1ab_0.tar.bz2 + sha256: 1637e850576b0cc1fda0fb2f4a4396bb30b140888e83787de2c8746af3df675e + md5: 07594783f950301f5943e6d080ffb4eb + depends: + - gast >=0.4,<0.5 + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + size: 16798 + timestamp: 1614765686812 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-22.0.0-py314hdafbbf9_0.conda + sha256: c10ea8100848236cda04307a00cdeba5a86358fc537132ffcc5cac8cc27f5547 + md5: ecb1085032bfa2bbd310807ca6c0c7f6 + depends: + - libarrow-acero 22.0.0.* + - libarrow-dataset 22.0.0.* + - libarrow-substrait 22.0.0.* + - libparquet 22.0.0.* + - pyarrow-core 22.0.0 *_0_* + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: APACHE + size: 26193 + timestamp: 1761648748916 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-22.0.0-py314hee6578b_0.conda + sha256: dd884207ed4c43d566a0fb6d46135669932dafce3f646f287b2c1347b1cb7391 + md5: 13fdbf20848018c21129b27b696c4e90 + depends: + - libarrow-acero 22.0.0.* + - libarrow-dataset 22.0.0.* + - libarrow-substrait 22.0.0.* + - libparquet 22.0.0.* + - pyarrow-core 22.0.0 *_0_* + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: APACHE + size: 26271 + timestamp: 1761648628782 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-22.0.0-py314he55896b_0.conda + sha256: 1c15052ed5cdd0478964ea0b0f73bbc5db1c49f9b6923a378ba4b8dd2d9b802d + md5: 27b21816e9427b5bb9f5686c122b8730 + depends: + - libarrow-acero 22.0.0.* + - libarrow-dataset 22.0.0.* + - libarrow-substrait 22.0.0.* + - libparquet 22.0.0.* + - pyarrow-core 22.0.0 *_0_* + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: APACHE + size: 26356 + timestamp: 1761649037869 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-22.0.0-py314h86ab7b2_0.conda + sha256: 78c7195c8f4c853e8ff1948f5908af70d523a8d9e708879b47ee4f9a4808f0d7 + md5: bf483b00a926179e1f4a8122c64f7a10 + depends: + - libarrow-acero 22.0.0.* + - libarrow-dataset 22.0.0.* + - libarrow-substrait 22.0.0.* + - libparquet 22.0.0.* + - pyarrow-core 22.0.0 *_0_* + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: APACHE + size: 26652 + timestamp: 1761648406768 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-22.0.0-py314h52d6ec5_0_cpu.conda + sha256: 89d1fdb21ca6488c2e7a262d84eaf3ab4fbdd555a3ce91915869d9bfe640b92e + md5: 3c690d2816c2fe6e8d02a0f60549a393 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 22.0.0.* *cpu + - libarrow-compute 22.0.0.* *cpu + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - apache-arrow-proc * cpu + - numpy >=1.21,<3 + license: Apache-2.0 + license_family: APACHE + size: 4814230 + timestamp: 1761648682122 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-22.0.0-py314h35e0213_0_cpu.conda + sha256: c502d7118b4b5fd59e38f5e8b5ac702ab2923f4c3f0fbbd71a8310fa47aef00b + md5: d46aeaef96eb344a170c178dc7f40a2d + depends: + - __osx >=10.13 + - libarrow 22.0.0.* *cpu + - libarrow-compute 22.0.0.* *cpu + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - apache-arrow-proc * cpu + - numpy >=1.21,<3 + license: Apache-2.0 + license_family: APACHE + size: 4792989 + timestamp: 1761648579819 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-22.0.0-py314hf20a12a_0_cpu.conda + sha256: d06476026a96d93bc44b0269e8b9abcc2b18adb56d82cd69d2f33e8cc0b47299 + md5: e02b151500dcd291ab7cd8f2bd46fef3 + depends: + - __osx >=11.0 + - libarrow 22.0.0.* *cpu + - libarrow-compute 22.0.0.* *cpu + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - numpy >=1.21,<3 + - apache-arrow-proc * cpu + license: Apache-2.0 + license_family: APACHE + size: 3912295 + timestamp: 1761648977007 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-core-22.0.0-py314hb5be3fa_0_cpu.conda + sha256: 316711f94c4bc8420479fabef4ab6d9c3a46d00bce2b0e402bd205c7954bff82 + md5: 5158c4f9ae4dc6924c4096f5745626f2 + depends: + - libarrow 22.0.0.* *cpu + - libarrow-compute 22.0.0.* *cpu + - libzlib >=1.3.1,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - numpy >=1.21,<3 + - apache-arrow-proc * cpu + license: Apache-2.0 + license_family: APACHE + size: 3526470 + timestamp: 1761648362882 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + size: 110100 + timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.5-pyhcf101f3_1.conda + sha256: 868569d9505b7fe246c880c11e2c44924d7613a8cdcc1f6ef85d5375e892f13d + md5: c3946ed24acdb28db1b5d63321dbca7d + depends: + - typing-inspection >=0.4.2 + - typing_extensions >=4.14.1 + - python >=3.10 + - typing-extensions >=4.6.1 + - annotated-types >=0.6.0 + - pydantic-core ==2.41.5 + - python + license: MIT + license_family: MIT + size: 340482 + timestamp: 1764434463101 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.41.5-py314h2e6c369_1.conda + sha256: 7e0ae379796e28a429f8e48f2fe22a0f232979d65ec455e91f8dac689247d39f + md5: 432b0716a1dfac69b86aa38fdd59b7e6 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 1943088 + timestamp: 1762988995556 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.41.5-py314ha7b6dee_1.conda + sha256: 7cb259e46ecb9f19eeea4d96035546376ce9370b51ffd18d57eb7170b08bbbf4 + md5: 8a9a08b79d530f482c9439790db774e1 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 1949458 + timestamp: 1762989007303 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.41.5-py314haad56a0_1.conda + sha256: dded9092d89f1d8c267d5ce8b5e21f935c51acb7a64330f507cdfb3b69a98116 + md5: 420a4b8024e9b22880f1e03b612afa7d + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __osx >=11.0 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 1784478 + timestamp: 1762989019956 +- conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.41.5-py314h9f07db2_1.conda + sha256: 51773479d973c0b0b96cf581cb8444061eaac9b6c28f1cc6d33afc39201d5f13 + md5: c1f37669ed289c378f3193b35c9df2a7 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 1971476 + timestamp: 1762989023313 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyerfa-2.0.1.5-py310h32771cd_2.conda + noarch: python + sha256: a3f25f921be09e15ed6ff46a1ec99ce9cca6affa4a086f6f39ad630e21e48fb7 + md5: e6efd9593a25d093b4ce9dd8053c4af7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - numpy >=1.21,<3 + - python + license: BSD-3-Clause + license_family: BSD + size: 295617 + timestamp: 1756821497270 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyerfa-2.0.1.5-py310hcbffc5d_2.conda + noarch: python + sha256: 06beb9ed2f6df706b5bd050e42819e49606d6256fe66dc7255c577a0140a2379 + md5: cd854c208de8cd3e2a6a878500021633 + depends: + - __osx >=10.13 + - numpy >=1.21,<3 + - python + license: BSD-3-Clause + license_family: BSD + size: 270277 + timestamp: 1756821799013 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyerfa-2.0.1.5-py310hbb12772_2.conda + noarch: python + sha256: ec2a947d95ffb46ca3a818272c8594f195b1e74369164c46f4512b2d66f7f4c4 + md5: 51a8f8137ff9e55513e5e722c86fb9f8 + depends: + - __osx >=11.0 + - numpy >=1.21,<3 + - python + license: BSD-3-Clause + license_family: BSD + size: 271352 + timestamp: 1756821964759 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyerfa-2.0.1.5-py310h1f63838_2.conda + noarch: python + sha256: 6d4df9c23096118c062254c5f4a9a7e111db198c3ed834b4180f2df145b513de + md5: 215016438dc9d0808d9409c250f26966 + depends: + - numpy >=1.21,<3 + - python + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 296016 + timestamp: 1756821645023 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a + md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + size: 889287 + timestamp: 1750615908735 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + sha256: 6814b61b94e95ffc45ec539a6424d8447895fef75b0fec7e1be31f5beee883fb + md5: 6c8979be6d7a17692793114fa26916e8 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 104044 + timestamp: 1758436411254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.9.3-py314hf36963e_1.conda + sha256: 54051f72018c7a980578859e3340ba2e4d529f064e5850db4314995ca0d6fc56 + md5: 8d1ffa0a622e8dda170beeadd1795e88 + depends: + - __glibc >=2.17,<3.0.a0 + - libclang13 >=21.1.2 + - libegl >=1.7.0,<2.0a0 + - libgcc >=14 + - libgl >=1.7.0,<2.0a0 + - libopengl >=1.7.0,<2.0a0 + - libstdcxx >=14 + - libvulkan-loader >=1.4.313.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - python >=3.14.0rc3,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - qt6-main 6.9.3.* + - qt6-main >=6.9.3,<6.10.0a0 + license: LGPL-3.0-only + license_family: LGPL + size: 10141491 + timestamp: 1759403061203 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyside6-6.9.3-py314h2c9462b_1.conda + sha256: dbd0e599d3155472c147e2fe75326f8379d6d6f1ac7905b5dc9d64e49c1242a8 + md5: ad4318d725ce9acbf8714ad3e9a2e0bf + depends: + - libclang13 >=21.1.2 + - libvulkan-loader >=1.4.313.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - python >=3.14.0rc3,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - qt6-main 6.9.3.* + - qt6-main >=6.9.3,<6.10.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-3.0-only + license_family: LGPL + size: 8901954 + timestamp: 1759403164005 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca + md5: e2fd202833c4a981ce8a65974fe4abd1 + depends: + - __win + - python >=3.9 + - win_inet_pton + license: BSD-3-Clause + license_family: BSD + size: 21784 + timestamp: 1733217448189 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pytables-3.10.2-py314h5611b9a_10.conda + sha256: 94628fe932e7aee3fdf4bdfd4a1832324b5a833b98ba103ac69e42d30514953c + md5: 98f9d542e85ac1ae6fcefa3ba3407e2d + depends: + - __glibc >=2.17,<3.0.a0 + - blosc >=1.21.6,<2.0a0 + - bzip2 >=1.0.8,<2.0a0 + - c-blosc2 >=2.22.0,<2.23.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - numexpr + - numpy >=1.20.0 + - numpy >=1.23,<3 + - packaging + - py-cpuinfo + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - typing-extensions >=4.4.0 + license: BSD-3-Clause + license_family: BSD + size: 1710124 + timestamp: 1761751448658 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pytables-3.10.2-py314hb51f073_10.conda + sha256: bc57d59d261b16f3086895b20d3c2ec2cbee1fae10f760197f304f54fba58d40 + md5: b32db0844a5993c9a7b2e975eae6a28b + depends: + - __osx >=10.13 + - blosc >=1.21.6,<2.0a0 + - bzip2 >=1.0.8,<2.0a0 + - c-blosc2 >=2.22.0,<2.23.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + - numexpr + - numpy >=1.20.0 + - numpy >=1.23,<3 + - packaging + - py-cpuinfo + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - typing-extensions >=4.4.0 + license: BSD-3-Clause + license_family: BSD + size: 1592351 + timestamp: 1761751753319 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pytables-3.10.2-py314h8eb144a_10.conda + sha256: 2862fad997d1cfa074be171403dfa5f983080062f56f98875cc5c3fd7462f7fd + md5: 86860ff3ab5e016d5af5a0eca346b31b + depends: + - __osx >=11.0 + - blosc >=1.21.6,<2.0a0 + - bzip2 >=1.0.8,<2.0a0 + - c-blosc2 >=2.22.0,<2.23.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + - numexpr + - numpy >=1.20.0 + - numpy >=1.23,<3 + - packaging + - py-cpuinfo + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - typing-extensions >=4.4.0 + license: BSD-3-Clause + license_family: BSD + size: 1777276 + timestamp: 1761751746264 +- conda: https://conda.anaconda.org/conda-forge/win-64/pytables-3.10.2-py314h2bd12ea_10.conda + sha256: 9cd2e83780fbe86069da001c985a7ff90862b138214b6d9744b87c5bf1e0b083 + md5: 63a28f5789c3e30019c7beda4323c0f0 + depends: + - blosc >=1.21.6,<2.0a0 + - bzip2 >=1.0.8,<2.0a0 + - c-blosc2 >=2.22.0,<2.23.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - numexpr + - numpy >=1.20.0 + - numpy >=1.23,<3 + - packaging + - py-cpuinfo + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - typing-extensions >=4.4.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 1538261 + timestamp: 1761831793226 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + sha256: 9e749fb465a8bedf0184d8b8996992a38de351f7c64e967031944978de03a520 + md5: 2b694bad8a50dc2f712f5368de866480 + depends: + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - colorama >=0.4 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + size: 299581 + timestamp: 1765062031645 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.3.0-pyhcf101f3_0.conda + sha256: e782cf0555e4d54102423ad3421c8122f97a7a7c2d55c677a91e32d7c3e2b059 + md5: 80eccce75e6728e9e728370984bdc6fd + depends: + - pytest >=8.2,<10 + - python >=3.10 + - typing_extensions >=4.12 + - backports.asyncio.runner >=1.1,<2 + - python + license: Apache-2.0 + license_family: APACHE + size: 39223 + timestamp: 1762797319837 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.2-h32b2ec7_100_cp314.conda + build_number: 100 + sha256: a120fb2da4e4d51dd32918c149b04a08815fd2bd52099dad1334647984bb07f1 + md5: 1cef1236a05c3a98f68c33ae9425f656 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.1,<4.0a0 + - libuuid >=2.41.2,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 36790521 + timestamp: 1765021515427 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.2-hf88997e_100_cp314.conda + build_number: 100 + sha256: cd9d41368cb7c531e82fbfdb01e274efbb176c464b59ec619538dd2580602191 + md5: 48921d5efb314c3e628089fc6e27e54a + depends: + - __osx >=10.13 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 14323056 + timestamp: 1765026108189 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.2-h40d2674_100_cp314.conda + build_number: 100 + sha256: 1a93782e90b53e04c2b1a50a0f8bf0887936649d19dba6a05b05c4b44dae96b7 + md5: 14f15ab0d31a2ee5635aa56e77132594 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 13575758 + timestamp: 1765021280625 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.2-h4b44e0e_100_cp314.conda + build_number: 100 + sha256: 6857d7c97cc71fe9ba298dcb1d3b66cc7df425132ab801babd655faa3df48f32 + md5: c3c73414d5ae3f543c531c978d9cc8b8 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - python_abi 3.14.* *_cp314 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 16833248 + timestamp: 1765020224759 + python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.1-pyhcf101f3_0.conda + sha256: aa98e0b1f5472161318f93224f1cfec1355ff69d2f79f896c0b9e033e4a6caf9 + md5: 083725d6cd3dc007f06d04bcf1e613a2 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + size: 26922 + timestamp: 1761503229008 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.2-h4df99d1_100.conda + sha256: 8203dc90a5cb6687f5bfcf332eeaf494ec95d24ed13fca3c82ef840f0bb92a5d + md5: 0064ab66736c4814864e808169dc7497 + depends: + - cpython 3.14.2.* + - python_abi * *_cp314 + license: Python-2.0 + size: 49287 + timestamp: 1765020424843 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + sha256: 1b03678d145b1675b757cba165a0d9803885807792f7eb4495e48a38858c3cca + md5: a28c984e0429aff3ab7386f7de56de6f + depends: + - python >=3.9 + license: Apache-2.0 + license_family: Apache + size: 27913 + timestamp: 1734420869885 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + sha256: e8392a8044d56ad017c08fec2b0eb10ae3d1235ac967d0aab8bd7b41c4a5eaf0 + md5: 88476ae6ebd24f39261e0854ac244f33 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + size: 144160 + timestamp: 1742745254292 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + build_number: 8 + sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 + md5: 0539938c55b6b1a59b560e843ad864a4 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 6989 + timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytokens-0.3.0-pyhcf101f3_0.conda + sha256: 562d54fa0717b7117ee7f6b5f832c6535bf5e44de2dfa2f7056912e53d346469 + md5: 4b1812cb7a8143ee00aef43831fb0d29 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 18860 + timestamp: 1765201048624 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0 + md5: bc8e3267d44011051f2eb14d22fb0960 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 189015 + timestamp: 1742920947249 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyvo-1.8-pyhd8ed1ab_0.conda + sha256: c5da475506154c76a869d4101f3b16c712d5244fc5b966d389286aac3e537eb5 + md5: 0334b0c99472b10f7154f164ce574927 + depends: + - astropy-base >=4.2 + - python >=3.9 + - requests + license: BSD-3-Clause + license_family: BSD + size: 901577 + timestamp: 1763053979710 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + sha256: 6918a8067f296f3c65d43e84558170c9e6c3f4dd735cfe041af41a7fdba7b171 + md5: 2d7b7ba21e8a8ced0eca553d4d53f773 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: PSF-2.0 + license_family: PSF + size: 6713155 + timestamp: 1756487145487 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-ctypes-0.2.3-py314h86ab7b2_3.conda + sha256: 70b43b8d6ac68a524e4d9dd0caf98f6c052918c1b658ee80af9e0269e2bc3a2a + md5: 2507b24a127696b044f441df16c5571c + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 58083 + timestamp: 1762489935449 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + sha256: 828af2fd7bb66afc9ab1c564c2046be391aaf66c0215f05afaf6d7a9a270fe2a + md5: b12f41c0d7fb5ab81709fcc86579688f + depends: + - python >=3.10.* + - yaml + track_features: + - pyyaml_no_compile + license: MIT + license_family: MIT + size: 45223 + timestamp: 1758891992558 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hfb55c3c_0.conda + noarch: python + sha256: a00a41b66c12d9c60e66b391e9a4832b7e28743348cf4b48b410b91927cd7819 + md5: 3399d43f564c905250c1aea268ebb935 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 212218 + timestamp: 1757387023399 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312hb7d603e_0.conda + noarch: python + sha256: 4e052fa3c4ed319e7bcc441fca09dee4ee4006ac6eb3d036a8d683fceda9304b + md5: 81511d0be03be793c622c408c909d6f9 + depends: + - python + - __osx >=10.13 + - libcxx >=19 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 191697 + timestamp: 1757387104297 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312hd65ceae_0.conda + noarch: python + sha256: ef33812c71eccf62ea171906c3e7fc1c8921f31e9cc1fbc3f079f3f074702061 + md5: bbd22b0f0454a5972f68a5f200643050 + depends: + - python + - __osx >=11.0 + - libcxx >=19 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + size: 191115 + timestamp: 1757387128258 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312hbb5da91_0.conda + noarch: python + sha256: fd46b30e6a1e4c129045e3174446de3ca90da917a595037d28595532ab915c5d + md5: 808d263ec97bbd93b41ca01552b5fbd4 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zeromq >=4.3.5,<4.3.6.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause + license_family: BSD + size: 185711 + timestamp: 1757387025899 +- conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + sha256: 776363493bad83308ba30bcb88c2552632581b143e8ee25b1982c8c743e73abc + md5: 353823361b1d27eb3960efb076dfcaf6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: LicenseRef-Qhull + size: 552937 + timestamp: 1720813982144 +- conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda + sha256: 79d804fa6af9c750e8b09482559814ae18cd8df549ecb80a4873537a5a31e06e + md5: dd1ea9ff27c93db7c01a7b7656bd4ad4 + depends: + - __osx >=10.13 + - libcxx >=16 + license: LicenseRef-Qhull + size: 528122 + timestamp: 1720814002588 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + sha256: 873ac689484262a51fd79bc6103c1a1bedbf524924d7f0088fb80703042805e4 + md5: 6483b1f59526e05d7d894e466b5b6924 + depends: + - __osx >=11.0 + - libcxx >=16 + license: LicenseRef-Qhull + size: 516376 + timestamp: 1720814307311 +- conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + sha256: 887d53486a37bd870da62b8fa2ebe3993f912ad04bd755e7ed7c47ced97cbaa8 + md5: 854fbdff64b572b5c0b470f334d34c11 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LicenseRef-Qhull + size: 1377020 + timestamp: 1720814433486 +- conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.9.3-h5c1c036_1.conda + sha256: 51537408ce1493d267b375b33ec02a060d77c4e00c7bef5e2e1c6724e08a23e3 + md5: 762af6d08fdfa7a45346b1466740bacd + depends: + - __glibc >=2.17,<3.0.a0 + - alsa-lib >=1.2.14,<1.3.0a0 + - dbus >=1.16.2,<2.0a0 + - double-conversion >=3.3.1,<3.4.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - harfbuzz >=12.1.0 + - icu >=75.1,<76.0a0 + - krb5 >=1.21.3,<1.22.0a0 + - libclang-cpp21.1 >=21.1.4,<21.2.0a0 + - libclang13 >=21.1.4 + - libcups >=2.3.3,<2.4.0a0 + - libdrm >=2.4.125,<2.5.0a0 + - libegl >=1.7.0,<2.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libgcc >=14 + - libgl >=1.7.0,<2.0a0 + - libglib >=2.86.0,<3.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - libllvm21 >=21.1.4,<21.2.0a0 + - libpng >=1.6.50,<1.7.0a0 + - libpq >=18.0,<19.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libstdcxx >=14 + - libtiff >=4.7.1,<4.8.0a0 + - libvulkan-loader >=1.4.328.1,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libxcb >=1.17.0,<2.0a0 + - libxkbcommon >=1.12.2,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - pcre2 >=10.46,<10.47.0a0 + - wayland >=1.24.0,<2.0a0 + - xcb-util >=0.4.1,<0.5.0a0 + - xcb-util-cursor >=0.1.5,<0.2.0a0 + - xcb-util-image >=0.4.0,<0.5.0a0 + - xcb-util-keysyms >=0.4.1,<0.5.0a0 + - xcb-util-renderutil >=0.3.10,<0.4.0a0 + - xcb-util-wm >=0.4.2,<0.5.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.6,<2.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxcomposite >=0.4.6,<1.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - xorg-libxdamage >=1.1.6,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrandr >=1.5.4,<2.0a0 + - xorg-libxtst >=1.2.5,<2.0a0 + - xorg-libxxf86vm >=1.1.6,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - qt 6.9.3 + license: LGPL-3.0-only + license_family: LGPL + size: 54785664 + timestamp: 1761308850008 +- conda: https://conda.anaconda.org/conda-forge/win-64/qt6-main-6.9.3-ha0de62e_1.conda + sha256: 257b999442d4e14e1e061890e7bd0620511f57324df3ad27bb3cf78b2a6cdcb3 + md5: ca2bfad3a24794a0f7cf413b03906ade + depends: + - double-conversion >=3.3.1,<3.4.0a0 + - harfbuzz >=12.1.0 + - icu >=75.1,<76.0a0 + - krb5 >=1.21.3,<1.22.0a0 + - libclang13 >=21.1.4 + - libglib >=2.86.0,<3.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - libpng >=1.6.50,<1.7.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libvulkan-loader >=1.4.328.1,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - pcre2 >=10.46,<10.47.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - qt 6.9.3 + license: LGPL-3.0-only + license_family: LGPL + size: 95659243 + timestamp: 1761312853504 +- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_0.conda + sha256: 2f225ddf4a274743045aded48053af65c31721e797a45beed6774fdc783febfb + md5: 0227d04521bc3d28c7995c7e1f99a721 + depends: + - libre2-11 2025.11.05 h7b12aa8_0 + license: BSD-3-Clause + license_family: BSD + size: 27316 + timestamp: 1762397780316 +- conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2025.11.05-h7df6414_0.conda + sha256: cd892b6b571fc6aaf9132a859e5ef0fae9e9ff980337ce7284798fa1d24bee5d + md5: 13dc8eedbaa30b753546e3d716f51816 + depends: + - libre2-11 2025.11.05 h554ac88_0 + license: BSD-3-Clause + license_family: BSD + size: 27381 + timestamp: 1762398153069 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-h64b956e_0.conda + sha256: 29c4bceb6b4530bac6820c30ba5a2f53fd26ed3e7003831ecf394e915b975fbc + md5: 1b35e663ed321840af65e7c5cde419f2 + depends: + - libre2-11 2025.11.05 h91c62da_0 + license: BSD-3-Clause + license_family: BSD + size: 27422 + timestamp: 1762398340843 +- conda: https://conda.anaconda.org/conda-forge/win-64/re2-2025.11.05-ha104f34_0.conda + sha256: 9d1bb3d15cdd3257baee5fc063221514482f91154cd1457af126e1ec460bbeac + md5: 50746f61f199c4c00d42e33f5d6cfd0b + depends: + - libre2-11 2025.11.05 h0eb2380_0 + license: BSD-3-Clause + license_family: BSD + size: 216623 + timestamp: 1762397986736 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 282480 + timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda + sha256: 53017e80453c4c1d97aaf78369040418dea14cf8f46a2fa999f31bd70b36c877 + md5: 342570f8e02f2f022147a7f841475784 + depends: + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 256712 + timestamp: 1740379577668 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 + md5: 63ef3f6e6d6d5c589e64f11263dc5676 + depends: + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 252359 + timestamp: 1740379663071 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + sha256: 8dc54e94721e9ab545d7234aa5192b74102263d3e704e6d0c8aa7008f2da2a7b + md5: db0c6b99149880c8ba515cf4abe93ee4 + depends: + - certifi >=2017.4.17 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - python >=3.9 + - urllib3 >=1.21.1,<3 + constrains: + - chardet >=3.0.2,<6 + license: Apache-2.0 + license_family: APACHE + size: 59263 + timestamp: 1755614348400 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + sha256: edfb44d0b6468a8dfced728534c755101f06f1a9870a7ad329ec51389f16b086 + md5: a247579d8a59931091b16a1e932bbed6 + depends: + - markdown-it-py >=2.2.0 + - pygments >=2.13.0,<3.0.0 + - python >=3.10 + - typing_extensions >=4.0.0,<5.0.0 + - python + license: MIT + license_family: MIT + size: 200840 + timestamp: 1760026188268 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.17.0-pyhcf101f3_0.conda + sha256: 1bfd53dfc4877e4613702be69f89a180fcbd31f065aba6b9024ee355fb881b82 + md5: c59bd4c924d9f3001803dc1c7c61da2d + depends: + - python >=3.10 + - rich >=13.7.1 + - click >=8.1.7 + - typing_extensions >=4.12.2 + - python + license: MIT + license_family: MIT + size: 31373 + timestamp: 1764252369301 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.14.8-h813ae00_0.conda + noarch: python + sha256: 4adf379daccb73f03297a6966d1200f6ea65e6a1513d749e7f782e32267fe2bb + md5: 295ce05c06920527a581a5e148a4eec6 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 11340280 + timestamp: 1764866215629 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.14.8-hd9f4cfa_0.conda + noarch: python + sha256: 686d612b38fa11566e8ddbdd4e8f5558f0bac76926328158f1fbcc1dae9c01da + md5: 544c6d626cf0b56068f3f4c59e8651ac + depends: + - python + - __osx >=10.13 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 11286425 + timestamp: 1764866316890 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.14.8-h382de68_0.conda + noarch: python + sha256: 97135a37ab2c55eac06d75569f08ff388af63ec1a0a2a122528b4951b8536027 + md5: f8c69cb8d0c9ac4ab0593926f21a2a3b + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 10302078 + timestamp: 1764866315123 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.14.8-h15e3a1f_0.conda + noarch: python + sha256: fbcaafffd55c7022464219b95658d38980ee04bb001d35c3d97e2e933d7c6bf7 + md5: 35ec53f16d22dc8b17e17865a98c2120 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 11874411 + timestamp: 1764866263950 +- conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + sha256: dec76e9faa3173579d34d226dbc91892417a80784911daf8e3f0eb9bad19d7a6 + md5: bade189a194e66b93c03021bd36c337b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: Apache + size: 394197 + timestamp: 1765160261434 +- conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2025.12.0-pyhd8ed1ab_0.conda + sha256: e060f0566161064453cf353c3a7618a22fa47959b3c7f7224528ca7ebeb2a4b0 + md5: 35a41338454bdba184c3136fbdc7186a + depends: + - aiobotocore >=2.5.4,<3.0.0 + - aiohttp + - fsspec 2025.12.0 + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 33928 + timestamp: 1764796365839 +- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda + sha256: ac76c6187848e529dd0ada06748c7470417ea3994dae24ce9844ff43adf07901 + md5: 881c9466d204a11f424225793bc3c27a + depends: + - __glibc >=2.17,<3.0.a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + - liblapack >=3.9.0,<4.0a0 + - libstdcxx >=14 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 16864022 + timestamp: 1763220800462 +- conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.16.3-py314h9d854bd_1.conda + sha256: d0f6c598d73f809d805ef3b2c4be42ca5b999d57bd2bdff05850c091979d05ba + md5: 017b471251f1d7401ed1dd63370bad2f + depends: + - __osx >=10.13 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - libgfortran5 >=15.2.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 15325764 + timestamp: 1763221416721 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.3-py314h624bdf2_1.conda + sha256: 34034cbd27588eb8522c90930da556a272555384d3d35952dc2f1750971c390d + md5: 8ff6098e9df32259abcd8475c46c419a + depends: + - __osx >=11.0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - libgfortran5 >=15.2.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 14084720 + timestamp: 1763220862474 +- conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.3-py314h5798d8a_1.conda + sha256: 8552e8afa3dac86c10d794b66b94b2bd31f93c702f34ab9571a7ed167379e3c2 + md5: c394de8d285d7040fa99672a65e0c72d + depends: + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 14937821 + timestamp: 1763221198564 +- conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.4.1-py314hdafbbf9_0.conda + sha256: f6883925a130126cdbdc62c2f43513db53c9f889cde4abc3bc66542336a87150 + md5: 54452085855583ccc3cc5dcd17b47ffe + depends: + - cryptography >=2.0 + - dbus + - jeepney >=0.6 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 34098 + timestamp: 1763045408414 +- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + sha256: 1d6534df8e7924d9087bd388fbac5bd868c5bf8971c36885f9f016da0657d22b + md5: 83ea3a2ddb7a75c1b09cea582aa4f106 + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 15018 + timestamp: 1762858315311 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + sha256: 48f3f6a76c34b2cfe80de9ce7f2283ecb55d5ed47367ba91e8bb8104e12b8f11 + md5: 98b6c9dc80eb87b2519b97bcf7e578dd + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + size: 45829 + timestamp: 1762948049098 +- conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + sha256: 1525e6d8e2edf32dabfe2a8e2fc8bf2df81c5ef9f0b5374a3d4ccfa672bfd949 + md5: 2e993292ec18af5cd480932d448598cf + depends: + - libcxx >=19 + - __osx >=10.13 + license: BSD-3-Clause + license_family: BSD + size: 40023 + timestamp: 1762948053450 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + sha256: cb9305ede19584115f43baecdf09a3866bfcd5bcca0d9e527bd76d9a1dbe2d8d + md5: fca4a2222994acd7f691e57f94b750c5 + depends: + - libcxx >=19 + - __osx >=11.0 + license: BSD-3-Clause + license_family: BSD + size: 38883 + timestamp: 1762948066818 +- conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda + sha256: d2deda1350abf8c05978b73cf7fe9147dd5c7f2f9b312692d1b98e52efad53c3 + md5: 3075846de68f942150069d4289aaad63 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: BSD-3-Clause + license_family: BSD + size: 67417 + timestamp: 1762948090450 +- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad + md5: 03fe290994c5e4ec17293cfb6bdce520 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + size: 15698 + timestamp: 1762941572482 +- conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + sha256: d1e3e06b5cf26093047e63c8cc77b70d970411c5cbc0cb1fad461a8a8df599f7 + md5: 0401a17ae845fa72c7210e206ec5647d + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + size: 28657 + timestamp: 1738440459037 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + sha256: c978576cf9366ba576349b93be1cfd9311c00537622a2f9e14ba2b90c97cae9c + md5: 18c019ccf43769d211f2cf78e9ad46c2 + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 37803 + timestamp: 1756330614547 +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc + depends: + - asttokens + - executing + - pure_eval + - python >=3.9 + license: MIT + license_family: MIT + size: 26988 + timestamp: 1733569565672 +- conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.50.0-pyhfdc7a7d_0.conda + sha256: ab9ab67faa3cf12f45f5ced316e2c50dc72b4046cd275612fae756fe9d4cf82c + md5: 68bcb398c375177cf117cf608c274f9d + depends: + - anyio >=3.6.2,<5 + - python >=3.10 + - typing_extensions >=4.10.0 + - python + license: BSD-3-Clause + license_family: BSD + size: 64760 + timestamp: 1762016292582 +- conda: https://conda.anaconda.org/conda-forge/noarch/stingray-2.2.10-pyhc455866_0.conda + sha256: c4264a43717656cf75095902bb1a90182708183a7164f3ba5e3d7f09991c8cbe + md5: 80293d9a4688265c3cd5733403021cf5 + depends: + - astropy-base >=4.0 + - matplotlib-base >=3.0,!=3.4.0 + - numpy >=1.17.0 + - python >=3.10 + - scipy >=1.1.0 + license: MIT + license_family: MIT + size: 49862321 + timestamp: 1761379023180 +- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda + sha256: c31cac57913a699745d124cdc016a63e31c5749f16f60b3202414d071fc50573 + md5: 17c38aaf14c640b85c4617ccb59c1146 + depends: + - libhwloc >=2.12.1,<2.12.2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + size: 155714 + timestamp: 1762510341121 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + sha256: 1544760538a40bcd8ace2b1d8ebe3eb5807ac268641f8acdc18c69c5ebfeaf64 + md5: 86bc20552bf46075e3d92b67f089172d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + size: 3284905 + timestamp: 1763054914403 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + sha256: 0d0b6cef83fec41bc0eb4f3b761c4621b7adfb14378051a8177bd9bb73d26779 + md5: bd9f1de651dbd80b51281c694827f78f + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + size: 3262702 + timestamp: 1763055085507 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7 + md5: a73d54a5abba6543cb2f0af1bfbd6851 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + size: 3125484 + timestamp: 1763055028377 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + sha256: 4581f4ffb432fefa1ac4f85c5682cc27014bcd66e7beaa0ee330e927a7858790 + md5: 7cb36e506a7dba4817970f8adb6396f9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: TCL + license_family: BSD + size: 3472313 + timestamp: 1763055164278 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + sha256: cb77c660b646c00a48ef942a9e1721ee46e90230c7c570cdeb5a893b5cce9bff + md5: d2732eb636c264dc9aa4cbee404b1a53 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 20973 + timestamp: 1760014679845 +- conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + sha256: 4e379e1c18befb134247f56021fdf18e112fb35e64dd1691858b0a0f3bea9a45 + md5: c07a6153f8306e45794774cf9b13bd32 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 53978 + timestamp: 1760707830681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.3-py314h5bd0f2a_0.conda + sha256: b8f9f9ae508d79c9c697eb01b6a8d2ed4bc1899370f44aa6497c8abbd15988ea + md5: e35f08043f54d26a1be93fdbf90d30c3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + size: 905436 + timestamp: 1765458949518 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.3-py314h6482030_0.conda + sha256: 783ff5e72fe309dffdadbabc9da39bce61a23eaf4cf1a8fccbea58cbc8852486 + md5: dbc922389daff37d23ac89178f5ad21b + depends: + - __osx >=10.13 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + size: 903268 + timestamp: 1765459306735 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.3-py314h0612a62_0.conda + sha256: 2d8ed4e017012f16483edf88fd9848ac52dbff25448d96f856a7598fdcf1190d + md5: fd6664676f3a2145d153b3967c6a19ef + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + size: 907916 + timestamp: 1765459269336 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.3-py314h5a2d7ad_0.conda + sha256: d3029e206dc6f83ab76932994f9b075f5fd71a214b2c64df2e0825d0ec4e0ba8 + md5: acfb3820f4d4858807aeb871d64b7144 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 909227 + timestamp: 1765459311895 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 + md5: 019a7385be9af33791c989871317e1ed + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 110051 + timestamp: 1733367480074 +- conda: https://conda.anaconda.org/conda-forge/noarch/traittypes-0.2.3-pyh332efcf_0.conda + sha256: 67a77ce374a792fc6d8e4d56c83c21b6cf3a7f43b6e98c1db2cbed2254144d05 + md5: d22a0bf07f57cfb1240185961d182a8d + depends: + - python >=3.9 + - traitlets >=4.2.2,<6.0 + license: BSD-3-Clause + license_family: BSD + size: 13283 + timestamp: 1761131966141 +- conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.20.0-pyhefaf540_1.conda + sha256: 17a1e572939af33d709248170871d4da74f7e32b48f2e9b5abca613e201c6e64 + md5: 23a53fdefc45ba3f4e075cc0997fd13b + depends: + - typer-slim-standard ==0.20.0 h4daf872_1 + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 79829 + timestamp: 1762984042927 +- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.20.0-pyhcf101f3_1.conda + sha256: 4b5ded929080b91367f128e7299619f6116f08bc77d9924a2f8766e2a1b18161 + md5: 4b02a515f3e882dcfe9cfbf0a1f5cd3a + depends: + - python >=3.10 + - click >=8.0.0 + - typing_extensions >=3.7.4.3 + - python + constrains: + - typer 0.20.0.* + - rich >=10.11.0 + - shellingham >=1.3.0 + license: MIT + license_family: MIT + size: 47951 + timestamp: 1762984042920 +- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.20.0-h4daf872_1.conda + sha256: 5027768bc9a580c8ffbf25872bb2208c058cbb79ae959b1cf2cc54b5d32c0377 + md5: 37b26aafb15a6687b31a3d8d7a1f04e7 + depends: + - typer-slim ==0.20.0 pyhcf101f3_1 + - rich + - shellingham + license: MIT + license_family: MIT + size: 5322 + timestamp: 1762984042927 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + size: 91383 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_1.conda + sha256: 70db27de58a97aeb7ba7448366c9853f91b21137492e0b4430251a1870aa8ff4 + md5: a0a4a3035667fc34f29bfbd5c190baa6 + depends: + - python >=3.10 + - typing_extensions >=4.12.0 + license: MIT + license_family: MIT + size: 18923 + timestamp: 1764158430324 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + sha256: 865716d3e2ccaca1218462645830d2370ab075a9a118c238728e1231a234bc6c + md5: e4e8496b68cf5f25e76fbe67f3856550 + license: LicenseRef-Public-Domain + size: 119010 + timestamp: 1765580300078 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + size: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/noarch/uncompresspy-0.4.1-pyhd8ed1ab_0.conda + sha256: 423320baa07b12f611f7d72d6d7136a6feca2ddf3691e3dcf073e84571d05c16 + md5: 06de15bda7ff6019d8e02e6682664b63 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 16160 + timestamp: 1760291806337 +- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.0-py314h5bd0f2a_1.conda + sha256: d1dafc15fc5d2b1dd5b0a525e8a815028de20dd53b2c775a1b56e8e4839fb736 + md5: 58e2ee530005067c5db23f33c6ab43d2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + size: 409745 + timestamp: 1763055060898 +- conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.0-py314h6482030_1.conda + sha256: 39e3ff3944c609fc2930ea270e5a9abceaf6b851136cafc7ffee5acf2788a7d8 + md5: d69097de15cbad36f1eaafda0bad598a + depends: + - __osx >=10.13 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + size: 405564 + timestamp: 1763055016092 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.0-py314h0612a62_1.conda + sha256: 48c51dd2ef696f7a1a3635716585a8e383a8c00e719305cfda2b480c36ee1283 + md5: c673decfe1f120b0717d0aa193b10060 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + size: 416770 + timestamp: 1763055099322 +- conda: https://conda.anaconda.org/conda-forge/win-64/unicodedata2-17.0.0-py314h5a2d7ad_1.conda + sha256: 47e061aec1487519c398e1c999ac3680f068f9e1d8574c8b365eac4787773250 + md5: 1f90bb13fa5ced89ca4dcc0af3bbebf3 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 405783 + timestamp: 1763054877424 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.1-pyhd8ed1ab_0.conda + sha256: a66fc716c9dc6eb048c40381b0d1c5842a1d74bba7ce3d16d80fc0a7232d8644 + md5: fb84f0f6ee8a0ad67213cd1bea98bf5b + depends: + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.10 + license: MIT + license_family: MIT + size: 102817 + timestamp: 1765212810619 +- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh31011fe_0.conda + sha256: 32e637726fd7cfeb74058e829b116e17514d001846fef56d8c763ec9ec5ac887 + md5: d3aa78bc38d9478e9eed5f128ba35f41 + depends: + - __unix + - click >=7.0 + - h11 >=0.8 + - python >=3.10 + - typing_extensions >=4.0 + license: BSD-3-Clause + license_family: BSD + size: 51717 + timestamp: 1760803935306 +- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.38.0-pyh5737063_0.conda + sha256: ebb120ec1626ced65f3965c08f9ac58d57a18488f991a87dad89f002a2094cb2 + md5: 8fb44dcece55529465f9e6f3e40eef61 + depends: + - __win + - click >=7.0 + - h11 >=0.8 + - python >=3.10 + - typing_extensions >=4.0 + license: BSD-3-Clause + license_family: BSD + size: 51772 + timestamp: 1760804061872 +- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h31011fe_0.conda + sha256: 3629a349257c0e129cbb84fd593759a31d68ac1219c0af8b8ed89b95b9574c9b + md5: 1ce870d7537376362672f5ff57109529 + depends: + - __unix + - httptools >=0.6.3 + - python-dotenv >=0.13 + - pyyaml >=5.1 + - uvicorn 0.38.0 pyh31011fe_0 + - uvloop >=0.14.0,!=0.15.0,!=0.15.1 + - watchfiles >=0.13 + - websockets >=10.4 + license: BSD-3-Clause + license_family: BSD + size: 7719 + timestamp: 1760803936446 +- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.38.0-h5737063_0.conda + sha256: ba4a9d4962a671efd2b911c0be9f576beecff8cc606344a46e0c67720e9f5dbc + md5: 816b80d606a73c2ffaf55e84c3ff2516 + depends: + - __win + - colorama >=0.4 + - httptools >=0.6.3 + - python-dotenv >=0.13 + - pyyaml >=5.1 + - uvicorn 0.38.0 pyh5737063_0 + - watchfiles >=0.13 + - websockets >=10.4 + license: BSD-3-Clause + license_family: BSD + size: 8179 + timestamp: 1760804064891 +- conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py314h5bd0f2a_1.conda + sha256: ad3058ed67e1de5f9a73622a44a5c7a51af6a4527cf4881ae22b8bb6bd30bceb + md5: 41f06d5cb2a80011c7da5a835721acdd + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libuv >=1.51.0,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT OR Apache-2.0 + size: 593392 + timestamp: 1762472837997 +- conda: https://conda.anaconda.org/conda-forge/osx-64/uvloop-0.22.1-py314h6482030_1.conda + sha256: 1b5fecf3c24b76fdc23b27baa552c1a6d10f4a73c73a5cca5fa8b188cd8dd7f7 + md5: e71ee20d1db39d10eb07bae8edfd5969 + depends: + - __osx >=10.13 + - libuv >=1.51.0,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT OR Apache-2.0 + size: 509743 + timestamp: 1762473238291 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/uvloop-0.22.1-py314h0612a62_1.conda + sha256: 7850dd9238beb14f9c7db1901229cc5d2ecd10d031cbdb712a95eba57a5d5992 + md5: 74683034f513752be1467c9232480a13 + depends: + - __osx >=11.0 + - libuv >=1.51.0,<2.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: MIT OR Apache-2.0 + size: 492509 + timestamp: 1762473163613 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda + sha256: 7036945b5fff304064108c22cbc1bb30e7536363782b0456681ee6cf209138bd + md5: 2d1c042360c09498891809a3765261be + depends: + - vc14_runtime >=14.42.34433 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + size: 19070 + timestamp: 1765216452130 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda + sha256: 7e8f7da25d7ce975bbe7d7e6d6e899bf1f253e524a3427cc135a79f3a79c457c + md5: fb8e4914c5ad1c71b3c519621e1df7b8 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_33 + constrains: + - vs2015_runtime 14.44.35208.* *_33 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + size: 684323 + timestamp: 1765216366832 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_33.conda + sha256: f79edd878094e86af2b2bc1455b0a81e02839a784fb093d5996ad4cf7b810101 + md5: 4cb6942b4bd846e51b4849f4a93c7e6d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_33 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + size: 115073 + timestamp: 1765216325898 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_33.conda + sha256: 93fc61d05770f4c6b66214ed3494f632bf6e0e6ee7fcb0fb0a847a4bed131953 + md5: 65e5a2127012cd4dbc9354579661b9fd + depends: + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + size: 19159 + timestamp: 1765216369037 +- conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.1.1-py314ha5689aa_0.conda + sha256: fcec93ca26320764c55042fc56b772a88533ed01f1c713553c985b379e174d09 + md5: fb190bbf05b3b963bea7ab7c20624d5d + depends: + - __glibc >=2.17,<3.0.a0 + - anyio >=3.0.0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 421969 + timestamp: 1760456771978 +- conda: https://conda.anaconda.org/conda-forge/osx-64/watchfiles-1.1.1-py314hc9c287a_0.conda + sha256: 860883a9d79688de244443859ff958f35a2363909310d6cc249e95f8453ad136 + md5: 289c6991af0a4a28091b9522e1bb5ec1 + depends: + - __osx >=10.13 + - anyio >=3.0.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 380050 + timestamp: 1760457275658 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchfiles-1.1.1-py314h8d4a433_0.conda + sha256: b9446970047031e66edf76548fa427fe0ce7e81655208dc2e2a0b0bf94ebf7ba + md5: 33c8e4a66a7cb5d75ba8165a6075cd28 + depends: + - __osx >=11.0 + - anyio >=3.0.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 367150 + timestamp: 1760457260426 +- conda: https://conda.anaconda.org/conda-forge/win-64/watchfiles-1.1.1-py314h170c82c_0.conda + sha256: b6b3ad95d6c2d92150c8b35367d987beffae083627bb49c996a78fc129ab2e00 + md5: f86852dadc13af0ec70e02b175159481 + depends: + - anyio >=3.0.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 305573 + timestamp: 1760457150003 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda + sha256: 3aa04ae8e9521d9b56b562376d944c3e52b69f9d2a0667f77b8953464822e125 + md5: 035da2e4f5770f036ff704fa17aace24 + depends: + - __glibc >=2.17,<3.0.a0 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + size: 329779 + timestamp: 1761174273487 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + sha256: e311b64e46c6739e2a35ab8582c20fa30eb608da130625ed379f4467219d4813 + md5: 7e1e5ff31239f9cd5855714df8a3783d + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 33670 + timestamp: 1758622418893 +- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 + md5: 2841eb5bfc75ce15e9a0054b98dcd64d + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 15496 + timestamp: 1733236131358 +- conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py314h31f8a6b_2.conda + sha256: 102c0acc2301908bcc0bd0c792e059cf8a6b93fc819f56c8a3b8a6b473afe58a + md5: e05c3cce47cc4f32f886eb17091ba6e2 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 380425 + timestamp: 1756476367704 +- conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-15.0.1-py314hcfd16f8_2.conda + sha256: 9b448d15a7e64dba0b10b16d3baa327b2249a9e0828b129829589a14ff34e9a7 + md5: bb2248bc747080459471cdaf10c7202a + depends: + - python + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 380757 + timestamp: 1756476422027 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-15.0.1-py314hf17b0b1_2.conda + sha256: c00677dc11e5f20e115ab7252c60893cd0bac9fc78b12678d62ba6b1b5dcb3f7 + md5: 22ef4a8d9fdd426f7fb9d5b3bf168c2a + depends: + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 383627 + timestamp: 1756476437332 +- conda: https://conda.anaconda.org/conda-forge/win-64/websockets-15.0.1-py314h4667ab5_2.conda + sha256: 678cee096988ceafad1ed5aea904fb662d7208ee0eb4ab68acabd97997e8d814 + md5: ddff54cc7821bfa04ec8fdc34b4d5dba + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 437357 + timestamp: 1756476400401 +- conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + sha256: 826af5e2c09e5e45361fa19168f46ff524e7a766022615678c3a670c45895d9a + md5: dc257b7e7cad9b79c1dfba194e92297b + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + size: 889195 + timestamp: 1762040404362 +- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f + md5: 46e441ba871f524e2b067929da3051c2 + depends: + - __win + - python >=3.9 + license: LicenseRef-Public-Domain + size: 9555 + timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.3-py314h5bd0f2a_1.conda + sha256: e2b6545651aed5e7dead39b7ba3bf8c2669f194c71e89621343bd0bb321a87f1 + md5: 82da729c870ada2f675689a39b4f697f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14.0rc2,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + size: 64997 + timestamp: 1756851739706 +- conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-1.17.3-py314h03d016b_1.conda + sha256: 72e67726778356a45bba26c598bd91f13e95b37d0f931e8217408f9e20527786 + md5: eddd65903cdc82babc86d48aba49acae + depends: + - __osx >=10.13 + - python >=3.14.0rc2,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + size: 61409 + timestamp: 1756851745948 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.3-py314hb84d1df_1.conda + sha256: 0f35a19fd99724e8620dc89a6fb9eb100d300f117292adde2c7e8cf12d566e10 + md5: 104bf69250e32a42ca144d7f7abd5d5c + depends: + - __osx >=11.0 + - python >=3.14.0rc2,<3.15.0a0 + - python >=3.14.0rc2,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + size: 61800 + timestamp: 1756851815321 +- conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.3-py314h5a2d7ad_1.conda + sha256: ecbee7584bc5dfcabed36240059a156dab0d6dd87a0246c71b32b82640558a78 + md5: 0172693b00f64c34667a5bdda0449eb9 + depends: + - python >=3.14.0rc2,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + size: 63873 + timestamp: 1756852097390 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda + sha256: ad8cab7e07e2af268449c2ce855cbb51f43f4664936eff679b1f3862e6e4b01d + md5: fdc27cb255a7a2cc73b7919a968b48f0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libxcb >=1.17.0,<2.0a0 + license: MIT + license_family: MIT + size: 20772 + timestamp: 1750436796633 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda + sha256: c2be9cae786fdb2df7c2387d2db31b285cf90ab3bfabda8fa75a596c3d20fc67 + md5: 4d1fc190b99912ed557a8236e958c559 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxcb >=1.13 + - libxcb >=1.17.0,<2.0a0 + - xcb-util-image >=0.4.0,<0.5.0a0 + - xcb-util-renderutil >=0.3.10,<0.4.0a0 + license: MIT + license_family: MIT + size: 20829 + timestamp: 1763366954390 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda + sha256: 94b12ff8b30260d9de4fd7a28cca12e028e572cbc504fd42aa2646ec4a5bded7 + md5: a0901183f08b6c7107aab109733a3c91 + depends: + - libgcc-ng >=12 + - libxcb >=1.16,<2.0.0a0 + - xcb-util >=0.4.1,<0.5.0a0 + license: MIT + license_family: MIT + size: 24551 + timestamp: 1718880534789 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.1-hb711507_0.conda + sha256: 546e3ee01e95a4c884b6401284bb22da449a2f4daf508d038fdfa0712fe4cc69 + md5: ad748ccca349aec3e91743e08b5e2b50 + depends: + - libgcc-ng >=12 + - libxcb >=1.16,<2.0.0a0 + license: MIT + license_family: MIT + size: 14314 + timestamp: 1718846569232 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda + sha256: 2d401dadc43855971ce008344a4b5bd804aca9487d8ebd83328592217daca3df + md5: 0e0cbe0564d03a99afd5fd7b362feecd + depends: + - libgcc-ng >=12 + - libxcb >=1.16,<2.0.0a0 + license: MIT + license_family: MIT + size: 16978 + timestamp: 1718848865819 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda + sha256: 31d44f297ad87a1e6510895740325a635dd204556aa7e079194a0034cdd7e66a + md5: 608e0ef8256b81d04456e8d211eee3e8 + depends: + - libgcc-ng >=12 + - libxcb >=1.16,<2.0.0a0 + license: MIT + license_family: MIT + size: 51689 + timestamp: 1718844051451 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda + sha256: aa03b49f402959751ccc6e21932d69db96a65a67343765672f7862332aa32834 + md5: 71ae752a748962161b4740eaff510258 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT + size: 396975 + timestamp: 1759543819846 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b + md5: fb901ff28063514abb6046c9ec2c4a45 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + size: 58628 + timestamp: 1734227592886 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 + md5: 1c74ff8c35dcadf952a16f752ca5aa49 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - xorg-libice >=1.1.2,<2.0a0 + license: MIT + license_family: MIT + size: 27590 + timestamp: 1741896361728 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + sha256: 51909270b1a6c5474ed3978628b341b4d4472cd22610e5f22b506855a5e20f67 + md5: db038ce880f100acc74dba10302b5630 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libxcb >=1.17.0,<2.0a0 + license: MIT + license_family: MIT + size: 835896 + timestamp: 1741901112627 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + sha256: 6bc6ab7a90a5d8ac94c7e300cc10beb0500eeba4b99822768ca2f2ef356f731b + md5: b2895afaf55bf96a8c8282a2e47a5de0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 15321 + timestamp: 1762976464266 +- conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.12-h8616949_1.conda + sha256: 928f28bd278c7da674b57d71b2e7f4ac4e7c7ce56b0bf0f60d6a074366a2e76d + md5: 47f1b8b4a76ebd0cd22bd7153e54a4dc + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 13810 + timestamp: 1762977180568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda + sha256: adae11db0f66f86156569415ed79cda75b2dbf4bea48d1577831db701438164f + md5: 78b548eed8227a689f93775d5d23ae09 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 14105 + timestamp: 1762976976084 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-hba3369d_1.conda + sha256: 156a583fa43609507146de1c4926172286d92458c307bb90871579601f6bc568 + md5: 8436cab9a76015dfe7208d3c9f97c156 + depends: + - libgcc >=14 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 109246 + timestamp: 1762977105140 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + sha256: 753f73e990c33366a91fd42cc17a3d19bb9444b9ca5ff983605fa9e953baf57f + md5: d3c295b50f092ab525ffe3c2aa4b7413 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + size: 13603 + timestamp: 1727884600744 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a + md5: 2ccd714aa2242315acaf0a67faea780b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + size: 32533 + timestamp: 1730908305254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + sha256: 43b9772fd6582bf401846642c4635c47a9b0e36ca08116b3ec3df36ab96e0ec0 + md5: b5fcc7172d22516e1f965490e65e33a4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + size: 13217 + timestamp: 1727891438799 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + sha256: 25d255fb2eef929d21ff660a0c687d38a6d2ccfbcbf0cc6aa738b12af6e9d142 + md5: 1dafce8548e38671bea82e3f5c6ce22f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 20591 + timestamp: 1762976546182 +- conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.5-h8616949_1.conda + sha256: b7b291cc5fd4e1223058542fca46f462221027779920dd433d68b98e858a4afc + md5: 435446d9d7db8e094d2c989766cfb146 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 19067 + timestamp: 1762977101974 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda + sha256: f7fa0de519d8da589995a1fe78ef74556bb8bc4172079ae3a8d20c3c81354906 + md5: 9d1299ace1924aa8f4e0bc8e71dd0cf7 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 19156 + timestamp: 1762977035194 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-hba3369d_1.conda + sha256: 366b8ae202c3b48958f0b8784bbfdc37243d3ee1b1cd4b8e76c10abe41fa258b + md5: a7c03e38aa9c0e84d41881b9236eacfb + depends: + - libgcc >=14 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 70691 + timestamp: 1762977015220 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + sha256: da5dc921c017c05f38a38bd75245017463104457b63a1ce633ed41f214159c14 + md5: febbab7d15033c913d53c7a2c102309d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + size: 50060 + timestamp: 1727752228921 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + sha256: 83c4c99d60b8784a611351220452a0a85b080668188dce5dfa394b723d7b64f4 + md5: ba231da7fccf9ea1e768caf5c7099b84 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT + size: 20071 + timestamp: 1759282564045 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + sha256: 1a724b47d98d7880f26da40e45f01728e7638e6ec69f35a3e11f92acd05f9e7a + md5: 17dcc85db3c7886650b8908b183d6876 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + size: 47179 + timestamp: 1727799254088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + sha256: ac0f037e0791a620a69980914a77cb6bb40308e26db11698029d6708f5aa8e0d + md5: 2de7f99d6581a4a7adbff607b5c278ca + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + size: 29599 + timestamp: 1727794874300 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 + md5: 96d57aba173e878a2089d5638016dc5e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + size: 33005 + timestamp: 1734229037766 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + sha256: 752fdaac5d58ed863bbf685bb6f98092fe1a488ea8ebb7ed7b606ccfce08637a + md5: 7bbe9a0cc0df0ac5f5a8ad6d6a11af2f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxi >=1.7.10,<2.0a0 + license: MIT + license_family: MIT + size: 32808 + timestamp: 1727964811275 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda + sha256: 8a4e2ee642f884e6b78c20c0892b85dd9b2a6e64a6044e903297e616be6ca35b + md5: 5efa5fa6243a622445fdfd72aee15efa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + license: MIT + license_family: MIT + size: 17819 + timestamp: 1734214575628 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + sha256: a335161bfa57b64e6794c3c354e7d49449b28b8d8a7c4ed02bf04c3f009953f9 + md5: a645bb90997d3fc2aea0adf6517059bd + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 79419 + timestamp: 1753484072608 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac + md5: 78a0fe9e9c50d2c381e8ee47e3ea437d + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 83386 + timestamp: 1753484079473 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 + md5: 433699cba6602098ae8957a323da2664 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 63944 + timestamp: 1753484092156 +- conda: https://conda.anaconda.org/conda-forge/noarch/yarl-1.22.0-pyh7db6752_0.conda + sha256: b04271f56c68483b411c5465afff73b8eabdea564e942f0e7afed06619272635 + md5: ca3c00c764cee005798a518cba79885c + depends: + - idna >=2.0 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.10 + track_features: + - yarl_no_compile + license: Apache-2.0 + license_family: Apache + size: 73066 + timestamp: 1761337117132 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda + sha256: 47cfe31255b91b4a6fa0e9dbaf26baa60ac97e033402dbc8b90ba5fee5ffe184 + md5: 8035e5b54c08429354d5d64027041cad + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + size: 310648 + timestamp: 1757370847287 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h6c33b1e_9.conda + sha256: 30aa5a2e9c7b8dbf6659a2ccd8b74a9994cdf6f87591fcc592970daa6e7d3f3c + md5: d940d809c42fbf85b05814c3290660f5 + depends: + - __osx >=10.13 + - libcxx >=19 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + size: 259628 + timestamp: 1757371000392 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda + sha256: b6f9c130646e5971f6cad708e1eee278f5c7eea3ca97ec2fdd36e7abb764a7b8 + md5: 26f39dfe38a2a65437c29d69906a0f68 + depends: + - __osx >=11.0 + - libcxx >=19 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + size: 244772 + timestamp: 1757371008525 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda + sha256: 690cf749692c8ea556646d1a47b5824ad41b2f6dfd949e4cdb6c44a352fcb1aa + md5: a6c8f8ee856f7c3c1576e14b86cd8038 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libsodium >=1.0.20,<1.0.21.0a0 + - krb5 >=1.21.3,<1.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + size: 265212 + timestamp: 1757370864284 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + sha256: b4533f7d9efc976511a73ef7d4a2473406d7f4c750884be8e8620b0ce70f4dae + md5: 30cd29cb87d819caead4d55184c1d115 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 24194 + timestamp: 1764460141901 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + sha256: 5d7c0e5f0005f74112a34a7425179f4eb6e73c92f5d109e6af4ddeca407c92ab + md5: c9f075ab2f33b3bbee9e62d4ad0a6cd8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib 1.3.1 hb9d3cd8_2 + license: Zlib + license_family: Other + size: 92286 + timestamp: 1727963153079 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.1-hd23fc13_2.conda + sha256: 219edbdfe7f073564375819732cbf7cc0d7c7c18d3f546a09c2dfaf26e4d69f3 + md5: c989e0295dcbdc08106fe5d9e935f0b9 + depends: + - __osx >=10.13 + - libzlib 1.3.1 hd23fc13_2 + license: Zlib + license_family: Other + size: 88544 + timestamp: 1727963189976 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + sha256: 58f8860756680a4831c1bf4f294e2354d187f2e999791d53b1941834c4b37430 + md5: e3170d898ca6cb48f1bb567afb92f775 + depends: + - __osx >=11.0 + - libzlib 1.3.1 h8359307_2 + license: Zlib + license_family: Other + size: 77606 + timestamp: 1727963209370 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.2-h54a6638_0.conda + sha256: 0afb07f3511031c35202036e2cd819c90edaa0c6a39a7a865146d3cb066bec96 + md5: 0faadd01896315ceea58bcc3479b1d21 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + license: Zlib + size: 135032 + timestamp: 1764715875371 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.2-h53ec75d_0.conda + sha256: 9183b2ada178d83ca6f8a66ba2ddcfb5f2476c2e866a4609c1f84dd5f32d796e + md5: 1e979f90e823b82604ab1da7e76c75e5 + depends: + - __osx >=10.13 + - libcxx >=19 + license: Zlib + size: 135199 + timestamp: 1764716055794 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.2-h248ca61_0.conda + sha256: 2fe2befe061a51c24fce7f5f071c47b45b43f8c8781c0c557edf7c733ab13b18 + md5: c2a30a3b30cf86ef97ec880d53a6571a + depends: + - libcxx >=19 + - __osx >=11.0 + license: Zlib + size: 105035 + timestamp: 1764716000870 +- conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.2-h5112557_0.conda + sha256: 331e63a801efc9aa47e0a7f7be5becc81d9c52c1163308182078108e003c12e5 + md5: 2b4f8712b09b5fd3182cda872ce8482c + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: Zlib + size: 134848 + timestamp: 1764715928393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + sha256: 47101a4055a70a4876ffc87b750ab2287b67eca793f21c8224be5e1ee6394d3f + md5: 727109b184d680772e3122f40136d5ca + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 528148 + timestamp: 1764777156963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 433413 + timestamp: 1764777166076 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 + md5: 053b84beec00b71ea8ff7a4f84b55207 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 388453 + timestamp: 1764777142545 diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..bc21b6d --- /dev/null +++ b/pixi.toml @@ -0,0 +1,87 @@ +[workspace] +name = "stingray-explorer" +version = "1.0.0" +description = "Desktop application for X-ray timing analysis using the Stingray library" +channels = ["conda-forge"] +platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"] + +[tasks] +# Development - Run these in separate terminals +start-backend = { cmd = "cd python-backend && python main.py", description = "Start Python backend server (auto-finds free port)" } +start-frontend = { cmd = "npm run dev", description = "Start Electron frontend" } + +# Run both together (recommended) +dev = { cmd = "bash scripts/dev.sh", description = "Start full development environment" } + +# Kill any orphaned backend processes +kill-backend = { cmd = "pkill -f 'python main.py' || true", description = "Kill any running backend processes" } + +# Build +build = { cmd = "npm run build", description = "Build the Electron app" } +build-frontend = { cmd = "npm run build", description = "Build frontend only" } + +# Install +install-npm = { cmd = "npm install", description = "Install npm dependencies" } +setup = { depends-on = ["install-npm"], description = "Setup the project" } + +# Testing +test = { depends-on = ["test-python"], description = "Run all tests" } + +# Utilities +lint = { cmd = "npm run lint", description = "Run linter" } +format = { cmd = "npm run format", description = "Format code" } +health-check = { cmd = "curl -s http://127.0.0.1:8765/health", description = "Check backend health" } + +# Package +package = { cmd = "npm run build && npm run package", description = "Package the application" } + +[dependencies] +# Python +python = ">=3.10" +pip = "*" + +# Stingray and scientific stack +stingray = ">=2.0" +numpy = ">=1.24" +scipy = ">=1.11" +astropy = ">=5.3" +pandas = ">=2.0" +matplotlib = ">=3.7" +pytables = ">=3.8" +numba = ">=0.58" # Optional but recommended for faster Stingray computations + +# FastAPI backend +fastapi = ">=0.109" +uvicorn = ">=0.27" +pydantic = ">=2.5" +httpx = ">=0.26" +python-multipart = "*" +aiofiles = "*" + +# Utilities +psutil = ">=5.9" +requests = ">=2.31" + +# HEASARC archive queries +astroquery = ">=0.4.8" + +# HTML parsing for HEASARC directory listings +beautifulsoup4 = ">=4.12" +lxml = ">=5.0" + +# Node.js (npm is included with nodejs) +nodejs = ">=20" + +[feature.dev.tasks] +test-python = { cmd = "pytest python-backend/tests", description = "Run Python backend tests" } + +[feature.dev.dependencies] +# Development dependencies +pytest = ">=7.0" +pytest-asyncio = "*" +black = "*" +ruff = "*" + +[environments] +default = { features = [], solve-group = "default" } +dev = { features = ["dev"], solve-group = "default" } diff --git a/pyproject.toml b/pyproject.toml index cf5ad94..f549119 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,4 @@ dependencies = [ ] [tool.pytest.ini_options] pythonpath = ["."] +testpaths = ["python-backend/tests"] diff --git a/python-backend/main.py b/python-backend/main.py new file mode 100644 index 0000000..1737919 --- /dev/null +++ b/python-backend/main.py @@ -0,0 +1,205 @@ +""" +Stingray Explorer Python Backend + +FastAPI server providing REST API endpoints for X-ray timing analysis +using the Stingray library. +""" + +import logging +import os +import signal +import socket +import sys +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from routes import data_routes, lightcurve_routes, spectrum_routes, timing_routes, export_routes, log_routes, archive_routes, job_routes +from services.state_manager import StateManager +from services.data_service import DataService +from services.job_manager import JobManager +from utils.performance_monitor import PerformanceMonitor +from utils.log_stream import log_stream_manager + + +# Filter to suppress /api/status access logs (polled every 2s, would flood logs) +class StatusEndpointFilter(logging.Filter): + """Filter out /api/status requests from uvicorn access logs.""" + + def filter(self, record: logging.LogRecord) -> bool: + return "/api/status" not in record.getMessage() + +# Global instances +state_manager: StateManager = None +performance_monitor: PerformanceMonitor = None +data_service: DataService = None +job_manager: JobManager = None + + +def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int: + """Find a free port starting from start_port.""" + for port in range(start_port, start_port + max_attempts): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", port)) + return port + except OSError: + continue + raise RuntimeError(f"Could not find a free port in range {start_port}-{start_port + max_attempts}") + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + """Application lifespan handler for startup/shutdown events.""" + global state_manager, performance_monitor, data_service, job_manager + + # Startup + print("Starting Stingray Explorer Backend...") + state_manager = StateManager() + performance_monitor = PerformanceMonitor() + data_service = DataService(state_manager, performance_monitor) + job_manager = JobManager(state_manager, data_service, max_workers=4) + + # Store in app state for access in routes + app.state.state_manager = state_manager + app.state.performance_monitor = performance_monitor + app.state.data_service = data_service + app.state.job_manager = job_manager + + # Install log streaming to capture Python logs and warnings + log_stream_manager.install(log_level=logging.DEBUG) + + print("Backend initialized successfully") + yield + + # Shutdown + print("Shutting down Stingray Explorer Backend...") + # Shutdown job manager + if job_manager: + job_manager.shutdown() + # Uninstall log streaming + log_stream_manager.uninstall() + + +def create_app() -> FastAPI: + """Create and configure the FastAPI application.""" + app = FastAPI( + title="Stingray Explorer API", + description="REST API for X-ray timing analysis using the Stingray library", + version="1.0.0", + lifespan=lifespan, + ) + + # Configure CORS for Electron renderer + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, restrict to electron app + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Register routes + app.include_router(data_routes.router, prefix="/api/data", tags=["Data"]) + app.include_router(lightcurve_routes.router, prefix="/api/lightcurve", tags=["Lightcurve"]) + app.include_router(spectrum_routes.router, prefix="/api/spectrum", tags=["Spectrum"]) + app.include_router(timing_routes.router, prefix="/api/timing", tags=["Timing"]) + app.include_router(export_routes.router, prefix="/api/export", tags=["Export"]) + app.include_router(log_routes.router, prefix="/api/logs", tags=["Logs"]) + app.include_router(archive_routes.router, prefix="/api/archive", tags=["Archive"]) + app.include_router(job_routes.router, prefix="/api/jobs", tags=["Jobs"]) + + @app.get("/") + async def root(): + """Root endpoint - health check.""" + return {"status": "ok", "message": "Stingray Explorer API is running"} + + @app.get("/health") + async def health_check(): + """Health check endpoint for Electron to verify backend is ready.""" + return { + "status": "healthy", + "version": "1.0.0", + "state_manager": state_manager is not None, + } + + @app.get("/api/status") + async def get_status(): + """Get current application status including process-specific resources.""" + backend_resources = None + + if performance_monitor: + mem_info = performance_monitor.get_memory_usage() + cpu_info = performance_monitor.get_cpu_usage() + + backend_resources = { + # Process-specific metrics (Python backend only) + "memory_mb": mem_info.get("process_mb", 0), + "memory_percent": mem_info.get("process_percent", 0), + "cpu_percent": cpu_info.get("process_percent", 0), + # System totals (for reference and percentage calculations) + "system_memory_total_mb": mem_info.get("system_total_gb", 0) * 1024, + "system_memory_available_mb": mem_info.get("system_available_gb", 0) * 1024, + "system_cpu_count": cpu_info.get("cpu_count", 1), + } + + return { + "event_lists_loaded": len(state_manager.get_event_data()) if state_manager else 0, + "lightcurves_loaded": len(state_manager.get_lightcurve_data()) if state_manager else 0, + "backend_resources": backend_resources, + } + + @app.post("/api/shutdown") + async def shutdown(): + """Shutdown the backend server.""" + import asyncio + import os + + async def shutdown_server(): + await asyncio.sleep(0.5) # Give time for response to be sent + os._exit(0) + + asyncio.create_task(shutdown_server()) + return {"status": "shutting_down"} + + return app + + +# Create app instance +app = create_app() + + +if __name__ == "__main__": + import uvicorn + + # Suppress /api/status access logs to avoid log flooding during polling + logging.getLogger("uvicorn.access").addFilter(StatusEndpointFilter()) + + # Get port from environment or command line, or find a free one + requested_port = os.environ.get("PORT") or (sys.argv[1] if len(sys.argv) > 1 else None) + + if requested_port: + port = int(requested_port) + else: + port = find_free_port(8765) + + # Print port so parent process can read it + print(f"BACKEND_PORT:{port}", flush=True) + + # Handle signals for graceful shutdown + def signal_handler(signum, frame): + print(f"\nReceived signal {signum}, shutting down...") + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + uvicorn.run( + "main:app", + host="127.0.0.1", + port=port, + reload=False, + log_level="info", + ) diff --git a/python-backend/models/__init__.py b/python-backend/models/__init__.py new file mode 100644 index 0000000..17c2e0d --- /dev/null +++ b/python-backend/models/__init__.py @@ -0,0 +1,7 @@ +""" +Data models for Stingray Explorer backend. +""" + +from models.job import Job, JobStatus, JobType + +__all__ = ["Job", "JobStatus", "JobType"] diff --git a/python-backend/models/job.py b/python-backend/models/job.py new file mode 100644 index 0000000..5f5df0b --- /dev/null +++ b/python-backend/models/job.py @@ -0,0 +1,165 @@ +""" +Job data model for background task queue. + +This module defines the Job dataclass and related enums for tracking +background tasks like file loading, data analysis, and downloads. +""" + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Dict, List, Optional +import uuid + + +class JobStatus(str, Enum): + """Status of a background job.""" + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class JobType(str, Enum): + """Type of background job.""" + LOAD_EVENT_LIST = "load_event_list" + LOAD_BATCH = "load_batch" + LOAD_FROM_URL = "load_from_url" + # Future job types: + # GENERATE_LIGHTCURVE = "generate_lightcurve" + # COMPUTE_POWER_SPECTRUM = "compute_power_spectrum" + # EXPORT_DATA = "export_data" + # DOWNLOAD_ARCHIVE = "download_archive" + + +@dataclass +class Job: + """ + Represents a background job in the queue. + + Attributes: + id: Unique identifier for the job (UUID) + type: Type of job (load_event_list, load_batch, etc.) + status: Current status of the job + progress: Progress percentage (0.0 to 1.0) + progress_message: Human-readable progress message + total_items: Total number of items to process (for batch jobs) + completed_items: Number of items completed + created_at: ISO timestamp when job was created + started_at: ISO timestamp when job started running + completed_at: ISO timestamp when job completed/failed/cancelled + params: Job-specific parameters (file paths, names, options, etc.) + result: Result data on successful completion + error: Error message on failure + display_name: Human-readable name for the job (shown in UI) + """ + id: str = field(default_factory=lambda: str(uuid.uuid4())) + type: JobType = JobType.LOAD_EVENT_LIST + status: JobStatus = JobStatus.PENDING + progress: float = 0.0 + progress_message: str = "Pending..." + total_items: int = 1 + completed_items: int = 0 + created_at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + started_at: Optional[str] = None + completed_at: Optional[str] = None + params: Dict[str, Any] = field(default_factory=dict) + result: Optional[Dict[str, Any]] = None + error: Optional[str] = None + display_name: str = "" + + def to_dict(self) -> Dict[str, Any]: + """Convert job to dictionary for JSON serialization.""" + return { + "id": self.id, + "type": self.type.value, + "status": self.status.value, + "progress": self.progress, + "progress_message": self.progress_message, + "total_items": self.total_items, + "completed_items": self.completed_items, + "created_at": self.created_at, + "started_at": self.started_at, + "completed_at": self.completed_at, + "params": self.params, + "result": self.result, + "error": self.error, + "display_name": self.display_name, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Job": + """Create a Job from a dictionary.""" + return cls( + id=data.get("id", str(uuid.uuid4())), + type=JobType(data.get("type", "load_event_list")), + status=JobStatus(data.get("status", "pending")), + progress=data.get("progress", 0.0), + progress_message=data.get("progress_message", "Pending..."), + total_items=data.get("total_items", 1), + completed_items=data.get("completed_items", 0), + created_at=data.get("created_at", datetime.now(timezone.utc).isoformat()), + started_at=data.get("started_at"), + completed_at=data.get("completed_at"), + params=data.get("params", {}), + result=data.get("result"), + error=data.get("error"), + display_name=data.get("display_name", ""), + ) + + def start(self) -> None: + """Mark job as started.""" + self.status = JobStatus.RUNNING + self.started_at = datetime.now(timezone.utc).isoformat() + self.progress_message = "Running..." + + def complete(self, result: Optional[Dict[str, Any]] = None) -> None: + """Mark job as completed successfully.""" + self.status = JobStatus.COMPLETED + self.completed_at = datetime.now(timezone.utc).isoformat() + self.progress = 1.0 + self.progress_message = "Completed" + self.result = result + + def fail(self, error: str) -> None: + """Mark job as failed.""" + self.status = JobStatus.FAILED + self.completed_at = datetime.now(timezone.utc).isoformat() + self.error = error + self.progress_message = f"Failed: {error}" + + def cancel(self) -> None: + """Mark job as cancelled.""" + self.status = JobStatus.CANCELLED + self.completed_at = datetime.now(timezone.utc).isoformat() + self.progress_message = "Cancelled" + + def update_progress( + self, + progress: float, + message: str = "", + completed_items: Optional[int] = None, + ) -> None: + """Update job progress.""" + self.progress = min(max(progress, 0.0), 1.0) # Clamp 0-1 + if message: + self.progress_message = message + if completed_items is not None: + self.completed_items = completed_items + + @property + def is_active(self) -> bool: + """Check if job is still active (pending or running).""" + return self.status in (JobStatus.PENDING, JobStatus.RUNNING) + + @property + def is_finished(self) -> bool: + """Check if job has finished (completed, failed, or cancelled).""" + return self.status in ( + JobStatus.COMPLETED, + JobStatus.FAILED, + JobStatus.CANCELLED, + ) diff --git a/python-backend/requirements.txt b/python-backend/requirements.txt new file mode 100644 index 0000000..97ef5ae --- /dev/null +++ b/python-backend/requirements.txt @@ -0,0 +1,30 @@ +# Stingray Explorer Python Backend Dependencies +# Core +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +pydantic>=2.5.0 + +# Stingray and scientific stack +stingray>=2.0.0 +numpy>=1.24.0 +scipy>=1.11.0 +astropy>=5.3.0 +pandas>=2.0.0 +matplotlib>=3.7.0 +tables>=3.8.0 + +# HTTP client (for URL loading) +requests>=2.31.0 +httpx>=0.26.0 + +# Performance monitoring +psutil>=5.9.0 + +# CORS support +python-multipart>=0.0.6 + +# Optional: async support +aiofiles>=23.2.1 + +# HEASARC archive queries +astroquery>=0.4.8 diff --git a/python-backend/routes/__init__.py b/python-backend/routes/__init__.py new file mode 100644 index 0000000..255de57 --- /dev/null +++ b/python-backend/routes/__init__.py @@ -0,0 +1,11 @@ +"""API routes for Stingray Explorer.""" + +from . import data_routes, lightcurve_routes, spectrum_routes, timing_routes, export_routes + +__all__ = [ + "data_routes", + "lightcurve_routes", + "spectrum_routes", + "timing_routes", + "export_routes", +] diff --git a/python-backend/routes/archive_routes.py b/python-backend/routes/archive_routes.py new file mode 100644 index 0000000..4c8efd5 --- /dev/null +++ b/python-backend/routes/archive_routes.py @@ -0,0 +1,287 @@ +""" +API routes for HEASARC archive operations. + +Provides endpoints for searching NASA's HEASARC archive and downloading +X-ray observation data with progress tracking. +""" + +import json +from typing import Any, Dict, Optional, Tuple + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +from services.archive_service import ArchiveService + +router = APIRouter() + + +def get_archive_service(request: Request) -> ArchiveService: + """Get ArchiveService instance from app state.""" + return ArchiveService( + state_manager=request.app.state.state_manager, + performance_monitor=request.app.state.performance_monitor, + ) + + +def _iso_dates_to_mjd_range( + start_date: Optional[str], + end_date: Optional[str], +) -> Optional[Tuple[float, float]]: + """ + Convert ISO date strings to MJD time range tuple. + + Args: + start_date: Start date in ISO format "YYYY-MM-DD" or None + end_date: End date in ISO format "YYYY-MM-DD" or None + + Returns: + Tuple of (mjd_start, mjd_end) or None if neither date is provided + """ + if not start_date and not end_date: + return None + + from astropy.time import Time + + # Use wide defaults when only one bound is specified + mjd_start = 0.0 # Before any real observation + mjd_end = 99999.0 # Far future + + if start_date: + try: + mjd_start = Time(start_date, format="iso").mjd + except Exception: + pass + + if end_date: + try: + # Add ~1 day to include the end date fully + mjd_end = Time(end_date, format="iso").mjd + 1.0 + except Exception: + pass + + return (mjd_start, mjd_end) + + +# Request/Response Models +class SearchByNameRequest(BaseModel): + """Request model for searching by source name.""" + source_name: str + mission: str + radius: float = 0.5 # Search radius in degrees + max_results: int = 100 + min_exposure: Optional[float] = None # Minimum exposure in seconds + start_date: Optional[str] = None # ISO date string "YYYY-MM-DD" + end_date: Optional[str] = None # ISO date string "YYYY-MM-DD" + + +class SearchByCoordinatesRequest(BaseModel): + """Request model for searching by coordinates.""" + ra: float # Right Ascension in degrees + dec: float # Declination in degrees + mission: str + radius: float = 0.5 # Search radius in degrees + max_results: int = 100 + min_exposure: Optional[float] = None # Minimum exposure in seconds + start_date: Optional[str] = None # ISO date string "YYYY-MM-DD" + end_date: Optional[str] = None # ISO date string "YYYY-MM-DD" + + +class SearchByObsidRequest(BaseModel): + """Request model for searching by Observation ID.""" + obsid: str + mission: str + + +class ListFilesRequest(BaseModel): + """Request model for listing files in an observation directory.""" + mission: str + obsid: str + obs_time: Optional[str] = None # Observation time (MJD or ISO string) for directory lookup + obs_data: Optional[Dict[str, Any]] = None # Additional observation data (e.g., prnb for RXTE, ra/dec for locate_data) + recursive: bool = True + max_depth: int = 3 + + +class DownloadToDiskRequest(BaseModel): + """Request model for downloading a file to local disk.""" + url: str # URL to download from + save_path: str # Local path to save the file + + +# Routes +@router.get("/catalogs") +async def get_catalogs( + service: ArchiveService = Depends(get_archive_service), +): + """ + Get list of supported HEASARC catalogs. + + Returns information about available X-ray mission catalogs + that can be searched. + """ + return service.get_supported_catalogs() + + +@router.post("/search/name") +async def search_by_name( + request: SearchByNameRequest, + service: ArchiveService = Depends(get_archive_service), +): + """ + Search HEASARC for observations by source name. + + Resolves the source name to coordinates using SIMBAD/NED, + then queries the HEASARC catalog for matching observations. + + Args: + source_name: Astronomical source name (e.g., "Crab", "Cyg X-1", "NGC 3783") + mission: Mission to search (e.g., "NICER", "NuSTAR", "Chandra") + radius: Search radius in degrees (default: 0.5) + max_results: Maximum number of results (default: 100) + """ + time_range = _iso_dates_to_mjd_range(request.start_date, request.end_date) + return service.search_by_name( + source_name=request.source_name, + mission=request.mission, + radius=request.radius, + max_results=request.max_results, + min_exposure=request.min_exposure, + time_range=time_range, + ) + + +@router.post("/search/coordinates") +async def search_by_coordinates( + request: SearchByCoordinatesRequest, + service: ArchiveService = Depends(get_archive_service), +): + """ + Search HEASARC for observations by coordinates. + + Args: + ra: Right Ascension in degrees + dec: Declination in degrees + mission: Mission to search (e.g., "NICER", "NuSTAR", "Chandra") + radius: Search radius in degrees (default: 0.5) + max_results: Maximum number of results (default: 100) + """ + time_range = _iso_dates_to_mjd_range(request.start_date, request.end_date) + return service.search_by_coordinates( + ra=request.ra, + dec=request.dec, + mission=request.mission, + radius=request.radius, + max_results=request.max_results, + min_exposure=request.min_exposure, + time_range=time_range, + ) + + +@router.post("/search/obsid") +async def search_by_obsid( + request: SearchByObsidRequest, + service: ArchiveService = Depends(get_archive_service), +): + """ + Search HEASARC for an observation by its Observation ID. + + Uses ADQL TAP query to directly look up the observation — + no coordinates needed. + + Args: + obsid: Observation ID (e.g., "4010080142") + mission: Mission to search (e.g., "NICER", "NuSTAR") + """ + return service.search_by_obsid( + obsid=request.obsid, + mission=request.mission, + ) + + +@router.get("/observation/{mission}/{obsid}") +async def get_observation_urls( + mission: str, + obsid: str, + service: ArchiveService = Depends(get_archive_service), +): + """ + Get download URLs for a specific observation. + + Returns available download URLs from HEASARC, SciServer, and AWS + for the specified observation. + """ + return service.get_observation_download_urls( + mission=mission, + obsid=obsid, + ) + + +@router.post("/list-files") +async def list_observation_files( + request: ListFilesRequest, + service: ArchiveService = Depends(get_archive_service), +): + """ + List all files in an observation directory. + + Returns a tree structure of files with metadata including: + - File names and paths + - File sizes (when available) + - File type classification (event, calibration, auxiliary, log, other) + - Full download URLs + + Args: + mission: Mission key (e.g., "NICER", "NuSTAR", "Chandra") + obsid: Observation ID + obs_time: Observation time (MJD or ISO string) - required for some missions + recursive: Whether to recursively list subdirectories (default: True) + max_depth: Maximum recursion depth (default: 3) + """ + return await service.list_observation_files( + mission=request.mission, + obsid=request.obsid, + obs_time=request.obs_time, + obs_data=request.obs_data, + recursive=request.recursive, + max_depth=request.max_depth, + ) + + +@router.post("/download-to-disk") +async def download_to_disk( + request: DownloadToDiskRequest, + service: ArchiveService = Depends(get_archive_service), +): + """ + Download a file from URL to local disk with SSE progress streaming. + + This endpoint bypasses CORS restrictions by downloading through the backend. + Progress is streamed as Server-Sent Events (SSE). + + SSE Event Format: + - type: "progress" - Download progress with bytes_downloaded, total_bytes, percent + - type: "complete" - Download finished with file_path and size_bytes + - type: "error" - An error occurred with error message + + Args: + url: URL to download from (e.g., HEASARC HTTPS URL) + save_path: Local path to save the file + """ + async def event_generator(): + async for event in service.download_file_to_disk( + url=request.url, + save_path=request.save_path, + ): + yield f"data: {json.dumps(event)}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) diff --git a/python-backend/routes/data_routes.py b/python-backend/routes/data_routes.py new file mode 100644 index 0000000..3b41f52 --- /dev/null +++ b/python-backend/routes/data_routes.py @@ -0,0 +1,468 @@ +""" +API routes for EventList data operations. +""" + +import asyncio +import json +from typing import List, Optional + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +from services.data_service import DataService +from services.state_manager import StateManager + +router = APIRouter() + + +def get_data_service(request: Request) -> DataService: + """Get DataService instance from app state.""" + return DataService( + state_manager=request.app.state.state_manager, + performance_monitor=request.app.state.performance_monitor, + ) + + +# Request/Response Models +class LoadEventListRequest(BaseModel): + file_path: str + name: str + fmt: str = "ogip" + rmf_file: Optional[str] = None + additional_columns: Optional[List[str]] = None + high_precision: bool = False + skip_checks: bool = False + notes: Optional[str] = None + + +class LoadEventListFromUrlRequest(BaseModel): + url: str + name: str + fmt: str = "ogip" + rmf_file: Optional[str] = None + additional_columns: Optional[List[str]] = None + high_precision: bool = False + skip_checks: bool = False + notes: Optional[str] = None + + +class SaveEventListRequest(BaseModel): + name: str + file_path: str + fmt: str = "ogip" + + +class CheckFileSizeRequest(BaseModel): + file_path: str + + +class LoadByTimeRangeRequest(BaseModel): + """Request model for true lazy loading by time range.""" + file_path: str + name: str + start_time: float + end_time: float + fmt: str = "ogip" + notes: Optional[str] = None + + +class LoadByEventCountRequest(BaseModel): + """Request model for true lazy loading by event count.""" + file_path: str + name: str + start_index: int = 0 + count: int = 10000 + fmt: str = "ogip" + notes: Optional[str] = None + + +class GetFileMetadataRequest(BaseModel): + """Request model for getting file metadata without loading.""" + file_path: str + fmt: str = "ogip" + + +class SingleFileConfig(BaseModel): + """Configuration for a single file in batch load.""" + file_path: str + name: str + fmt: str = "ogip" + rmf_file: Optional[str] = None + additional_columns: Optional[List[str]] = None + high_precision: bool = False + skip_checks: bool = False + # Per-file partial loading (only used if use_same_settings=False) + use_partial_loading: bool = False + partial_mode: str = "time_range" + time_range_start: Optional[float] = None + time_range_end: Optional[float] = None + event_start_index: Optional[int] = None + event_count: Optional[int] = None + # Per-file notes + notes: Optional[str] = None + + +class BatchLoadEventListRequest(BaseModel): + """Request for batch loading multiple files.""" + files: List[SingleFileConfig] + + # Toggle: same settings vs per-file + use_same_settings: bool = True + + # Shared settings (used when use_same_settings=True) + shared_fmt: str = "ogip" + shared_rmf_file: Optional[str] = None + shared_additional_columns: Optional[List[str]] = None + shared_high_precision: bool = False + shared_skip_checks: bool = False + shared_use_partial_loading: bool = False + shared_partial_mode: str = "time_range" + shared_time_range_start: Optional[float] = None + shared_time_range_end: Optional[float] = None + shared_event_start_index: Optional[int] = None + shared_event_count: Optional[int] = None + + +class BatchFileSizeRequest(BaseModel): + """Request for checking batch file sizes.""" + file_paths: List[str] + + +# Routes +@router.post("/load") +async def load_event_list( + request: LoadEventListRequest, + service: DataService = Depends(get_data_service), +): + """Load an EventList from a file. + + Uses asyncio.to_thread() to avoid blocking the event loop, + allowing other async operations (like resource monitoring) to continue. + """ + return await asyncio.to_thread( + service.load_event_list, + file_path=request.file_path, + name=request.name, + fmt=request.fmt, + rmf_file=request.rmf_file, + additional_columns=request.additional_columns, + high_precision=request.high_precision, + skip_checks=request.skip_checks, + notes=request.notes, + ) + + +@router.post("/load-url") +async def load_event_list_from_url( + request: LoadEventListFromUrlRequest, + service: DataService = Depends(get_data_service), +): + """Load an EventList from a URL. + + Uses asyncio.to_thread() to avoid blocking the event loop. + """ + return await asyncio.to_thread( + service.load_event_list_from_url, + url=request.url, + name=request.name, + fmt=request.fmt, + rmf_file=request.rmf_file, + additional_columns=request.additional_columns, + high_precision=request.high_precision, + skip_checks=request.skip_checks, + ) + + +@router.post("/load-url-stream") +async def load_event_list_from_url_stream( + request: LoadEventListFromUrlRequest, + service: DataService = Depends(get_data_service), +): + """ + Load an EventList from a URL with SSE streaming for progress updates. + + Returns Server-Sent Events (SSE) with download and processing progress, + allowing the frontend to show real-time download progress. + + SSE Event Format: + - type: "progress" - Download progress with bytes_downloaded, total_bytes, percent + - type: "processing" - Download complete, now loading event list + - type: "complete" - Successfully loaded, includes data summary + - type: "error" - An error occurred + """ + async def event_generator(): + async for event in service.load_event_list_from_url_stream( + url=request.url, + name=request.name, + fmt=request.fmt, + rmf_file=request.rmf_file, + additional_columns=request.additional_columns, + high_precision=request.high_precision, + skip_checks=request.skip_checks, + notes=request.notes, + ): + yield f"data: {json.dumps(event)}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) + + +@router.post("/save") +async def save_event_list( + request: SaveEventListRequest, + service: DataService = Depends(get_data_service), +): + """Save an EventList to disk. + + Uses asyncio.to_thread() to avoid blocking the event loop. + """ + return await asyncio.to_thread( + service.save_event_list, + name=request.name, + file_path=request.file_path, + fmt=request.fmt, + ) + + +@router.delete("/{name}") +async def delete_event_list( + name: str, + service: DataService = Depends(get_data_service), +): + """Delete an EventList from state.""" + return service.delete_event_list(name) + + +@router.get("/{name}") +async def get_event_list_info( + name: str, + service: DataService = Depends(get_data_service), +): + """Get information about an EventList.""" + return service.get_event_list_info(name) + + +@router.get("/") +async def list_event_lists( + service: DataService = Depends(get_data_service), +): + """List all loaded EventLists.""" + return service.list_event_lists() + + +@router.post("/check-size") +async def check_file_size( + request: CheckFileSizeRequest, + service: DataService = Depends(get_data_service), +): + """Check file size and get loading recommendations.""" + return service.check_file_size(request.file_path) + + +@router.delete("/") +async def clear_all_event_lists( + service: DataService = Depends(get_data_service), +): + """Clear all loaded EventLists from memory.""" + return service.clear_all_event_lists() + + +@router.get("/{name}/full-preview") +async def get_event_list_full_preview( + name: str, + time_limit: int = 10, + service: DataService = Depends(get_data_service), +): + """Get full preview of an EventList with all attributes.""" + return service.get_event_list_full_preview(name=name, time_limit=time_limit) + + +# ========================================================================= +# PARTIAL LOADING ROUTES +# These endpoints use FITSTimeseriesReader to load only a portion of the file +# ========================================================================= + + +@router.post("/load-by-time-range") +async def load_event_list_by_time_range( + request: LoadByTimeRangeRequest, + service: DataService = Depends(get_data_service), +): + """ + Load events within a specific time range using true lazy loading. + + Uses FITSTimeseriesReader to load only events within the specified + time window without reading the entire file into memory. + + Uses asyncio.to_thread() to avoid blocking the event loop. + """ + return await asyncio.to_thread( + service.load_event_list_by_time_range, + file_path=request.file_path, + name=request.name, + start_time=request.start_time, + end_time=request.end_time, + fmt=request.fmt, + notes=request.notes, + ) + + +@router.post("/load-by-event-count") +async def load_event_list_by_event_count( + request: LoadByEventCountRequest, + service: DataService = Depends(get_data_service), +): + """ + Load a specific number of events using true lazy loading. + + Uses FITSTimeseriesReader slicing to load only the requested events + without reading the entire file into memory. + + Uses asyncio.to_thread() to avoid blocking the event loop. + """ + return await asyncio.to_thread( + service.load_event_list_by_event_count, + file_path=request.file_path, + name=request.name, + start_index=request.start_index, + count=request.count, + fmt=request.fmt, + notes=request.notes, + ) + + +@router.post("/metadata") +async def get_file_metadata( + request: GetFileMetadataRequest, + service: DataService = Depends(get_data_service), +): + """ + Get metadata from a FITS file without loading the full data. + + Returns file info, event count, time range, GTI, and loading recommendations + without loading any event data into memory. + + Uses asyncio.to_thread() to avoid blocking the event loop. + """ + return await asyncio.to_thread( + service.get_file_metadata, + file_path=request.file_path, + fmt=request.fmt, + ) + + +# ========================================================================= +# BATCH LOADING ROUTES +# Load multiple files in parallel +# ========================================================================= + + +@router.post("/check-batch-size") +async def check_batch_file_size( + request: BatchFileSizeRequest, + service: DataService = Depends(get_data_service), +): + """ + Check sizes of multiple files and estimate total memory usage. + + Returns per-file and total memory estimates with risk levels. + """ + return service.check_batch_file_size(file_paths=request.file_paths) + + +@router.post("/load-batch") +async def load_batch_event_lists( + request: BatchLoadEventListRequest, + service: DataService = Depends(get_data_service), +): + """ + Load multiple EventLists in parallel using threads. + + Supports two modes: + - use_same_settings=True: Apply shared_* settings to all files + - use_same_settings=False: Use per-file settings from each SingleFileConfig + + Returns aggregated results with successful[], failed[], and summary stats. + + Note: Uses asyncio.to_thread() to avoid blocking the event loop, + allowing other async operations (like resource monitoring) to continue. + """ + # Convert Pydantic models to dicts for the service + files_dict = [f.model_dump() for f in request.files] + + # Run the blocking batch load in a thread to avoid blocking the event loop + return await asyncio.to_thread( + service.load_batch_event_lists, + files=files_dict, + use_same_settings=request.use_same_settings, + shared_fmt=request.shared_fmt, + shared_rmf_file=request.shared_rmf_file, + shared_additional_columns=request.shared_additional_columns, + shared_high_precision=request.shared_high_precision, + shared_skip_checks=request.shared_skip_checks, + shared_use_partial_loading=request.shared_use_partial_loading, + shared_partial_mode=request.shared_partial_mode, + shared_time_range_start=request.shared_time_range_start, + shared_time_range_end=request.shared_time_range_end, + shared_event_start_index=request.shared_event_start_index, + shared_event_count=request.shared_event_count, + ) + + +@router.post("/load-batch-stream") +async def load_batch_event_lists_stream( + request: BatchLoadEventListRequest, + service: DataService = Depends(get_data_service), +): + """ + Stream batch loading results via Server-Sent Events (SSE). + + Returns individual file completion events as they finish, allowing + the frontend to update progress in real-time rather than waiting + for all files to complete. + + SSE Event Format: + - type: "file_complete" - A single file finished (success or failure) + - type: "complete" - All files finished, includes summary stats + - type: "error" - Pre-validation error (e.g., duplicate names) + + This endpoint is preferred for batch loading multiple files, + especially when some files may be significantly larger than others. + """ + files_dict = [f.model_dump() for f in request.files] + + async def event_generator(): + async for event in service.load_batch_event_lists_stream( + files=files_dict, + use_same_settings=request.use_same_settings, + shared_fmt=request.shared_fmt, + shared_rmf_file=request.shared_rmf_file, + shared_additional_columns=request.shared_additional_columns, + shared_high_precision=request.shared_high_precision, + shared_skip_checks=request.shared_skip_checks, + shared_use_partial_loading=request.shared_use_partial_loading, + shared_partial_mode=request.shared_partial_mode, + shared_time_range_start=request.shared_time_range_start, + shared_time_range_end=request.shared_time_range_end, + shared_event_start_index=request.shared_event_start_index, + shared_event_count=request.shared_event_count, + ): + yield f"data: {json.dumps(event)}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", # Disable nginx buffering if behind proxy + }, + ) diff --git a/python-backend/routes/export_routes.py b/python-backend/routes/export_routes.py new file mode 100644 index 0000000..dbe06e8 --- /dev/null +++ b/python-backend/routes/export_routes.py @@ -0,0 +1,111 @@ +""" +API routes for data export operations. +""" + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel + +from services.export_service import ExportService + +router = APIRouter() + + +def get_export_service(request: Request) -> ExportService: + """Get ExportService instance from app state.""" + return ExportService( + state_manager=request.app.state.state_manager, + performance_monitor=request.app.state.performance_monitor, + ) + + +# Request Models +class ExportToCsvRequest(BaseModel): + name: str + file_path: str + + +class ExportToHdf5Request(BaseModel): + name: str + file_path: str + data_type: str = "event_list" + + +class ExportToFitsRequest(BaseModel): + name: str + file_path: str + data_type: str = "event_list" + + +# Routes +@router.post("/event-list/csv") +async def export_event_list_to_csv( + request: ExportToCsvRequest, + service: ExportService = Depends(get_export_service), +): + """Export an EventList to CSV file.""" + return service.export_event_list_to_csv( + name=request.name, + file_path=request.file_path, + ) + + +@router.post("/lightcurve/csv") +async def export_lightcurve_to_csv( + request: ExportToCsvRequest, + service: ExportService = Depends(get_export_service), +): + """Export a Lightcurve to CSV file.""" + return service.export_lightcurve_to_csv( + name=request.name, + file_path=request.file_path, + ) + + +@router.post("/spectrum/csv") +async def export_spectrum_to_csv( + request: ExportToCsvRequest, + service: ExportService = Depends(get_export_service), +): + """Export a spectrum to CSV file.""" + return service.export_power_spectrum_to_csv( + name=request.name, + file_path=request.file_path, + ) + + +@router.post("/bispectrum/csv") +async def export_bispectrum_to_csv( + request: ExportToCsvRequest, + service: ExportService = Depends(get_export_service), +): + """Export a bispectrum to CSV file.""" + return service.export_bispectrum_to_csv( + name=request.name, + file_path=request.file_path, + ) + + +@router.post("/hdf5") +async def export_to_hdf5( + request: ExportToHdf5Request, + service: ExportService = Depends(get_export_service), +): + """Export data to HDF5 file.""" + return service.export_to_hdf5( + name=request.name, + file_path=request.file_path, + data_type=request.data_type, + ) + + +@router.post("/fits") +async def export_to_fits( + request: ExportToFitsRequest, + service: ExportService = Depends(get_export_service), +): + """Export data to FITS file.""" + return service.export_to_fits( + name=request.name, + file_path=request.file_path, + data_type=request.data_type, + ) diff --git a/python-backend/routes/job_routes.py b/python-backend/routes/job_routes.py new file mode 100644 index 0000000..b71518a --- /dev/null +++ b/python-backend/routes/job_routes.py @@ -0,0 +1,455 @@ +""" +Job API routes for background task queue. + +Provides REST endpoints for submitting, listing, streaming, and cancelling jobs. +""" + +import json +import logging +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +# ============================================================================= +# Request/Response Models +# ============================================================================= + +class SubmitLoadJobRequest(BaseModel): + """Request body for submitting a single file load job.""" + file_path: str = Field(..., description="Path to the file to load") + name: str = Field(..., description="Name for the loaded event list") + fmt: str = Field(default="ogip", description="File format") + rmf_file: Optional[str] = Field(default=None, description="RMF file path") + additional_columns: Optional[List[str]] = Field(default=None, description="Additional columns to load") + high_precision: bool = Field(default=False, description="Use high precision loading") + skip_checks: bool = Field(default=False, description="Skip validation checks") + notes: Optional[str] = Field(default=None, description="User notes/comments") + use_partial_loading: bool = Field(default=False, description="Use partial/lazy loading") + partial_mode: str = Field(default="time_range", description="Partial loading mode") + time_range_start: Optional[float] = Field(default=None, description="Start time for time range loading") + time_range_end: Optional[float] = Field(default=None, description="End time for time range loading") + event_start_index: Optional[int] = Field(default=None, description="Start index for event count loading") + event_count: Optional[int] = Field(default=None, description="Number of events to load") + + +class FileConfig(BaseModel): + """Configuration for a single file in batch loading.""" + file_path: str + name: str + fmt: Optional[str] = None + rmf_file: Optional[str] = None + additional_columns: Optional[List[str]] = None + high_precision: Optional[bool] = None + skip_checks: Optional[bool] = None + use_partial_loading: Optional[bool] = None + partial_mode: Optional[str] = None + time_range_start: Optional[float] = None + time_range_end: Optional[float] = None + event_start_index: Optional[int] = None + event_count: Optional[int] = None + notes: Optional[str] = None + + +class SubmitBatchJobRequest(BaseModel): + """Request body for submitting a batch load job.""" + files: List[FileConfig] = Field(..., description="List of files to load") + use_same_settings: bool = Field(default=True, description="Use shared settings for all files") + shared_fmt: str = Field(default="ogip", description="Shared file format") + shared_rmf_file: Optional[str] = Field(default=None, description="Shared RMF file path") + shared_additional_columns: Optional[List[str]] = Field(default=None, description="Shared additional columns") + shared_high_precision: bool = Field(default=False, description="Shared high precision setting") + shared_skip_checks: bool = Field(default=False, description="Shared skip checks setting") + shared_use_partial_loading: bool = Field(default=False, description="Shared partial loading setting") + shared_partial_mode: str = Field(default="time_range", description="Shared partial loading mode") + shared_time_range_start: Optional[float] = Field(default=None, description="Shared time range start") + shared_time_range_end: Optional[float] = Field(default=None, description="Shared time range end") + shared_event_start_index: Optional[int] = Field(default=None, description="Shared event start index") + shared_event_count: Optional[int] = Field(default=None, description="Shared event count") + + +class SubmitUrlJobRequest(BaseModel): + """Request body for submitting a URL download job.""" + url: str = Field(..., description="URL to download") + name: str = Field(..., description="Name for the loaded event list") + fmt: str = Field(default="ogip", description="File format") + rmf_file: Optional[str] = Field(default=None, description="RMF file path") + additional_columns: Optional[List[str]] = Field(default=None, description="Additional columns to load") + high_precision: bool = Field(default=False, description="Use high precision loading") + skip_checks: bool = Field(default=False, description="Skip validation checks") + notes: Optional[str] = Field(default=None, description="User notes/comments") + + +class CheckNameRequest(BaseModel): + """Request body for checking name conflicts.""" + name: str = Field(..., description="Name to check") + + +class ApiResponse(BaseModel): + """Standard API response format.""" + success: bool + data: Optional[Any] = None + message: str = "" + error: Optional[str] = None + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def get_job_manager(request: Request): + """Get the job manager from app state.""" + job_manager = getattr(request.app.state, "job_manager", None) + if job_manager is None: + raise HTTPException(status_code=500, detail="Job manager not initialized") + return job_manager + + +# ============================================================================= +# API Endpoints +# ============================================================================= + +@router.post("/submit-load", response_model=ApiResponse) +async def submit_load_job(request: Request, body: SubmitLoadJobRequest) -> ApiResponse: + """ + Submit a single file load job. + + Returns immediately with the job ID. The actual loading happens + asynchronously in a background thread. + """ + job_manager = get_job_manager(request) + + try: + job = job_manager.submit_load_job( + file_path=body.file_path, + name=body.name, + fmt=body.fmt, + rmf_file=body.rmf_file, + additional_columns=body.additional_columns, + high_precision=body.high_precision, + skip_checks=body.skip_checks, + notes=body.notes, + use_partial_loading=body.use_partial_loading, + partial_mode=body.partial_mode, + time_range_start=body.time_range_start, + time_range_end=body.time_range_end, + event_start_index=body.event_start_index, + event_count=body.event_count, + ) + + return ApiResponse( + success=True, + data=job.to_dict(), + message=f"Job submitted: {job.display_name}", + ) + + except Exception as e: + logger.exception("Failed to submit load job") + return ApiResponse( + success=False, + error=str(e), + message="Failed to submit job", + ) + + +@router.post("/submit-batch", response_model=ApiResponse) +async def submit_batch_job(request: Request, body: SubmitBatchJobRequest) -> ApiResponse: + """ + Submit a batch load job for multiple files. + + Returns immediately with the job ID. The actual loading happens + asynchronously in a background thread. + """ + job_manager = get_job_manager(request) + + try: + # Convert FileConfig models to dicts + files = [f.model_dump() for f in body.files] + + job = job_manager.submit_batch_load_job( + files=files, + use_same_settings=body.use_same_settings, + shared_fmt=body.shared_fmt, + shared_rmf_file=body.shared_rmf_file, + shared_additional_columns=body.shared_additional_columns, + shared_high_precision=body.shared_high_precision, + shared_skip_checks=body.shared_skip_checks, + shared_use_partial_loading=body.shared_use_partial_loading, + shared_partial_mode=body.shared_partial_mode, + shared_time_range_start=body.shared_time_range_start, + shared_time_range_end=body.shared_time_range_end, + shared_event_start_index=body.shared_event_start_index, + shared_event_count=body.shared_event_count, + ) + + return ApiResponse( + success=True, + data=job.to_dict(), + message=f"Batch job submitted: {len(files)} files", + ) + + except Exception as e: + logger.exception("Failed to submit batch job") + return ApiResponse( + success=False, + error=str(e), + message="Failed to submit batch job", + ) + + +@router.post("/submit-url", response_model=ApiResponse) +async def submit_url_job(request: Request, body: SubmitUrlJobRequest) -> ApiResponse: + """ + Submit a URL download and load job. + + Returns immediately with the job ID. The actual download and loading + happens asynchronously in a background thread. + """ + job_manager = get_job_manager(request) + + try: + job = job_manager.submit_url_load_job( + url=body.url, + name=body.name, + fmt=body.fmt, + rmf_file=body.rmf_file, + additional_columns=body.additional_columns, + high_precision=body.high_precision, + skip_checks=body.skip_checks, + notes=body.notes, + ) + + return ApiResponse( + success=True, + data=job.to_dict(), + message=f"URL job submitted: {job.display_name}", + ) + + except Exception as e: + logger.exception("Failed to submit URL job") + return ApiResponse( + success=False, + error=str(e), + message="Failed to submit URL job", + ) + + +@router.get("/", response_model=ApiResponse) +async def list_jobs( + request: Request, + include_completed: bool = True, + limit: int = 50, +) -> ApiResponse: + """ + List all jobs, newest first. + + Args: + include_completed: Include completed/failed/cancelled jobs + limit: Maximum number of jobs to return + """ + job_manager = get_job_manager(request) + + try: + jobs = job_manager.list_jobs( + include_completed=include_completed, + limit=limit, + ) + + return ApiResponse( + success=True, + data=[job.to_dict() for job in jobs], + message=f"Found {len(jobs)} jobs", + ) + + except Exception as e: + logger.exception("Failed to list jobs") + return ApiResponse( + success=False, + error=str(e), + message="Failed to list jobs", + ) + + +@router.get("/active", response_model=ApiResponse) +async def get_active_jobs(request: Request) -> ApiResponse: + """Get all active (pending or running) jobs.""" + job_manager = get_job_manager(request) + + try: + jobs = job_manager.get_active_jobs() + + return ApiResponse( + success=True, + data=[job.to_dict() for job in jobs], + message=f"Found {len(jobs)} active jobs", + ) + + except Exception as e: + logger.exception("Failed to get active jobs") + return ApiResponse( + success=False, + error=str(e), + message="Failed to get active jobs", + ) + + +@router.get("/stream") +async def stream_job_updates(request: Request): + """ + SSE endpoint for real-time job updates. + + Streams job status updates as they happen, including: + - job_created: New job submitted + - job_started: Job execution started + - job_progress: Job progress updated + - job_completed: Job finished successfully + - job_failed: Job failed with error + - job_cancelled: Job was cancelled + - heartbeat: Keep-alive signal + + First sends initial_state with all currently active jobs. + """ + job_manager = get_job_manager(request) + + async def event_generator(): + """Generate SSE events from job updates.""" + async for update in job_manager.stream_updates(): + data = json.dumps(update) + yield f"data: {data}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", # Disable nginx buffering + }, + ) + + +@router.get("/{job_id}", response_model=ApiResponse) +async def get_job(request: Request, job_id: str) -> ApiResponse: + """Get a specific job by ID.""" + job_manager = get_job_manager(request) + + try: + job = job_manager.get_job(job_id) + + if job is None: + return ApiResponse( + success=False, + error="Job not found", + message=f"No job found with ID: {job_id}", + ) + + return ApiResponse( + success=True, + data=job.to_dict(), + message="Job found", + ) + + except Exception as e: + logger.exception(f"Failed to get job {job_id}") + return ApiResponse( + success=False, + error=str(e), + message="Failed to get job", + ) + + +@router.post("/{job_id}/cancel", response_model=ApiResponse) +async def cancel_job(request: Request, job_id: str) -> ApiResponse: + """ + Cancel a pending job. + + Only pending jobs can be cancelled. Running jobs cannot be interrupted. + """ + job_manager = get_job_manager(request) + + try: + success = job_manager.cancel_job(job_id) + + if success: + return ApiResponse( + success=True, + message=f"Job {job_id} cancelled", + ) + else: + job = job_manager.get_job(job_id) + if job is None: + return ApiResponse( + success=False, + error="Job not found", + message=f"No job found with ID: {job_id}", + ) + else: + return ApiResponse( + success=False, + error="Cannot cancel job", + message=f"Job is {job.status.value}, only pending jobs can be cancelled", + ) + + except Exception as e: + logger.exception(f"Failed to cancel job {job_id}") + return ApiResponse( + success=False, + error=str(e), + message="Failed to cancel job", + ) + + +@router.post("/check-name", response_model=ApiResponse) +async def check_name_conflict(request: Request, body: CheckNameRequest) -> ApiResponse: + """ + Check if a name conflicts with existing data or pending jobs. + + Returns conflict status and suggests an alternative name if needed. + """ + job_manager = get_job_manager(request) + + try: + result = job_manager.check_name_conflict(body.name) + + return ApiResponse( + success=True, + data=result, + message="Name conflict checked" if result["has_conflict"] else "Name available", + ) + + except Exception as e: + logger.exception("Failed to check name conflict") + return ApiResponse( + success=False, + error=str(e), + message="Failed to check name conflict", + ) + + +@router.delete("/completed", response_model=ApiResponse) +async def clear_completed_jobs(request: Request) -> ApiResponse: + """Clear all completed/failed/cancelled jobs.""" + job_manager = get_job_manager(request) + + try: + count = job_manager.clear_completed_jobs() + + return ApiResponse( + success=True, + data={"cleared_count": count}, + message=f"Cleared {count} completed jobs", + ) + + except Exception as e: + logger.exception("Failed to clear completed jobs") + return ApiResponse( + success=False, + error=str(e), + message="Failed to clear completed jobs", + ) diff --git a/python-backend/routes/lightcurve_routes.py b/python-backend/routes/lightcurve_routes.py new file mode 100644 index 0000000..bb05a4e --- /dev/null +++ b/python-backend/routes/lightcurve_routes.py @@ -0,0 +1,120 @@ +""" +API routes for Lightcurve operations. +""" + +import asyncio +from typing import List, Optional + +from fastapi import APIRouter, Depends, Query, Request +from pydantic import BaseModel, Field + +from services.lightcurve_service import DEFAULT_MAX_PLOT_POINTS, LightcurveService + +router = APIRouter() + + +def get_lightcurve_service(request: Request) -> LightcurveService: + """Get LightcurveService instance from app state.""" + return LightcurveService( + state_manager=request.app.state.state_manager, + performance_monitor=request.app.state.performance_monitor, + ) + + +# Request Models +class CreateLightcurveFromEventListRequest(BaseModel): + event_list_name: str + dt: float + output_name: str + gti: Optional[List[List[float]]] = None + max_points: Optional[int] = Field(default=DEFAULT_MAX_PLOT_POINTS, ge=0) + + +class CreateLightcurveFromArraysRequest(BaseModel): + times: List[float] + counts: List[float] + dt: float + output_name: str + + +class RebinLightcurveRequest(BaseModel): + name: str + rebin_factor: float + output_name: str + max_points: Optional[int] = Field(default=DEFAULT_MAX_PLOT_POINTS, ge=0) + + +# Routes +@router.post("/from-event-list") +async def create_lightcurve_from_event_list( + request: CreateLightcurveFromEventListRequest, + service: LightcurveService = Depends(get_lightcurve_service), +): + """Create a Lightcurve from an EventList.""" + return await asyncio.to_thread( + service.create_lightcurve_from_event_list, + event_list_name=request.event_list_name, + dt=request.dt, + output_name=request.output_name, + gti=request.gti, + max_points=request.max_points, + ) + + +@router.post("/from-arrays") +async def create_lightcurve_from_arrays( + request: CreateLightcurveFromArraysRequest, + service: LightcurveService = Depends(get_lightcurve_service), +): + """Create a Lightcurve from time and count arrays.""" + return await asyncio.to_thread( + service.create_lightcurve_from_arrays, + times=request.times, + counts=request.counts, + dt=request.dt, + output_name=request.output_name, + ) + + +@router.post("/rebin") +async def rebin_lightcurve( + request: RebinLightcurveRequest, + service: LightcurveService = Depends(get_lightcurve_service), +): + """Rebin a lightcurve.""" + return await asyncio.to_thread( + service.rebin_lightcurve, + name=request.name, + rebin_factor=request.rebin_factor, + output_name=request.output_name, + max_points=request.max_points, + ) + + +@router.get("/{name}") +async def get_lightcurve_data( + name: str, + max_points: int = Query(default=DEFAULT_MAX_PLOT_POINTS, ge=0), + service: LightcurveService = Depends(get_lightcurve_service), +): + """Get lightcurve data for plotting.""" + return await asyncio.to_thread( + service.get_lightcurve_data, name, max_points=max_points + ) + + +@router.get("/") +async def list_lightcurves( + service: LightcurveService = Depends(get_lightcurve_service), +): + """List all loaded lightcurves.""" + return await asyncio.to_thread(service.list_lightcurves) + + +@router.delete("/{name}") +async def delete_lightcurve( + name: str, + service: LightcurveService = Depends(get_lightcurve_service), +): + """Delete a lightcurve from state.""" + return await asyncio.to_thread(service.delete_lightcurve, name) diff --git a/python-backend/routes/log_routes.py b/python-backend/routes/log_routes.py new file mode 100644 index 0000000..80979db --- /dev/null +++ b/python-backend/routes/log_routes.py @@ -0,0 +1,112 @@ +""" +API routes for real-time log streaming via Server-Sent Events (SSE). +""" + +import json +import logging +import warnings + +from fastapi import APIRouter, Request +from fastapi.responses import StreamingResponse + +from utils.log_stream import log_stream_manager + +router = APIRouter() +logger = logging.getLogger(__name__) + + +@router.get("/stream") +async def stream_logs(request: Request) -> StreamingResponse: + """ + Stream log messages via Server-Sent Events (SSE). + + This endpoint provides real-time streaming of Python logging output + and warnings to the frontend. Logs are delivered as JSON objects + in the SSE data field. + + Event Format: + Log event: + { + "type": "log", + "timestamp": "2024-01-15T10:30:00.123Z", + "level": "warn", + "source": "python", + "logger": "stingray.events", + "message": "No GTI found, using whole time range" + } + + Heartbeat (every 30s of inactivity): + { + "type": "heartbeat", + "timestamp": "2024-01-15T10:30:30.000Z" + } + + Returns: + StreamingResponse with SSE content type + """ + + async def event_generator(): + """Generate SSE events from the log stream.""" + async for log_entry in log_stream_manager.stream_logs(): + # Check if client disconnected + if await request.is_disconnected(): + break + + # Format as SSE data event + yield f"data: {json.dumps(log_entry)}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", # Disable nginx buffering if behind proxy + }, + ) + + +@router.get("/status") +async def get_log_status() -> dict: + """ + Get the status of the log streaming system. + + Returns: + Dictionary with status information: + - installed: Whether log streaming is active + - active_connections: Number of connected SSE clients + - queue_size: Number of buffered log entries + """ + return { + "success": True, + "data": { + "installed": log_stream_manager.is_installed, + "active_connections": log_stream_manager.active_connections, + "queue_size": log_stream_manager.queue_size, + }, + "message": "Log stream status retrieved", + "error": None, + } + + +@router.post("/test") +async def test_log_generation() -> dict: + """ + Generate test log messages at various levels for testing the SSE stream. + + This endpoint is for development/testing purposes only. + """ + logger.debug("This is a DEBUG test message") + logger.info("This is an INFO test message") + logger.warning("This is a WARNING test message") + logger.error("This is an ERROR test message") + + # Also test warning capture + warnings.warn("This is a test warning from Python warnings module", UserWarning) + + return { + "success": True, + "data": None, + "message": "Test logs generated (debug, info, warning, error, and a Python warning)", + "error": None, + } diff --git a/python-backend/routes/spectrum_routes.py b/python-backend/routes/spectrum_routes.py new file mode 100644 index 0000000..c899d4b --- /dev/null +++ b/python-backend/routes/spectrum_routes.py @@ -0,0 +1,182 @@ +""" +API routes for spectrum operations. +""" + +import asyncio +from typing import Optional + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel + +from services.spectrum_service import SpectrumService + +router = APIRouter() + + +def get_spectrum_service(request: Request) -> SpectrumService: + """Get SpectrumService instance from app state.""" + return SpectrumService( + state_manager=request.app.state.state_manager, + performance_monitor=request.app.state.performance_monitor, + ) + + +# Request Models +class CreatePowerSpectrumRequest(BaseModel): + event_list_name: str + dt: float + norm: str = "leahy" + output_name: Optional[str] = None + + +class CreateAveragedPowerSpectrumRequest(BaseModel): + event_list_name: str + dt: float + segment_size: float + norm: str = "leahy" + output_name: Optional[str] = None + + +class CreateCrossSpectrumRequest(BaseModel): + event_list_1_name: str + event_list_2_name: str + dt: float + norm: str = "leahy" + output_name: Optional[str] = None + + +class CreateAveragedCrossSpectrumRequest(BaseModel): + event_list_1_name: str + event_list_2_name: str + dt: float + segment_size: float + norm: str = "leahy" + output_name: Optional[str] = None + + +class CreateDynamicalPowerSpectrumRequest(BaseModel): + event_list_name: str + dt: float + segment_size: float + norm: str = "leahy" + output_name: Optional[str] = None + + +class RebinSpectrumRequest(BaseModel): + name: str + rebin_factor: float + log: bool = False + output_name: Optional[str] = None + + +# Routes +@router.post("/power-spectrum") +async def create_power_spectrum( + request: CreatePowerSpectrumRequest, + service: SpectrumService = Depends(get_spectrum_service), +): + """Create a power spectrum from an EventList.""" + return await asyncio.to_thread( + service.create_power_spectrum, + event_list_name=request.event_list_name, + dt=request.dt, + norm=request.norm, + output_name=request.output_name, + ) + + +@router.post("/averaged-power-spectrum") +async def create_averaged_power_spectrum( + request: CreateAveragedPowerSpectrumRequest, + service: SpectrumService = Depends(get_spectrum_service), +): + """Create an averaged power spectrum from an EventList.""" + return await asyncio.to_thread( + service.create_averaged_power_spectrum, + event_list_name=request.event_list_name, + dt=request.dt, + segment_size=request.segment_size, + norm=request.norm, + output_name=request.output_name, + ) + + +@router.post("/cross-spectrum") +async def create_cross_spectrum( + request: CreateCrossSpectrumRequest, + service: SpectrumService = Depends(get_spectrum_service), +): + """Create a cross spectrum from two EventLists.""" + return await asyncio.to_thread( + service.create_cross_spectrum, + event_list_1_name=request.event_list_1_name, + event_list_2_name=request.event_list_2_name, + dt=request.dt, + norm=request.norm, + output_name=request.output_name, + ) + + +@router.post("/averaged-cross-spectrum") +async def create_averaged_cross_spectrum( + request: CreateAveragedCrossSpectrumRequest, + service: SpectrumService = Depends(get_spectrum_service), +): + """Create an averaged cross spectrum from two EventLists.""" + return await asyncio.to_thread( + service.create_averaged_cross_spectrum, + event_list_1_name=request.event_list_1_name, + event_list_2_name=request.event_list_2_name, + dt=request.dt, + segment_size=request.segment_size, + norm=request.norm, + output_name=request.output_name, + ) + + +@router.post("/dynamical-power-spectrum") +async def create_dynamical_power_spectrum( + request: CreateDynamicalPowerSpectrumRequest, + service: SpectrumService = Depends(get_spectrum_service), +): + """Create a dynamical power spectrum from an EventList.""" + return await asyncio.to_thread( + service.create_dynamical_power_spectrum, + event_list_name=request.event_list_name, + dt=request.dt, + segment_size=request.segment_size, + norm=request.norm, + output_name=request.output_name, + ) + + +@router.post("/rebin") +async def rebin_spectrum( + request: RebinSpectrumRequest, + service: SpectrumService = Depends(get_spectrum_service), +): + """Rebin a spectrum.""" + return await asyncio.to_thread( + service.rebin_spectrum, + name=request.name, + rebin_factor=request.rebin_factor, + log=request.log, + output_name=request.output_name, + ) + + +@router.get("/") +async def list_spectra( + service: SpectrumService = Depends(get_spectrum_service), +): + """List all loaded spectra.""" + return await asyncio.to_thread(service.list_spectra) + + +@router.delete("/{name}") +async def delete_spectrum( + name: str, + service: SpectrumService = Depends(get_spectrum_service), +): + """Delete a spectrum from state.""" + return await asyncio.to_thread(service.delete_spectrum, name) diff --git a/python-backend/routes/timing_routes.py b/python-backend/routes/timing_routes.py new file mode 100644 index 0000000..12871df --- /dev/null +++ b/python-backend/routes/timing_routes.py @@ -0,0 +1,123 @@ +""" +API routes for timing analysis operations. +""" + +import asyncio +from typing import Dict, Optional, Tuple + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel + +from services.timing_service import TimingService + +router = APIRouter() + + +def get_timing_service(request: Request) -> TimingService: + """Get TimingService instance from app state.""" + return TimingService( + state_manager=request.app.state.state_manager, + performance_monitor=request.app.state.performance_monitor, + ) + + +# Request Models +class CreateBispectrumRequest(BaseModel): + event_list_name: str + dt: float + maxlag: int = 25 + scale: str = "unbiased" + window: str = "uniform" + output_name: Optional[str] = None + + +class CalculatePowerColorsRequest(BaseModel): + event_list_name: str + dt: float + segment_size: float + freq_ranges: Dict[str, Tuple[float, float]] + output_name: Optional[str] = None + + +class CalculateTimeLagsRequest(BaseModel): + event_list_1_name: str + event_list_2_name: str + dt: float + segment_size: float + freq_range: Optional[Tuple[float, float]] = None + output_name: Optional[str] = None + + +class CalculateCoherenceRequest(BaseModel): + event_list_1_name: str + event_list_2_name: str + dt: float + segment_size: float + output_name: Optional[str] = None + + +# Routes +@router.post("/bispectrum") +async def create_bispectrum( + request: CreateBispectrumRequest, + service: TimingService = Depends(get_timing_service), +): + """Create a bispectrum from an EventList.""" + return await asyncio.to_thread( + service.create_bispectrum, + event_list_name=request.event_list_name, + dt=request.dt, + maxlag=request.maxlag, + scale=request.scale, + window=request.window, + output_name=request.output_name, + ) + + +@router.post("/power-colors") +async def calculate_power_colors( + request: CalculatePowerColorsRequest, + service: TimingService = Depends(get_timing_service), +): + """Calculate power colors from frequency bands.""" + return await asyncio.to_thread( + service.calculate_power_colors, + event_list_name=request.event_list_name, + dt=request.dt, + segment_size=request.segment_size, + freq_ranges=request.freq_ranges, + output_name=request.output_name, + ) + + +@router.post("/time-lags") +async def calculate_time_lags( + request: CalculateTimeLagsRequest, + service: TimingService = Depends(get_timing_service), +): + """Calculate time lags between two event lists.""" + return await asyncio.to_thread( + service.calculate_time_lags, + event_list_1_name=request.event_list_1_name, + event_list_2_name=request.event_list_2_name, + dt=request.dt, + segment_size=request.segment_size, + freq_range=request.freq_range, + output_name=request.output_name, + ) + + +@router.post("/coherence") +async def calculate_coherence( + request: CalculateCoherenceRequest, + service: TimingService = Depends(get_timing_service), +): + """Calculate coherence between two event lists.""" + return await asyncio.to_thread( + service.calculate_coherence, + event_list_1_name=request.event_list_1_name, + event_list_2_name=request.event_list_2_name, + dt=request.dt, + segment_size=request.segment_size, + output_name=request.output_name, + ) diff --git a/python-backend/services/__init__.py b/python-backend/services/__init__.py new file mode 100644 index 0000000..1309c2a --- /dev/null +++ b/python-backend/services/__init__.py @@ -0,0 +1,17 @@ +"""Services for Stingray Explorer backend.""" + +from .state_manager import StateManager +from .data_service import DataService +from .lightcurve_service import LightcurveService +from .spectrum_service import SpectrumService +from .timing_service import TimingService +from .export_service import ExportService + +__all__ = [ + "StateManager", + "DataService", + "LightcurveService", + "SpectrumService", + "TimingService", + "ExportService", +] diff --git a/python-backend/services/archive_service.py b/python-backend/services/archive_service.py new file mode 100644 index 0000000..9a5e0b1 --- /dev/null +++ b/python-backend/services/archive_service.py @@ -0,0 +1,1573 @@ +""" +Archive service for HEASARC catalog queries and data downloads. + +Provides functionality to search NASA's HEASARC archive for X-ray observations +and download data with progress tracking. +""" + +import asyncio +import os +import re +import tempfile +import time +from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple + +import httpx +from astropy import units as u +from astropy.coordinates import SkyCoord +from stingray import EventList + +from .base_service import BaseService + + +# Supported HEASARC catalogs for X-ray missions +SUPPORTED_CATALOGS = { + "NICER": { + "catalog": "nicermastr", + "display_name": "NICER", + "description": "Neutron star Interior Composition Explorer", + }, + "NuSTAR": { + "catalog": "numaster", + "display_name": "NuSTAR", + "description": "Nuclear Spectroscopic Telescope Array", + }, + "XMM-Newton": { + "catalog": "xmmmaster", + "display_name": "XMM-Newton", + "description": "X-ray Multi-Mirror Mission", + }, + "Chandra": { + "catalog": "chanmaster", + "display_name": "Chandra", + "description": "Chandra X-ray Observatory", + }, + "Swift": { + "catalog": "swiftmastr", + "display_name": "Swift", + "description": "Neil Gehrels Swift Observatory", + }, + "RXTE": { + "catalog": "xtemaster", + "display_name": "RXTE", + "description": "Rossi X-ray Timing Explorer", + }, + "IXPE": { + "catalog": "ixmaster", + "display_name": "IXPE", + "description": "Imaging X-ray Polarimetry Explorer", + }, + "Suzaku": { + "catalog": "suzamaster", + "display_name": "Suzaku", + "description": "Suzaku X-ray Satellite", + }, + "ASCA": { + "catalog": "ascamaster", + "display_name": "ASCA", + "description": "Advanced Satellite for Cosmology and Astrophysics", + }, + "XRISM": { + "catalog": "xrismmastr", + "display_name": "XRISM", + "description": "X-Ray Imaging and Spectroscopy Mission", + }, + "Hitomi": { + "catalog": "hitomaster", + "display_name": "Hitomi", + "description": "Hitomi (ASTRO-H) X-ray Satellite", + }, +} + + +def _to_python_float(val: Any) -> Optional[float]: + """Convert numpy numeric types to Python float for JSON serialization.""" + if val is None: + return None + try: + return float(val) + except (TypeError, ValueError): + return None + + +def _to_python_int(val: Any) -> Optional[int]: + """Convert numpy numeric types to Python int for JSON serialization.""" + if val is None: + return None + try: + return int(val) + except (TypeError, ValueError): + return None + + +class ArchiveService(BaseService): + """ + Service for HEASARC archive operations. + + Handles catalog queries and data downloads without any UI dependencies. + """ + + def get_supported_catalogs(self) -> Dict[str, Any]: + """ + Get list of supported HEASARC catalogs. + + Returns: + Result dictionary with catalog information + """ + catalogs = [ + { + "id": key, + "catalog": info["catalog"], + "display_name": info["display_name"], + "description": info["description"], + } + for key, info in SUPPORTED_CATALOGS.items() + ] + + return self.create_result( + success=True, + data={"catalogs": catalogs}, + message=f"Found {len(catalogs)} supported catalogs", + ) + + def _resolve_source_name(self, source_name: str) -> Optional[SkyCoord]: + """ + Resolve a source name to coordinates using SIMBAD/NED. + + Args: + source_name: Astronomical source name (e.g., "Crab", "Cyg X-1") + + Returns: + SkyCoord object or None if resolution fails + """ + try: + return SkyCoord.from_name(source_name) + except Exception: + return None + + def _table_to_observations( + self, table: Any, catalog_name: str + ) -> List[Dict[str, Any]]: + """ + Convert astropy Table from HEASARC query to list of observation dicts. + + Args: + table: Astropy Table from Heasarc.query_region() + catalog_name: Name of the HEASARC catalog + + Returns: + List of observation dictionaries + """ + observations = [] + + if table is None or len(table) == 0: + return observations + + # Column mapping varies by catalog - try common column names + # HEASARC returns lowercase column names from astroquery + obsid_cols = ["obsid", "obs_id", "observation_id", "seq_num", "sequence_number", + "OBSID", "OBS_ID", "OBSERVATION_ID", "SEQ_NUM", "SEQUENCE_NUMBER"] + name_cols = ["name", "target_name", "object", "src_name", "NAME", "TARGET_NAME", "OBJECT", "SRC_NAME"] + ra_cols = ["ra", "ra_obj", "ra_pnt", "RA", "RA_OBJ", "RA_PNT"] + dec_cols = ["dec", "dec_obj", "dec_pnt", "DEC", "DEC_OBJ", "DEC_PNT"] + # Mission-specific exposure columns: + # - NICER, Chandra, RXTE: "exposure" + # - NuSTAR: "exposure_a" (FPMA), also has exposure_b (FPMB) + # - XMM-Newton: "duration" + # - Swift: "xrt_exposure", "uvot_exposure", "bat_exposure" + # Swift catalog has NO generic "exposure" column, so we must + # prioritize instrument-specific columns for Swift. + if catalog_name == "Swift": + # For Swift, prefer xrt_exposure first (X-ray timing), then + # fall back to bat_exposure (BAT-only triggers have 0 XRT exposure) + exposure_cols = [ + "xrt_exposure", "XRT_EXPOSURE", + "bat_exposure", "BAT_EXPOSURE", + "uvot_exposure", "UVOT_EXPOSURE", + "exposure", "duration", "ontime", "livetime", + "EXPOSURE", "DURATION", "ONTIME", "LIVETIME", + ] + elif catalog_name == "IXPE": + # IXPE has per-detector-unit exposures: exposure_1, exposure_2, exposure_3 + # The main "exposure" column also exists + exposure_cols = [ + "exposure", "exposure_1", "exposure_2", "exposure_3", + "ontime_1", "ontime_2", "ontime_3", + "EXPOSURE", "EXPOSURE_1", "EXPOSURE_2", "EXPOSURE_3", + ] + else: + exposure_cols = [ + "exposure", "exposure_a", "duration", + "ontime", "livetime", "good_time", "xrt_exposure", + "EXPOSURE", "EXPOSURE_A", "DURATION", + "ONTIME", "LIVETIME", "GOOD_TIME", "XRT_EXPOSURE", + ] + time_cols = ["time", "start_time", "date_obs", "tstart", "TIME", "START_TIME", "DATE_OBS", "TSTART"] + + # Get table column names once + table_cols = set(table.colnames) + + def get_column_value(row: Any, col_names: List[str], default: Any = None) -> Any: + """Get value from first matching column.""" + for col in col_names: + if col in table_cols: + val = row[col] + # Handle masked arrays + if hasattr(val, "mask") and val.mask: + continue + return val + return default + + # RXTE-specific columns + prnb_cols = ["prnb", "PRNB"] + + # NICER-specific columns + nicer_status_cols = ["processing_status", "PROCESSING_STATUS"] + nicer_fpm_cols = ["num_fpm", "NUM_FPM"] + + for row in table: + try: + obs = { + "obsid": str(get_column_value(row, obsid_cols, "")), + "name": str(get_column_value(row, name_cols, "Unknown")), + "ra": _to_python_float(get_column_value(row, ra_cols)), + "dec": _to_python_float(get_column_value(row, dec_cols)), + "exposure": _to_python_float(get_column_value(row, exposure_cols, 0)), + "time": str(get_column_value(row, time_cols, "")), + "catalog": catalog_name, + } + + # Add mission-specific fields + # Swift: Include per-instrument exposures and pick the best + # non-zero exposure for the main "exposure" field + if catalog_name == "Swift": + swift_xrt_cols = ["xrt_exposure", "XRT_EXPOSURE"] + swift_bat_cols = ["bat_exposure", "BAT_EXPOSURE"] + swift_uvot_cols = ["uvot_exposure", "UVOT_EXPOSURE"] + xrt_exp = _to_python_float(get_column_value(row, swift_xrt_cols)) + bat_exp = _to_python_float(get_column_value(row, swift_bat_cols)) + uvot_exp = _to_python_float(get_column_value(row, swift_uvot_cols)) + obs["xrt_exposure"] = xrt_exp + obs["bat_exposure"] = bat_exp + obs["uvot_exposure"] = uvot_exp + # Use the best non-zero instrument exposure as the main + # exposure value (prefer XRT > BAT > UVOT) + if not obs["exposure"] or obs["exposure"] == 0: + for inst_exp in [xrt_exp, bat_exp, uvot_exp]: + if inst_exp and inst_exp > 0: + obs["exposure"] = inst_exp + break + + # IXPE: Include per-detector-unit exposures + if catalog_name == "IXPE": + ixpe_du1_cols = ["exposure_1", "EXPOSURE_1"] + ixpe_du2_cols = ["exposure_2", "EXPOSURE_2"] + ixpe_du3_cols = ["exposure_3", "EXPOSURE_3"] + obs["exposure_du1"] = _to_python_float(get_column_value(row, ixpe_du1_cols)) + obs["exposure_du2"] = _to_python_float(get_column_value(row, ixpe_du2_cols)) + obs["exposure_du3"] = _to_python_float(get_column_value(row, ixpe_du3_cols)) + + # NICER: Include processing status and number of FPMs + if catalog_name == "NICER": + obs["processing_status"] = str( + get_column_value(row, nicer_status_cols, "") + ) + obs["num_fpm"] = _to_python_int( + get_column_value(row, nicer_fpm_cols) + ) + + # NuSTAR: Include FPMB exposure, observation mode, issue flag + if catalog_name == "NuSTAR": + exp_b_cols = ["exposure_b", "EXPOSURE_B"] + obs_mode_cols = ["observation_mode", "OBSERVATION_MODE"] + issue_cols = ["issue_flag", "ISSUE_FLAG"] + obs["exposure_b"] = _to_python_float(get_column_value(row, exp_b_cols)) + obs["observation_mode"] = str(get_column_value(row, obs_mode_cols, "")) + obs["issue_flag"] = _to_python_int(get_column_value(row, issue_cols)) + + # XMM-Newton: Include per-instrument exposures, modes, and status + # query_region() returns: status, data_in_heasarc (always available) + # ADQL/TAP returns: pn_time, pn_mode, mos1_time, mos1_mode, + # mos2_time, mos2_mode (only via ObsID search) + if catalog_name == "XMM-Newton": + pn_time_cols = ["pn_time", "PN_TIME"] + pn_mode_cols = ["pn_mode", "PN_MODE"] + mos1_time_cols = ["mos1_time", "MOS1_TIME"] + mos1_mode_cols = ["mos1_mode", "MOS1_MODE"] + mos2_time_cols = ["mos2_time", "MOS2_TIME"] + mos2_mode_cols = ["mos2_mode", "MOS2_MODE"] + status_cols = ["status", "STATUS"] + data_avail_cols = ["data_in_heasarc", "DATA_IN_HEASARC"] + obs["pn_time"] = _to_python_float(get_column_value(row, pn_time_cols)) + obs["pn_mode"] = str(get_column_value(row, pn_mode_cols, "")) + obs["mos1_time"] = _to_python_float(get_column_value(row, mos1_time_cols)) + obs["mos1_mode"] = str(get_column_value(row, mos1_mode_cols, "")) + obs["mos2_time"] = _to_python_float(get_column_value(row, mos2_time_cols)) + obs["mos2_mode"] = str(get_column_value(row, mos2_mode_cols, "")) + obs["xmm_status"] = str(get_column_value(row, status_cols, "")) + obs["data_in_heasarc"] = str(get_column_value(row, data_avail_cols, "")) + + # Chandra: Include detector, grating, status + if catalog_name == "Chandra": + detector_cols = ["detector", "DETECTOR"] + grating_cols = ["grating", "GRATING"] + chandra_status_cols = ["status", "STATUS"] + obs["detector"] = str(get_column_value(row, detector_cols, "")) + obs["grating"] = str(get_column_value(row, grating_cols, "")) + obs["chandra_status"] = str(get_column_value(row, chandra_status_cols, "")) + + # RXTE: Include proposal number for directory lookup + prnb = get_column_value(row, prnb_cols) + if prnb is not None: + obs["prnb"] = str(prnb) + + # Only include observations with valid obsid + if obs["obsid"]: + observations.append(obs) + except Exception: + # Skip malformed rows + continue + + return observations + + def _apply_table_filters( + self, + table: Any, + min_exposure: Optional[float] = None, + time_range: Optional[Tuple[float, float]] = None, + ) -> Any: + """ + Apply post-query filters to an astropy Table. + + Args: + table: Astropy Table from HEASARC query + min_exposure: Minimum exposure time in seconds + time_range: Tuple of (mjd_start, mjd_end) for date filtering + + Returns: + Filtered astropy Table + """ + if table is None or len(table) == 0: + return table + + if min_exposure is not None: + exposure_col = None + for col in ["exposure", "exposure_a", "duration", "ontime", + "xrt_exposure", "bat_exposure"]: + if col in table.colnames: + exposure_col = col + break + if exposure_col is not None: + try: + table = table[table[exposure_col] >= min_exposure] + except Exception: + pass + + if time_range is not None: + time_col = None + for col in ["time", "start_time", "date_obs", "tstart"]: + if col in table.colnames: + time_col = col + break + if time_col is not None: + try: + mjd_start, mjd_end = time_range + table = table[ + (table[time_col] >= mjd_start) + & (table[time_col] <= mjd_end) + ] + except Exception: + pass + + return table + + def search_by_name( + self, + source_name: str, + mission: str, + radius: float = 0.5, + max_results: int = 100, + min_exposure: Optional[float] = None, + time_range: Optional[Tuple[float, float]] = None, + ) -> Dict[str, Any]: + """ + Search HEASARC for observations by source name. + + Uses SIMBAD/NED to resolve the source name to coordinates, + then queries the appropriate HEASARC catalog. + + Args: + source_name: Astronomical source name (e.g., "Crab", "Cyg X-1") + mission: Mission key (e.g., "NICER", "NuSTAR") + radius: Search radius in degrees + max_results: Maximum number of results to return + min_exposure: Minimum exposure time in seconds (post-query filter) + time_range: Tuple of (mjd_start, mjd_end) for date filtering + + Returns: + Result dictionary with observations + """ + try: + # Import here to avoid startup delay + from astroquery.heasarc import Heasarc + + # Validate mission + if mission not in SUPPORTED_CATALOGS: + return self.create_result( + success=False, + data=None, + message=f"Unsupported mission: {mission}", + error=f"Supported missions: {list(SUPPORTED_CATALOGS.keys())}", + ) + + catalog_info = SUPPORTED_CATALOGS[mission] + catalog_name = catalog_info["catalog"] + + # Resolve source name to coordinates + coords = self._resolve_source_name(source_name) + if coords is None: + return self.create_result( + success=False, + data=None, + message=f"Could not resolve source name: '{source_name}'", + error="Name resolution failed via SIMBAD/NED", + ) + + # Query HEASARC + try: + table = Heasarc.query_region( + coords, + catalog=catalog_name, + radius=radius * u.deg, + ) + except Exception as e: + return self.create_result( + success=False, + data=None, + message=f"HEASARC query failed: {str(e)}", + error=str(e), + ) + + # Apply post-query filters + table = self._apply_table_filters(table, min_exposure, time_range) + + # Convert to observations + observations = self._table_to_observations(table, mission) + + # Limit results + if len(observations) > max_results: + observations = observations[:max_results] + + return self.create_result( + success=True, + data={ + "observations": observations, + "count": len(observations), + "source_name": source_name, + "resolved_ra": _to_python_float(coords.ra.deg), + "resolved_dec": _to_python_float(coords.dec.deg), + "mission": mission, + "radius": radius, + }, + message=f"Found {len(observations)} observations for '{source_name}' in {mission}", + ) + + except Exception as e: + return self.handle_error( + e, + "Searching HEASARC by name", + source_name=source_name, + mission=mission, + ) + + def search_by_coordinates( + self, + ra: float, + dec: float, + mission: str, + radius: float = 0.5, + max_results: int = 100, + min_exposure: Optional[float] = None, + time_range: Optional[Tuple[float, float]] = None, + ) -> Dict[str, Any]: + """ + Search HEASARC for observations by coordinates. + + Args: + ra: Right Ascension in degrees + dec: Declination in degrees + mission: Mission key (e.g., "NICER", "NuSTAR") + radius: Search radius in degrees + max_results: Maximum number of results to return + min_exposure: Minimum exposure time in seconds (post-query filter) + time_range: Tuple of (mjd_start, mjd_end) for date filtering + + Returns: + Result dictionary with observations + """ + try: + # Import here to avoid startup delay + from astroquery.heasarc import Heasarc + + # Validate mission + if mission not in SUPPORTED_CATALOGS: + return self.create_result( + success=False, + data=None, + message=f"Unsupported mission: {mission}", + error=f"Supported missions: {list(SUPPORTED_CATALOGS.keys())}", + ) + + catalog_info = SUPPORTED_CATALOGS[mission] + catalog_name = catalog_info["catalog"] + + # Create coordinates + coords = SkyCoord(ra=ra * u.deg, dec=dec * u.deg) + + # Query HEASARC + try: + table = Heasarc.query_region( + coords, + catalog=catalog_name, + radius=radius * u.deg, + ) + except Exception as e: + return self.create_result( + success=False, + data=None, + message=f"HEASARC query failed: {str(e)}", + error=str(e), + ) + + # Apply post-query filters + table = self._apply_table_filters(table, min_exposure, time_range) + + # Convert to observations + observations = self._table_to_observations(table, mission) + + # Limit results + if len(observations) > max_results: + observations = observations[:max_results] + + return self.create_result( + success=True, + data={ + "observations": observations, + "count": len(observations), + "ra": ra, + "dec": dec, + "mission": mission, + "radius": radius, + }, + message=f"Found {len(observations)} observations at RA={ra:.4f}, Dec={dec:.4f} in {mission}", + ) + + except Exception as e: + return self.handle_error( + e, + "Searching HEASARC by coordinates", + ra=ra, + dec=dec, + mission=mission, + ) + + def search_by_obsid( + self, + obsid: str, + mission: str, + ) -> Dict[str, Any]: + """ + Search HEASARC for an observation by its ObsID using ADQL TAP query. + + This does not require coordinates — it directly queries the catalog + by observation ID. + + Args: + obsid: Observation ID to search for + mission: Mission key (e.g., "NICER", "NuSTAR") + + Returns: + Result dictionary with observations + """ + try: + from astroquery.heasarc import Heasarc + + # Validate mission + if mission not in SUPPORTED_CATALOGS: + return self.create_result( + success=False, + data=None, + message=f"Unsupported mission: {mission}", + error=f"Supported missions: {list(SUPPORTED_CATALOGS.keys())}", + ) + + if not obsid or not obsid.strip(): + return self.create_result( + success=False, + data=None, + message="Please enter an Observation ID", + error="Empty obsid", + ) + + catalog_info = SUPPORTED_CATALOGS[mission] + catalog_name = catalog_info["catalog"] + + # Sanitize obsid for ADQL + safe_obsid = obsid.strip().replace("'", "''") + adql = f"SELECT * FROM {catalog_name} WHERE obsid = '{safe_obsid}'" + + try: + tap_result = Heasarc.query_tap(adql, maxrec=10) + table = tap_result.to_table() + except Exception as e: + return self.create_result( + success=False, + data=None, + message=f"HEASARC ObsID query failed: {str(e)}", + error=str(e), + ) + + if table is None or len(table) == 0: + return self.create_result( + success=True, + data={ + "observations": [], + "count": 0, + "obsid": obsid, + "mission": mission, + }, + message=f"No observations found for ObsID '{obsid}' in {mission}", + ) + + # Convert to observations + observations = self._table_to_observations(table, mission) + + return self.create_result( + success=True, + data={ + "observations": observations, + "count": len(observations), + "obsid": obsid, + "mission": mission, + }, + message=f"Found {len(observations)} observation(s) for ObsID '{obsid}' in {mission}", + ) + + except Exception as e: + return self.handle_error( + e, + "Searching HEASARC by ObsID", + obsid=obsid, + mission=mission, + ) + + def get_observation_download_urls( + self, + mission: str, + obsid: str, + ) -> Dict[str, Any]: + """ + Get download URLs for an observation. + + Constructs browse URLs based on known HEASARC patterns. + + Args: + mission: Mission key (e.g., "NICER", "NuSTAR") + obsid: Observation ID + + Returns: + Result dictionary with download URLs + """ + try: + # Validate mission + if mission not in SUPPORTED_CATALOGS: + return self.create_result( + success=False, + data=None, + message=f"Unsupported mission: {mission}", + error=f"Supported missions: {list(SUPPORTED_CATALOGS.keys())}", + ) + + # Construct URLs based on known HEASARC patterns + urls = self._construct_download_urls(mission, obsid) + + return self.create_result( + success=True, + data={ + "urls": urls, + "mission": mission, + "obsid": obsid, + }, + message=f"Found download URLs for {mission} observation {obsid}", + ) + + except Exception as e: + return self.handle_error( + e, + "Getting download URLs", + mission=mission, + obsid=obsid, + ) + + def _construct_download_urls(self, mission: str, obsid: str) -> Dict[str, str]: + """ + Construct download URLs based on mission-specific patterns. + + HEASARC has standard URL patterns for each mission's data archive. + + Args: + mission: Mission key + obsid: Observation ID + + Returns: + Dict with url types as keys and URLs as values + """ + urls = {} + + # Base HEASARC FTP/HTTPS URL + base_url = "https://heasarc.gsfc.nasa.gov/FTP" + + if mission == "NICER": + # NICER data path: /nicer/data/obs/YYYY_MM/OBSID/ + # We can't know the exact date folder without more info + # But we can construct a search URL + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dnicermastr&obsid={obsid}" + + elif mission == "NuSTAR": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dnumaster&obsid={obsid}" + + elif mission == "XMM-Newton": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dxmmmaster&obsid={obsid}" + + elif mission == "Chandra": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dchanmaster&obsid={obsid}" + + elif mission == "Swift": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dswiftmastr&obsid={obsid}" + + elif mission == "RXTE": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dxtemaster&obsid={obsid}" + + elif mission == "IXPE": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dixmaster&obsid={obsid}" + + elif mission == "Suzaku": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dsuzamaster&obsid={obsid}" + + elif mission == "ASCA": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dascamaster&obsid={obsid}" + + elif mission == "XRISM": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dxrismmastr&obsid={obsid}" + + elif mission == "Hitomi": + urls["browse"] = f"https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3browse.pl?tablehead=name%3Dhitomaster&obsid={obsid}" + + return urls + + async def _list_directory_files(self, directory_url: str) -> List[str]: + """ + Parse HEASARC HTTPS directory listing to get file names. + + HEASARC serves directory listings as HTML pages. This method fetches + the HTML and extracts file names from anchor tags. + + Args: + directory_url: URL of the directory to list + + Returns: + List of file names (not full URLs) + + Raises: + httpx.HTTPStatusError: If the HTTP request fails + """ + from bs4 import BeautifulSoup + + async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client: + response = await client.get(directory_url) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "lxml") + files = [] + + for link in soup.find_all("a"): + href = link.get("href", "") + # Skip navigation links (parent dir, sorting, etc.) + if not href or href.startswith("?") or href.startswith("/"): + continue + # Skip subdirectories (end with /) + if href.endswith("/"): + continue + # Clean up URL-encoded characters + files.append(href) + + return files + + async def _list_directory_with_metadata( + self, directory_url: str + ) -> List[Dict[str, Any]]: + """ + Parse HEASARC HTTPS directory listing to get files with metadata. + + Returns both files and subdirectories with size information when available. + + Args: + directory_url: URL of the directory to list + + Returns: + List of dicts with keys: name, is_directory, size_bytes (may be None) + """ + from bs4 import BeautifulSoup + + async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client: + response = await client.get(directory_url) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "lxml") + entries = [] + + # HEASARC directory listings typically use
 format with size info
+        # or standard  tags. Try to extract size from the text content.
+        pre_content = soup.find("pre")
+
+        if pre_content:
+            # Parse Apache-style directory listing in 
 tag
+            # Format: "Name                    Last modified      Size"
+            text = pre_content.get_text()
+            for link in pre_content.find_all("a"):
+                href = link.get("href", "")
+                if not href or href.startswith("?") or href.startswith("/"):
+                    continue
+                if href == "../":
+                    continue
+
+                is_directory = href.endswith("/")
+                name = href.rstrip("/")
+
+                # Try to extract size from the line containing this link
+                size_bytes = None
+                link_text = link.get_text()
+                # Find the line containing this link and extract size
+                for line in text.split("\n"):
+                    if link_text in line:
+                        # Look for size pattern (e.g., "125M", "45K", "1.2G", "12345")
+                        parts = line.split()
+                        for part in parts:
+                            size_bytes = self._parse_size(part)
+                            if size_bytes is not None:
+                                break
+                        break
+
+                entries.append({
+                    "name": name,
+                    "is_directory": is_directory,
+                    "size_bytes": size_bytes,
+                })
+        else:
+            # Fallback: just get names from anchor tags
+            for link in soup.find_all("a"):
+                href = link.get("href", "")
+                if not href or href.startswith("?") or href.startswith("/"):
+                    continue
+                if href == "../":
+                    continue
+
+                is_directory = href.endswith("/")
+                name = href.rstrip("/")
+
+                entries.append({
+                    "name": name,
+                    "is_directory": is_directory,
+                    "size_bytes": None,
+                })
+
+        return entries
+
+    def _parse_size(self, size_str: str) -> Optional[int]:
+        """
+        Parse size string like '125M', '45K', '1.2G', '12345' to bytes.
+
+        Args:
+            size_str: Size string to parse
+
+        Returns:
+            Size in bytes or None if not a valid size
+        """
+        size_str = size_str.strip()
+        if not size_str:
+            return None
+
+        # Try numeric first
+        try:
+            return int(size_str)
+        except ValueError:
+            pass
+
+        # Try with suffix
+        suffixes = {
+            "K": 1024,
+            "M": 1024 * 1024,
+            "G": 1024 * 1024 * 1024,
+            "T": 1024 * 1024 * 1024 * 1024,
+        }
+
+        for suffix, multiplier in suffixes.items():
+            if size_str.upper().endswith(suffix):
+                try:
+                    num = float(size_str[:-1])
+                    return int(num * multiplier)
+                except ValueError:
+                    pass
+
+        return None
+
+    def _classify_file_type(self, filename: str, mission: str) -> str:
+        """
+        Classify a file into a type category based on its name.
+
+        Args:
+            filename: File name to classify
+            mission: Mission name for mission-specific patterns
+
+        Returns:
+            File type: 'event', 'calibration', 'auxiliary', 'log', 'other'
+        """
+        filename_lower = filename.lower()
+
+        # Strip compression suffixes for pattern matching
+        for ext in ('.gz', '.bz2', '.z', '.zip'):
+            if filename_lower.endswith(ext):
+                filename_lower = filename_lower[:-len(ext)]
+                break
+
+        # Event file patterns
+        event_patterns = [
+            "_cl.evt",
+            "_ufa.evt",
+            "evt.fits",
+            "_evt2.fits",
+            "evli",  # XMM event list
+        ]
+        if any(p in filename_lower for p in event_patterns):
+            return "event"
+
+        # Calibration patterns
+        cal_patterns = [
+            "_cal",
+            "response",
+            ".rmf",
+            ".arf",
+            "caldb",
+            "matrix",
+        ]
+        if any(p in filename_lower for p in cal_patterns):
+            return "calibration"
+
+        # Auxiliary patterns
+        aux_patterns = [
+            ".att",
+            ".orb",
+            "mkf",
+            ".gti",
+            "_uf.evt",  # Unfiltered (not cleaned)
+            "attitude",
+            "orbit",
+            "housekeeping",
+            "hk",
+            "_asol",   # Chandra aspect solution
+            "_dtf",    # Chandra dead time factor (HRC)
+            "_bpix",   # Chandra bad pixel list
+            "_fov",    # Chandra field of view
+        ]
+        if any(p in filename_lower for p in aux_patterns):
+            return "auxiliary"
+
+        # Log patterns
+        log_patterns = [".log", "readme", "index.html"]
+        if any(p in filename_lower for p in log_patterns):
+            return "log"
+
+        return "other"
+
+    def _format_size(self, size_bytes: Optional[int]) -> str:
+        """Format size in bytes to human-readable string."""
+        if size_bytes is None:
+            return "Unknown"
+
+        if size_bytes < 1024:
+            return f"{size_bytes} B"
+        elif size_bytes < 1024 * 1024:
+            return f"{size_bytes / 1024:.1f} KB"
+        elif size_bytes < 1024 * 1024 * 1024:
+            return f"{size_bytes / (1024 * 1024):.1f} MB"
+        else:
+            return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
+
+    async def list_observation_files(
+        self,
+        mission: str,
+        obsid: str,
+        obs_time: Optional[str] = None,
+        obs_data: Optional[Dict[str, Any]] = None,
+        recursive: bool = True,
+        max_depth: int = 3,
+    ) -> Dict[str, Any]:
+        """
+        List all files in an observation directory.
+
+        Uses _get_observation_directory_url() to get the base URL, then
+        recursively parses HTML directory listings to build a file tree.
+
+        Args:
+            mission: Mission key (e.g., "NICER", "NuSTAR")
+            obsid: Observation ID
+            obs_time: Observation time (MJD or ISO string) for directory lookup
+            obs_data: Additional observation data (e.g., prnb for RXTE, ra/dec for coordinate queries)
+            recursive: Whether to recursively list subdirectories
+            max_depth: Maximum recursion depth
+
+        Returns:
+            Result dictionary with file tree structure
+        """
+        try:
+            # Validate mission
+            if mission not in SUPPORTED_CATALOGS:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Unsupported mission: {mission}",
+                    error=f"Supported missions: {list(SUPPORTED_CATALOGS.keys())}",
+                )
+
+            # Get base directory URL
+            base_url = self._get_observation_directory_url(mission, obsid, obs_time, obs_data)
+
+            if not base_url:
+                # Try locate_data as fallback
+                base_url = await self._locate_observation_directory(mission, obsid, obs_data)
+
+            if not base_url:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Could not locate observation directory for {mission} {obsid}",
+                    error="Directory URL construction failed. The observation time may be needed.",
+                )
+
+            # Recursively list files
+            files = await self._list_files_recursive(
+                base_url, mission, recursive, max_depth, 0
+            )
+
+            return self.create_result(
+                success=True,
+                data={
+                    "base_url": base_url,
+                    "files": files,
+                    "mission": mission,
+                    "obsid": obsid,
+                    "total_files": self._count_files(files),
+                },
+                message=f"Found {self._count_files(files)} files in {mission} observation {obsid}",
+            )
+
+        except httpx.HTTPStatusError as e:
+            return self.create_result(
+                success=False,
+                data=None,
+                message=f"HTTP error listing directory: {e.response.status_code}",
+                error=str(e),
+            )
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Listing observation files",
+                mission=mission,
+                obsid=obsid,
+            )
+
+    async def _list_files_recursive(
+        self,
+        directory_url: str,
+        mission: str,
+        recursive: bool,
+        max_depth: int,
+        current_depth: int,
+    ) -> List[Dict[str, Any]]:
+        """
+        Recursively list files in a directory.
+
+        Args:
+            directory_url: URL of the directory to list
+            mission: Mission name for file classification
+            recursive: Whether to recurse into subdirectories
+            max_depth: Maximum recursion depth
+            current_depth: Current recursion depth
+
+        Returns:
+            List of file/directory entries
+        """
+        entries = await self._list_directory_with_metadata(directory_url)
+        result = []
+
+        for entry in entries:
+            name = entry["name"]
+            is_directory = entry["is_directory"]
+            size_bytes = entry["size_bytes"]
+
+            full_url = directory_url.rstrip("/") + "/" + name
+
+            if is_directory:
+                children = []
+                if recursive and current_depth < max_depth:
+                    try:
+                        children = await self._list_files_recursive(
+                            full_url + "/",
+                            mission,
+                            recursive,
+                            max_depth,
+                            current_depth + 1,
+                        )
+                    except Exception as e:
+                        # Log but don't fail if a subdirectory can't be listed
+                        print(f"Could not list subdirectory {full_url}: {e}")
+
+                result.append({
+                    "path": name,
+                    "name": name,
+                    "is_directory": True,
+                    "file_type": "directory",
+                    "size_bytes": None,
+                    "size_display": "",
+                    "full_url": full_url + "/",
+                    "children": children,
+                })
+            else:
+                # For files, try to get size via HEAD request if not available
+                if size_bytes is None:
+                    try:
+                        async with httpx.AsyncClient(timeout=10.0) as client:
+                            head_resp = await client.head(full_url, follow_redirects=True)
+                            size_bytes = int(head_resp.headers.get("content-length", 0)) or None
+                    except Exception:
+                        pass
+
+                file_type = self._classify_file_type(name, mission)
+                result.append({
+                    "path": name,
+                    "name": name,
+                    "is_directory": False,
+                    "file_type": file_type,
+                    "size_bytes": size_bytes,
+                    "size_display": self._format_size(size_bytes),
+                    "full_url": full_url,
+                })
+
+        return result
+
+    def _count_files(self, entries: List[Dict[str, Any]]) -> int:
+        """Count total number of files (not directories) in a tree."""
+        count = 0
+        for entry in entries:
+            if entry["is_directory"]:
+                count += self._count_files(entry.get("children", []))
+            else:
+                count += 1
+        return count
+
+    def _get_observation_directory_url(
+        self,
+        mission: str,
+        obsid: str,
+        obs_time: Optional[str] = None,
+        obs_data: Optional[Dict[str, Any]] = None,
+    ) -> Optional[str]:
+        """
+        Construct the likely directory URL for an observation.
+
+        HEASARC organizes data in mission-specific directory structures.
+        This method constructs the most likely directory URL based on
+        known patterns.
+
+        Args:
+            mission: Mission key
+            obsid: Observation ID
+            obs_time: Observation time (MJD float as string, or ISO date string)
+                     Required for NICER and Swift to determine the date-based subdirectory
+            obs_data: Additional observation data (e.g., prnb for RXTE)
+
+        Returns:
+            Directory URL or None if pattern unknown
+        """
+        from astropy.time import Time
+
+        base_url = "https://heasarc.gsfc.nasa.gov/FTP"
+        obs_data = obs_data or {}
+
+        # Helper to parse obs_time to YYYY_MM format
+        def get_year_month(time_str: Optional[str]) -> Optional[str]:
+            if not time_str:
+                return None
+            try:
+                # Try parsing as MJD first (numeric string)
+                try:
+                    mjd = float(time_str)
+                    t = Time(mjd, format="mjd")
+                except ValueError:
+                    # Try as ISO format
+                    t = Time(time_str, format="isot")
+                return t.datetime.strftime("%Y_%m")
+            except Exception:
+                return None
+
+        if mission == "NICER":
+            # NICER: /nicer/data/obs/YYYY_MM/OBSID/
+            year_month = get_year_month(obs_time)
+            if year_month:
+                return f"{base_url}/nicer/data/obs/{year_month}/{obsid}/"
+            return None
+
+        elif mission == "NuSTAR":
+            # NuSTAR ObsID format: CPPttxxxvvv (11 digits)
+            #   C = source category (1 digit): 1=calibration, 3=ToO, 6=AGN, 8=galactic
+            #   PP = proposal/cycle (2 digits): 00=primary, 01+=extended missions
+            # HEASARC archive path: /nustar/data/obs/PP/C/OBSID/
+            #   PP = obsid[1:3] (proposal cycle)
+            #   C  = obsid[0]  (source category)
+            # Example: 60002023006 -> /nustar/data/obs/00/6/60002023006/
+            if len(obsid) >= 4:
+                return f"{base_url}/nustar/data/obs/{obsid[1:3]}/{obsid[0]}/{obsid}/"
+            return None
+
+        elif mission == "Chandra":
+            # Chandra: /chandra/data/byobsid/X/OBSID/
+            # X = LAST digit of obsid
+            # Example: 758 -> /chandra/data/byobsid/8/758/
+            if obsid and obsid[-1].isdigit():
+                return f"{base_url}/chandra/data/byobsid/{obsid[-1]}/{obsid}/"
+            return None
+
+        elif mission == "Swift":
+            # Swift: /swift/data/obs/YYYY_MM/OBSID/
+            year_month = get_year_month(obs_time)
+            if year_month:
+                return f"{base_url}/swift/data/obs/{year_month}/{obsid}/"
+            return None
+
+        elif mission == "XMM-Newton":
+            # XMM: /xmm/data/rev0/OBSID/
+            return f"{base_url}/xmm/data/rev0/{obsid}/"
+
+        elif mission == "RXTE":
+            # RXTE: /xte/data/archive/AO{cycle}/P{prnb}/{obsid}/
+            # prnb is the proposal number from the observation data
+            # The AO cycle can be estimated from the observation date or prnb
+            prnb = obs_data.get("prnb")
+            if prnb:
+                # Estimate AO cycle from prnb
+                # RXTE had AO cycles 1-16 (1996-2012)
+                # prnb format is typically 5 digits, early proposals are lower numbers
+                try:
+                    prnb_num = int(prnb)
+                    # Rough mapping based on proposal number ranges
+                    # This is an approximation - locate_data is more reliable
+                    if prnb_num < 10000:
+                        ao_cycle = "AO1"
+                    elif prnb_num < 20000:
+                        ao_cycle = "AO2"
+                    elif prnb_num < 30000:
+                        ao_cycle = "AO3"
+                    elif prnb_num < 40000:
+                        ao_cycle = "AO4"
+                    elif prnb_num < 50000:
+                        ao_cycle = "AO5"
+                    elif prnb_num < 60000:
+                        ao_cycle = "AO6"
+                    elif prnb_num < 70000:
+                        ao_cycle = "AO7"
+                    elif prnb_num < 80000:
+                        ao_cycle = "AO8"
+                    elif prnb_num < 90000:
+                        ao_cycle = "AO9"
+                    elif prnb_num < 93000:
+                        ao_cycle = "AO10"
+                    elif prnb_num < 94000:
+                        ao_cycle = "AO11"
+                    elif prnb_num < 95000:
+                        ao_cycle = "AO12"
+                    elif prnb_num < 96000:
+                        ao_cycle = "AO13"
+                    elif prnb_num < 97000:
+                        ao_cycle = "AO14"
+                    elif prnb_num < 98000:
+                        ao_cycle = "AO15"
+                    else:
+                        ao_cycle = "AO16"
+                    # Note: RXTE archive uses /xte/ not /rxte/ in the path
+                    return f"{base_url}/xte/data/archive/{ao_cycle}/P{prnb}/{obsid}/"
+                except (ValueError, TypeError):
+                    pass
+            # Cannot construct URL without prnb - fallback to locate_data
+            return None
+
+        elif mission == "IXPE":
+            # IXPE: /ixpe/data/obs/NN/OBSID/
+            # NN = first 2 digits of obsid (e.g., 02001099 -> /obs/02/02001099/)
+            if len(obsid) >= 2:
+                return f"{base_url}/ixpe/data/obs/{obsid[:2]}/{obsid}/"
+            return None
+
+        elif mission == "Suzaku":
+            # Suzaku: /suzaku/data/obs/N/OBSID/
+            # N = first digit of obsid
+            if obsid and obsid[0].isdigit():
+                return f"{base_url}/suzaku/data/obs/{obsid[0]}/{obsid}/"
+            return None
+
+        elif mission == "ASCA":
+            # ASCA: /asca/data/rev2/OBSID/
+            # Flat structure, direct obsid directory
+            return f"{base_url}/asca/data/rev2/{obsid}/"
+
+        elif mission == "XRISM":
+            # XRISM: /xrism/data/obs/N/OBSID/
+            # N = first digit of obsid
+            if obsid and obsid[0].isdigit():
+                return f"{base_url}/xrism/data/obs/{obsid[0]}/{obsid}/"
+            return None
+
+        elif mission == "Hitomi":
+            # Hitomi: /hitomi/data/obs/N/OBSID/
+            # N = first digit of obsid
+            if obsid and obsid[0].isdigit():
+                return f"{base_url}/hitomi/data/obs/{obsid[0]}/{obsid}/"
+            return None
+
+        return None
+
+    async def download_file_to_disk(
+        self,
+        url: str,
+        save_path: str,
+    ) -> AsyncGenerator[Dict[str, Any], None]:
+        """
+        Download a file from URL and save to local disk with progress streaming.
+
+        This method bypasses CORS restrictions by downloading on the backend.
+        Progress is streamed via SSE events.
+
+        Args:
+            url: URL to download from (e.g., HEASARC HTTPS URL)
+            save_path: Local path to save the file
+
+        Yields:
+            Dict with event type and data:
+            - {"type": "progress", "percent": 45, "bytes_downloaded": ..., "total_bytes": ...}
+            - {"type": "complete", "file_path": "...", "size_bytes": ...}
+            - {"type": "error", "error": "..."}
+        """
+        try:
+            # Ensure the parent directory exists
+            save_dir = os.path.dirname(save_path)
+            if save_dir and not os.path.exists(save_dir):
+                os.makedirs(save_dir, exist_ok=True)
+
+            async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
+                async with client.stream("GET", url) as response:
+                    response.raise_for_status()
+
+                    # Get total size from headers if available
+                    total_bytes = int(response.headers.get("content-length", 0))
+                    bytes_downloaded = 0
+
+                    # Open file for writing
+                    with open(save_path, "wb") as f:
+                        async for chunk in response.aiter_bytes(chunk_size=65536):
+                            f.write(chunk)
+                            bytes_downloaded += len(chunk)
+
+                            # Calculate progress
+                            if total_bytes > 0:
+                                percent = (bytes_downloaded / total_bytes) * 100
+                            else:
+                                percent = 0
+
+                            yield {
+                                "type": "progress",
+                                "bytes_downloaded": bytes_downloaded,
+                                "total_bytes": total_bytes,
+                                "percent": round(percent, 1),
+                            }
+
+                            # Small yield to allow other async operations
+                            await asyncio.sleep(0)
+
+            # Get actual file size after writing
+            actual_size = os.path.getsize(save_path)
+
+            yield {
+                "type": "complete",
+                "file_path": save_path,
+                "size_bytes": actual_size,
+            }
+
+        except httpx.HTTPStatusError as e:
+            yield {
+                "type": "error",
+                "error": f"HTTP {e.response.status_code}: {e.response.reason_phrase}",
+            }
+        except httpx.RequestError as e:
+            yield {
+                "type": "error",
+                "error": f"Request failed: {str(e)}",
+            }
+        except OSError as e:
+            yield {
+                "type": "error",
+                "error": f"File system error: {str(e)}",
+            }
+        except Exception as e:
+            yield {
+                "type": "error",
+                "error": f"Download failed: {str(e)}",
+            }
+
+    async def _locate_observation_directory(
+        self,
+        mission: str,
+        obsid: str,
+        obs_data: Optional[Dict[str, Any]] = None,
+    ) -> Optional[str]:
+        """
+        Locate the observation directory URL using Heasarc.locate_data().
+
+        This uses astroquery's Heasarc class to find the actual data location,
+        which handles the complex directory structures of different missions.
+
+        The correct API usage is:
+        1. Query to get observation row(s) from the catalog
+        2. Call locate_data(table) with the query result table
+
+        Args:
+            mission: Mission key
+            obsid: Observation ID
+            obs_data: Additional observation data (e.g., ra/dec for coordinate queries)
+
+        Returns:
+            Directory URL or None if not found
+        """
+        try:
+            from astroquery.heasarc import Heasarc
+
+            catalog_name = SUPPORTED_CATALOGS[mission]["catalog"]
+            obs_data = obs_data or {}
+
+            # Step 1: Query to get observation row
+            # We need the actual table row for locate_data
+            # Best approach: query by coordinates if available, then filter by obsid
+            table = None
+
+            # Try coordinate-based query if we have ra/dec
+            ra = obs_data.get("ra")
+            dec = obs_data.get("dec")
+            if ra is not None and dec is not None:
+                try:
+                    coords = SkyCoord(ra=float(ra) * u.deg, dec=float(dec) * u.deg)
+                    table = Heasarc.query_region(
+                        coords,
+                        catalog=catalog_name,
+                        radius=0.5 * u.deg,
+                    )
+                    print(f"Coordinate query for {mission} returned {len(table) if table else 0} results")
+                except Exception as coord_err:
+                    print(f"Coordinate query failed: {coord_err}")
+
+            # Fallback: use ADQL via query_tap to find the observation by obsid
+            if table is None or len(table) == 0:
+                try:
+                    safe_obsid = obsid.strip().replace("'", "''")
+                    adql = f"SELECT * FROM {catalog_name} WHERE obsid = '{safe_obsid}'"
+                    tap_result = Heasarc.query_tap(adql, maxrec=1)
+                    tap_table = tap_result.to_table()
+                    if tap_table is not None and len(tap_table) > 0:
+                        # TAP results lack __row column, so locate_data won't work.
+                        # Instead, extract time and construct URL directly.
+                        time_cols = ["time", "start_time", "date_obs", "tstart"]
+                        obs_time_val = None
+                        for tc in time_cols:
+                            if tc in tap_table.colnames:
+                                obs_time_val = str(tap_table[0][tc])
+                                break
+                        if obs_time_val:
+                            url = self._get_observation_directory_url(
+                                mission, obsid, obs_time_val, obs_data
+                            )
+                            if url:
+                                return url
+                except Exception as adql_err:
+                    print(f"ADQL obsid query failed: {adql_err}")
+                    return None
+
+            if table is None or len(table) == 0:
+                print(f"No observations found in {catalog_name}")
+                return None
+
+            # Step 2: Filter to the specific obsid
+            obsid_cols = ["obsid", "obs_id", "observation_id", "OBSID", "OBS_ID"]
+            obsid_column = None
+            for col in obsid_cols:
+                if col in table.colnames:
+                    obsid_column = col
+                    break
+
+            if obsid_column is None:
+                print(f"Could not find obsid column in {catalog_name}")
+                return None
+
+            # Filter to the specific obsid
+            mask = [str(row[obsid_column]).strip() == str(obsid).strip() for row in table]
+            if not any(mask):
+                print(f"Obsid {obsid} not found in query results")
+                return None
+
+            filtered_table = table[mask][:1]
+
+            # Step 3: Call locate_data with the filtered table row
+            try:
+                result = Heasarc.locate_data(filtered_table)
+            except Exception as locate_err:
+                print(f"locate_data API call failed: {locate_err}")
+                return None
+
+            if result is None or len(result) == 0:
+                return None
+
+            # Step 4: Extract the access_url from the result
+            # locate_data returns a table with columns: ID, access_url, sciserver, aws, etc.
+            if "access_url" in result.colnames:
+                for row in result:
+                    url = str(row["access_url"]).strip()
+                    if url and "heasarc.gsfc.nasa.gov" in url:
+                        # Clean up double slashes in path (common in HEASARC URLs)
+                        url = re.sub(r"([^:])//+", r"\1/", url)
+                        if not url.endswith("/"):
+                            url += "/"
+                        return url
+
+            # Fallback: look in any column for HEASARC URLs
+            for row in result:
+                for col in result.colnames:
+                    val = str(row[col])
+                    if "heasarc.gsfc.nasa.gov" in val and "/FTP/" in val:
+                        url = val.strip()
+                        url = re.sub(r"([^:])//+", r"\1/", url)
+                        if not url.endswith("/"):
+                            url += "/"
+                        return url
+
+            return None
+
+        except Exception as e:
+            # Log but don't fail - we'll try alternative methods
+            print(f"locate_data failed for {mission}/{obsid}: {e}")
+            return None
diff --git a/python-backend/services/base_service.py b/python-backend/services/base_service.py
new file mode 100644
index 0000000..b8bca70
--- /dev/null
+++ b/python-backend/services/base_service.py
@@ -0,0 +1,98 @@
+"""
+Base service class for Stingray Explorer backend.
+
+Provides common functionality for all services.
+"""
+
+from typing import Any, Dict, Optional
+
+from .state_manager import StateManager
+from utils.error_handler import ErrorHandler
+from utils.performance_monitor import PerformanceMonitor
+
+
+class BaseService:
+    """
+    Base class for all services in the Stingray Explorer backend.
+
+    Provides:
+    - Access to StateManager for data persistence
+    - Access to ErrorHandler for consistent error handling
+    - Standard result format for all service methods
+    """
+
+    def __init__(
+        self,
+        state_manager: StateManager,
+        performance_monitor: Optional[PerformanceMonitor] = None,
+    ):
+        """
+        Initialize the base service.
+
+        Args:
+            state_manager: The application state manager instance
+            performance_monitor: Optional performance monitor instance
+        """
+        self.state = state_manager
+        self.error_handler = ErrorHandler
+        self.performance_monitor = performance_monitor
+
+    def create_result(
+        self,
+        success: bool,
+        data: Any = None,
+        message: str = "",
+        error: Optional[str] = None,
+        **kwargs: Any,
+    ) -> Dict[str, Any]:
+        """
+        Create a standardized result dictionary.
+
+        All service methods should return results in this format.
+
+        Args:
+            success: Whether the operation succeeded
+            data: The result data
+            message: User-friendly message
+            error: Technical error message
+            **kwargs: Additional fields to include
+
+        Returns:
+            Standardized result dictionary
+        """
+        result: Dict[str, Any] = {
+            "success": success,
+            "data": data,
+            "message": message,
+            "error": error,
+        }
+        result.update(kwargs)
+        return result
+
+    def handle_error(
+        self,
+        exception: Exception,
+        context: str,
+        **context_data: Any,
+    ) -> Dict[str, Any]:
+        """
+        Handle an exception and return a standardized error result.
+
+        Args:
+            exception: The exception that occurred
+            context: Description of the operation that failed
+            **context_data: Additional context data
+
+        Returns:
+            Error result dictionary
+        """
+        user_msg, tech_msg = self.error_handler.handle_error(
+            exception, context=context, **context_data
+        )
+
+        return self.create_result(
+            success=False,
+            data=None,
+            message=user_msg,
+            error=tech_msg,
+        )
diff --git a/python-backend/services/data_service.py b/python-backend/services/data_service.py
new file mode 100644
index 0000000..d78d771
--- /dev/null
+++ b/python-backend/services/data_service.py
@@ -0,0 +1,2634 @@
+"""
+Data service for EventList operations.
+
+Handles loading, saving, and managing event lists.
+Includes lazy loading support for large files.
+"""
+
+import asyncio
+import os
+import tempfile
+import time
+import warnings
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Any, AsyncGenerator, Dict, List, Optional
+
+import numpy as np
+import psutil
+import requests
+from astropy.io import fits
+from stingray import EventList
+from stingray.io import FITSTimeseriesReader
+
+from .base_service import BaseService
+
+
+def _to_python_float(val):
+    """Convert numpy numeric types to Python float for JSON serialization."""
+    if val is None:
+        return None
+    try:
+        # Handle numpy scalars (including longdouble/float128)
+        return float(val)
+    except (TypeError, ValueError):
+        return None
+
+
+def _to_python_float_list(arr):
+    """Convert numpy array to list of Python floats for JSON serialization."""
+    if arr is None:
+        return None
+    try:
+        # Convert each element explicitly to handle longdouble
+        return [float(x) for x in arr]
+    except (TypeError, ValueError):
+        return arr.tolist() if hasattr(arr, 'tolist') else list(arr)
+
+
+class DataService(BaseService):
+    """
+    Service for EventList data operations.
+
+    Handles loading, saving, and managing event lists without any UI dependencies.
+    """
+
+    def _fix_inverted_gti(self, event_list: EventList) -> bool:
+        """
+        Fix inverted GTI intervals in an EventList (in-place).
+
+        When Stingray loads unsorted data without a GTI extension, it sets
+        GTI to [time[0], time[-1]] which can have start > stop for unsorted times.
+        This causes Stingray's internal check_gtis to fail on later operations.
+
+        This method sorts each GTI interval to ensure start <= stop.
+
+        Args:
+            event_list: The EventList to fix (modified in-place)
+
+        Returns:
+            True if any GTI was fixed, False otherwise
+        """
+        if event_list.gti is None or len(event_list.gti) == 0:
+            return False
+
+        fixed_any = False
+        fixed_gti = []
+        for g in event_list.gti:
+            start, stop = g[0], g[1]
+            if stop < start:
+                # Swap to fix inverted interval
+                fixed_gti.append([stop, start])
+                fixed_any = True
+            else:
+                fixed_gti.append([start, stop])
+
+        if fixed_any:
+            event_list.gti = np.array(fixed_gti)
+
+        return fixed_any
+
+    def _validate_gti(self, event_list: EventList) -> List[str]:
+        """
+        Validate GTI (Good Time Intervals) for an EventList.
+
+        Checks for:
+        - Empty or missing GTI
+        - Invalid GTI intervals (stop <= start)
+        - Events falling outside GTI boundaries
+
+        Args:
+            event_list: The EventList to validate
+
+        Returns:
+            List of warning messages (empty if no issues found)
+        """
+        warnings: List[str] = []
+
+        # Check 1: Empty or missing GTI
+        if event_list.gti is None or len(event_list.gti) == 0:
+            warnings.append("GTI is empty or missing - no valid observation intervals defined")
+            return warnings  # Can't do further checks without GTI
+
+        # Check 2: Invalid GTI intervals (stop <= start)
+        invalid_intervals = []
+        for i, (start, stop) in enumerate(event_list.gti):
+            if stop <= start:
+                invalid_intervals.append(i)
+        if invalid_intervals:
+            warnings.append(
+                f"Found {len(invalid_intervals)} invalid GTI interval(s) where stop <= start "
+                f"(indices: {invalid_intervals[:5]}{'...' if len(invalid_intervals) > 5 else ''})"
+            )
+
+        # Check 3: Events outside GTI boundaries
+        if event_list.time is not None and len(event_list.time) > 0:
+            times = event_list.time
+            gti = event_list.gti
+
+            # Check if each event falls within at least one GTI
+            inside_gti = np.zeros(len(times), dtype=bool)
+            for start, stop in gti:
+                inside_gti |= (times >= start) & (times <= stop)
+
+            events_outside = np.sum(~inside_gti)
+            if events_outside > 0:
+                total_events = len(times)
+                percent_outside = (events_outside / total_events) * 100
+                warnings.append(
+                    f"{events_outside} events ({percent_outside:.2f}%) fall outside GTI boundaries"
+                )
+
+        return warnings
+
+    def _validate_data_quality(self, event_list: EventList) -> List[Dict[str, Any]]:
+        """
+        Validate data quality beyond GTI checks.
+
+        Performs comprehensive validation including:
+        - NaN values in time/energy arrays
+        - Time ordering (monotonically increasing)
+        - Negative energy/PI values
+
+        Args:
+            event_list: The EventList to validate
+
+        Returns:
+            List of all validation check results with type, status, severity, message, and details
+        """
+        validations: List[Dict[str, Any]] = []
+
+        # Check 1: NaN in time array
+        if event_list.time is not None and len(event_list.time) > 0:
+            nan_count = int(np.sum(np.isnan(event_list.time)))
+            validations.append({
+                "type": "nan_time",
+                "name": "Time Array NaN Check",
+                "description": "Check for NaN (Not a Number) values in time array",
+                "status": "fail" if nan_count > 0 else "pass",
+                "severity": "error" if nan_count > 0 else "pass",
+                "message": f"Found {nan_count} NaN values" if nan_count > 0 else "No NaN values found",
+                "count": nan_count,
+                "total": len(event_list.time)
+            })
+
+            # Check 2: Time ordering (monotonically increasing)
+            time_diffs = np.diff(event_list.time)
+            disorder_count = int(np.sum(time_diffs < 0))
+            validations.append({
+                "type": "time_ordering",
+                "name": "Time Ordering Check",
+                "description": "Check if time values are monotonically increasing",
+                "status": "fail" if disorder_count > 0 else "pass",
+                "severity": "warning" if disorder_count > 0 else "pass",
+                "message": f"Found {disorder_count} time inversions (not monotonically increasing)" if disorder_count > 0 else "Time values are monotonically increasing",
+                "count": disorder_count,
+                "total": len(event_list.time) - 1
+            })
+        else:
+            validations.append({
+                "type": "nan_time",
+                "name": "Time Array NaN Check",
+                "description": "Check for NaN values in time array",
+                "status": "skip",
+                "severity": "skip",
+                "message": "No time data available",
+                "count": 0,
+                "total": 0
+            })
+            validations.append({
+                "type": "time_ordering",
+                "name": "Time Ordering Check",
+                "description": "Check if time values are monotonically increasing",
+                "status": "skip",
+                "severity": "skip",
+                "message": "No time data available",
+                "count": 0,
+                "total": 0
+            })
+
+        # Check 3: NaN in energy array
+        if event_list.energy is not None and len(event_list.energy) > 0:
+            nan_energy = int(np.sum(np.isnan(event_list.energy)))
+            validations.append({
+                "type": "nan_energy",
+                "name": "Energy Array NaN Check",
+                "description": "Check for NaN values in energy array",
+                "status": "fail" if nan_energy > 0 else "pass",
+                "severity": "error" if nan_energy > 0 else "pass",
+                "message": f"Found {nan_energy} NaN values" if nan_energy > 0 else "No NaN values found",
+                "count": nan_energy,
+                "total": len(event_list.energy)
+            })
+
+            # Check 4: Negative energy values
+            neg_energy = int(np.sum(event_list.energy < 0))
+            validations.append({
+                "type": "negative_energy",
+                "name": "Negative Energy Check",
+                "description": "Check for negative energy values (physically invalid)",
+                "status": "fail" if neg_energy > 0 else "pass",
+                "severity": "error" if neg_energy > 0 else "pass",
+                "message": f"Found {neg_energy} negative energy values" if neg_energy > 0 else "All energy values are non-negative",
+                "count": neg_energy,
+                "total": len(event_list.energy)
+            })
+        else:
+            validations.append({
+                "type": "nan_energy",
+                "name": "Energy Array NaN Check",
+                "description": "Check for NaN values in energy array",
+                "status": "skip",
+                "severity": "skip",
+                "message": "No energy data available",
+                "count": 0,
+                "total": 0
+            })
+            validations.append({
+                "type": "negative_energy",
+                "name": "Negative Energy Check",
+                "description": "Check for negative energy values",
+                "status": "skip",
+                "severity": "skip",
+                "message": "No energy data available",
+                "count": 0,
+                "total": 0
+            })
+
+        # Check 5: Negative PI values
+        if event_list.pi is not None and len(event_list.pi) > 0:
+            neg_pi = int(np.sum(event_list.pi < 0))
+            validations.append({
+                "type": "negative_pi",
+                "name": "Negative PI Check",
+                "description": "Check for negative PI (Pulse Invariant) channel values",
+                "status": "fail" if neg_pi > 0 else "pass",
+                "severity": "error" if neg_pi > 0 else "pass",
+                "message": f"Found {neg_pi} negative PI values" if neg_pi > 0 else "All PI values are non-negative",
+                "count": neg_pi,
+                "total": len(event_list.pi)
+            })
+        else:
+            validations.append({
+                "type": "negative_pi",
+                "name": "Negative PI Check",
+                "description": "Check for negative PI channel values",
+                "status": "skip",
+                "severity": "skip",
+                "message": "No PI data available",
+                "count": 0,
+                "total": 0
+            })
+
+        # Check 6: GTI validity (start < stop for all intervals)
+        if event_list.gti is not None and len(event_list.gti) > 0:
+            invalid_gti = sum(1 for g in event_list.gti if g[1] <= g[0])
+            validations.append({
+                "type": "gti_validity",
+                "name": "GTI Interval Check",
+                "description": "Check that all GTI intervals have valid start < stop times",
+                "status": "fail" if invalid_gti > 0 else "pass",
+                "severity": "error" if invalid_gti > 0 else "pass",
+                "message": f"Found {invalid_gti} invalid GTI intervals (stop <= start)" if invalid_gti > 0 else "All GTI intervals are valid",
+                "count": invalid_gti,
+                "total": len(event_list.gti)
+            })
+        else:
+            validations.append({
+                "type": "gti_validity",
+                "name": "GTI Interval Check",
+                "description": "Check that all GTI intervals have valid start < stop times",
+                "status": "skip",
+                "severity": "skip",
+                "message": "No GTI data available",
+                "count": 0,
+                "total": 0
+            })
+
+        return validations
+
+    def _parse_fits_header_string(self, header_str: str) -> Dict[str, str]:
+        """
+        Parse a FITS header string into a dictionary.
+
+        FITS headers have 80-character lines with format:
+        KEYWORD = value / comment
+        or
+        KEYWORD = 'string value' / comment
+
+        Args:
+            header_str: Raw FITS header string
+
+        Returns:
+            Dictionary of keyword -> value mappings
+        """
+        parsed: Dict[str, str] = {}
+
+        # Split into 80-character cards (FITS standard)
+        # Some headers may be newline-separated instead
+        if '\n' in header_str:
+            lines = header_str.split('\n')
+        else:
+            # Split into 80-char chunks
+            lines = [header_str[i:i+80] for i in range(0, len(header_str), 80)]
+
+        for line in lines:
+            if not line or len(line) < 8:
+                continue
+
+            # Skip COMMENT, HISTORY, and END cards
+            keyword = line[:8].strip()
+            if not keyword or keyword in ('COMMENT', 'HISTORY', 'END', ''):
+                continue
+
+            # Check for value indicator '='
+            if len(line) > 9 and line[8] == '=':
+                value_part = line[9:].strip()
+
+                # Handle quoted string values
+                if value_part.startswith("'"):
+                    # Find closing quote (may contain escaped quotes '')
+                    end_quote = 1
+                    while end_quote < len(value_part):
+                        if value_part[end_quote] == "'":
+                            if end_quote + 1 < len(value_part) and value_part[end_quote + 1] == "'":
+                                end_quote += 2  # Skip escaped quote
+                            else:
+                                break
+                        else:
+                            end_quote += 1
+                    value = value_part[1:end_quote].replace("''", "'").strip()
+                else:
+                    # Numeric or boolean value - take until comment marker
+                    if '/' in value_part:
+                        value = value_part.split('/')[0].strip()
+                    else:
+                        value = value_part.strip()
+
+                    # Handle boolean
+                    if value == 'T':
+                        value = 'True'
+                    elif value == 'F':
+                        value = 'False'
+
+                if value:
+                    parsed[keyword] = value
+
+        return parsed
+
+    def _extract_fits_header(self, event_list: EventList) -> Dict[str, Any]:
+        """
+        Extract key FITS header information from an EventList.
+
+        Extracts commonly used header keywords including:
+        - Object name, observation ID
+        - RA/Dec coordinates
+        - Exposure, ontime, livetime
+        - Observation dates
+        - Creator software
+
+        Args:
+            event_list: The EventList to extract headers from
+
+        Returns:
+            Dictionary containing extracted header information
+        """
+        header_info: Dict[str, Any] = {}
+        raw_header = getattr(event_list, "header", None)
+
+        if raw_header is None:
+            return header_info
+
+        # Parse string headers into a dictionary first
+        header_dict: Dict[str, str] = {}
+        if isinstance(raw_header, str):
+            header_dict = self._parse_fits_header_string(raw_header)
+        elif isinstance(raw_header, dict):
+            header_dict = {str(k): str(v) for k, v in raw_header.items()}
+        elif hasattr(raw_header, 'get'):
+            # Handle astropy Header-like objects - convert to dict
+            try:
+                for key in raw_header.keys():
+                    if key and key.strip() and key not in ('COMMENT', 'HISTORY', ''):
+                        val = raw_header.get(key)
+                        if val is not None:
+                            header_dict[str(key)] = str(val)
+            except Exception:
+                pass
+
+        if not header_dict:
+            return header_info
+
+        # Define key headers to extract with output key and type conversion
+        key_headers = [
+            ("OBJECT", "object", str),
+            ("OBS_ID", "obs_id", str),
+            ("RA_NOM", "ra_nom", float),
+            ("DEC_NOM", "dec_nom", float),
+            ("RA_OBJ", "ra_obj", float),
+            ("DEC_OBJ", "dec_obj", float),
+            ("EXPOSURE", "exposure", float),
+            ("ONTIME", "ontime", float),
+            ("LIVETIME", "livetime", float),
+            ("DATE-OBS", "date_obs", str),
+            ("DATE-END", "date_end", str),
+            ("TSTART", "tstart", float),
+            ("TSTOP", "tstop", float),
+            ("CREATOR", "creator", str),
+            ("TELESCOP", "telescop", str),
+            ("INSTRUME", "instrume", str),
+            ("DATAMODE", "datamode", str),
+            ("OBSERVER", "observer", str),
+        ]
+
+        for fits_key, output_key, type_func in key_headers:
+            if fits_key in header_dict:
+                value = header_dict[fits_key]
+                try:
+                    if type_func == float:
+                        header_info[output_key] = _to_python_float(float(value))
+                    else:
+                        header_info[output_key] = type_func(value)
+                except (ValueError, TypeError):
+                    header_info[output_key] = str(value)
+
+        # Include full raw header
+        raw_header_dict = {}
+        for k, v in header_dict.items():
+            try:
+                raw_header_dict[str(k)] = str(v)
+            except Exception:
+                pass
+        if raw_header_dict:
+            header_info["raw_header"] = raw_header_dict
+
+        return header_info
+
+    def _detect_fits_file_type(self, file_path: str) -> Dict[str, Any]:
+        """
+        Detect the type of a FITS file by reading its headers.
+
+        This pre-check prevents cryptic Stingray errors when users try to load
+        Light Curve files as Event Lists (or other type mismatches).
+
+        Returns:
+            Dictionary with:
+            - file_type: 'event_list', 'light_curve', 'spectrum', 'unknown'
+            - details: Dict with HDUCLAS1, EXTNAME, columns found
+            - is_event_list: bool
+            - error_message: str or None (user-friendly message if not event list)
+        """
+        result: Dict[str, Any] = {
+            "file_type": "unknown",
+            "details": {},
+            "is_event_list": False,
+            "error_message": None
+        }
+
+        try:
+            with fits.open(file_path) as hdulist:
+                for hdu in hdulist:
+                    if hdu.name in ['PRIMARY', '']:
+                        continue
+
+                    header = hdu.header
+                    extname = header.get('EXTNAME', '').upper()
+                    hduclas1 = header.get('HDUCLAS1', '').upper()
+
+                    result["details"] = {
+                        "extname": extname,
+                        "hduclas1": hduclas1,
+                        "extension_name": hdu.name
+                    }
+
+                    # Check for Light Curve
+                    if 'LIGHT' in hduclas1 or extname == 'RATE':
+                        result["file_type"] = "light_curve"
+                        result["is_event_list"] = False
+                        result["error_message"] = (
+                            f"This is a Light Curve file (HDUCLAS1='{hduclas1}', "
+                            f"EXTNAME='{extname}'). "
+                            f"Use the Light Curve analysis tools instead of Event List loading."
+                        )
+                        return result
+
+                    # Check for Event List
+                    if extname == 'EVENTS' or hduclas1 == 'EVENTS':
+                        result["file_type"] = "event_list"
+                        result["is_event_list"] = True
+                        return result
+
+                    # Check for Spectrum
+                    if 'SPECTRUM' in hduclas1 or extname == 'SPECTRUM':
+                        result["file_type"] = "spectrum"
+                        result["is_event_list"] = False
+                        result["error_message"] = (
+                            f"This is a Spectrum file (HDUCLAS1='{hduclas1}'). "
+                            f"Use spectral analysis tools instead of Event List loading."
+                        )
+                        return result
+
+            # If we get here, couldn't determine type - allow attempt
+            result["file_type"] = "unknown"
+            result["is_event_list"] = True  # Allow unknown files to attempt loading
+
+        except Exception as e:
+            # If we can't read headers, let the normal loading handle errors
+            result["file_type"] = "unknown"
+            result["is_event_list"] = True
+            result["details"]["error"] = str(e)
+
+        return result
+
+    def load_event_list(
+        self,
+        file_path: str,
+        name: str,
+        fmt: str = "ogip",
+        rmf_file: Optional[str] = None,
+        additional_columns: Optional[List[str]] = None,
+        high_precision: bool = False,
+        skip_checks: bool = False,
+        notes: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Load an EventList from a file.
+
+        Args:
+            file_path: Path to the event file
+            name: Name to assign to the loaded event list
+            fmt: File format (ogip, hdf5, hea, etc.)
+            rmf_file: Optional path to RMF file
+            additional_columns: Optional list of additional columns to read
+            high_precision: Use numpy.float128 for time array (pulsar timing)
+            skip_checks: Skip time ordering and GTI validation (performance)
+            notes: Optional user notes/comments about this data
+
+        Returns:
+            Result dictionary with the EventList data
+        """
+        try:
+            # Validate the name doesn't already exist
+            if self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"An event list with the name '{name}' already exists.",
+                    error=None,
+                )
+
+            # Auto-detect format from file extension if not explicitly specified differently
+            file_ext = os.path.splitext(file_path)[1].lower()
+            if file_ext in ['.hdf5', '.h5']:
+                fmt = 'hdf5'
+            elif file_ext in ['.ecsv']:
+                fmt = 'ascii.ecsv'
+            # Otherwise use the provided fmt (default: ogip for FITS files)
+
+            # Detect file type before attempting to load (for FITS files)
+            is_fits = fmt.lower() in ['ogip', 'hea', 'fits', 'evt']
+            if is_fits:
+                file_type_info = self._detect_fits_file_type(file_path)
+                if not file_type_info["is_event_list"]:
+                    return self.create_result(
+                        success=False,
+                        data=None,
+                        message=f"Cannot load '{name}' as Event List: "
+                                f"{file_type_info['error_message']}",
+                        error=f"File type: {file_type_info['file_type']}",
+                    )
+
+            # Capture Stingray/library warnings during loading
+            stingray_warnings: List[str] = []
+            with warnings.catch_warnings(record=True) as caught_warnings:
+                warnings.simplefilter("always")  # Catch all warnings
+
+                # Load the event list using Stingray
+                event_list = EventList.read(
+                    file_path,
+                    fmt=fmt,
+                    rmf_file=rmf_file,
+                    additional_columns=additional_columns,
+                    high_precision=high_precision,
+                    skip_checks=skip_checks,
+                )
+
+                # Collect warning messages
+                for w in caught_warnings:
+                    stingray_warnings.append(str(w.message))
+
+            # Fix inverted GTI intervals (common with unsorted data)
+            # This must be done before storing, as Stingray's check_gtis
+            # will fail on later operations if GTI has start > stop
+            gti_was_fixed = self._fix_inverted_gti(event_list)
+            if gti_was_fixed:
+                stingray_warnings.append(
+                    "GTI intervals were inverted (start > stop) and have been automatically fixed. "
+                    "This typically occurs with unsorted event data."
+                )
+
+            # Store user notes on the event list object
+            if notes:
+                event_list.notes = notes
+
+            # Add to state manager
+            self.state.add_event_data(name, event_list)
+
+            # Validate GTI and collect warnings
+            gti_warnings = self._validate_gti(event_list)
+
+            # Run comprehensive data quality validation
+            validation_issues = self._validate_data_quality(event_list)
+
+            # Prepare serializable summary (use helper for numpy type conversion)
+            summary = {
+                "name": name,
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "has_energy": event_list.energy is not None,
+                "has_pi": event_list.pi is not None,
+                "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                "gti_warnings": gti_warnings if gti_warnings else None,
+                "stingray_warnings": stingray_warnings if stingray_warnings else None,
+                "validation_issues": validation_issues if validation_issues else None,
+                "notes": notes if notes else None,
+            }
+
+            # Build message with warnings if present
+            message = f"EventList '{name}' loaded successfully ({len(event_list.time)} events)"
+            if gti_warnings:
+                message += f" [GTI warnings: {len(gti_warnings)}]"
+            if stingray_warnings:
+                message += f" [Stingray warnings: {len(stingray_warnings)}]"
+            if validation_issues:
+                error_count = sum(1 for v in validation_issues if v["severity"] == "error")
+                warn_count = sum(1 for v in validation_issues if v["severity"] == "warning")
+                if error_count > 0:
+                    message += f" [Data errors: {error_count}]"
+                if warn_count > 0:
+                    message += f" [Data warnings: {warn_count}]"
+
+            return self.create_result(
+                success=True,
+                data=summary,
+                message=message,
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Loading event list", file_path=file_path, name=name, fmt=fmt
+            )
+
+    def load_event_list_from_url(
+        self,
+        url: str,
+        name: str,
+        fmt: str = "ogip",
+        rmf_file: Optional[str] = None,
+        additional_columns: Optional[List[str]] = None,
+        high_precision: bool = False,
+        skip_checks: bool = False,
+    ) -> Dict[str, Any]:
+        """
+        Load an EventList from a URL.
+
+        Args:
+            url: URL to download the event file from
+            name: Name to assign to the loaded event list
+            fmt: File format
+            rmf_file: Optional path to local RMF file for energy calibration
+            additional_columns: Optional list of additional columns to read
+            high_precision: Use numpy.float128 for time array (pulsar timing)
+            skip_checks: Skip time ordering and GTI validation (performance)
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            # Validate the name doesn't already exist
+            if self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"An event list with the name '{name}' already exists.",
+                    error=None,
+                )
+
+            # Download file to temporary location
+            response = requests.get(url, stream=True, timeout=30)
+            response.raise_for_status()
+
+            # Create temporary file
+            with tempfile.NamedTemporaryFile(delete=False, suffix=f".{fmt}") as tmp_file:
+                for chunk in response.iter_content(chunk_size=1024):
+                    if chunk:
+                        tmp_file.write(chunk)
+                temp_filename = tmp_file.name
+
+            # Detect file type before attempting to load (for FITS files)
+            is_fits = fmt.lower() in ['ogip', 'hea', 'fits', 'evt']
+            if is_fits:
+                file_type_info = self._detect_fits_file_type(temp_filename)
+                if not file_type_info["is_event_list"]:
+                    # Clean up temp file
+                    if os.path.exists(temp_filename):
+                        os.remove(temp_filename)
+                    return self.create_result(
+                        success=False,
+                        data=None,
+                        message=f"Cannot load '{name}' as Event List: "
+                                f"{file_type_info['error_message']}",
+                        error=f"File type: {file_type_info['file_type']}",
+                    )
+
+            # Load the event list with guaranteed temp file cleanup
+            try:
+                event_list = EventList.read(
+                    temp_filename,
+                    fmt=fmt,
+                    rmf_file=rmf_file,
+                    additional_columns=additional_columns,
+                    high_precision=high_precision,
+                    skip_checks=skip_checks,
+                )
+            finally:
+                # Clean up temporary file even if loading fails
+                if os.path.exists(temp_filename):
+                    os.remove(temp_filename)
+
+            # Fix inverted GTI intervals (common with unsorted data)
+            gti_was_fixed = self._fix_inverted_gti(event_list)
+
+            # Add to state manager
+            self.state.add_event_data(name, event_list)
+
+            # Validate GTI and collect warnings
+            gti_warnings = self._validate_gti(event_list)
+            if gti_was_fixed:
+                gti_warnings.insert(0, "GTI intervals were inverted and automatically fixed.")
+
+            summary = {
+                "name": name,
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "has_energy": event_list.energy is not None,
+                "has_pi": event_list.pi is not None,
+                "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                "gti_warnings": gti_warnings if gti_warnings else None,
+            }
+
+            # Build message with warnings if present
+            message = f"EventList '{name}' loaded successfully from URL"
+            if gti_warnings:
+                message += f" [GTI warnings: {len(gti_warnings)}]"
+
+            return self.create_result(
+                success=True,
+                data=summary,
+                message=message,
+            )
+
+        except requests.RequestException as e:
+            return self.create_result(
+                success=False,
+                data=None,
+                message=f"Failed to download file from URL: {str(e)}",
+                error=str(e),
+            )
+        except Exception as e:
+            return self.handle_error(
+                e, "Loading event list from URL", url=url, name=name, fmt=fmt
+            )
+
+    async def load_event_list_from_url_stream(
+        self,
+        url: str,
+        name: str,
+        fmt: str = "ogip",
+        rmf_file: Optional[str] = None,
+        additional_columns: Optional[List[str]] = None,
+        high_precision: bool = False,
+        skip_checks: bool = False,
+        notes: Optional[str] = None,
+    ) -> AsyncGenerator[Dict[str, Any], None]:
+        """
+        Load an EventList from a URL with SSE streaming for progress updates.
+
+        Yields progress events during download and processing, allowing
+        the frontend to show real-time progress.
+
+        Args:
+            url: URL to download the event file from
+            name: Name to assign to the loaded event list
+            fmt: File format
+            rmf_file: Optional path to local RMF file for energy calibration
+            additional_columns: Optional list of additional columns to read
+            high_precision: Use numpy.float128 for time array (pulsar timing)
+            skip_checks: Skip time ordering and GTI validation (performance)
+            notes: Optional notes/comments to attach to the event list
+
+        Yields:
+            Dict events with types: 'progress', 'processing', 'complete', 'error'
+        """
+        import httpx
+
+        try:
+            # Validate the name doesn't already exist
+            if self.state.has_event_data(name):
+                yield {
+                    "type": "error",
+                    "error": f"An event list with the name '{name}' already exists.",
+                }
+                return
+
+            # Start download with progress tracking
+            temp_filename = None
+            try:
+                async with httpx.AsyncClient(timeout=300.0) as client:
+                    # Use streaming response to track download progress
+                    async with client.stream("GET", url) as response:
+                        response.raise_for_status()
+
+                        # Get total file size if available
+                        total_bytes = int(response.headers.get("content-length", 0))
+
+                        # Create temporary file
+                        with tempfile.NamedTemporaryFile(delete=False, suffix=f".{fmt}") as tmp_file:
+                            temp_filename = tmp_file.name
+                            bytes_downloaded = 0
+
+                            # Download in chunks with progress updates
+                            async for chunk in response.aiter_bytes(chunk_size=65536):
+                                tmp_file.write(chunk)
+                                bytes_downloaded += len(chunk)
+
+                                # Yield progress event
+                                percent = (bytes_downloaded / total_bytes * 100) if total_bytes > 0 else 0
+                                yield {
+                                    "type": "progress",
+                                    "bytes_downloaded": bytes_downloaded,
+                                    "total_bytes": total_bytes,
+                                    "percent": round(percent, 1),
+                                }
+
+                                # Allow event loop to process
+                                await asyncio.sleep(0)
+
+                # Yield processing event
+                yield {
+                    "type": "processing",
+                    "message": "Download complete, loading event list...",
+                }
+                await asyncio.sleep(0)
+
+                # Detect file type before attempting to load (for FITS files)
+                is_fits = fmt.lower() in ['ogip', 'hea', 'fits', 'evt']
+                if is_fits:
+                    file_type_info = self._detect_fits_file_type(temp_filename)
+                    if not file_type_info["is_event_list"]:
+                        # Clean up temp file
+                        if temp_filename and os.path.exists(temp_filename):
+                            os.remove(temp_filename)
+                        yield {
+                            "type": "error",
+                            "error": f"Cannot load '{name}' as Event List: {file_type_info['error_message']}",
+                        }
+                        return
+
+                # Load the event list (blocking, but typically fast after download)
+                def _load_event_list():
+                    return EventList.read(
+                        temp_filename,
+                        fmt=fmt,
+                        rmf_file=rmf_file,
+                        additional_columns=additional_columns,
+                        high_precision=high_precision,
+                        skip_checks=skip_checks,
+                    )
+
+                # Run blocking I/O in thread pool
+                loop = asyncio.get_event_loop()
+                event_list = await loop.run_in_executor(None, _load_event_list)
+
+            finally:
+                # Clean up temporary file
+                if temp_filename and os.path.exists(temp_filename):
+                    os.remove(temp_filename)
+
+            # Fix inverted GTI intervals (common with unsorted data)
+            gti_was_fixed = self._fix_inverted_gti(event_list)
+
+            # Add to state manager with notes
+            self.state.add_event_data(name, event_list, notes=notes)
+
+            # Validate GTI and collect warnings
+            gti_warnings = self._validate_gti(event_list)
+            if gti_was_fixed:
+                gti_warnings.insert(0, "GTI intervals were inverted and automatically fixed.")
+
+            # Validate data quality
+            validation_issues = self._validate_data_quality(event_list)
+
+            summary = {
+                "name": name,
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "has_energy": event_list.energy is not None,
+                "has_pi": event_list.pi is not None,
+                "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                "gti_warnings": gti_warnings if gti_warnings else None,
+                "validation_issues": validation_issues,
+                "notes": notes,
+            }
+
+            # Build message with warnings if present
+            message = f"EventList '{name}' loaded successfully from URL"
+            if gti_warnings:
+                message += f" [GTI warnings: {len(gti_warnings)}]"
+
+            yield {
+                "type": "complete",
+                "data": summary,
+                "message": message,
+            }
+
+        except httpx.HTTPStatusError as e:
+            yield {
+                "type": "error",
+                "error": f"HTTP error {e.response.status_code}: {e.response.reason_phrase}",
+            }
+        except httpx.RequestError as e:
+            yield {
+                "type": "error",
+                "error": f"Failed to download file from URL: {str(e)}",
+            }
+        except Exception as e:
+            yield {
+                "type": "error",
+                "error": f"Error loading event list: {str(e)}",
+            }
+
+    def save_event_list(
+        self,
+        name: str,
+        file_path: str,
+        fmt: str = "hdf5",
+    ) -> Dict[str, Any]:
+        """
+        Save an EventList to disk.
+
+        Args:
+            name: Name of the event list in state
+            file_path: Path where to save the file
+            fmt: File format to save as ('hdf5', 'ascii.ecsv', or 'pickle')
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            if not self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"No event list found with name '{name}'",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(name)
+
+            # Ensure directory exists (handle case where file_path has no directory)
+            dirname = os.path.dirname(file_path)
+            if dirname:
+                os.makedirs(dirname, exist_ok=True)
+
+            # Save using Stingray's native write method with requested format
+            # Supported formats: hdf5, ascii.ecsv, pickle
+            if fmt in ('hdf5', 'ascii.ecsv', 'pickle'):
+                event_list.write(file_path, fmt=fmt)
+            else:
+                # Default to HDF5 for unrecognized formats
+                # HDF5 preserves all metadata (GTI, MJDREF, etc.) and supports float128
+                event_list.write(file_path, fmt='hdf5')
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path},
+                message=f"EventList '{name}' saved to '{file_path}' (format: {fmt})",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Saving event list", name=name, file_path=file_path, fmt=fmt
+            )
+
+    def delete_event_list(self, name: str) -> Dict[str, Any]:
+        """Delete an EventList from state."""
+        try:
+            if not self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"No event list found with name '{name}'",
+                    error=None,
+                )
+
+            self.state.remove_event_data(name)
+
+            return self.create_result(
+                success=True,
+                data={"name": name},
+                message=f"EventList '{name}' deleted successfully",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Deleting event list", name=name)
+
+    def get_event_list_info(self, name: str) -> Dict[str, Any]:
+        """
+        Get information about an EventList.
+
+        Args:
+            name: Name of the event list
+
+        Returns:
+            Result dictionary with event list information
+        """
+        try:
+            if not self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"No event list found with name '{name}'",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(name)
+
+            # Basic info - use helper functions for numpy type conversion
+            duration = _to_python_float(event_list.time.max() - event_list.time.min())
+            info = {
+                "name": name,
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "duration": duration,
+                "has_energy": event_list.energy is not None,
+                "has_pi": event_list.pi is not None,
+                "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                "mjdref": _to_python_float(event_list.mjdref),
+            }
+
+            # GTI details
+            if event_list.gti is not None and len(event_list.gti) > 0:
+                info["gti_list"] = [
+                    [_to_python_float(g[0]), _to_python_float(g[1])]
+                    for g in event_list.gti
+                ]
+                info["total_gti_time"] = _to_python_float(sum(g[1] - g[0] for g in event_list.gti))
+
+            # Energy/PI range
+            if event_list.energy is not None:
+                info["energy_range"] = [
+                    _to_python_float(event_list.energy.min()),
+                    _to_python_float(event_list.energy.max())
+                ]
+            if event_list.pi is not None:
+                info["pi_range"] = [int(event_list.pi.min()), int(event_list.pi.max())]
+
+            # Mission metadata (if available)
+            if hasattr(event_list, 'mission') and event_list.mission:
+                info["mission"] = str(event_list.mission)
+            if hasattr(event_list, 'instr') and event_list.instr:
+                info["instrument"] = str(event_list.instr)
+
+            # Time statistics
+            if len(event_list.time) > 1:
+                # Sort times to get accurate time differences
+                sorted_times = np.sort(event_list.time)
+                time_diffs = sorted_times[1:] - sorted_times[:-1]
+                info["mean_count_rate"] = _to_python_float(len(event_list.time) / duration) if duration and duration > 0 else 0
+                info["min_time_diff"] = _to_python_float(time_diffs.min())
+                info["max_time_diff"] = _to_python_float(time_diffs.max())
+                info["mean_time_diff"] = _to_python_float(np.mean(time_diffs))
+                info["median_time_diff"] = _to_python_float(np.median(time_diffs))
+                info["std_time_diff"] = _to_python_float(np.std(time_diffs))
+
+            # Per-GTI rates
+            if event_list.gti is not None and len(event_list.gti) > 0:
+                per_gti_rates = []
+                for start, stop in event_list.gti:
+                    mask = (event_list.time >= start) & (event_list.time <= stop)
+                    gti_events = int(np.sum(mask))
+                    gti_duration = float(stop - start)
+                    rate = gti_events / gti_duration if gti_duration > 0 else 0
+                    per_gti_rates.append({
+                        "start": _to_python_float(start),
+                        "stop": _to_python_float(stop),
+                        "events": gti_events,
+                        "duration": _to_python_float(gti_duration),
+                        "rate": _to_python_float(rate),
+                    })
+                info["per_gti_rates"] = per_gti_rates
+
+            # Notes
+            info["notes"] = getattr(event_list, "notes", None) or None
+
+            return self.create_result(
+                success=True,
+                data=info,
+                message=f"EventList '{name}' info retrieved",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Getting event list info", name=name)
+
+    def list_event_lists(self) -> Dict[str, Any]:
+        """List all loaded EventLists."""
+        try:
+            event_data = self.state.get_event_data()
+
+            summaries = []
+            for name, event_list in event_data:
+                summaries.append({
+                    "name": name,
+                    "n_events": len(event_list.time),
+                    "time_range": [
+                        _to_python_float(event_list.time.min()),
+                        _to_python_float(event_list.time.max())
+                    ],
+                    "has_energy": event_list.energy is not None,
+                    "has_pi": event_list.pi is not None,
+                    "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                })
+
+            return self.create_result(
+                success=True,
+                data=summaries,
+                message=f"Found {len(summaries)} event list(s)",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Listing event lists")
+
+    def check_file_size(self, file_path: str) -> Dict[str, Any]:
+        """Check file size and provide loading recommendations based on available RAM."""
+        try:
+            file_size = os.path.getsize(file_path)
+            file_size_mb = file_size / (1024**2)
+            file_size_gb = file_size / (1024**3)
+
+            # Get memory info first - we need this for smart recommendations
+            memory_info = self._get_memory_info()
+            available_ram_mb = memory_info["available_mb"]
+
+            # Estimate memory needed to load the EventList
+            # FITS files typically expand to ~3x file size in memory
+            estimated_memory_mb = self._estimate_memory_usage(file_size, "fits") / (1024**2)
+
+            # Calculate what percentage of available RAM this would use
+            ram_usage_percent = (estimated_memory_mb / available_ram_mb) * 100 if available_ram_mb > 0 else 100
+
+            # Determine risk level based on RAM usage percentage
+            # This is smarter than just file size - adapts to user's system
+            if ram_usage_percent > 80:
+                risk_level = "critical"  # Would use >80% of available RAM
+            elif ram_usage_percent > 50:
+                risk_level = "risky"     # Would use >50% of available RAM
+            elif ram_usage_percent > 30:
+                risk_level = "caution"   # Would use >30% of available RAM
+            else:
+                risk_level = "safe"      # Would use <30% of available RAM
+
+            # Recommend lazy loading if it would use more than 30% of available RAM
+            recommend_lazy = ram_usage_percent > 30
+
+            return self.create_result(
+                success=True,
+                data={
+                    "file_size_bytes": file_size,
+                    "file_size_mb": file_size_mb,
+                    "file_size_gb": file_size_gb,
+                    "risk_level": risk_level,
+                    "recommend_lazy": recommend_lazy,
+                    "estimated_memory_mb": estimated_memory_mb,
+                    "ram_usage_percent": round(ram_usage_percent, 1),
+                    "memory_info": memory_info,
+                },
+                message=f"File size: {file_size_mb:.2f} MB, Est. RAM usage: {ram_usage_percent:.1f}% of available",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Checking file size", file_path=file_path)
+
+    def clear_all_event_lists(self) -> Dict[str, Any]:
+        """Clear all loaded event lists from memory."""
+        try:
+            count = self.state.clear_event_data()
+
+            return self.create_result(
+                success=True,
+                data={"count": count},
+                message=f"Cleared {count} event list(s) from memory",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Clearing all event lists")
+
+    def _get_memory_info(self) -> Dict[str, Any]:
+        """Get current system memory information."""
+        vm = psutil.virtual_memory()
+        process = psutil.Process()
+        return {
+            "total_mb": vm.total / (1024**2),
+            "available_mb": vm.available / (1024**2),
+            "used_mb": vm.used / (1024**2),
+            "percent": vm.percent,
+            "process_mb": process.memory_info().rss / (1024**2),
+        }
+
+    def _estimate_memory_usage(self, file_size: int, fmt: str = "fits") -> int:
+        """
+        Estimate memory needed to load file into EventList.
+
+        Based on Stingray's official benchmarks:
+        - FITS event file: ~3x file size
+        - HDF5: ~2x file size
+        """
+        multipliers = {
+            "fits": 3,
+            "evt": 3,
+            "ogip": 3,
+            "hea": 3,
+            "hdf5": 2,
+        }
+        multiplier = multipliers.get(fmt, 3)
+        return int(file_size * multiplier)
+
+    def _can_load_safely(
+        self,
+        file_path: str,
+        safety_margin: float = 0.5,
+        fmt: str = "fits",
+    ) -> bool:
+        """Check if file can be safely loaded into memory."""
+        file_size = os.path.getsize(file_path)
+        available_ram = psutil.virtual_memory().available
+        needed_ram = self._estimate_memory_usage(file_size, fmt)
+        safe_limit = available_ram * safety_margin
+        return needed_ram < safe_limit
+
+    def get_event_list_full_preview(self, name: str, time_limit: int = 10) -> Dict[str, Any]:
+        """
+        Get full preview of an EventList with all attributes.
+
+        Args:
+            name: Name of the event list
+            time_limit: Number of time entries to show in preview
+
+        Returns:
+            Result dictionary with comprehensive preview data
+        """
+        try:
+            if not self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"No event list found with name '{name}'",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(name)
+
+            # Build comprehensive preview
+            # Use helper functions to convert numpy types (including longdouble) to Python types
+            preview = {
+                "name": name,
+                # Core data - use explicit float conversion for longdouble support
+                "times_preview": _to_python_float_list(event_list.time[:time_limit]),
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "duration": _to_python_float(event_list.time.max() - event_list.time.min()),
+
+                # Energy data
+                "has_energy": event_list.energy is not None,
+                "energy_preview": (
+                    _to_python_float_list(event_list.energy[:time_limit])
+                    if event_list.energy is not None
+                    else None
+                ),
+                "energy_range": (
+                    [
+                        _to_python_float(event_list.energy.min()),
+                        _to_python_float(event_list.energy.max())
+                    ]
+                    if event_list.energy is not None
+                    else None
+                ),
+
+                # PI data
+                "has_pi": event_list.pi is not None,
+                "pi_preview": (
+                    [int(x) for x in event_list.pi[:time_limit]]
+                    if event_list.pi is not None
+                    else None
+                ),
+                "pi_range": (
+                    [int(event_list.pi.min()), int(event_list.pi.max())]
+                    if event_list.pi is not None
+                    else None
+                ),
+
+                # GTI data - sort each interval to handle inverted GTIs from unsorted data
+                # When Stingray loads unsorted data without a GTI extension, it sets
+                # GTI to [time[0], time[-1]] which can have start > stop for unsorted times
+                "gti_count": 0,  # Will be set below
+                "gti_list": None,  # Will be set below
+                "total_gti_time": None,  # Will be set below
+
+                # Reference time - mjdref is often numpy.longdouble
+                "mjdref": _to_python_float(event_list.mjdref),
+
+                # Metadata (if available) - convert to string to be safe
+                "mission": str(getattr(event_list, "mission", None)) if getattr(event_list, "mission", None) else None,
+                "instrument": str(getattr(event_list, "instr", None)) if getattr(event_list, "instr", None) else None,
+                "detector_id": (
+                    str(getattr(event_list, "detector_id", None))
+                    if hasattr(event_list, "detector_id") and event_list.detector_id is not None
+                    else None
+                ),
+                "ephem": str(getattr(event_list, "ephem", None)) if getattr(event_list, "ephem", None) else None,
+                "timeref": str(getattr(event_list, "timeref", None)) if getattr(event_list, "timeref", None) else None,
+                "timesys": str(getattr(event_list, "timesys", None)) if getattr(event_list, "timesys", None) else None,
+
+                # Statistics
+                "mean_count_rate": None,
+                "min_time_diff": None,
+                "max_time_diff": None,
+            }
+
+            # Process GTI data - sort each interval to handle inverted GTIs from unsorted data
+            # When Stingray loads unsorted data without a GTI extension, it sets
+            # GTI to [time[0], time[-1]] which can have start > stop for unsorted times
+            valid_gti = None
+            if event_list.gti is not None and len(event_list.gti) > 0:
+                # Sort each GTI interval to ensure [start, stop] order (start <= stop)
+                valid_gti = [
+                    [_to_python_float(min(g[0], g[1])), _to_python_float(max(g[0], g[1]))]
+                    for g in event_list.gti
+                ]
+            preview["gti_count"] = len(valid_gti) if valid_gti else 0
+            preview["gti_list"] = valid_gti
+            preview["total_gti_time"] = (
+                _to_python_float(sum(g[1] - g[0] for g in valid_gti))
+                if valid_gti
+                else None
+            )
+
+            # Calculate time statistics
+            duration = preview["duration"]
+            if duration and duration > 0:
+                preview["mean_count_rate"] = _to_python_float(len(event_list.time) / duration)
+
+            if len(event_list.time) > 1:
+                sorted_times = np.sort(event_list.time)
+                time_diffs = sorted_times[1:] - sorted_times[:-1]
+                preview["min_time_diff"] = _to_python_float(time_diffs.min())
+                preview["max_time_diff"] = _to_python_float(time_diffs.max())
+                preview["mean_time_diff"] = _to_python_float(time_diffs.mean())
+                # Enhanced time statistics
+                preview["median_time_diff"] = _to_python_float(np.median(time_diffs))
+                preview["std_time_diff"] = _to_python_float(np.std(time_diffs))
+
+            # Per-GTI rates - use the validated/sorted GTI intervals
+            if valid_gti:
+                per_gti_rates = []
+                for gti_entry in valid_gti:
+                    start, stop = gti_entry[0], gti_entry[1]
+                    mask = (event_list.time >= start) & (event_list.time <= stop)
+                    gti_events = int(np.sum(mask))
+                    gti_duration = float(stop - start)
+                    rate = gti_events / gti_duration if gti_duration > 0 else 0
+                    per_gti_rates.append({
+                        "start": _to_python_float(start),
+                        "stop": _to_python_float(stop),
+                        "events": gti_events,
+                        "duration": _to_python_float(gti_duration),
+                        "rate": _to_python_float(rate),
+                    })
+                preview["per_gti_rates"] = per_gti_rates
+
+            # Check for additional columns
+            additional_cols = []
+            for attr in dir(event_list):
+                if not attr.startswith("_") and attr not in [
+                    "time", "energy", "pi", "gti", "mjdref", "mission", "instr",
+                    "detector_id", "ephem", "timeref", "timesys", "header",
+                    "notes", "ncounts", "dt", "n"
+                ]:
+                    val = getattr(event_list, attr, None)
+                    if isinstance(val, np.ndarray) and len(val) == len(event_list.time):
+                        additional_cols.append(attr)
+            preview["additional_columns"] = additional_cols
+
+            # User notes
+            preview["notes"] = getattr(event_list, "notes", None) or None
+
+            # Data quality validation
+            validation_issues = self._validate_data_quality(event_list)
+            preview["validation_issues"] = validation_issues if validation_issues else None
+
+            # FITS header information
+            header_info = self._extract_fits_header(event_list)
+            preview["header_info"] = header_info if header_info else None
+
+            return self.create_result(
+                success=True,
+                data=preview,
+                message=f"Full preview for '{name}' retrieved",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Getting event list full preview", name=name)
+
+    # =========================================================================
+    # PARTIAL LOADING METHODS
+    # These methods use FITSTimeseriesReader to load only a portion of the file
+    # =========================================================================
+    #
+    # TODO: Implement true chunk-based lazy loading for streaming analysis
+    #
+    # FITSTimeseriesReader supports genuine lazy/streaming I/O via:
+    # - reader.split_by_number_of_samples(N) -> Generator yielding N-event chunks
+    # - reader.filter_at_time_intervals(intervals) -> Generator for time ranges
+    # - reader.apply_gti_lists(gti_lists) -> Generator for GTI-based splits
+    #
+    # This would allow processing huge files without loading them fully:
+    #   for chunk in reader.split_by_number_of_samples(100000):
+    #       ps = AveragedPowerspectrum(chunk, segment_size=128)
+    #       # Aggregate results...
+    #
+    # Implementation would require:
+    # 1. Store FITSTimeseriesReader objects in StateManager (not just EventLists)
+    # 2. Create generator-based iteration endpoints
+    # 3. Implement chunked analysis methods (works well with Averaged* classes)
+    #
+    # Note: Only "averaged" analysis methods (AveragedPowerspectrum, etc.) benefit
+    # from chunking. Single-FFT methods need all data at once.
+    # =========================================================================
+
+    def load_event_list_by_time_range(
+        self,
+        file_path: str,
+        name: str,
+        start_time: float,
+        end_time: float,
+        fmt: str = "ogip",
+        notes: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Load events within a specific time range using true lazy loading.
+
+        Uses FITSTimeseriesReader.filter_at_time_intervals() to load only
+        events within the specified time window without reading the entire file.
+
+        Args:
+            file_path: Path to the FITS event file
+            name: Name to assign to the loaded event list
+            start_time: Start time (in seconds from file start or absolute)
+            end_time: End time (in seconds from file start or absolute)
+            fmt: File format (only FITS formats supported for lazy loading)
+            notes: Optional user notes/comments about this data
+
+        Returns:
+            Result dictionary with the filtered EventList
+        """
+        try:
+            # Validate the name doesn't already exist
+            if self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"An event list with the name '{name}' already exists.",
+                    error=None,
+                )
+
+            # Check format - only FITS supports true lazy loading
+            is_fits = fmt.lower() in ['ogip', 'hea', 'fits', 'evt']
+            if not is_fits:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"True lazy loading only supports FITS formats. Got: {fmt}",
+                    error="Unsupported format for lazy loading",
+                )
+
+            # Detect file type before attempting to load
+            file_type_info = self._detect_fits_file_type(file_path)
+            if not file_type_info["is_event_list"]:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Cannot load '{name}' as Event List: "
+                            f"{file_type_info['error_message']}",
+                    error=f"File type: {file_type_info['file_type']}",
+                )
+
+            # Create the reader
+            reader = FITSTimeseriesReader(
+                file_path, output_class=EventList, data_kind="events"
+            )
+
+            # Get file metadata
+            original_gti = reader.gti
+            mjdref = getattr(reader, 'mjdref', 0.0)
+            mission = getattr(reader, 'mission', None)
+            instr = getattr(reader, 'instr', None)
+
+            # Get total event count for reporting
+            times_reader = FITSTimeseriesReader(file_path, data_kind="times")
+            total_events = len(times_reader[:])
+
+            # Calculate absolute times if relative times are provided
+            # If start_time is small (< 1000), treat as relative to GTI start
+            if original_gti is not None and len(original_gti) > 0:
+                gti_start = float(original_gti[0, 0])
+                gti_end = float(original_gti[-1, 1])
+                total_duration = float(np.sum(original_gti[:, 1] - original_gti[:, 0]))
+
+                # If times look relative (small values), convert to absolute
+                if start_time < 1000 and end_time < 1000:
+                    abs_start = gti_start + start_time
+                    abs_end = gti_start + end_time
+                else:
+                    abs_start = start_time
+                    abs_end = end_time
+
+                # Clamp to valid range
+                abs_start = max(abs_start, gti_start)
+                abs_end = min(abs_end, gti_end)
+            else:
+                abs_start = start_time
+                abs_end = end_time
+                total_duration = 0.0
+
+            if abs_start >= abs_end:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Invalid time range: start ({abs_start}) >= end ({abs_end})",
+                    error="Invalid time range",
+                )
+
+            # Use filter_at_time_intervals to get events in the time range
+            # This returns a generator - we get the first (and only) result
+            time_intervals = [[abs_start, abs_end]]
+            event_list = None
+
+            for filtered_events in reader.filter_at_time_intervals(time_intervals):
+                event_list = filtered_events
+                break  # Only one interval
+
+            if event_list is None or len(event_list.time) == 0:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"No events found in time range [{start_time}, {end_time}]",
+                    error="Empty time range",
+                )
+
+            # Set metadata that might not be transferred
+            event_list.mjdref = _to_python_float(mjdref) or 0.0
+            if mission:
+                event_list.mission = mission
+            if instr:
+                event_list.instr = instr
+
+            # Store user notes on the event list object
+            if notes:
+                event_list.notes = notes
+
+            # Fix inverted GTI intervals (common with unsorted data)
+            gti_was_fixed = self._fix_inverted_gti(event_list)
+
+            # Add to state manager
+            self.state.add_event_data(name, event_list)
+
+            # Validate GTI and collect warnings
+            gti_warnings = self._validate_gti(event_list)
+            if gti_was_fixed:
+                gti_warnings.insert(0, "GTI intervals were inverted and automatically fixed.")
+
+            # Run comprehensive data quality validation
+            validation_issues = self._validate_data_quality(event_list)
+
+            # Calculate loaded duration
+            loaded_duration = abs_end - abs_start
+
+            summary = {
+                "name": name,
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "has_energy": event_list.energy is not None,
+                "has_pi": event_list.pi is not None,
+                "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                "gti_warnings": gti_warnings if gti_warnings else None,
+                "validation_issues": validation_issues if validation_issues else None,
+                "notes": notes if notes else None,
+                "lazy_loading_info": {
+                    "method": "time_range",
+                    "requested_range": [start_time, end_time],
+                    "actual_range": [abs_start, abs_end],
+                    "loaded_duration": loaded_duration,
+                    "total_file_duration": total_duration,
+                    "total_file_events": total_events,
+                    "events_loaded_percent": (len(event_list.time) / total_events * 100) if total_events > 0 else 0,
+                },
+            }
+
+            message = (
+                f"Lazy loaded '{name}': {len(event_list.time)} events "
+                f"({summary['lazy_loading_info']['events_loaded_percent']:.1f}% of file) "
+                f"from time range [{start_time:.1f}s - {end_time:.1f}s]"
+            )
+            if gti_warnings:
+                message += f" [GTI warnings: {len(gti_warnings)}]"
+            if validation_issues:
+                error_count = sum(1 for v in validation_issues if v["severity"] == "error")
+                warn_count = sum(1 for v in validation_issues if v["severity"] == "warning")
+                if error_count > 0:
+                    message += f" [Data errors: {error_count}]"
+                if warn_count > 0:
+                    message += f" [Data warnings: {warn_count}]"
+
+            return self.create_result(
+                success=True,
+                data=summary,
+                message=message,
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Loading event list by time range",
+                file_path=file_path, name=name, start_time=start_time, end_time=end_time
+            )
+
+    def load_event_list_by_event_count(
+        self,
+        file_path: str,
+        name: str,
+        start_index: int = 0,
+        count: int = 10000,
+        fmt: str = "ogip",
+        notes: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Load a specific number of events using true lazy loading.
+
+        Uses FITSTimeseriesReader slicing to load only the requested events
+        without reading the entire file into memory.
+
+        Args:
+            file_path: Path to the FITS event file
+            name: Name to assign to the loaded event list
+            start_index: Starting event index (0-based)
+            count: Number of events to load
+            fmt: File format (only FITS formats supported for lazy loading)
+            notes: Optional user notes/comments about this data
+
+        Returns:
+            Result dictionary with the sliced EventList
+        """
+        try:
+            # Validate the name doesn't already exist
+            if self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"An event list with the name '{name}' already exists.",
+                    error=None,
+                )
+
+            # Check format
+            is_fits = fmt.lower() in ['ogip', 'hea', 'fits', 'evt']
+            if not is_fits:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"True lazy loading only supports FITS formats. Got: {fmt}",
+                    error="Unsupported format for lazy loading",
+                )
+
+            # Detect file type before attempting to load
+            file_type_info = self._detect_fits_file_type(file_path)
+            if not file_type_info["is_event_list"]:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Cannot load '{name}' as Event List: "
+                            f"{file_type_info['error_message']}",
+                    error=f"File type: {file_type_info['file_type']}",
+                )
+
+            # Create the reader
+            reader = FITSTimeseriesReader(
+                file_path, output_class=EventList, data_kind="events"
+            )
+
+            # Get file metadata
+            original_gti = reader.gti
+            mjdref = getattr(reader, 'mjdref', 0.0)
+            mission = getattr(reader, 'mission', None)
+            instr = getattr(reader, 'instr', None)
+
+            # Get total event count
+            times_reader = FITSTimeseriesReader(file_path, data_kind="times")
+            all_times = times_reader[:]
+            total_events = len(all_times)
+
+            if original_gti is not None and len(original_gti) > 0:
+                total_duration = float(np.sum(original_gti[:, 1] - original_gti[:, 0]))
+            else:
+                total_duration = float(all_times.max() - all_times.min()) if total_events > 0 else 0.0
+
+            # Validate indices
+            if start_index < 0:
+                start_index = 0
+            if start_index >= total_events:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Start index {start_index} exceeds total events {total_events}",
+                    error="Invalid start index",
+                )
+
+            end_index = min(start_index + count, total_events)
+            actual_count = end_index - start_index
+
+            # Use slice to load only requested events
+            event_list = reader[start_index:end_index]
+
+            if event_list is None or len(event_list.time) == 0:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"No events found in range [{start_index}:{end_index}]",
+                    error="Empty event range",
+                )
+
+            # Set metadata that might not be transferred
+            event_list.mjdref = _to_python_float(mjdref) or 0.0
+            if mission:
+                event_list.mission = mission
+            if instr:
+                event_list.instr = instr
+
+            # Store user notes on the event list object
+            if notes:
+                event_list.notes = notes
+
+            # Fix inverted GTI intervals (common with unsorted data)
+            gti_was_fixed = self._fix_inverted_gti(event_list)
+
+            # Add to state manager
+            self.state.add_event_data(name, event_list)
+
+            # Validate GTI and collect warnings
+            gti_warnings = self._validate_gti(event_list)
+            if gti_was_fixed:
+                gti_warnings.insert(0, "GTI intervals were inverted and automatically fixed.")
+
+            # Run comprehensive data quality validation
+            validation_issues = self._validate_data_quality(event_list)
+
+            summary = {
+                "name": name,
+                "n_events": len(event_list.time),
+                "time_range": [
+                    _to_python_float(event_list.time.min()),
+                    _to_python_float(event_list.time.max())
+                ],
+                "has_energy": event_list.energy is not None,
+                "has_pi": event_list.pi is not None,
+                "gti_count": len(event_list.gti) if event_list.gti is not None else 0,
+                "gti_warnings": gti_warnings if gti_warnings else None,
+                "validation_issues": validation_issues if validation_issues else None,
+                "notes": notes if notes else None,
+                "lazy_loading_info": {
+                    "method": "event_count",
+                    "start_index": start_index,
+                    "end_index": end_index,
+                    "events_requested": count,
+                    "events_loaded": actual_count,
+                    "total_file_events": total_events,
+                    "total_file_duration": total_duration,
+                    "events_loaded_percent": (actual_count / total_events * 100) if total_events > 0 else 0,
+                },
+            }
+
+            message = (
+                f"Lazy loaded '{name}': {actual_count} events "
+                f"({summary['lazy_loading_info']['events_loaded_percent']:.1f}% of file) "
+                f"from indices [{start_index}:{end_index}]"
+            )
+            if gti_warnings:
+                message += f" [GTI warnings: {len(gti_warnings)}]"
+            if validation_issues:
+                error_count = sum(1 for v in validation_issues if v["severity"] == "error")
+                warn_count = sum(1 for v in validation_issues if v["severity"] == "warning")
+                if error_count > 0:
+                    message += f" [Data errors: {error_count}]"
+                if warn_count > 0:
+                    message += f" [Data warnings: {warn_count}]"
+
+            return self.create_result(
+                success=True,
+                data=summary,
+                message=message,
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Loading event list by event count",
+                file_path=file_path, name=name, start_index=start_index, count=count
+            )
+
+    def get_file_metadata(self, file_path: str, fmt: str = "ogip") -> Dict[str, Any]:
+        """
+        Get metadata from a FITS file without loading the full data.
+
+        Uses FITSTimeseriesReader to efficiently read only metadata:
+        - Total event count
+        - Time range
+        - GTI information
+        - Mission/instrument info
+        - Available columns
+
+        This is useful for previewing large files before deciding
+        what portion to load.
+
+        Args:
+            file_path: Path to the FITS event file
+            fmt: File format
+
+        Returns:
+            Result dictionary with file metadata
+        """
+        try:
+            # Check format
+            is_fits = fmt.lower() in ['ogip', 'hea', 'fits', 'evt']
+            if not is_fits:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Metadata preview only supports FITS formats. Got: {fmt}",
+                    error="Unsupported format",
+                )
+
+            file_size = os.path.getsize(file_path)
+            file_size_mb = file_size / (1024**2)
+            file_size_gb = file_size / (1024**3)
+
+            # Read metadata directly from FITS headers - NO full data loading!
+            # This is much faster than using FITSTimeseriesReader for large files
+            total_events = 0
+            time_min = None
+            time_max = None
+            available_columns = []
+            mjdref = 0.0
+            mission = None
+            instr = None
+            original_gti = None
+
+            with fits.open(file_path) as hdulist:
+                # Find the EVENTS or EVT extension
+                events_hdu = None
+                for hdu in hdulist:
+                    if hdu.name.upper() in ['EVENTS', 'EVT']:
+                        events_hdu = hdu
+                        break
+
+                if events_hdu is not None:
+                    # Get row count from header (NAXIS2) - no data loading!
+                    total_events = events_hdu.header.get('NAXIS2', 0)
+                    available_columns = [col.name for col in events_hdu.columns]
+
+                    # Get MJDREF from header
+                    mjdref = events_hdu.header.get('MJDREF', 0.0)
+                    if mjdref == 0.0:
+                        # Some files split MJDREF into integer and fractional parts
+                        mjdrefi = events_hdu.header.get('MJDREFI', 0)
+                        mjdreff = events_hdu.header.get('MJDREFF', 0.0)
+                        mjdref = mjdrefi + mjdreff
+
+                    # Get mission/instrument from header
+                    mission = events_hdu.header.get('TELESCOP', None) or events_hdu.header.get('MISSION', None)
+                    instr = events_hdu.header.get('INSTRUME', None)
+
+                    # Get time range from header keywords if available (fast!)
+                    tstart = events_hdu.header.get('TSTART', None)
+                    tstop = events_hdu.header.get('TSTOP', None)
+
+                    if tstart is not None and tstop is not None:
+                        time_min = float(tstart)
+                        time_max = float(tstop)
+                    elif total_events > 0:
+                        # Fallback: read only first and last few rows (much faster than full load)
+                        time_col = events_hdu.data['TIME']
+                        time_min = float(time_col[0])
+                        time_max = float(time_col[-1])
+
+                # Read GTI extension (usually small, OK to load fully)
+                gti_hdu = None
+                for hdu in hdulist:
+                    if hdu.name.upper() in ['GTI', 'STDGTI']:
+                        gti_hdu = hdu
+                        break
+
+                if gti_hdu is not None and gti_hdu.data is not None:
+                    start_col = gti_hdu.data['START'] if 'START' in gti_hdu.columns.names else None
+                    stop_col = gti_hdu.data['STOP'] if 'STOP' in gti_hdu.columns.names else None
+                    if start_col is not None and stop_col is not None:
+                        original_gti = np.column_stack([start_col, stop_col])
+
+            # Calculate duration
+            duration = (time_max - time_min) if (time_min is not None and time_max is not None) else 0.0
+
+            # GTI info
+            gti_count = len(original_gti) if original_gti is not None else 0
+            total_gti_time = None
+            if original_gti is not None and len(original_gti) > 0:
+                total_gti_time = float(np.sum(original_gti[:, 1] - original_gti[:, 0]))
+
+            # Determine risk level for loading
+            if file_size_gb > 10:
+                risk_level = "critical"
+            elif file_size_gb > 5:
+                risk_level = "risky"
+            elif file_size_gb > 1:
+                risk_level = "caution"
+            else:
+                risk_level = "safe"
+
+            metadata = {
+                "file_path": file_path,
+                "file_size_mb": file_size_mb,
+                "file_size_gb": file_size_gb,
+                "risk_level": risk_level,
+                "total_events": total_events,
+                "time_range": [time_min, time_max],
+                "duration": duration,
+                "gti_count": gti_count,
+                "total_gti_time": total_gti_time,
+                "gti_list": (
+                    [[_to_python_float(g[0]), _to_python_float(g[1])] for g in original_gti]
+                    if original_gti is not None
+                    else None
+                ),
+                "mjdref": _to_python_float(mjdref),
+                "mission": mission,
+                "instrument": instr,
+                "available_columns": available_columns,
+                "recommended_loading": self._recommend_loading_strategy(
+                    total_events, file_size_gb, risk_level
+                ),
+            }
+
+            return self.create_result(
+                success=True,
+                data=metadata,
+                message=f"Metadata retrieved: {total_events} events, {file_size_mb:.1f} MB, {gti_count} GTI",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Getting file metadata", file_path=file_path
+            )
+
+    def _recommend_loading_strategy(
+        self,
+        total_events: int,
+        file_size_gb: float,
+        risk_level: str,
+    ) -> Dict[str, Any]:
+        """
+        Recommend a loading strategy based on file characteristics.
+
+        Returns recommendations for how to load the file efficiently.
+
+        Strategies:
+        - 'full': Safe to load the entire file
+        - 'time_range': Use partial loading by time range
+        - 'event_count': Use partial loading by event count (for very large files)
+        """
+        recommendations = {
+            "can_load_full": risk_level in ["safe"],
+            "recommend_lazy": risk_level in ["caution", "risky", "critical"],
+            "suggested_chunk_size": None,
+            "suggested_time_chunk": None,
+            "strategy": "full",
+        }
+
+        if risk_level == "critical":
+            # Very large file (>10GB) - must use partial loading
+            recommendations["strategy"] = "event_count"
+            recommendations["suggested_chunk_size"] = min(100000, max(1000, total_events // 10))
+            recommendations["suggested_time_chunk"] = 100.0  # seconds
+        elif risk_level == "risky":
+            # Large file (5-10GB) - strongly recommend partial loading
+            recommendations["strategy"] = "time_range"
+            recommendations["suggested_chunk_size"] = min(500000, max(10000, total_events // 5))
+            recommendations["suggested_time_chunk"] = 500.0
+        elif risk_level == "caution":
+            # Medium file (1-5GB) - partial loading recommended
+            recommendations["strategy"] = "time_range"
+            recommendations["suggested_chunk_size"] = min(1000000, max(50000, total_events // 2))
+            recommendations["suggested_time_chunk"] = 1000.0
+        else:
+            # Small file (<1GB) - safe to load fully
+            recommendations["strategy"] = "full"
+
+        return recommendations
+
+    # =========================================================================
+    # BATCH LOADING METHODS
+    # Load multiple files in parallel using ThreadPoolExecutor
+    # =========================================================================
+
+    def _get_risk_level(self, ram_percent: float) -> str:
+        """Determine risk level based on RAM usage percentage."""
+        if ram_percent > 80:
+            return "critical"
+        elif ram_percent > 50:
+            return "risky"
+        elif ram_percent > 30:
+            return "caution"
+        else:
+            return "safe"
+
+    def check_batch_file_size(self, file_paths: List[str]) -> Dict[str, Any]:
+        """
+        Check sizes of multiple files and estimate total memory usage.
+
+        Args:
+            file_paths: List of file paths to check
+
+        Returns:
+            Result dictionary with per-file and total memory estimates
+        """
+        try:
+            memory_info = self._get_memory_info()
+            available_ram_mb = memory_info["available_mb"]
+
+            files_info = []
+            total_size_mb = 0.0
+            total_estimated_ram_mb = 0.0
+
+            for path in file_paths:
+                if not os.path.exists(path):
+                    files_info.append({
+                        "file_path": path,
+                        "file_name": os.path.basename(path),
+                        "error": "File not found",
+                        "size_mb": 0,
+                        "estimated_ram_mb": 0,
+                        "ram_percent": 0,
+                        "risk_level": "critical",
+                    })
+                    continue
+
+                size_bytes = os.path.getsize(path)
+                size_mb = size_bytes / (1024**2)
+
+                # Determine format from extension
+                ext = os.path.splitext(path)[1].lower()
+                fmt = "fits" if ext in ['.fits', '.fit', '.fts', '.evt'] else "hdf5" if ext in ['.hdf5', '.h5'] else "fits"
+
+                estimated_ram_mb = self._estimate_memory_usage(size_bytes, fmt) / (1024**2)
+                ram_percent = (estimated_ram_mb / available_ram_mb) * 100 if available_ram_mb > 0 else 100
+
+                files_info.append({
+                    "file_path": path,
+                    "file_name": os.path.basename(path),
+                    "size_mb": round(size_mb, 1),
+                    "estimated_ram_mb": round(estimated_ram_mb, 1),
+                    "ram_percent": round(ram_percent, 1),
+                    "risk_level": self._get_risk_level(ram_percent),
+                })
+
+                total_size_mb += size_mb
+                total_estimated_ram_mb += estimated_ram_mb
+
+            total_ram_percent = (total_estimated_ram_mb / available_ram_mb) * 100 if available_ram_mb > 0 else 100
+
+            return self.create_result(
+                success=True,
+                data={
+                    "files": files_info,
+                    "total": {
+                        "size_mb": round(total_size_mb, 1),
+                        "estimated_ram_mb": round(total_estimated_ram_mb, 1),
+                        "ram_percent": round(total_ram_percent, 1),
+                        "risk_level": self._get_risk_level(total_ram_percent),
+                    },
+                    "available_ram_mb": round(available_ram_mb, 1),
+                    "file_count": len(file_paths),
+                    "recommend_partial_loading": total_ram_percent > 30,
+                },
+                message=f"Checked {len(file_paths)} files: {total_size_mb:.1f} MB total, ~{total_ram_percent:.0f}% of available RAM",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Checking batch file sizes")
+
+    def load_batch_event_lists(
+        self,
+        files: List[Dict[str, Any]],
+        use_same_settings: bool = True,
+        # Shared settings (used when use_same_settings=True)
+        shared_fmt: str = "ogip",
+        shared_rmf_file: Optional[str] = None,
+        shared_additional_columns: Optional[List[str]] = None,
+        shared_high_precision: bool = False,
+        shared_skip_checks: bool = False,
+        shared_use_partial_loading: bool = False,
+        shared_partial_mode: str = "time_range",
+        shared_time_range_start: Optional[float] = None,
+        shared_time_range_end: Optional[float] = None,
+        shared_event_start_index: Optional[int] = None,
+        shared_event_count: Optional[int] = None,
+        max_workers: Optional[int] = None,
+    ) -> Dict[str, Any]:
+        """
+        Load multiple EventLists in parallel using threads.
+
+        Args:
+            files: List of dicts with file configurations. Each dict should have:
+                - file_path: str (required)
+                - name: str (required)
+                - fmt: str (optional, used if use_same_settings=False)
+                - rmf_file: Optional[str]
+                - additional_columns: Optional[List[str]]
+                - high_precision: bool
+                - skip_checks: bool
+                - use_partial_loading: bool
+                - partial_mode: str ('time_range' or 'event_count')
+                - time_range_start/end: Optional[float]
+                - event_start_index/count: Optional[int]
+            use_same_settings: If True, use shared_* settings for all files
+            shared_*: Settings applied to all files when use_same_settings=True
+            max_workers: Max parallel threads (default: min(cpu_count, len(files), 8))
+
+        Returns:
+            Result with successful[], failed[], and summary statistics
+        """
+        start_time = time.time()
+
+        if not files:
+            return self.create_result(
+                success=False,
+                message="No files provided for batch loading",
+            )
+
+        # Pre-validate: check for duplicate names in the request
+        names = [f.get("name", "") for f in files]
+        duplicate_names = [name for name in set(names) if names.count(name) > 1]
+        if duplicate_names:
+            return self.create_result(
+                success=False,
+                message=f"Duplicate names in request: {duplicate_names}",
+            )
+
+        # Pre-validate: check no names already exist in state
+        existing_names = []
+        for f in files:
+            name = f.get("name", "")
+            if name and self.state.has_event_data(name):
+                existing_names.append(name)
+        if existing_names:
+            return self.create_result(
+                success=False,
+                message=f"Names already exist in state: {existing_names}",
+            )
+
+        # Determine worker count
+        if max_workers is None:
+            max_workers = min(os.cpu_count() or 4, len(files), 8)
+
+        successful: List[Dict[str, Any]] = []
+        failed: List[Dict[str, Any]] = []
+
+        def load_single_file(file_config: Dict[str, Any]) -> Dict[str, Any]:
+            """Load a single file with the appropriate settings."""
+            file_path = file_config.get("file_path", "")
+            name = file_config.get("name", "")
+
+            if not file_path or not name:
+                return {
+                    "success": False,
+                    "name": name,
+                    "file_path": file_path,
+                    "error": "Missing file_path or name",
+                }
+
+            # Determine settings to use
+            if use_same_settings:
+                fmt = shared_fmt
+                rmf_file = shared_rmf_file
+                additional_columns = shared_additional_columns
+                high_precision = shared_high_precision
+                skip_checks = shared_skip_checks
+                use_partial = shared_use_partial_loading
+                partial_mode = shared_partial_mode
+                time_start = shared_time_range_start
+                time_end = shared_time_range_end
+                event_start = shared_event_start_index
+                event_cnt = shared_event_count
+                notes = None  # No shared notes for batch loading
+            else:
+                # Use per-file settings
+                fmt = file_config.get("fmt", "ogip")
+                rmf_file = file_config.get("rmf_file")
+                additional_columns = file_config.get("additional_columns")
+                high_precision = file_config.get("high_precision", False)
+                skip_checks = file_config.get("skip_checks", False)
+                use_partial = file_config.get("use_partial_loading", False)
+                partial_mode = file_config.get("partial_mode", "time_range")
+                time_start = file_config.get("time_range_start")
+                time_end = file_config.get("time_range_end")
+                event_start = file_config.get("event_start_index")
+                event_cnt = file_config.get("event_count")
+                notes = file_config.get("notes")
+
+            try:
+                if use_partial:
+                    if partial_mode == "time_range":
+                        if time_start is None or time_end is None:
+                            return {
+                                "success": False,
+                                "name": name,
+                                "file_path": file_path,
+                                "error": "Partial loading (time_range) requires time_range_start and time_range_end",
+                            }
+                        result = self.load_event_list_by_time_range(
+                            file_path, name, time_start, time_end, fmt, notes
+                        )
+                    else:  # event_count
+                        result = self.load_event_list_by_event_count(
+                            file_path, name,
+                            event_start or 0,
+                            event_cnt or 10000,
+                            fmt,
+                            notes
+                        )
+                else:
+                    result = self.load_event_list(
+                        file_path, name, fmt,
+                        rmf_file, additional_columns,
+                        high_precision, skip_checks,
+                        notes
+                    )
+
+                return {
+                    "success": result.get("success", False),
+                    "name": name,
+                    "file_path": file_path,
+                    "data": result.get("data"),
+                    "message": result.get("message"),
+                    "error": result.get("error") if not result.get("success") else None,
+                }
+            except Exception as e:
+                return {
+                    "success": False,
+                    "name": name,
+                    "file_path": file_path,
+                    "error": str(e),
+                }
+
+        # Execute loading in parallel
+        with ThreadPoolExecutor(max_workers=max_workers) as executor:
+            # Submit all tasks
+            future_to_file = {
+                executor.submit(load_single_file, f): f
+                for f in files
+            }
+
+            # Collect results as they complete
+            for future in as_completed(future_to_file):
+                file_info = future_to_file[future]
+                try:
+                    result = future.result()
+                    if result.get("success"):
+                        successful.append({
+                            "name": result["name"],
+                            "file_path": result["file_path"],
+                            "data": result.get("data"),
+                            "message": result.get("message"),
+                        })
+                    else:
+                        failed.append({
+                            "name": result.get("name", file_info.get("name", "")),
+                            "file_path": result.get("file_path", file_info.get("file_path", "")),
+                            "error": result.get("error", "Unknown error"),
+                        })
+                except Exception as e:
+                    failed.append({
+                        "name": file_info.get("name", ""),
+                        "file_path": file_info.get("file_path", ""),
+                        "error": str(e),
+                    })
+
+        total_time_ms = (time.time() - start_time) * 1000
+        total_events = sum(
+            s.get("data", {}).get("n_events", 0) if s.get("data") else 0
+            for s in successful
+        )
+
+        return self.create_result(
+            success=len(failed) == 0,
+            data={
+                "successful": successful,
+                "failed": failed,
+                "summary": {
+                    "total_files": len(files),
+                    "success_count": len(successful),
+                    "failure_count": len(failed),
+                    "total_events_loaded": total_events,
+                    "total_time_ms": round(total_time_ms, 1),
+                    "workers_used": max_workers,
+                },
+            },
+            message=f"Loaded {len(successful)}/{len(files)} files ({total_events:,} total events) in {total_time_ms:.0f}ms",
+        )
+
+    async def load_batch_event_lists_stream(
+        self,
+        files: List[Dict[str, Any]],
+        use_same_settings: bool = True,
+        shared_fmt: str = "ogip",
+        shared_rmf_file: Optional[str] = None,
+        shared_additional_columns: Optional[List[str]] = None,
+        shared_high_precision: bool = False,
+        shared_skip_checks: bool = False,
+        shared_use_partial_loading: bool = False,
+        shared_partial_mode: str = "time_range",
+        shared_time_range_start: Optional[float] = None,
+        shared_time_range_end: Optional[float] = None,
+        shared_event_start_index: Optional[int] = None,
+        shared_event_count: Optional[int] = None,
+        max_workers: Optional[int] = None,
+    ) -> AsyncGenerator[Dict[str, Any], None]:
+        """
+        Stream batch loading results as they complete via SSE.
+
+        Yields events for each file completion and a final summary event.
+        This allows the frontend to update progress incrementally instead of
+        waiting for all files to complete.
+
+        Args:
+            files: List of file configurations (same as load_batch_event_lists)
+            use_same_settings: If True, use shared_* settings for all files
+            shared_*: Shared settings applied when use_same_settings=True
+            max_workers: Max parallel threads
+
+        Yields:
+            Dict events with type 'file_complete' or 'complete'
+        """
+        start_time = time.time()
+
+        if not files:
+            yield {
+                "type": "error",
+                "error": "No files provided for batch loading",
+            }
+            return
+
+        # Pre-validate: check for duplicate names in the request
+        names = [f.get("name", "") for f in files]
+        duplicate_names = [name for name in set(names) if names.count(name) > 1]
+        if duplicate_names:
+            yield {
+                "type": "error",
+                "error": f"Duplicate names in request: {duplicate_names}",
+            }
+            return
+
+        # Pre-validate: check no names already exist in state
+        existing_names = []
+        for f in files:
+            name = f.get("name", "")
+            if name and self.state.has_event_data(name):
+                existing_names.append(name)
+        if existing_names:
+            yield {
+                "type": "error",
+                "error": f"Names already exist in state: {existing_names}",
+            }
+            return
+
+        # Determine worker count
+        if max_workers is None:
+            max_workers = min(os.cpu_count() or 4, len(files), 8)
+
+        successful: List[Dict[str, Any]] = []
+        failed: List[Dict[str, Any]] = []
+
+        def load_single_file(file_config: Dict[str, Any]) -> Dict[str, Any]:
+            """Load a single file with the appropriate settings."""
+            file_path = file_config.get("file_path", "")
+            name = file_config.get("name", "")
+
+            if not file_path or not name:
+                return {
+                    "success": False,
+                    "name": name,
+                    "file_path": file_path,
+                    "error": "Missing file_path or name",
+                }
+
+            # Determine settings to use
+            if use_same_settings:
+                fmt = shared_fmt
+                rmf_file = shared_rmf_file
+                additional_columns = shared_additional_columns
+                high_precision = shared_high_precision
+                skip_checks = shared_skip_checks
+                use_partial = shared_use_partial_loading
+                partial_mode = shared_partial_mode
+                time_start = shared_time_range_start
+                time_end = shared_time_range_end
+                event_start = shared_event_start_index
+                event_cnt = shared_event_count
+                notes = None  # No shared notes for batch loading
+            else:
+                # Use per-file settings
+                fmt = file_config.get("fmt", "ogip")
+                rmf_file = file_config.get("rmf_file")
+                additional_columns = file_config.get("additional_columns")
+                high_precision = file_config.get("high_precision", False)
+                skip_checks = file_config.get("skip_checks", False)
+                use_partial = file_config.get("use_partial_loading", False)
+                partial_mode = file_config.get("partial_mode", "time_range")
+                time_start = file_config.get("time_range_start")
+                time_end = file_config.get("time_range_end")
+                event_start = file_config.get("event_start_index")
+                event_cnt = file_config.get("event_count")
+                notes = file_config.get("notes")
+
+            try:
+                if use_partial:
+                    if partial_mode == "time_range":
+                        if time_start is None or time_end is None:
+                            return {
+                                "success": False,
+                                "name": name,
+                                "file_path": file_path,
+                                "error": "Partial loading (time_range) requires time_range_start and time_range_end",
+                            }
+                        result = self.load_event_list_by_time_range(
+                            file_path, name, time_start, time_end, fmt, notes
+                        )
+                    else:  # event_count
+                        result = self.load_event_list_by_event_count(
+                            file_path, name,
+                            event_start or 0,
+                            event_cnt or 10000,
+                            fmt,
+                            notes
+                        )
+                else:
+                    result = self.load_event_list(
+                        file_path, name, fmt,
+                        rmf_file, additional_columns,
+                        high_precision, skip_checks,
+                        notes
+                    )
+
+                return {
+                    "success": result.get("success", False),
+                    "name": name,
+                    "file_path": file_path,
+                    "data": result.get("data"),
+                    "message": result.get("message"),
+                    "error": result.get("error") if not result.get("success") else None,
+                }
+            except Exception as e:
+                return {
+                    "success": False,
+                    "name": name,
+                    "file_path": file_path,
+                    "error": str(e),
+                }
+
+        # Execute loading in parallel, yielding results as they complete
+        # Use asyncio to wrap thread pool futures for proper async handling
+        loop = asyncio.get_event_loop()
+
+        with ThreadPoolExecutor(max_workers=max_workers) as executor:
+            # Submit all tasks and wrap them as asyncio futures
+            future_to_file = {
+                executor.submit(load_single_file, f): f
+                for f in files
+            }
+
+            # Convert to asyncio futures for proper async iteration
+            pending = {
+                asyncio.wrap_future(future): (future, file_info)
+                for future, file_info in future_to_file.items()
+            }
+
+            completed = 0
+            while pending:
+                # Wait for the next future to complete (non-blocking)
+                done, _ = await asyncio.wait(
+                    pending.keys(),
+                    return_when=asyncio.FIRST_COMPLETED
+                )
+
+                for async_future in done:
+                    original_future, file_info = pending.pop(async_future)
+                    completed += 1
+
+                    try:
+                        result = async_future.result()
+                        if result.get("success"):
+                            successful.append({
+                                "name": result["name"],
+                                "file_path": result["file_path"],
+                                "data": result.get("data"),
+                                "message": result.get("message"),
+                            })
+                            yield {
+                                "type": "file_complete",
+                                "name": result["name"],
+                                "file_path": result["file_path"],
+                                "success": True,
+                                "completed": completed,
+                                "total": len(files),
+                                "data": result.get("data"),
+                            }
+                        else:
+                            error_msg = result.get("error", "Unknown error")
+                            failed.append({
+                                "name": result.get("name", file_info.get("name", "")),
+                                "file_path": result.get("file_path", file_info.get("file_path", "")),
+                                "error": error_msg,
+                            })
+                            yield {
+                                "type": "file_complete",
+                                "name": result.get("name", file_info.get("name", "")),
+                                "file_path": result.get("file_path", file_info.get("file_path", "")),
+                                "success": False,
+                                "completed": completed,
+                                "total": len(files),
+                                "error": error_msg,
+                            }
+                    except Exception as e:
+                        failed.append({
+                            "name": file_info.get("name", ""),
+                            "file_path": file_info.get("file_path", ""),
+                            "error": str(e),
+                        })
+                        yield {
+                            "type": "file_complete",
+                            "name": file_info.get("name", ""),
+                            "file_path": file_info.get("file_path", ""),
+                            "success": False,
+                            "completed": completed,
+                            "total": len(files),
+                            "error": str(e),
+                        }
+
+                    # Allow event loop to flush the SSE response
+                    await asyncio.sleep(0)
+
+        # Final completion event with summary
+        total_time_ms = (time.time() - start_time) * 1000
+        total_events = sum(
+            s.get("data", {}).get("n_events", 0) if s.get("data") else 0
+            for s in successful
+        )
+
+        yield {
+            "type": "complete",
+            "total_time_ms": round(total_time_ms, 1),
+            "success_count": len(successful),
+            "failure_count": len(failed),
+            "total_events": total_events,
+            "workers_used": max_workers,
+        }
diff --git a/python-backend/services/export_service.py b/python-backend/services/export_service.py
new file mode 100644
index 0000000..27f3362
--- /dev/null
+++ b/python-backend/services/export_service.py
@@ -0,0 +1,355 @@
+"""
+Export service for data export operations.
+
+Handles exporting data to various formats.
+"""
+
+import os
+from typing import Any, Dict
+
+import numpy as np
+import pandas as pd
+
+from .base_service import BaseService
+
+
+class ExportService(BaseService):
+    """
+    Service for data export operations.
+
+    Handles conversion and export of astronomical data.
+    """
+
+    def export_event_list_to_csv(
+        self,
+        name: str,
+        file_path: str,
+    ) -> Dict[str, Any]:
+        """
+        Export an EventList to CSV file.
+
+        Args:
+            name: Name of the event list in state
+            file_path: Path where to save the CSV file
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            if not self.state.has_event_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{name}' not found",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(name)
+
+            # Create DataFrame
+            data = {"time": event_list.time}
+            if event_list.energy is not None:
+                data["energy"] = event_list.energy
+            if event_list.pi is not None:
+                data["pi"] = event_list.pi
+
+            df = pd.DataFrame(data)
+
+            # Ensure directory exists
+            os.makedirs(os.path.dirname(file_path), exist_ok=True)
+
+            # Save to CSV
+            df.to_csv(file_path, index=False)
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path, "n_rows": len(df)},
+                message=f"EventList exported to '{file_path}'",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Exporting event list to CSV", name=name, file_path=file_path
+            )
+
+    def export_lightcurve_to_csv(
+        self,
+        name: str,
+        file_path: str,
+    ) -> Dict[str, Any]:
+        """
+        Export a Lightcurve to CSV file.
+
+        Args:
+            name: Name of the lightcurve in state
+            file_path: Path where to save the CSV file
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            if not self.state.has_lightcurve_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Lightcurve '{name}' not found",
+                    error=None,
+                )
+
+            lightcurve = self.state.get_lightcurve_data(name)
+
+            df = pd.DataFrame({
+                "time": lightcurve.time,
+                "counts": lightcurve.counts,
+            })
+
+            os.makedirs(os.path.dirname(file_path), exist_ok=True)
+            df.to_csv(file_path, index=False)
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path, "n_rows": len(df)},
+                message=f"Lightcurve exported to '{file_path}'",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Exporting lightcurve to CSV", name=name, file_path=file_path
+            )
+
+    def export_power_spectrum_to_csv(
+        self,
+        name: str,
+        file_path: str,
+    ) -> Dict[str, Any]:
+        """
+        Export a power spectrum to CSV file.
+
+        Args:
+            name: Name of the spectrum in state
+            file_path: Path where to save the CSV file
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            if not self.state.has_spectrum_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Spectrum '{name}' not found",
+                    error=None,
+                )
+
+            spectrum = self.state.get_spectrum_data(name)
+
+            df = pd.DataFrame({
+                "frequency": spectrum.freq,
+                "power": spectrum.power,
+            })
+
+            os.makedirs(os.path.dirname(file_path), exist_ok=True)
+            df.to_csv(file_path, index=False)
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path, "n_rows": len(df)},
+                message=f"Spectrum exported to '{file_path}'",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Exporting spectrum to CSV", name=name, file_path=file_path
+            )
+
+    def export_bispectrum_to_csv(
+        self,
+        name: str,
+        file_path: str,
+    ) -> Dict[str, Any]:
+        """
+        Export a bispectrum to CSV file.
+
+        Args:
+            name: Name of the bispectrum in state
+            file_path: Path where to save the CSV file
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            bispectrum = self.state.get_analysis_result(name)
+
+            if bispectrum is None:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Bispectrum '{name}' not found",
+                    error=None,
+                )
+
+            # Create flattened DataFrame for 2D bispectrum data
+            freq_grid, lags_grid = np.meshgrid(
+                bispectrum.freq, bispectrum.lags
+            )
+
+            df = pd.DataFrame({
+                "frequency": freq_grid.flatten(),
+                "lags": lags_grid.flatten(),
+                "cum3": bispectrum.cum3.flatten(),
+                "magnitude": bispectrum.bispec_mag.flatten(),
+                "phase": bispectrum.bispec_phase.flatten(),
+            })
+
+            os.makedirs(os.path.dirname(file_path), exist_ok=True)
+            df.to_csv(file_path, index=False)
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path, "n_rows": len(df)},
+                message=f"Bispectrum exported to '{file_path}'",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Exporting bispectrum to CSV", name=name, file_path=file_path
+            )
+
+    def export_to_hdf5(
+        self,
+        name: str,
+        file_path: str,
+        data_type: str = "event_list",
+    ) -> Dict[str, Any]:
+        """
+        Export data to HDF5 file.
+
+        Args:
+            name: Name of the data in state
+            file_path: Path where to save the HDF5 file
+            data_type: Type of data ("event_list", "lightcurve", "spectrum")
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            if data_type == "event_list":
+                if not self.state.has_event_data(name):
+                    return self.create_result(
+                        success=False,
+                        data=None,
+                        message=f"EventList '{name}' not found",
+                        error=None,
+                    )
+                data = self.state.get_event_data(name)
+                table = data.to_astropy_table()
+
+            elif data_type == "lightcurve":
+                if not self.state.has_lightcurve_data(name):
+                    return self.create_result(
+                        success=False,
+                        data=None,
+                        message=f"Lightcurve '{name}' not found",
+                        error=None,
+                    )
+                data = self.state.get_lightcurve_data(name)
+                # Convert lightcurve to table
+                from astropy.table import Table
+                table = Table({
+                    "time": data.time,
+                    "counts": data.counts,
+                })
+
+            elif data_type == "spectrum":
+                if not self.state.has_spectrum_data(name):
+                    return self.create_result(
+                        success=False,
+                        data=None,
+                        message=f"Spectrum '{name}' not found",
+                        error=None,
+                    )
+                data = self.state.get_spectrum_data(name)
+                from astropy.table import Table
+                table = Table({
+                    "frequency": data.freq,
+                    "power": data.power,
+                })
+
+            else:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Unknown data type: {data_type}",
+                    error=None,
+                )
+
+            os.makedirs(os.path.dirname(file_path), exist_ok=True)
+            table.write(file_path, format="hdf5", path="data", overwrite=True)
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path},
+                message=f"Data exported to HDF5: '{file_path}'",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Exporting to HDF5",
+                name=name,
+                file_path=file_path,
+                data_type=data_type,
+            )
+
+    def export_to_fits(
+        self,
+        name: str,
+        file_path: str,
+        data_type: str = "event_list",
+    ) -> Dict[str, Any]:
+        """
+        Export data to FITS file.
+
+        Args:
+            name: Name of the data in state
+            file_path: Path where to save the FITS file
+            data_type: Type of data
+
+        Returns:
+            Result dictionary
+        """
+        try:
+            if data_type == "event_list":
+                if not self.state.has_event_data(name):
+                    return self.create_result(
+                        success=False,
+                        data=None,
+                        message=f"EventList '{name}' not found",
+                        error=None,
+                    )
+                data = self.state.get_event_data(name)
+                os.makedirs(os.path.dirname(file_path), exist_ok=True)
+                data.write(file_path, fmt="ogip")
+
+            else:
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"FITS export not supported for {data_type}",
+                    error=None,
+                )
+
+            return self.create_result(
+                success=True,
+                data={"file_path": file_path},
+                message=f"Data exported to FITS: '{file_path}'",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Exporting to FITS",
+                name=name,
+                file_path=file_path,
+                data_type=data_type,
+            )
diff --git a/python-backend/services/job_manager.py b/python-backend/services/job_manager.py
new file mode 100644
index 0000000..0efd511
--- /dev/null
+++ b/python-backend/services/job_manager.py
@@ -0,0 +1,808 @@
+"""
+Job Manager for background task queue.
+
+This module provides a thread-safe job queue system with SSE streaming
+for real-time progress updates. Jobs are executed in a ThreadPoolExecutor
+to avoid blocking the main event loop.
+"""
+
+import asyncio
+import logging
+import queue
+import threading
+from concurrent.futures import Future, ThreadPoolExecutor
+from datetime import datetime, timezone
+from typing import Any, AsyncGenerator, Callable, Dict, List, Optional
+
+from models.job import Job, JobStatus, JobType
+
+logger = logging.getLogger(__name__)
+
+# Maximum number of completed jobs to retain
+MAX_COMPLETED_JOBS = 100
+
+
+class JobManager:
+    """
+    Thread-safe background job manager with SSE streaming support.
+
+    Manages a queue of background jobs, executing them in a thread pool
+    while providing real-time progress updates via SSE.
+    """
+
+    def __init__(
+        self,
+        state_manager: Any,
+        data_service: Any,
+        max_workers: int = 4,
+    ) -> None:
+        """
+        Initialize the job manager.
+
+        Args:
+            state_manager: StateManager instance for checking name conflicts
+            data_service: DataService instance for executing load operations
+            max_workers: Maximum number of concurrent worker threads
+        """
+        self._state_manager = state_manager
+        self._data_service = data_service
+        self._lock = threading.RLock()
+
+        # Job storage: id -> Job
+        self._jobs: Dict[str, Job] = {}
+
+        # Thread pool for background execution
+        self._executor = ThreadPoolExecutor(
+            max_workers=max_workers,
+            thread_name_prefix="job_worker"
+        )
+
+        # Futures for tracking running jobs
+        self._futures: Dict[str, Future] = {}
+
+        # SSE update queue for broadcasting job updates
+        self._update_queue: queue.Queue[Dict[str, Any]] = queue.Queue(maxsize=1000)
+
+        # Active SSE connections counter
+        self._active_connections = 0
+
+        logger.info(f"JobManager initialized with {max_workers} workers")
+
+    def shutdown(self) -> None:
+        """Shutdown the job manager and its thread pool."""
+        logger.info("Shutting down JobManager...")
+        self._executor.shutdown(wait=False)
+        logger.info("JobManager shutdown complete")
+
+    # =========================================================================
+    # Job Management
+    # =========================================================================
+
+    def get_job(self, job_id: str) -> Optional[Job]:
+        """Get a job by ID."""
+        with self._lock:
+            return self._jobs.get(job_id)
+
+    def list_jobs(
+        self,
+        include_completed: bool = True,
+        limit: int = 50,
+    ) -> List[Job]:
+        """
+        List all jobs, newest first.
+
+        Args:
+            include_completed: Include completed/failed/cancelled jobs
+            limit: Maximum number of jobs to return
+
+        Returns:
+            List of jobs sorted by creation time (newest first)
+        """
+        with self._lock:
+            jobs = list(self._jobs.values())
+
+            if not include_completed:
+                jobs = [j for j in jobs if j.is_active]
+
+            # Sort by created_at descending (newest first)
+            jobs.sort(key=lambda j: j.created_at, reverse=True)
+
+            return jobs[:limit]
+
+    def get_active_jobs(self) -> List[Job]:
+        """Get all active (pending or running) jobs."""
+        with self._lock:
+            return [j for j in self._jobs.values() if j.is_active]
+
+    def cancel_job(self, job_id: str) -> bool:
+        """
+        Cancel a pending job.
+
+        Only pending jobs can be cancelled. Running jobs cannot be
+        interrupted (thread pool limitation).
+
+        Args:
+            job_id: ID of the job to cancel
+
+        Returns:
+            True if job was cancelled, False if not found or not cancellable
+        """
+        with self._lock:
+            job = self._jobs.get(job_id)
+            if not job:
+                return False
+
+            if job.status != JobStatus.PENDING:
+                return False
+
+            # Cancel the future if it exists
+            future = self._futures.get(job_id)
+            if future and not future.done():
+                future.cancel()
+
+            job.cancel()
+            self._broadcast_update("job_cancelled", job)
+            logger.info(f"Cancelled job {job_id}")
+            return True
+
+    def clear_completed_jobs(self) -> int:
+        """
+        Remove all completed/failed/cancelled jobs.
+
+        Returns:
+            Number of jobs removed
+        """
+        with self._lock:
+            to_remove = [
+                job_id for job_id, job in self._jobs.items()
+                if job.is_finished
+            ]
+
+            for job_id in to_remove:
+                del self._jobs[job_id]
+                self._futures.pop(job_id, None)
+
+            logger.info(f"Cleared {len(to_remove)} completed jobs")
+            return len(to_remove)
+
+    def _cleanup_old_jobs(self) -> None:
+        """Remove oldest completed jobs if exceeding limit."""
+        with self._lock:
+            finished_jobs = [
+                (job_id, job) for job_id, job in self._jobs.items()
+                if job.is_finished
+            ]
+
+            if len(finished_jobs) <= MAX_COMPLETED_JOBS:
+                return
+
+            # Sort by completed_at (oldest first)
+            finished_jobs.sort(key=lambda x: x[1].completed_at or "")
+
+            # Remove oldest jobs exceeding limit
+            to_remove = len(finished_jobs) - MAX_COMPLETED_JOBS
+            for job_id, _ in finished_jobs[:to_remove]:
+                del self._jobs[job_id]
+                self._futures.pop(job_id, None)
+
+            logger.debug(f"Cleaned up {to_remove} old completed jobs")
+
+    # =========================================================================
+    # Name Conflict Checking
+    # =========================================================================
+
+    def check_name_conflict(self, name: str) -> Dict[str, Any]:
+        """
+        Check if a name conflicts with existing data or pending jobs.
+
+        Args:
+            name: Name to check
+
+        Returns:
+            Dict with conflict status and suggested alternative if needed
+        """
+        with self._lock:
+            # Check against loaded event lists
+            if self._state_manager.has_event_data(name):
+                return {
+                    "has_conflict": True,
+                    "conflict_source": "loaded_data",
+                    "suggested_name": self._suggest_unique_name(name),
+                }
+
+            # Check against pending/running job names
+            for job in self._jobs.values():
+                if job.is_active:
+                    # Check job's target name(s)
+                    job_name = job.params.get("name")
+                    if job_name == name:
+                        return {
+                            "has_conflict": True,
+                            "conflict_source": "pending_job",
+                            "job_id": job.id,
+                            "suggested_name": self._suggest_unique_name(name),
+                        }
+
+                    # For batch jobs, check all file names
+                    files = job.params.get("files", [])
+                    for f in files:
+                        if f.get("name") == name:
+                            return {
+                                "has_conflict": True,
+                                "conflict_source": "pending_job",
+                                "job_id": job.id,
+                                "suggested_name": self._suggest_unique_name(name),
+                            }
+
+            return {"has_conflict": False}
+
+    def _suggest_unique_name(self, base_name: str) -> str:
+        """Generate a unique name by appending a number suffix."""
+        # Get all existing names
+        existing_names = set()
+
+        # From state manager
+        existing_names.update(self._state_manager.list_event_names())
+
+        # From active jobs
+        for job in self._jobs.values():
+            if job.is_active:
+                if job.params.get("name"):
+                    existing_names.add(job.params["name"])
+                for f in job.params.get("files", []):
+                    if f.get("name"):
+                        existing_names.add(f["name"])
+
+        # Find a unique name
+        if base_name not in existing_names:
+            return base_name
+
+        counter = 1
+        while f"{base_name}_{counter}" in existing_names:
+            counter += 1
+
+        return f"{base_name}_{counter}"
+
+    # =========================================================================
+    # Job Submission
+    # =========================================================================
+
+    def submit_load_job(
+        self,
+        file_path: str,
+        name: str,
+        fmt: str = "ogip",
+        rmf_file: Optional[str] = None,
+        additional_columns: Optional[List[str]] = None,
+        high_precision: bool = False,
+        skip_checks: bool = False,
+        notes: Optional[str] = None,
+        use_partial_loading: bool = False,
+        partial_mode: str = "time_range",
+        time_range_start: Optional[float] = None,
+        time_range_end: Optional[float] = None,
+        event_start_index: Optional[int] = None,
+        event_count: Optional[int] = None,
+    ) -> Job:
+        """
+        Submit a single file load job.
+
+        Returns immediately with the job object. The actual loading
+        happens asynchronously in the thread pool.
+
+        Args:
+            file_path: Path to the file to load
+            name: Name for the loaded event list
+            ... other load parameters ...
+
+        Returns:
+            The created Job object
+        """
+        # Create display name from filename
+        import os
+        display_name = os.path.basename(file_path)
+
+        job = Job(
+            type=JobType.LOAD_EVENT_LIST,
+            display_name=display_name,
+            params={
+                "file_path": file_path,
+                "name": name,
+                "fmt": fmt,
+                "rmf_file": rmf_file,
+                "additional_columns": additional_columns,
+                "high_precision": high_precision,
+                "skip_checks": skip_checks,
+                "notes": notes,
+                "use_partial_loading": use_partial_loading,
+                "partial_mode": partial_mode,
+                "time_range_start": time_range_start,
+                "time_range_end": time_range_end,
+                "event_start_index": event_start_index,
+                "event_count": event_count,
+            },
+        )
+
+        with self._lock:
+            self._jobs[job.id] = job
+            self._cleanup_old_jobs()
+
+        # Submit to thread pool
+        future = self._executor.submit(self._execute_load_job, job)
+        future.add_done_callback(lambda f: self._on_job_complete(job.id, f))
+
+        with self._lock:
+            self._futures[job.id] = future
+
+        self._broadcast_update("job_created", job)
+        logger.info(f"Submitted load job {job.id} for {file_path}")
+
+        return job
+
+    def submit_batch_load_job(
+        self,
+        files: List[Dict[str, Any]],
+        use_same_settings: bool = True,
+        shared_fmt: str = "ogip",
+        shared_rmf_file: Optional[str] = None,
+        shared_additional_columns: Optional[List[str]] = None,
+        shared_high_precision: bool = False,
+        shared_skip_checks: bool = False,
+        shared_use_partial_loading: bool = False,
+        shared_partial_mode: str = "time_range",
+        shared_time_range_start: Optional[float] = None,
+        shared_time_range_end: Optional[float] = None,
+        shared_event_start_index: Optional[int] = None,
+        shared_event_count: Optional[int] = None,
+    ) -> Job:
+        """
+        Submit a batch file load job.
+
+        Args:
+            files: List of file configs with file_path, name, and optional settings
+            ... shared settings ...
+
+        Returns:
+            The created Job object
+        """
+        display_name = f"Batch load ({len(files)} files)"
+
+        job = Job(
+            type=JobType.LOAD_BATCH,
+            display_name=display_name,
+            total_items=len(files),
+            params={
+                "files": files,
+                "use_same_settings": use_same_settings,
+                "shared_fmt": shared_fmt,
+                "shared_rmf_file": shared_rmf_file,
+                "shared_additional_columns": shared_additional_columns,
+                "shared_high_precision": shared_high_precision,
+                "shared_skip_checks": shared_skip_checks,
+                "shared_use_partial_loading": shared_use_partial_loading,
+                "shared_partial_mode": shared_partial_mode,
+                "shared_time_range_start": shared_time_range_start,
+                "shared_time_range_end": shared_time_range_end,
+                "shared_event_start_index": shared_event_start_index,
+                "shared_event_count": shared_event_count,
+            },
+        )
+
+        with self._lock:
+            self._jobs[job.id] = job
+            self._cleanup_old_jobs()
+
+        # Submit to thread pool
+        future = self._executor.submit(self._execute_batch_load_job, job)
+        future.add_done_callback(lambda f: self._on_job_complete(job.id, f))
+
+        with self._lock:
+            self._futures[job.id] = future
+
+        self._broadcast_update("job_created", job)
+        logger.info(f"Submitted batch load job {job.id} for {len(files)} files")
+
+        return job
+
+    def submit_url_load_job(
+        self,
+        url: str,
+        name: str,
+        fmt: str = "ogip",
+        rmf_file: Optional[str] = None,
+        additional_columns: Optional[List[str]] = None,
+        high_precision: bool = False,
+        skip_checks: bool = False,
+        notes: Optional[str] = None,
+    ) -> Job:
+        """
+        Submit a URL download and load job.
+
+        Args:
+            url: URL to download
+            name: Name for the loaded event list
+            ... other load parameters ...
+
+        Returns:
+            The created Job object
+        """
+        # Extract filename from URL for display
+        import os
+        from urllib.parse import urlparse
+        parsed = urlparse(url)
+        display_name = os.path.basename(parsed.path) or "URL download"
+
+        job = Job(
+            type=JobType.LOAD_FROM_URL,
+            display_name=display_name,
+            params={
+                "url": url,
+                "name": name,
+                "fmt": fmt,
+                "rmf_file": rmf_file,
+                "additional_columns": additional_columns,
+                "high_precision": high_precision,
+                "skip_checks": skip_checks,
+                "notes": notes,
+            },
+        )
+
+        with self._lock:
+            self._jobs[job.id] = job
+            self._cleanup_old_jobs()
+
+        # Submit to thread pool
+        future = self._executor.submit(self._execute_url_load_job, job)
+        future.add_done_callback(lambda f: self._on_job_complete(job.id, f))
+
+        with self._lock:
+            self._futures[job.id] = future
+
+        self._broadcast_update("job_created", job)
+        logger.info(f"Submitted URL load job {job.id} for {url}")
+
+        return job
+
+    # =========================================================================
+    # Job Execution
+    # =========================================================================
+
+    def _execute_load_job(self, job: Job) -> None:
+        """Execute a single file load job in a worker thread."""
+        job.start()
+        self._broadcast_update("job_started", job)
+
+        try:
+            params = job.params
+            job.update_progress(0.1, f"Loading {job.display_name}...")
+            self._broadcast_update("job_progress", job)
+
+            # Determine loading method
+            if params.get("use_partial_loading"):
+                if params.get("partial_mode") == "time_range":
+                    result = self._data_service.load_event_list_by_time_range(
+                        file_path=params["file_path"],
+                        name=params["name"],
+                        start_time=params["time_range_start"],
+                        end_time=params["time_range_end"],
+                        fmt=params["fmt"],
+                        notes=params.get("notes"),
+                    )
+                else:  # event_count
+                    result = self._data_service.load_event_list_by_event_count(
+                        file_path=params["file_path"],
+                        name=params["name"],
+                        start_index=params.get("event_start_index", 0),
+                        count=params.get("event_count", 10000),
+                        fmt=params["fmt"],
+                        notes=params.get("notes"),
+                    )
+            else:
+                result = self._data_service.load_event_list(
+                    file_path=params["file_path"],
+                    name=params["name"],
+                    fmt=params["fmt"],
+                    rmf_file=params.get("rmf_file"),
+                    additional_columns=params.get("additional_columns"),
+                    high_precision=params.get("high_precision", False),
+                    skip_checks=params.get("skip_checks", False),
+                    notes=params.get("notes"),
+                )
+
+            if result.get("success"):
+                job.complete(result.get("data"))
+                self._broadcast_update("job_completed", job)
+                logger.info(f"Job {job.id} completed successfully")
+            else:
+                error_msg = result.get("message") or result.get("error") or "Unknown error"
+                job.fail(error_msg)
+                self._broadcast_update("job_failed", job)
+                logger.error(f"Job {job.id} failed: {error_msg}")
+
+        except Exception as e:
+            error_msg = str(e)
+            job.fail(error_msg)
+            self._broadcast_update("job_failed", job)
+            logger.exception(f"Job {job.id} failed with exception")
+
+    def _execute_batch_load_job(self, job: Job) -> None:
+        """Execute a batch load job in a worker thread."""
+        job.start()
+        self._broadcast_update("job_started", job)
+
+        try:
+            params = job.params
+            files = params["files"]
+            total = len(files)
+
+            successful = []
+            failed = []
+
+            for i, file_config in enumerate(files):
+                if job.status == JobStatus.CANCELLED:
+                    break
+
+                file_path = file_config["file_path"]
+                name = file_config["name"]
+
+                # Update progress
+                progress = (i / total)
+                job.update_progress(
+                    progress,
+                    f"Loading {i + 1}/{total}: {name}",
+                    completed_items=i,
+                )
+                self._broadcast_update("job_progress", job)
+
+                # Determine settings (per-file or shared)
+                if params.get("use_same_settings"):
+                    fmt = params.get("shared_fmt", "ogip")
+                    rmf_file = params.get("shared_rmf_file")
+                    additional_columns = params.get("shared_additional_columns")
+                    high_precision = params.get("shared_high_precision", False)
+                    skip_checks = params.get("shared_skip_checks", False)
+                    use_partial = params.get("shared_use_partial_loading", False)
+                    partial_mode = params.get("shared_partial_mode", "time_range")
+                    time_start = params.get("shared_time_range_start")
+                    time_end = params.get("shared_time_range_end")
+                    event_start = params.get("shared_event_start_index")
+                    event_cnt = params.get("shared_event_count")
+                else:
+                    fmt = file_config.get("fmt", "ogip")
+                    rmf_file = file_config.get("rmf_file")
+                    additional_columns = file_config.get("additional_columns")
+                    high_precision = file_config.get("high_precision", False)
+                    skip_checks = file_config.get("skip_checks", False)
+                    use_partial = file_config.get("use_partial_loading", False)
+                    partial_mode = file_config.get("partial_mode", "time_range")
+                    time_start = file_config.get("time_range_start")
+                    time_end = file_config.get("time_range_end")
+                    event_start = file_config.get("event_start_index")
+                    event_cnt = file_config.get("event_count")
+
+                notes = file_config.get("notes")
+
+                try:
+                    # Load the file
+                    if use_partial:
+                        if partial_mode == "time_range":
+                            result = self._data_service.load_event_list_by_time_range(
+                                file_path=file_path,
+                                name=name,
+                                start_time=time_start,
+                                end_time=time_end,
+                                fmt=fmt,
+                                notes=notes,
+                            )
+                        else:
+                            result = self._data_service.load_event_list_by_event_count(
+                                file_path=file_path,
+                                name=name,
+                                start_index=event_start or 0,
+                                count=event_cnt or 10000,
+                                fmt=fmt,
+                                notes=notes,
+                            )
+                    else:
+                        result = self._data_service.load_event_list(
+                            file_path=file_path,
+                            name=name,
+                            fmt=fmt,
+                            rmf_file=rmf_file,
+                            additional_columns=additional_columns,
+                            high_precision=high_precision,
+                            skip_checks=skip_checks,
+                            notes=notes,
+                        )
+
+                    if result.get("success"):
+                        successful.append({
+                            "name": name,
+                            "file_path": file_path,
+                            "data": result.get("data"),
+                        })
+                    else:
+                        error_msg = result.get("message") or result.get("error") or "Unknown error"
+                        failed.append({
+                            "name": name,
+                            "file_path": file_path,
+                            "error": error_msg,
+                        })
+
+                except Exception as e:
+                    failed.append({
+                        "name": name,
+                        "file_path": file_path,
+                        "error": str(e),
+                    })
+
+            # Final update
+            job.update_progress(1.0, "Complete", completed_items=total)
+
+            if len(failed) == 0:
+                job.complete({
+                    "successful": successful,
+                    "failed": failed,
+                    "success_count": len(successful),
+                    "failure_count": len(failed),
+                    "total_files": total,
+                })
+                self._broadcast_update("job_completed", job)
+                logger.info(f"Batch job {job.id} completed: {len(successful)}/{total} files")
+            elif len(successful) == 0:
+                job.fail(f"All {total} files failed to load")
+                self._broadcast_update("job_failed", job)
+                logger.error(f"Batch job {job.id} failed: all files failed")
+            else:
+                # Partial success
+                job.complete({
+                    "successful": successful,
+                    "failed": failed,
+                    "success_count": len(successful),
+                    "failure_count": len(failed),
+                    "total_files": total,
+                })
+                self._broadcast_update("job_completed", job)
+                logger.warning(
+                    f"Batch job {job.id} partial success: "
+                    f"{len(successful)}/{total} files loaded"
+                )
+
+        except Exception as e:
+            job.fail(str(e))
+            self._broadcast_update("job_failed", job)
+            logger.exception(f"Batch job {job.id} failed with exception")
+
+    def _execute_url_load_job(self, job: Job) -> None:
+        """Execute a URL download and load job in a worker thread."""
+        job.start()
+        self._broadcast_update("job_started", job)
+
+        try:
+            params = job.params
+            url = params["url"]
+
+            job.update_progress(0.1, "Downloading...")
+            self._broadcast_update("job_progress", job)
+
+            # Use the synchronous URL loading method
+            result = self._data_service.load_event_list_from_url(
+                url=url,
+                name=params["name"],
+                fmt=params["fmt"],
+                rmf_file=params.get("rmf_file"),
+                additional_columns=params.get("additional_columns"),
+                high_precision=params.get("high_precision", False),
+                skip_checks=params.get("skip_checks", False),
+            )
+
+            if result.get("success"):
+                job.complete(result.get("data"))
+                self._broadcast_update("job_completed", job)
+                logger.info(f"URL job {job.id} completed successfully")
+            else:
+                error_msg = result.get("message") or result.get("error") or "Unknown error"
+                job.fail(error_msg)
+                self._broadcast_update("job_failed", job)
+                logger.error(f"URL job {job.id} failed: {error_msg}")
+
+        except Exception as e:
+            job.fail(str(e))
+            self._broadcast_update("job_failed", job)
+            logger.exception(f"URL job {job.id} failed with exception")
+
+    def _on_job_complete(self, job_id: str, future: Future) -> None:
+        """Callback when a job future completes."""
+        with self._lock:
+            self._futures.pop(job_id, None)
+
+        # Handle any unexpected exceptions from the future
+        try:
+            future.result()  # Will re-raise any exception
+        except Exception as e:
+            job = self.get_job(job_id)
+            if job and job.is_active:
+                job.fail(f"Unexpected error: {e}")
+                self._broadcast_update("job_failed", job)
+                logger.exception(f"Job {job_id} future raised unexpected exception")
+
+    # =========================================================================
+    # SSE Streaming
+    # =========================================================================
+
+    def _broadcast_update(self, event_type: str, job: Job) -> None:
+        """Broadcast a job update to all SSE connections."""
+        update = {
+            "type": event_type,
+            "timestamp": datetime.now(timezone.utc).isoformat(),
+            "job": job.to_dict(),
+        }
+
+        try:
+            self._update_queue.put_nowait(update)
+        except queue.Full:
+            # Queue full, drop oldest and try again
+            try:
+                self._update_queue.get_nowait()
+                self._update_queue.put_nowait(update)
+            except queue.Empty:
+                pass
+
+    async def stream_updates(
+        self,
+        heartbeat_interval: float = 30.0,
+    ) -> AsyncGenerator[Dict[str, Any], None]:
+        """
+        Async generator that yields job updates for SSE streaming.
+
+        Includes periodic heartbeat events to keep the connection alive.
+
+        Args:
+            heartbeat_interval: Seconds between heartbeat events
+
+        Yields:
+            Job update dictionaries ready for JSON serialization
+        """
+        self._active_connections += 1
+        last_heartbeat = asyncio.get_event_loop().time()
+
+        # First, send current state of all active jobs
+        active_jobs = self.get_active_jobs()
+        if active_jobs:
+            yield {
+                "type": "initial_state",
+                "timestamp": datetime.now(timezone.utc).isoformat(),
+                "jobs": [job.to_dict() for job in active_jobs],
+            }
+
+        try:
+            while True:
+                # Check for updates in the queue
+                try:
+                    update = self._update_queue.get_nowait()
+                    yield update
+                    last_heartbeat = asyncio.get_event_loop().time()
+                except queue.Empty:
+                    # No updates, check if we need a heartbeat
+                    current_time = asyncio.get_event_loop().time()
+                    if current_time - last_heartbeat >= heartbeat_interval:
+                        yield {
+                            "type": "heartbeat",
+                            "timestamp": datetime.now(timezone.utc).isoformat(),
+                        }
+                        last_heartbeat = current_time
+
+                # Small sleep to yield control
+                await asyncio.sleep(0.1)
+
+        finally:
+            self._active_connections -= 1
+
+    @property
+    def active_connections(self) -> int:
+        """Get the number of active SSE connections."""
+        return self._active_connections
+
+
+# Global singleton instance (initialized in main.py lifespan)
+job_manager: Optional[JobManager] = None
diff --git a/python-backend/services/lightcurve_service.py b/python-backend/services/lightcurve_service.py
new file mode 100644
index 0000000..bc0802f
--- /dev/null
+++ b/python-backend/services/lightcurve_service.py
@@ -0,0 +1,310 @@
+"""
+Lightcurve service for lightcurve operations.
+
+Handles creation and manipulation of lightcurves.
+"""
+
+from typing import Any, Dict, List, Optional
+
+import numpy as np
+from stingray import Lightcurve
+
+from .base_service import BaseService
+
+# Cap on points transferred for plotting. The full-resolution Lightcurve stays
+# in StateManager; only the JSON payload is strided.
+DEFAULT_MAX_PLOT_POINTS = 200_000
+
+
+def _decimate_for_plot(time, counts, max_points):
+    """Stride-decimate arrays for display. Returns (time, counts, stride).
+
+    max_points: Cap on points in the JSON payload; None or 0 sends full resolution.
+    """
+    n = len(time)
+    if not max_points or max_points < 0 or n <= max_points:
+        return time, counts, 1
+    stride = int(np.ceil(n / max_points))
+    return time[::stride], counts[::stride], stride
+
+
+class LightcurveService(BaseService):
+    """
+    Service for Lightcurve operations.
+
+    Handles creation and manipulation of lightcurves without any UI dependencies.
+    """
+
+    def create_lightcurve_from_event_list(
+        self,
+        event_list_name: str,
+        dt: float,
+        output_name: str,
+        gti: Optional[List[List[float]]] = None,
+        max_points: Optional[int] = DEFAULT_MAX_PLOT_POINTS,
+    ) -> Dict[str, Any]:
+        """
+        Create a Lightcurve from an EventList.
+
+        Args:
+            event_list_name: Name of the EventList in state
+            dt: Time binning in seconds
+            output_name: Name to save the lightcurve as
+            gti: Optional Good Time Intervals
+            max_points: Cap on points in the JSON payload; None or 0 sends full resolution.
+
+        Returns:
+            Result dictionary with lightcurve data
+        """
+        try:
+            if not self.state.has_event_data(event_list_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_name}' not found",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(event_list_name)
+
+            # Create lightcurve from event list
+            lc = event_list.to_lc(dt=dt)
+
+            # Apply GTIs if provided
+            if gti is not None:
+                gti_array = np.array(gti)
+                lc = lc.apply_gtis(gti_array)
+
+            # Save to state
+            self.state.add_lightcurve_data(output_name, lc)
+
+            # Prepare response data
+            plot_time, plot_counts, stride = _decimate_for_plot(
+                lc.time, lc.counts, max_points
+            )
+            lc_data = {
+                "name": output_name,
+                "time": plot_time.astype(float).tolist(),
+                "counts": plot_counts.astype(float).tolist(),
+                "dt": float(lc.dt),
+                "n_bins": len(lc.time),
+                "plot_stride": stride,
+                "time_range": [float(lc.time.min()), float(lc.time.max())],
+                "count_rate_mean": float(np.mean(lc.counts / lc.dt)),
+            }
+
+            return self.create_result(
+                success=True,
+                data=lc_data,
+                message=f"Lightcurve '{output_name}' created (dt={dt}s, {len(lc.time)} bins)",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Creating lightcurve", event_list=event_list_name, dt=dt
+            )
+
+    def create_lightcurve_from_arrays(
+        self,
+        times: List[float],
+        counts: List[float],
+        dt: float,
+        output_name: str,
+    ) -> Dict[str, Any]:
+        """
+        Create a Lightcurve from time and count arrays.
+
+        Args:
+            times: Array of time values
+            counts: Array of count values
+            dt: Time binning in seconds
+            output_name: Name to save the lightcurve as
+
+        Returns:
+            Result dictionary with lightcurve data
+        """
+        try:
+            times_arr = np.array(times)
+            counts_arr = np.array(counts)
+
+            lc = Lightcurve(times_arr, counts_arr, dt=dt, skip_checks=True)
+
+            # Save to state
+            self.state.add_lightcurve_data(output_name, lc)
+
+            lc_data = {
+                "name": output_name,
+                "time": lc.time.astype(float).tolist(),
+                "counts": lc.counts.astype(float).tolist(),
+                "dt": float(lc.dt),
+                "n_bins": len(lc.time),
+            }
+
+            return self.create_result(
+                success=True,
+                data=lc_data,
+                message=f"Lightcurve '{output_name}' created from arrays",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Creating lightcurve from arrays", dt=dt)
+
+    def rebin_lightcurve(
+        self,
+        name: str,
+        rebin_factor: float,
+        output_name: str,
+        max_points: Optional[int] = DEFAULT_MAX_PLOT_POINTS,
+    ) -> Dict[str, Any]:
+        """
+        Rebin a lightcurve.
+
+        Args:
+            name: Name of the lightcurve to rebin
+            rebin_factor: Rebinning factor
+            output_name: Name for the rebinned lightcurve
+            max_points: Cap on points in the JSON payload; None or 0 sends full resolution.
+
+        Returns:
+            Result dictionary with rebinned lightcurve data
+        """
+        try:
+            if not self.state.has_lightcurve_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Lightcurve '{name}' not found",
+                    error=None,
+                )
+
+            lightcurve = self.state.get_lightcurve_data(name)
+            # stingray's first positional param is dt_new (absolute); we promise
+            # factor semantics, so pass f= explicitly.
+            rebinned_lc = lightcurve.rebin(f=rebin_factor)
+
+            # Save to state
+            self.state.add_lightcurve_data(output_name, rebinned_lc)
+
+            plot_time, plot_counts, stride = _decimate_for_plot(
+                rebinned_lc.time, rebinned_lc.counts, max_points
+            )
+            lc_data = {
+                "name": output_name,
+                "time": plot_time.astype(float).tolist(),
+                "counts": plot_counts.astype(float).tolist(),
+                "dt": float(rebinned_lc.dt),
+                "n_bins": len(rebinned_lc.time),
+                "plot_stride": stride,
+                "count_rate_mean": float(np.mean(rebinned_lc.counts / rebinned_lc.dt)),
+            }
+
+            return self.create_result(
+                success=True,
+                data=lc_data,
+                message=f"Lightcurve rebinned (factor={rebin_factor})",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Rebinning lightcurve", name=name, rebin_factor=rebin_factor
+            )
+
+    def get_lightcurve_data(
+        self, name: str, max_points: Optional[int] = DEFAULT_MAX_PLOT_POINTS
+    ) -> Dict[str, Any]:
+        """
+        Get lightcurve data for plotting.
+
+        Args:
+            name: Name of the lightcurve
+            max_points: Cap on points in the JSON payload; None or 0 sends full resolution.
+
+        Returns:
+            Result dictionary with lightcurve data
+        """
+        try:
+            if not self.state.has_lightcurve_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Lightcurve '{name}' not found",
+                    error=None,
+                )
+
+            lc = self.state.get_lightcurve_data(name)
+
+            plot_time, plot_counts, stride = _decimate_for_plot(
+                lc.time, lc.counts, max_points
+            )
+            lc_data = {
+                "name": name,
+                "time": plot_time.astype(float).tolist(),
+                "counts": plot_counts.astype(float).tolist(),
+                "dt": float(lc.dt),
+                "n_bins": len(lc.time),
+                "plot_stride": stride,
+                "time_range": [float(lc.time.min()), float(lc.time.max())],
+                "count_rate_mean": float(np.mean(lc.counts / lc.dt)),
+                "count_stats": {
+                    "mean": float(np.mean(lc.counts)),
+                    "std": float(np.std(lc.counts)),
+                    "min": float(np.min(lc.counts)),
+                    "max": float(np.max(lc.counts)),
+                },
+            }
+
+            return self.create_result(
+                success=True,
+                data=lc_data,
+                message=f"Lightcurve '{name}' data retrieved",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Getting lightcurve data", name=name)
+
+    def list_lightcurves(self) -> Dict[str, Any]:
+        """List all loaded lightcurves."""
+        try:
+            lc_data = self.state.get_lightcurve_data()
+
+            summaries = []
+            for name, lc in lc_data:
+                summaries.append(
+                    {
+                        "name": name,
+                        "n_bins": len(lc.time),
+                        "dt": float(lc.dt),
+                        "time_range": [float(lc.time.min()), float(lc.time.max())],
+                    }
+                )
+
+            return self.create_result(
+                success=True,
+                data=summaries,
+                message=f"Found {len(summaries)} lightcurve(s)",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Listing lightcurves")
+
+    def delete_lightcurve(self, name: str) -> Dict[str, Any]:
+        """Delete a lightcurve from state."""
+        try:
+            if not self.state.has_lightcurve_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Lightcurve '{name}' not found",
+                    error=None,
+                )
+
+            self.state.remove_lightcurve_data(name)
+
+            return self.create_result(
+                success=True,
+                data={"name": name},
+                message=f"Lightcurve '{name}' deleted",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Deleting lightcurve", name=name)
diff --git a/python-backend/services/spectrum_service.py b/python-backend/services/spectrum_service.py
new file mode 100644
index 0000000..777baef
--- /dev/null
+++ b/python-backend/services/spectrum_service.py
@@ -0,0 +1,583 @@
+"""
+Spectrum service for spectral analysis operations.
+
+Handles power spectrum, cross spectrum, and dynamical power spectrum operations.
+"""
+
+from typing import Any, Dict, Optional
+
+import numpy as np
+from stingray import (
+    AveragedCrossspectrum,
+    AveragedPowerspectrum,
+    Crossspectrum,
+    DynamicalPowerspectrum,
+    Powerspectrum,
+)
+
+from .base_service import BaseService
+
+
+def _power_to_lists(power) -> tuple:
+    """Split a (possibly complex) power array into JSON-safe magnitude and phase lists.
+
+    Returns (power_list, phase_list_or_None). Non-finite values become None so
+    strict JSON (and JS JSON.parse) never sees NaN/Infinity.
+    """
+    arr = np.asarray(power)
+    if np.iscomplexobj(arr):
+        mag = np.abs(arr)
+        phase = np.angle(arr)
+        return _finite_list(mag), _finite_list(phase)
+    return _finite_list(arr), None
+
+
+def _finite_list(arr) -> list:
+    """Convert a float array to a list, replacing non-finite values with None."""
+    values = np.asarray(arr, dtype=float)
+    if np.isfinite(values).all():
+        return values.tolist()
+    return [float(v) if np.isfinite(v) else None for v in values]
+
+
+def _segment_size_error(segment_size: float, dt: float) -> Optional[str]:
+    """Human-readable rejection for segment sizes that stingray fails on cryptically.
+
+    Needs at least 3 time bins per segment to produce a non-empty spectrum.
+    """
+    if segment_size / dt < 3:
+        return (
+            f"segment_size ({segment_size}s) must be at least 3x dt ({dt}s) "
+            "to produce a non-empty spectrum"
+        )
+    return None
+
+
+def _overlap_error(
+    events1, events2, segment_size: Optional[float] = None
+) -> Optional[str]:
+    """Readable rejection when two event lists share no time overlap.
+
+    Optional segment_size check: if provided and the overlap is shorter than
+    one segment, stingray will produce zero segments (cryptic error), so we
+    reject early with a human-readable message.
+    """
+    if len(events1.time) == 0 or len(events2.time) == 0:
+        return "one of the event lists contains no events"
+    start = max(float(events1.time[0]), float(events2.time[0]))
+    stop = min(float(events1.time[-1]), float(events2.time[-1]))
+    if stop <= start:
+        return (
+            "the two event lists have no overlapping time range "
+            f"({events1.time[0]:.1f}-{events1.time[-1]:.1f}s vs "
+            f"{events2.time[0]:.1f}-{events2.time[-1]:.1f}s)"
+        )
+    if segment_size is not None and (stop - start) < segment_size:
+        return (
+            f"the overlapping time range ({stop - start:.1f}s) is shorter than "
+            f"the segment size ({segment_size}s)"
+        )
+    return None
+
+
+class SpectrumService(BaseService):
+    """
+    Service for spectral analysis operations.
+
+    Handles power spectrum, cross spectrum, and dynamical power spectrum.
+    """
+
+    def create_power_spectrum(
+        self,
+        event_list_name: str,
+        dt: float,
+        norm: str = "leahy",
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Create a power spectrum from an EventList.
+
+        Args:
+            event_list_name: Name of the EventList in state
+            dt: Time binning in seconds
+            norm: Normalization type ("leahy", "frac", "abs", "none")
+            output_name: Optional name to save the spectrum
+
+        Returns:
+            Result dictionary with power spectrum data
+        """
+        try:
+            if not self.state.has_event_data(event_list_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_name}' not found",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(event_list_name)
+            lc = event_list.to_lc(dt=dt)
+            ps = Powerspectrum(lc, norm=norm)
+
+            # Save if name provided
+            if output_name:
+                self.state.add_spectrum_data(output_name, ps)
+
+            power_list, phase_list = _power_to_lists(ps.power)
+            ps_data = {
+                "name": output_name,
+                "freq": ps.freq.tolist(),
+                "power": power_list,
+                "power_phase": phase_list,
+                "norm": norm,
+                "n_freq": len(ps.freq),
+                "df": float(ps.df),
+                "freq_range": [float(ps.freq[0]), float(ps.freq[-1])],
+            }
+
+            return self.create_result(
+                success=True,
+                data=ps_data,
+                message=f"Power spectrum created (dt={dt}s, norm={norm})",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Creating power spectrum",
+                event_list=event_list_name,
+                dt=dt,
+                norm=norm,
+            )
+
+    def create_averaged_power_spectrum(
+        self,
+        event_list_name: str,
+        dt: float,
+        segment_size: float,
+        norm: str = "leahy",
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Create an averaged power spectrum from an EventList.
+
+        Args:
+            event_list_name: Name of the EventList in state
+            dt: Time binning in seconds
+            segment_size: Segment size in seconds for averaging
+            norm: Normalization type
+            output_name: Optional name to save the spectrum
+
+        Returns:
+            Result dictionary with averaged power spectrum data
+        """
+        try:
+            if not self.state.has_event_data(event_list_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_name}' not found",
+                    error=None,
+                )
+
+            seg_error = _segment_size_error(segment_size, dt)
+            if seg_error:
+                return self.create_result(
+                    success=False, data=None, message=seg_error, error=None
+                )
+
+            event_list = self.state.get_event_data(event_list_name)
+            lc = event_list.to_lc(dt=dt)
+            ps = AveragedPowerspectrum.from_lightcurve(lc, segment_size, norm=norm)
+
+            if output_name:
+                self.state.add_spectrum_data(output_name, ps)
+
+            power_list, phase_list = _power_to_lists(ps.power)
+            ps_data = {
+                "name": output_name,
+                "freq": ps.freq.tolist(),
+                "power": power_list,
+                "power_phase": phase_list,
+                "norm": norm,
+                "n_freq": len(ps.freq),
+                "df": float(ps.df),
+                "segment_size": segment_size,
+                "n_segments": int(ps.m) if hasattr(ps, "m") else None,
+            }
+
+            return self.create_result(
+                success=True,
+                data=ps_data,
+                message=f"Averaged power spectrum created (segment={segment_size}s)",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Creating averaged power spectrum",
+                event_list=event_list_name,
+                dt=dt,
+                segment_size=segment_size,
+                norm=norm,
+            )
+
+    def create_cross_spectrum(
+        self,
+        event_list_1_name: str,
+        event_list_2_name: str,
+        dt: float,
+        norm: str = "leahy",
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Create a cross spectrum from two EventLists.
+
+        Args:
+            event_list_1_name: Name of first EventList
+            event_list_2_name: Name of second EventList
+            dt: Time binning in seconds
+            norm: Normalization type
+            output_name: Optional name to save the spectrum
+
+        Returns:
+            Result dictionary with cross spectrum data
+        """
+        try:
+            if not self.state.has_event_data(event_list_1_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_1_name}' not found",
+                    error=None,
+                )
+
+            if not self.state.has_event_data(event_list_2_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_2_name}' not found",
+                    error=None,
+                )
+
+            event_list_1 = self.state.get_event_data(event_list_1_name)
+            event_list_2 = self.state.get_event_data(event_list_2_name)
+
+            overlap_error = _overlap_error(event_list_1, event_list_2)
+            if overlap_error:
+                return self.create_result(
+                    success=False, data=None, message=overlap_error, error=None
+                )
+
+            cs = Crossspectrum.from_events(
+                events1=event_list_1,
+                events2=event_list_2,
+                dt=dt,
+                norm=norm,
+            )
+
+            if output_name:
+                self.state.add_spectrum_data(output_name, cs)
+
+            power_list, phase_list = _power_to_lists(cs.power)
+            cs_data = {
+                "name": output_name,
+                "freq": cs.freq.tolist(),
+                "power": power_list,
+                "power_phase": phase_list,
+                "norm": norm,
+                "n_freq": len(cs.freq),
+                "df": float(cs.df),
+            }
+
+            return self.create_result(
+                success=True,
+                data=cs_data,
+                message=f"Cross spectrum created (dt={dt}s)",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Creating cross spectrum",
+                event_list_1=event_list_1_name,
+                event_list_2=event_list_2_name,
+                dt=dt,
+                norm=norm,
+            )
+
+    def create_averaged_cross_spectrum(
+        self,
+        event_list_1_name: str,
+        event_list_2_name: str,
+        dt: float,
+        segment_size: float,
+        norm: str = "leahy",
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Create an averaged cross spectrum from two EventLists.
+
+        Args:
+            event_list_1_name: Name of first EventList
+            event_list_2_name: Name of second EventList
+            dt: Time binning in seconds
+            segment_size: Segment size in seconds
+            norm: Normalization type
+            output_name: Optional name to save the spectrum
+
+        Returns:
+            Result dictionary with averaged cross spectrum data
+        """
+        try:
+            if not self.state.has_event_data(event_list_1_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_1_name}' not found",
+                    error=None,
+                )
+
+            if not self.state.has_event_data(event_list_2_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_2_name}' not found",
+                    error=None,
+                )
+
+            seg_error = _segment_size_error(segment_size, dt)
+            if seg_error:
+                return self.create_result(
+                    success=False, data=None, message=seg_error, error=None
+                )
+
+            event_list_1 = self.state.get_event_data(event_list_1_name)
+            event_list_2 = self.state.get_event_data(event_list_2_name)
+
+            overlap_error = _overlap_error(
+                event_list_1, event_list_2, segment_size=segment_size
+            )
+            if overlap_error:
+                return self.create_result(
+                    success=False, data=None, message=overlap_error, error=None
+                )
+
+            lc1 = event_list_1.to_lc(dt=dt)
+            lc2 = event_list_2.to_lc(dt=dt)
+
+            cs = AveragedCrossspectrum.from_lightcurve(
+                lc1=lc1,
+                lc2=lc2,
+                segment_size=segment_size,
+                norm=norm,
+            )
+
+            if output_name:
+                self.state.add_spectrum_data(output_name, cs)
+
+            power_list, phase_list = _power_to_lists(cs.power)
+            cs_data = {
+                "name": output_name,
+                "freq": cs.freq.tolist(),
+                "power": power_list,
+                "power_phase": phase_list,
+                "norm": norm,
+                "n_freq": len(cs.freq),
+                "df": float(cs.df),
+                "segment_size": segment_size,
+                "n_segments": int(cs.m) if hasattr(cs, "m") else None,
+            }
+
+            return self.create_result(
+                success=True,
+                data=cs_data,
+                message=f"Averaged cross spectrum created (segment={segment_size}s)",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Creating averaged cross spectrum",
+                event_list_1=event_list_1_name,
+                event_list_2=event_list_2_name,
+                dt=dt,
+                segment_size=segment_size,
+                norm=norm,
+            )
+
+    def create_dynamical_power_spectrum(
+        self,
+        event_list_name: str,
+        dt: float,
+        segment_size: float,
+        norm: str = "leahy",
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Create a dynamical power spectrum from an EventList.
+
+        Args:
+            event_list_name: Name of the EventList in state
+            dt: Time binning in seconds
+            segment_size: Segment size in seconds
+            norm: Normalization type
+            output_name: Optional name to save the spectrum
+
+        Returns:
+            Result dictionary with dynamical power spectrum data
+        """
+        try:
+            if not self.state.has_event_data(event_list_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_name}' not found",
+                    error=None,
+                )
+
+            seg_error = _segment_size_error(segment_size, dt)
+            if seg_error:
+                return self.create_result(
+                    success=False, data=None, message=seg_error, error=None
+                )
+
+            event_list = self.state.get_event_data(event_list_name)
+            lc = event_list.to_lc(dt=dt)
+            dps = DynamicalPowerspectrum(lc, segment_size=segment_size, norm=norm)
+
+            if output_name:
+                self.state.add_spectrum_data(output_name, dps)
+
+            dps_data = {
+                "name": output_name,
+                "freq": dps.freq.tolist(),
+                "time": dps.time.astype(float).tolist(),
+                "dyn_ps": [_finite_list(row) for row in dps.dyn_ps],
+                "norm": norm,
+                "segment_size": segment_size,
+                "shape": list(dps.dyn_ps.shape),
+            }
+
+            return self.create_result(
+                success=True,
+                data=dps_data,
+                message=f"Dynamical power spectrum created (segment={segment_size}s)",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Creating dynamical power spectrum",
+                event_list=event_list_name,
+                dt=dt,
+                segment_size=segment_size,
+                norm=norm,
+            )
+
+    def rebin_spectrum(
+        self,
+        name: str,
+        rebin_factor: float,
+        log: bool = False,
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Rebin a spectrum.
+
+        Args:
+            name: Name of the spectrum to rebin
+            rebin_factor: Rebinning factor
+            log: If True, use logarithmic rebinning
+            output_name: Optional name for rebinned spectrum
+
+        Returns:
+            Result dictionary with rebinned spectrum data
+        """
+        try:
+            if not self.state.has_spectrum_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Spectrum '{name}' not found",
+                    error=None,
+                )
+
+            spectrum = self.state.get_spectrum_data(name)
+
+            if log:
+                rebinned = spectrum.rebin_log(rebin_factor)
+            else:
+                # stingray rebin()'s positional arg is df (Hz), not a factor; use f=
+                rebinned = spectrum.rebin(f=rebin_factor)
+
+            if output_name:
+                self.state.add_spectrum_data(output_name, rebinned)
+
+            power_list, phase_list = _power_to_lists(rebinned.power)
+            data = {
+                "name": output_name,
+                "freq": rebinned.freq.tolist(),
+                "power": power_list,
+                "power_phase": phase_list,
+                "norm": getattr(rebinned, "norm", None),
+                "n_freq": len(rebinned.freq),
+            }
+
+            return self.create_result(
+                success=True,
+                data=data,
+                message=f"Spectrum rebinned ({'log' if log else 'linear'}, factor={rebin_factor})",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e, "Rebinning spectrum", name=name, rebin_factor=rebin_factor, log=log
+            )
+
+    def list_spectra(self) -> Dict[str, Any]:
+        """List all loaded spectra."""
+        try:
+            spec_data = self.state.get_spectrum_data()
+
+            summaries = []
+            for name, spec in spec_data:
+                spec_type = type(spec).__name__
+                summaries.append(
+                    {
+                        "name": name,
+                        "type": spec_type,
+                        "n_freq": len(spec.freq) if hasattr(spec, "freq") else None,
+                    }
+                )
+
+            return self.create_result(
+                success=True,
+                data=summaries,
+                message=f"Found {len(summaries)} spectrum/spectra",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Listing spectra")
+
+    def delete_spectrum(self, name: str) -> Dict[str, Any]:
+        """Delete a spectrum from state."""
+        try:
+            if not self.state.has_spectrum_data(name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"Spectrum '{name}' not found",
+                    error=None,
+                )
+
+            self.state.remove_spectrum_data(name)
+
+            return self.create_result(
+                success=True,
+                data={"name": name},
+                message=f"Spectrum '{name}' deleted",
+            )
+
+        except Exception as e:
+            return self.handle_error(e, "Deleting spectrum", name=name)
diff --git a/python-backend/services/state_manager.py b/python-backend/services/state_manager.py
new file mode 100644
index 0000000..e7abb24
--- /dev/null
+++ b/python-backend/services/state_manager.py
@@ -0,0 +1,296 @@
+"""
+State management for Stingray Explorer backend.
+
+Manages loaded data including EventLists, Lightcurves, and analysis results.
+"""
+
+import gc
+import logging
+from typing import Any, Dict, List, Optional, Tuple
+import threading
+
+logger = logging.getLogger(__name__)
+
+
+def _force_garbage_collection() -> int:
+    """
+    Force garbage collection across all generations.
+
+    This is more thorough than a single gc.collect() call.
+    See: https://docs.python.org/3/library/gc.html
+
+    Returns:
+        Total number of unreachable objects collected
+    """
+    collected = 0
+    # Collect all generations (0, 1, 2) for thorough cleanup
+    for generation in range(3):
+        collected += gc.collect(generation)
+    return collected
+
+
+class StateManager:
+    """
+    Thread-safe state manager for Stingray Explorer.
+
+    Manages the application state including loaded event lists,
+    lightcurves, and analysis results.
+    """
+
+    def __init__(self):
+        """Initialize the state manager."""
+        self._event_data: Dict[str, Any] = {}
+        self._lightcurve_data: Dict[str, Any] = {}
+        self._spectrum_data: Dict[str, Any] = {}
+        self._analysis_results: Dict[str, Any] = {}
+        self._lock = threading.RLock()
+
+    # Event data methods
+    def add_event_data(self, name: str, event_list: Any) -> None:
+        """
+        Add an event list to state.
+
+        Args:
+            name: Unique name for the event list
+            event_list: Stingray EventList object
+        """
+        with self._lock:
+            self._event_data[name] = event_list
+
+    def get_event_data(self, name: Optional[str] = None) -> Any:
+        """
+        Get event list(s) from state.
+
+        Args:
+            name: Name of specific event list, or None for all
+
+        Returns:
+            Single EventList if name provided, otherwise list of (name, event_list) tuples
+        """
+        with self._lock:
+            if name is not None:
+                return self._event_data.get(name)
+            return list(self._event_data.items())
+
+    def has_event_data(self, name: str) -> bool:
+        """Check if an event list with given name exists."""
+        with self._lock:
+            return name in self._event_data
+
+    def remove_event_data(self, name: str) -> bool:
+        """
+        Remove an event list from state and free memory.
+
+        Args:
+            name: Name of the event list to remove
+
+        Returns:
+            True if removed, False if not found
+        """
+        with self._lock:
+            if name in self._event_data:
+                # Pop the object from dict (removes reference from dict)
+                event_list = self._event_data.pop(name)
+                # Explicitly set internal arrays to None to help GC
+                # This breaks any internal references
+                if hasattr(event_list, 'time'):
+                    event_list.time = None
+                if hasattr(event_list, 'energy'):
+                    event_list.energy = None
+                if hasattr(event_list, 'pi'):
+                    event_list.pi = None
+                if hasattr(event_list, 'gti'):
+                    event_list.gti = None
+                # Delete the local reference
+                del event_list
+                # Force garbage collection
+                collected = _force_garbage_collection()
+                logger.debug(f"Removed event list '{name}', collected {collected} objects")
+                return True
+            return False
+
+    def list_event_names(self) -> List[str]:
+        """Get list of all event list names."""
+        with self._lock:
+            return list(self._event_data.keys())
+
+    def clear_event_data(self) -> int:
+        """
+        Clear all event lists from state and free memory.
+
+        Returns:
+            Number of event lists cleared
+        """
+        with self._lock:
+            count = len(self._event_data)
+            # Clear internal arrays for each event list to help GC
+            for event_list in self._event_data.values():
+                if hasattr(event_list, 'time'):
+                    event_list.time = None
+                if hasattr(event_list, 'energy'):
+                    event_list.energy = None
+                if hasattr(event_list, 'pi'):
+                    event_list.pi = None
+                if hasattr(event_list, 'gti'):
+                    event_list.gti = None
+            # Clear the dictionary
+            self._event_data.clear()
+            # Force garbage collection
+            collected = _force_garbage_collection()
+            logger.debug(f"Cleared {count} event lists, collected {collected} objects")
+            return count
+
+    # Lightcurve data methods
+    def add_lightcurve_data(self, name: str, lightcurve: Any) -> None:
+        """
+        Add a lightcurve to state.
+
+        Args:
+            name: Unique name for the lightcurve
+            lightcurve: Stingray Lightcurve object
+        """
+        with self._lock:
+            self._lightcurve_data[name] = lightcurve
+
+    def get_lightcurve_data(self, name: Optional[str] = None) -> Any:
+        """
+        Get lightcurve(s) from state.
+
+        Args:
+            name: Name of specific lightcurve, or None for all
+
+        Returns:
+            Single Lightcurve if name provided, otherwise list of (name, lightcurve) tuples
+        """
+        with self._lock:
+            if name is not None:
+                return self._lightcurve_data.get(name)
+            return list(self._lightcurve_data.items())
+
+    def has_lightcurve_data(self, name: str) -> bool:
+        """Check if a lightcurve with given name exists."""
+        with self._lock:
+            return name in self._lightcurve_data
+
+    def remove_lightcurve_data(self, name: str) -> bool:
+        """Remove a lightcurve from state and free memory."""
+        with self._lock:
+            if name in self._lightcurve_data:
+                lc = self._lightcurve_data.pop(name)
+                # Clear numpy arrays
+                if hasattr(lc, 'time'):
+                    lc.time = None
+                if hasattr(lc, 'counts'):
+                    lc.counts = None
+                if hasattr(lc, 'count_err'):
+                    lc.count_err = None
+                del lc
+                _force_garbage_collection()
+                return True
+            return False
+
+    def list_lightcurve_names(self) -> List[str]:
+        """Get list of all lightcurve names."""
+        with self._lock:
+            return list(self._lightcurve_data.keys())
+
+    # Spectrum data methods
+    def add_spectrum_data(self, name: str, spectrum: Any) -> None:
+        """Add a spectrum to state."""
+        with self._lock:
+            self._spectrum_data[name] = spectrum
+
+    def get_spectrum_data(self, name: Optional[str] = None) -> Any:
+        """Get spectrum(s) from state."""
+        with self._lock:
+            if name is not None:
+                return self._spectrum_data.get(name)
+            return list(self._spectrum_data.items())
+
+    def has_spectrum_data(self, name: str) -> bool:
+        """Check if a spectrum with given name exists."""
+        with self._lock:
+            return name in self._spectrum_data
+
+    def remove_spectrum_data(self, name: str) -> bool:
+        """Remove a spectrum from state and free memory."""
+        with self._lock:
+            if name in self._spectrum_data:
+                spectrum = self._spectrum_data.pop(name)
+                # Clear numpy arrays (Powerspectrum/Crossspectrum have freq, power, etc.)
+                if hasattr(spectrum, 'freq'):
+                    spectrum.freq = None
+                if hasattr(spectrum, 'power'):
+                    spectrum.power = None
+                if hasattr(spectrum, 'power_err'):
+                    spectrum.power_err = None
+                del spectrum
+                _force_garbage_collection()
+                return True
+            return False
+
+    def list_spectrum_names(self) -> List[str]:
+        """Get list of all spectrum names."""
+        with self._lock:
+            return list(self._spectrum_data.keys())
+
+    # Analysis results methods
+    def add_analysis_result(self, name: str, result: Any) -> None:
+        """Add an analysis result to state."""
+        with self._lock:
+            self._analysis_results[name] = result
+
+    def get_analysis_result(self, name: Optional[str] = None) -> Any:
+        """Get analysis result(s) from state."""
+        with self._lock:
+            if name is not None:
+                return self._analysis_results.get(name)
+            return list(self._analysis_results.items())
+
+    def remove_analysis_result(self, name: str) -> bool:
+        """Remove an analysis result from state."""
+        with self._lock:
+            if name in self._analysis_results:
+                del self._analysis_results[name]
+                return True
+            return False
+
+    # Utility methods
+    def clear_all(self) -> None:
+        """Clear all state data and free memory."""
+        with self._lock:
+            # Clear internal arrays for all objects to help GC
+            for event_list in self._event_data.values():
+                for attr in ['time', 'energy', 'pi', 'gti']:
+                    if hasattr(event_list, attr):
+                        setattr(event_list, attr, None)
+
+            for lc in self._lightcurve_data.values():
+                for attr in ['time', 'counts', 'count_err']:
+                    if hasattr(lc, attr):
+                        setattr(lc, attr, None)
+
+            for spectrum in self._spectrum_data.values():
+                for attr in ['freq', 'power', 'power_err']:
+                    if hasattr(spectrum, attr):
+                        setattr(spectrum, attr, None)
+
+            # Clear all dictionaries
+            self._event_data.clear()
+            self._lightcurve_data.clear()
+            self._spectrum_data.clear()
+            self._analysis_results.clear()
+
+            # Force garbage collection
+            collected = _force_garbage_collection()
+            logger.debug(f"Cleared all state data, collected {collected} objects")
+
+    def get_summary(self) -> Dict[str, int]:
+        """Get a summary of stored data counts."""
+        with self._lock:
+            return {
+                "event_lists": len(self._event_data),
+                "lightcurves": len(self._lightcurve_data),
+                "spectra": len(self._spectrum_data),
+                "analysis_results": len(self._analysis_results),
+            }
diff --git a/python-backend/services/timing_service.py b/python-backend/services/timing_service.py
new file mode 100644
index 0000000..9bb0a89
--- /dev/null
+++ b/python-backend/services/timing_service.py
@@ -0,0 +1,450 @@
+"""
+Timing service for timing analysis operations.
+
+Handles bispectrum, power colors, and other timing analysis.
+"""
+
+from typing import Any, Dict, Optional
+
+import numpy as np
+from stingray import Bispectrum, DynamicalPowerspectrum
+
+from .base_service import BaseService
+
+
+def _finite_list(arr) -> list:
+    """Convert a float array to a list, replacing non-finite values with None."""
+    values = np.asarray(arr, dtype=float)
+    if np.isfinite(values).all():
+        return values.tolist()
+    return [float(v) if np.isfinite(v) else None for v in values]
+
+
+def _segment_size_error(segment_size: float, dt: float) -> Optional[str]:
+    """Human-readable rejection for segment sizes that stingray fails on cryptically.
+
+    Needs at least 3 time bins per segment to produce a non-empty spectrum.
+    """
+    if segment_size / dt < 3:
+        return (
+            f"segment_size ({segment_size}s) must be at least 3x dt ({dt}s) "
+            "to produce a non-empty spectrum"
+        )
+    return None
+
+
+def _overlap_error(
+    events1, events2, segment_size: Optional[float] = None
+) -> Optional[str]:
+    """Readable rejection when two event lists share no time overlap.
+
+    Optional segment_size check: if provided and the overlap is shorter than
+    one segment, stingray will produce zero segments (cryptic error), so we
+    reject early with a human-readable message.
+    """
+    if len(events1.time) == 0 or len(events2.time) == 0:
+        return "one of the event lists contains no events"
+    start = max(float(events1.time[0]), float(events2.time[0]))
+    stop = min(float(events1.time[-1]), float(events2.time[-1]))
+    if stop <= start:
+        return (
+            "the two event lists have no overlapping time range "
+            f"({events1.time[0]:.1f}-{events1.time[-1]:.1f}s vs "
+            f"{events2.time[0]:.1f}-{events2.time[-1]:.1f}s)"
+        )
+    if segment_size is not None and (stop - start) < segment_size:
+        return (
+            f"the overlapping time range ({stop - start:.1f}s) is shorter than "
+            f"the segment size ({segment_size}s)"
+        )
+    return None
+
+
+class TimingService(BaseService):
+    """
+    Service for timing analysis operations.
+
+    Handles bispectrum, power colors, and higher-order timing analysis.
+    """
+
+    def create_bispectrum(
+        self,
+        event_list_name: str,
+        dt: float,
+        maxlag: int = 25,
+        scale: str = "unbiased",
+        window: str = "uniform",
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Create a bispectrum from an EventList.
+
+        The bispectrum is used to detect non-linear interactions and
+        non-Gaussian features in the data.
+
+        Args:
+            event_list_name: Name of the EventList in state
+            dt: Time binning in seconds
+            maxlag: Maximum lag for bispectrum calculation
+            scale: Scaling type ("biased" or "unbiased")
+            window: Window function type
+            output_name: Optional name to save the result
+
+        Returns:
+            Result dictionary with bispectrum data
+        """
+        try:
+            if not self.state.has_event_data(event_list_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_name}' not found",
+                    error=None,
+                )
+
+            event_list = self.state.get_event_data(event_list_name)
+            lc = event_list.to_lc(dt=dt)
+            bs = Bispectrum(lc, maxlag=maxlag, scale=scale, window=window)
+
+            if output_name:
+                self.state.add_analysis_result(output_name, bs)
+
+            bs_data = {
+                "name": output_name,
+                "freq": bs.freq.tolist(),
+                "lags": bs.lags.tolist(),
+                "bispec_mag": bs.bispec_mag.tolist(),
+                "bispec_phase": bs.bispec_phase.tolist(),
+                # cum3 omitted from the payload — large and unused by the UI;
+                # recompute server-side if ever needed.
+                "maxlag": maxlag,
+                "scale": scale,
+                "window": window,
+            }
+
+            return self.create_result(
+                success=True,
+                data=bs_data,
+                message=f"Bispectrum created (maxlag={maxlag})",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Creating bispectrum",
+                event_list=event_list_name,
+                dt=dt,
+                maxlag=maxlag,
+            )
+
+    def calculate_power_colors(
+        self,
+        event_list_name: str,
+        dt: float,
+        segment_size: float,
+        freq_ranges: Dict[str, tuple],
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Calculate power colors from frequency bands.
+
+        Power colors are ratios of band-mean power in different frequency bands,
+        useful for source classification and state analysis.
+
+        Args:
+            event_list_name: Name of the EventList in state
+            dt: Time binning in seconds
+            segment_size: Segment size in seconds
+            freq_ranges: Dictionary of frequency ranges
+            output_name: Optional name to save the result
+
+        Returns:
+            Result dictionary with power colors
+        """
+        try:
+            if not self.state.has_event_data(event_list_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_name}' not found",
+                    error=None,
+                )
+
+            seg_error = _segment_size_error(segment_size, dt)
+            if seg_error:
+                return self.create_result(
+                    success=False, data=None, message=seg_error, error=None
+                )
+
+            event_list = self.state.get_event_data(event_list_name)
+            lc = event_list.to_lc(dt=dt)
+            dps = DynamicalPowerspectrum(lc, segment_size=segment_size, norm="leahy")
+
+            # Mean power in each frequency band per time segment.
+            # dps.dyn_ps has shape (n_freq, n_time); mask along axis 0 (freq).
+            power_colors = {}
+            for band_name, (f_min, f_max) in freq_ranges.items():
+                mask = (dps.freq >= f_min) & (dps.freq < f_max)
+                band_mean_power = dps.dyn_ps[mask, :].mean(axis=0)
+                power_colors[band_name] = _finite_list(band_mean_power)
+
+            result_data = {
+                "name": output_name,
+                "power_colors": power_colors,
+                "time": dps.time.astype(float).tolist(),
+                "freq_ranges": freq_ranges,
+            }
+
+            if output_name:
+                self.state.add_analysis_result(output_name, result_data)
+
+            return self.create_result(
+                success=True,
+                data=result_data,
+                message=f"Power colors calculated for {len(freq_ranges)} bands",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Calculating power colors",
+                event_list=event_list_name,
+                dt=dt,
+                segment_size=segment_size,
+            )
+
+    def calculate_time_lags(
+        self,
+        event_list_1_name: str,
+        event_list_2_name: str,
+        dt: float,
+        segment_size: float,
+        freq_range: Optional[tuple] = None,
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Calculate time lags between two event lists.
+
+        Args:
+            event_list_1_name: Name of first EventList
+            event_list_2_name: Name of second EventList
+            dt: Time binning in seconds
+            segment_size: Segment size in seconds
+            freq_range: Optional frequency range to calculate lags for
+            output_name: Optional name to save the result
+
+        Returns:
+            Result dictionary with time lags
+        """
+        try:
+            if not self.state.has_event_data(event_list_1_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_1_name}' not found",
+                    error=None,
+                )
+
+            if not self.state.has_event_data(event_list_2_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_2_name}' not found",
+                    error=None,
+                )
+
+            seg_error = _segment_size_error(segment_size, dt)
+            if seg_error:
+                return self.create_result(
+                    success=False, data=None, message=seg_error, error=None
+                )
+
+            from stingray import AveragedCrossspectrum
+
+            event_list_1 = self.state.get_event_data(event_list_1_name)
+            event_list_2 = self.state.get_event_data(event_list_2_name)
+
+            overlap_error = _overlap_error(
+                event_list_1, event_list_2, segment_size=segment_size
+            )
+            if overlap_error:
+                return self.create_result(
+                    success=False, data=None, message=overlap_error, error=None
+                )
+
+            lc1 = event_list_1.to_lc(dt=dt)
+            lc2 = event_list_2.to_lc(dt=dt)
+
+            cs = AveragedCrossspectrum.from_lightcurve(
+                lc1=lc1,
+                lc2=lc2,
+                segment_size=segment_size,
+                norm="leahy",
+            )
+
+            # Stingray's time_lag() returns (lag, lag_err) for averaged spectra.
+            lag_result = cs.time_lag()
+            if isinstance(lag_result, tuple):
+                time_lags, time_lags_err = lag_result
+            else:
+                time_lags, time_lags_err = lag_result, None
+
+            freq = np.asarray(cs.freq, dtype=float)
+            time_lags = np.real(np.asarray(time_lags))
+            if time_lags_err is not None:
+                time_lags_err = np.real(np.asarray(time_lags_err))
+
+            if time_lags.shape != freq.shape:
+                raise ValueError(
+                    f"Unexpected time_lag() result shape {time_lags.shape}"
+                )
+
+            if freq_range:
+                mask = (freq >= freq_range[0]) & (freq <= freq_range[1])
+                freq = freq[mask]
+                time_lags = time_lags[mask]
+                if time_lags_err is not None:
+                    time_lags_err = time_lags_err[mask]
+
+            result_data = {
+                "name": output_name,
+                "freq": freq.tolist(),
+                "time_lags": _finite_list(time_lags),
+                "time_lags_err": (
+                    _finite_list(time_lags_err) if time_lags_err is not None else None
+                ),
+                "freq_range": freq_range,
+            }
+
+            if output_name:
+                self.state.add_analysis_result(output_name, result_data)
+
+            return self.create_result(
+                success=True,
+                data=result_data,
+                message="Time lags calculated",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Calculating time lags",
+                event_list_1=event_list_1_name,
+                event_list_2=event_list_2_name,
+                dt=dt,
+                segment_size=segment_size,
+            )
+
+    def calculate_coherence(
+        self,
+        event_list_1_name: str,
+        event_list_2_name: str,
+        dt: float,
+        segment_size: float,
+        output_name: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        """
+        Calculate coherence between two event lists.
+
+        Args:
+            event_list_1_name: Name of first EventList
+            event_list_2_name: Name of second EventList
+            dt: Time binning in seconds
+            segment_size: Segment size in seconds
+            output_name: Optional name to save the result
+
+        Returns:
+            Result dictionary with coherence data
+        """
+        try:
+            if not self.state.has_event_data(event_list_1_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_1_name}' not found",
+                    error=None,
+                )
+
+            if not self.state.has_event_data(event_list_2_name):
+                return self.create_result(
+                    success=False,
+                    data=None,
+                    message=f"EventList '{event_list_2_name}' not found",
+                    error=None,
+                )
+
+            seg_error = _segment_size_error(segment_size, dt)
+            if seg_error:
+                return self.create_result(
+                    success=False, data=None, message=seg_error, error=None
+                )
+
+            from stingray import AveragedCrossspectrum
+
+            event_list_1 = self.state.get_event_data(event_list_1_name)
+            event_list_2 = self.state.get_event_data(event_list_2_name)
+
+            overlap_error = _overlap_error(
+                event_list_1, event_list_2, segment_size=segment_size
+            )
+            if overlap_error:
+                return self.create_result(
+                    success=False, data=None, message=overlap_error, error=None
+                )
+
+            lc1 = event_list_1.to_lc(dt=dt)
+            lc2 = event_list_2.to_lc(dt=dt)
+
+            cs = AveragedCrossspectrum.from_lightcurve(
+                lc1=lc1,
+                lc2=lc2,
+                segment_size=segment_size,
+                norm="leahy",
+            )
+
+            # Stingray's coherence() returns (coherence, uncertainty) for
+            # averaged cross spectra (Vaughan & Nowak 1997).
+            coh_result = cs.coherence()
+            if isinstance(coh_result, tuple):
+                coherence_vals, coherence_err = coh_result
+            else:
+                coherence_vals, coherence_err = coh_result, None
+
+            coherence_vals = np.real(np.asarray(coherence_vals))
+            if coherence_vals.shape != np.asarray(cs.freq).shape:
+                raise ValueError(
+                    f"Unexpected coherence() result shape {coherence_vals.shape}"
+                )
+
+            # Uncertainty formula goes negative where coh > 1; report magnitude as the half-width.
+            result_data = {
+                "name": output_name,
+                "freq": cs.freq.tolist(),
+                "coherence": _finite_list(coherence_vals),
+                "coherence_err": (
+                    _finite_list(np.abs(np.real(np.asarray(coherence_err))))
+                    if coherence_err is not None
+                    else None
+                ),
+                "segment_size": segment_size,
+                "n_segments": int(cs.m) if hasattr(cs, "m") else None,
+            }
+
+            if output_name:
+                self.state.add_analysis_result(output_name, result_data)
+
+            return self.create_result(
+                success=True,
+                data=result_data,
+                message="Coherence calculated",
+            )
+
+        except Exception as e:
+            return self.handle_error(
+                e,
+                "Calculating coherence",
+                event_list_1=event_list_1_name,
+                event_list_2=event_list_2_name,
+                dt=dt,
+                segment_size=segment_size,
+            )
diff --git a/python-backend/tests/__init__.py b/python-backend/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/python-backend/tests/conftest.py b/python-backend/tests/conftest.py
new file mode 100644
index 0000000..0f37260
--- /dev/null
+++ b/python-backend/tests/conftest.py
@@ -0,0 +1,38 @@
+"""Shared fixtures for backend service tests.
+
+python-backend is not an installable package (hyphenated dir name), so tests
+add it to sys.path and import the same way main.py does (cwd=python-backend).
+"""
+
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+import numpy as np
+import pytest
+from stingray import EventList
+
+from services.state_manager import StateManager
+
+
+def make_event_list(
+    seed: int, n_events: int = 20000, length: float = 64.0
+) -> EventList:
+    """Deterministic synthetic event list spanning [0, length] seconds."""
+    rng = np.random.default_rng(seed)
+    times = np.sort(rng.uniform(0.0, length, n_events))
+    energy = rng.uniform(0.5, 10.0, n_events)
+    return EventList(time=times, energy=energy, gti=[[0.0, length]])
+
+
+@pytest.fixture()
+def state_manager() -> StateManager:
+    return StateManager()
+
+
+@pytest.fixture()
+def loaded_state(state_manager: StateManager) -> StateManager:
+    state_manager.add_event_data("ev1", make_event_list(1))
+    state_manager.add_event_data("ev2", make_event_list(2))
+    return state_manager
diff --git a/python-backend/tests/test_lightcurve_service.py b/python-backend/tests/test_lightcurve_service.py
new file mode 100644
index 0000000..311741c
--- /dev/null
+++ b/python-backend/tests/test_lightcurve_service.py
@@ -0,0 +1,65 @@
+import json
+
+import pytest
+
+from services.lightcurve_service import LightcurveService
+
+
+def test_decimation_caps_returned_points(loaded_state):
+    svc = LightcurveService(loaded_state)
+    # 64 s span at dt=0.001 -> 64000 bins; cap at 5000 plot points.
+    result = svc.create_lightcurve_from_event_list(
+        "ev1", dt=0.001, output_name="lc_fine", max_points=5000
+    )
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    data = result["data"]
+    assert data["n_bins"] == 64000  # true resolution is reported
+    assert len(data["time"]) <= 5000  # transferred arrays are capped
+    assert data["plot_stride"] == 13  # ceil(64000 / 5000)
+    assert len(data["time"]) == len(data["counts"])
+
+
+def test_no_decimation_below_cap(loaded_state):
+    svc = LightcurveService(loaded_state)
+    result = svc.create_lightcurve_from_event_list(
+        "ev1", dt=1.0, output_name="lc_coarse"
+    )
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    data = result["data"]
+    assert data["plot_stride"] == 1
+    assert len(data["time"]) == data["n_bins"]
+
+
+def test_get_lightcurve_data_decimates(loaded_state):
+    svc = LightcurveService(loaded_state)
+    svc.create_lightcurve_from_event_list("ev1", dt=0.001, output_name="lc_fine2")
+    result = svc.get_lightcurve_data("lc_fine2", max_points=1000)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    assert len(result["data"]["time"]) <= 1000
+    assert result["data"]["plot_stride"] == 64
+
+
+def test_stats_use_full_resolution_and_zero_disables_decimation(loaded_state):
+    svc = LightcurveService(loaded_state)
+    decimated = svc.create_lightcurve_from_event_list(
+        "ev1", dt=0.001, output_name="lc_stats_a", max_points=1000
+    )
+    full = svc.get_lightcurve_data("lc_stats_a", max_points=0)
+    assert full["data"]["plot_stride"] == 1
+    assert len(full["data"]["time"]) == full["data"]["n_bins"]
+    # count_rate_mean must come from the FULL arrays, not the decimated payload
+    assert decimated["data"]["count_rate_mean"] == (
+        sum(full["data"]["counts"]) / (full["data"]["n_bins"] * full["data"]["dt"])
+    )
+
+
+def test_rebin_scales_dt_by_factor(loaded_state):
+    svc = LightcurveService(loaded_state)
+    svc.create_lightcurve_from_event_list("ev1", dt=0.5, output_name="lc_base")
+    result = svc.rebin_lightcurve("lc_base", rebin_factor=2.0, output_name="lc_base_r2")
+    assert result["success"], result
+    assert result["data"]["dt"] == pytest.approx(1.0)  # 2 x 0.5, factor semantics
+    assert "count_rate_mean" in result["data"]
diff --git a/python-backend/tests/test_route_concurrency.py b/python-backend/tests/test_route_concurrency.py
new file mode 100644
index 0000000..602c6b5
--- /dev/null
+++ b/python-backend/tests/test_route_concurrency.py
@@ -0,0 +1,63 @@
+"""Verify analysis routes run blocking work off the event loop.
+
+A handler that calls the synchronous service directly blocks the loop, so a
+concurrent asyncio.sleep cannot complete on time. With asyncio.to_thread, the
+sleep returns at the expected time while the slow computation runs in a thread.
+"""
+
+import asyncio
+import time
+
+import httpx
+import pytest
+
+from services.state_manager import StateManager
+from utils.performance_monitor import PerformanceMonitor
+
+
+@pytest.mark.asyncio
+async def test_lightcurve_create_does_not_block_event_loop(monkeypatch):
+    import services.lightcurve_service as lcs_mod
+    from main import create_app
+
+    def slow_create(self, **kwargs):
+        time.sleep(0.6)
+        return {"success": True, "data": None, "message": "ok", "error": None}
+
+    monkeypatch.setattr(
+        lcs_mod.LightcurveService, "create_lightcurve_from_event_list", slow_create
+    )
+
+    app = create_app()
+    # ASGITransport does not run the lifespan; provide state manually.
+    app.state.state_manager = StateManager()
+    app.state.performance_monitor = PerformanceMonitor()
+
+    transport = httpx.ASGITransport(app=app)
+    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
+        slow_task = asyncio.create_task(
+            client.post(
+                "/api/lightcurve/from-event-list",
+                json={"event_list_name": "x", "dt": 0.1, "output_name": "y"},
+            )
+        )
+        # Start the clock before the first yield so the measurement captures
+        # the block wherever the first scheduler checkpoint lands.
+        t0 = time.monotonic()
+        # Yield control so the slow task can start executing.
+        await asyncio.sleep(0)
+
+        # Measure how long the yield plus a 0.05s sleep actually take.
+        # If the event loop is blocked by the sync service call, control
+        # cannot return until the blocking work finishes (~0.6s later), so
+        # the measured duration will be ~0.65s instead of ~0.05s.
+        await asyncio.sleep(0.05)
+        elapsed = time.monotonic() - t0
+
+        probe = await client.get("/")
+        slow_response = await slow_task
+
+        assert probe.status_code == 200
+        assert slow_response.status_code == 200
+        # Without to_thread the sleep is delayed ~0.55s by the blocked loop.
+        assert elapsed < 0.4, f"event loop was blocked for {elapsed:.2f}s"
diff --git a/python-backend/tests/test_smoke.py b/python-backend/tests/test_smoke.py
new file mode 100644
index 0000000..4b7fd23
--- /dev/null
+++ b/python-backend/tests/test_smoke.py
@@ -0,0 +1,4 @@
+def test_services_import_and_state_works(loaded_state):
+    assert loaded_state.has_event_data("ev1")
+    assert loaded_state.has_event_data("ev2")
+    assert len(loaded_state.get_event_data("ev1").time) == 20000
diff --git a/python-backend/tests/test_spectrum_service.py b/python-backend/tests/test_spectrum_service.py
new file mode 100644
index 0000000..6e4b359
--- /dev/null
+++ b/python-backend/tests/test_spectrum_service.py
@@ -0,0 +1,113 @@
+import json
+
+import numpy as np
+import pytest
+
+from services.spectrum_service import SpectrumService, _finite_list
+
+
+def test_cross_spectrum_is_strict_json_serializable(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_cross_spectrum("ev1", "ev2", dt=0.0625)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)  # complex or NaN values raise here
+    data = result["data"]
+    assert all(isinstance(p, float) for p in data["power"][:10])
+    assert data["power_phase"] is not None
+    assert len(data["power_phase"]) == len(data["power"])
+
+
+def test_averaged_cross_spectrum_is_strict_json_serializable(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_averaged_cross_spectrum(
+        "ev1", "ev2", dt=0.0625, segment_size=8.0
+    )
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    assert result["data"]["power_phase"] is not None
+    assert result["data"]["n_segments"] == 8  # 64 s fixture / 8 s segments
+
+
+def test_power_spectrum_has_null_phase(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_power_spectrum("ev1", dt=0.0625)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    assert result["data"]["power_phase"] is None
+
+
+def test_rebin_of_stored_cross_spectrum_serializes(loaded_state):
+    svc = SpectrumService(loaded_state)
+    created = svc.create_cross_spectrum("ev1", "ev2", dt=0.0625, output_name="cs1")
+    assert created["success"], created
+    rebinned = svc.rebin_spectrum("cs1", rebin_factor=0.1, log=True)
+    assert rebinned["success"], rebinned
+    json.dumps(rebinned, allow_nan=False)
+
+
+def test_finite_list_maps_nonfinite_to_none():
+    assert _finite_list(np.array([1.0, np.nan, np.inf, -np.inf])) == [
+        1.0,
+        None,
+        None,
+        None,
+    ]
+
+
+def test_cross_spectrum_power_is_magnitude(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_cross_spectrum("ev1", "ev2", dt=0.0625, output_name="cs_mag")
+    assert result["success"], result
+    cs = loaded_state.get_spectrum_data("cs_mag")
+    expected_mag = np.abs(np.asarray(cs.power))
+    expected_phase = np.angle(np.asarray(cs.power))
+    assert np.allclose(result["data"]["power"], expected_mag)
+    assert np.allclose(result["data"]["power_phase"], expected_phase)
+
+
+def test_linear_rebin_scales_df_by_factor(loaded_state):
+    svc = SpectrumService(loaded_state)
+    created = svc.create_power_spectrum("ev1", dt=0.0625, output_name="ps_lin")
+    assert created["success"], created
+    base_df = created["data"]["df"]
+    result = svc.rebin_spectrum("ps_lin", rebin_factor=2.0, log=False)
+    assert result["success"], result
+    freq = result["data"]["freq"]
+    new_df = freq[1] - freq[0]
+    assert new_df == pytest.approx(2.0 * base_df, rel=1e-6)
+    assert result["data"]["norm"] == created["data"]["norm"]
+
+
+def test_dynamical_power_spectrum_serializes(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_dynamical_power_spectrum("ev1", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+
+
+def test_averaged_power_spectrum_serializes_with_n_segments(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_averaged_power_spectrum("ev1", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    assert result["data"]["n_segments"] == 8
+
+
+def test_tiny_segment_size_rejected_with_readable_message(loaded_state):
+    svc = SpectrumService(loaded_state)
+    result = svc.create_averaged_power_spectrum("ev1", dt=0.0625, segment_size=0.125)
+    assert not result["success"]
+    assert "3x dt" in result["message"]
+
+
+def test_disjoint_event_lists_rejected_readably(loaded_state):
+    import numpy as np
+    from stingray import EventList
+
+    rng = np.random.default_rng(7)
+    far = np.sort(rng.uniform(1000.0, 1064.0, 5000))
+    loaded_state.add_event_data("ev_far", EventList(time=far, gti=[[1000.0, 1064.0]]))
+    svc = SpectrumService(loaded_state)
+    result = svc.create_cross_spectrum("ev1", "ev_far", dt=0.0625)
+    assert not result["success"]
+    assert "no overlapping time range" in result["message"]
diff --git a/python-backend/tests/test_timing_service.py b/python-backend/tests/test_timing_service.py
new file mode 100644
index 0000000..5f28057
--- /dev/null
+++ b/python-backend/tests/test_timing_service.py
@@ -0,0 +1,164 @@
+import json
+
+import numpy as np
+
+from services.timing_service import TimingService
+
+
+def test_coherence_of_identical_signals_is_one(loaded_state):
+    svc = TimingService(loaded_state)
+    # An event list crossed with itself has coherence == 1 at all frequencies.
+    result = svc.calculate_coherence("ev1", "ev1", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    coh = np.asarray(result["data"]["coherence"], dtype=float)
+    # Identical inputs give raw coherence == 1 exactly; the Ingram-2019 noise-bias
+    # correction can push individual noise-dominated bins above 1 without bound
+    # (~(N/P)^2 / n_bin), so only a coarse cap discriminates against the old
+    # |unnorm_power|^2 bug, whose values were ~1e8.
+    assert np.all(coh < 2.0)
+    assert np.median(coh) > 0.9  # measured 0.993 for this fixture
+
+
+def test_coherence_includes_uncertainty(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.calculate_coherence("ev1", "ev2", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    data = result["data"]
+    assert "coherence_err" in data
+    assert data["coherence_err"] is not None
+    assert len(data["coherence_err"]) == len(data["coherence"])
+
+
+def test_time_lags_include_errors_and_serialize(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.calculate_time_lags("ev1", "ev2", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    data = result["data"]
+    assert "time_lags_err" in data
+    assert len(data["freq"]) == len(data["time_lags"])
+    assert data["time_lags_err"] is not None
+    assert len(data["time_lags_err"]) == len(data["time_lags"])
+
+
+def test_time_lags_freq_range_filters_all_arrays(loaded_state):
+    svc = TimingService(loaded_state)
+    full = svc.calculate_time_lags("ev1", "ev2", dt=0.0625, segment_size=8.0)
+    sub = svc.calculate_time_lags(
+        "ev1", "ev2", dt=0.0625, segment_size=8.0, freq_range=(0.5, 2.0)
+    )
+    assert sub["success"], sub
+    freqs = np.asarray(sub["data"]["freq"], dtype=float)
+    assert freqs.min() >= 0.5
+    assert freqs.max() <= 2.0
+    assert len(sub["data"]["freq"]) < len(full["data"]["freq"])
+    assert len(sub["data"]["time_lags"]) == len(sub["data"]["freq"])
+    if sub["data"]["time_lags_err"] is not None:
+        assert len(sub["data"]["time_lags_err"]) == len(sub["data"]["freq"])
+
+
+def test_power_colors_serializes(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.calculate_power_colors(
+        "ev1",
+        dt=0.0625,
+        segment_size=8.0,
+        freq_ranges={
+            "A": (0.125, 0.5),
+            "B": (0.5, 1.0),
+            "C": (1.0, 2.0),
+            "D": (2.0, 4.0),
+        },
+    )
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    data = result["data"]
+    assert len(data["time"]) == 8  # 64 s / 8 s segments
+    for band in data["power_colors"].values():
+        assert len(band) == len(data["time"])
+
+
+def test_bispectrum_serializes(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.create_bispectrum("ev1", dt=0.25, maxlag=10)
+    assert result["success"], result
+    json.dumps(result, allow_nan=False)
+    assert "cum3" not in result["data"]
+
+
+def test_time_lag_of_identical_signals_is_zero(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.calculate_time_lags("ev1", "ev1", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    lags = np.asarray([v for v in result["data"]["time_lags"] if v is not None])
+    assert np.max(np.abs(lags)) < 1e-10
+
+
+def test_tiny_segment_size_rejected_for_coherence(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.calculate_coherence("ev1", "ev2", dt=0.0625, segment_size=0.125)
+    assert not result["success"]
+    assert "3x dt" in result["message"]
+
+
+def test_coherence_of_independent_signals_is_low(loaded_state):
+    svc = TimingService(loaded_state)
+    result = svc.calculate_coherence("ev1", "ev2", dt=0.0625, segment_size=8.0)
+    assert result["success"], result
+    coh = np.asarray(
+        [v for v in result["data"]["coherence"] if v is not None], dtype=float
+    )
+    assert np.median(coh) < 0.5  # measured ~0.1 for independent fixtures
+
+
+def test_disjoint_event_lists_rejected_for_coherence(loaded_state):
+    rng = np.random.default_rng(8)
+    far = np.sort(rng.uniform(1000.0, 1064.0, 5000))
+    from stingray import EventList
+
+    loaded_state.add_event_data("ev_far", EventList(time=far, gti=[[1000.0, 1064.0]]))
+    svc = TimingService(loaded_state)
+    result = svc.calculate_coherence("ev1", "ev_far", dt=0.0625, segment_size=8.0)
+    assert not result["success"]
+    assert "no overlapping time range" in result["message"]
+
+
+def test_time_lag_sign_convention_for_shifted_signal(loaded_state):
+    # Pin the sign convention the UI will document: ev_shifted = ev1 delayed by 0.1 s.
+    from stingray import EventList
+
+    ev1 = loaded_state.get_event_data("ev1")
+    # Keep the same GTI as ev1 so both light curves share one bin grid; with
+    # gti=[[0.1, 64.1]] the GTI intersection misaligns the grids (0.1 is not a
+    # multiple of dt) and the effective shift becomes 2 bins = 0.125 s.
+    shifted_times = ev1.time + 0.1
+    shifted_times = shifted_times[shifted_times < 64.0]
+    shifted = EventList(time=np.sort(shifted_times), gti=[[0.0, 64.0]])
+    loaded_state.add_event_data("ev_shifted", shifted)
+    svc = TimingService(loaded_state)
+    result = svc.calculate_time_lags(
+        "ev1", "ev_shifted", dt=0.0625, segment_size=8.0, freq_range=(0.25, 2.0)
+    )
+    assert result["success"], result
+    lags = np.asarray([v for v in result["data"]["time_lags"] if v is not None])
+    median_lag = float(np.median(lags))
+    # Magnitude must recover the 0.1 s shift well below the phase-wrap limit (5 Hz).
+    assert abs(abs(median_lag) - 0.1) < 0.02
+    # Observed: median_lag = -0.095 for channel 2 delayed by 0.1 s → stingray
+    # convention: positive lag means channel 2 (second list) leads channel 1;
+    # a delayed second channel yields negative lags.
+
+
+def test_short_overlap_rejected_for_coherence(loaded_state):
+    # ev1 covers 0–64 s; ev_partial covers 56–120 s → 8 s overlap < 16 s segment.
+    from stingray import EventList
+
+    rng = np.random.default_rng(99)
+    partial_times = np.sort(rng.uniform(56.0, 120.0, 5000))
+    ev_partial = EventList(time=partial_times, gti=[[56.0, 120.0]])
+    loaded_state.add_event_data("ev_partial", ev_partial)
+    svc = TimingService(loaded_state)
+    result = svc.calculate_coherence("ev1", "ev_partial", dt=0.0625, segment_size=16.0)
+    assert not result["success"]
+    assert "shorter than the segment size" in result["message"]
diff --git a/python-backend/utils/__init__.py b/python-backend/utils/__init__.py
new file mode 100644
index 0000000..fe2a948
--- /dev/null
+++ b/python-backend/utils/__init__.py
@@ -0,0 +1,6 @@
+"""Utilities for Stingray Explorer backend."""
+
+from .performance_monitor import PerformanceMonitor
+from .error_handler import ErrorHandler
+
+__all__ = ["PerformanceMonitor", "ErrorHandler"]
diff --git a/python-backend/utils/error_handler.py b/python-backend/utils/error_handler.py
new file mode 100644
index 0000000..da34a6d
--- /dev/null
+++ b/python-backend/utils/error_handler.py
@@ -0,0 +1,140 @@
+"""
+Error handling utilities for Stingray Explorer.
+"""
+
+import traceback
+from typing import Any, Dict, Optional, Tuple
+
+
+class ErrorHandler:
+    """
+    Centralized error handling for Stingray Explorer backend.
+
+    Provides consistent error formatting and user-friendly messages.
+    """
+
+    # Mapping of common exception types to user-friendly messages
+    ERROR_MESSAGES: Dict[type, str] = {
+        FileNotFoundError: "The specified file could not be found",
+        PermissionError: "Permission denied when accessing the file",
+        ValueError: "Invalid value provided",
+        TypeError: "Invalid data type",
+        MemoryError: "Operation requires more memory than available",
+        IOError: "Error reading or writing data",
+        KeyError: "Required data field not found",
+    }
+
+    @classmethod
+    def handle_error(
+        cls,
+        exception: Exception,
+        context: str = "",
+        include_traceback: bool = False,
+        **context_data: Any,
+    ) -> Tuple[str, str]:
+        """
+        Handle an exception and return user-friendly and technical messages.
+
+        Args:
+            exception: The exception that occurred
+            context: Description of the operation that failed
+            include_traceback: Whether to include full traceback in technical message
+            **context_data: Additional context data to include
+
+        Returns:
+            Tuple of (user_friendly_message, technical_message)
+
+        Example:
+            >>> try:
+            ...     data = load_file(path)
+            ... except Exception as e:
+            ...     user_msg, tech_msg = ErrorHandler.handle_error(
+            ...         e, context="Loading file", file_path=path
+            ...     )
+        """
+        # Get exception type and message
+        exc_type = type(exception)
+        exc_message = str(exception)
+
+        # Create user-friendly message
+        base_message = cls.ERROR_MESSAGES.get(
+            exc_type, f"An error occurred: {exc_type.__name__}"
+        )
+
+        if context:
+            user_message = f"{context}: {base_message}"
+        else:
+            user_message = base_message
+
+        if exc_message and exc_message != str(exc_type):
+            user_message = f"{user_message}. {exc_message}"
+
+        # Create technical message
+        tech_parts = [
+            f"Exception: {exc_type.__name__}",
+            f"Message: {exc_message}",
+        ]
+
+        if context:
+            tech_parts.append(f"Context: {context}")
+
+        if context_data:
+            context_str = ", ".join(f"{k}={v}" for k, v in context_data.items())
+            tech_parts.append(f"Data: {context_str}")
+
+        if include_traceback:
+            tech_parts.append(f"Traceback:\n{traceback.format_exc()}")
+
+        technical_message = " | ".join(tech_parts)
+
+        return user_message, technical_message
+
+    @classmethod
+    def format_validation_error(
+        cls, field: str, expected: str, received: Any
+    ) -> Tuple[str, str]:
+        """
+        Format a validation error message.
+
+        Args:
+            field: The field that failed validation
+            expected: Description of expected value
+            received: The actual value received
+
+        Returns:
+            Tuple of (user_friendly_message, technical_message)
+        """
+        user_message = f"Invalid {field}: expected {expected}, got {type(received).__name__}"
+        technical_message = f"Validation error: {field}={received!r}, expected {expected}"
+
+        return user_message, technical_message
+
+    @classmethod
+    def create_error_response(
+        cls,
+        exception: Exception,
+        context: str = "",
+        **context_data: Any,
+    ) -> Dict[str, Any]:
+        """
+        Create a standardized error response dictionary.
+
+        Args:
+            exception: The exception that occurred
+            context: Description of the operation that failed
+            **context_data: Additional context data
+
+        Returns:
+            Error response dictionary
+        """
+        user_msg, tech_msg = cls.handle_error(
+            exception, context=context, **context_data
+        )
+
+        return {
+            "success": False,
+            "data": None,
+            "message": user_msg,
+            "error": tech_msg,
+            "error_type": type(exception).__name__,
+        }
diff --git a/python-backend/utils/log_stream.py b/python-backend/utils/log_stream.py
new file mode 100644
index 0000000..9229df8
--- /dev/null
+++ b/python-backend/utils/log_stream.py
@@ -0,0 +1,313 @@
+"""
+Log streaming utilities for real-time log delivery via SSE.
+
+This module captures Python logging output and warnings, then streams them
+to connected frontend clients through Server-Sent Events (SSE).
+"""
+
+import asyncio
+import logging
+import queue
+import warnings
+from collections import deque
+from datetime import datetime, timezone
+from typing import Any, AsyncGenerator, Callable, Optional
+
+# Loggers to skip (reduce noise)
+SKIP_LOGGERS = frozenset({
+    "uvicorn.access",
+    "uvicorn.error",
+})
+
+
+class StreamingLogHandler(logging.Handler):
+    """
+    Custom logging handler that pushes log records to a queue for SSE streaming.
+
+    Maps Python log levels to frontend-compatible levels:
+    - DEBUG -> debug
+    - INFO -> info
+    - WARNING -> warn
+    - ERROR/CRITICAL -> error
+    """
+
+    LEVEL_MAP = {
+        logging.DEBUG: "debug",
+        logging.INFO: "info",
+        logging.WARNING: "warn",
+        logging.ERROR: "error",
+        logging.CRITICAL: "error",
+    }
+
+    def __init__(
+        self,
+        log_queue: "queue.Queue[dict[str, Any]]",
+        manager: "LogStreamManager",
+    ) -> None:
+        """
+        Initialize the handler with a queue for log entries.
+
+        Args:
+            log_queue: Thread-safe queue to push log entries to
+            manager: The LogStreamManager instance for history storage
+        """
+        super().__init__()
+        self._queue = log_queue
+        self._manager = manager
+
+    def emit(self, record: logging.LogRecord) -> None:
+        """
+        Emit a log record by pushing it to the queue and storing in history.
+
+        Args:
+            record: The log record to emit
+        """
+        # Skip noisy loggers
+        if record.name in SKIP_LOGGERS:
+            return
+
+        try:
+            # Map level to frontend-compatible string
+            level = self.LEVEL_MAP.get(record.levelno, "info")
+
+            # Format the message
+            message = self.format(record)
+
+            # Create log entry
+            log_entry = {
+                "type": "log",
+                "timestamp": datetime.now(timezone.utc).isoformat(),
+                "level": level,
+                "source": "python",
+                "logger": record.name,
+                "message": message,
+            }
+
+            # Store in history for replay to new SSE clients
+            self._manager._add_to_history(log_entry)
+
+            # Non-blocking put (drop if queue is full)
+            try:
+                self._queue.put_nowait(log_entry)
+            except queue.Full:
+                # Queue full, drop oldest entry and try again
+                try:
+                    self._queue.get_nowait()
+                    self._queue.put_nowait(log_entry)
+                except queue.Empty:
+                    pass
+
+        except Exception:
+            # Don't let logging errors crash the app
+            self.handleError(record)
+
+
+class LogStreamManager:
+    """
+    Manages log streaming infrastructure for SSE delivery.
+
+    Captures Python logging output and warnings, buffering them in a queue
+    for delivery to connected SSE clients. Supports multiple concurrent
+    connections and handles cleanup on shutdown.
+    """
+
+    def __init__(self, max_queue_size: int = 1000, max_history: int = 100) -> None:
+        """
+        Initialize the log stream manager.
+
+        Args:
+            max_queue_size: Maximum number of log entries to buffer
+            max_history: Maximum number of log entries to keep in history for replay
+        """
+        self._queue: queue.Queue[dict[str, Any]] = queue.Queue(maxsize=max_queue_size)
+        self._history: deque[dict[str, Any]] = deque(maxlen=max_history)
+        self._handler: Optional[StreamingLogHandler] = None
+        self._original_showwarning: Optional[Callable[..., None]] = None
+        self._installed = False
+        self._active_connections = 0
+
+    def install(self, log_level: int = logging.DEBUG) -> None:
+        """
+        Install the log handler and warning capture.
+
+        Args:
+            log_level: Minimum log level to capture (default: DEBUG)
+        """
+        if self._installed:
+            return
+
+        # Create and configure handler
+        self._handler = StreamingLogHandler(self._queue, self)
+        self._handler.setLevel(log_level)
+
+        # Set formatter
+        formatter = logging.Formatter("%(message)s")
+        self._handler.setFormatter(formatter)
+
+        # Add to root logger
+        root_logger = logging.getLogger()
+        root_logger.addHandler(self._handler)
+
+        # Capture warnings
+        self._original_showwarning = warnings.showwarning
+        warnings.showwarning = self._capture_warning
+
+        self._installed = True
+        logging.getLogger(__name__).info("Log streaming installed")
+
+    def uninstall(self) -> None:
+        """Remove the log handler and restore original warning handling."""
+        if not self._installed:
+            return
+
+        # Remove handler from root logger
+        if self._handler:
+            root_logger = logging.getLogger()
+            root_logger.removeHandler(self._handler)
+            self._handler = None
+
+        # Restore original showwarning
+        if self._original_showwarning:
+            warnings.showwarning = self._original_showwarning
+            self._original_showwarning = None
+
+        # Clear the queue
+        while not self._queue.empty():
+            try:
+                self._queue.get_nowait()
+            except queue.Empty:
+                break
+
+        self._installed = False
+        logging.getLogger(__name__).info("Log streaming uninstalled")
+
+    def _add_to_history(self, log_entry: dict[str, Any]) -> None:
+        """
+        Add a log entry to the history buffer for replay to new SSE clients.
+
+        Args:
+            log_entry: The log entry to store
+        """
+        self._history.append(log_entry)
+
+    def _capture_warning(
+        self,
+        message: Warning | str,
+        category: type[Warning],
+        filename: str,
+        lineno: int,
+        file: Any = None,
+        line: str | None = None,
+    ) -> None:
+        """
+        Capture warnings and route them to the log stream.
+
+        Args:
+            message: The warning message
+            category: The warning category class
+            filename: The file where the warning occurred
+            lineno: The line number
+            file: File to write to (ignored, we capture it)
+            line: Source code line (optional)
+        """
+        # Format warning message
+        warning_msg = f"{category.__name__}: {message}"
+        if filename and lineno:
+            warning_msg = f"{filename}:{lineno}: {warning_msg}"
+
+        # Create log entry
+        log_entry = {
+            "type": "log",
+            "timestamp": datetime.now(timezone.utc).isoformat(),
+            "level": "warn",
+            "source": "python",
+            "logger": f"warnings.{category.__name__}",
+            "message": warning_msg,
+        }
+
+        # Store in history for replay to new SSE clients
+        self._add_to_history(log_entry)
+
+        # Push to queue
+        try:
+            self._queue.put_nowait(log_entry)
+        except queue.Full:
+            try:
+                self._queue.get_nowait()
+                self._queue.put_nowait(log_entry)
+            except queue.Empty:
+                pass
+
+        # Also call original handler if it exists (for console output)
+        if self._original_showwarning:
+            self._original_showwarning(message, category, filename, lineno, file, line)
+
+    async def stream_logs(
+        self,
+        heartbeat_interval: float = 30.0,
+    ) -> AsyncGenerator[dict[str, Any], None]:
+        """
+        Async generator that yields log entries for SSE streaming.
+
+        Replays history to new connections, then continues with live stream.
+        Includes periodic heartbeat events to keep the connection alive.
+
+        Args:
+            heartbeat_interval: Seconds between heartbeat events (default: 30)
+
+        Yields:
+            Log entry dictionaries ready for JSON serialization
+        """
+        # Clear history if this is a new session (no active connections)
+        # This prevents stale logs from previous Electron sessions being replayed
+        if self._active_connections == 0:
+            self._history.clear()
+
+        self._active_connections += 1
+        last_heartbeat = asyncio.get_event_loop().time()
+
+        try:
+            # Replay history for new connections
+            for log_entry in list(self._history):
+                yield log_entry
+
+            while True:
+                # Check for log entries in the queue
+                try:
+                    log_entry = self._queue.get_nowait()
+                    yield log_entry
+                    last_heartbeat = asyncio.get_event_loop().time()
+                except queue.Empty:
+                    # No logs available, check if we need a heartbeat
+                    current_time = asyncio.get_event_loop().time()
+                    if current_time - last_heartbeat >= heartbeat_interval:
+                        yield {
+                            "type": "heartbeat",
+                            "timestamp": datetime.now(timezone.utc).isoformat(),
+                        }
+                        last_heartbeat = current_time
+
+                # Small sleep to yield control and not spin
+                await asyncio.sleep(0.1)
+
+        finally:
+            self._active_connections -= 1
+
+    @property
+    def is_installed(self) -> bool:
+        """Check if log streaming is currently installed."""
+        return self._installed
+
+    @property
+    def active_connections(self) -> int:
+        """Get the number of active SSE connections."""
+        return self._active_connections
+
+    @property
+    def queue_size(self) -> int:
+        """Get the current queue size."""
+        return self._queue.qsize()
+
+
+# Global singleton instance
+log_stream_manager = LogStreamManager()
diff --git a/python-backend/utils/performance_monitor.py b/python-backend/utils/performance_monitor.py
new file mode 100644
index 0000000..845a7db
--- /dev/null
+++ b/python-backend/utils/performance_monitor.py
@@ -0,0 +1,180 @@
+"""
+Performance monitoring utilities for Stingray Explorer.
+"""
+
+import time
+import psutil
+from contextlib import contextmanager
+from dataclasses import dataclass, field
+from typing import Any, Dict, Generator, List, Optional
+
+
+@dataclass
+class OperationMetrics:
+    """Metrics for a single operation."""
+
+    name: str
+    duration_ms: float
+    memory_mb: float
+    success: bool
+    context: Dict[str, Any] = field(default_factory=dict)
+    timestamp: float = field(default_factory=time.time)
+
+
+class PerformanceMonitor:
+    """
+    Monitor performance of operations in the Stingray Explorer backend.
+
+    Tracks execution time, memory usage, and operation success rates.
+    """
+
+    def __init__(self, max_history: int = 100):
+        """
+        Initialize the performance monitor.
+
+        Args:
+            max_history: Maximum number of operations to keep in history
+        """
+        self.max_history = max_history
+        self.history: List[OperationMetrics] = []
+        self._process = psutil.Process()
+
+    @contextmanager
+    def track_operation(
+        self, name: str, **context: Any
+    ) -> Generator[None, None, None]:
+        """
+        Context manager to track an operation's performance.
+
+        Args:
+            name: Name of the operation
+            **context: Additional context to log with the metrics
+
+        Example:
+            >>> with monitor.track_operation("load_file", file_path="/data/obs.evt"):
+            ...     data = load_file("/data/obs.evt")
+        """
+        start_time = time.perf_counter()
+        start_memory = self._get_memory_mb()
+        success = True
+
+        try:
+            yield
+        except Exception:
+            success = False
+            raise
+        finally:
+            end_time = time.perf_counter()
+            end_memory = self._get_memory_mb()
+
+            metrics = OperationMetrics(
+                name=name,
+                duration_ms=(end_time - start_time) * 1000,
+                memory_mb=end_memory - start_memory,
+                success=success,
+                context=context,
+            )
+
+            self._add_to_history(metrics)
+
+    def _get_memory_mb(self) -> float:
+        """Get current memory usage in megabytes."""
+        try:
+            return self._process.memory_info().rss / (1024 * 1024)
+        except Exception:
+            return 0.0
+
+    def _add_to_history(self, metrics: OperationMetrics) -> None:
+        """Add metrics to history, trimming if necessary."""
+        self.history.append(metrics)
+        if len(self.history) > self.max_history:
+            self.history = self.history[-self.max_history :]
+
+    def get_memory_usage(self) -> Dict[str, float]:
+        """
+        Get current memory usage information.
+
+        Returns:
+            Dictionary with memory usage metrics
+        """
+        try:
+            memory_info = self._process.memory_info()
+            virtual_memory = psutil.virtual_memory()
+
+            return {
+                "process_mb": memory_info.rss / (1024 * 1024),
+                "process_percent": self._process.memory_percent(),
+                "system_total_gb": virtual_memory.total / (1024**3),
+                "system_available_gb": virtual_memory.available / (1024**3),
+                "system_percent": virtual_memory.percent,
+            }
+        except Exception:
+            return {}
+
+    def get_cpu_usage(self) -> Dict[str, float]:
+        """
+        Get current CPU usage information.
+
+        Returns:
+            Dictionary with CPU usage metrics
+
+        Note:
+            Using interval=0.1 for cpu_percent() to get accurate readings.
+            Without an interval, the first call returns 0.0 (psutil quirk).
+        """
+        try:
+            return {
+                "process_percent": self._process.cpu_percent(interval=0.1),
+                "system_percent": psutil.cpu_percent(interval=None),
+                "cpu_count": psutil.cpu_count(),
+            }
+        except Exception:
+            return {}
+
+    def get_recent_operations(self, count: int = 10) -> List[Dict[str, Any]]:
+        """
+        Get recent operation metrics.
+
+        Args:
+            count: Number of recent operations to return
+
+        Returns:
+            List of operation metrics dictionaries
+        """
+        recent = self.history[-count:] if self.history else []
+        return [
+            {
+                "name": op.name,
+                "duration_ms": op.duration_ms,
+                "memory_mb": op.memory_mb,
+                "success": op.success,
+                "context": op.context,
+                "timestamp": op.timestamp,
+            }
+            for op in recent
+        ]
+
+    def get_statistics(self) -> Dict[str, Any]:
+        """
+        Get aggregate statistics from operation history.
+
+        Returns:
+            Dictionary with aggregate statistics
+        """
+        if not self.history:
+            return {"total_operations": 0}
+
+        durations = [op.duration_ms for op in self.history]
+        successes = sum(1 for op in self.history if op.success)
+
+        return {
+            "total_operations": len(self.history),
+            "success_rate": successes / len(self.history),
+            "avg_duration_ms": sum(durations) / len(durations),
+            "max_duration_ms": max(durations),
+            "min_duration_ms": min(durations),
+        }
+
+    def clear_history(self) -> None:
+        """Clear operation history."""
+        self.history.clear()
diff --git a/resources/icon.ico b/resources/icon.ico
new file mode 100644
index 0000000..858841c
Binary files /dev/null and b/resources/icon.ico differ
diff --git a/resources/icon.png b/resources/icon.png
new file mode 100644
index 0000000..61a4499
Binary files /dev/null and b/resources/icon.png differ
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..452c573
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# Build the Stingray Explorer application
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+echo "Building Stingray Explorer..."
+
+cd "$PROJECT_ROOT"
+
+# Install npm dependencies if needed
+if [ ! -d "node_modules" ]; then
+    echo "Installing npm dependencies..."
+    npm install
+fi
+
+# Build the application
+echo "Building Electron app..."
+npm run build
+
+echo "Build complete!"
+echo "Output is in the dist/ directory."
diff --git a/scripts/dev.sh b/scripts/dev.sh
new file mode 100755
index 0000000..2293f71
--- /dev/null
+++ b/scripts/dev.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# Start the full development environment
+#
+# Note: The Python backend is spawned and managed by Electron, not this script.
+# This ensures Electron can capture all backend stdout/stderr for the log panel.
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+echo "Starting Stingray Explorer development environment..."
+
+# Cleanup function - kills any orphaned backend processes on exit
+cleanup() {
+    echo ""
+    echo "Cleaning up..."
+    # Kill any orphaned python backend processes (safety measure)
+    pkill -9 -f "${PROJECT_ROOT}/python-backend/main.py" 2>/dev/null || true
+    echo "Cleanup complete."
+    exit 0
+}
+
+# Set up traps for various signals
+trap cleanup EXIT
+trap cleanup SIGINT
+trap cleanup SIGTERM
+trap cleanup SIGHUP
+
+# Start the Electron app (it will spawn and manage the Python backend)
+echo "Starting Electron app..."
+echo "Note: Python backend will be started by Electron for proper log capture."
+cd "$PROJECT_ROOT"
+
+# Use dev:linux on Linux to disable sandbox (avoids permission issues)
+if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+    npm run dev:linux
+else
+    npm run dev
+fi
+
+# Cleanup will be called automatically via trap
diff --git a/scripts/package.sh b/scripts/package.sh
new file mode 100755
index 0000000..917e48f
--- /dev/null
+++ b/scripts/package.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Package the Stingray Explorer application for distribution
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+BACKEND_DIR="$PROJECT_ROOT/python-backend"
+DIST_DIR="$PROJECT_ROOT/dist"
+
+echo "Packaging Stingray Explorer..."
+
+cd "$PROJECT_ROOT"
+
+# Build the application first
+"$SCRIPT_DIR/build.sh"
+
+# Create a distribution package with Python backend
+echo "Creating distribution package..."
+
+# Create resources directory for Python backend
+RESOURCES_DIR="$DIST_DIR/resources/python-backend"
+mkdir -p "$RESOURCES_DIR"
+
+# Copy Python backend files
+cp -r "$BACKEND_DIR"/* "$RESOURCES_DIR/"
+
+echo "Package created in $DIST_DIR"
+echo ""
+echo "Note: For production distribution, you'll need to:"
+echo "1. Bundle Python with the application or require it as a dependency"
+echo "2. Use electron-builder to create platform-specific installers"
+echo "3. Sign the application for macOS and Windows"
diff --git a/scripts/setup-python.sh b/scripts/setup-python.sh
new file mode 100755
index 0000000..04aa9a4
--- /dev/null
+++ b/scripts/setup-python.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Setup Python environment for Stingray Explorer backend
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+BACKEND_DIR="$PROJECT_ROOT/python-backend"
+VENV_DIR="$PROJECT_ROOT/.venv"
+
+echo "Setting up Python environment for Stingray Explorer..."
+
+# Check if Python 3 is available
+if ! command -v python3 &> /dev/null; then
+    echo "Error: Python 3 is required but not installed."
+    exit 1
+fi
+
+# Create virtual environment if it doesn't exist
+if [ ! -d "$VENV_DIR" ]; then
+    echo "Creating virtual environment..."
+    python3 -m venv "$VENV_DIR"
+fi
+
+# Activate virtual environment
+source "$VENV_DIR/bin/activate"
+
+# Upgrade pip
+echo "Upgrading pip..."
+pip install --upgrade pip
+
+# Install requirements
+echo "Installing Python dependencies..."
+pip install -r "$BACKEND_DIR/requirements.txt"
+
+echo "Python environment setup complete!"
+echo ""
+echo "To activate the virtual environment, run:"
+echo "  source $VENV_DIR/bin/activate"
diff --git a/scripts/start-backend.sh b/scripts/start-backend.sh
new file mode 100755
index 0000000..52f3e65
--- /dev/null
+++ b/scripts/start-backend.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+# Start the Python backend server using Pixi
+pixi run start-backend
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..cb4c237
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,781 @@
+import React, { useState, useEffect, createContext, useContext, useMemo } from 'react';
+import { RouterProvider, createHashRouter } from 'react-router-dom';
+import { ThemeProvider as MuiThemeProvider, createTheme, Theme } from '@mui/material/styles';
+import CssBaseline from '@mui/material/CssBaseline';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+// MUI Palette augmentation for stingrayGreen
+declare module '@mui/material/styles' {
+  interface Palette {
+    stingrayGreen: Palette['primary'];
+  }
+  interface PaletteOptions {
+    stingrayGreen?: PaletteOptions['primary'];
+  }
+}
+
+// Layout
+import MainLayout from '@/components/layout/MainLayout';
+
+// Pages
+import HomePage from '@/pages/Home';
+import DataIngestionPage from '@/pages/DataIngestion';
+import NotFoundPage from '@/pages/NotFound';
+
+// QuickLook Pages
+import EventListPage from '@/pages/QuickLook/EventList';
+import LightCurvePage from '@/pages/QuickLook/LightCurve';
+import PowerSpectrumPage from '@/pages/QuickLook/PowerSpectrum';
+import AvgPowerSpectrumPage from '@/pages/QuickLook/AvgPowerSpectrum';
+import CrossSpectrumPage from '@/pages/QuickLook/CrossSpectrum';
+import AvgCrossSpectrumPage from '@/pages/QuickLook/AvgCrossSpectrum';
+import DynamicalPowerSpectrumPage from '@/pages/QuickLook/DynamicalPowerSpectrum';
+import CoherencePage from '@/pages/QuickLook/Coherence';
+import TimeLagsPage from '@/pages/QuickLook/TimeLags';
+import CrossCorrelationPage from '@/pages/QuickLook/CrossCorrelation';
+import AutoCorrelationPage from '@/pages/QuickLook/AutoCorrelation';
+import DeadTimeCorrectionsPage from '@/pages/QuickLook/DeadTimeCorrections';
+import BispectrumPage from '@/pages/QuickLook/Bispectrum';
+import PowerColorsPage from '@/pages/QuickLook/PowerColors';
+import CovarianceSpectrumPage from '@/pages/QuickLook/CovarianceSpectrum';
+import AvgCovarianceSpectrumPage from '@/pages/QuickLook/AvgCovarianceSpectrum';
+import VariableEnergySpectrumPage from '@/pages/QuickLook/VariableEnergySpectrum';
+import RmsEnergySpectrumPage from '@/pages/QuickLook/RmsEnergySpectrum';
+import LagEnergySpectrumPage from '@/pages/QuickLook/LagEnergySpectrum';
+import ExcessVarianceSpectrumPage from '@/pages/QuickLook/ExcessVarianceSpectrum';
+
+// Utilities Pages
+import StatisticalFunctionsPage from '@/pages/Utilities/StatisticalFunctions';
+import GTIPage from '@/pages/Utilities/GTI';
+import IOPage from '@/pages/Utilities/IO';
+import MissionIOPage from '@/pages/Utilities/MissionIO';
+import MiscPage from '@/pages/Utilities/Misc';
+
+// Modeling Pages
+import ModelBuilderPage from '@/pages/Modeling/ModelBuilder';
+import MLEFittingPage from '@/pages/Modeling/MLEFitting';
+import MCMCFittingPage from '@/pages/Modeling/MCMCFitting';
+
+// Pulsar Pages
+import PeriodSearchPage from '@/pages/Pulsar/PeriodSearch';
+import PhaseFoldingPage from '@/pages/Pulsar/PhaseFolding';
+import PhaseogramPage from '@/pages/Pulsar/Phaseogram';
+
+// Simulator Page
+import SimulatorPage from '@/pages/Simulator';
+
+// Hooks
+import { useJobStream } from '@/hooks/useJobStream';
+
+// Theme Context
+interface ThemeContextType {
+  darkMode: boolean;
+  toggleDarkMode: () => void;
+}
+
+export const ThemeContext = createContext({
+  darkMode: false,
+  toggleDarkMode: () => {},
+});
+
+export const useThemeContext = (): ThemeContextType => useContext(ThemeContext);
+
+// Backend Context
+interface BackendContextType {
+  port: number | null;
+  isReady: boolean;
+  error: string | null;
+}
+
+export const BackendContext = createContext({
+  port: null,
+  isReady: false,
+  error: null,
+});
+
+export const useBackendContext = (): BackendContextType => useContext(BackendContext);
+
+// Create React Query client
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      staleTime: 5 * 60 * 1000, // 5 minutes
+      retry: 1,
+    },
+  },
+});
+
+// ─── Shared typography ────────────────────────────────────────────────────────
+const fontDisplay = '"JetBrains Mono", "Fira Code", "Source Code Pro", monospace';
+const fontBody = '"IBM Plex Sans", "Source Sans 3", -apple-system, sans-serif';
+
+const sharedTypography = {
+  fontFamily: fontBody,
+  h1: { fontFamily: fontDisplay, fontWeight: 700, letterSpacing: '-0.02em' },
+  h2: { fontFamily: fontDisplay, fontWeight: 700, letterSpacing: '-0.01em' },
+  h3: { fontFamily: fontDisplay, fontWeight: 600, letterSpacing: '-0.01em' },
+  h4: { fontFamily: fontDisplay, fontWeight: 600, letterSpacing: '0' },
+  h5: { fontFamily: fontDisplay, fontWeight: 500, letterSpacing: '0' },
+  h6: { fontFamily: fontDisplay, fontWeight: 500, letterSpacing: '0.01em' },
+  subtitle1: { fontFamily: fontBody, fontWeight: 500 },
+  subtitle2: { fontFamily: fontBody, fontWeight: 500 },
+  body1: { fontFamily: fontBody, fontWeight: 400, lineHeight: 1.6 },
+  body2: { fontFamily: fontBody, fontWeight: 400, lineHeight: 1.5 },
+  button: { fontFamily: fontBody, fontWeight: 600, letterSpacing: '0.02em', textTransform: 'none' as const },
+  caption: { fontFamily: fontBody, fontWeight: 400 },
+  overline: { fontFamily: fontDisplay, fontWeight: 500, letterSpacing: '0.1em', textTransform: 'uppercase' as const },
+};
+
+const sharedShape = { borderRadius: 8 };
+
+// ─── Dark theme (primary / default) ──────────────────────────────────────────
+const createDarkTheme = (): Theme =>
+  createTheme({
+    palette: {
+      mode: 'dark',
+      primary: { main: '#00d4aa', light: '#33e0be', dark: '#00a885', contrastText: '#0a0e1a' },
+      secondary: { main: '#3b82f6', light: '#60a5fa', dark: '#2563eb', contrastText: '#ffffff' },
+      stingrayGreen: { main: '#5ead61', light: '#8edf91', dark: '#3d7a40', contrastText: '#ffffff' },
+      background: { default: '#0a0e1a', paper: '#121829' },
+      text: { primary: '#e2e8f0', secondary: '#94a3b8', disabled: '#475569' },
+      divider: 'rgba(148, 163, 184, 0.12)',
+      success: { main: '#22c55e', light: '#4ade80', dark: '#16a34a' },
+      warning: { main: '#f59e0b', light: '#fbbf24', dark: '#d97706' },
+      error: { main: '#ef4444', light: '#f87171', dark: '#dc2626' },
+      info: { main: '#3b82f6', light: '#60a5fa', dark: '#2563eb' },
+    },
+    typography: sharedTypography,
+    shape: sharedShape,
+    transitions: {
+      easing: { easeInOut: 'cubic-bezier(0.22, 0.61, 0.36, 1)' },
+    },
+    components: {
+      MuiCssBaseline: {
+        styleOverrides: {
+          body: {
+            transition: 'background-color 0.3s cubic-bezier(0.22, 0.61, 0.36, 1)',
+          },
+        },
+      },
+      MuiPaper: {
+        styleOverrides: {
+          root: {
+            backgroundImage: 'none',
+            transition: 'background-color 0.2s ease, box-shadow 0.2s ease',
+          },
+        },
+      },
+      MuiCard: {
+        styleOverrides: {
+          root: {
+            background: 'rgba(18, 24, 41, 0.6)',
+            backdropFilter: 'blur(12px) saturate(150%)',
+            WebkitBackdropFilter: 'blur(12px) saturate(150%)',
+            border: '1px solid rgba(148, 163, 184, 0.12)',
+            transition: 'border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1)',
+            '&:hover': {
+              borderColor: 'rgba(0, 212, 170, 0.3)',
+              boxShadow: '0 0 20px rgba(0, 212, 170, 0.1), 0 8px 32px rgba(0, 0, 0, 0.3)',
+            },
+          },
+        },
+      },
+      MuiButton: {
+        styleOverrides: {
+          root: {
+            borderRadius: 8,
+            transition: 'all 0.2s ease',
+          },
+          contained: {
+            boxShadow: '0 2px 8px rgba(0, 212, 170, 0.15)',
+            '&:hover': {
+              boxShadow: '0 4px 20px rgba(0, 212, 170, 0.25), 0 0 40px rgba(0, 212, 170, 0.1)',
+            },
+          },
+          outlined: {
+            borderColor: 'rgba(148, 163, 184, 0.2)',
+            '&:hover': {
+              borderColor: '#00d4aa',
+              boxShadow: '0 0 12px rgba(0, 212, 170, 0.15)',
+            },
+          },
+        },
+      },
+      MuiIconButton: {
+        styleOverrides: {
+          root: {
+            transition: 'all 0.2s ease',
+            '&:hover': {
+              backgroundColor: 'rgba(0, 212, 170, 0.08)',
+              boxShadow: '0 0 12px rgba(0, 212, 170, 0.12)',
+            },
+          },
+        },
+      },
+      MuiChip: {
+        styleOverrides: {
+          root: {
+            fontFamily: fontBody,
+            fontWeight: 500,
+          },
+          outlined: {
+            borderColor: 'rgba(148, 163, 184, 0.2)',
+          },
+          filled: {
+            backgroundColor: 'rgba(0, 212, 170, 0.12)',
+          },
+        },
+      },
+      MuiTextField: {
+        styleOverrides: {
+          root: {
+            '& .MuiOutlinedInput-root': {
+              transition: 'box-shadow 0.2s ease',
+              '&.Mui-focused': {
+                boxShadow: '0 0 0 3px rgba(0, 212, 170, 0.12)',
+              },
+            },
+          },
+        },
+      },
+      MuiAppBar: {
+        styleOverrides: {
+          root: {
+            backgroundImage: 'none',
+          },
+        },
+      },
+      MuiDrawer: {
+        styleOverrides: {
+          paper: {
+            backgroundImage: 'none',
+          },
+        },
+      },
+      MuiDialog: {
+        styleOverrides: {
+          paper: {
+            background: 'rgba(18, 24, 41, 0.85)',
+            backdropFilter: 'blur(16px) saturate(150%)',
+            WebkitBackdropFilter: 'blur(16px) saturate(150%)',
+            border: '1px solid rgba(148, 163, 184, 0.12)',
+          },
+        },
+      },
+      MuiTooltip: {
+        styleOverrides: {
+          tooltip: {
+            fontFamily: fontBody,
+            fontSize: '0.75rem',
+            backgroundColor: 'rgba(18, 24, 41, 0.9)',
+            backdropFilter: 'blur(8px)',
+            border: '1px solid rgba(148, 163, 184, 0.12)',
+          },
+        },
+      },
+      MuiAlert: {
+        styleOverrides: {
+          root: {
+            backdropFilter: 'blur(8px)',
+            border: '1px solid',
+          },
+          standardSuccess: {
+            backgroundColor: 'rgba(34, 197, 94, 0.1)',
+            borderColor: 'rgba(34, 197, 94, 0.2)',
+          },
+          standardWarning: {
+            backgroundColor: 'rgba(245, 158, 11, 0.1)',
+            borderColor: 'rgba(245, 158, 11, 0.2)',
+          },
+          standardError: {
+            backgroundColor: 'rgba(239, 68, 68, 0.1)',
+            borderColor: 'rgba(239, 68, 68, 0.2)',
+          },
+          standardInfo: {
+            backgroundColor: 'rgba(59, 130, 246, 0.1)',
+            borderColor: 'rgba(59, 130, 246, 0.2)',
+          },
+        },
+      },
+      MuiDivider: {
+        styleOverrides: {
+          root: {
+            borderColor: 'rgba(148, 163, 184, 0.08)',
+          },
+        },
+      },
+      MuiListItemButton: {
+        styleOverrides: {
+          root: {
+            borderRadius: 6,
+            margin: '1px 6px',
+            transition: 'all 0.2s ease',
+            '&:hover': {
+              backgroundColor: 'rgba(0, 212, 170, 0.06)',
+            },
+            '&.Mui-selected': {
+              backgroundColor: 'rgba(0, 212, 170, 0.1)',
+              '&:hover': {
+                backgroundColor: 'rgba(0, 212, 170, 0.14)',
+              },
+            },
+          },
+        },
+      },
+      MuiTab: {
+        styleOverrides: {
+          root: {
+            fontFamily: fontBody,
+            fontWeight: 500,
+            textTransform: 'none',
+          },
+        },
+      },
+      MuiTableHead: {
+        styleOverrides: {
+          root: {
+            '& .MuiTableCell-head': {
+              fontFamily: fontDisplay,
+              fontWeight: 500,
+              fontSize: '0.75rem',
+              letterSpacing: '0.05em',
+              textTransform: 'uppercase',
+              backgroundColor: 'rgba(0, 212, 170, 0.04)',
+              borderBottom: '1px solid rgba(148, 163, 184, 0.12)',
+            },
+          },
+        },
+      },
+      MuiTableCell: {
+        styleOverrides: {
+          root: {
+            borderBottom: '1px solid rgba(148, 163, 184, 0.06)',
+          },
+        },
+      },
+      MuiLinearProgress: {
+        styleOverrides: {
+          root: {
+            borderRadius: 4,
+            backgroundColor: 'rgba(0, 212, 170, 0.08)',
+          },
+        },
+      },
+      MuiMenu: {
+        styleOverrides: {
+          paper: {
+            background: 'rgba(18, 24, 41, 0.9)',
+            backdropFilter: 'blur(12px) saturate(150%)',
+            WebkitBackdropFilter: 'blur(12px) saturate(150%)',
+            border: '1px solid rgba(148, 163, 184, 0.12)',
+          },
+        },
+      },
+      MuiPopover: {
+        styleOverrides: {
+          paper: {
+            background: 'rgba(18, 24, 41, 0.9)',
+            backdropFilter: 'blur(12px) saturate(150%)',
+            WebkitBackdropFilter: 'blur(12px) saturate(150%)',
+            border: '1px solid rgba(148, 163, 184, 0.12)',
+          },
+        },
+      },
+    },
+  });
+
+// ─── Light theme (refined Observatory Light) ─────────────────────────────────
+const createLightTheme = (): Theme =>
+  createTheme({
+    palette: {
+      mode: 'light',
+      primary: { main: '#0d9b7a', light: '#00d4aa', dark: '#087a60', contrastText: '#ffffff' },
+      secondary: { main: '#2563eb', light: '#3b82f6', dark: '#1d4ed8', contrastText: '#ffffff' },
+      stingrayGreen: { main: '#4a9a4d', light: '#5ead61', dark: '#2e7d32', contrastText: '#ffffff' },
+      background: { default: '#f0f2f5', paper: '#ffffff' },
+      text: { primary: '#1e293b', secondary: '#64748b', disabled: '#94a3b8' },
+      divider: 'rgba(30, 41, 59, 0.12)',
+      success: { main: '#16a34a', light: '#22c55e', dark: '#15803d' },
+      warning: { main: '#d97706', light: '#f59e0b', dark: '#b45309' },
+      error: { main: '#dc2626', light: '#ef4444', dark: '#b91c1c' },
+      info: { main: '#2563eb', light: '#3b82f6', dark: '#1d4ed8' },
+    },
+    typography: sharedTypography,
+    shape: sharedShape,
+    transitions: {
+      easing: { easeInOut: 'cubic-bezier(0.22, 0.61, 0.36, 1)' },
+    },
+    components: {
+      MuiCssBaseline: {
+        styleOverrides: {
+          body: {
+            transition: 'background-color 0.3s cubic-bezier(0.22, 0.61, 0.36, 1)',
+          },
+        },
+      },
+      MuiPaper: {
+        styleOverrides: {
+          root: {
+            backgroundImage: 'none',
+            transition: 'background-color 0.2s ease, box-shadow 0.2s ease',
+          },
+        },
+      },
+      MuiCard: {
+        styleOverrides: {
+          root: {
+            background: 'rgba(255, 255, 255, 0.7)',
+            backdropFilter: 'blur(12px) saturate(150%)',
+            WebkitBackdropFilter: 'blur(12px) saturate(150%)',
+            border: '1px solid rgba(30, 41, 59, 0.08)',
+            transition: 'border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1)',
+            '&:hover': {
+              borderColor: 'rgba(13, 155, 122, 0.3)',
+              boxShadow: '0 4px 24px rgba(13, 155, 122, 0.08), 0 8px 32px rgba(0, 0, 0, 0.06)',
+            },
+          },
+        },
+      },
+      MuiButton: {
+        styleOverrides: {
+          root: {
+            borderRadius: 8,
+            transition: 'all 0.2s ease',
+          },
+          contained: {
+            boxShadow: '0 2px 8px rgba(13, 155, 122, 0.15)',
+            '&:hover': {
+              boxShadow: '0 4px 20px rgba(13, 155, 122, 0.2)',
+            },
+          },
+          outlined: {
+            borderColor: 'rgba(30, 41, 59, 0.2)',
+            '&:hover': {
+              borderColor: '#0d9b7a',
+              boxShadow: '0 0 12px rgba(13, 155, 122, 0.1)',
+            },
+          },
+        },
+      },
+      MuiIconButton: {
+        styleOverrides: {
+          root: {
+            transition: 'all 0.2s ease',
+            '&:hover': {
+              backgroundColor: 'rgba(13, 155, 122, 0.08)',
+            },
+          },
+        },
+      },
+      MuiChip: {
+        styleOverrides: {
+          root: {
+            fontFamily: fontBody,
+            fontWeight: 500,
+          },
+          outlined: {
+            borderColor: 'rgba(30, 41, 59, 0.2)',
+          },
+          filled: {
+            backgroundColor: 'rgba(13, 155, 122, 0.1)',
+          },
+        },
+      },
+      MuiTextField: {
+        styleOverrides: {
+          root: {
+            '& .MuiOutlinedInput-root': {
+              transition: 'box-shadow 0.2s ease',
+              '&.Mui-focused': {
+                boxShadow: '0 0 0 3px rgba(13, 155, 122, 0.1)',
+              },
+            },
+          },
+        },
+      },
+      MuiAppBar: {
+        styleOverrides: {
+          root: {
+            backgroundImage: 'none',
+          },
+        },
+      },
+      MuiDrawer: {
+        styleOverrides: {
+          paper: {
+            backgroundImage: 'none',
+          },
+        },
+      },
+      MuiDialog: {
+        styleOverrides: {
+          paper: {
+            background: 'rgba(255, 255, 255, 0.9)',
+            backdropFilter: 'blur(16px) saturate(150%)',
+            WebkitBackdropFilter: 'blur(16px) saturate(150%)',
+            border: '1px solid rgba(30, 41, 59, 0.08)',
+          },
+        },
+      },
+      MuiTooltip: {
+        styleOverrides: {
+          tooltip: {
+            fontFamily: fontBody,
+            fontSize: '0.75rem',
+            backgroundColor: 'rgba(30, 41, 59, 0.9)',
+            border: '1px solid rgba(30, 41, 59, 0.12)',
+          },
+        },
+      },
+      MuiAlert: {
+        styleOverrides: {
+          root: {
+            border: '1px solid',
+          },
+          standardSuccess: {
+            backgroundColor: 'rgba(22, 163, 74, 0.08)',
+            borderColor: 'rgba(22, 163, 74, 0.2)',
+          },
+          standardWarning: {
+            backgroundColor: 'rgba(217, 119, 6, 0.08)',
+            borderColor: 'rgba(217, 119, 6, 0.2)',
+          },
+          standardError: {
+            backgroundColor: 'rgba(220, 38, 38, 0.08)',
+            borderColor: 'rgba(220, 38, 38, 0.2)',
+          },
+          standardInfo: {
+            backgroundColor: 'rgba(37, 99, 235, 0.08)',
+            borderColor: 'rgba(37, 99, 235, 0.2)',
+          },
+        },
+      },
+      MuiDivider: {
+        styleOverrides: {
+          root: {
+            borderColor: 'rgba(30, 41, 59, 0.08)',
+          },
+        },
+      },
+      MuiListItemButton: {
+        styleOverrides: {
+          root: {
+            borderRadius: 6,
+            margin: '1px 6px',
+            transition: 'all 0.2s ease',
+            '&:hover': {
+              backgroundColor: 'rgba(13, 155, 122, 0.06)',
+            },
+            '&.Mui-selected': {
+              backgroundColor: 'rgba(13, 155, 122, 0.1)',
+              '&:hover': {
+                backgroundColor: 'rgba(13, 155, 122, 0.14)',
+              },
+            },
+          },
+        },
+      },
+      MuiTab: {
+        styleOverrides: {
+          root: {
+            fontFamily: fontBody,
+            fontWeight: 500,
+            textTransform: 'none',
+          },
+        },
+      },
+      MuiTableHead: {
+        styleOverrides: {
+          root: {
+            '& .MuiTableCell-head': {
+              fontFamily: fontDisplay,
+              fontWeight: 500,
+              fontSize: '0.75rem',
+              letterSpacing: '0.05em',
+              textTransform: 'uppercase',
+              backgroundColor: 'rgba(13, 155, 122, 0.04)',
+              borderBottom: '1px solid rgba(30, 41, 59, 0.12)',
+            },
+          },
+        },
+      },
+      MuiTableCell: {
+        styleOverrides: {
+          root: {
+            borderBottom: '1px solid rgba(30, 41, 59, 0.06)',
+          },
+        },
+      },
+      MuiLinearProgress: {
+        styleOverrides: {
+          root: {
+            borderRadius: 4,
+            backgroundColor: 'rgba(13, 155, 122, 0.08)',
+          },
+        },
+      },
+      MuiMenu: {
+        styleOverrides: {
+          paper: {
+            background: 'rgba(255, 255, 255, 0.95)',
+            backdropFilter: 'blur(12px)',
+            WebkitBackdropFilter: 'blur(12px)',
+            border: '1px solid rgba(30, 41, 59, 0.08)',
+          },
+        },
+      },
+      MuiPopover: {
+        styleOverrides: {
+          paper: {
+            background: 'rgba(255, 255, 255, 0.95)',
+            backdropFilter: 'blur(12px)',
+            WebkitBackdropFilter: 'blur(12px)',
+            border: '1px solid rgba(30, 41, 59, 0.08)',
+          },
+        },
+      },
+    },
+  });
+
+// Router configuration
+const router = createHashRouter([
+  {
+    path: '/',
+    element: ,
+    children: [
+      { index: true, element:  },
+      { path: 'data-ingestion', element:  },
+
+      // QuickLook routes
+      { path: 'quicklook/event-list', element:  },
+      { path: 'quicklook/light-curve', element:  },
+      { path: 'quicklook/power-spectrum', element:  },
+      { path: 'quicklook/avg-power-spectrum', element:  },
+      { path: 'quicklook/cross-spectrum', element:  },
+      { path: 'quicklook/avg-cross-spectrum', element:  },
+      { path: 'quicklook/dynamical-power-spectrum', element:  },
+      { path: 'quicklook/coherence', element:  },
+      { path: 'quicklook/time-lags', element:  },
+      { path: 'quicklook/cross-correlation', element:  },
+      { path: 'quicklook/auto-correlation', element:  },
+      { path: 'quicklook/dead-time-corrections', element:  },
+      { path: 'quicklook/bispectrum', element:  },
+      { path: 'quicklook/power-colors', element:  },
+      { path: 'quicklook/covariance-spectrum', element:  },
+      { path: 'quicklook/avg-covariance-spectrum', element:  },
+      { path: 'quicklook/variable-energy-spectrum', element:  },
+      { path: 'quicklook/rms-energy-spectrum', element:  },
+      { path: 'quicklook/lag-energy-spectrum', element:  },
+      { path: 'quicklook/excess-variance-spectrum', element:  },
+
+      // Utilities routes
+      { path: 'utilities/statistical-functions', element:  },
+      { path: 'utilities/gti', element:  },
+      { path: 'utilities/io', element:  },
+      { path: 'utilities/mission-io', element:  },
+      { path: 'utilities/misc', element:  },
+
+      // Modeling routes
+      { path: 'modeling/builder', element:  },
+      { path: 'modeling/mle', element:  },
+      { path: 'modeling/mcmc', element:  },
+
+      // Pulsar routes
+      { path: 'pulsar/search', element:  },
+      { path: 'pulsar/folding', element:  },
+      { path: 'pulsar/phaseogram', element:  },
+
+      // Simulator
+      { path: 'simulator', element:  },
+
+      // 404
+      { path: '*', element:  },
+    ],
+  },
+]);
+
+/**
+ * Component that initializes the job stream SSE connection.
+ * Must be inside BackendContext.Provider to access backend state.
+ */
+const JobStreamInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  useJobStream();
+  return <>{children};
+};
+
+// Main App Component
+const App: React.FC = () => {
+  const [darkMode, setDarkMode] = useState(() => {
+    const saved = localStorage.getItem('darkMode');
+    return saved !== null ? JSON.parse(saved) : true;
+  });
+
+  const [backendState, setBackendState] = useState({
+    port: null,
+    isReady: false,
+    error: null,
+  });
+
+  // Toggle dark mode
+  const toggleDarkMode = (): void => {
+    setDarkMode((prev) => {
+      const newValue = !prev;
+      localStorage.setItem('darkMode', JSON.stringify(newValue));
+      return newValue;
+    });
+  };
+
+  // Theme memoization
+  const theme = useMemo(() => (darkMode ? createDarkTheme() : createLightTheme()), [darkMode]);
+
+  // Listen for Python backend events
+  useEffect(() => {
+    if (typeof window !== 'undefined' && window.electronAPI) {
+      const unsubscribeReady = window.electronAPI.onPythonReady((port) => {
+        setBackendState({ port, isReady: true, error: null });
+      });
+
+      const unsubscribeError = window.electronAPI.onPythonError((error) => {
+        setBackendState((prev) => ({ ...prev, error }));
+      });
+
+      const unsubscribeStarting = window.electronAPI.onPythonStarting(() => {
+        setBackendState({ port: null, isReady: false, error: null });
+      });
+
+      // Check if already ready
+      window.electronAPI.getBackendPort().then((port) => {
+        if (port) {
+          window.electronAPI.isPythonRunning().then((isRunning) => {
+            if (isRunning) {
+              setBackendState({ port, isReady: true, error: null });
+            }
+          });
+        }
+      });
+
+      return () => {
+        unsubscribeReady();
+        unsubscribeError();
+        unsubscribeStarting();
+      };
+    }
+  }, []);
+
+  return (
+    
+      
+        
+          
+            
+            
+              
+            
+          
+        
+      
+    
+  );
+};
+
+export default App;
diff --git a/src/api/archiveApi.ts b/src/api/archiveApi.ts
new file mode 100644
index 0000000..48ce243
--- /dev/null
+++ b/src/api/archiveApi.ts
@@ -0,0 +1,320 @@
+/**
+ * API functions for HEASARC archive operations
+ */
+
+import { apiClient, ApiResponse } from './client';
+
+// Types
+
+/** Supported HEASARC catalog information */
+export interface HeasarcCatalog {
+  id: string;
+  catalog: string;
+  display_name: string;
+  description: string;
+}
+
+/** HEASARC observation result */
+export interface HeasarcObservation {
+  obsid: string;
+  name: string;
+  ra: number | null;
+  dec: number | null;
+  exposure: number | null;
+  time: string;
+  catalog: string;
+  // Mission-specific fields
+  prnb?: string;  // RXTE proposal number
+  // Swift instrument-specific exposures
+  xrt_exposure?: number | null;
+  bat_exposure?: number | null;
+  uvot_exposure?: number | null;
+  // IXPE per-detector-unit exposures
+  exposure_du1?: number | null;
+  exposure_du2?: number | null;
+  exposure_du3?: number | null;
+  // NuSTAR-specific fields
+  exposure_b?: number | null;     // FPMB exposure (seconds)
+  observation_mode?: string;       // "SCIENCE" or "SLEW"
+  issue_flag?: number | null;      // 0=OK, 1=known issues
+  // NICER-specific fields
+  processing_status?: string;
+  num_fpm?: number | null;
+  // XMM-Newton-specific fields (per-instrument data only via ObsID/ADQL search)
+  pn_time?: number | null;      // EPIC-PN exposure (seconds)
+  pn_mode?: string;             // EPIC-PN observation mode
+  mos1_time?: number | null;    // EPIC-MOS1 exposure (seconds)
+  mos1_mode?: string;           // EPIC-MOS1 observation mode
+  mos2_time?: number | null;    // EPIC-MOS2 exposure (seconds)
+  mos2_mode?: string;           // EPIC-MOS2 observation mode
+  xmm_status?: string;          // "archived" or "scheduled"
+  data_in_heasarc?: string;     // "Y" or "N"
+  // Chandra-specific fields
+  detector?: string;            // "ACIS-I", "ACIS-S", "HRC-I", "HRC-S"
+  grating?: string;             // "NONE", "HETG", "LETG"
+  chandra_status?: string;      // "archived", "observed", "scheduled", etc.
+}
+
+/** Search result from HEASARC */
+export interface SearchResult {
+  observations: HeasarcObservation[];
+  count: number;
+  mission: string;
+  radius?: number;
+  // For name search
+  source_name?: string;
+  resolved_ra?: number;
+  resolved_dec?: number;
+  // For coordinate search
+  ra?: number;
+  dec?: number;
+  // For obsid search
+  obsid?: string;
+}
+
+/** Download URLs for an observation */
+export interface ObservationUrls {
+  urls: Record;
+  mission: string;
+  obsid: string;
+}
+
+// File Browser types
+export type FileType = 'event' | 'calibration' | 'auxiliary' | 'log' | 'directory' | 'other';
+
+export interface FileEntry {
+  path: string;
+  name: string;
+  is_directory: boolean;
+  file_type: FileType;
+  size_bytes: number | null;
+  size_display: string;
+  full_url: string;
+  children?: FileEntry[];
+}
+
+export interface ListFilesResponse {
+  base_url: string;
+  files: FileEntry[];
+  mission: string;
+  obsid: string;
+  total_files: number;
+}
+
+// Download to disk SSE event types
+export interface DownloadToDiskProgressEvent {
+  type: 'progress';
+  bytes_downloaded: number;
+  total_bytes: number;
+  percent: number;
+}
+
+export interface DownloadToDiskCompleteEvent {
+  type: 'complete';
+  file_path: string;
+  size_bytes: number;
+}
+
+export interface DownloadToDiskErrorEvent {
+  type: 'error';
+  error: string;
+}
+
+export type DownloadToDiskEvent =
+  | DownloadToDiskProgressEvent
+  | DownloadToDiskCompleteEvent
+  | DownloadToDiskErrorEvent;
+
+// API functions
+export const archiveApi = {
+  /**
+   * Get list of supported HEASARC catalogs
+   */
+  async getCatalogs(): Promise> {
+    return apiClient.get('/api/archive/catalogs');
+  },
+
+  /**
+   * Search HEASARC by source name
+   */
+  async searchByName(params: {
+    source_name: string;
+    mission: string;
+    radius?: number;
+    max_results?: number;
+    min_exposure?: number;
+    start_date?: string;  // ISO "YYYY-MM-DD"
+    end_date?: string;    // ISO "YYYY-MM-DD"
+  }): Promise> {
+    return apiClient.post('/api/archive/search/name', {
+      source_name: params.source_name,
+      mission: params.mission,
+      radius: params.radius ?? 0.5,
+      max_results: params.max_results ?? 100,
+      min_exposure: params.min_exposure,
+      start_date: params.start_date,
+      end_date: params.end_date,
+    });
+  },
+
+  /**
+   * Search HEASARC by coordinates
+   */
+  async searchByCoordinates(params: {
+    ra: number;
+    dec: number;
+    mission: string;
+    radius?: number;
+    max_results?: number;
+    min_exposure?: number;
+    start_date?: string;  // ISO "YYYY-MM-DD"
+    end_date?: string;    // ISO "YYYY-MM-DD"
+  }): Promise> {
+    return apiClient.post('/api/archive/search/coordinates', {
+      ra: params.ra,
+      dec: params.dec,
+      mission: params.mission,
+      radius: params.radius ?? 0.5,
+      max_results: params.max_results ?? 100,
+      min_exposure: params.min_exposure,
+      start_date: params.start_date,
+      end_date: params.end_date,
+    });
+  },
+
+  /**
+   * Search HEASARC by Observation ID
+   */
+  async searchByObsid(params: {
+    obsid: string;
+    mission: string;
+  }): Promise> {
+    return apiClient.post('/api/archive/search/obsid', {
+      obsid: params.obsid,
+      mission: params.mission,
+    });
+  },
+
+  /**
+   * Get download URLs for an observation
+   */
+  async getObservationUrls(
+    mission: string,
+    obsid: string
+  ): Promise> {
+    return apiClient.get(`/api/archive/observation/${mission}/${obsid}`);
+  },
+
+  /**
+   * List all files in an observation directory
+   *
+   * Returns a tree structure of files with metadata including sizes,
+   * file type classification, and download URLs.
+   *
+   * @param params.obs_data - Additional observation data for directory lookup:
+   *   - ra/dec: Coordinates for locate_data query (helps find directory)
+   *   - prnb: RXTE proposal number (required for RXTE)
+   */
+  async listObservationFiles(params: {
+    mission: string;
+    obsid: string;
+    obs_time?: string;
+    obs_data?: {
+      ra?: number | null;
+      dec?: number | null;
+      prnb?: string;
+    };
+    recursive?: boolean;
+    max_depth?: number;
+  }): Promise> {
+    return apiClient.post('/api/archive/list-files', {
+      mission: params.mission,
+      obsid: params.obsid,
+      obs_time: params.obs_time,
+      obs_data: params.obs_data,
+      recursive: params.recursive ?? true,
+      max_depth: params.max_depth ?? 3,
+    });
+  },
+
+  /**
+   * Download a file from URL to local disk with SSE progress streaming.
+   *
+   * This function routes the download through the backend to bypass CORS
+   * restrictions. Progress is streamed as SSE events.
+   *
+   * @param params.url - URL to download from (e.g., HEASARC HTTPS URL)
+   * @param params.save_path - Local path to save the file
+   * @yields DownloadToDiskEvent - Progress, complete, or error events
+   */
+  async *downloadToDiskSSE(params: {
+    url: string;
+    save_path: string;
+  }): AsyncGenerator {
+    const port = await apiClient.getPort();
+    const endpointUrl = `http://localhost:${port}/api/archive/download-to-disk`;
+
+    const response = await fetch(endpointUrl, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        url: params.url,
+        save_path: params.save_path,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+    }
+
+    const reader = response.body?.getReader();
+    if (!reader) {
+      throw new Error('No response body available for streaming');
+    }
+
+    const decoder = new TextDecoder();
+    let buffer = '';
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+
+        // Parse SSE format: "data: {...}\n\n"
+        const lines = buffer.split('\n\n');
+        buffer = lines.pop() || '';
+
+        for (const line of lines) {
+          if (line.startsWith('data: ')) {
+            const jsonStr = line.slice(6);
+            try {
+              const event = JSON.parse(jsonStr) as DownloadToDiskEvent;
+              yield event;
+            } catch (parseError) {
+              console.error('Failed to parse SSE event:', parseError, jsonStr);
+            }
+          }
+        }
+      }
+
+      // Process remaining buffer
+      if (buffer.trim() && buffer.startsWith('data: ')) {
+        const jsonStr = buffer.slice(6).trim();
+        if (jsonStr) {
+          try {
+            const event = JSON.parse(jsonStr) as DownloadToDiskEvent;
+            yield event;
+          } catch (parseError) {
+            console.error('Failed to parse final SSE event:', parseError, jsonStr);
+          }
+        }
+      }
+    } finally {
+      reader.releaseLock();
+    }
+  },
+};
+
+export default archiveApi;
diff --git a/src/api/client.ts b/src/api/client.ts
new file mode 100644
index 0000000..bf0a0f4
--- /dev/null
+++ b/src/api/client.ts
@@ -0,0 +1,116 @@
+/**
+ * API client for communicating with the Python backend
+ */
+
+export interface ApiResponse {
+  success: boolean;
+  data: T | null;
+  message: string;
+  error: string | null;
+}
+
+class ApiClient {
+  private baseUrl: string = 'http://127.0.0.1:8765';
+  private port: number = 8765;
+
+  async setPort(port: number): Promise {
+    this.port = port;
+    this.baseUrl = `http://127.0.0.1:${port}`;
+  }
+
+  async getPort(): Promise {
+    // Try to get port from Electron IPC
+    if (window.electronAPI?.getBackendPort) {
+      try {
+        const port = await window.electronAPI.getBackendPort();
+        if (port && port !== this.port) {
+          console.log(`[ApiClient] Port updated: ${this.port} -> ${port}`);
+          await this.setPort(port);
+        }
+      } catch (error) {
+        console.warn('[ApiClient] Failed to get backend port from Electron:', error);
+      }
+    }
+    return this.port;
+  }
+
+  async request(
+    endpoint: string,
+    options: RequestInit = {}
+  ): Promise> {
+    // Always sync port before request
+    await this.getPort();
+    const url = `${this.baseUrl}${endpoint}`;
+
+    console.log(`[ApiClient] ${options.method || 'GET'} ${url}`);
+
+    const defaultHeaders: Record = {
+      'Content-Type': 'application/json',
+    };
+
+    const config: RequestInit = {
+      ...options,
+      headers: {
+        ...defaultHeaders,
+        ...options.headers,
+      },
+    };
+
+    try {
+      const response = await fetch(url, config);
+
+      if (!response.ok) {
+        const errorData = await response.json().catch(() => ({}));
+        console.error(`[ApiClient] HTTP ${response.status}:`, errorData);
+        return {
+          success: false,
+          data: null,
+          message: errorData.message || `HTTP error: ${response.status}`,
+          error: errorData.error || response.statusText,
+        };
+      }
+
+      const data = await response.json();
+      console.log(`[ApiClient] Response:`, data.success ? 'success' : 'failed');
+      return data;
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+      console.error(`[ApiClient] Fetch error:`, error);
+      return {
+        success: false,
+        data: null,
+        message: `Request failed: ${errorMessage}`,
+        error: errorMessage,
+      };
+    }
+  }
+
+  async get(endpoint: string): Promise> {
+    return this.request(endpoint, { method: 'GET' });
+  }
+
+  async post(endpoint: string, data?: unknown): Promise> {
+    return this.request(endpoint, {
+      method: 'POST',
+      body: data ? JSON.stringify(data) : undefined,
+    });
+  }
+
+  async delete(endpoint: string): Promise> {
+    return this.request(endpoint, { method: 'DELETE' });
+  }
+
+  async healthCheck(): Promise {
+    try {
+      const response = await this.get<{ status: string }>('/health');
+      return response.success && response.data?.status === 'healthy';
+    } catch {
+      return false;
+    }
+  }
+}
+
+// Export singleton instance
+export const apiClient = new ApiClient();
+
+export default apiClient;
diff --git a/src/api/dataApi.ts b/src/api/dataApi.ts
new file mode 100644
index 0000000..cc1f892
--- /dev/null
+++ b/src/api/dataApi.ts
@@ -0,0 +1,755 @@
+/**
+ * API functions for EventList data operations
+ */
+
+import { apiClient, ApiResponse } from './client';
+
+// Types
+
+/** Validation check result from data quality checks */
+export interface ValidationIssue {
+  type: string;
+  name?: string;
+  description?: string;
+  status: 'pass' | 'fail' | 'skip';
+  severity: 'error' | 'warning' | 'pass' | 'skip';
+  message: string;
+  count?: number;
+  total?: number;
+}
+
+/** Per-GTI rate information */
+export interface PerGtiRate {
+  start: number;
+  stop: number;
+  events: number;
+  duration: number;
+  rate: number;
+}
+
+/** FITS header information extracted from the event file */
+export interface FitsHeaderInfo {
+  object?: string;
+  obs_id?: string;
+  ra_nom?: number;
+  dec_nom?: number;
+  ra_obj?: number;
+  dec_obj?: number;
+  exposure?: number;
+  ontime?: number;
+  livetime?: number;
+  date_obs?: string;
+  date_end?: string;
+  tstart?: number;
+  tstop?: number;
+  creator?: string;
+  telescop?: string;
+  instrume?: string;
+  datamode?: string;
+  observer?: string;
+  raw_header?: Record;
+}
+
+export interface EventListSummary {
+  name: string;
+  n_events: number;
+  time_range: [number, number];
+  has_energy?: boolean;
+  has_pi?: boolean;
+  gti_count?: number;
+  gti_warnings?: string[] | null;
+  stingray_warnings?: string[] | null;
+  validation_issues?: ValidationIssue[] | null;
+  notes?: string | null;
+}
+
+export interface EventListInfo extends EventListSummary {
+  duration: number;
+  mjdref: number | null;
+  // GTI details
+  gti_list?: [number, number][];
+  total_gti_time?: number;
+  // Energy/PI range
+  energy_range?: [number, number];
+  pi_range?: [number, number];
+  // Mission metadata
+  mission?: string;
+  instrument?: string;
+  // Time statistics
+  mean_count_rate?: number;
+  min_time_diff?: number;
+  max_time_diff?: number;
+  mean_time_diff?: number;
+  median_time_diff?: number;
+  std_time_diff?: number;
+  // Per-GTI rates
+  per_gti_rates?: PerGtiRate[];
+}
+
+export interface MemoryInfo {
+  total_mb: number;
+  available_mb: number;
+  used_mb: number;
+  percent: number;
+  process_mb: number;
+}
+
+export interface FileSizeInfo {
+  file_size_bytes: number;
+  file_size_mb: number;
+  file_size_gb: number;
+  risk_level: 'safe' | 'caution' | 'risky' | 'critical';
+  recommend_lazy: boolean;
+  estimated_memory_mb?: number;
+  ram_usage_percent?: number;
+  memory_info?: MemoryInfo;
+}
+
+// Lazy loading types
+export interface LazyLoadingInfo {
+  method: 'time_range' | 'event_count';
+  // For time_range method
+  requested_range?: [number, number];
+  actual_range?: [number, number];
+  loaded_duration?: number;
+  // For event_count method
+  start_index?: number;
+  end_index?: number;
+  events_requested?: number;
+  events_loaded?: number;
+  // Common fields
+  total_file_duration: number;
+  total_file_events: number;
+  events_loaded_percent: number;
+}
+
+export interface EventListLazyLoadedSummary extends EventListSummary {
+  lazy_loading_info?: LazyLoadingInfo;
+  // These are inherited from EventListSummary but explicitly listed for clarity:
+  // validation_issues?: ValidationIssue[] | null;
+  // notes?: string | null;
+}
+
+export interface LoadingRecommendation {
+  can_load_full: boolean;
+  recommend_lazy: boolean;
+  suggested_chunk_size: number | null;
+  suggested_time_chunk: number | null;
+  strategy: 'full' | 'time_range' | 'event_count';
+}
+
+export interface FileMetadata {
+  file_path: string;
+  file_size_mb: number;
+  file_size_gb: number;
+  risk_level: 'safe' | 'caution' | 'risky' | 'critical';
+  total_events: number;
+  time_range: [number | null, number | null];
+  duration: number;
+  gti_count: number;
+  total_gti_time: number | null;
+  gti_list: [number, number][] | null;
+  mjdref: number | null;
+  mission: string | null;
+  instrument: string | null;
+  available_columns: string[];
+  recommended_loading: LoadingRecommendation;
+}
+
+// Batch loading types
+export interface SingleFileConfig {
+  file_path: string;
+  name: string;
+  fmt?: string;
+  rmf_file?: string;
+  additional_columns?: string[];
+  high_precision?: boolean;
+  skip_checks?: boolean;
+  use_partial_loading?: boolean;
+  partial_mode?: 'time_range' | 'event_count';
+  time_range_start?: number;
+  time_range_end?: number;
+  event_start_index?: number;
+  event_count?: number;
+  notes?: string;
+}
+
+export interface BatchLoadRequest {
+  files: SingleFileConfig[];
+  use_same_settings: boolean;
+  // Shared settings (used when use_same_settings=true)
+  shared_fmt?: string;
+  shared_rmf_file?: string;
+  shared_additional_columns?: string[];
+  shared_high_precision?: boolean;
+  shared_skip_checks?: boolean;
+  shared_use_partial_loading?: boolean;
+  shared_partial_mode?: 'time_range' | 'event_count';
+  shared_time_range_start?: number;
+  shared_time_range_end?: number;
+  shared_event_start_index?: number;
+  shared_event_count?: number;
+}
+
+export interface BatchLoadSuccessItem {
+  name: string;
+  file_path: string;
+  data: EventListSummary | null;
+  message?: string;
+}
+
+export interface BatchLoadFailedItem {
+  name: string;
+  file_path: string;
+  error: string;
+}
+
+export interface BatchLoadSummary {
+  total_files: number;
+  success_count: number;
+  failure_count: number;
+  total_events_loaded: number;
+  total_time_ms: number;
+  workers_used: number;
+}
+
+export interface BatchLoadResult {
+  successful: BatchLoadSuccessItem[];
+  failed: BatchLoadFailedItem[];
+  summary: BatchLoadSummary;
+}
+
+export interface BatchFileSizeInfo {
+  file_path: string;
+  file_name: string;
+  size_mb: number;
+  estimated_ram_mb: number;
+  ram_percent: number;
+  risk_level: 'safe' | 'caution' | 'risky' | 'critical';
+  error?: string;
+}
+
+export interface BatchSizeTotals {
+  size_mb: number;
+  estimated_ram_mb: number;
+  ram_percent: number;
+  risk_level: 'safe' | 'caution' | 'risky' | 'critical';
+}
+
+export interface BatchSizeResult {
+  files: BatchFileSizeInfo[];
+  total: BatchSizeTotals;
+  available_ram_mb: number;
+  file_count: number;
+  recommend_partial_loading: boolean;
+}
+
+// SSE Streaming types for batch loading
+export interface BatchStreamEventFileComplete {
+  type: 'file_complete';
+  name: string;
+  file_path: string;
+  success: boolean;
+  completed: number;
+  total: number;
+  data?: EventListSummary;
+  error?: string;
+}
+
+export interface BatchStreamEventComplete {
+  type: 'complete';
+  total_time_ms: number;
+  success_count: number;
+  failure_count: number;
+  total_events: number;
+  workers_used: number;
+}
+
+export interface BatchStreamEventError {
+  type: 'error';
+  error: string;
+}
+
+export type BatchStreamEvent =
+  | BatchStreamEventFileComplete
+  | BatchStreamEventComplete
+  | BatchStreamEventError;
+
+// URL Download SSE Streaming types
+export interface UrlDownloadProgressEvent {
+  type: 'progress';
+  bytes_downloaded: number;
+  total_bytes: number;
+  percent: number;
+}
+
+export interface UrlDownloadProcessingEvent {
+  type: 'processing';
+  message: string;
+}
+
+export interface UrlDownloadCompleteEvent {
+  type: 'complete';
+  data: EventListSummary;
+  message: string;
+}
+
+export interface UrlDownloadErrorEvent {
+  type: 'error';
+  error: string;
+}
+
+export type UrlDownloadStreamEvent =
+  | UrlDownloadProgressEvent
+  | UrlDownloadProcessingEvent
+  | UrlDownloadCompleteEvent
+  | UrlDownloadErrorEvent;
+
+export interface EventListFullPreview {
+  name: string;
+  // Core data
+  times_preview: number[];
+  n_events: number;
+  time_range: [number, number];
+  duration: number;
+  // Energy data
+  has_energy: boolean;
+  energy_preview: number[] | null;
+  energy_range: [number, number] | null;
+  // PI data
+  has_pi: boolean;
+  pi_preview: number[] | null;
+  pi_range: [number, number] | null;
+  // GTI data
+  gti_count: number;
+  gti_list: [number, number][] | null;
+  total_gti_time: number | null;
+  // Reference time
+  mjdref: number | null;
+  // Metadata
+  mission: string | null;
+  instrument: string | null;
+  detector_id: string | null;
+  ephem: string | null;
+  timeref: string | null;
+  timesys: string | null;
+  // Statistics
+  mean_count_rate: number | null;
+  min_time_diff: number | null;
+  max_time_diff: number | null;
+  mean_time_diff?: number | null;
+  // Enhanced time statistics
+  median_time_diff?: number | null;
+  std_time_diff?: number | null;
+  // Per-GTI rates
+  per_gti_rates?: PerGtiRate[] | null;
+  // Additional columns
+  additional_columns: string[];
+  // User notes
+  notes?: string | null;
+  // Data validation
+  validation_issues?: ValidationIssue[] | null;
+  // FITS header information
+  header_info?: FitsHeaderInfo | null;
+}
+
+// API functions
+export const dataApi = {
+  /**
+   * Load an EventList from a file
+   */
+  async loadEventList(params: {
+    file_path: string;
+    name: string;
+    fmt?: string;
+    rmf_file?: string;
+    additional_columns?: string[];
+    high_precision?: boolean;
+    skip_checks?: boolean;
+    notes?: string;
+  }): Promise> {
+    return apiClient.post('/api/data/load', {
+      file_path: params.file_path,
+      name: params.name,
+      fmt: params.fmt || 'ogip',
+      rmf_file: params.rmf_file,
+      additional_columns: params.additional_columns,
+      high_precision: params.high_precision || false,
+      skip_checks: params.skip_checks || false,
+      notes: params.notes,
+    });
+  },
+
+  /**
+   * Load an EventList from a URL
+   */
+  async loadEventListFromUrl(params: {
+    url: string;
+    name: string;
+    fmt?: string;
+    rmf_file?: string;
+    additional_columns?: string[];
+    high_precision?: boolean;
+    skip_checks?: boolean;
+    notes?: string;
+  }): Promise> {
+    return apiClient.post('/api/data/load-url', {
+      url: params.url,
+      name: params.name,
+      fmt: params.fmt || 'ogip',
+      rmf_file: params.rmf_file,
+      additional_columns: params.additional_columns,
+      high_precision: params.high_precision || false,
+      skip_checks: params.skip_checks || false,
+      notes: params.notes,
+    });
+  },
+
+  /**
+   * Load an EventList from a URL with SSE streaming for progress updates.
+   *
+   * This function returns an async generator that yields UrlDownloadStreamEvent
+   * objects as the download progresses. This allows the UI to show real-time
+   * download progress.
+   *
+   * @param params - URL loading parameters
+   * @yields UrlDownloadStreamEvent - Progress, processing, complete, or error events
+   */
+  async *loadEventListFromUrlSSE(params: {
+    url: string;
+    name: string;
+    fmt?: string;
+    rmf_file?: string;
+    additional_columns?: string[];
+    high_precision?: boolean;
+    skip_checks?: boolean;
+    notes?: string;
+  }): AsyncGenerator {
+    const port = await apiClient.getPort();
+    const url = `http://localhost:${port}/api/data/load-url-stream`;
+
+    const response = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        url: params.url,
+        name: params.name,
+        fmt: params.fmt || 'ogip',
+        rmf_file: params.rmf_file,
+        additional_columns: params.additional_columns,
+        high_precision: params.high_precision || false,
+        skip_checks: params.skip_checks || false,
+        notes: params.notes,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+    }
+
+    const reader = response.body?.getReader();
+    if (!reader) {
+      throw new Error('No response body available for streaming');
+    }
+
+    const decoder = new TextDecoder();
+    let buffer = '';
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+
+        // Parse SSE format: "data: {...}\n\n"
+        const lines = buffer.split('\n\n');
+        buffer = lines.pop() || '';
+
+        for (const line of lines) {
+          if (line.startsWith('data: ')) {
+            const jsonStr = line.slice(6);
+            try {
+              const event = JSON.parse(jsonStr) as UrlDownloadStreamEvent;
+              yield event;
+            } catch (parseError) {
+              console.error('Failed to parse SSE event:', parseError, jsonStr);
+            }
+          }
+        }
+      }
+
+      // Process remaining buffer
+      if (buffer.trim() && buffer.startsWith('data: ')) {
+        const jsonStr = buffer.slice(6).trim();
+        if (jsonStr) {
+          try {
+            const event = JSON.parse(jsonStr) as UrlDownloadStreamEvent;
+            yield event;
+          } catch (parseError) {
+            console.error('Failed to parse final SSE event:', parseError, jsonStr);
+          }
+        }
+      }
+    } finally {
+      reader.releaseLock();
+    }
+  },
+
+  /**
+   * Save an EventList to disk
+   */
+  async saveEventList(params: {
+    name: string;
+    file_path: string;
+    fmt?: string;
+  }): Promise> {
+    return apiClient.post('/api/data/save', {
+      name: params.name,
+      file_path: params.file_path,
+      fmt: params.fmt || 'hdf5',
+    });
+  },
+
+  /**
+   * Delete an EventList from state
+   */
+  async deleteEventList(name: string): Promise> {
+    return apiClient.delete(`/api/data/${encodeURIComponent(name)}`);
+  },
+
+  /**
+   * Get information about an EventList
+   */
+  async getEventListInfo(name: string): Promise> {
+    return apiClient.get(`/api/data/${encodeURIComponent(name)}`);
+  },
+
+  /**
+   * List all loaded EventLists
+   */
+  async listEventLists(): Promise> {
+    return apiClient.get('/api/data/');
+  },
+
+  /**
+   * Check file size and get loading recommendations
+   */
+  async checkFileSize(file_path: string): Promise> {
+    return apiClient.post('/api/data/check-size', { file_path });
+  },
+
+  /**
+   * Clear all loaded EventLists from memory
+   */
+  async clearAllEventLists(): Promise> {
+    return apiClient.delete('/api/data/');
+  },
+
+  /**
+   * Get full preview of an EventList with all attributes
+   */
+  async getEventListFullPreview(
+    name: string,
+    timeLimit?: number
+  ): Promise> {
+    const params = timeLimit ? `?time_limit=${timeLimit}` : '';
+    return apiClient.get(`/api/data/${encodeURIComponent(name)}/full-preview${params}`);
+  },
+
+  // =========================================================================
+  // TRUE LAZY LOADING API FUNCTIONS
+  // These use FITSTimeseriesReader for genuine lazy/streaming I/O
+  // =========================================================================
+
+  /**
+   * Load events within a specific time range using true lazy loading.
+   * Only reads the events within the time window from disk.
+   */
+  async loadEventListByTimeRange(params: {
+    file_path: string;
+    name: string;
+    start_time: number;
+    end_time: number;
+    fmt?: string;
+    notes?: string;
+  }): Promise> {
+    return apiClient.post('/api/data/load-by-time-range', {
+      file_path: params.file_path,
+      name: params.name,
+      start_time: params.start_time,
+      end_time: params.end_time,
+      fmt: params.fmt || 'ogip',
+      notes: params.notes,
+    });
+  },
+
+  /**
+   * Load a specific number of events using true lazy loading.
+   * Only reads the requested event range from disk.
+   */
+  async loadEventListByEventCount(params: {
+    file_path: string;
+    name: string;
+    start_index?: number;
+    count?: number;
+    fmt?: string;
+    notes?: string;
+  }): Promise> {
+    return apiClient.post('/api/data/load-by-event-count', {
+      file_path: params.file_path,
+      name: params.name,
+      start_index: params.start_index ?? 0,
+      count: params.count ?? 10000,
+      fmt: params.fmt || 'ogip',
+      notes: params.notes,
+    });
+  },
+
+  /**
+   * Get file metadata without loading the full data.
+   * Returns event count, time range, GTI, and loading recommendations.
+   */
+  async getFileMetadata(params: {
+    file_path: string;
+    fmt?: string;
+  }): Promise> {
+    return apiClient.post('/api/data/metadata', {
+      file_path: params.file_path,
+      fmt: params.fmt || 'ogip',
+    });
+  },
+
+  // =========================================================================
+  // BATCH LOADING API FUNCTIONS
+  // Load multiple files in parallel
+  // =========================================================================
+
+  /**
+   * Check sizes of multiple files and estimate total memory usage.
+   * Returns per-file and total memory estimates with risk levels.
+   */
+  async checkBatchFileSize(
+    file_paths: string[]
+  ): Promise> {
+    return apiClient.post('/api/data/check-batch-size', { file_paths });
+  },
+
+  /**
+   * Load multiple EventLists in parallel using threads.
+   *
+   * @param params.files - Array of file configurations
+   * @param params.use_same_settings - If true, use shared_* settings for all files
+   * @param params.shared_* - Shared settings applied when use_same_settings=true
+   */
+  async loadBatchEventLists(
+    params: BatchLoadRequest
+  ): Promise> {
+    return apiClient.post('/api/data/load-batch', {
+      files: params.files,
+      use_same_settings: params.use_same_settings,
+      shared_fmt: params.shared_fmt || 'ogip',
+      shared_rmf_file: params.shared_rmf_file,
+      shared_additional_columns: params.shared_additional_columns,
+      shared_high_precision: params.shared_high_precision || false,
+      shared_skip_checks: params.shared_skip_checks || false,
+      shared_use_partial_loading: params.shared_use_partial_loading || false,
+      shared_partial_mode: params.shared_partial_mode || 'time_range',
+      shared_time_range_start: params.shared_time_range_start,
+      shared_time_range_end: params.shared_time_range_end,
+      shared_event_start_index: params.shared_event_start_index,
+      shared_event_count: params.shared_event_count,
+    });
+  },
+
+  /**
+   * Load multiple EventLists with SSE streaming for real-time progress.
+   *
+   * This function returns an async generator that yields BatchStreamEvent
+   * objects as each file completes loading. This allows the UI to update
+   * immediately when each file finishes rather than waiting for all files.
+   *
+   * @param params - Same parameters as loadBatchEventLists
+   * @yields BatchStreamEvent - Events for each file completion and final summary
+   */
+  async *loadBatchEventListsSSE(
+    params: BatchLoadRequest
+  ): AsyncGenerator {
+    const port = await apiClient.getPort();
+    const url = `http://localhost:${port}/api/data/load-batch-stream`;
+
+    const response = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        files: params.files,
+        use_same_settings: params.use_same_settings,
+        shared_fmt: params.shared_fmt || 'ogip',
+        shared_rmf_file: params.shared_rmf_file,
+        shared_additional_columns: params.shared_additional_columns,
+        shared_high_precision: params.shared_high_precision || false,
+        shared_skip_checks: params.shared_skip_checks || false,
+        shared_use_partial_loading: params.shared_use_partial_loading || false,
+        shared_partial_mode: params.shared_partial_mode || 'time_range',
+        shared_time_range_start: params.shared_time_range_start,
+        shared_time_range_end: params.shared_time_range_end,
+        shared_event_start_index: params.shared_event_start_index,
+        shared_event_count: params.shared_event_count,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+    }
+
+    const reader = response.body?.getReader();
+    if (!reader) {
+      throw new Error('No response body available for streaming');
+    }
+
+    const decoder = new TextDecoder();
+    let buffer = '';
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+
+        // Parse SSE format: "data: {...}\n\n"
+        const lines = buffer.split('\n\n');
+        buffer = lines.pop() || ''; // Keep incomplete chunk
+
+        for (const line of lines) {
+          if (line.startsWith('data: ')) {
+            const jsonStr = line.slice(6);
+            try {
+              const event = JSON.parse(jsonStr) as BatchStreamEvent;
+              yield event;
+            } catch (parseError) {
+              console.error('Failed to parse SSE event:', parseError, jsonStr);
+            }
+          }
+        }
+      }
+
+      // Process any remaining data in the buffer
+      if (buffer.trim() && buffer.startsWith('data: ')) {
+        const jsonStr = buffer.slice(6).trim();
+        if (jsonStr) {
+          try {
+            const event = JSON.parse(jsonStr) as BatchStreamEvent;
+            yield event;
+          } catch (parseError) {
+            console.error('Failed to parse final SSE event:', parseError, jsonStr);
+          }
+        }
+      }
+    } finally {
+      reader.releaseLock();
+    }
+  },
+};
+
+export default dataApi;
diff --git a/src/api/exportApi.ts b/src/api/exportApi.ts
new file mode 100644
index 0000000..afe91ed
--- /dev/null
+++ b/src/api/exportApi.ts
@@ -0,0 +1,86 @@
+/**
+ * API functions for data export operations
+ */
+
+import { apiClient, ApiResponse } from './client';
+
+// Types
+export interface ExportResult {
+  file_path: string;
+  n_rows?: number;
+}
+
+// API functions
+export const exportApi = {
+  /**
+   * Export an EventList to CSV file
+   */
+  async exportEventListToCsv(params: {
+    name: string;
+    file_path: string;
+  }): Promise> {
+    return apiClient.post('/api/export/event-list/csv', params);
+  },
+
+  /**
+   * Export a Lightcurve to CSV file
+   */
+  async exportLightcurveToCsv(params: {
+    name: string;
+    file_path: string;
+  }): Promise> {
+    return apiClient.post('/api/export/lightcurve/csv', params);
+  },
+
+  /**
+   * Export a spectrum to CSV file
+   */
+  async exportSpectrumToCsv(params: {
+    name: string;
+    file_path: string;
+  }): Promise> {
+    return apiClient.post('/api/export/spectrum/csv', params);
+  },
+
+  /**
+   * Export a bispectrum to CSV file
+   */
+  async exportBispectrumToCsv(params: {
+    name: string;
+    file_path: string;
+  }): Promise> {
+    return apiClient.post('/api/export/bispectrum/csv', params);
+  },
+
+  /**
+   * Export data to HDF5 file
+   */
+  async exportToHdf5(params: {
+    name: string;
+    file_path: string;
+    data_type?: 'event_list' | 'lightcurve' | 'spectrum';
+  }): Promise> {
+    return apiClient.post('/api/export/hdf5', {
+      name: params.name,
+      file_path: params.file_path,
+      data_type: params.data_type || 'event_list',
+    });
+  },
+
+  /**
+   * Export data to FITS file
+   */
+  async exportToFits(params: {
+    name: string;
+    file_path: string;
+    data_type?: 'event_list';
+  }): Promise> {
+    return apiClient.post('/api/export/fits', {
+      name: params.name,
+      file_path: params.file_path,
+      data_type: params.data_type || 'event_list',
+    });
+  },
+};
+
+export default exportApi;
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 0000000..1d07bfd
--- /dev/null
+++ b/src/api/index.ts
@@ -0,0 +1,21 @@
+/**
+ * API module exports
+ */
+
+export { apiClient, type ApiResponse } from './client';
+export { dataApi, type EventListSummary, type EventListInfo, type FileSizeInfo } from './dataApi';
+export { lightcurveApi, type LightcurveData, type LightcurveSummary } from './lightcurveApi';
+export {
+  spectrumApi,
+  type PowerSpectrumData,
+  type DynamicalPowerSpectrumData,
+  type SpectrumSummary,
+} from './spectrumApi';
+export {
+  timingApi,
+  type BispectrumData,
+  type PowerColorsData,
+  type TimeLagsData,
+  type CoherenceData,
+} from './timingApi';
+export { exportApi, type ExportResult } from './exportApi';
diff --git a/src/api/jobApi.ts b/src/api/jobApi.ts
new file mode 100644
index 0000000..f38bf70
--- /dev/null
+++ b/src/api/jobApi.ts
@@ -0,0 +1,207 @@
+/**
+ * API functions for background job operations.
+ */
+
+import { apiClient, ApiResponse } from './client';
+import type {
+  Job,
+  JobStreamEvent,
+  NameConflictResult,
+  SubmitLoadJobParams,
+  SubmitBatchJobParams,
+  SubmitUrlJobParams,
+} from '@/types/job';
+
+export const jobApi = {
+  /**
+   * Submit a single file load job.
+   *
+   * Returns immediately with the job ID. The actual loading happens
+   * asynchronously in a background thread.
+   */
+  async submitLoadJob(params: SubmitLoadJobParams): Promise> {
+    return apiClient.post('/api/jobs/submit-load', {
+      file_path: params.file_path,
+      name: params.name,
+      fmt: params.fmt || 'ogip',
+      rmf_file: params.rmf_file,
+      additional_columns: params.additional_columns,
+      high_precision: params.high_precision || false,
+      skip_checks: params.skip_checks || false,
+      notes: params.notes,
+      use_partial_loading: params.use_partial_loading || false,
+      partial_mode: params.partial_mode || 'time_range',
+      time_range_start: params.time_range_start,
+      time_range_end: params.time_range_end,
+      event_start_index: params.event_start_index,
+      event_count: params.event_count,
+    });
+  },
+
+  /**
+   * Submit a batch load job for multiple files.
+   *
+   * Returns immediately with the job ID. The actual loading happens
+   * asynchronously in a background thread.
+   */
+  async submitBatchJob(params: SubmitBatchJobParams): Promise> {
+    return apiClient.post('/api/jobs/submit-batch', {
+      files: params.files,
+      use_same_settings: params.use_same_settings ?? true,
+      shared_fmt: params.shared_fmt || 'ogip',
+      shared_rmf_file: params.shared_rmf_file,
+      shared_additional_columns: params.shared_additional_columns,
+      shared_high_precision: params.shared_high_precision || false,
+      shared_skip_checks: params.shared_skip_checks || false,
+      shared_use_partial_loading: params.shared_use_partial_loading || false,
+      shared_partial_mode: params.shared_partial_mode || 'time_range',
+      shared_time_range_start: params.shared_time_range_start,
+      shared_time_range_end: params.shared_time_range_end,
+      shared_event_start_index: params.shared_event_start_index,
+      shared_event_count: params.shared_event_count,
+    });
+  },
+
+  /**
+   * Submit a URL download and load job.
+   *
+   * Returns immediately with the job ID. The actual download and loading
+   * happens asynchronously in a background thread.
+   */
+  async submitUrlJob(params: SubmitUrlJobParams): Promise> {
+    return apiClient.post('/api/jobs/submit-url', {
+      url: params.url,
+      name: params.name,
+      fmt: params.fmt || 'ogip',
+      rmf_file: params.rmf_file,
+      additional_columns: params.additional_columns,
+      high_precision: params.high_precision || false,
+      skip_checks: params.skip_checks || false,
+      notes: params.notes,
+    });
+  },
+
+  /**
+   * List all jobs.
+   *
+   * @param includeCompleted - Include completed/failed/cancelled jobs
+   * @param limit - Maximum number of jobs to return
+   */
+  async listJobs(
+    includeCompleted: boolean = true,
+    limit: number = 50
+  ): Promise> {
+    return apiClient.get(
+      `/api/jobs/?include_completed=${includeCompleted}&limit=${limit}`
+    );
+  },
+
+  /**
+   * Get all active (pending or running) jobs.
+   */
+  async getActiveJobs(): Promise> {
+    return apiClient.get('/api/jobs/active');
+  },
+
+  /**
+   * Get a specific job by ID.
+   */
+  async getJob(jobId: string): Promise> {
+    return apiClient.get(`/api/jobs/${jobId}`);
+  },
+
+  /**
+   * Cancel a pending job.
+   *
+   * Only pending jobs can be cancelled. Running jobs cannot be interrupted.
+   */
+  async cancelJob(jobId: string): Promise> {
+    return apiClient.post(`/api/jobs/${jobId}/cancel`);
+  },
+
+  /**
+   * Check if a name conflicts with existing data or pending jobs.
+   */
+  async checkNameConflict(name: string): Promise> {
+    return apiClient.post('/api/jobs/check-name', { name });
+  },
+
+  /**
+   * Clear all completed/failed/cancelled jobs.
+   */
+  async clearCompletedJobs(): Promise> {
+    return apiClient.delete('/api/jobs/completed');
+  },
+
+  /**
+   * Create an SSE stream for job updates.
+   *
+   * This function returns an async generator that yields JobStreamEvent
+   * objects as jobs are created, updated, and completed.
+   *
+   * @yields JobStreamEvent - Events for job updates
+   */
+  async *streamJobUpdates(): AsyncGenerator {
+    const port = await apiClient.getPort();
+    const url = `http://localhost:${port}/api/jobs/stream`;
+
+    const response = await fetch(url, {
+      method: 'GET',
+      headers: { Accept: 'text/event-stream' },
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+    }
+
+    const reader = response.body?.getReader();
+    if (!reader) {
+      throw new Error('No response body available for streaming');
+    }
+
+    const decoder = new TextDecoder();
+    let buffer = '';
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+
+        // Parse SSE format: "data: {...}\n\n"
+        const lines = buffer.split('\n\n');
+        buffer = lines.pop() || ''; // Keep incomplete chunk
+
+        for (const line of lines) {
+          if (line.startsWith('data: ')) {
+            const jsonStr = line.slice(6);
+            try {
+              const event = JSON.parse(jsonStr) as JobStreamEvent;
+              yield event;
+            } catch (parseError) {
+              console.error('[JobApi] Failed to parse SSE event:', parseError, jsonStr);
+            }
+          }
+        }
+      }
+
+      // Process any remaining data in the buffer
+      if (buffer.trim() && buffer.startsWith('data: ')) {
+        const jsonStr = buffer.slice(6).trim();
+        if (jsonStr) {
+          try {
+            const event = JSON.parse(jsonStr) as JobStreamEvent;
+            yield event;
+          } catch (parseError) {
+            console.error('[JobApi] Failed to parse final SSE event:', parseError, jsonStr);
+          }
+        }
+      }
+    } finally {
+      reader.releaseLock();
+    }
+  },
+};
+
+export default jobApi;
diff --git a/src/api/lightcurveApi.ts b/src/api/lightcurveApi.ts
new file mode 100644
index 0000000..0aca4e8
--- /dev/null
+++ b/src/api/lightcurveApi.ts
@@ -0,0 +1,108 @@
+/**
+ * API functions for Lightcurve operations
+ */
+
+import { apiClient, ApiResponse } from './client';
+
+// Types
+export interface LightcurveData {
+  name: string;
+  time: number[];
+  counts: number[];
+  dt: number;
+  n_bins: number;
+  plot_stride?: number;
+  time_range?: [number, number];
+  count_rate_mean?: number;
+  count_stats?: {
+    mean: number;
+    std: number;
+    min: number;
+    max: number;
+  };
+}
+
+export interface LightcurveSummary {
+  name: string;
+  n_bins: number;
+  dt: number;
+  time_range: [number, number];
+}
+
+// API functions
+export const lightcurveApi = {
+  /**
+   * Create a Lightcurve from an EventList
+   */
+  async createFromEventList(params: {
+    event_list_name: string;
+    dt: number;
+    output_name: string;
+    gti?: number[][];
+    max_points?: number;
+  }): Promise> {
+    return apiClient.post('/api/lightcurve/from-event-list', {
+      event_list_name: params.event_list_name,
+      dt: params.dt,
+      output_name: params.output_name,
+      gti: params.gti,
+      max_points: params.max_points,
+    });
+  },
+
+  /**
+   * Create a Lightcurve from arrays
+   */
+  async createFromArrays(params: {
+    times: number[];
+    counts: number[];
+    dt: number;
+    output_name: string;
+  }): Promise> {
+    return apiClient.post('/api/lightcurve/from-arrays', params);
+  },
+
+  /**
+   * Rebin a lightcurve
+   */
+  async rebin(params: {
+    name: string;
+    rebin_factor: number;
+    output_name: string;
+    max_points?: number;
+  }): Promise> {
+    return apiClient.post('/api/lightcurve/rebin', {
+      name: params.name,
+      rebin_factor: params.rebin_factor,
+      output_name: params.output_name,
+      max_points: params.max_points,
+    });
+  },
+
+  /**
+   * Get lightcurve data for plotting
+   */
+  async getLightcurveData(
+    name: string,
+    maxPoints?: number
+  ): Promise> {
+    const query = maxPoints ? `?max_points=${maxPoints}` : '';
+    return apiClient.get(`/api/lightcurve/${encodeURIComponent(name)}${query}`);
+  },
+
+  /**
+   * List all loaded lightcurves
+   */
+  async listLightcurves(): Promise> {
+    return apiClient.get('/api/lightcurve/');
+  },
+
+  /**
+   * Delete a lightcurve from state
+   */
+  async deleteLightcurve(name: string): Promise> {
+    return apiClient.delete(`/api/lightcurve/${encodeURIComponent(name)}`);
+  },
+};
+
+export default lightcurveApi;
diff --git a/src/api/logApi.ts b/src/api/logApi.ts
new file mode 100644
index 0000000..7498777
--- /dev/null
+++ b/src/api/logApi.ts
@@ -0,0 +1,198 @@
+/**
+ * Log streaming API client using Server-Sent Events (SSE).
+ *
+ * Connects to the backend log stream and pushes log entries to the logStore.
+ */
+
+import { useLogStore } from '@/store/logStore';
+import { apiClient } from './client';
+
+/**
+ * Log entry received from the backend SSE stream.
+ */
+interface StreamLogEntry {
+  type: 'log' | 'heartbeat';
+  timestamp: string;
+  level?: 'info' | 'warn' | 'error' | 'debug';
+  source?: 'python';
+  logger?: string;
+  message?: string;
+}
+
+/**
+ * SSE client for real-time log streaming from the backend.
+ *
+ * Features:
+ * - Automatic reconnection with exponential backoff
+ * - Heartbeat handling to detect stale connections
+ * - Integration with Zustand logStore
+ */
+class LogStreamClient {
+  private eventSource: EventSource | null = null;
+  private reconnectAttempts: number = 0;
+  private maxReconnectAttempts: number = 10;
+  private reconnectTimer: ReturnType | null = null;
+  private isConnecting: boolean = false;
+
+  /**
+   * Connect to the log stream SSE endpoint.
+   *
+   * Will automatically reconnect on connection loss with exponential backoff.
+   */
+  async connect(): Promise {
+    // Avoid duplicate connections
+    if (this.eventSource || this.isConnecting) {
+      console.log('[LogStreamClient] Already connected or connecting');
+      return;
+    }
+
+    this.isConnecting = true;
+
+    try {
+      // Get current port from API client
+      const port = await apiClient.getPort();
+      const url = `http://127.0.0.1:${port}/api/logs/stream`;
+
+      console.log('[LogStreamClient] Connecting to:', url);
+
+      this.eventSource = new EventSource(url);
+
+      this.eventSource.onopen = (): void => {
+        console.log('[LogStreamClient] Connected to log stream');
+        this.reconnectAttempts = 0;
+        this.isConnecting = false;
+
+        // Add a log entry to indicate connection
+        useLogStore.getState().addLog({
+          level: 'info',
+          source: 'frontend',
+          message: 'Connected to Python log stream',
+        });
+      };
+
+      this.eventSource.onmessage = (event: MessageEvent): void => {
+        try {
+          const data: StreamLogEntry = JSON.parse(event.data);
+
+          // Skip heartbeat events
+          if (data.type === 'heartbeat') {
+            return;
+          }
+
+          // Add log entry to store
+          if (data.type === 'log' && data.level && data.message) {
+            useLogStore.getState().addLog({
+              level: data.level,
+              source: data.source || 'python',
+              message: data.message,
+            });
+          }
+        } catch (error) {
+          console.error('[LogStreamClient] Failed to parse log event:', error);
+        }
+      };
+
+      this.eventSource.onerror = (): void => {
+        console.warn('[LogStreamClient] Connection error, will attempt reconnect');
+        this.isConnecting = false;
+        this.handleDisconnect();
+      };
+    } catch (error) {
+      console.error('[LogStreamClient] Failed to connect:', error);
+      this.isConnecting = false;
+      this.scheduleReconnect();
+    }
+  }
+
+  /**
+   * Disconnect from the log stream.
+   */
+  disconnect(): void {
+    console.log('[LogStreamClient] Disconnecting');
+
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+    }
+
+    if (this.eventSource) {
+      this.eventSource.close();
+      this.eventSource = null;
+    }
+
+    this.reconnectAttempts = 0;
+    this.isConnecting = false;
+  }
+
+  /**
+   * Check if currently connected to the log stream.
+   */
+  isConnected(): boolean {
+    return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
+  }
+
+  /**
+   * Handle disconnection and schedule reconnect.
+   */
+  private handleDisconnect(): void {
+    if (this.eventSource) {
+      this.eventSource.close();
+      this.eventSource = null;
+    }
+
+    this.scheduleReconnect();
+  }
+
+  /**
+   * Schedule a reconnection attempt with exponential backoff.
+   */
+  private scheduleReconnect(): void {
+    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+      console.error('[LogStreamClient] Max reconnect attempts reached, giving up');
+      useLogStore.getState().addLog({
+        level: 'error',
+        source: 'frontend',
+        message: 'Log stream connection failed after maximum retries',
+      });
+      return;
+    }
+
+    // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s (capped at 32s)
+    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 32000);
+    this.reconnectAttempts++;
+
+    console.log(
+      `[LogStreamClient] Scheduling reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`
+    );
+
+    this.reconnectTimer = setTimeout(() => {
+      this.reconnectTimer = null;
+      this.connect();
+    }, delay);
+  }
+}
+
+// Export singleton instance
+export const logStreamClient = new LogStreamClient();
+
+/**
+ * Log API namespace for direct function access.
+ */
+export const logApi = {
+  /**
+   * Connect to the log stream.
+   */
+  connect: (): Promise => logStreamClient.connect(),
+
+  /**
+   * Disconnect from the log stream.
+   */
+  disconnect: (): void => logStreamClient.disconnect(),
+
+  /**
+   * Check if connected to the log stream.
+   */
+  isConnected: (): boolean => logStreamClient.isConnected(),
+};
+
+export default logApi;
diff --git a/src/api/spectrumApi.ts b/src/api/spectrumApi.ts
new file mode 100644
index 0000000..75576e8
--- /dev/null
+++ b/src/api/spectrumApi.ts
@@ -0,0 +1,166 @@
+/**
+ * API functions for spectrum operations
+ */
+
+import { apiClient, ApiResponse } from './client';
+
+// Types
+export interface PowerSpectrumData {
+  name: string | null;
+  freq: number[];
+  power: Array;
+  power_phase?: Array | null;
+  norm?: string;
+  n_freq: number;
+  df?: number;
+  freq_range?: [number, number];
+  segment_size?: number;
+  n_segments?: number;
+}
+
+export interface DynamicalPowerSpectrumData {
+  name: string | null;
+  freq: number[];
+  time: number[];
+  dyn_ps: Array>;
+  norm: string;
+  segment_size: number;
+  shape: [number, number];
+}
+
+export interface SpectrumSummary {
+  name: string;
+  type: string;
+  n_freq: number | null;
+}
+
+// API functions
+export const spectrumApi = {
+  /**
+   * Create a power spectrum from an EventList
+   */
+  async createPowerSpectrum(params: {
+    event_list_name: string;
+    dt: number;
+    norm?: string;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/spectrum/power-spectrum', {
+      event_list_name: params.event_list_name,
+      dt: params.dt,
+      norm: params.norm || 'leahy',
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Create an averaged power spectrum from an EventList
+   */
+  async createAveragedPowerSpectrum(params: {
+    event_list_name: string;
+    dt: number;
+    segment_size: number;
+    norm?: string;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/spectrum/averaged-power-spectrum', {
+      event_list_name: params.event_list_name,
+      dt: params.dt,
+      segment_size: params.segment_size,
+      norm: params.norm || 'leahy',
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Create a cross spectrum from two EventLists
+   */
+  async createCrossSpectrum(params: {
+    event_list_1_name: string;
+    event_list_2_name: string;
+    dt: number;
+    norm?: string;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/spectrum/cross-spectrum', {
+      event_list_1_name: params.event_list_1_name,
+      event_list_2_name: params.event_list_2_name,
+      dt: params.dt,
+      norm: params.norm || 'leahy',
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Create an averaged cross spectrum from two EventLists
+   */
+  async createAveragedCrossSpectrum(params: {
+    event_list_1_name: string;
+    event_list_2_name: string;
+    dt: number;
+    segment_size: number;
+    norm?: string;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/spectrum/averaged-cross-spectrum', {
+      event_list_1_name: params.event_list_1_name,
+      event_list_2_name: params.event_list_2_name,
+      dt: params.dt,
+      segment_size: params.segment_size,
+      norm: params.norm || 'leahy',
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Create a dynamical power spectrum from an EventList
+   */
+  async createDynamicalPowerSpectrum(params: {
+    event_list_name: string;
+    dt: number;
+    segment_size: number;
+    norm?: string;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/spectrum/dynamical-power-spectrum', {
+      event_list_name: params.event_list_name,
+      dt: params.dt,
+      segment_size: params.segment_size,
+      norm: params.norm || 'leahy',
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Rebin a spectrum
+   */
+  async rebinSpectrum(params: {
+    name: string;
+    rebin_factor: number;
+    log?: boolean;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/spectrum/rebin', {
+      name: params.name,
+      rebin_factor: params.rebin_factor,
+      log: params.log || false,
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * List all loaded spectra
+   */
+  async listSpectra(): Promise> {
+    return apiClient.get('/api/spectrum/');
+  },
+
+  /**
+   * Delete a spectrum from state
+   */
+  async deleteSpectrum(name: string): Promise> {
+    return apiClient.delete(`/api/spectrum/${encodeURIComponent(name)}`);
+  },
+};
+
+export default spectrumApi;
diff --git a/src/api/timingApi.ts b/src/api/timingApi.ts
new file mode 100644
index 0000000..89422b3
--- /dev/null
+++ b/src/api/timingApi.ts
@@ -0,0 +1,126 @@
+/**
+ * API functions for timing analysis operations
+ */
+
+import { apiClient, ApiResponse } from './client';
+
+// Types
+export interface BispectrumData {
+  name: string | null;
+  freq: number[];
+  lags: number[];
+  bispec_mag: number[][];
+  bispec_phase: number[][];
+  maxlag: number;
+  scale: string;
+  window: string;
+}
+
+export interface PowerColorsData {
+  name: string | null;
+  power_colors: Record>;
+  time: number[];
+  freq_ranges: Record;
+}
+
+export interface TimeLagsData {
+  name: string | null;
+  freq: number[];
+  time_lags: Array;
+  time_lags_err?: Array | null;
+  freq_range: [number, number] | null;
+}
+
+export interface CoherenceData {
+  name: string | null;
+  freq: number[];
+  coherence: Array;
+  coherence_err?: Array | null;
+  segment_size?: number;
+  n_segments?: number | null;
+}
+
+// API functions
+export const timingApi = {
+  /**
+   * Create a bispectrum from an EventList
+   */
+  async createBispectrum(params: {
+    event_list_name: string;
+    dt: number;
+    maxlag?: number;
+    scale?: string;
+    window?: string;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/timing/bispectrum', {
+      event_list_name: params.event_list_name,
+      dt: params.dt,
+      maxlag: params.maxlag || 25,
+      scale: params.scale || 'unbiased',
+      window: params.window || 'uniform',
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Calculate power colors from frequency bands
+   */
+  async calculatePowerColors(params: {
+    event_list_name: string;
+    dt: number;
+    segment_size: number;
+    freq_ranges: Record;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/timing/power-colors', {
+      event_list_name: params.event_list_name,
+      dt: params.dt,
+      segment_size: params.segment_size,
+      freq_ranges: params.freq_ranges,
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Calculate time lags between two event lists
+   */
+  async calculateTimeLags(params: {
+    event_list_1_name: string;
+    event_list_2_name: string;
+    dt: number;
+    segment_size: number;
+    freq_range?: [number, number];
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/timing/time-lags', {
+      event_list_1_name: params.event_list_1_name,
+      event_list_2_name: params.event_list_2_name,
+      dt: params.dt,
+      segment_size: params.segment_size,
+      freq_range: params.freq_range,
+      output_name: params.output_name,
+    });
+  },
+
+  /**
+   * Calculate coherence between two event lists
+   */
+  async calculateCoherence(params: {
+    event_list_1_name: string;
+    event_list_2_name: string;
+    dt: number;
+    segment_size: number;
+    output_name?: string;
+  }): Promise> {
+    return apiClient.post('/api/timing/coherence', {
+      event_list_1_name: params.event_list_1_name,
+      event_list_2_name: params.event_list_2_name,
+      dt: params.dt,
+      segment_size: params.segment_size,
+      output_name: params.output_name,
+    });
+  },
+};
+
+export default timingApi;
diff --git a/src/components/analysis/EventListSelector.test.tsx b/src/components/analysis/EventListSelector.test.tsx
new file mode 100644
index 0000000..b640baf
--- /dev/null
+++ b/src/components/analysis/EventListSelector.test.tsx
@@ -0,0 +1,103 @@
+import React, { useState } from 'react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { renderWithProviders } from '@/test/testUtils';
+
+const listEventLists = vi.fn();
+vi.mock('@/api/dataApi', () => ({
+  dataApi: { listEventLists: (...args: unknown[]) => listEventLists(...args) },
+}));
+
+import EventListSelector from './EventListSelector';
+
+const Harness: React.FC = () => {
+  const [value, setValue] = useState('');
+  return ;
+};
+
+const DualHarness: React.FC = () => {
+  const [value1, setValue1] = useState('');
+  const [value2, setValue2] = useState('');
+  return (
+    <>
+      
+      
+    
+  );
+};
+
+describe('EventListSelector', () => {
+  beforeEach(() => listEventLists.mockReset());
+
+  it('lists loaded event lists and selects one', async () => {
+    listEventLists.mockResolvedValue({
+      success: true,
+      data: [
+        { name: 'obs1', n_events: 1000, time_range: [0, 10] },
+        { name: 'obs2', n_events: 2000, time_range: [0, 20] },
+      ],
+      message: '',
+      error: null,
+    });
+    renderWithProviders();
+    const select = await screen.findByLabelText('Event list');
+    await userEvent.click(select);
+    await userEvent.click(await screen.findByText(/obs2/));
+    await waitFor(() => expect(screen.getByLabelText('Event list')).toHaveTextContent('obs2'));
+  });
+
+  it('shows an empty-state prompt linking to data ingestion', async () => {
+    listEventLists.mockResolvedValue({ success: true, data: [], message: '', error: null });
+    renderWithProviders();
+    expect(await screen.findByText(/No event lists loaded/)).toBeInTheDocument();
+    expect(screen.getByRole('link', { name: /Load data/ })).toHaveAttribute(
+      'href',
+      '/data-ingestion'
+    );
+  });
+
+  it('shares one query across two instances and selections stay independent', async () => {
+    listEventLists.mockResolvedValue({
+      success: true,
+      data: [
+        { name: 'obs1', n_events: 1000, time_range: [0, 10] },
+        { name: 'obs2', n_events: 2000, time_range: [0, 20] },
+      ],
+      message: '',
+      error: null,
+    });
+    renderWithProviders();
+    await screen.findByLabelText('Event list 1');
+    const select2 = await screen.findByLabelText('Event list 2');
+    expect(listEventLists).toHaveBeenCalledTimes(1);
+    await userEvent.click(select2);
+    await userEvent.click(await screen.findByText(/obs1/));
+    await waitFor(() => expect(screen.getByLabelText('Event list 2')).toHaveTextContent('obs1'));
+    expect(screen.getByLabelText('Event list 1')).not.toHaveTextContent('obs1');
+  });
+
+  it('refreshes from empty to populated via the refresh button', async () => {
+    listEventLists.mockResolvedValueOnce({ success: true, data: [], message: '', error: null });
+    renderWithProviders();
+    expect(await screen.findByText(/No event lists loaded/)).toBeInTheDocument();
+    listEventLists.mockResolvedValue({
+      success: true,
+      data: [{ name: 'obs1', n_events: 1000, time_range: [0, 10] }],
+      message: '',
+      error: null,
+    });
+    await userEvent.click(screen.getByRole('button', { name: 'Refresh event lists' }));
+    const select = await screen.findByLabelText('Event list');
+    await userEvent.click(select);
+    expect(await screen.findByText(/obs1/)).toBeInTheDocument();
+  });
+
+  it('shows an error alert with a refresh affordance when listing fails', async () => {
+    listEventLists.mockResolvedValue({ success: false, data: null, message: 'boom', error: 'boom' });
+    renderWithProviders();
+    expect(await screen.findByText(/Failed to load event lists/)).toBeInTheDocument();
+    expect(screen.getByText(/boom/)).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: 'Refresh event lists' })).toBeInTheDocument();
+  });
+});
diff --git a/src/components/analysis/EventListSelector.tsx b/src/components/analysis/EventListSelector.tsx
new file mode 100644
index 0000000..21eabd8
--- /dev/null
+++ b/src/components/analysis/EventListSelector.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import {
+  Alert,
+  Box,
+  CircularProgress,
+  FormControl,
+  IconButton,
+  InputLabel,
+  Link,
+  MenuItem,
+  Select,
+  Tooltip,
+} from '@mui/material';
+import RefreshIcon from '@mui/icons-material/Refresh';
+import { Link as RouterLink } from 'react-router-dom';
+import { useEventLists } from '@/hooks/useEventLists';
+
+interface EventListSelectorProps {
+  label: string;
+  value: string;
+  onChange: (name: string) => void;
+}
+
+/** Dropdown of event lists currently loaded in the backend. */
+const EventListSelector: React.FC = ({ label, value, onChange }) => {
+  const { data, isLoading, isError, error, refetch, isFetching } = useEventLists();
+  const labelId = `event-list-selector-${React.useId()}`;
+
+  // Auto-clear a selection that no longer exists in the list (deleted
+  // elsewhere or backend restart) so consumers never submit stale names.
+  React.useEffect(() => {
+    if (
+      !isLoading &&
+      !isFetching &&
+      value !== '' &&
+      data !== undefined &&
+      !data.some((ev) => ev.name === value)
+    ) {
+      onChange('');
+    }
+  }, [data, isLoading, isFetching, value, onChange]);
+
+  const refreshButton = (
+    
+      
+         refetch()}
+          disabled={isFetching}
+        >
+          {isFetching ?  : }
+        
+      
+    
+  );
+
+  // Only take over the UI with an error when there is no usable (stale) data;
+  // a failed background refetch keeps the populated dropdown rendered.
+  if (isError && data === undefined) {
+    return (
+      
+        Failed to load event lists: {error instanceof Error ? error.message : 'unknown error'}
+      
+    );
+  }
+
+  if (!isLoading && (data?.length ?? 0) === 0) {
+    return (
+      
+        No event lists loaded.{' '}
+        
+          Load data
+        {' '}
+        first.
+      
+    );
+  }
+
+  return (
+    
+      
+        {label}
+        
+      
+      {refreshButton}
+    
+  );
+};
+
+export default EventListSelector;
diff --git a/src/components/common/LogPanel.tsx b/src/components/common/LogPanel.tsx
new file mode 100644
index 0000000..2015d52
--- /dev/null
+++ b/src/components/common/LogPanel.tsx
@@ -0,0 +1,491 @@
+import React, { useEffect, useRef, useState } from 'react';
+import {
+  Box,
+  Collapse,
+  IconButton,
+  Typography,
+  TextField,
+  Chip,
+  Tooltip,
+  Paper,
+  Stack,
+  Divider,
+  InputAdornment,
+} from '@mui/material';
+import {
+  ExpandLess,
+  ExpandMore,
+  Delete,
+  ContentCopy,
+  Search,
+  Terminal,
+  Error as ErrorIcon,
+  Warning as WarningIcon,
+  Info as InfoIcon,
+  BugReport as DebugIcon,
+  Code as RawIcon,
+  FormatListBulleted as FormattedIcon,
+} from '@mui/icons-material';
+import { useLogStore, selectFilteredLogs, LogEntry } from '@/store/logStore';
+import { useBackendContext } from '@/App';
+import { logStreamClient } from '@/api/logApi';
+
+interface LogPanelProps {
+  /** Left offset to avoid overlapping left sidebar */
+  leftOffset?: number;
+  /** Right offset to avoid overlapping right toolbar */
+  rightOffset?: number;
+}
+
+/**
+ * Format timestamp for display
+ */
+const formatTimestamp = (date: Date): string => {
+  return date.toLocaleTimeString('en-US', {
+    hour12: false,
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit',
+  });
+};
+
+/**
+ * Get icon for log level
+ */
+const getLevelIcon = (level: LogEntry['level']): React.ReactNode => {
+  switch (level) {
+    case 'error':
+      return ;
+    case 'warn':
+      return ;
+    case 'info':
+      return ;
+    case 'debug':
+      return ;
+    default:
+      return null;
+  }
+};
+
+/**
+ * Get color for log level
+ */
+const getLevelColor = (level: LogEntry['level']): string => {
+  switch (level) {
+    case 'error':
+      return 'error.main';
+    case 'warn':
+      return 'warning.main';
+    case 'info':
+      return 'info.main';
+    case 'debug':
+      return 'text.secondary';
+    default:
+      return 'text.primary';
+  }
+};
+
+/**
+ * Get color for source
+ */
+const getSourceColor = (source: LogEntry['source']): 'primary' | 'secondary' | 'default' => {
+  switch (source) {
+    case 'python':
+      return 'primary';
+    case 'electron':
+      return 'secondary';
+    case 'frontend':
+      return 'default';
+    default:
+      return 'default';
+  }
+};
+
+/**
+ * Log entry row component (formatted view)
+ */
+const LogRow: React.FC<{ log: LogEntry }> = ({ log }) => {
+  return (
+    
+      
+        {formatTimestamp(log.timestamp)}
+      
+      {getLevelIcon(log.level)}
+      
+      
+        {log.message}
+      
+    
+  );
+};
+
+/**
+ * Format log entry as raw terminal output
+ */
+const formatRawLog = (log: LogEntry): string => {
+  const timestamp = formatTimestamp(log.timestamp);
+  const level = log.level.toUpperCase().padEnd(5);
+  const source = `[${log.source}]`.padEnd(10);
+  return `${timestamp} ${level} ${source} ${log.message}`;
+};
+
+/**
+ * Raw log view component - shows logs exactly like terminal output
+ */
+const RawLogView: React.FC<{ logs: LogEntry[] }> = ({ logs }) => {
+  return (
+    
+      {logs.map((log) => (
+        
+          {formatRawLog(log)}
+        
+      ))}
+    
+  );
+};
+
+/**
+ * LogPanel component - displays logs in a collapsible panel
+ */
+const LogPanel: React.FC = ({ leftOffset = 0, rightOffset = 0 }) => {
+  const {
+    isOpen,
+    togglePanel,
+    clearLogs,
+    filter,
+    toggleLevelFilter,
+    toggleSourceFilter,
+    setSearchFilter,
+  } = useLogStore();
+
+  const logs = useLogStore(selectFilteredLogs);
+  const logsEndRef = useRef(null);
+  const [autoScroll, setAutoScroll] = useState(true);
+  const [rawView, setRawView] = useState(false);
+  const { isReady: backendReady } = useBackendContext();
+
+  // Listen for logs from Electron main process
+  useEffect(() => {
+    if (typeof window !== 'undefined' && window.electronAPI) {
+      // Set up the log listener first
+      const unsubscribe = window.electronAPI.onLog((log) => {
+        useLogStore.getState().addLog(log);
+      });
+
+      // Signal that we're ready to receive logs (triggers flush of buffered logs)
+      window.electronAPI.signalLogReady();
+
+      return unsubscribe;
+    }
+  }, []);
+
+  // Connect to Python backend log stream via SSE when backend is ready
+  useEffect(() => {
+    if (backendReady) {
+      // Connect immediately - history replay ensures we don't miss startup logs
+      logStreamClient.connect();
+
+      return () => {
+        logStreamClient.disconnect();
+      };
+    }
+  }, [backendReady]);
+
+  // Auto-scroll to bottom when new logs arrive
+  useEffect(() => {
+    if (autoScroll && logsEndRef.current) {
+      logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
+    }
+  }, [logs, autoScroll]);
+
+  // Handle scroll to detect if user scrolled up
+  const handleScroll = (e: React.UIEvent): void => {
+    const element = e.currentTarget;
+    const isAtBottom = element.scrollHeight - element.scrollTop - element.clientHeight < 50;
+    setAutoScroll(isAtBottom);
+  };
+
+  // Copy all visible logs to clipboard
+  const handleCopyLogs = (): void => {
+    const logText = logs
+      .map((log) => `[${formatTimestamp(log.timestamp)}] [${log.level.toUpperCase()}] [${log.source}] ${log.message}`)
+      .join('\n');
+    navigator.clipboard.writeText(logText);
+  };
+
+  const levelFilters: LogEntry['level'][] = ['info', 'warn', 'error', 'debug'];
+  const sourceFilters: LogEntry['source'][] = ['python', 'electron', 'frontend'];
+
+  return (
+    
+            theme.palette.mode === 'dark'
+              ? 'linear-gradient(to right, #00d4aa, #3b82f6, transparent)'
+              : 'linear-gradient(to right, #0d9b7a, #2563eb, transparent)',
+          opacity: 0.4,
+          zIndex: 1,
+        },
+      }}
+    >
+      {/* Header */}
+      
+        
+          
+          Console
+          
+        
+         { e.stopPropagation(); togglePanel(); }}>
+          {isOpen ?  : }
+        
+      
+
+      {/* Content */}
+      
+        
+          {/* Toolbar */}
+          
+            {/* Search */}
+             setSearchFilter(e.target.value)}
+              InputProps={{
+                startAdornment: (
+                  
+                    
+                  
+                ),
+                sx: { fontSize: '0.8rem', height: 32 },
+              }}
+              sx={{ width: 200 }}
+            />
+
+            
+
+            {/* Level filters */}
+            
+              {levelFilters.map((level) => (
+                 toggleLevelFilter(level)}
+                  sx={{
+                    height: 24,
+                    fontSize: '0.7rem',
+                    textTransform: 'capitalize',
+                  }}
+                />
+              ))}
+            
+
+            
+
+            {/* Source filters */}
+            
+              {sourceFilters.map((source) => (
+                 toggleSourceFilter(source)}
+                  sx={{
+                    height: 24,
+                    fontSize: '0.7rem',
+                    textTransform: 'capitalize',
+                  }}
+                />
+              ))}
+            
+
+            
+
+            {/* Actions */}
+            
+               setRawView(!rawView)}>
+                {rawView ?  : }
+              
+            
+            
+              
+                
+              
+            
+            
+              
+                
+              
+            
+          
+
+          {/* Log entries */}
+          
+                theme.palette.mode === 'dark' ? '#080c16' : '#fafbfc',
+              '&::-webkit-scrollbar': {
+                width: 8,
+              },
+              '&::-webkit-scrollbar-track': {
+                backgroundColor: 'background.paper',
+              },
+              '&::-webkit-scrollbar-thumb': {
+                backgroundColor: 'action.disabled',
+                borderRadius: 4,
+              },
+            }}
+          >
+            {logs.length === 0 ? (
+              
+                No logs to display
+              
+            ) : rawView ? (
+              <>
+                
+                
+ + ) : ( + <> + {logs.map((log) => ( + + ))} +
+ + )} + + + + + ); +}; + +export default LogPanel; diff --git a/src/components/common/NotificationToast.tsx b/src/components/common/NotificationToast.tsx new file mode 100644 index 0000000..70e78c6 --- /dev/null +++ b/src/components/common/NotificationToast.tsx @@ -0,0 +1,153 @@ +/** + * NotificationToast - Auto-popup toast notifications + * + * Displays a Snackbar toast whenever a new notification is added to the store. + * Notifications slide in from the right side and auto-dismiss after a few seconds. + */ + +import React, { useEffect, useState } from 'react'; +import { + Snackbar, + Alert, + AlertTitle, + IconButton, + Typography, + Slide, + SlideProps, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { useUIStore, Notification } from '@/store/uiStore'; + +// Slide transition from right +function SlideTransition(props: SlideProps) { + return ; +} + +// Auto-hide duration based on notification type (ms) +const AUTO_HIDE_DURATION: Record = { + info: 4000, + success: 3000, + warning: 5000, + error: 6000, +}; + +const NotificationToast: React.FC = () => { + const { notifications, markNotificationRead } = useUIStore(); + const [open, setOpen] = useState(false); + const [currentNotification, setCurrentNotification] = useState(null); + const [lastNotificationId, setLastNotificationId] = useState(null); + + // Watch for new notifications + useEffect(() => { + if (notifications.length > 0) { + const latestNotification = notifications[0]; + + // Only show toast for new, unread notifications + if (latestNotification.id !== lastNotificationId && !latestNotification.read) { + setCurrentNotification(latestNotification); + setLastNotificationId(latestNotification.id); + setOpen(true); + } + } + }, [notifications, lastNotificationId]); + + const handleClose = (_event?: React.SyntheticEvent | Event, reason?: string): void => { + // Don't close on clickaway - only on explicit close or timeout + if (reason === 'clickaway') { + return; + } + setOpen(false); + }; + + const handleExited = (): void => { + // Mark as read when the toast closes + if (currentNotification) { + markNotificationRead(currentNotification.id); + } + }; + + if (!currentNotification) { + return null; + } + + return ( + + { + const colors: Record = { + success: 'rgba(34, 197, 94, 0.3)', + error: 'rgba(239, 68, 68, 0.3)', + warning: 'rgba(245, 158, 11, 0.3)', + info: 'rgba(59, 130, 246, 0.3)', + }; + return colors[currentNotification.type] || 'rgba(148, 163, 184, 0.12)'; + }, + boxShadow: () => { + const glows: Record = { + success: '0 4px 24px rgba(34, 197, 94, 0.15)', + error: '0 4px 24px rgba(239, 68, 68, 0.15)', + warning: '0 4px 24px rgba(245, 158, 11, 0.15)', + info: '0 4px 24px rgba(59, 130, 246, 0.15)', + }; + return glows[currentNotification.type] || '0 4px 24px rgba(0,0,0,0.1)'; + }, + '& .MuiAlert-message': { + width: '100%', + }, + }} + action={ + + + + } + > + + {currentNotification.title} + + + {currentNotification.message} + + + + ); +}; + +export default NotificationToast; diff --git a/src/components/common/PageTemplate.tsx b/src/components/common/PageTemplate.tsx new file mode 100644 index 0000000..a7eacb1 --- /dev/null +++ b/src/components/common/PageTemplate.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { Box, Typography, Paper, Chip, Alert } from '@mui/material'; +import ConstructionIcon from '@mui/icons-material/Construction'; + +interface PageTemplateProps { + title: string; + description?: string; + category?: string; + status?: 'ready' | 'coming-soon' | 'in-development'; + children?: React.ReactNode; +} + +/** + * Reusable page template component + */ +const PageTemplate: React.FC = ({ + title, + description, + category, + status = 'coming-soon', + children, +}) => { + return ( + + {/* Header */} + + {category && ( + + )} + + theme.palette.mode === 'dark' + ? '0 0 30px rgba(0, 212, 170, 0.08)' + : 'none', + }} + > + {title} + + {description && ( + + {description} + + )} + + + {/* Status indicator for pages under development */} + {status !== 'ready' && ( + } + sx={{ mb: 3 }} + > + {status === 'in-development' + ? 'This feature is currently under development.' + : 'This feature is coming soon in a future release.'} + + )} + + {/* Page content */} + {children || ( + + theme.palette.mode === 'dark' + ? 'rgba(18, 24, 41, 0.4)' + : 'rgba(255, 255, 255, 0.6)', + backdropFilter: 'blur(8px)', + border: '1px solid', + borderColor: 'divider', + }} + > + + + {title} + + + This analysis module will be implemented soon. + + + )} + + ); +}; + +export default PageTemplate; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..2e86fca --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,327 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { + AppBar, + Toolbar, + Typography, + IconButton, + Box, + Tooltip, + Chip, + InputBase, + Paper, + Fade, + ClickAwayListener, +} from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; +import MenuOpenIcon from '@mui/icons-material/MenuOpen'; +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import SettingsIcon from '@mui/icons-material/Settings'; +import SearchIcon from '@mui/icons-material/Search'; +import CloseIcon from '@mui/icons-material/Close'; +import CircleIcon from '@mui/icons-material/Circle'; +import ViewSidebarIcon from '@mui/icons-material/ViewSidebar'; +import { ThemeContext, useBackendContext } from '../../App'; +import { useUIStore } from '@/store/uiStore'; + +interface HeaderProps { + onToggleSidebar: () => void; + sidebarOpen: boolean; + onToggleRightToolbar: () => void; + rightToolbarOpen: boolean; +} + +/** + * Application header with navigation controls and status indicators + */ +const Header: React.FC = ({ + onToggleSidebar, + sidebarOpen, + onToggleRightToolbar, + rightToolbarOpen, +}) => { + const { darkMode, toggleDarkMode } = useContext(ThemeContext); + const { isReady, error } = useBackendContext(); + const { searchOpen, searchQuery, setSearchOpen, setSearchQuery } = useUIStore(); + + const [localSearchQuery, setLocalSearchQuery] = useState(''); + + // With titleBarStyle: 'hiddenInset' macOS draws the traffic lights over the + // web content, so the toolbar must reserve space for them on darwin only. + const [isMac, setIsMac] = useState(false); + useEffect(() => { + let mounted = true; + void window.electronAPI?.getPlatform().then((platform) => { + if (mounted) setIsMac(platform === 'darwin'); + }); + return () => { + mounted = false; + }; + }, []); + + const handleRestartBackend = async (): Promise => { + if (window.electronAPI) { + await window.electronAPI.restartPython(); + } + }; + + const handleSearchOpen = (): void => { + setSearchOpen(true); + setLocalSearchQuery(searchQuery); + }; + + const handleSearchClose = (): void => { + setSearchOpen(false); + setLocalSearchQuery(''); + }; + + const handleSearchSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + setSearchQuery(localSearchQuery); + // TODO: Implement actual search functionality + console.log('Search query:', localSearchQuery); + }; + + return ( + theme.zIndex.drawer + 1, + backgroundColor: (theme) => + theme.palette.mode === 'dark' + ? 'rgba(10, 14, 26, 0.8)' + : 'rgba(255, 255, 255, 0.8)', + backdropFilter: 'blur(12px) saturate(150%)', + WebkitBackdropFilter: 'blur(12px) saturate(150%)', + borderBottom: '1px solid', + borderColor: 'divider', + '&::after': { + content: '""', + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: '1px', + background: (theme) => + theme.palette.mode === 'dark' + ? 'linear-gradient(to right, #00d4aa, #3b82f6, transparent)' + : 'linear-gradient(to right, #0d9b7a, #2563eb, transparent)', + opacity: 0.4, + }, + }} + > + {/* The whole bar is a window drag region; interactive children opt out. */} + + {/* Spacer clearing the macOS traffic lights. Sized from the Window + Controls Overlay env var (enabled via titleBarOverlay in main.ts), + which the compositor recomputes on page zoom and fullscreen — + fixed pixel offsets drift against the OS-drawn buttons when the + user zooms. 16px breathing room after the safe-area edge; 80px + fallback if the env var is ever unavailable. A real element, not + Toolbar padding: sx padding loses the cascade against + MuiToolbar-gutters' media rule. */} + {isMac && ( + + )} + {/* Left sidebar toggle */} + + {sidebarOpen ? : } + + + {/* Logo and title */} + + Stingray Explorer + + theme.palette.mode === 'dark' + ? '0 0 20px rgba(0, 212, 170, 0.15)' + : 'none', + }} + > + Stingray Explorer + + + + {/* Subtitle */} + + Next-Generation Spectral Timing Made Easy + + + {/* Spacer */} + + + {/* Search bar */} + {searchOpen ? ( + + + + theme.palette.mode === 'dark' + ? 'rgba(18, 24, 41, 0.6)' + : 'rgba(240, 242, 245, 0.8)', + backdropFilter: 'blur(8px)', + border: '1px solid', + borderColor: 'divider', + transition: 'border-color 0.2s ease, box-shadow 0.2s ease', + '&:focus-within': { + borderColor: 'primary.main', + boxShadow: (theme) => + `0 0 0 3px ${theme.palette.mode === 'dark' ? 'rgba(0, 212, 170, 0.12)' : 'rgba(13, 155, 122, 0.1)'}`, + }, + }} + elevation={0} + > + + setLocalSearchQuery(e.target.value)} + autoFocus + sx={{ flex: 1, fontSize: '0.8rem', fontFamily: '"IBM Plex Sans", sans-serif' }} + /> + + + + + + + ) : ( + + + + + + )} + + {/* Backend status indicator */} + + + } + label={isReady ? 'Backend Ready' : error ? 'Error' : 'Starting...'} + size="small" + variant="outlined" + color={isReady ? 'success' : error ? 'error' : 'warning'} + sx={{ + mr: 2, + fontFamily: '"IBM Plex Sans", sans-serif', + fontSize: '0.7rem', + fontWeight: 500, + backgroundColor: (theme) => + theme.palette.mode === 'dark' + ? 'rgba(18, 24, 41, 0.5)' + : 'rgba(240, 242, 245, 0.5)', + backdropFilter: 'blur(4px)', + }} + /> + + + {/* Action buttons */} + + {/* Restart backend */} + + + + + + + {/* Dark mode toggle */} + + + {darkMode ? : } + + + + {/* Settings */} + + + + + + + + {/* Right toolbar toggle - rightmost */} + + + + + + + + ); +}; + +export default Header; diff --git a/src/components/layout/JobStatusPanel.tsx b/src/components/layout/JobStatusPanel.tsx new file mode 100644 index 0000000..5cddbe0 --- /dev/null +++ b/src/components/layout/JobStatusPanel.tsx @@ -0,0 +1,316 @@ +/** + * Job Status Panel for the sidebar. + * + * Displays active and recently completed background jobs with + * real-time progress updates. Persists across page navigation. + */ + +import React, { useState, useContext } from 'react'; +import { + Box, + Typography, + IconButton, + Collapse, + List, + ListItem, + ListItemText, + ListItemSecondaryAction, + Badge, + Tooltip, + Divider, + Button, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import CancelIcon from '@mui/icons-material/Cancel'; +import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; +import SyncIcon from '@mui/icons-material/Sync'; +import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; +import CloseIcon from '@mui/icons-material/Close'; +import WifiIcon from '@mui/icons-material/Wifi'; +import WifiOffIcon from '@mui/icons-material/WifiOff'; +import { useJobStore } from '@/store/jobStore'; +import { jobApi } from '@/api/jobApi'; +import { ThemeContext } from '@/App'; +import type { Job, JobStatus } from '@/types/job'; + +interface JobStatusPanelProps { + /** Whether the sidebar is expanded */ + sidebarOpen: boolean; +} + +/** + * Get icon for job status. + */ +const getStatusIcon = (status: JobStatus): React.ReactNode => { + switch (status) { + case 'pending': + return ; + case 'running': + return ; + case 'completed': + return ; + case 'failed': + return ; + case 'cancelled': + return ; + default: + return null; + } +}; + +/** + * Format relative time (e.g., "2m ago"). + */ +const formatRelativeTime = (isoString: string): string => { + const date = new Date(isoString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSec = Math.floor(diffMs / 1000); + + if (diffSec < 60) return 'just now'; + if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; + if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`; + return `${Math.floor(diffSec / 86400)}d ago`; +}; + +/** + * Single job item component. + */ +const JobItem: React.FC<{ job: Job; onCancel: (id: string) => void }> = ({ job, onCancel }) => { + const isActive = job.status === 'pending' || job.status === 'running'; + + return ( + + + {getStatusIcon(job.status)} + + {isActive && job.status === 'pending' && ( + + + onCancel(job.id)}> + + + + + )} + + + {job.status === 'running' && job.total_items > 1 && ( + + {job.completed_items}/{job.total_items} files + + )} + + {!isActive && ( + + {formatRelativeTime(job.completed_at || job.created_at)} + + )} + + ); +}; + +/** + * Job Status Panel component. + */ +const JobStatusPanel: React.FC = ({ sidebarOpen }) => { + const { darkMode } = useContext(ThemeContext); + const [expanded, setExpanded] = useState(true); + const [showCompleted, setShowCompleted] = useState(false); + + const { + isConnected, + getActiveJobs, + getCompletedJobs, + getActiveJobCount, + clearCompletedJobs, + } = useJobStore(); + + const activeJobs = getActiveJobs(); + const completedJobs = getCompletedJobs(); + const activeCount = getActiveJobCount(); + + const handleCancel = async (jobId: string): Promise => { + try { + await jobApi.cancelJob(jobId); + } catch (error) { + console.error('Failed to cancel job:', error); + } + }; + + const handleClearCompleted = (): void => { + clearCompletedJobs(); + // Also clear on backend + jobApi.clearCompletedJobs().catch(console.error); + }; + + // Don't render if sidebar is collapsed + if (!sidebarOpen) { + return null; + } + + const hasJobs = activeJobs.length > 0 || completedJobs.length > 0; + + return ( + + {/* Header */} + setExpanded(!expanded)} + > + + + + Jobs + + + + {isConnected ? ( + + ) : ( + + )} + + + + {expanded ? : } + + + + {/* Content */} + + + {/* Active Jobs */} + {activeJobs.length > 0 && ( + + {activeJobs.map((job) => ( + + ))} + + )} + + {/* Completed Jobs Toggle */} + {completedJobs.length > 0 && ( + <> + + + + {showCompleted && ( + + + + + + )} + + + + + {completedJobs.slice(0, 10).map((job) => ( + + ))} + {completedJobs.length > 10 && ( + + +{completedJobs.length - 10} more + + )} + + + + )} + + {/* Empty state - no jobs at all */} + {!hasJobs && ( + + No jobs running or completed + + )} + + {/* Empty state for active (when there are completed jobs) */} + {activeJobs.length === 0 && completedJobs.length > 0 && !showCompleted && ( + + No active jobs + + )} + + + + + ); +}; + +export default JobStatusPanel; diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx new file mode 100644 index 0000000..f099db9 --- /dev/null +++ b/src/components/layout/MainLayout.tsx @@ -0,0 +1,120 @@ +import React, { useState, useCallback } from 'react'; +import { Outlet } from 'react-router-dom'; +import { Box, Toolbar } from '@mui/material'; +import Sidebar from './Sidebar'; +import Header from './Header'; +import RightToolbar, { TOOLBAR_WIDTH } from './RightToolbar'; +import LogPanel from '@/components/common/LogPanel'; +import NotificationToast from '@/components/common/NotificationToast'; +import { useUIStore } from '@/store/uiStore'; + +// Sidebar widths +const MAIN_DRAWER_WIDTH = 240; +const SUB_DRAWER_WIDTH = 240; + +// LogPanel collapsed header height +const LOG_PANEL_HEADER_HEIGHT = 40; + +/** + * Main layout component that wraps all pages + * Includes sidebar, header, right toolbar, and main content area + * (Footer functionality has been consolidated into RightToolbar) + */ +const MainLayout: React.FC = () => { + const [sidebarOpen, setSidebarOpen] = useState(true); + const [submenuOpen, setSubmenuOpen] = useState(false); + const { rightToolbarCollapsed, toggleRightToolbar } = useUIStore(); + + const handleToggleSidebar = useCallback((): void => { + setSidebarOpen((prev) => !prev); + }, []); + + const handleSubmenuStateChange = useCallback((isOpen: boolean): void => { + setSubmenuOpen(isOpen); + }, []); + + // Calculate main content offset based on sidebar state + const leftOffset = sidebarOpen + ? submenuOpen + ? MAIN_DRAWER_WIDTH + SUB_DRAWER_WIDTH + : MAIN_DRAWER_WIDTH + : 0; + + // Calculate right offset based on right toolbar state + const rightOffset = rightToolbarCollapsed ? 0 : TOOLBAR_WIDTH; + + return ( + + {/* Header */} +
+ + {/* Toolbar spacer to account for fixed header */} + + + {/* Main content area with sidebars */} + + {/* Left Sidebar */} + + + {/* Main content */} + + theme.transitions.create(['margin-left', 'margin-right'], { + easing: theme.transitions.easing.easeInOut, + duration: theme.transitions.duration.standard, + }), + marginLeft: `${leftOffset}px`, + marginRight: `${rightOffset}px`, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + backgroundColor: 'background.default', + // Subtle inset shadow where content meets sidebars + boxShadow: (theme) => + theme.palette.mode === 'dark' + ? 'inset 4px 0 12px -4px rgba(0,0,0,0.3), inset -4px 0 12px -4px rgba(0,0,0,0.3)' + : 'inset 4px 0 8px -4px rgba(0,0,0,0.04), inset -4px 0 8px -4px rgba(0,0,0,0.04)', + }} + > + {/* Page content */} + + + + + + {/* Right Toolbar */} + + + + {/* Log Panel - at bottom of main content area (avoids sidebars) */} + + + {/* Toast notifications - auto-popup for new notifications */} + + + ); +}; + +export default MainLayout; diff --git a/src/components/layout/RightToolbar.tsx b/src/components/layout/RightToolbar.tsx new file mode 100644 index 0000000..c2ddf4f --- /dev/null +++ b/src/components/layout/RightToolbar.tsx @@ -0,0 +1,809 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + IconButton, + Tooltip, + Divider, + Badge, + CircularProgress, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Typography, + List, + ListItem, + ListItemButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Popover, + Chip, + Link, +} from '@mui/material'; +import TerminalIcon from '@mui/icons-material/Terminal'; +import BugReportIcon from '@mui/icons-material/BugReport'; +import NotificationsIcon from '@mui/icons-material/Notifications'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import LogoutIcon from '@mui/icons-material/Logout'; +import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; +import DeveloperModeIcon from '@mui/icons-material/DeveloperMode'; +import MemoryIcon from '@mui/icons-material/Memory'; +import StorageIcon from '@mui/icons-material/Storage'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import MenuBookIcon from '@mui/icons-material/MenuBook'; +import ForumIcon from '@mui/icons-material/Forum'; +import { useUIStore, Notification, AppResources } from '@/store/uiStore'; +import { useLogStore } from '@/store/logStore'; +import { useBackendContext } from '@/App'; + +const TOOLBAR_WIDTH = 52; +const HEADER_HEIGHT = 64; + +interface RightToolbarProps { + visible?: boolean; +} + +/** + * Right toolbar with quick actions, status indicators, and system info + * (Consolidated from previous Footer + RightToolbar) + */ +const RightToolbar: React.FC = ({ visible = true }) => { + const { + isProcessing, + processingMessage, + processingProgress, + notifications, + unreadNotificationCount, + markNotificationRead, + clearNotifications, + addNotification, + appResources, + setAppResources, + } = useUIStore(); + + const { togglePanel: toggleLogPanel, isOpen: logPanelOpen } = useLogStore(); + const { isReady: backendReady, port: backendPort } = useBackendContext(); + + // App version + const [version, setVersion] = useState(''); + + // Menu/popover states + const [notificationsAnchor, setNotificationsAnchor] = useState(null); + const [debugAnchor, setDebugAnchor] = useState(null); + const [helpAnchor, setHelpAnchor] = useState(null); + const [resourcesAnchor, setResourcesAnchor] = useState(null); + + // Dialog states + const [selectedNotification, setSelectedNotification] = useState(null); + const [aboutOpen, setAboutOpen] = useState(false); + + // Resource monitoring state + const [monitoringActive, setMonitoringActive] = useState(false); + + // Get app version on mount + useEffect(() => { + const getVersion = async (): Promise => { + if (window.electronAPI) { + const appVersion = await window.electronAPI.getAppVersion(); + setVersion(appVersion); + } + }; + getVersion(); + }, []); + + // Fetch app-specific resources from backend AND Electron + const fetchResources = async (): Promise => { + try { + // Fetch backend resources + let backendRes = null; + let systemMemoryTotalMb = 16 * 1024; // Default 16GB + let systemMemoryAvailableMb = 8 * 1024; // Default 8GB + let systemCpuCount = 1; // Default to 1 core + + if (backendReady && backendPort) { + try { + const response = await fetch(`http://127.0.0.1:${backendPort}/api/status`); + if (response.ok) { + const data = await response.json(); + if (data.backend_resources) { + backendRes = { + memoryMb: data.backend_resources.memory_mb || 0, + cpuPercent: data.backend_resources.cpu_percent || 0, + }; + systemMemoryTotalMb = data.backend_resources.system_memory_total_mb || systemMemoryTotalMb; + systemMemoryAvailableMb = data.backend_resources.system_memory_available_mb || systemMemoryAvailableMb; + systemCpuCount = data.backend_resources.system_cpu_count || systemCpuCount; + } + } + } catch { + // Backend might not be ready + } + } + + // Fetch Electron resources + let electronMainRes = null; + let electronRendererRes = null; + + if (window.electronAPI) { + try { + const electronRes = await window.electronAPI.getElectronResources(); + if (electronRes.main) { + electronMainRes = { + memoryMb: electronRes.main.memory_mb || 0, + cpuPercent: electronRes.main.cpu_percent || 0, + }; + } + if (electronRes.renderer) { + electronRendererRes = { + memoryMb: electronRes.renderer.memory_mb || 0, + cpuPercent: electronRes.renderer.cpu_percent || 0, + }; + } + } catch { + // Electron API might not be available + } + } + + // Calculate totals + const totalMemoryMb = + (backendRes?.memoryMb || 0) + + (electronMainRes?.memoryMb || 0) + + (electronRendererRes?.memoryMb || 0); + + // Raw CPU sum (can exceed 100% on multi-core systems) + const totalCpuPercent = + (backendRes?.cpuPercent || 0) + + (electronMainRes?.cpuPercent || 0) + + (electronRendererRes?.cpuPercent || 0); + + // Calculate app percentage of system memory + const appMemoryPercent = systemMemoryTotalMb > 0 + ? (totalMemoryMb / systemMemoryTotalMb) * 100 + : 0; + + // Normalize CPU to total system capacity (0-100%) + // e.g., 200% on 8 cores = 25% of total system CPU + const appCpuPercent = systemCpuCount > 0 + ? totalCpuPercent / systemCpuCount + : 0; + + const combined: AppResources = { + backend: backendRes, + electronMain: electronMainRes, + electronRenderer: electronRendererRes, + totalMemoryMb, + totalCpuPercent, + systemMemoryTotalMb, + systemMemoryAvailableMb, + systemCpuCount, + appMemoryPercent, + appCpuPercent, + }; + + setAppResources(combined); + } catch { + // Error fetching resources + } + }; + + // Poll resources when monitoring is active + useEffect(() => { + if (!monitoringActive) return; + + fetchResources(); + const interval = setInterval(fetchResources, 2000); // Poll every 2 seconds + return () => clearInterval(interval); + }, [monitoringActive, backendReady, backendPort]); + + // Toggle resource monitoring + const handleToggleMonitoring = (): void => { + if (!monitoringActive) { + fetchResources(); + } + setMonitoringActive((prev) => !prev); + }; + + const handleOpenExternal = async (url: string): Promise => { + if (window.electronAPI) { + await window.electronAPI.openExternal(url); + } + }; + + const handleOpenDevTools = (): void => { + if (window.electronAPI) { + window.electronAPI.openDevTools(); + } + }; + + const handleToggleTerminal = (): void => { + toggleLogPanel(); + }; + + const handleQuitApp = (): void => { + if (window.electronAPI) { + window.electronAPI.closeWindow(); + } + }; + + const handleNotificationClick = (notification: Notification): void => { + markNotificationRead(notification.id); + setSelectedNotification(notification); + }; + + const handleCloseNotificationDetail = (): void => { + setSelectedNotification(null); + }; + + const getNotificationIcon = (type: Notification['type']): React.ReactNode => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + case 'warning': + return ; + default: + return ; + } + }; + + const getResourceColor = (): 'success' | 'warning' | 'error' | 'default' => { + if (!appResources || !monitoringActive) return 'default'; + // Use app's percentage of system memory and CPU (whichever is higher) + const memPercent = appResources.appMemoryPercent; + const cpuPercent = appResources.appCpuPercent; + const maxPercent = Math.max(memPercent, cpuPercent); + if (maxPercent > 50) return 'error'; // App using >50% of system resources + if (maxPercent > 25) return 'warning'; // App using >25% of system resources + return 'success'; + }; + + if (!visible) { + return null; + } + + return ( + + theme.palette.mode === 'dark' ? '#0d1220' : '#f8f9fb', + borderLeft: '1px solid', + borderColor: 'divider', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + py: 1, + gap: 0.5, + position: 'fixed', + top: HEADER_HEIGHT, + right: 0, + zIndex: 1100, + overflowY: 'auto', + overflowX: 'hidden', + }} + > + {/* === Status Section === */} + + {/* Processing indicator + Backend status */} + + + {isProcessing ? processingMessage || 'Processing...' : 'System idle'} + + + Backend: {backendReady ? `Connected (port ${backendPort})` : 'Disconnected'} + + + } + placement="left" + > + + + {isProcessing ? ( + processingProgress !== null ? ( + + ) : ( + + ) + ) : ( + + )} + + {/* Backend status dot */} + + + + + {/* Resource Monitor */} + + setResourcesAnchor(e.currentTarget)} + color={getResourceColor()} + sx={{ + animation: monitoringActive ? 'pulse-slow 3s infinite' : 'none', + '@keyframes pulse-slow': { + '0%, 100%': { opacity: 1 }, + '50%': { opacity: 0.7 }, + }, + }} + > + + + + + {/* Terminal / Log Panel */} + + + + + + + theme.palette.mode === 'dark' ? 'linear-gradient(to right, transparent, rgba(0, 212, 170, 0.2), transparent) 1' : 'none' }} /> + + {/* === Actions Section === */} + + {/* Notifications */} + + setNotificationsAnchor(e.currentTarget)} + size="small" + > + + + + + + + {/* Help & Support */} + + setHelpAnchor(e.currentTarget)} + size="small" + > + + + + + {/* Debug Tools */} + + setDebugAnchor(e.currentTarget)} + size="small" + > + + + + + {/* Spacer */} + + + theme.palette.mode === 'dark' ? 'linear-gradient(to right, transparent, rgba(0, 212, 170, 0.2), transparent) 1' : 'none' }} /> + + {/* === Bottom Section === */} + + {/* About */} + + setAboutOpen(true)} + size="small" + > + + + + + {/* Quit */} + + + + + + + {/* === Menus & Dialogs === */} + + {/* Resources Popover */} + setResourcesAnchor(null)} + anchorOrigin={{ vertical: 'center', horizontal: 'left' }} + transformOrigin={{ vertical: 'center', horizontal: 'right' }} + PaperProps={{ sx: { width: 340, p: 2 } }} + > + + App Resources + + + + {monitoringActive && appResources ? ( + + {/* Total App Usage */} + theme.palette.mode === 'dark' ? 'rgba(0, 212, 170, 0.06)' : 'rgba(13, 155, 122, 0.05)', borderRadius: 1, mb: 1.5, border: '1px solid', borderColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(0, 212, 170, 0.15)' : 'rgba(13, 155, 122, 0.15)' }}> + + TOTAL APP USAGE + + + Memory + + {appResources.totalMemoryMb >= 1024 + ? `${(appResources.totalMemoryMb / 1024).toFixed(2)} GB` + : `${appResources.totalMemoryMb.toFixed(0)} MB`} + + ({appResources.appMemoryPercent.toFixed(1)}% of system) + + + + + CPU + + {appResources.appCpuPercent.toFixed(1)}% + + ({appResources.systemCpuCount} cores) + + + + + + {/* Breakdown by Process */} + + Breakdown by Process + + + {/* Python Backend */} + + Python Backend + + {appResources.backend + ? `${appResources.backend.memoryMb.toFixed(0)} MB | ${(appResources.backend.cpuPercent / appResources.systemCpuCount).toFixed(1)}%` + : 'N/A'} + + + {/* Electron Main */} + + Electron Main + + {appResources.electronMain + ? `${appResources.electronMain.memoryMb.toFixed(0)} MB | ${(appResources.electronMain.cpuPercent / appResources.systemCpuCount).toFixed(1)}%` + : 'N/A'} + + + {/* Electron Renderer */} + + Electron Renderer + + {appResources.electronRenderer + ? `${appResources.electronRenderer.memoryMb.toFixed(0)} MB | ${(appResources.electronRenderer.cpuPercent / appResources.systemCpuCount).toFixed(1)}%` + : 'N/A'} + + + + + {/* System Info */} + + System: {(appResources.systemMemoryTotalMb / 1024).toFixed(0)} GB total |{' '} + {(appResources.systemMemoryAvailableMb / 1024).toFixed(1)} GB available + + + ) : ( + + + {monitoringActive ? 'Loading...' : 'Click "Paused" to start monitoring'} + + + )} + + + + {/* Notifications Menu */} + setNotificationsAnchor(null)} + anchorOrigin={{ vertical: 'top', horizontal: 'left' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + PaperProps={{ sx: { width: 320, maxHeight: 400 } }} + > + + Notifications + {notifications.length > 0 && ( + + )} + + + {notifications.length === 0 ? ( + + + No notifications + + + ) : ( + + {notifications.slice(0, 10).map((notification) => ( + + handleNotificationClick(notification)} + sx={{ + opacity: notification.read ? 0.6 : 1, + bgcolor: notification.read ? 'transparent' : 'action.hover', + }} + > + + {getNotificationIcon(notification.type)} + + + + + ))} + + )} + + + {/* Help Menu */} + setHelpAnchor(null)} + anchorOrigin={{ vertical: 'center', horizontal: 'left' }} + transformOrigin={{ vertical: 'center', horizontal: 'right' }} + > + { handleOpenExternal('https://docs.stingray.science/'); setHelpAnchor(null); }}> + + + + Documentation + + { handleOpenExternal('https://github.com/kartikmandar-GSOC24/StingrayExplorer/issues'); setHelpAnchor(null); }}> + + + + Report Issue + + { handleOpenExternal('https://github.com/StingraySoftware/stingray/discussions'); setHelpAnchor(null); }}> + + + + Community + + + { handleOpenExternal('https://github.com/StingraySoftware/stingray'); setHelpAnchor(null); }}> + + + + Stingray GitHub + + { handleOpenExternal('https://github.com/kartikmandar-GSOC24/StingrayExplorer'); setHelpAnchor(null); }}> + + + + Explorer GitHub + + + + {/* Debug Menu */} + setDebugAnchor(null)} + anchorOrigin={{ vertical: 'center', horizontal: 'left' }} + transformOrigin={{ vertical: 'center', horizontal: 'right' }} + > + { handleOpenDevTools(); setDebugAnchor(null); }}> + + + + Open DevTools + + { + addNotification({ type: 'success', title: 'Test', message: 'Test notification sent!' }); + setDebugAnchor(null); + }}> + + + + Test Notification + + + + + + + + + + + {/* About Dialog */} + setAboutOpen(false)} + maxWidth="sm" + fullWidth + > + + + Stingray Explorer { (e.target as HTMLImageElement).style.display = 'none'; }} + /> + + Stingray Explorer + + {version ? `Version ${version}` : 'Desktop Application'} | © {new Date().getFullYear()} Kartik Mandar + + + + + + + Stingray Explorer is a comprehensive data analysis and visualization dashboard for X-ray astronomy + time series data. Built on top of the{' '} + handleOpenExternal('https://github.com/StingraySoftware/stingray')}> + Stingray + {' '} + Python library for spectral-timing analysis. + + + + Core Dependencies + + +
    +
  • + handleOpenExternal('https://stingray.readthedocs.io/')}> + Stingray + {' '} + - Spectral-timing software +
  • +
  • + handleOpenExternal('https://www.astropy.org/')}> + Astropy + {' '} + - Core astronomy library +
  • +
  • + handleOpenExternal('https://numpy.org/')}> + NumPy + {' '} + & SciPy - Scientific computing +
  • +
  • + handleOpenExternal('https://fastapi.tiangolo.com/')}> + FastAPI + {' '} + - Backend framework +
  • +
  • + handleOpenExternal('https://www.electronjs.org/')}> + Electron + {' '} + - Desktop framework +
  • +
  • + handleOpenExternal('https://react.dev/')}> + React + {' '} + & Material UI - Frontend +
  • +
+
+ + + Acknowledgements + + + This project was developed as part of Google Summer of Code 2024 under the OpenAstronomy + organization. Special thanks to the Stingray team and mentors for their guidance and support. + + + + Contact + + + Kartik Mandar -{' '} + handleOpenExternal('https://github.com/kartikmandar')}> + @kartikmandar + + +
+ + + + +
+ + {/* Notification Detail Dialog */} + + {selectedNotification && ( + <> + + {getNotificationIcon(selectedNotification.type)} + + {selectedNotification.title} + + + + + {selectedNotification.message} + + + {new Date(selectedNotification.timestamp).toLocaleString()} + + + + + + + )} + +
+ ); +}; + +export default RightToolbar; +export { TOOLBAR_WIDTH }; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..ffbc64c --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,478 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { + Box, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Typography, + IconButton, +} from '@mui/material'; +import HomeIcon from '@mui/icons-material/Home'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import AnalyticsIcon from '@mui/icons-material/Analytics'; +import BuildIcon from '@mui/icons-material/Build'; +import ModelTrainingIcon from '@mui/icons-material/ModelTraining'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import ScienceIcon from '@mui/icons-material/Science'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import CloseIcon from '@mui/icons-material/Close'; +import { ThemeContext } from '../../App'; +import JobStatusPanel from './JobStatusPanel'; + +// Sidebar widths +const MAIN_DRAWER_WIDTH = 240; +const SUB_DRAWER_WIDTH = 240; + +interface SidebarProps { + open: boolean; + onSubmenuStateChange: (isOpen: boolean) => void; +} + +interface SubmenuItem { + text: string; + path: string; +} + +interface MenuItem { + text: string; + icon: React.ReactNode; + path: string; + hasSubmenu?: boolean; + submenuItems?: SubmenuItem[]; +} + +/** + * Sidebar navigation component with VAST-style categorized submenu + */ +const Sidebar: React.FC = ({ open, onSubmenuStateChange }) => { + const navigate = useNavigate(); + const location = useLocation(); + const { darkMode } = useContext(ThemeContext); + const [activeSubmenu, setActiveSubmenu] = useState(null); + const [showSubmenu, setShowSubmenu] = useState(true); + + // Notify parent of submenu state changes + useEffect(() => { + onSubmenuStateChange(!!activeSubmenu && showSubmenu); + }, [activeSubmenu, showSubmenu, onSubmenuStateChange]); + + // Navigation items configuration + const menuItems: MenuItem[] = [ + { + text: 'Home', + icon: , + path: '/', + }, + { + text: 'Data Ingestion', + icon: , + path: '/data-ingestion', + }, + { + text: 'QuickLook Analysis', + icon: , + path: '/quicklook', + hasSubmenu: true, + submenuItems: [ + { text: 'Event List', path: '/quicklook/event-list' }, + { text: 'Light Curve', path: '/quicklook/light-curve' }, + { text: 'Power Spectrum', path: '/quicklook/power-spectrum' }, + { text: 'Avg Power Spectrum', path: '/quicklook/avg-power-spectrum' }, + { text: 'Cross Spectrum', path: '/quicklook/cross-spectrum' }, + { text: 'Avg Cross Spectrum', path: '/quicklook/avg-cross-spectrum' }, + { text: 'Dynamical Power Spectrum', path: '/quicklook/dynamical-power-spectrum' }, + { text: 'Coherence', path: '/quicklook/coherence' }, + { text: 'Time Lags', path: '/quicklook/time-lags' }, + { text: 'Cross Correlation', path: '/quicklook/cross-correlation' }, + { text: 'Auto Correlation', path: '/quicklook/auto-correlation' }, + { text: 'Dead Time Corrections', path: '/quicklook/dead-time-corrections' }, + { text: 'Bispectrum', path: '/quicklook/bispectrum' }, + { text: 'Power Colors', path: '/quicklook/power-colors' }, + { text: 'Covariance Spectrum', path: '/quicklook/covariance-spectrum' }, + { text: 'Avg Covariance Spectrum', path: '/quicklook/avg-covariance-spectrum' }, + { text: 'Variable Energy Spectrum', path: '/quicklook/variable-energy-spectrum' }, + { text: 'RMS Energy Spectrum', path: '/quicklook/rms-energy-spectrum' }, + { text: 'Lag Energy Spectrum', path: '/quicklook/lag-energy-spectrum' }, + { text: 'Excess Variance Spectrum', path: '/quicklook/excess-variance-spectrum' }, + ], + }, + { + text: 'Utilities', + icon: , + path: '/utilities', + hasSubmenu: true, + submenuItems: [ + { text: 'Statistical Functions', path: '/utilities/statistical-functions' }, + { text: 'GTI Functionality', path: '/utilities/gti' }, + { text: 'I/O Functionality', path: '/utilities/io' }, + { text: 'Mission Specific I/O', path: '/utilities/mission-io' }, + { text: 'Misc', path: '/utilities/misc' }, + ], + }, + { + text: 'Modeling', + icon: , + path: '/modeling', + hasSubmenu: true, + submenuItems: [ + { text: 'Model Builder', path: '/modeling/builder' }, + { text: 'MLE Fitting', path: '/modeling/mle' }, + { text: 'MCMC Fitting', path: '/modeling/mcmc' }, + ], + }, + { + text: 'Pulsar', + icon: , + path: '/pulsar', + hasSubmenu: true, + submenuItems: [ + { text: 'Period Search', path: '/pulsar/search' }, + { text: 'Phase Folding', path: '/pulsar/folding' }, + { text: 'Phaseogram', path: '/pulsar/phaseogram' }, + ], + }, + { + text: 'Simulator', + icon: , + path: '/simulator', + }, + ]; + + const getActiveMenuItem = (): MenuItem | undefined => { + return menuItems.find((item) => item.text === activeSubmenu); + }; + + const handleMenuClick = (item: MenuItem): void => { + if (item.hasSubmenu) { + setActiveSubmenu(item.text); + setShowSubmenu(true); + } else { + setActiveSubmenu(null); + setShowSubmenu(false); + navigate(item.path); + } + }; + + // Categorization for QuickLook submenu + const quicklookCategories: Record = { + 'Time Domain': ['Event List', 'Light Curve'], + 'Frequency Domain': [ + 'Power Spectrum', + 'Avg Power Spectrum', + 'Cross Spectrum', + 'Avg Cross Spectrum', + 'Dynamical Power Spectrum', + 'Coherence', + 'Time Lags', + ], + 'Correlation Analysis': ['Cross Correlation', 'Auto Correlation'], + 'Advanced Analysis': [ + 'Dead Time Corrections', + 'Bispectrum', + 'Power Colors', + 'Covariance Spectrum', + 'Avg Covariance Spectrum', + ], + 'Energy Dependent Analysis': [ + 'Variable Energy Spectrum', + 'RMS Energy Spectrum', + 'Lag Energy Spectrum', + 'Excess Variance Spectrum', + ], + }; + + // Active item styling + const getItemSx = (isActive: boolean) => ({ + pl: 2, + position: 'relative' as const, + '&::before': isActive + ? { + content: '""', + position: 'absolute', + left: 0, + top: '20%', + bottom: '20%', + width: '3px', + borderRadius: '0 2px 2px 0', + backgroundColor: 'primary.main', + boxShadow: darkMode + ? '0 0 8px rgba(0, 212, 170, 0.4), 0 0 16px rgba(0, 212, 170, 0.15)' + : '0 0 6px rgba(13, 155, 122, 0.3)', + } + : {}, + backgroundColor: isActive + ? darkMode + ? 'rgba(0, 212, 170, 0.08)' + : 'rgba(13, 155, 122, 0.08)' + : 'transparent', + '&:hover': { + backgroundColor: darkMode + ? 'rgba(0, 212, 170, 0.06)' + : 'rgba(13, 155, 122, 0.06)', + }, + }); + + const renderMainMenu = (): React.ReactNode => ( + + {menuItems.map((item) => { + const isActive = !item.hasSubmenu && location.pathname === item.path; + return ( + + handleMenuClick(item)} + selected={isActive} + sx={getItemSx(isActive)} + > + + {item.icon} + + + {item.hasSubmenu && ( + + )} + + + ); + })} + + ); + + const renderSubmenu = (): React.ReactNode => { + const activeItem = getActiveMenuItem(); + if (!activeItem?.submenuItems) return null; + + // Determine if we should use categories (only for QuickLook) + const useCategories = activeItem.text === 'QuickLook Analysis'; + const categories = useCategories ? quicklookCategories : null; + + return ( + <> + {/* Submenu header */} + + + {activeItem.text} + + setShowSubmenu(false)} sx={{ ml: 1 }}> + + + + + {/* Categorized or flat list */} + {categories ? ( + // Categorized rendering for QuickLook + Object.entries(categories).map(([category, items]) => ( + + + {category} + + + {activeItem.submenuItems + ?.filter((subItem) => items.includes(subItem.text)) + .map((subItem) => { + const isActive = location.pathname === subItem.path; + return ( + + navigate(subItem.path)} + selected={isActive} + sx={getItemSx(isActive)} + > + + + + ); + })} + + + )) + ) : ( + // Flat list for other menus + + {activeItem.submenuItems?.map((subItem) => { + const isActive = location.pathname === subItem.path; + return ( + + navigate(subItem.path)} + selected={isActive} + sx={getItemSx(isActive)} + > + + + + ); + })} + + )} + + ); + }; + + // Scrollbar styles + const scrollbarStyles = { + overflow: 'auto', + height: '100%', + '&::-webkit-scrollbar': { + width: '4px', + backgroundColor: 'transparent', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: darkMode ? 'rgba(0, 212, 170, 0.2)' : 'rgba(13, 155, 122, 0.15)', + borderRadius: '4px', + '&:hover': { + backgroundColor: darkMode ? 'rgba(0, 212, 170, 0.35)' : 'rgba(13, 155, 122, 0.3)', + }, + }, + '&::-webkit-scrollbar-track': { + backgroundColor: 'transparent', + }, + }; + + // Sidebar background — slightly darker than paper for depth + const sidebarBg = darkMode ? '#0d1220' : '#f8f9fb'; + + return ( + <> + {/* Main Sidebar */} + theme.zIndex.drawer, + }} + > + {/* Menu items - scrollable */} + + {renderMainMenu()} + + + {/* Job Status Panel - fixed at bottom */} + + + + {/* Submenu Sidebar */} + theme.zIndex.drawer, + }} + > + + {renderSubmenu()} + + + + ); +}; + +export default Sidebar; diff --git a/src/components/plots/PlotlyChart.test.tsx b/src/components/plots/PlotlyChart.test.tsx new file mode 100644 index 0000000..c4ecf7d --- /dev/null +++ b/src/components/plots/PlotlyChart.test.tsx @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('react-plotly.js', () => ({ + default: ({ data, layout }: { data: unknown[]; layout: Record }) => ( +
).map((t) => t.type).join(',')} + data-xtype={(layout.xaxis as { type?: string })?.type ?? 'linear'} + data-ytype={(layout.yaxis as { type?: string })?.type ?? 'linear'} + data-xgrid={(layout.xaxis as { gridcolor?: string })?.gridcolor ?? ''} + /> + ), +})); + +import PlotlyChart from './PlotlyChart'; + +describe('PlotlyChart', () => { + it('renders traces and merges page layout over theme defaults', async () => { + render( + + ); + await waitFor(() => expect(screen.getByTestId('plotly-mock')).toBeInTheDocument()); + expect(screen.getByTestId('plotly-mock').dataset.traces).toBe('1'); + expect(screen.getByTestId('plotly-mock').dataset.xtype).toBe('log'); + expect(screen.getByTestId('plotly-mock').dataset.ytype).toBe('log'); + expect(screen.getByTestId('plotly-mock').dataset.xgrid).not.toBe(''); + }); + + it('falls back from scattergl to scatter when WebGL is unavailable', async () => { + // jsdom canvases have no WebGL: getContext('webgl') returns null, which is + // exactly the environment the fallback must handle (plotly would otherwise + // render "WebGL is not supported by your browser" into the plot div). + render( + + ); + await waitFor(() => expect(screen.getByTestId('plotly-mock')).toBeInTheDocument()); + expect(screen.getByTestId('plotly-mock').dataset.tracetypes).toBe('scatter,heatmap'); + }); +}); diff --git a/src/components/plots/PlotlyChart.tsx b/src/components/plots/PlotlyChart.tsx new file mode 100644 index 0000000..6b226b1 --- /dev/null +++ b/src/components/plots/PlotlyChart.tsx @@ -0,0 +1,88 @@ +import React, { Suspense, useMemo } from 'react'; +import { Box, CircularProgress, useTheme } from '@mui/material'; +import type { Config, Data, Layout } from 'plotly.js'; + +// plotly.js is ~3 MB; load it only when a page actually renders a chart. +const Plot = React.lazy(() => import('react-plotly.js')); + +export interface PlotlyChartProps { + data: Data[]; + layout?: Partial; + height?: number | string; +} + +const PLOT_CONFIG: Partial = { + responsive: true, + displaylogo: false, + modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'], +}; + +// Plotly's gl traces request their context with failIfMajorPerformanceCaveat, +// so a renderer stuck in software compositing (e.g. after a GPU-process +// failure) refuses them and plotly renders "WebGL is not supported by your +// browser" into the plot div. Probe with the same flag once per session and +// transparently fall back to SVG scatter when gl isn't genuinely available. +let webglSupport: boolean | null = null; + +function webglAvailable(): boolean { + if (webglSupport === null) { + try { + const canvas = document.createElement('canvas'); + webglSupport = !!canvas.getContext('webgl', { failIfMajorPerformanceCaveat: true }); + } catch { + webglSupport = false; + } + } + return webglSupport; +} + +const PlotlyChart: React.FC = ({ data, layout = {}, height = 440 }) => { + const displayData = useMemo(() => { + if (webglAvailable()) return data; + return data.map((trace) => + (trace as { type?: string }).type === 'scattergl' + ? ({ ...trace, type: 'scatter' } as Data) + : trace + ); + }, [data]); + + const theme = useTheme(); + const isDark = theme.palette.mode === 'dark'; + const gridColor = isDark ? 'rgba(148, 163, 184, 0.12)' : 'rgba(100, 116, 139, 0.2)'; + + const mergedLayout: Partial = { + autosize: true, + paper_bgcolor: 'rgba(0,0,0,0)', + plot_bgcolor: 'rgba(0,0,0,0)', + font: { + family: '"IBM Plex Sans", sans-serif', + size: 12, + color: theme.palette.text.primary, + }, + margin: { l: 64, r: 24, t: 24, b: 52 }, + showlegend: false, + ...layout, + xaxis: { gridcolor: gridColor, zeroline: false, ...layout.xaxis }, + yaxis: { gridcolor: gridColor, zeroline: false, ...layout.yaxis }, + }; + + return ( + + + + } + > + + + ); +}; + +export default PlotlyChart; diff --git a/src/hooks/useAnalysisRunner.test.tsx b/src/hooks/useAnalysisRunner.test.tsx new file mode 100644 index 0000000..23ea46a --- /dev/null +++ b/src/hooks/useAnalysisRunner.test.tsx @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { act, renderHook } from '@testing-library/react'; +import { useAnalysisRunner } from './useAnalysisRunner'; +import { useUIStore } from '@/store/uiStore'; + +describe('useAnalysisRunner', () => { + beforeEach(() => { + useUIStore.setState({ notifications: [], unreadNotificationCount: 0 }); + }); + + it('stores the result and pushes a success notification', async () => { + const { result } = renderHook(() => useAnalysisRunner<{ v: number }>('Test Op')); + await act(async () => { + await result.current.run(async () => ({ + success: true, + data: { v: 42 }, + message: 'computed', + error: null, + })); + }); + expect(result.current.result?.v).toBe(42); + expect(result.current.running).toBe(false); + expect(result.current.error).toBeNull(); + const notes = useUIStore.getState().notifications; + expect(notes[0].type).toBe('success'); + expect(notes[0].title).toBe('Test Op'); + }); + + it('captures success:false as an error and keeps the previous result', async () => { + const { result } = renderHook(() => useAnalysisRunner<{ v: number }>('Test Op')); + await act(async () => { + await result.current.run(async () => ({ + success: true, + data: { v: 1 }, + message: '', + error: null, + })); + }); + await act(async () => { + await result.current.run(async () => ({ + success: false, + data: null, + message: 'bad dt', + error: 'bad dt', + })); + }); + expect(result.current.error).toBe('bad dt'); + expect(result.current.result?.v).toBe(1); + expect(useUIStore.getState().notifications[0].type).toBe('error'); + }); + + it('reset clears result, error, and running', async () => { + const { result } = renderHook(() => useAnalysisRunner<{ v: number }>('Test Op')); + await act(async () => { + await result.current.run(async () => ({ + success: true, + data: { v: 7 }, + message: '', + error: null, + })); + }); + act(() => { + result.current.reset(); + }); + expect(result.current.result).toBeNull(); + expect(result.current.error).toBeNull(); + expect(result.current.running).toBe(false); + }); + + it('captures thrown errors (network failures)', async () => { + const { result } = renderHook(() => useAnalysisRunner('Test Op')); + await act(async () => { + await result.current.run(async () => { + throw new Error('connection refused'); + }); + }); + expect(result.current.error).toBe('connection refused'); + }); +}); diff --git a/src/hooks/useAnalysisRunner.ts b/src/hooks/useAnalysisRunner.ts new file mode 100644 index 0000000..f07fd44 --- /dev/null +++ b/src/hooks/useAnalysisRunner.ts @@ -0,0 +1,51 @@ +import { useCallback, useState } from 'react'; +import { ApiResponse } from '@/api/client'; +import { useUIStore } from '@/store/uiStore'; + +interface AnalysisRunnerState { + result: T | null; + running: boolean; + error: string | null; +} + +/** + * Owns the lifecycle of a single analysis request: running flag, last + * successful result, last error, and success/error notifications. + * On failure the previous result is kept so the plot doesn't vanish. + */ +export function useAnalysisRunner(label: string) { + const addNotification = useUIStore((s) => s.addNotification); + const [state, setState] = useState>({ + result: null, + running: false, + error: null, + }); + + const run = useCallback( + async (call: () => Promise>): Promise => { + setState((s) => ({ ...s, running: true, error: null })); + try { + const res = await call(); + if (res.success && res.data != null) { + setState({ result: res.data, running: false, error: null }); + addNotification({ type: 'success', title: label, message: res.message || 'Done' }); + } else { + const msg = res.error || res.message || 'Operation failed'; + setState((s) => ({ ...s, running: false, error: msg })); + addNotification({ type: 'error', title: label, message: msg }); + } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + setState((s) => ({ ...s, running: false, error: msg })); + addNotification({ type: 'error', title: label, message: msg }); + } + }, + [label, addNotification] + ); + + const reset = useCallback((): void => { + setState({ result: null, running: false, error: null }); + }, []); + + return { ...state, run, reset }; +} diff --git a/src/hooks/useEventLists.test.tsx b/src/hooks/useEventLists.test.tsx new file mode 100644 index 0000000..21c0364 --- /dev/null +++ b/src/hooks/useEventLists.test.tsx @@ -0,0 +1,45 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; + +const listEventLists = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { listEventLists: (...args: unknown[]) => listEventLists(...args) }, +})); + +import { useEventLists } from './useEventLists'; + +/** Mint a fresh QueryClient per test, outside the wrapper render path. */ +const createWrapper = (): React.FC<{ children: React.ReactNode }> => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} + ); + return Wrapper; +}; + +describe('useEventLists', () => { + beforeEach(() => listEventLists.mockReset()); + + it('returns event list summaries on success', async () => { + listEventLists.mockResolvedValue({ + success: true, + data: [{ name: 'ev1', n_events: 10, time_range: [0, 1] }], + message: '', + error: null, + }); + const { result } = renderHook(() => useEventLists(), { wrapper: createWrapper() }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.[0].name).toBe('ev1'); + }); + + it('surfaces a success:false response as a query error', async () => { + listEventLists.mockResolvedValue({ success: false, data: null, message: 'boom', error: 'boom' }); + const { result } = renderHook(() => useEventLists(), { wrapper: createWrapper() }); + await waitFor(() => expect(result.current.isError).toBe(true)); + expect((result.current.error as Error).message).toBe('boom'); + }); +}); diff --git a/src/hooks/useEventLists.ts b/src/hooks/useEventLists.ts new file mode 100644 index 0000000..621a360 --- /dev/null +++ b/src/hooks/useEventLists.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { dataApi, EventListSummary } from '@/api/dataApi'; + +export const EVENT_LISTS_QUERY_KEY = ['eventLists'] as const; + +/** + * Loaded event lists from the backend. The ApiClient re-resolves the backend + * port on every request, so this works without gating on backend readiness; + * failures surface as query errors with a retry affordance in the UI. + */ +export function useEventLists() { + return useQuery({ + queryKey: EVENT_LISTS_QUERY_KEY, + queryFn: async (): Promise => { + const res = await dataApi.listEventLists(); + if (!res.success) { + throw new Error(res.error || res.message || 'Failed to list event lists'); + } + return res.data ?? []; + }, + staleTime: 5_000, + }); +} diff --git a/src/hooks/useJobStream.ts b/src/hooks/useJobStream.ts new file mode 100644 index 0000000..9d8455e --- /dev/null +++ b/src/hooks/useJobStream.ts @@ -0,0 +1,354 @@ +/** + * React hook for managing the SSE connection to the job stream. + * + * This hook establishes and maintains a persistent SSE connection to + * receive real-time job updates. It handles connection lifecycle, + * automatic reconnection on disconnect, and updates the job store. + * It also triggers detailed notifications when jobs complete or fail, + * including individual error notifications for batch job failures. + */ + +import { useEffect, useRef, useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useJobStore } from '@/store/jobStore'; +import { useUIStore, NotificationType } from '@/store/uiStore'; +import { useBackendContext } from '@/App'; +import { jobApi } from '@/api/jobApi'; +import { EVENT_LISTS_QUERY_KEY } from '@/hooks/useEventLists'; +import type { Job, JobStreamEvent } from '@/types/job'; + +/** + * Interface for a failed file in a batch job result. + */ +interface BatchFailedFile { + name: string; + file_path: string; + error: string; +} + +/** + * Interface for a successful file in a batch job result. + */ +interface BatchSuccessfulFile { + name: string; + file_path: string; + data?: { + n_events?: number; + gti_warnings?: string[]; + stingray_warnings?: string[]; + validation_issues?: string[]; + }; +} + +/** + * Interface for batch job result structure. + */ +interface BatchJobResult { + successful?: BatchSuccessfulFile[]; + failed?: BatchFailedFile[]; + success_count?: number; + failure_count?: number; + total_files?: number; +} + +/** Reconnection delay in milliseconds */ +const RECONNECT_DELAY = 5000; + +/** Maximum reconnection attempts before giving up */ +const MAX_RECONNECT_ATTEMPTS = 10; + +/** + * Build a detailed notification message from a completed job. + */ +function buildCompletionMessage(job: Job): string { + const result = job.result || {}; + const parts: string[] = []; + + // Add event count if available + if (typeof result.event_count === 'number') { + parts.push(`${result.event_count.toLocaleString()} events`); + } + + // Add time range if available + if (typeof result.time_start === 'number' && typeof result.time_end === 'number') { + const duration = (result.time_end as number) - (result.time_start as number); + parts.push(`${duration.toFixed(2)}s duration`); + } + + // Add warnings count if any + if (Array.isArray(result.warnings) && result.warnings.length > 0) { + parts.push(`${result.warnings.length} warning${result.warnings.length > 1 ? 's' : ''}`); + } + + // For batch jobs, show success/total counts + if (job.type === 'load_batch' && typeof result.success_count === 'number') { + const total = result.total_files || job.total_items; + parts.push(`${result.success_count}/${total} files loaded`); + } + + if (parts.length > 0) { + return `${job.display_name} (${parts.join(', ')})`; + } + + return job.display_name; +} + +/** + * Extract filename from a file path for display in notifications. + */ +function getFilename(filePath: string): string { + return filePath.split('/').pop() || filePath; +} + +/** + * Handle batch job warnings and failures by creating individual notifications. + * + * For batch jobs that complete with partial failures: + * - Creates an error notification for each failed file with its specific error + * - Optionally could surface GTI/stingray warnings from successful files + */ +function handleBatchWarnings( + job: Job, + addNotification: (notification: { type: NotificationType; title: string; message: string }) => void +): void { + const result = job.result as BatchJobResult | null; + + if (!result) { + return; + } + + const failed = result.failed; + + // Create individual error notifications for each failed file + if (Array.isArray(failed) && failed.length > 0) { + for (const failedFile of failed) { + const filename = getFilename(failedFile.file_path); + addNotification({ + type: 'error', + title: `Failed: ${filename}`, + message: failedFile.error || 'Unknown error occurred', + }); + } + } + + // Optionally surface significant warnings from successful files + // This is commented out by default to avoid notification overload, + // but can be enabled if users want to see all warnings + /* + const successful = result.successful; + if (Array.isArray(successful)) { + for (const successFile of successful) { + const warnings = [ + ...(successFile.data?.gti_warnings || []), + ...(successFile.data?.stingray_warnings || []), + ]; + + if (warnings.length > 0) { + const filename = getFilename(successFile.file_path); + addNotification({ + type: 'warning', + title: `Warnings: ${filename}`, + message: `${warnings.length} warning(s): ${warnings[0]}${warnings.length > 1 ? '...' : ''}`, + }); + } + } + } + */ +} + +/** + * Hook to manage the job stream SSE connection. + * + * Should be called once at the app root level to establish and + * maintain the connection throughout the app lifecycle. + */ +export function useJobStream(): void { + const { isReady: backendReady, port } = useBackendContext(); + const { + setConnected, + handleEvent, + incrementReconnectAttempts, + resetReconnectAttempts, + reconnectAttempts, + } = useJobStore(); + const addNotification = useUIStore((state) => state.addNotification); + const queryClient = useQueryClient(); + + // Track if we should be connected + const shouldConnectRef = useRef(false); + + // Track the abort controller for cleanup + const abortControllerRef = useRef(null); + + // Track reconnection timeout + const reconnectTimeoutRef = useRef | null>(null); + + // Track which jobs we've already notified to avoid duplicates on reconnection + const notifiedJobsRef = useRef>(new Set()); + + /** + * Handle job completion/failure notifications. + * Only triggers notifications for jobs we haven't already notified about. + */ + const handleJobNotification = useCallback((event: JobStreamEvent): void => { + // Only handle completion and failure events + if (event.type !== 'job_completed' && event.type !== 'job_failed') { + return; + } + + const job = event.job; + + // Skip if we've already notified about this job + if (notifiedJobsRef.current.has(job.id)) { + return; + } + + // Mark as notified + notifiedJobsRef.current.add(job.id); + + // Clean up old entries (keep last 100 to avoid memory leak) + if (notifiedJobsRef.current.size > 100) { + const entries = Array.from(notifiedJobsRef.current); + notifiedJobsRef.current = new Set(entries.slice(-100)); + } + + if (event.type === 'job_completed') { + // All job types load event lists, so refresh any mounted event list + // queries (e.g. analysis page selectors). Kept inside the + // notifiedJobsRef dedup guard so SSE reconnect replays don't + // re-invalidate for jobs we've already processed. + void queryClient.invalidateQueries({ queryKey: EVENT_LISTS_QUERY_KEY }); + + const message = buildCompletionMessage(job); + addNotification({ + type: 'success', + title: 'Data Loaded', + message, + }); + + // For batch jobs, show individual error notifications for failed files + if (job.type === 'load_batch') { + handleBatchWarnings(job, addNotification); + } + } else if (event.type === 'job_failed') { + addNotification({ + type: 'error', + title: 'Load Failed', + message: job.error || `Failed to load: ${job.display_name}`, + }); + } + }, [addNotification, queryClient]); + + const connect = useCallback(async () => { + if (!backendReady || !port) { + console.log('[JobStream] Backend not ready, waiting...'); + return; + } + + if (!shouldConnectRef.current) { + console.log('[JobStream] Connection not requested'); + return; + } + + // Cancel any pending reconnection + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + // Create new abort controller + abortControllerRef.current = new AbortController(); + + console.log('[JobStream] Connecting to job stream...'); + + try { + // Process events from the stream + for await (const event of jobApi.streamJobUpdates()) { + // Check if we should stop + if (abortControllerRef.current?.signal.aborted) { + console.log('[JobStream] Connection aborted'); + break; + } + + // On first event, mark as connected + setConnected(true, null); + resetReconnectAttempts(); + + // Handle the event (update job store) + handleEvent(event); + + // Trigger notifications for completed/failed jobs + handleJobNotification(event); + + // Log non-heartbeat events for debugging + if (event.type !== 'heartbeat') { + console.log('[JobStream] Event:', event.type); + } + } + + // Stream ended normally + console.log('[JobStream] Stream ended'); + setConnected(false, null); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('[JobStream] Connection error:', errorMessage); + setConnected(false, errorMessage); + } + + // Schedule reconnection if we should still be connected + if (shouldConnectRef.current && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + incrementReconnectAttempts(); + const delay = RECONNECT_DELAY * Math.min(reconnectAttempts + 1, 3); // Exponential backoff up to 3x + console.log(`[JobStream] Reconnecting in ${delay}ms (attempt ${reconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`); + reconnectTimeoutRef.current = setTimeout(connect, delay); + } else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + console.error('[JobStream] Max reconnection attempts reached, giving up'); + setConnected(false, 'Max reconnection attempts reached'); + } + }, [ + backendReady, + port, + setConnected, + handleEvent, + handleJobNotification, + incrementReconnectAttempts, + resetReconnectAttempts, + reconnectAttempts, + ]); + + const disconnect = useCallback(() => { + shouldConnectRef.current = false; + + // Cancel pending reconnection + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + // Abort current connection + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + + setConnected(false, null); + console.log('[JobStream] Disconnected'); + }, [setConnected]); + + // Connect when backend becomes ready + useEffect(() => { + if (backendReady && port) { + shouldConnectRef.current = true; + connect(); + } + + return () => { + disconnect(); + }; + }, [backendReady, port, connect, disconnect]); + + // Reconnect when reconnectAttempts changes (for reconnection loop) + // This effect is intentionally left with an empty dep array to avoid loops +} + +export default useJobStream; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b94ab1e --- /dev/null +++ b/src/index.css @@ -0,0 +1,287 @@ +/* ========================================================================== + Stingray Explorer — Observatory Dark Global Styles + ========================================================================== */ + +/* Font Imports (self-hosted via @fontsource) */ +@import '@fontsource/jetbrains-mono/400.css'; +@import '@fontsource/jetbrains-mono/500.css'; +@import '@fontsource/jetbrains-mono/700.css'; +@import '@fontsource/ibm-plex-sans/400.css'; +@import '@fontsource/ibm-plex-sans/400-italic.css'; +@import '@fontsource/ibm-plex-sans/500.css'; +@import '@fontsource/ibm-plex-sans/600.css'; +@import '@fontsource/ibm-plex-sans/700.css'; +@import '@fontsource/ibm-plex-mono/400.css'; +@import '@fontsource/ibm-plex-mono/700.css'; + +/* -------------------------------------------------------------------------- + CSS Custom Properties + -------------------------------------------------------------------------- */ +:root { + /* Glass effects */ + --glass-blur: 12px; + --glass-saturation: 150%; + --glass-bg-dark: rgba(18, 24, 41, 0.6); + --glass-bg-light: rgba(255, 255, 255, 0.7); + --glass-border: rgba(148, 163, 184, 0.12); + + /* Glow effects */ + --glow-primary: rgba(0, 212, 170, 0.15); + --glow-secondary: rgba(59, 130, 246, 0.15); + --glow-stingray: rgba(94, 173, 97, 0.12); + + /* Grain */ + --grain-opacity: 0.03; + + /* Transitions */ + --transition-smooth: cubic-bezier(0.22, 0.61, 0.36, 1); + --transition-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); + + /* Font stacks */ + --font-display: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace; + --font-body: 'IBM Plex Sans', 'Source Sans 3', -apple-system, sans-serif; + --font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', monospace; +} + +/* -------------------------------------------------------------------------- + Reset & Base + -------------------------------------------------------------------------- */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; + width: 100%; +} + +body { + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; +} + +/* -------------------------------------------------------------------------- + Grain Texture Overlay (atmospheric depth) + -------------------------------------------------------------------------- */ +body::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 9999; + opacity: var(--grain-opacity); + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 256px 256px; +} + +/* -------------------------------------------------------------------------- + Scrollbar — Teal-tinted, thin + -------------------------------------------------------------------------- */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(0, 212, 170, 0.2); + border-radius: 3px; + transition: background-color 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 212, 170, 0.4); +} + +/* Light mode scrollbar */ +[data-theme='light'] ::-webkit-scrollbar-thumb { + background-color: rgba(13, 155, 122, 0.2); +} + +[data-theme='light'] ::-webkit-scrollbar-thumb:hover { + background-color: rgba(13, 155, 122, 0.35); +} + +/* -------------------------------------------------------------------------- + Selection & Focus + -------------------------------------------------------------------------- */ +::selection { + background-color: rgba(0, 212, 170, 0.3); + color: #e2e8f0; +} + +:focus-visible { + outline: 2px solid rgba(0, 212, 170, 0.6); + outline-offset: 2px; +} + +/* -------------------------------------------------------------------------- + Utility Classes + -------------------------------------------------------------------------- */ + +/* Prevent text selection on UI elements */ +.no-select { + user-select: none; + -webkit-user-select: none; +} + +/* Drag region for window (macOS) */ +.titlebar-drag-region { + -webkit-app-region: drag; +} + +.titlebar-no-drag { + -webkit-app-region: no-drag; +} + +/* Glass surface utility */ +.glass-surface { + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); +} + +/* Glow hover utility */ +.glow-hover { + transition: box-shadow 0.3s var(--transition-smooth), + border-color 0.3s var(--transition-smooth); +} + +.glow-hover:hover { + box-shadow: 0 0 20px var(--glow-primary), 0 0 40px rgba(0, 212, 170, 0.05); + border-color: rgba(0, 212, 170, 0.3); +} + +/* Stagger reveal — children animate in sequence */ +.stagger-reveal > * { + opacity: 0; + animation: fadeInUp 0.5s var(--transition-smooth) forwards; +} + +.stagger-reveal > *:nth-child(1) { animation-delay: 0ms; } +.stagger-reveal > *:nth-child(2) { animation-delay: 80ms; } +.stagger-reveal > *:nth-child(3) { animation-delay: 160ms; } +.stagger-reveal > *:nth-child(4) { animation-delay: 240ms; } +.stagger-reveal > *:nth-child(5) { animation-delay: 320ms; } +.stagger-reveal > *:nth-child(6) { animation-delay: 400ms; } +.stagger-reveal > *:nth-child(7) { animation-delay: 480ms; } +.stagger-reveal > *:nth-child(8) { animation-delay: 560ms; } + +/* Gradient border bottom */ +.gradient-border-bottom { + border-image: linear-gradient(to right, #00d4aa, #3b82f6, transparent) 1; +} + +/* Force display font */ +.text-display { + font-family: var(--font-display) !important; +} + +/* Force mono font */ +.text-mono { + font-family: var(--font-mono) !important; +} + +/* -------------------------------------------------------------------------- + Plotly Chart Container + -------------------------------------------------------------------------- + Note: never add a global .js-plotly-plot width/height !important rule here. + It overrides PlotlyChart's explicit inline sizing and collapses every chart + to zero height inside auto-height containers (the plot renders but occupies + no space until a window resize forces a re-measure). */ + +/* -------------------------------------------------------------------------- + Keyframe Animations + -------------------------------------------------------------------------- */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideIn { + from { + transform: translateX(-20px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes glowPulse { + 0%, 100% { box-shadow: 0 0 8px var(--glow-primary); } + 50% { box-shadow: 0 0 20px var(--glow-primary); } +} + +@keyframes borderGlow { + 0%, 100% { border-color: rgba(0, 212, 170, 0.2); } + 50% { border-color: rgba(0, 212, 170, 0.5); } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes statusPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes starTwinkle { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +/* Animation utility classes */ +.animate-fade-in { + animation: fadeIn 0.2s ease-in-out; +} + +.animate-slide-in { + animation: slideIn 0.3s ease-out; +} + +.animate-fade-in-up { + animation: fadeInUp 0.5s var(--transition-smooth); +} + +/* Loading spinner */ +.loading-spinner { + display: inline-block; + width: 24px; + height: 24px; + border: 3px solid rgba(0, 212, 170, 0.1); + border-radius: 50%; + border-top-color: #00d4aa; + animation: spin 1s ease-in-out infinite; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..ea3ecf9 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +// Render the application +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/pages/DataIngestion/FileBrowserDialog.tsx b/src/pages/DataIngestion/FileBrowserDialog.tsx new file mode 100644 index 0000000..83b9063 --- /dev/null +++ b/src/pages/DataIngestion/FileBrowserDialog.tsx @@ -0,0 +1,626 @@ +/** + * File Browser Dialog for HEASARC observations + * + * Shows a tree view of files in an observation directory, + * allowing users to select and download files to their local disk. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, + CircularProgress, + Alert, + Checkbox, + IconButton, + Collapse, + LinearProgress, + Chip, +} from '@mui/material'; +import FolderIcon from '@mui/icons-material/Folder'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; +import StarIcon from '@mui/icons-material/Star'; +import SettingsIcon from '@mui/icons-material/Settings'; +import AttachFileIcon from '@mui/icons-material/AttachFile'; +import DescriptionIcon from '@mui/icons-material/Description'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import SaveAltIcon from '@mui/icons-material/SaveAlt'; +import { + archiveApi, + FileEntry, + FileType, +} from '@/api/archiveApi'; +import { useUIStore } from '@/store/uiStore'; + +interface ObsData { + ra?: number | null; + dec?: number | null; + prnb?: string; +} + +interface FileBrowserDialogProps { + open: boolean; + onClose: () => void; + mission: string; + obsid: string; + obsTime: string; + targetName: string; + obsData?: ObsData; + onDownloadComplete?: (filePath: string) => void; +} + +interface DownloadState { + status: 'idle' | 'downloading' | 'complete' | 'error'; + message: string; + percent: number; + filePath?: string; + error?: string; +} + +// File type to icon mapping +const getFileIcon = (fileType: FileType): React.ReactNode => { + switch (fileType) { + case 'event': + return ; + case 'calibration': + return ; + case 'auxiliary': + return ; + case 'log': + return ; + default: + return ; + } +}; + +interface FileTreeItemProps { + entry: FileEntry; + depth: number; + selectedFiles: Set; + onToggleSelect: (entry: FileEntry) => void; + expandedDirs: Set; + onToggleExpand: (path: string) => void; +} + +const FileTreeItem: React.FC = ({ + entry, + depth, + selectedFiles, + onToggleSelect, + expandedDirs, + onToggleExpand, +}) => { + const isExpanded = expandedDirs.has(entry.full_url); + const isSelected = selectedFiles.has(entry.full_url); + + return ( + + entry.is_directory && onToggleExpand(entry.full_url)} + > + {/* Expand/collapse button for directories */} + {entry.is_directory ? ( + + {isExpanded ? : } + + ) : ( + + )} + + {/* Checkbox for files only */} + {!entry.is_directory && ( + onToggleSelect(entry)} + onClick={(e) => e.stopPropagation()} + sx={{ p: 0.25 }} + /> + )} + + {/* Icon */} + + {entry.is_directory ? ( + isExpanded ? ( + + ) : ( + + ) + ) : ( + getFileIcon(entry.file_type) + )} + + + {/* File name */} + + {entry.name} + + + {/* File type chip for event files */} + {entry.file_type === 'event' && ( + + )} + + {/* File size */} + {!entry.is_directory && entry.size_display && ( + + {entry.size_display} + + )} + + + {/* Children */} + {entry.is_directory && entry.children && ( + + {entry.children.map((child) => ( + + ))} + + )} + + ); +}; + +const FileBrowserDialog: React.FC = ({ + open, + onClose, + mission, + obsid, + obsTime, + targetName, + obsData, + onDownloadComplete, +}) => { + const { addNotification } = useUIStore(); + + // Loading and data state + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [files, setFiles] = useState([]); + const [baseUrl, setBaseUrl] = useState(''); + + // Selection state + const [selectedFiles, setSelectedFiles] = useState>(new Set()); + const [expandedDirs, setExpandedDirs] = useState>(new Set()); + + // Download state + const [downloadState, setDownloadState] = useState({ + status: 'idle', + message: '', + percent: 0, + }); + + // Load files when dialog opens + useEffect(() => { + if (open) { + loadFiles(); + } + }, [open, mission, obsid, obsTime, obsData]); + + const loadFiles = async (): Promise => { + setLoading(true); + setError(null); + setSelectedFiles(new Set()); + setExpandedDirs(new Set()); + setDownloadState({ status: 'idle', message: '', percent: 0 }); + + try { + const response = await archiveApi.listObservationFiles({ + mission, + obsid, + obs_time: obsTime, + obs_data: obsData, + recursive: true, + max_depth: 3, + }); + + if (response.success && response.data) { + setFiles(response.data.files); + setBaseUrl(response.data.base_url); + + // Auto-expand directories with event files and auto-select first event file + const dirsToExpand = new Set(); + let firstEventFileUrl: string | null = null; + + const findEventFiles = (entries: FileEntry[], parentUrl: string): void => { + for (const entry of entries) { + if (entry.is_directory && entry.children) { + const hasEventFile = entry.children.some( + (c) => c.file_type === 'event' || c.is_directory + ); + if (hasEventFile) { + dirsToExpand.add(entry.full_url); + } + findEventFiles(entry.children, entry.full_url); + } else if (entry.file_type === 'event' && !firstEventFileUrl) { + firstEventFileUrl = entry.full_url; + dirsToExpand.add(parentUrl); + } + } + }; + + findEventFiles(response.data.files, response.data.base_url); + setExpandedDirs(dirsToExpand); + + if (firstEventFileUrl) { + setSelectedFiles(new Set([firstEventFileUrl])); + } + } else { + setError(response.message || 'Failed to list files'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to list files'); + } finally { + setLoading(false); + } + }; + + const handleToggleSelect = useCallback((entry: FileEntry): void => { + setSelectedFiles((prev) => { + const next = new Set(prev); + if (next.has(entry.full_url)) { + next.delete(entry.full_url); + } else { + next.add(entry.full_url); + } + return next; + }); + }, []); + + const handleToggleExpand = useCallback((path: string): void => { + setExpandedDirs((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }, []); + + // Get selected file info + const getSelectedFileInfo = (): { url: string; name: string; size: number } | null => { + const findFile = (entries: FileEntry[]): FileEntry | null => { + for (const entry of entries) { + if (selectedFiles.has(entry.full_url) && !entry.is_directory) { + return entry; + } + if (entry.is_directory && entry.children) { + const found = findFile(entry.children); + if (found) return found; + } + } + return null; + }; + + const file = findFile(files); + if (file) { + return { + url: file.full_url, + name: file.name, + size: file.size_bytes || 0, + }; + } + return null; + }; + + // Calculate selected files info for display + const getSelectedFilesInfo = (): { count: number; totalSize: number; hasEventFile: boolean } => { + let count = 0; + let totalSize = 0; + let hasEventFile = false; + + const checkFiles = (entries: FileEntry[]): void => { + for (const entry of entries) { + if (entry.is_directory && entry.children) { + checkFiles(entry.children); + } else if (selectedFiles.has(entry.full_url)) { + count++; + if (entry.size_bytes) totalSize += entry.size_bytes; + if (entry.file_type === 'event') hasEventFile = true; + } + } + }; + + checkFiles(files); + return { count, totalSize, hasEventFile }; + }; + + const selectedInfo = getSelectedFilesInfo(); + + const formatTotalSize = (bytes: number): string => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; + }; + + // Handle download to local disk via backend (bypasses CORS) + const handleDownload = async (): Promise => { + const fileInfo = getSelectedFileInfo(); + if (!fileInfo) { + addNotification({ + type: 'warning', + title: 'No File Selected', + message: 'Please select a file to download', + }); + return; + } + + // Ask user where to save the file + const savePath = await window.electronAPI.saveFile({ + title: 'Save Downloaded File', + defaultPath: fileInfo.name, + filters: [ + { name: 'FITS Files', extensions: ['fits', 'fits.gz', 'evt', 'evt.gz'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }); + + if (!savePath) { + return; // User cancelled + } + + setDownloadState({ + status: 'downloading', + message: 'Starting download...', + percent: 0, + }); + + try { + // Download via backend to bypass CORS restrictions + // The backend downloads from HEASARC and saves directly to disk + for await (const event of archiveApi.downloadToDiskSSE({ + url: fileInfo.url, + save_path: savePath, + })) { + if (event.type === 'progress') { + const { bytes_downloaded, total_bytes, percent } = event; + if (total_bytes > 0) { + setDownloadState({ + status: 'downloading', + message: `Downloading: ${percent.toFixed(1)}% (${formatTotalSize(bytes_downloaded)} / ${formatTotalSize(total_bytes)})`, + percent, + }); + } else { + setDownloadState({ + status: 'downloading', + message: `Downloading: ${formatTotalSize(bytes_downloaded)}`, + percent: 0, + }); + } + } else if (event.type === 'complete') { + setDownloadState({ + status: 'complete', + message: `Downloaded to: ${event.file_path}`, + percent: 100, + filePath: event.file_path, + }); + + addNotification({ + type: 'success', + title: 'Download Complete', + message: `File saved to ${event.file_path}. Use "Load from Local" to load it.`, + }); + + onDownloadComplete?.(event.file_path); + } else if (event.type === 'error') { + throw new Error(event.error); + } + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Download failed'; + setDownloadState({ + status: 'error', + message: errorMsg, + percent: 0, + error: errorMsg, + }); + addNotification({ + type: 'error', + title: 'Download Failed', + message: errorMsg, + }); + } + }; + + // Open file location in system file manager + const handleShowInFolder = (): void => { + if (downloadState.filePath) { + window.electronAPI.showItemInFolder(downloadState.filePath); + } + }; + + const isDownloading = downloadState.status === 'downloading'; + + return ( + + + + + + + Browse Files: {mission} Observation {obsid} + + + Target: {targetName} + + + + + + + {loading ? ( + + + + Loading file list... + + + ) : error ? ( + + {error} + + ) : ( + <> + {/* Base URL display */} + + {baseUrl} + + + {/* File tree */} + + {files.length === 0 ? ( + + No files found in this observation directory. + + ) : ( + files.map((entry) => ( + + )) + )} + + + {/* Selection info */} + + + Selected: {selectedInfo.count} file{selectedInfo.count !== 1 ? 's' : ''} + {selectedInfo.totalSize > 0 && ` (${formatTotalSize(selectedInfo.totalSize)})`} + + {selectedInfo.hasEventFile && ( + } + label="Event file" + size="small" + color="warning" + /> + )} + + + {/* Download progress/status */} + {downloadState.status !== 'idle' && ( + + {downloadState.status === 'error' ? ( + {downloadState.message} + ) : downloadState.status === 'complete' ? ( + + Show in Folder + + } + > + {downloadState.message} + + ) : ( + <> + + + {downloadState.message} + + 0 ? 'determinate' : 'indeterminate'} + value={downloadState.percent} + sx={{ height: 8, borderRadius: 1 }} + /> + + )} + + )} + + {/* Hint */} + + Select a file and click "Download" to save it locally. + Then use the "Load from Local" tab to load it into the application. + + + )} + + + + + + + + ); +}; + +export default FileBrowserDialog; diff --git a/src/pages/DataIngestion/HeasarcBrowserPanel.tsx b/src/pages/DataIngestion/HeasarcBrowserPanel.tsx new file mode 100644 index 0000000..1e376d9 --- /dev/null +++ b/src/pages/DataIngestion/HeasarcBrowserPanel.tsx @@ -0,0 +1,1102 @@ +/** + * HEASARC Browser Panel + * + * Allows users to search and download X-ray observation data + * from NASA's HEASARC archive. + */ + +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + Button, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, + ToggleButton, + ToggleButtonGroup, + Tooltip, + IconButton, + Collapse, +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import PublicIcon from '@mui/icons-material/Public'; +import MyLocationIcon from '@mui/icons-material/MyLocation'; +import TextFieldsIcon from '@mui/icons-material/TextFields'; +import TagIcon from '@mui/icons-material/Tag'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import { + archiveApi, + HeasarcCatalog, + HeasarcObservation, +} from '@/api/archiveApi'; +import { useUIStore } from '@/store/uiStore'; +import FileBrowserDialog from './FileBrowserDialog'; + +interface HeasarcBrowserPanelProps { + onDataLoaded?: () => void; +} + +type SearchMode = 'name' | 'coordinates' | 'obsid'; +type SortOrder = 'none' | 'asc' | 'desc'; + +interface ObsData { + ra?: number | null; + dec?: number | null; + prnb?: string; +} + +interface FileBrowserState { + open: boolean; + mission: string; + obsid: string; + obsTime: string; + targetName: string; + obsData?: ObsData; +} + +const HeasarcBrowserPanel: React.FC = ({ onDataLoaded }) => { + const { addNotification } = useUIStore(); + + // Catalogs state + const [catalogs, setCatalogs] = useState([]); + const [loadingCatalogs, setLoadingCatalogs] = useState(true); + + // Search form state + const [searchMode, setSearchMode] = useState('name'); + const [selectedMission, setSelectedMission] = useState('NICER'); + const [sourceName, setSourceName] = useState(''); + const [ra, setRa] = useState(''); + const [dec, setDec] = useState(''); + const [searchRadius, setSearchRadius] = useState(0.5); + const [obsidInput, setObsidInput] = useState(''); + const [maxResults, setMaxResults] = useState(100); + + // Filter state + const [showFilters, setShowFilters] = useState(false); + const [minExposure, setMinExposure] = useState(''); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + // Search results state + const [isSearching, setIsSearching] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [searchMessage, setSearchMessage] = useState(''); + + // File browser dialog state + const [fileBrowser, setFileBrowser] = useState({ + open: false, + mission: '', + obsid: '', + obsTime: '', + targetName: '', + }); + + // Sort state for Date/Time column + const [dateSortOrder, setDateSortOrder] = useState('none'); + + // Mission-specific display flags + const isNicer = selectedMission === 'NICER'; + const isNuSTAR = selectedMission === 'NuSTAR'; + const isXMM = selectedMission === 'XMM-Newton'; + const isChandra = selectedMission === 'Chandra'; + + // XMM-Newton: per-instrument data only available via ADQL (ObsID search) + // Show Instruments column only when at least one result has the data + const hasXmmInstrumentData = isXMM && searchResults.some((obs) => obs.pn_time != null); + + // Chandra: show Detector column only when at least one result has detector info + const hasDetectorData = isChandra && searchResults.some((obs) => obs.detector); + + // Fetch supported catalogs on mount + useEffect(() => { + const fetchCatalogs = async (): Promise => { + try { + const response = await archiveApi.getCatalogs(); + if (response.success && response.data) { + setCatalogs(response.data.catalogs); + } + } catch (error) { + console.error('Failed to fetch catalogs:', error); + addNotification({ + type: 'error', + title: 'Error', + message: 'Failed to fetch HEASARC catalogs', + }); + } finally { + setLoadingCatalogs(false); + } + }; + fetchCatalogs(); + }, [addNotification]); + + /** Build filter params for name/coordinate searches */ + const getFilterParams = (): { + min_exposure?: number; + start_date?: string; + end_date?: string; + } => { + const params: { min_exposure?: number; start_date?: string; end_date?: string } = {}; + const minExpVal = parseFloat(minExposure); + if (!isNaN(minExpVal) && minExpVal > 0) { + params.min_exposure = minExpVal; + } + if (startDate) { + params.start_date = startDate; + } + if (endDate) { + params.end_date = endDate; + } + return params; + }; + + // Handle search + const handleSearch = async (): Promise => { + setIsSearching(true); + setSearchResults([]); + setSearchMessage(''); + setDateSortOrder('none'); // Reset sort order on new search + + try { + if (searchMode === 'obsid') { + // ObsID search + if (!obsidInput.trim()) { + addNotification({ + type: 'warning', + title: 'Missing Input', + message: 'Please enter an Observation ID', + }); + setIsSearching(false); + return; + } + + const response = await archiveApi.searchByObsid({ + obsid: obsidInput.trim(), + mission: selectedMission, + }); + + if (response.success && response.data) { + setSearchResults(response.data.observations); + setSearchMessage(response.message); + } else { + addNotification({ + type: 'error', + title: 'Search Failed', + message: response.message || 'Search failed', + }); + } + } else if (searchMode === 'name') { + if (!sourceName.trim()) { + addNotification({ + type: 'warning', + title: 'Missing Input', + message: 'Please enter a source name', + }); + setIsSearching(false); + return; + } + + const response = await archiveApi.searchByName({ + source_name: sourceName.trim(), + mission: selectedMission, + radius: searchRadius, + max_results: maxResults, + ...getFilterParams(), + }); + + if (response.success && response.data) { + setSearchResults(response.data.observations); + setSearchMessage(response.message); + if (response.data.resolved_ra !== undefined && response.data.resolved_dec !== undefined) { + addNotification({ + type: 'info', + title: 'Source Resolved', + message: `Source resolved to: RA = ${response.data.resolved_ra.toFixed(4)}, Dec = ${response.data.resolved_dec.toFixed(4)}`, + }); + } + } else { + addNotification({ + type: 'error', + title: 'Search Failed', + message: response.message || 'Search failed', + }); + } + } else { + // Coordinate search + const raNum = parseFloat(ra); + const decNum = parseFloat(dec); + + if (isNaN(raNum) || isNaN(decNum)) { + addNotification({ + type: 'warning', + title: 'Invalid Coordinates', + message: 'Please enter valid RA and Dec values in degrees', + }); + setIsSearching(false); + return; + } + + const response = await archiveApi.searchByCoordinates({ + ra: raNum, + dec: decNum, + mission: selectedMission, + radius: searchRadius, + max_results: maxResults, + ...getFilterParams(), + }); + + if (response.success && response.data) { + setSearchResults(response.data.observations); + setSearchMessage(response.message); + } else { + addNotification({ + type: 'error', + title: 'Search Failed', + message: response.message || 'Search failed', + }); + } + } + } catch (error) { + console.error('Search error:', error); + addNotification({ + type: 'error', + title: 'Error', + message: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + }); + } finally { + setIsSearching(false); + } + }; + + // Handle opening the file browser for an observation + const handleBrowseFiles = (obs: HeasarcObservation): void => { + setFileBrowser({ + open: true, + mission: selectedMission, + obsid: obs.obsid, + obsTime: obs.time, + targetName: obs.name, + obsData: { + ra: obs.ra, + dec: obs.dec, + prnb: obs.prnb, + }, + }); + }; + + // Handle closing the file browser + const handleCloseFileBrowser = (): void => { + setFileBrowser((prev) => ({ ...prev, open: false })); + }; + + // Handle download complete - file was saved to disk + const handleDownloadComplete = (filePath: string): void => { + console.log('File downloaded to:', filePath); + // Could optionally switch to the Local tab or pre-fill the file path + onDataLoaded?.(); + }; + + // Handle fallback to browse page + const handleBrowseFallback = async (obs: HeasarcObservation): Promise => { + const response = await archiveApi.getObservationUrls(selectedMission, obs.obsid); + if (response.success && response.data?.urls?.browse) { + window.open(response.data.urls.browse, '_blank'); + addNotification({ + type: 'info', + title: 'HEASARC Browser', + message: `Opening HEASARC browse page for ${obs.obsid}. Download the event file manually.`, + }); + } + }; + + // Format exposure time + const formatExposure = (seconds: number | null): string => { + if (seconds === null || seconds === undefined) return 'N/A'; + if (seconds < 60) return `${seconds.toFixed(0)}s`; + if (seconds < 3600) return `${(seconds / 60).toFixed(1)} min`; + return `${(seconds / 3600).toFixed(2)} hr`; + }; + + /** + * Format exposure for display, with Swift instrument awareness. + * For Swift observations where XRT exposure is 0 but BAT has data, + * shows BAT exposure with an instrument label. + */ + const formatObservationExposure = (obs: HeasarcObservation): { label: string; instrument: string | null } => { + // For Swift, check instrument-specific exposures + if (selectedMission === 'Swift' && obs.xrt_exposure !== undefined) { + const xrt = obs.xrt_exposure ?? 0; + const bat = obs.bat_exposure ?? 0; + const uvot = obs.uvot_exposure ?? 0; + + if (xrt > 0) { + return { label: formatExposure(xrt), instrument: 'XRT' }; + } + if (bat > 0) { + return { label: formatExposure(bat), instrument: 'BAT' }; + } + if (uvot > 0) { + return { label: formatExposure(uvot), instrument: 'UVOT' }; + } + return { label: '0s', instrument: null }; + } + + // For NuSTAR: only show FPMA label when FPMB data is also available (ADQL/ObsID search) + // query_region() doesn't return exposure_b, so don't mislead with "FPMA" label + if (selectedMission === 'NuSTAR') { + const instrument = obs.exposure_b != null ? 'FPMA' : null; + return { label: formatExposure(obs.exposure), instrument }; + } + + // For IXPE, show main exposure (per-DU breakdown available in tooltip) + if (selectedMission === 'IXPE' && obs.exposure_du1 !== undefined) { + return { label: formatExposure(obs.exposure), instrument: null }; + } + + // For Chandra, show exposure with detector context + if (selectedMission === 'Chandra') { + return { label: formatExposure(obs.exposure), instrument: null }; + } + + // For XMM-Newton, show duration as main exposure + if (selectedMission === 'XMM-Newton') { + return { label: formatExposure(obs.exposure), instrument: null }; + } + + return { label: formatExposure(obs.exposure), instrument: null }; + }; + + // Format coordinates + const formatCoord = (value: number | null, decimals: number = 4): string => { + if (value === null || value === undefined) return 'N/A'; + return value.toFixed(decimals); + }; + + /** + * Convert Modified Julian Date (MJD) to human-readable date+time string. + * MJD is days since midnight on November 17, 1858. + * The fractional part represents the time of day. + */ + const formatMjdToDateTime = (mjdString: string): string => { + if (!mjdString || mjdString === 'N/A') return 'N/A'; + + try { + const mjd = parseFloat(mjdString); + if (isNaN(mjd)) return 'N/A'; + + // MJD epoch: November 17, 1858 00:00:00 UTC + // Convert MJD to JavaScript Date + // JD = MJD + 2400000.5 + // Unix epoch (Jan 1, 1970) = JD 2440587.5 + // So: Unix days = MJD - 40587 + const unixDays = mjd - 40587; + const unixMs = unixDays * 24 * 60 * 60 * 1000; + const date = new Date(unixMs); + + // Format as DD-MM-YYYY HH:MM:SS + const day = date.getUTCDate().toString().padStart(2, '0'); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const year = date.getUTCFullYear(); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + + return `${day}-${month}-${year} ${hours}:${minutes}:${seconds}`; + } catch { + return 'N/A'; + } + }; + + // Handle Date/Time column header click for sorting + const handleDateSortClick = (): void => { + setDateSortOrder((prev) => { + if (prev === 'none') return 'asc'; + if (prev === 'asc') return 'desc'; + return 'none'; + }); + }; + + // Get sorted results based on current sort order + const getSortedResults = (): HeasarcObservation[] => { + if (dateSortOrder === 'none') { + return searchResults; // Original order from API + } + + return [...searchResults].sort((a, b) => { + const mjdA = parseFloat(a.time) || 0; + const mjdB = parseFloat(b.time) || 0; + + if (dateSortOrder === 'asc') { + return mjdA - mjdB; // Oldest first + } else { + return mjdB - mjdA; // Newest first + } + }); + }; + + /** Get processing status color for NICER Chip */ + const getStatusColor = (status: string | undefined): 'success' | 'warning' | 'default' => { + if (!status) return 'default'; + const upper = status.toUpperCase(); + if (upper === 'VALIDATED') return 'success'; + if (upper === 'PROCESSED') return 'warning'; + return 'default'; + }; + + /** Get processing status tooltip description */ + const getStatusTooltip = (status: string | undefined): string => { + if (!status) return ''; + const upper = status.toUpperCase(); + if (upper === 'VALIDATED') return 'Data is fully processed, quality-checked, and available in the archive'; + if (upper === 'PROCESSED') return 'Data has been processed but not yet validated by the NICER team'; + if (upper === 'NOTPROCESSED') return 'Data has not been processed yet'; + return status; + }; + + /** Get XMM-Newton status display label (capitalize raw HEASARC value) */ + const getXmmStatusLabel = (status: string | undefined): string => { + if (!status) return ''; + const upper = status.toUpperCase(); + if (upper === 'ARCHIVED') return 'Archived'; + if (upper === 'SCHEDULED') return 'Scheduled'; + return status; + }; + + /** Get XMM-Newton status chip color */ + const getXmmStatusColor = (status: string | undefined): 'success' | 'warning' | 'default' => { + if (!status) return 'default'; + const upper = status.toUpperCase(); + if (upper === 'ARCHIVED') return 'success'; + if (upper === 'SCHEDULED') return 'warning'; + return 'default'; + }; + + /** Get XMM-Newton status tooltip description */ + const getXmmStatusTooltip = (status: string | undefined, dataInHeasarc: string | undefined): string => { + if (!status) return ''; + const upper = status.toUpperCase(); + const dataAvail = dataInHeasarc === 'Y' + ? 'Data files available in HEASARC' + : dataInHeasarc === 'N' + ? 'Data files not yet available in HEASARC' + : ''; + if (upper === 'ARCHIVED') return `Observation completed and archived. ${dataAvail}`; + if (upper === 'SCHEDULED') return `Observation scheduled but not yet executed. ${dataAvail}`; + return `${status}. ${dataAvail}`; + }; + + /** Get Chandra status display label */ + const getChandraStatusLabel = (status: string | undefined): string => { + if (!status) return ''; + const upper = status.toUpperCase(); + if (upper === 'ARCHIVED') return 'Archived'; + if (upper === 'OBSERVED') return 'Observed'; + if (upper === 'SCHEDULED') return 'Scheduled'; + return status; + }; + + /** Get Chandra status chip color */ + const getChandraStatusColor = (status: string | undefined): 'success' | 'primary' | 'default' => { + if (!status) return 'default'; + const upper = status.toUpperCase(); + if (upper === 'ARCHIVED') return 'success'; + if (upper === 'OBSERVED') return 'primary'; + return 'default'; + }; + + /** Get Chandra status tooltip description */ + const getChandraStatusTooltip = (status: string | undefined): string => { + if (!status) return ''; + const upper = status.toUpperCase(); + if (upper === 'ARCHIVED') return 'Observation data processed and available for download'; + if (upper === 'OBSERVED') return 'Observation completed, data processing in progress'; + if (upper === 'SCHEDULED') return 'Observation scheduled but not yet executed'; + return status; + }; + + /** Format Chandra detector + grating display */ + const formatChandraDetector = (obs: HeasarcObservation): { label: string; tooltip: string } => { + const detector = obs.detector || ''; + const grating = obs.grating || ''; + if (!detector) return { label: '', tooltip: '' }; + + const hasGrating = grating && grating.toUpperCase() !== 'NONE'; + const label = hasGrating ? `${detector} / ${grating}` : detector; + + let tooltip = detector; + if (detector.toUpperCase() === 'ACIS-S') { + tooltip = hasGrating + ? `ACIS-S with ${grating} grating — spectroscopy mode` + : 'ACIS-S — CCD array, supports CC mode (2.85ms timing)'; + } else if (detector.toUpperCase() === 'ACIS-I') { + tooltip = hasGrating + ? `ACIS-I with ${grating} grating` + : 'ACIS-I — imaging array, 3.3s standard frame time'; + } else if (detector.toUpperCase() === 'HRC-I') { + tooltip = 'HRC-I — microchannel plate, 16\u03BCs time resolution'; + } else if (detector.toUpperCase() === 'HRC-S') { + tooltip = hasGrating + ? `HRC-S with ${grating} grating — high-resolution spectroscopy` + : 'HRC-S — microchannel plate, LETG readout optimized'; + } + + return { label, tooltip }; + }; + + if (loadingCatalogs) { + return ( + + + + ); + } + + return ( + + {/* Header */} + + + Browse HEASARC Archive + + + + Search NASA's HEASARC archive for X-ray observations by source name, coordinates, or ObsID + + + {/* Mission Selector */} + + Mission + + + + {/* Search Mode Toggle */} + + + Search by: + + value && setSearchMode(value)} + size="small" + fullWidth + > + + + Source Name + + + + Coordinates + + + + ObsID + + + + + {/* Search Inputs */} + {searchMode === 'name' && ( + setSourceName(e.target.value)} + fullWidth + size="small" + sx={{ mb: 2 }} + placeholder="e.g., Crab, Cyg X-1, NGC 3783, GRS 1915+105" + helperText="Enter an astronomical source name (resolved via SIMBAD/NED)" + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> + )} + + {searchMode === 'coordinates' && ( + + setRa(e.target.value)} + size="small" + sx={{ flex: 1 }} + placeholder="e.g., 83.6287" + helperText="Right Ascension" + type="number" + inputProps={{ step: 0.0001 }} + /> + setDec(e.target.value)} + size="small" + sx={{ flex: 1 }} + placeholder="e.g., 22.0145" + helperText="Declination" + type="number" + inputProps={{ step: 0.0001 }} + /> + + )} + + {searchMode === 'obsid' && ( + setObsidInput(e.target.value)} + fullWidth + size="small" + sx={{ mb: 2 }} + placeholder="e.g., 4010080142" + helperText="Enter an exact Observation ID to look up" + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> + )} + + {/* Search Radius + Max Results (hidden for ObsID mode) */} + {searchMode !== 'obsid' && ( + + setSearchRadius(parseFloat(e.target.value) || 0.5)} + size="small" + sx={{ width: 200 }} + type="number" + inputProps={{ step: 0.1, min: 0.01, max: 10 }} + /> + { + const val = parseInt(e.target.value, 10); + if (!isNaN(val) && val > 0) setMaxResults(val); + }} + size="small" + sx={{ width: 140 }} + type="number" + inputProps={{ min: 1, step: 50 }} + /> + + )} + + {/* Filters (shown for name/coordinates modes) */} + {searchMode !== 'obsid' && ( + + + + + + setMinExposure(e.target.value)} + size="small" + sx={{ width: 160 }} + type="number" + inputProps={{ min: 0, step: 100 }} + placeholder="e.g., 1000" + /> + setStartDate(e.target.value)} + size="small" + sx={{ width: 180 }} + type="date" + InputLabelProps={{ shrink: true }} + /> + setEndDate(e.target.value)} + size="small" + sx={{ width: 180 }} + type="date" + InputLabelProps={{ shrink: true }} + /> + {(minExposure || startDate || endDate) && ( + + )} + + + + + )} + + {/* Search Button */} + + + {/* Search Results */} + {searchMessage && ( + + {searchMessage} + + )} + + {searchResults.length > 0 && ( + + + + + ObsID + Target + RA + Dec + Exposure + + + Date/Time (UTC) + {dateSortOrder === 'asc' && } + {dateSortOrder === 'desc' && } + + + MJD + {isNicer && Status} + {isNuSTAR && Mode} + {isXMM && Status} + {hasXmmInstrumentData && Instruments} + {hasDetectorData && Detector} + {isChandra && Status} + Action + + + + {getSortedResults().map((obs) => ( + + + + + {obs.obsid} + + {isNuSTAR && obs.issue_flag === 1 && ( + + + + )} + + + + + + {obs.name} + + + + + + {formatCoord(obs.ra)} + + + + + {formatCoord(obs.dec)} + + + + {(() => { + const { label, instrument } = formatObservationExposure(obs); + const effectiveExposure = instrument === 'BAT' + ? obs.bat_exposure ?? 0 + : instrument === 'UVOT' + ? obs.uvot_exposure ?? 0 + : obs.exposure ?? 0; + return ( + + 10000 + ? 'success' + : effectiveExposure > 1000 + ? 'primary' + : 'default' + } + /> + + ); + })()} + + + + {formatMjdToDateTime(obs.time)} + + + + + {obs.time || 'N/A'} + + + {isNicer && ( + + {obs.processing_status ? ( + + + + ) : ( + + — + + )} + + )} + {isNuSTAR && ( + + {obs.observation_mode ? ( + + + + ) : ( + + — + + )} + + )} + {isXMM && ( + + + {obs.xmm_status ? ( + + + + ) : ( + + )} + {obs.data_in_heasarc === 'N' && ( + + + + )} + + + )} + {hasXmmInstrumentData && ( + + + {(obs.pn_time ?? 0) > 0 && ( + + + + )} + {(obs.mos1_time ?? 0) > 0 && ( + + + + )} + {(obs.mos2_time ?? 0) > 0 && ( + + + + )} + {(obs.pn_time ?? 0) === 0 && (obs.mos1_time ?? 0) === 0 && (obs.mos2_time ?? 0) === 0 && ( + + )} + + + )} + {hasDetectorData && ( + + {obs.detector ? ( + + + + ) : ( + + )} + + )} + {isChandra && ( + + {obs.chandra_status ? ( + + + + ) : ( + + )} + + )} + + + + handleBrowseFiles(obs)} + > + + + + + handleBrowseFallback(obs)} + > + + + + + + + ))} + +
+
+ )} + + {/* File Browser Dialog */} + + + {/* Help Text */} + + + How to use: + + +
    +
  1. Select a mission (e.g., NICER, NuSTAR)
  2. +
  3. Enter a source name, coordinates, or ObsID
  4. +
  5. Click Search to find observations
  6. +
  7. Click the folder icon to browse available files
  8. +
  9. Select an event file and click Download & Load
  10. +
+
+ + The file browser shows all available files in the observation directory. + Event files are marked with a star icon. + Use "Show Filters" to filter by minimum exposure or date range. + +
+
+ ); +}; + +export default HeasarcBrowserPanel; diff --git a/src/pages/DataIngestion/index.tsx b/src/pages/DataIngestion/index.tsx new file mode 100644 index 0000000..902af9d --- /dev/null +++ b/src/pages/DataIngestion/index.tsx @@ -0,0 +1,3109 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, + Typography, + Paper, + Button, + Grid, + Card, + CardContent, + TextField, + Select, + MenuItem, + FormControl, + InputLabel, + Alert, + CircularProgress, + List, + ListItem, + ListItemText, + ListItemSecondaryAction, + IconButton, + Chip, + Divider, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Collapse, + LinearProgress, + FormControlLabel, + Checkbox, + Tabs, + Tab, + Radio, + RadioGroup, +} from '@mui/material'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import DeleteIcon from '@mui/icons-material/Delete'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import InfoIcon from '@mui/icons-material/Info'; +import CloseIcon from '@mui/icons-material/Close'; +import LinkIcon from '@mui/icons-material/Link'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import SettingsIcon from '@mui/icons-material/Settings'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import SaveIcon from '@mui/icons-material/Save'; +import ClearAllIcon from '@mui/icons-material/ClearAll'; +import HistoryIcon from '@mui/icons-material/History'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import MemoryIcon from '@mui/icons-material/Memory'; +import PrecisionManufacturingIcon from '@mui/icons-material/PrecisionManufacturing'; +import BoltIcon from '@mui/icons-material/Bolt'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import NumbersIcon from '@mui/icons-material/Numbers'; +import QueryStatsIcon from '@mui/icons-material/QueryStats'; +import PublicIcon from '@mui/icons-material/Public'; +import { + dataApi, + EventListSummary, + EventListInfo, + FileSizeInfo, + EventListFullPreview, + FileMetadata, + SingleFileConfig, + BatchSizeResult, + ValidationIssue, +} from '@/api/dataApi'; +import { jobApi } from '@/api/jobApi'; +import type { BatchFileConfig } from '@/types/job'; +import HeasarcBrowserPanel from './HeasarcBrowserPanel'; +import { apiClient } from '@/api/client'; +import { useUIStore } from '@/store/uiStore'; +import { useJobStore } from '@/store/jobStore'; + +type AlertSeverity = 'success' | 'error' | 'warning' | 'info'; + +interface AlertState { + open: boolean; + message: string; + severity: AlertSeverity; +} + +// localStorage persistence for last loaded files +const LAST_LOADED_FILES_KEY = 'lastLoadedFiles'; + +interface LastLoadedFilesData { + files: string[]; + fileNames: Record; + timestamp: number; +} + +const saveLastLoadedFiles = (files: string[], names: Record): void => { + const data: LastLoadedFilesData = { + files, + fileNames: names, + timestamp: Date.now(), + }; + localStorage.setItem(LAST_LOADED_FILES_KEY, JSON.stringify(data)); +}; + +const getLastLoadedFiles = (): LastLoadedFilesData | null => { + const saved = localStorage.getItem(LAST_LOADED_FILES_KEY); + if (!saved) return null; + try { + return JSON.parse(saved) as LastLoadedFilesData; + } catch { + return null; + } +}; + +const DataIngestionPage: React.FC = () => { + // Global notification store + const { addNotification } = useUIStore(); + + // Job store for watching job completions and refreshing data + const { jobs } = useJobStore(); + + // Form state - Local File (batch mode) + const [selectedFiles, setSelectedFiles] = useState([]); + const [fileNames, setFileNames] = useState>({}); + const [fileFormat, setFileFormat] = useState('ogip'); + const [isLoading, setIsLoading] = useState(false); + + // Batch file settings mode + const [useSameSettings, setUseSameSettings] = useState(true); + const [perFileConfigs, setPerFileConfigs] = useState>>({}); + const [expandedFileSettings, setExpandedFileSettings] = useState>({}); + + // Batch size info + const [batchSizeInfo, setBatchSizeInfo] = useState(null); + const [isCheckingBatchSize, setIsCheckingBatchSize] = useState(false); + + // Batch loading progress - kept for potential future use but not used with job queue + const [_batchProgress, _setBatchProgress] = useState<{ + loading: boolean; + total: number; + completed: number; + } | null>(null); + // Note: batchResult removed - job queue handles results in sidebar + + // Advanced options state + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + const [rmfFile, setRmfFile] = useState(''); + const [additionalColumns, setAdditionalColumns] = useState(''); + const [fileSizeInfo, setFileSizeInfo] = useState(null); + const [isCheckingFileSize, setIsCheckingFileSize] = useState(false); + + // Notes/Comments for the data + const [eventNotes, setEventNotes] = useState(''); + + + // Advanced loading options + const [highPrecision, setHighPrecision] = useState(false); + const [skipChecks, setSkipChecks] = useState(false); + + // True lazy loading options + const [useTrueLazyLoading, setUseTrueLazyLoading] = useState(false); + const [trueLazyMode, setTrueLazyMode] = useState<'time_range' | 'event_count'>('time_range'); + const [timeRangeStart, setTimeRangeStart] = useState(0); + const [timeRangeEnd, setTimeRangeEnd] = useState(100); + const [eventCountStart, setEventCountStart] = useState(0); + const [eventCount, setEventCount] = useState(10000); + const [fileMetadata, setFileMetadata] = useState(null); + const [isLoadingMetadata, setIsLoadingMetadata] = useState(false); + + // Tab state for data input method + const [dataInputTab, setDataInputTab] = useState(0); + + // Form state - URL Loading + const [urlInput, setUrlInput] = useState(''); + const [urlEventListName, setUrlEventListName] = useState(''); + const [urlFormat, setUrlFormat] = useState('ogip'); + const [isLoadingUrl, setIsLoadingUrl] = useState(false); + // Note: urlDownloadProgress removed - job queue handles progress in sidebar + + // Loaded data state + const [loadedEventLists, setLoadedEventLists] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); + + // Alert state (local page alert) + const [alert, setAlert] = useState({ + open: false, + message: '', + severity: 'info', + }); + + // Details dialog state + const [detailsOpen, setDetailsOpen] = useState(false); + const [detailsLoading, setDetailsLoading] = useState(false); + const [selectedEventListDetails, setSelectedEventListDetails] = useState(null); + + // Full preview dialog state + const [fullPreviewOpen, setFullPreviewOpen] = useState(false); + const [fullPreviewLoading, setFullPreviewLoading] = useState(false); + const [fullPreviewData, setFullPreviewData] = useState(null); + const [previewTabValue, setPreviewTabValue] = useState(0); + + // Save format dialog state + const [saveFormatDialogOpen, setSaveFormatDialogOpen] = useState(false); + const [saveEventListName, setSaveEventListName] = useState(''); + const [selectedSaveFormat, setSelectedSaveFormat] = useState('hdf5'); + + // Last loaded files state (for restore functionality) + const [hasLastLoadedFiles, setHasLastLoadedFiles] = useState(false); + + // Fetch loaded event lists on mount and after operations + const fetchEventLists = useCallback(async (): Promise => { + setIsRefreshing(true); + try { + // Ensure we have the correct port + await apiClient.getPort(); + const response = await dataApi.listEventLists(); + if (response.success && response.data) { + setLoadedEventLists(response.data); + } + } catch (error) { + console.error('Failed to fetch event lists:', error); + } finally { + setIsRefreshing(false); + } + }, []); + + useEffect(() => { + fetchEventLists(); + }, [fetchEventLists]); + + // Refresh event list when jobs complete + useEffect(() => { + // Count completed load jobs + const completedLoadJobs = Object.values(jobs).filter( + (job) => + job.status === 'completed' && + (job.type === 'load_event_list' || job.type === 'load_batch' || job.type === 'load_from_url') + ); + + if (completedLoadJobs.length > 0) { + // Refresh the event list to show newly loaded data + fetchEventLists(); + } + }, [jobs, fetchEventLists]); + + // Check for last loaded files on mount + useEffect(() => { + const lastLoaded = getLastLoadedFiles(); + setHasLastLoadedFiles(lastLoaded !== null && lastLoaded.files.length > 0); + }, []); + + // Show alert helper - sends all alerts to the global notification center + const showAlert = (message: string, severity: AlertSeverity, title?: string): void => { + // Map severity to notification type + const typeMap: Record = { + success: 'success', + error: 'error', + warning: 'warning', + info: 'info', + }; + const defaultTitles: Record = { + success: 'Success', + error: 'Error', + warning: 'Warning', + info: 'Info', + }; + + addNotification({ + type: typeMap[severity], + title: title || defaultTitles[severity], + message, + }); + + // Clear any existing local alert + setAlert({ open: false, message: '', severity: 'info' }); + }; + + // Auto-detect file format from extension + const detectFormatFromExtension = (filePath: string): string => { + const ext = filePath.toLowerCase().split('.').pop(); + if (ext === 'hdf5' || ext === 'h5') return 'hdf5'; + if (ext === 'ecsv') return 'ascii.ecsv'; + if (ext === 'pkl' || ext === 'pickle') return 'pickle'; + // Default to ogip for .fits, .evt, .fit, .fts, .gz, etc. + return 'ogip'; + }; + + // Check file size when file is selected (single file) + const checkFileSize = async (filePath: string): Promise => { + setIsCheckingFileSize(true); + try { + await apiClient.getPort(); + const response = await dataApi.checkFileSize(filePath); + if (response.success && response.data) { + setFileSizeInfo(response.data); + // Auto-enable lazy loading if recommended for large files + if (response.data.recommend_lazy && !useTrueLazyLoading) { + setUseTrueLazyLoading(true); + } + } + } catch (error) { + console.error('Failed to check file size:', error); + setFileSizeInfo(null); + } finally { + setIsCheckingFileSize(false); + } + }; + + // Check batch file sizes when multiple files are selected + const checkBatchFileSize = async (filePaths: string[]): Promise => { + if (filePaths.length === 0) return; + + setIsCheckingBatchSize(true); + try { + await apiClient.getPort(); + const response = await dataApi.checkBatchFileSize(filePaths); + if (response.success && response.data) { + setBatchSizeInfo(response.data); + // Auto-enable lazy loading if recommended + if (response.data.recommend_partial_loading && !useTrueLazyLoading) { + setUseTrueLazyLoading(true); + } + } + } catch (error) { + console.error('Failed to check batch file sizes:', error); + setBatchSizeInfo(null); + } finally { + setIsCheckingBatchSize(false); + } + }; + + // Fetch file metadata for true lazy loading (uses first selected file as reference) + const fetchFileMetadata = async (): Promise => { + if (selectedFiles.length === 0) { + showAlert('Please select a file first', 'warning'); + return; + } + + setIsLoadingMetadata(true); + try { + await apiClient.getPort(); + // Use first file for metadata preview + const response = await dataApi.getFileMetadata({ + file_path: selectedFiles[0], + fmt: fileFormat, + }); + if (response.success && response.data) { + setFileMetadata(response.data); + // Auto-populate time range with full file duration + if (response.data.time_range[0] !== null && response.data.time_range[1] !== null) { + setTimeRangeStart(0); + setTimeRangeEnd(Math.min(response.data.duration, 100)); + } + // Auto-populate event count with recommendation + if (response.data.recommended_loading.suggested_chunk_size) { + setEventCount(response.data.recommended_loading.suggested_chunk_size); + } + const message = selectedFiles.length > 1 + ? `First file has ${response.data.total_events.toLocaleString()} events over ${response.data.duration.toFixed(1)}s (settings will apply to all files)` + : `File has ${response.data.total_events.toLocaleString()} events over ${response.data.duration.toFixed(1)}s`; + showAlert(message, 'success', 'File Metadata'); + } else { + showAlert(response.message || 'Failed to fetch file metadata', 'error', 'Metadata Error'); + } + } catch (error) { + console.error('Failed to fetch file metadata:', error); + showAlert('Failed to fetch file metadata', 'error', 'Metadata Error'); + } finally { + setIsLoadingMetadata(false); + } + }; + + // Handle file selection via Electron dialog (supports multiple files) + const handleBrowseFiles = async (): Promise => { + if (!window.electronAPI) { + showAlert('File dialog not available (Electron API not found)', 'error'); + return; + } + + const files = await window.electronAPI.openFile({ + title: 'Select Event List Files', + filters: [ + { name: 'All Files', extensions: ['*'] }, + { name: 'FITS Files', extensions: ['fits', 'fit', 'fts', 'evt', 'fits.gz', 'fit.gz', 'fts.gz', 'evt.gz', 'gz'] }, + { name: 'HDF5 Files', extensions: ['hdf5', 'h5'] }, + { name: 'Text Files', extensions: ['txt', 'csv', 'dat', 'ecsv'] }, + ], + multiple: true, // Enable multi-select + }); + + if (files && files.length > 0) { + // Batch result cleared (job queue handles results) // Clear previous batch result + + // APPEND mode: merge with existing selection (filter duplicates) + const existingSet = new Set(selectedFiles); + const newFiles = files.filter((f) => !existingSet.has(f)); + const mergedFiles = [...selectedFiles, ...newFiles]; + setSelectedFiles(mergedFiles); + + // Auto-detect format from first new file's extension (only update if adding new files) + if (newFiles.length > 0 && selectedFiles.length === 0) { + const detectedFormat = detectFormatFromExtension(newFiles[0]); + setFileFormat(detectedFormat); + } + + // Merge names: keep existing names, generate for new files only + const mergedNames = { ...fileNames }; + newFiles.forEach((f) => { + const baseName = f.split('/').pop()?.split('.')[0] || 'event_list'; + // Ensure unique names by checking against all existing and new names + let uniqueName = baseName; + let counter = 1; + while (Object.values(mergedNames).includes(uniqueName)) { + uniqueName = `${baseName}_${counter}`; + counter++; + } + mergedNames[f] = uniqueName; + }); + setFileNames(mergedNames); + + // Merge per-file configs: keep existing, initialize new ones + const mergedConfigs = { ...perFileConfigs }; + newFiles.forEach((f) => { + const fileDetectedFormat = detectFormatFromExtension(f); + mergedConfigs[f] = { + fmt: fileDetectedFormat, + high_precision: false, + skip_checks: false, + use_partial_loading: false, + partial_mode: 'time_range', + time_range_start: 0, + time_range_end: 100, + event_start_index: 0, + event_count: 10000, + notes: '', + }; + }); + setPerFileConfigs(mergedConfigs); + + // Check batch file sizes for merged selection + if (mergedFiles.length > 1) { + checkBatchFileSize(mergedFiles); + setFileSizeInfo(null); // Clear single file info + } else if (mergedFiles.length === 1) { + // Single file - use existing single file check + checkFileSize(mergedFiles[0]); + setBatchSizeInfo(null); + } + } + }; + + // Remove a file from the selection + const handleRemoveFile = (filePath: string): void => { + const newFiles = selectedFiles.filter((f) => f !== filePath); + setSelectedFiles(newFiles); + + // Update file names + const newNames = { ...fileNames }; + delete newNames[filePath]; + setFileNames(newNames); + + // Update per-file configs + const newConfigs = { ...perFileConfigs }; + delete newConfigs[filePath]; + setPerFileConfigs(newConfigs); + + // Re-check sizes + if (newFiles.length > 1) { + checkBatchFileSize(newFiles); + setFileSizeInfo(null); + } else if (newFiles.length === 1) { + checkFileSize(newFiles[0]); + setBatchSizeInfo(null); + } else { + setFileSizeInfo(null); + setBatchSizeInfo(null); + } + }; + + // Clear all selected files + const handleClearSelection = (): void => { + setSelectedFiles([]); + setFileNames({}); + setPerFileConfigs({}); + setBatchSizeInfo(null); + setFileSizeInfo(null); + // Batch result cleared (job queue handles results) + setExpandedFileSettings({}); + }; + + // Restore last loaded files from localStorage + const handleRestoreLastFiles = async (): Promise => { + const lastLoaded = getLastLoadedFiles(); + if (!lastLoaded || lastLoaded.files.length === 0) { + showAlert('No previously loaded files found', 'info'); + return; + } + + if (!window.electronAPI) { + showAlert('Electron API not available', 'error'); + return; + } + + // Validate files still exist + const existingFiles: string[] = []; + const missingFiles: string[] = []; + + for (const filePath of lastLoaded.files) { + const exists = await window.electronAPI.fileExists(filePath); + if (exists) { + existingFiles.push(filePath); + } else { + missingFiles.push(filePath); + } + } + + if (existingFiles.length === 0) { + showAlert('None of the previously loaded files exist anymore', 'warning'); + return; + } + + // Batch result cleared (job queue handles results) + + // Merge with current selection (same logic as browse) + const existingSet = new Set(selectedFiles); + const newFiles = existingFiles.filter((f) => !existingSet.has(f)); + const mergedFiles = [...selectedFiles, ...newFiles]; + setSelectedFiles(mergedFiles); + + // Restore names from localStorage for existing files, generate for others + const mergedNames = { ...fileNames }; + newFiles.forEach((f) => { + if (lastLoaded.fileNames[f]) { + // Use saved name, but ensure uniqueness + let uniqueName = lastLoaded.fileNames[f]; + let counter = 1; + while (Object.values(mergedNames).includes(uniqueName)) { + uniqueName = `${lastLoaded.fileNames[f]}_${counter}`; + counter++; + } + mergedNames[f] = uniqueName; + } else { + const baseName = f.split('/').pop()?.split('.')[0] || 'event_list'; + let uniqueName = baseName; + let counter = 1; + while (Object.values(mergedNames).includes(uniqueName)) { + uniqueName = `${baseName}_${counter}`; + counter++; + } + mergedNames[f] = uniqueName; + } + }); + setFileNames(mergedNames); + + // Initialize per-file configs for new files + const mergedConfigs = { ...perFileConfigs }; + newFiles.forEach((f) => { + const fileDetectedFormat = detectFormatFromExtension(f); + mergedConfigs[f] = { + fmt: fileDetectedFormat, + high_precision: false, + skip_checks: false, + use_partial_loading: false, + partial_mode: 'time_range', + time_range_start: 0, + time_range_end: 100, + event_start_index: 0, + event_count: 10000, + notes: '', + }; + }); + setPerFileConfigs(mergedConfigs); + + // Auto-detect format if this is the first selection + if (selectedFiles.length === 0 && newFiles.length > 0) { + const detectedFormat = detectFormatFromExtension(newFiles[0]); + setFileFormat(detectedFormat); + } + + // Check batch file sizes for merged selection + if (mergedFiles.length > 1) { + checkBatchFileSize(mergedFiles); + setFileSizeInfo(null); + } else if (mergedFiles.length === 1) { + checkFileSize(mergedFiles[0]); + setBatchSizeInfo(null); + } + + // Show appropriate message + if (missingFiles.length > 0) { + showAlert( + `Selected ${existingFiles.length} files. ${missingFiles.length} file(s) no longer exist.`, + 'warning' + ); + } else { + showAlert(`Selected ${existingFiles.length} previously loaded file${existingFiles.length > 1 ? 's' : ''}`, 'success'); + } + }; + + // Update file name + const handleFileNameChange = (filePath: string, newName: string): void => { + setFileNames((prev) => ({ + ...prev, + [filePath]: newName, + })); + }; + + // Update per-file config + const handlePerFileConfigChange = ( + filePath: string, + updates: Partial + ): void => { + setPerFileConfigs((prev) => ({ + ...prev, + [filePath]: { + ...prev[filePath], + ...updates, + }, + })); + }; + + // Toggle per-file settings expansion + const toggleFileSettings = (filePath: string): void => { + setExpandedFileSettings((prev) => ({ + ...prev, + [filePath]: !prev[filePath], + })); + }; + + // Handle RMF file selection + const handleBrowseRmfFile = async (): Promise => { + if (!window.electronAPI) { + showAlert('File dialog not available (Electron API not found)', 'error'); + return; + } + + const files = await window.electronAPI.openFile({ + title: 'Select RMF (Response Matrix) File', + filters: [ + { name: 'RMF Files', extensions: ['rmf', 'rsp'] }, + { name: 'FITS Files', extensions: ['fits', 'fit'] }, + { name: 'All Files', extensions: ['*'] }, + ], + multiple: false, + }); + + if (files && files.length > 0) { + setRmfFile(files[0]); + } + }; + + // Handle per-file RMF file browsing + const handleBrowsePerFileRmf = async (filePath: string): Promise => { + if (!window.electronAPI) { + showAlert('File dialog not available (Electron API not found)', 'error'); + return; + } + + const files = await window.electronAPI.openFile({ + title: 'Select RMF (Response Matrix) File', + filters: [ + { name: 'RMF Files', extensions: ['rmf', 'rsp'] }, + { name: 'FITS Files', extensions: ['fits', 'fit'] }, + { name: 'All Files', extensions: ['*'] }, + ], + multiple: false, + }); + + if (files && files.length > 0) { + handlePerFileConfigChange(filePath, { rmf_file: files[0] }); + } + }; + + // Handle loading files (single or batch) - submits background jobs + const handleLoadFile = async (): Promise => { + if (selectedFiles.length === 0) { + showAlert('Please select a file first', 'warning'); + return; + } + + // Validate names + const names = Object.values(fileNames); + const emptyNames = selectedFiles.filter((f) => !fileNames[f]?.trim()); + if (emptyNames.length > 0) { + showAlert('Please provide names for all selected files', 'warning'); + return; + } + + // Check for duplicate names + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + showAlert('File names must be unique', 'warning'); + return; + } + + setIsLoading(true); + // Batch result cleared (job queue handles results) + setAlert({ open: false, message: '', severity: 'info' }); + + try { + await apiClient.getPort(); + + // Parse additional columns if provided (for shared settings) + const additionalColumnsArray = additionalColumns.trim() + ? additionalColumns.split(',').map((col) => col.trim()).filter((col) => col) + : undefined; + + // Single file: submit a single load job + if (selectedFiles.length === 1) { + const response = await jobApi.submitLoadJob({ + file_path: selectedFiles[0], + name: fileNames[selectedFiles[0]].trim(), + fmt: fileFormat, + rmf_file: rmfFile || undefined, + additional_columns: additionalColumnsArray, + high_precision: highPrecision, + skip_checks: skipChecks, + notes: eventNotes.trim() || undefined, + use_partial_loading: useTrueLazyLoading, + partial_mode: trueLazyMode, + time_range_start: useTrueLazyLoading && trueLazyMode === 'time_range' ? timeRangeStart : undefined, + time_range_end: useTrueLazyLoading && trueLazyMode === 'time_range' ? timeRangeEnd : undefined, + event_start_index: useTrueLazyLoading && trueLazyMode === 'event_count' ? eventCountStart : undefined, + event_count: useTrueLazyLoading && trueLazyMode === 'event_count' ? eventCount : undefined, + }); + + if (response.success && response.data) { + showAlert( + `Job submitted: ${response.data.display_name}. Check sidebar for progress.`, + 'info', + 'Job Submitted' + ); + // Save files to localStorage before clearing form + saveLastLoadedFiles(selectedFiles, fileNames); + setHasLastLoadedFiles(true); + resetForm(); + } else { + showAlert(response.message || 'Failed to submit load job', 'error', 'Job Submit Failed'); + } + } else { + // Multiple files: submit a batch load job + const fileConfigs: BatchFileConfig[] = selectedFiles.map((f) => { + const perFile = perFileConfigs[f] || {}; + return { + file_path: f, + name: fileNames[f].trim(), + fmt: useSameSettings ? fileFormat : (perFile.fmt || 'ogip'), + rmf_file: useSameSettings ? (rmfFile || undefined) : perFile.rmf_file, + additional_columns: useSameSettings ? additionalColumnsArray : perFile.additional_columns, + high_precision: useSameSettings ? highPrecision : (perFile.high_precision || false), + skip_checks: useSameSettings ? skipChecks : (perFile.skip_checks || false), + use_partial_loading: useSameSettings ? useTrueLazyLoading : (perFile.use_partial_loading || false), + partial_mode: useSameSettings ? trueLazyMode : (perFile.partial_mode || 'time_range'), + time_range_start: useSameSettings ? timeRangeStart : perFile.time_range_start, + time_range_end: useSameSettings ? timeRangeEnd : perFile.time_range_end, + event_start_index: useSameSettings ? eventCountStart : perFile.event_start_index, + event_count: useSameSettings ? eventCount : perFile.event_count, + notes: useSameSettings ? (eventNotes.trim() || undefined) : (perFile.notes?.trim() || undefined), + }; + }); + + const response = await jobApi.submitBatchJob({ + files: fileConfigs, + use_same_settings: useSameSettings, + shared_fmt: fileFormat, + shared_rmf_file: rmfFile || undefined, + shared_additional_columns: additionalColumnsArray, + shared_high_precision: highPrecision, + shared_skip_checks: skipChecks, + shared_use_partial_loading: useTrueLazyLoading, + shared_partial_mode: trueLazyMode, + shared_time_range_start: useTrueLazyLoading ? timeRangeStart : undefined, + shared_time_range_end: useTrueLazyLoading ? timeRangeEnd : undefined, + shared_event_start_index: useTrueLazyLoading ? eventCountStart : undefined, + shared_event_count: useTrueLazyLoading ? eventCount : undefined, + }); + + if (response.success && response.data) { + showAlert( + `Batch job submitted: ${selectedFiles.length} files. Check sidebar for progress.`, + 'info', + 'Batch Job Submitted' + ); + // Save files to localStorage and clear form + saveLastLoadedFiles(selectedFiles, fileNames); + setHasLastLoadedFiles(true); + resetForm(); + } else { + showAlert(response.message || 'Failed to submit batch job', 'error', 'Batch Job Submit Failed'); + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + showAlert(`Error: ${errorMessage}`, 'error', 'Job Submit Error'); + } finally { + setIsLoading(false); + } + }; + + // Reset form after successful load + const resetForm = (): void => { + setSelectedFiles([]); + setFileNames({}); + setPerFileConfigs({}); + setExpandedFileSettings({}); + setRmfFile(''); + setAdditionalColumns(''); + setEventNotes(''); + setFileSizeInfo(null); + setBatchSizeInfo(null); + setFileMetadata(null); + setUseTrueLazyLoading(false); + setHighPrecision(false); + setSkipChecks(false); + setTimeRangeStart(0); + setTimeRangeEnd(100); + setEventCountStart(0); + setEventCount(10000); + // Batch result cleared (job queue handles results) + }; + + // Handle deleting an event list + const handleDeleteEventList = async (name: string): Promise => { + try { + await apiClient.getPort(); + const response = await dataApi.deleteEventList(name); + + if (response.success) { + showAlert(`Event List '${name}' deleted`, 'success', 'Data Deleted'); + await fetchEventLists(); + } else { + showAlert(response.message || 'Failed to delete Event List', 'error', 'Delete Failed'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + showAlert(`Error: ${errorMessage}`, 'error', 'Delete Error'); + } + }; + + // Handle opening the save format dialog + const handleSaveEventList = (name: string): void => { + setSaveEventListName(name); + setSelectedSaveFormat('hdf5'); // Reset to default + setSaveFormatDialogOpen(true); + }; + + // Handle the actual save after format is selected + const handleConfirmSave = async (): Promise => { + setSaveFormatDialogOpen(false); + + if (!window.electronAPI) { + showAlert('Save dialog not available (Electron API not found)', 'error'); + return; + } + + // Determine file extension based on selected format + const extensionMap: Record = { + 'hdf5': 'hdf5', + 'ascii.ecsv': 'ecsv', + 'pickle': 'pkl', + }; + const ext = extensionMap[selectedSaveFormat] || 'hdf5'; + + // Determine file filter based on selected format + const filterMap: Record = { + 'hdf5': { name: 'HDF5 Files', extensions: ['hdf5', 'h5'] }, + 'ascii.ecsv': { name: 'ASCII ECSV Files', extensions: ['ecsv'] }, + 'pickle': { name: 'Pickle Files', extensions: ['pkl'] }, + }; + const filter = filterMap[selectedSaveFormat] || filterMap['hdf5']; + + const filePath = await window.electronAPI.saveFile({ + title: `Save Event List: ${saveEventListName}`, + defaultPath: `${saveEventListName}.${ext}`, + filters: [filter], + }); + + if (!filePath) { + return; // User cancelled + } + + try { + await apiClient.getPort(); + + const response = await dataApi.saveEventList({ + name: saveEventListName, + file_path: filePath, + fmt: selectedSaveFormat, + }); + + if (response.success) { + showAlert(`Event List '${saveEventListName}' saved to ${filePath}`, 'success', 'Data Saved'); + } else { + showAlert(response.message || 'Failed to save Event List', 'error', 'Save Failed'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + showAlert(`Error: ${errorMessage}`, 'error', 'Save Error'); + } + }; + + // Handle clearing all event lists + const handleClearAll = async (): Promise => { + if (loadedEventLists.length === 0) { + showAlert('No event lists to clear', 'warning'); + return; + } + + try { + await apiClient.getPort(); + const response = await dataApi.clearAllEventLists(); + + if (response.success) { + showAlert(response.message || 'All event lists cleared', 'success', 'Data Cleared'); + await fetchEventLists(); + } else { + showAlert(response.message || 'Failed to clear event lists', 'error', 'Clear Failed'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + showAlert(`Error: ${errorMessage}`, 'error', 'Clear Error'); + } + }; + + // Handle loading from URL - submits a background job + const handleLoadFromUrl = async (): Promise => { + if (!urlInput.trim()) { + showAlert('Please enter a URL', 'warning'); + return; + } + + if (!urlEventListName.trim()) { + showAlert('Please provide a name for the Event List', 'warning'); + return; + } + + // Basic URL validation + try { + new URL(urlInput.trim()); + } catch { + showAlert('Please enter a valid URL', 'warning'); + return; + } + + setIsLoadingUrl(true); + setAlert({ open: false, message: '', severity: 'info' }); + + try { + await apiClient.getPort(); + + // Submit URL download job + const response = await jobApi.submitUrlJob({ + url: urlInput.trim(), + name: urlEventListName.trim(), + fmt: urlFormat, + high_precision: highPrecision, + skip_checks: skipChecks, + }); + + if (response.success && response.data) { + showAlert( + `URL download job submitted: ${response.data.display_name}. Check sidebar for progress.`, + 'info', + 'Job Submitted' + ); + // Reset form + setUrlInput(''); + setUrlEventListName(''); + } else { + showAlert(response.message || 'Failed to submit URL job', 'error', 'Job Submit Failed'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + showAlert(`Error: ${errorMessage}`, 'error', 'URL Job Submit Error'); + } finally { + setIsLoadingUrl(false); + } + }; + + // Handle viewing event list details + const handleViewDetails = async (name: string): Promise => { + setDetailsLoading(true); + setDetailsOpen(true); + + try { + await apiClient.getPort(); + const response = await dataApi.getEventListInfo(name); + + if (response.success && response.data) { + setSelectedEventListDetails(response.data); + } else { + showAlert(response.message || 'Failed to fetch details', 'error', 'Details Error'); + setDetailsOpen(false); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + showAlert(`Error: ${errorMessage}`, 'error', 'Details Error'); + setDetailsOpen(false); + } finally { + setDetailsLoading(false); + } + }; + + // Close details dialog + const handleCloseDetails = (): void => { + setDetailsOpen(false); + setSelectedEventListDetails(null); + }; + + // Handle viewing full preview of event list + const handleViewFullPreview = async (name: string): Promise => { + setFullPreviewLoading(true); + setFullPreviewOpen(true); + setPreviewTabValue(0); + + try { + await apiClient.getPort(); + const response = await dataApi.getEventListFullPreview(name, 10); + + if (response.success && response.data) { + setFullPreviewData(response.data); + } else { + showAlert(response.message || 'Failed to fetch full preview', 'error', 'Preview Error'); + setFullPreviewOpen(false); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + showAlert(`Error: ${errorMessage}`, 'error', 'Preview Error'); + setFullPreviewOpen(false); + } finally { + setFullPreviewLoading(false); + } + }; + + // Close full preview dialog + const handleCloseFullPreview = (): void => { + setFullPreviewOpen(false); + setFullPreviewData(null); + setPreviewTabValue(0); + }; + + // Format number with commas + const formatNumber = (num: number): string => { + return num.toLocaleString(); + }; + + // Format time range + const formatTimeRange = (range: [number, number]): string => { + const duration = range[1] - range[0]; + return `${duration.toFixed(2)}s`; + }; + + return ( + + + Data Ingestion + + + Load X-ray astronomy event list files for analysis + + + {/* Alert */} + {alert.open && ( + setAlert({ ...alert, open: false })} + sx={{ mb: 3 }} + > + {alert.message} + + )} + + {/* Data Input Tabs */} + + setDataInputTab(newValue)} + variant="fullWidth" + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + } + iconPosition="start" + label="Local File" + sx={{ textTransform: 'none' }} + /> + } + iconPosition="start" + label="From URL" + sx={{ textTransform: 'none' }} + /> + } + iconPosition="start" + label="Browse HEASARC" + sx={{ textTransform: 'none' }} + /> + + + + {/* Tab 0: Load from Local File */} + {dataInputTab === 0 && ( + + + + Load Local File + + + + Load FITS, HDF5, or text event list files from your computer (supports multiple files) + + + {/* File Selection */} + + + + {hasLastLoadedFiles && ( + + + + )} + + + {/* Selected Files List */} + {selectedFiles.length > 0 && ( + + + + Selected Files ({selectedFiles.length}) + {selectedFiles.length > 1 && ( + + )} + + + + + {/* Batch Size Info (for multiple files) */} + {(isCheckingBatchSize || isCheckingFileSize) && ( + + + + Checking file sizes... + + + )} + + {/* Batch Size Summary */} + {batchSizeInfo && !isCheckingBatchSize && ( + + + + {batchSizeInfo.total.risk_level === 'safe' ? ( + + ) : ( + + )} + + Total: {batchSizeInfo.total.size_mb.toFixed(1)} MB → ~{batchSizeInfo.total.estimated_ram_mb.toFixed(0)} MB RAM + + + + + + Available RAM: {batchSizeInfo.available_ram_mb.toFixed(0)} MB + + {batchSizeInfo.recommend_partial_loading && ( + + Consider using partial loading to reduce memory usage. + + )} + + )} + + {/* Single file size info */} + {fileSizeInfo && !isCheckingFileSize && selectedFiles.length === 1 && ( + + + {fileSizeInfo.risk_level === 'safe' ? ( + + ) : ( + + )} + + {fileSizeInfo.file_size_mb < 1 + ? `${(fileSizeInfo.file_size_bytes / 1024).toFixed(1)} KB` + : fileSizeInfo.file_size_gb >= 1 + ? `${fileSizeInfo.file_size_gb.toFixed(2)} GB` + : `${fileSizeInfo.file_size_mb.toFixed(1)} MB`} + + + + {fileSizeInfo.ram_usage_percent !== undefined && ( + + Would use ~{fileSizeInfo.ram_usage_percent.toFixed(0)}% of available RAM + + )} + + )} + + {/* Settings Mode Toggle (only for batch) */} + {selectedFiles.length > 1 && ( + + setUseSameSettings(e.target.checked)} + size="small" + /> + } + label={ + + Apply same settings to all files + + } + /> + + {useSameSettings + ? 'All files will use the format and options below' + : 'Each file can have different settings (expand to configure)'} + + + )} + + {/* File List */} + + + {selectedFiles.map((filePath, index) => { + const fileName = filePath.split('/').pop() || filePath; + const sizeInfo = batchSizeInfo?.files.find((f) => f.file_path === filePath); + const isExpanded = expandedFileSettings[filePath]; + + return ( + + {index > 0 && } + + + handleFileNameChange(filePath, e.target.value)} + placeholder="Name" + sx={{ width: 140, flexShrink: 0 }} + inputProps={{ style: { fontSize: '0.875rem' } }} + /> + + + {fileName} + + {sizeInfo && ( + + {sizeInfo.size_mb.toFixed(1)} MB + {sizeInfo.ram_percent > 30 && ( + + )} + + )} + + {/* Per-file settings button (only when not using same settings) */} + {selectedFiles.length > 1 && !useSameSettings && ( + toggleFileSettings(filePath)} + color={isExpanded ? 'primary' : 'default'} + > + + + )} + handleRemoveFile(filePath)} + color="error" + > + + + + + {/* Per-file settings (collapsed) */} + {!useSameSettings && isExpanded && ( + + + {/* Format */} + + + Format + + + + + {/* RMF File */} + + + RMF File (optional) + + + handlePerFileConfigChange(filePath, { rmf_file: e.target.value })} + fullWidth + placeholder="Path to RMF file" + InputProps={{ readOnly: true }} + inputProps={{ style: { fontSize: '0.75rem' } }} + /> + + {perFileConfigs[filePath]?.rmf_file && ( + handlePerFileConfigChange(filePath, { rmf_file: undefined })} + > + + + )} + + + + {/* Additional Columns */} + + handlePerFileConfigChange(filePath, { + additional_columns: e.target.value + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + })} + fullWidth + placeholder="e.g., PI, ENERGY, DET_ID" + helperText="Comma-separated" + inputProps={{ style: { fontSize: '0.75rem' } }} + /> + + + {/* Notes */} + + handlePerFileConfigChange(filePath, { + notes: e.target.value + })} + fullWidth + multiline + rows={2} + placeholder="Add notes about this file..." + inputProps={{ style: { fontSize: '0.75rem' } }} + /> + + + {/* High Precision & Skip Checks */} + + + handlePerFileConfigChange(filePath, { high_precision: e.target.checked })} + size="small" + /> + } + label={ + + + High precision + + } + /> + + + + + handlePerFileConfigChange(filePath, { skip_checks: e.target.checked })} + size="small" + /> + } + label={ + + + Skip checks + + } + /> + + + + {/* Partial Loading */} + + + handlePerFileConfigChange(filePath, { use_partial_loading: e.target.checked })} + size="small" + color="secondary" + /> + } + label={ + + + Partial loading + + } + /> + + + + {perFileConfigs[filePath]?.use_partial_loading && ( + <> + + + Mode + + + + {perFileConfigs[filePath]?.partial_mode === 'time_range' ? ( + <> + + handlePerFileConfigChange(filePath, { time_range_start: parseFloat(e.target.value) || 0 })} + fullWidth + /> + + + handlePerFileConfigChange(filePath, { time_range_end: parseFloat(e.target.value) || 100 })} + fullWidth + /> + + + ) : ( + <> + + handlePerFileConfigChange(filePath, { event_start_index: parseInt(e.target.value) || 0 })} + fullWidth + /> + + + handlePerFileConfigChange(filePath, { event_count: parseInt(e.target.value) || 10000 })} + fullWidth + /> + + + )} + + )} + + + )} + + + ); + })} + + + + )} + + + + {/* File Format - only shown when using same settings for all files */} + {useSameSettings && ( + + File Format + + + )} + + {/* Advanced Options Toggle - only shown when using same settings for all files */} + {useSameSettings && ( + + )} + + {/* Advanced Options Content - only shown when using same settings for all files */} + + + {/* RMF File */} + + RMF File (for PI → Energy calibration) + + + setRmfFile(e.target.value)} + fullWidth + size="small" + placeholder="Optional: Path to RMF file" + InputProps={{ + readOnly: true, + }} + /> + + {rmfFile && ( + setRmfFile('')}> + + + )} + + + {/* Additional Columns */} + + Additional Columns + + setAdditionalColumns(e.target.value)} + fullWidth + size="small" + placeholder="e.g., PI, ENERGY, DET_ID (comma-separated)" + helperText="Extra columns to read from the file" + sx={{ mb: 2 }} + /> + + {/* Notes/Comments */} + + Notes / Comments + + setEventNotes(e.target.value)} + fullWidth + size="small" + multiline + rows={2} + placeholder="Add notes about this data (e.g., observation details, analysis purpose...)" + helperText="Optional annotations stored with the event list" + sx={{ mb: 2 }} + /> + + + + {/* Loading Options */} + + + Loading Options + + + {/* High Precision */} + + High Precision Timing + + Uses numpy.float128 (128-bit) instead of float64 for time arrays, providing ~18 more decimal digits of precision. + + + Use when: Pulsar timing analysis, millisecond pulsars, phase-coherent timing, or any analysis requiring nanosecond-level accuracy. + + + Avoid when: General spectral/timing analysis, large files (increases memory ~2x), or when float64 precision is sufficient. + + + } + arrow + placement="right" + > + setHighPrecision(e.target.checked)} + size="small" + /> + } + label={ + + + High precision timing + + } + /> + + + {/* Skip Checks */} + + Skip Validation Checks + + Bypasses time ordering verification and GTI (Good Time Interval) validation during loading. + + + Use when: Loading trusted/verified data, re-loading previously validated files, or when you need faster loading and will validate manually. + + + Avoid when: Loading new/untrusted data, data from unfamiliar sources, or when data integrity is critical for your analysis. + + + } + arrow + placement="right" + > + setSkipChecks(e.target.checked)} + size="small" + /> + } + label={ + + + Skip validation checks + + } + /> + + + {/* Partial Loading */} + + Partial Loading + + Loads only a specific portion of the file (by time range or event count) without reading the entire file into memory. + + + Use when: Working with large FITS files (>1GB), exploring data before full analysis, or when you only need a specific time segment. + + + Avoid when: Using non-FITS formats (HDF5, CSV), or when you need the complete dataset for analysis. + + + Note: Only FITS formats are supported. + + + } + arrow + placement="right" + > + setUseTrueLazyLoading(e.target.checked)} + size="small" + color="secondary" + /> + } + label={ + + + Partial loading + + } + /> + + + {/* True Lazy Loading Options */} + {useTrueLazyLoading && ( + + {/* Get Metadata Button */} + + + {/* File Metadata Display */} + {fileMetadata && ( + + + Total Events: {fileMetadata.total_events.toLocaleString()} + + + Duration: {fileMetadata.duration.toFixed(2)}s + + + File Size: {fileMetadata.file_size_mb.toFixed(1)} MB + + + GTIs: {fileMetadata.gti_count} + + {fileMetadata.mission && ( + + Mission: {fileMetadata.mission} + + )} + + + )} + + {/* Mode Selection */} + + Loading Mode + + + + {/* Time Range Mode */} + {trueLazyMode === 'time_range' && ( + + setTimeRangeStart(parseFloat(e.target.value) || 0)} + size="small" + fullWidth + inputProps={{ min: 0, step: 1 }} + /> + setTimeRangeEnd(parseFloat(e.target.value) || 100)} + size="small" + fullWidth + inputProps={{ min: 0, step: 1 }} + /> + + )} + + {/* Event Count Mode */} + {trueLazyMode === 'event_count' && ( + + setEventCountStart(parseInt(e.target.value) || 0)} + size="small" + fullWidth + inputProps={{ min: 0, step: 1000 }} + /> + setEventCount(parseInt(e.target.value) || 10000)} + size="small" + fullWidth + inputProps={{ min: 1, step: 1000 }} + /> + + )} + + + {trueLazyMode === 'time_range' + ? `Will load events from ${timeRangeStart}s to ${timeRangeEnd}s (${timeRangeEnd - timeRangeStart}s duration)` + : `Will load ${eventCount.toLocaleString()} events starting from index ${eventCountStart.toLocaleString()}`} + + + )} + + {/* Loading mode indicator */} + {useTrueLazyLoading && ( + } + > + {trueLazyMode === 'time_range' + ? `Partial loading: Only events in [${timeRangeStart}s - ${timeRangeEnd}s] will be loaded.` + : `Partial loading: Only ${eventCount.toLocaleString()} events starting at index ${eventCountStart} will be loaded.`} + + )} +
+ + + {/* Load Button */} + + + {/* Note: Batch loading progress is now shown in the sidebar job queue */} + + )} + + {/* Tab 1: Load from URL */} + {dataInputTab === 1 && ( + + + + Load from URL + + + + Fetch event list data directly from a remote URL (use raw links for GitHub) + + + {/* URL Input */} + setUrlInput(e.target.value)} + fullWidth + size="small" + sx={{ mb: 2 }} + placeholder="https://example.com/data/events.fits" + helperText="Enter the direct link to the event file" + InputProps={{ + startAdornment: , + }} + /> + + {/* Event List Name */} + setUrlEventListName(e.target.value)} + fullWidth + size="small" + sx={{ mb: 2 }} + placeholder="Enter a name for this event list" + helperText="This name will be used to reference the data" + /> + + {/* File Format */} + + File Format + + + + {/* Note: Download progress is now shown in the sidebar job queue */} + + {/* Load Button */} + + + )} + + {/* Tab 2: Browse HEASARC */} + {dataInputTab === 2 && ( + + )} + + + + {/* Loaded Event Lists */} + + + Loaded Event Lists + + {loadedEventLists.length > 0 && ( + + )} + + + + + + + + + {loadedEventLists.length === 0 ? ( + + No event lists loaded yet. Use the options above to load your data. + + ) : ( + + {loadedEventLists.map((eventList, index) => ( + + {index > 0 && } + + + + {eventList.name} + + {eventList.has_energy && ( + + )} + {eventList.has_pi && ( + + )} + {eventList.gti_warnings && eventList.gti_warnings.length > 0 && ( + + 1 ? 's' : ''}`} + size="small" + color="warning" + icon={} + /> + + )} + {/* Validation issues */} + {eventList.validation_issues && eventList.validation_issues.length > 0 && (() => { + const errors = eventList.validation_issues.filter((v) => v.severity === 'error'); + const warnings = eventList.validation_issues.filter((v) => v.severity === 'warning'); + return ( + <> + {errors.length > 0 && ( + e.message).join('\n')}> + 1 ? 's' : ''}`} + size="small" + color="error" + icon={} + /> + + )} + {warnings.length > 0 && ( + w.message).join('\n')}> + 1 ? 's' : ''}`} + size="small" + color="warning" + variant="outlined" + /> + + )} + + ); + })()} + {/* Notes indicator */} + {eventList.notes && ( + + + + )} + + } + secondary={ + + + {formatNumber(eventList.n_events)} events + + + • + + + Duration: {formatTimeRange(eventList.time_range)} + + {eventList.gti_count !== undefined && eventList.gti_count > 0 && ( + <> + + • + + + {eventList.gti_count} GTI(s) + + + )} + + } + /> + + + handleViewDetails(eventList.name)} + > + + + + + handleViewFullPreview(eventList.name)} + color="secondary" + > + + + + + handleSaveEventList(eventList.name)} + color="primary" + > + + + + + handleDeleteEventList(eventList.name)} + color="error" + > + + + + + + + ))} + + )} + + + {/* Event List Details Dialog */} + + + + Event List Details: {selectedEventListDetails?.name || ''} + + + + + + + {detailsLoading ? ( + + + + ) : selectedEventListDetails ? ( + + {/* Basic Info */} + + + Basic Information + + + + Events + {formatNumber(selectedEventListDetails.n_events)} + + + Duration + {selectedEventListDetails.duration.toFixed(6)}s + + + Mean Count Rate + + {selectedEventListDetails.mean_count_rate?.toFixed(6) || 'N/A'} cts/s + + + + MJDREF + {selectedEventListDetails.mjdref || 'N/A'} + + {selectedEventListDetails.mission && ( + + Mission + {selectedEventListDetails.mission} + + )} + {selectedEventListDetails.instrument && ( + + Instrument + {selectedEventListDetails.instrument} + + )} + + + + + + {/* Time Range */} + + + Time Information + + + + Start Time + {selectedEventListDetails.time_range[0].toFixed(6)} + + + End Time + {selectedEventListDetails.time_range[1].toFixed(6)} + + {selectedEventListDetails.min_time_diff !== undefined && ( + + Min Time Diff + {selectedEventListDetails.min_time_diff.toExponential(6)}s + + )} + + + + + + {/* Data Columns */} + + + Data Columns + + + + {selectedEventListDetails.has_energy && ( + + )} + {selectedEventListDetails.has_pi && ( + + )} + + + + {/* GTI Table */} + {selectedEventListDetails.gti_list && selectedEventListDetails.gti_list.length > 0 && ( + <> + + + + Good Time Intervals ({selectedEventListDetails.gti_count} GTI{selectedEventListDetails.gti_count !== 1 ? 's' : ''}) + {selectedEventListDetails.total_gti_time && ( + + Total: {selectedEventListDetails.total_gti_time.toFixed(6)}s + + )} + + + + + + # + Start + Stop + Duration + + + + {selectedEventListDetails.gti_list.map((gti, index) => ( + + {index + 1} + {gti[0].toFixed(6)} + {gti[1].toFixed(6)} + {(gti[1] - gti[0]).toFixed(6)}s + + ))} + +
+
+
+ + )} +
+ ) : ( + No details available + )} +
+ + + +
+ + {/* Full Preview Dialog */} + + + + + + Full Preview: {fullPreviewData?.name || ''} + + + + + + + + {fullPreviewLoading ? ( + + + + ) : fullPreviewData ? ( + + {/* Tabs for different sections */} + setPreviewTabValue(newValue)} + sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }} + variant="scrollable" + scrollButtons="auto" + > + + + + + + + + + + {/* Overview Tab */} + {previewTabValue === 0 && ( + + + + + + {formatNumber(fullPreviewData.n_events)} + + Total Events + + + + + + {fullPreviewData.duration.toFixed(6)}s + + Duration + + + + + + {fullPreviewData.mean_count_rate?.toFixed(6) || 'N/A'} + + Mean Count Rate (cts/s) + + + + + + {fullPreviewData.gti_count} + + GTI Count + + + + + + + {fullPreviewData.has_energy && } + {fullPreviewData.has_pi && } + {fullPreviewData.mission && } + {fullPreviewData.instrument && } + + + )} + + {/* Time Data Tab */} + {previewTabValue === 1 && ( + + Time Range + + + Start Time + + {fullPreviewData.time_range[0].toFixed(6)} + + + + End Time + + {fullPreviewData.time_range[1].toFixed(6)} + + + + MJDREF + + {fullPreviewData.mjdref || 'N/A'} + + + + + + + Time Statistics + + + Min Time Diff + + {fullPreviewData.min_time_diff?.toExponential(6) || 'N/A'}s + + + + Max Time Diff + + {fullPreviewData.max_time_diff?.toExponential(6) || 'N/A'}s + + + + Mean Time Diff + + {fullPreviewData.mean_time_diff?.toExponential(6) || 'N/A'}s + + + + Median Time Diff + + {fullPreviewData.median_time_diff?.toExponential(6) || 'N/A'}s + + + + Std Dev Time Diff + + {fullPreviewData.std_time_diff?.toExponential(6) || 'N/A'}s + + + + + {/* Per-GTI Rates */} + {fullPreviewData.per_gti_rates && fullPreviewData.per_gti_rates.length > 0 && ( + <> + + + Per-GTI Count Rates ({fullPreviewData.per_gti_rates.length} GTI{fullPreviewData.per_gti_rates.length !== 1 ? 's' : ''}) + + + + + + # + Start (s) + Stop (s) + Events + Duration (s) + Rate (cts/s) + + + + {fullPreviewData.per_gti_rates.map((gti, index) => ( + + {index + 1} + {gti.start.toFixed(6)} + {gti.stop.toFixed(6)} + {gti.events.toLocaleString()} + {gti.duration.toFixed(6)} + {gti.rate.toFixed(6)} + + ))} + +
+
+ + )} + + + + + Time Preview (first {fullPreviewData.times_preview.length} entries) + + + + + + # + Time (s) + + + + {fullPreviewData.times_preview.map((time, index) => ( + + {index + 1} + {time.toFixed(6)} + + ))} + +
+
+
+ )} + + {/* Energy & PI Tab */} + {previewTabValue === 2 && ( + + {/* Energy Section */} + Energy Data + {fullPreviewData.has_energy ? ( + <> + + + Energy Range + + {fullPreviewData.energy_range + ? `${fullPreviewData.energy_range[0].toFixed(5)} - ${fullPreviewData.energy_range[1].toFixed(5)} keV` + : 'N/A'} + + + + {fullPreviewData.energy_preview && ( + <> + + Energy Preview (first {fullPreviewData.energy_preview.length} entries) + + + {fullPreviewData.energy_preview.map((energy, index) => ( + + ))} + + + )} + + ) : ( + No energy data available in this EventList + )} + + + + {/* PI Section */} + PI (Pulse Invariant) Data + {fullPreviewData.has_pi ? ( + <> + + + PI Range + + {fullPreviewData.pi_range + ? `${fullPreviewData.pi_range[0]} - ${fullPreviewData.pi_range[1]}` + : 'N/A'} + + + + {fullPreviewData.pi_preview && ( + <> + + PI Preview (first {fullPreviewData.pi_preview.length} entries) + + + {fullPreviewData.pi_preview.map((pi, index) => ( + + ))} + + + )} + + ) : ( + No PI data available in this EventList + )} + + )} + + {/* GTIs Tab */} + {previewTabValue === 3 && ( + + + Good Time Intervals ({fullPreviewData.gti_count} GTI{fullPreviewData.gti_count !== 1 ? 's' : ''}) + + {fullPreviewData.total_gti_time && ( + + Total GTI Time: {fullPreviewData.total_gti_time.toFixed(6)}s + + )} + {fullPreviewData.gti_list && fullPreviewData.gti_list.length > 0 ? ( + + + + + # + Start (s) + Stop (s) + Duration (s) + + + + {fullPreviewData.gti_list.map((gti, index) => ( + + {index + 1} + {gti[0].toFixed(6)} + {gti[1].toFixed(6)} + {(gti[1] - gti[0]).toFixed(6)} + + ))} + +
+
+ ) : ( + No GTI data available + )} +
+ )} + + {/* Metadata Tab */} + {previewTabValue === 4 && ( + + Mission Metadata + + + Mission + {fullPreviewData.mission || 'N/A'} + + + Instrument + {fullPreviewData.instrument || 'N/A'} + + + Detector ID + {fullPreviewData.detector_id || 'N/A'} + + + + + + Time System + + + Time Reference + {fullPreviewData.timeref || 'N/A'} + + + Time System + {fullPreviewData.timesys || 'N/A'} + + + Ephemeris + {fullPreviewData.ephem || 'N/A'} + + + + {fullPreviewData.additional_columns && fullPreviewData.additional_columns.length > 0 && ( + <> + + Additional Columns + + {fullPreviewData.additional_columns.map((col, index) => ( + + ))} + + + )} + + {/* User Notes */} + {fullPreviewData.notes && ( + <> + + User Notes + + + {fullPreviewData.notes} + + + + )} + + )} + + {/* Header Tab */} + {previewTabValue === 5 && ( + + {fullPreviewData.header_info && Object.keys(fullPreviewData.header_info).length > 0 ? ( + <> + Key FITS Headers + + {fullPreviewData.header_info.object && ( + + Object + {fullPreviewData.header_info.object} + + )} + {fullPreviewData.header_info.obs_id && ( + + OBS_ID + {fullPreviewData.header_info.obs_id} + + )} + {(fullPreviewData.header_info.ra_nom !== undefined || fullPreviewData.header_info.ra_obj !== undefined) && ( + + RA + + {(fullPreviewData.header_info.ra_nom ?? fullPreviewData.header_info.ra_obj)?.toFixed(7) || 'N/A'}° + + + )} + {(fullPreviewData.header_info.dec_nom !== undefined || fullPreviewData.header_info.dec_obj !== undefined) && ( + + Dec + + {(fullPreviewData.header_info.dec_nom ?? fullPreviewData.header_info.dec_obj)?.toFixed(7) || 'N/A'}° + + + )} + {fullPreviewData.header_info.exposure !== undefined && ( + + Exposure + + {fullPreviewData.header_info.exposure?.toFixed(6) || 'N/A'}s + + + )} + {fullPreviewData.header_info.ontime !== undefined && ( + + Ontime + + {fullPreviewData.header_info.ontime?.toFixed(6) || 'N/A'}s + + + )} + {fullPreviewData.header_info.livetime !== undefined && ( + + Livetime + + {fullPreviewData.header_info.livetime?.toFixed(6) || 'N/A'}s + + + )} + {fullPreviewData.header_info.date_obs && ( + + DATE-OBS + {fullPreviewData.header_info.date_obs} + + )} + {fullPreviewData.header_info.date_end && ( + + DATE-END + {fullPreviewData.header_info.date_end} + + )} + {fullPreviewData.header_info.telescop && ( + + Telescope + {fullPreviewData.header_info.telescop} + + )} + {fullPreviewData.header_info.instrume && ( + + Instrument + {fullPreviewData.header_info.instrume} + + )} + {fullPreviewData.header_info.creator && ( + + Creator + {fullPreviewData.header_info.creator} + + )} + {fullPreviewData.header_info.observer && ( + + Observer + {fullPreviewData.header_info.observer} + + )} + {fullPreviewData.header_info.datamode && ( + + Data Mode + {fullPreviewData.header_info.datamode} + + )} + + + {/* Raw Header Table */} + {fullPreviewData.header_info.raw_header && Object.keys(fullPreviewData.header_info.raw_header).length > 0 && ( + <> + + + Raw FITS Header ({Object.keys(fullPreviewData.header_info.raw_header).length} entries) + + + + + + Keyword + Value + + + + {Object.entries(fullPreviewData.header_info.raw_header).map(([key, value]) => ( + + {key} + {value} + + ))} + +
+
+ + )} + + ) : ( + No FITS header information available for this EventList + )} +
+ )} + + {/* Validation Tab */} + {previewTabValue === 6 && ( + + Data Quality Validation + {fullPreviewData.validation_issues && fullPreviewData.validation_issues.length > 0 ? ( + <> + {/* Summary */} + + {(() => { + const passed = fullPreviewData.validation_issues.filter((v: ValidationIssue) => v.status === 'pass'); + const failed = fullPreviewData.validation_issues.filter((v: ValidationIssue) => v.status === 'fail'); + const skipped = fullPreviewData.validation_issues.filter((v: ValidationIssue) => v.status === 'skip'); + const errors = fullPreviewData.validation_issues.filter((v: ValidationIssue) => v.severity === 'error'); + const warnings = fullPreviewData.validation_issues.filter((v: ValidationIssue) => v.severity === 'warning'); + return ( + <> + } + /> + {failed.length > 0 && ( + } + /> + )} + {skipped.length > 0 && ( + + )} + {errors.length > 0 && ( + + )} + {warnings.length > 0 && ( + + )} + + ); + })()} + + + {/* All Checks List */} + + + + + Status + Check + Result + Details + + + + {fullPreviewData.validation_issues.map((issue: ValidationIssue, index: number) => ( + + + {issue.status === 'pass' && ( + + )} + {issue.status === 'fail' && ( + + )} + {issue.status === 'skip' && ( + + )} + + + + {issue.name || issue.type} + + {issue.description && ( + + {issue.description} + + )} + + + {issue.message} + + + {issue.status !== 'skip' && issue.total !== undefined && issue.total > 0 && ( + + {issue.count?.toLocaleString() || 0} / {issue.total?.toLocaleString()} + + )} + + + ))} + +
+
+ + ) : ( + + + No validation data available. Try reloading the event list. + + + )} +
+ )} +
+ ) : ( + No preview data available + )} +
+ + + +
+ + {/* Save Format Selection Dialog */} + setSaveFormatDialogOpen(false)} + maxWidth="xs" + fullWidth + > + + Choose Save Format + + + + Select the format for saving "{saveEventListName}": + + + setSelectedSaveFormat(e.target.value)} + > + } + label={ + + + HDF5 (Recommended) + + + Binary format. Preserves all metadata (GTI, MJDREF, etc.). Fast I/O, compact size. + + + } + /> + } + label={ + + + ASCII ECSV + + + Human-readable text format. Good for sharing and inspection. Larger file size. + + + } + sx={{ mt: 1 }} + /> + } + label={ + + + Pickle + + + Python-only format. Not recommended for long-term storage or sharing. + + + } + sx={{ mt: 1 }} + /> + + + + + + + + + + {/* CSS for refresh animation */} + + + ); +}; + +export default DataIngestionPage; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx new file mode 100644 index 0000000..dfa0eb0 --- /dev/null +++ b/src/pages/Home/index.tsx @@ -0,0 +1,301 @@ +import React from 'react'; +import { + Box, + Typography, + Card, + CardContent, + CardActionArea, + Grid, + Chip, + useTheme, +} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import AnalyticsIcon from '@mui/icons-material/Analytics'; +import BuildIcon from '@mui/icons-material/Build'; +import ModelTrainingIcon from '@mui/icons-material/ModelTraining'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import ScienceIcon from '@mui/icons-material/Science'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; + +interface QuickAccessCard { + title: string; + description: string; + icon: React.ReactNode; + path: string; + color: string; + glowColor: string; +} + +/** + * Home page with quick access cards and welcome message + */ +const HomePage: React.FC = () => { + const navigate = useNavigate(); + const theme = useTheme(); + const isDark = theme.palette.mode === 'dark'; + + const quickAccessCards: QuickAccessCard[] = [ + { + title: 'Load Data', + description: 'Import FITS, HDF5, or text files for analysis', + icon: , + path: '/data-ingestion', + color: '#3b82f6', + glowColor: 'rgba(59, 130, 246, 0.15)', + }, + { + title: 'QuickLook Analysis', + description: 'Power spectra, cross spectra, light curves, and more', + icon: , + path: '/quicklook/power-spectrum', + color: '#00d4aa', + glowColor: 'rgba(0, 212, 170, 0.15)', + }, + { + title: 'Pulsar Analysis', + description: 'Period search, phase folding, and phaseograms', + icon: , + path: '/pulsar/search', + color: '#a855f7', + glowColor: 'rgba(168, 85, 247, 0.15)', + }, + { + title: 'Modeling', + description: 'Model fitting with MLE and MCMC methods', + icon: , + path: '/modeling/builder', + color: '#f59e0b', + glowColor: 'rgba(245, 158, 11, 0.15)', + }, + { + title: 'Simulator', + description: 'Generate synthetic light curves and event lists', + icon: , + path: '/simulator', + color: '#ef4444', + glowColor: 'rgba(239, 68, 68, 0.15)', + }, + { + title: 'Utilities', + description: 'GTI handling, statistics, and I/O tools', + icon: , + path: '/utilities/gti', + color: '#64748b', + glowColor: 'rgba(100, 116, 139, 0.15)', + }, + ]; + + return ( + + {/* Welcome hero section */} + + + + Welcome to Stingray Explorer + + + Next-Generation Spectral Timing Made Easy + + + A comprehensive data analysis and visualization dashboard for X-ray astronomy + time series data. Built on top of the Stingray library, it provides an intuitive + graphical interface for analyzing event lists, generating light curves, computing + various types of spectra, and performing advanced timing analysis. + + + {['Stingray 2.0+', 'X-ray Astronomy', 'Time Series Analysis'].map((label) => ( + + ))} + + + + + {/* Quick Access heading */} + + + Quick Access + + + + + {/* Quick Access Cards */} + + {quickAccessCards.map((card) => ( + + + navigate(card.path)} + sx={{ height: '100%', p: 1 }} + > + + + + {card.icon} + + + {card.title} + + + + {card.description} + + + + + + ))} + + + ); +}; + +export default HomePage; diff --git a/src/pages/Modeling/MCMCFitting/index.tsx b/src/pages/Modeling/MCMCFitting/index.tsx new file mode 100644 index 0000000..badf0eb --- /dev/null +++ b/src/pages/Modeling/MCMCFitting/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const MCMCFittingPage: React.FC = () => { + return ( + + ); +}; + +export default MCMCFittingPage; diff --git a/src/pages/Modeling/MLEFitting/index.tsx b/src/pages/Modeling/MLEFitting/index.tsx new file mode 100644 index 0000000..0e056d4 --- /dev/null +++ b/src/pages/Modeling/MLEFitting/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const MLEFittingPage: React.FC = () => { + return ( + + ); +}; + +export default MLEFittingPage; diff --git a/src/pages/Modeling/ModelBuilder/index.tsx b/src/pages/Modeling/ModelBuilder/index.tsx new file mode 100644 index 0000000..cfd9b66 --- /dev/null +++ b/src/pages/Modeling/ModelBuilder/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const ModelBuilderPage: React.FC = () => { + return ( + + ); +}; + +export default ModelBuilderPage; diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx new file mode 100644 index 0000000..9a02f2a --- /dev/null +++ b/src/pages/NotFound/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Box, Typography, Button } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import HomeIcon from '@mui/icons-material/Home'; + +const NotFoundPage: React.FC = () => { + const navigate = useNavigate(); + + return ( + + + theme.palette.mode === 'dark' + ? '0 0 40px rgba(0, 212, 170, 0.2)' + : 'none', + }} + > + 404 + + + Page not found + + + + ); +}; + +export default NotFoundPage; diff --git a/src/pages/Pulsar/PeriodSearch/index.tsx b/src/pages/Pulsar/PeriodSearch/index.tsx new file mode 100644 index 0000000..19786b1 --- /dev/null +++ b/src/pages/Pulsar/PeriodSearch/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const PeriodSearchPage: React.FC = () => { + return ( + + ); +}; + +export default PeriodSearchPage; diff --git a/src/pages/Pulsar/PhaseFolding/index.tsx b/src/pages/Pulsar/PhaseFolding/index.tsx new file mode 100644 index 0000000..46cb219 --- /dev/null +++ b/src/pages/Pulsar/PhaseFolding/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const PhaseFoldingPage: React.FC = () => { + return ( + + ); +}; + +export default PhaseFoldingPage; diff --git a/src/pages/Pulsar/Phaseogram/index.tsx b/src/pages/Pulsar/Phaseogram/index.tsx new file mode 100644 index 0000000..4b64a50 --- /dev/null +++ b/src/pages/Pulsar/Phaseogram/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const PhaseogramPage: React.FC = () => { + return ( + + ); +}; + +export default PhaseogramPage; diff --git a/src/pages/QuickLook/AutoCorrelation/index.tsx b/src/pages/QuickLook/AutoCorrelation/index.tsx new file mode 100644 index 0000000..fbd25ec --- /dev/null +++ b/src/pages/QuickLook/AutoCorrelation/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const AutoCorrelationPage: React.FC = () => { + return ( + + ); +}; + +export default AutoCorrelationPage; diff --git a/src/pages/QuickLook/AvgCovarianceSpectrum/index.tsx b/src/pages/QuickLook/AvgCovarianceSpectrum/index.tsx new file mode 100644 index 0000000..740421b --- /dev/null +++ b/src/pages/QuickLook/AvgCovarianceSpectrum/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const AvgCovarianceSpectrumPage: React.FC = () => { + return ( + + ); +}; + +export default AvgCovarianceSpectrumPage; diff --git a/src/pages/QuickLook/AvgCrossSpectrum/index.tsx b/src/pages/QuickLook/AvgCrossSpectrum/index.tsx new file mode 100644 index 0000000..cad715a --- /dev/null +++ b/src/pages/QuickLook/AvgCrossSpectrum/index.tsx @@ -0,0 +1,286 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, PowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const AvgCrossSpectrumPage: React.FC = () => { + const [eventList1, setEventList1] = useState(''); + const [eventList2, setEventList2] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('16'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logX, setLogX] = useState(true); + const [logY, setLogY] = useState(true); + const [rebinFactor, setRebinFactor] = useState('0.02'); + const [logRebin, setLogRebin] = useState(true); + const [lastStoredName, setLastStoredName] = useState(null); + const lastActionRef = useRef<'create' | 'rebin'>('create'); + const { result, running, error, run } = useAnalysisRunner('Averaged Cross Spectrum'); + + useEffect(() => { + if (result?.name) { + setLastStoredName(result.name); + } else if (result && lastActionRef.current === 'create') { + setLastStoredName(null); + } + }, [result]); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const rebinNum = parsePositiveNumber(rebinFactor); + const canRun = eventList1 !== '' && eventList2 !== '' && dtNum !== null && segNum !== null && !running; + const rebinValid = logRebin ? rebinNum !== null : rebinNum !== null && rebinNum > 1; + + const handleRun = (): void => { + lastActionRef.current = 'create'; + if (!dtNum || !segNum) return; + void run(() => + spectrumApi.createAveragedCrossSpectrum({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + segment_size: segNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const handleRebin = (): void => { + lastActionRef.current = 'rebin'; + if (!lastStoredName || !rebinNum) return; + void run(() => + spectrumApi.rebinSpectrum({ name: lastStoredName, rebin_factor: rebinNum, log: logRebin }) + ); + }; + + const magnitudeTrace: Data[] = result + ? [ + { + x: result.freq, + y: result.power, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + const phaseTrace: Data[] = + result && result.power_phase + ? [ + { + x: result.freq, + y: result.power_phase, + type: 'scattergl', + mode: 'markers', + marker: { color: '#3b82f6', size: 3 }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + helperText="Required to enable rebinning" + /> + + + + {lastStoredName && ( + <> + + + Rebin '{lastStoredName}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && !rebinValid} + helperText={logRebin ? 'Each bin grows by (1 + f)' : 'Must be > 1'} + /> + { + const checked = e.target.checked; + setLogRebin(checked); + if (!checked && rebinNum !== null && rebinNum <= 1) { + setRebinFactor('2'); + } + }} + /> + } + label="Logarithmic" + /> + + + + )} + + + + + + + + + + Result + + {result?.norm && } + {result && } + {result?.segment_size !== undefined && ( + + )} + {result?.n_segments != null && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + setLogY(e.target.checked)} />} + label="log |C|" + /> + + {error && ( + + {error} + + )} + {result ? ( + + + + Cross-power magnitude + + + + {phaseTrace.length > 0 && ( + + + Cross-spectrum phase + + + + )} + + ) : ( + + + Choose two event lists and compute their averaged cross spectrum. + + + )} + + + + + + ); +}; + +export default AvgCrossSpectrumPage; diff --git a/src/pages/QuickLook/AvgPowerSpectrum/index.tsx b/src/pages/QuickLook/AvgPowerSpectrum/index.tsx new file mode 100644 index 0000000..0aeece5 --- /dev/null +++ b/src/pages/QuickLook/AvgPowerSpectrum/index.tsx @@ -0,0 +1,248 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, PowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const AvgPowerSpectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('16'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logX, setLogX] = useState(true); + const [logY, setLogY] = useState(true); + const [rebinFactor, setRebinFactor] = useState('0.02'); + const [logRebin, setLogRebin] = useState(true); + const [lastStoredName, setLastStoredName] = useState(null); + const lastActionRef = useRef<'create' | 'rebin'>('create'); + const { result, running, error, run } = useAnalysisRunner('Averaged Power Spectrum'); + + useEffect(() => { + if (result?.name) { + setLastStoredName(result.name); + } else if (result && lastActionRef.current === 'create') { + setLastStoredName(null); + } + }, [result]); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const rebinNum = parsePositiveNumber(rebinFactor); + const canRun = eventList !== '' && dtNum !== null && segNum !== null && !running; + const rebinValid = logRebin ? rebinNum !== null : rebinNum !== null && rebinNum > 1; + + const handleRun = (): void => { + lastActionRef.current = 'create'; + if (!dtNum || !segNum) return; + void run(() => + spectrumApi.createAveragedPowerSpectrum({ + event_list_name: eventList, + dt: dtNum, + segment_size: segNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const handleRebin = (): void => { + lastActionRef.current = 'rebin'; + if (!lastStoredName || !rebinNum) return; + void run(() => + spectrumApi.rebinSpectrum({ name: lastStoredName, rebin_factor: rebinNum, log: logRebin }) + ); + }; + + const plotData: Data[] = result + ? [ + { + x: result.freq, + y: result.power, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + placeholder={eventList ? `${eventList}_aps` : ''} + helperText="Required to enable rebinning" + /> + + + + {lastStoredName && ( + <> + + + Rebin '{lastStoredName}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && !rebinValid} + helperText={logRebin ? 'Each bin grows by (1 + f)' : 'Must be > 1'} + /> + { + const checked = e.target.checked; + setLogRebin(checked); + if (!checked && rebinNum !== null && rebinNum <= 1) { + setRebinFactor('2'); + } + }} + /> + } + label="Logarithmic" + /> + + + + )} + + + + + + + + + + Result + + {result?.norm && } + {result && } + {result?.n_segments != null && ( + + )} + {result?.df !== undefined && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + setLogY(e.target.checked)} />} + label="log P" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Choose an event list and compute a power spectrum. + + + )} + + + + + + ); +}; + +export default AvgPowerSpectrumPage; diff --git a/src/pages/QuickLook/Bispectrum/index.tsx b/src/pages/QuickLook/Bispectrum/index.tsx new file mode 100644 index 0000000..77520b6 --- /dev/null +++ b/src/pages/QuickLook/Bispectrum/index.tsx @@ -0,0 +1,226 @@ +import React, { useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + Tab, + Tabs, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, BispectrumData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const SCALE_OPTIONS = ['biased', 'unbiased']; +const WINDOW_OPTIONS = ['uniform', 'parzen', 'hamming', 'hanning', 'triangular', 'welch', 'blackmann', 'flat-top']; +const MAXLAG_CAP = 500; + +const BispectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.1'); + const [maxlag, setMaxlag] = useState('25'); + const [scale, setScale] = useState('unbiased'); + const [windowFn, setWindowFn] = useState('uniform'); + const [outputName, setOutputName] = useState(''); + const [tab, setTab] = useState(0); + const [logZ, setLogZ] = useState(true); + const { result, running, error, run } = useAnalysisRunner('Bispectrum'); + + const dtNum = parsePositiveNumber(dt); + const maxlagNum = parsePositiveNumber(maxlag); + const maxlagInt = maxlagNum === null ? null : Math.round(maxlagNum); + const maxlagValid = maxlagInt !== null && maxlagInt >= 1 && maxlagInt <= MAXLAG_CAP; + const canRun = eventList !== '' && dtNum !== null && maxlagValid && !running; + + const handleRun = (): void => { + if (!dtNum || !maxlagInt || !maxlagValid) return; + void run(() => + timingApi.createBispectrum({ + event_list_name: eventList, + dt: dtNum, + maxlag: maxlagInt, + scale, + window: windowFn, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const heatmapData = useMemo(() => { + if (!result) return []; + if (tab === 0) { + const z = logZ + ? result.bispec_mag.map((row) => row.map((v) => (v > 0 ? Math.log10(v) : null))) + : result.bispec_mag; + return [ + { + z, + x: result.freq, + y: result.freq, + type: 'heatmap', + colorscale: 'Viridis', + colorbar: { title: { text: logZ ? 'log10 |B|' : '|B|' } }, + } as Data, + ]; + } + return [ + { + z: result.bispec_phase, + x: result.freq, + y: result.freq, + type: 'heatmap', + colorscale: 'RdBu', + zmid: 0, + colorbar: { title: { text: 'Phase (rad)' } }, + } as Data, + ]; + }, [result, tab, logZ]); + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setMaxlag(e.target.value)} + error={maxlag !== '' && !maxlagValid} + helperText={ + maxlag !== '' && !maxlagValid + ? 'Integer between 1 and 500' + : 'Bispectrum size is (2·maxlag+1)²; keep ≤ 100' + } + /> + + Scale + + + + Window + + + setOutputName(e.target.value)} + /> + + + + + + + + + + + setTab(v)} sx={{ flexGrow: 1 }}> + + + + {result && } + {result && } + {tab === 0 && ( + setLogZ(e.target.checked)} />} + label="log color" + /> + )} + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Compute a bispectrum to see magnitude and phase maps. + + + )} + + + + + + ); +}; + +export default BispectrumPage; diff --git a/src/pages/QuickLook/Coherence/index.tsx b/src/pages/QuickLook/Coherence/index.tsx new file mode 100644 index 0000000..49ceb3c --- /dev/null +++ b/src/pages/QuickLook/Coherence/index.tsx @@ -0,0 +1,165 @@ +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControlLabel, + Grid, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, CoherenceData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const CoherencePage: React.FC = () => { + const [eventList1, setEventList1] = useState(''); + const [eventList2, setEventList2] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('16'); + const [logX, setLogX] = useState(true); + const { result, running, error, run } = useAnalysisRunner('Coherence'); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const canRun = eventList1 !== '' && eventList2 !== '' && dtNum !== null && segNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum || !segNum) return; + void run(() => + timingApi.calculateCoherence({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + segment_size: segNum, + }) + ); + }; + + const traces: Data[] = result + ? [ + { + x: result.freq, + y: result.coherence, + type: 'scattergl', + mode: 'lines+markers', + marker: { size: 4, color: '#00d4aa' }, + line: { color: '#00d4aa', width: 1 }, + error_y: result.coherence_err + ? { type: 'data', array: result.coherence_err, visible: true, color: 'rgba(0, 212, 170, 0.35)' } + : undefined, + } as Data, + ] + : []; + + return ( + + + + + + + Parameters + + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + + + + + + + + + + + Result + + {result?.n_segments != null && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Choose two event lists and compute their coherence. + + + )} + + + + + + ); +}; + +export default CoherencePage; diff --git a/src/pages/QuickLook/CovarianceSpectrum/index.tsx b/src/pages/QuickLook/CovarianceSpectrum/index.tsx new file mode 100644 index 0000000..d22648f --- /dev/null +++ b/src/pages/QuickLook/CovarianceSpectrum/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const CovarianceSpectrumPage: React.FC = () => { + return ( + + ); +}; + +export default CovarianceSpectrumPage; diff --git a/src/pages/QuickLook/CrossCorrelation/index.tsx b/src/pages/QuickLook/CrossCorrelation/index.tsx new file mode 100644 index 0000000..e05b225 --- /dev/null +++ b/src/pages/QuickLook/CrossCorrelation/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const CrossCorrelationPage: React.FC = () => { + return ( + + ); +}; + +export default CrossCorrelationPage; diff --git a/src/pages/QuickLook/CrossSpectrum/index.tsx b/src/pages/QuickLook/CrossSpectrum/index.tsx new file mode 100644 index 0000000..e51b359 --- /dev/null +++ b/src/pages/QuickLook/CrossSpectrum/index.tsx @@ -0,0 +1,269 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, PowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const CrossSpectrumPage: React.FC = () => { + const [eventList1, setEventList1] = useState(''); + const [eventList2, setEventList2] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logX, setLogX] = useState(true); + const [logY, setLogY] = useState(true); + const [rebinFactor, setRebinFactor] = useState('0.02'); + const [logRebin, setLogRebin] = useState(true); + const [lastStoredName, setLastStoredName] = useState(null); + const lastActionRef = useRef<'create' | 'rebin'>('create'); + const { result, running, error, run } = useAnalysisRunner('Cross Spectrum'); + + useEffect(() => { + if (result?.name) { + setLastStoredName(result.name); + } else if (result && lastActionRef.current === 'create') { + setLastStoredName(null); + } + }, [result]); + + const dtNum = parsePositiveNumber(dt); + const rebinNum = parsePositiveNumber(rebinFactor); + const canRun = eventList1 !== '' && eventList2 !== '' && dtNum !== null && !running; + const rebinValid = logRebin ? rebinNum !== null : rebinNum !== null && rebinNum > 1; + + const handleRun = (): void => { + lastActionRef.current = 'create'; + if (!dtNum) return; + void run(() => + spectrumApi.createCrossSpectrum({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const handleRebin = (): void => { + lastActionRef.current = 'rebin'; + if (!lastStoredName || !rebinNum) return; + void run(() => + spectrumApi.rebinSpectrum({ name: lastStoredName, rebin_factor: rebinNum, log: logRebin }) + ); + }; + + const magnitudeTrace: Data[] = result + ? [ + { + x: result.freq, + y: result.power, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + const phaseTrace: Data[] = + result && result.power_phase + ? [ + { + x: result.freq, + y: result.power_phase, + type: 'scattergl', + mode: 'markers', + marker: { color: '#3b82f6', size: 3 }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + helperText="Required to enable rebinning" + /> + + + + {lastStoredName && ( + <> + + + Rebin '{lastStoredName}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && !rebinValid} + helperText={logRebin ? 'Each bin grows by (1 + f)' : 'Must be > 1'} + /> + { + const checked = e.target.checked; + setLogRebin(checked); + if (!checked && rebinNum !== null && rebinNum <= 1) { + setRebinFactor('2'); + } + }} + /> + } + label="Logarithmic" + /> + + + + )} + + + + + + + + + + Result + + {result?.norm && } + {result && } + setLogX(e.target.checked)} />} + label="log f" + /> + setLogY(e.target.checked)} />} + label="log |C|" + /> + + {error && ( + + {error} + + )} + {result ? ( + + + + Cross-power magnitude + + + + {phaseTrace.length > 0 && ( + + + Cross-spectrum phase + + + + )} + + ) : ( + + + Choose two event lists and compute their cross spectrum. + + + )} + + + + + + ); +}; + +export default CrossSpectrumPage; diff --git a/src/pages/QuickLook/DeadTimeCorrections/index.tsx b/src/pages/QuickLook/DeadTimeCorrections/index.tsx new file mode 100644 index 0000000..aa4fb31 --- /dev/null +++ b/src/pages/QuickLook/DeadTimeCorrections/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const DeadTimeCorrectionsPage: React.FC = () => { + return ( + + ); +}; + +export default DeadTimeCorrectionsPage; diff --git a/src/pages/QuickLook/DynamicalPowerSpectrum/index.tsx b/src/pages/QuickLook/DynamicalPowerSpectrum/index.tsx new file mode 100644 index 0000000..6e438c5 --- /dev/null +++ b/src/pages/QuickLook/DynamicalPowerSpectrum/index.tsx @@ -0,0 +1,201 @@ +import React, { useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, DynamicalPowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const DynamicalPowerSpectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('8'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logZ, setLogZ] = useState(true); + const { result, running, error, run } = useAnalysisRunner( + 'Dynamical Power Spectrum' + ); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const canRun = eventList !== '' && dtNum !== null && segNum !== null && !running; + + const handleRun = (): void => { + if (!dtNum || !segNum) return; + void run(() => + spectrumApi.createDynamicalPowerSpectrum({ + event_list_name: eventList, + dt: dtNum, + segment_size: segNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + // dyn_ps rows correspond to frequencies (n_freq x n_times) — matches + // Plotly's convention that z[i] pairs with y[i]. + const zValues: Array> | undefined = useMemo( + () => + result + ? logZ + ? result.dyn_ps.map((row) => row.map((v) => (v !== null && v > 0 ? Math.log10(v) : null))) + : result.dyn_ps + : undefined, + [result, logZ] + ); + + const heatmap: Data[] = useMemo( + () => + result && zValues + ? [ + { + z: zValues, + x: result.time, + y: result.freq, + type: 'heatmap', + colorscale: 'Viridis', + colorbar: { title: { text: logZ ? 'log10 P' : 'Power' } }, + } as Data, + ] + : [], + [result, zValues, logZ] + ); + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + /> + + + + + + + + + + + + Result + + {result && ( + + )} + setLogZ(e.target.checked)} />} + label="log color" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Compute a dynamical power spectrum to see the time-frequency map. + + + )} + + + + + + ); +}; + +export default DynamicalPowerSpectrumPage; diff --git a/src/pages/QuickLook/EventList/index.test.tsx b/src/pages/QuickLook/EventList/index.test.tsx new file mode 100644 index 0000000..7c9c690 --- /dev/null +++ b/src/pages/QuickLook/EventList/index.test.tsx @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '@/test/testUtils'; + +const listEventLists = vi.fn(); +const getEventListInfo = vi.fn(); +const getEventListFullPreview = vi.fn(); +const deleteEventList = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { + listEventLists: (...a: unknown[]) => listEventLists(...a), + getEventListInfo: (...a: unknown[]) => getEventListInfo(...a), + getEventListFullPreview: (...a: unknown[]) => getEventListFullPreview(...a), + deleteEventList: (...a: unknown[]) => deleteEventList(...a), + }, +})); +vi.mock('@/components/plots/PlotlyChart', () => ({ + default: () =>
, +})); + +import EventListPage from './index'; + +describe('EventListPage', () => { + beforeEach(() => { + listEventLists.mockReset(); + getEventListInfo.mockReset(); + deleteEventList.mockReset(); + listEventLists.mockResolvedValue({ + success: true, + data: [{ name: 'obs1', n_events: 5000, time_range: [0, 100] }], + message: '', + error: null, + }); + getEventListInfo.mockResolvedValue({ + success: true, + data: { + name: 'obs1', + n_events: 5000, + time_range: [0, 100], + duration: 100, + mjdref: 56000, + gti_count: 2, + gti_list: [ + [0, 40], + [60, 100], + ], + mean_count_rate: 50, + }, + message: '', + error: null, + }); + }); + + it('lists event lists and shows details when one is selected', async () => { + renderWithProviders(); + await userEvent.click(await screen.findByText(/obs1/)); + expect(await screen.findByText('Duration (s)')).toBeInTheDocument(); + expect(getEventListInfo).toHaveBeenCalledWith('obs1'); + // GTI table rows + expect(await screen.findByText('Good Time Intervals')).toBeInTheDocument(); + }); + + it('deletes an event list via the confirmation dialog', async () => { + deleteEventList.mockResolvedValue({ + success: true, + data: { name: 'obs1' }, + message: '', + error: null, + }); + renderWithProviders(); + await userEvent.click(await screen.findByRole('button', { name: 'Delete obs1' })); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + await userEvent.click(screen.getByRole('button', { name: 'Delete' })); + expect(deleteEventList).toHaveBeenCalledWith('obs1'); + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/QuickLook/EventList/index.tsx b/src/pages/QuickLook/EventList/index.tsx new file mode 100644 index 0000000..602c879 --- /dev/null +++ b/src/pages/QuickLook/EventList/index.tsx @@ -0,0 +1,360 @@ +import React, { useState } from 'react'; +import type { Data } from 'plotly.js'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + IconButton, + List, + ListItem, + ListItemButton, + ListItemText, + Stack, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tabs, + Tooltip, + Typography, +} from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import DeleteIcon from '@mui/icons-material/Delete'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import { EVENT_LISTS_QUERY_KEY, useEventLists } from '@/hooks/useEventLists'; +import { dataApi, EventListFullPreview, EventListInfo } from '@/api/dataApi'; +import { useUIStore } from '@/store/uiStore'; + +const mono = { fontFamily: '"JetBrains Mono", monospace' }; + +const eventListInfoKey = (name: string) => ['eventListInfo', name] as const; +const eventListPreviewKey = (name: string) => ['eventListPreview', name] as const; + +const formatNum = (v: number | null | undefined, digits = 3): string => + v === null || v === undefined + ? '—' + : Number(v).toLocaleString(undefined, { maximumFractionDigits: digits }); + +const InfoRow: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( + + + {label} + + + {value} + + +); + +const EventListPage: React.FC = () => { + const [selected, setSelected] = useState(null); + const [tab, setTab] = useState(0); + const [deleteTarget, setDeleteTarget] = useState(null); + const addNotification = useUIStore((s) => s.addNotification); + const queryClient = useQueryClient(); + const { data: eventLists, isLoading, isError, error, refetch, isFetching } = useEventLists(); + + const infoQuery = useQuery({ + queryKey: eventListInfoKey(selected ?? ''), + enabled: selected !== null, + queryFn: async (): Promise => { + const res = await dataApi.getEventListInfo(selected as string); + if (!res.success || !res.data) throw new Error(res.error || res.message); + return res.data; + }, + }); + + const previewQuery = useQuery({ + queryKey: eventListPreviewKey(selected ?? ''), + enabled: selected !== null && tab === 1, + queryFn: async (): Promise => { + const res = await dataApi.getEventListFullPreview(selected as string); + if (!res.success || !res.data) throw new Error(res.error || res.message); + return res.data; + }, + }); + + const handleDelete = async (): Promise => { + if (!deleteTarget) return; + const res = await dataApi.deleteEventList(deleteTarget); + if (res.success) { + addNotification({ type: 'success', title: 'Event List', message: `Deleted '${deleteTarget}'` }); + if (selected === deleteTarget) setSelected(null); + queryClient.removeQueries({ queryKey: eventListInfoKey(deleteTarget) }); + queryClient.removeQueries({ queryKey: eventListPreviewKey(deleteTarget) }); + await queryClient.invalidateQueries({ queryKey: EVENT_LISTS_QUERY_KEY }); + } else { + addNotification({ + type: 'error', + title: 'Event List', + message: res.error || res.message || 'Delete failed', + }); + } + setDeleteTarget(null); + }; + + const info = infoQuery.data; + const preview = previewQuery.data; + + return ( + + + + + + + Loaded event lists + + + refetch()} disabled={isFetching}> + {isFetching ? : } + + + + + {isError && ( + + {error instanceof Error ? error.message : 'Failed to load'} + + )} + {isLoading && } + {!isLoading && (eventLists?.length ?? 0) === 0 && ( + + Nothing loaded yet — use Data Ingestion first. + + )} + + {(eventLists ?? []).map((ev) => ( + setDeleteTarget(ev.name)} + > + + + } + > + setSelected(ev.name)}> + + + + ))} + + + + + + + + + {!selected && ( + + + Select an event list to inspect it. + + + )} + {selected && ( + <> + + + {selected} + + {info?.mission && } + {info?.instrument && } + + setTab(v)} sx={{ mb: 2 }}> + + + + + {tab === 0 && infoQuery.isError && ( + + {infoQuery.error instanceof Error ? infoQuery.error.message : 'Failed to load info'} + + )} + {tab === 0 && infoQuery.isLoading && } + + {tab === 0 && info && ( + + + + + + + + + + + + + {(info.validation_issues ?? []) + .filter((v) => v.severity === 'error' || v.severity === 'warning') + .map((v, i) => ( + + {v.message} + + ))} + {info.notes && ( + + {info.notes} + + )} + + {(info.gti_list?.length ?? 0) > 0 && ( + + + + Good Time Intervals + + + + + + # + Start + Stop + Dur. (s) + Rate (cts/s) + + + + {(info.gti_list ?? []).map((g, i) => { + const rate = info.per_gti_rates?.[i]; + return ( + + {i + 1} + {formatNum(g[0])} + {formatNum(g[1])} + {formatNum(g[1] - g[0])} + {formatNum(rate?.rate)} + + ); + })} + +
+
+
+ )} +
+ )} + + {tab === 1 && previewQuery.isLoading && } + {tab === 1 && previewQuery.isError && ( + + {previewQuery.error instanceof Error + ? previewQuery.error.message + : 'Failed to load preview'} + + )} + {tab === 1 && preview && ( + + + + Arrival time distribution (preview sample) + + + + {preview.has_energy && preview.energy_preview && ( + + + Energy distribution (preview sample) + + + + )} + + )} + + )} +
+
+
+
+ + setDeleteTarget(null)}> + Delete event list? + + + Remove '{deleteTarget}' from backend memory? This cannot be undone. + + + + + + + +
+ ); +}; + +export default EventListPage; diff --git a/src/pages/QuickLook/ExcessVarianceSpectrum/index.tsx b/src/pages/QuickLook/ExcessVarianceSpectrum/index.tsx new file mode 100644 index 0000000..be56222 --- /dev/null +++ b/src/pages/QuickLook/ExcessVarianceSpectrum/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const ExcessVarianceSpectrumPage: React.FC = () => { + return ( + + ); +}; + +export default ExcessVarianceSpectrumPage; diff --git a/src/pages/QuickLook/LagEnergySpectrum/index.tsx b/src/pages/QuickLook/LagEnergySpectrum/index.tsx new file mode 100644 index 0000000..e58e38f --- /dev/null +++ b/src/pages/QuickLook/LagEnergySpectrum/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const LagEnergySpectrumPage: React.FC = () => { + return ( + + ); +}; + +export default LagEnergySpectrumPage; diff --git a/src/pages/QuickLook/LightCurve/index.test.tsx b/src/pages/QuickLook/LightCurve/index.test.tsx new file mode 100644 index 0000000..a66fc45 --- /dev/null +++ b/src/pages/QuickLook/LightCurve/index.test.tsx @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '@/test/testUtils'; +import { useUIStore } from '@/store/uiStore'; + +const listEventLists = vi.fn(); +vi.mock('@/api/dataApi', () => ({ + dataApi: { listEventLists: (...a: unknown[]) => listEventLists(...a) }, +})); + +const createFromEventList = vi.fn(); +const listLightcurves = vi.fn(); +const getLightcurveData = vi.fn(); +const rebin = vi.fn(); +const deleteLightcurve = vi.fn(); +vi.mock('@/api/lightcurveApi', () => ({ + lightcurveApi: { + createFromEventList: (...a: unknown[]) => createFromEventList(...a), + listLightcurves: (...a: unknown[]) => listLightcurves(...a), + getLightcurveData: (...a: unknown[]) => getLightcurveData(...a), + rebin: (...a: unknown[]) => rebin(...a), + deleteLightcurve: (...a: unknown[]) => deleteLightcurve(...a), + }, +})); +vi.mock('@/components/plots/PlotlyChart', () => ({ + default: () =>
, +})); + +import LightCurvePage from './index'; + +describe('LightCurvePage', () => { + beforeEach(() => { + useUIStore.setState({ notifications: [], unreadNotificationCount: 0 }); + listEventLists.mockResolvedValue({ + success: true, + data: [{ name: 'obs1', n_events: 5000, time_range: [0, 100] }], + message: '', + error: null, + }); + listLightcurves.mockResolvedValue({ success: true, data: [], message: '', error: null }); + createFromEventList.mockResolvedValue({ + success: true, + data: { + name: 'obs1_lc', + time: [0.5, 1.5, 2.5], + counts: [10, 12, 9], + dt: 1, + n_bins: 3, + plot_stride: 1, + count_rate_mean: 10.3, + }, + message: 'created', + error: null, + }); + }); + + it('creates a light curve with parsed parameters and plots it', async () => { + renderWithProviders(); + await userEvent.click(await screen.findByLabelText('Event list')); + await userEvent.click(await screen.findByText(/obs1/)); + const dtField = screen.getByLabelText(/Time bin/); + await userEvent.clear(dtField); + await userEvent.type(dtField, '1.0'); + await userEvent.click(screen.getByRole('button', { name: /Generate/ })); + await waitFor(() => + expect(createFromEventList).toHaveBeenCalledWith( + expect.objectContaining({ event_list_name: 'obs1', dt: 1, output_name: 'obs1_lc' }) + ) + ); + expect(await screen.findByTestId('chart')).toBeInTheDocument(); + }); + + it('disables Generate until inputs are valid', async () => { + renderWithProviders(); + const button = await screen.findByRole('button', { name: /Generate/ }); + expect(button).toBeDisabled(); + }); +}); diff --git a/src/pages/QuickLook/LightCurve/index.tsx b/src/pages/QuickLook/LightCurve/index.tsx new file mode 100644 index 0000000..d86b001 --- /dev/null +++ b/src/pages/QuickLook/LightCurve/index.tsx @@ -0,0 +1,291 @@ +import React, { useEffect, useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + Stack, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import DeleteIcon from '@mui/icons-material/Delete'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { lightcurveApi, LightcurveData, LightcurveSummary } from '@/api/lightcurveApi'; +import { parsePositiveNumber } from '@/utils/numbers'; +import { useUIStore } from '@/store/uiStore'; + +const LIGHTCURVES_QUERY_KEY = ['lightcurves'] as const; + +const LightCurvePage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('1.0'); + const [outputName, setOutputName] = useState(''); + const [existingSelection, setExistingSelection] = useState(''); + const [rebinFactor, setRebinFactor] = useState('2'); + const addNotification = useUIStore((s) => s.addNotification); + const queryClient = useQueryClient(); + const { result, running, error, run } = useAnalysisRunner('Light Curve'); + + const existingQuery = useQuery({ + queryKey: LIGHTCURVES_QUERY_KEY, + queryFn: async (): Promise => { + const res = await lightcurveApi.listLightcurves(); + if (!res.success) throw new Error(res.error || res.message); + return res.data ?? []; + }, + }); + + // Any successful create/rebin changes the stored set — refresh the list. + useEffect(() => { + if (result) void queryClient.invalidateQueries({ queryKey: LIGHTCURVES_QUERY_KEY }); + }, [result, queryClient]); + + // Auto-clear a stored-curve selection that no longer exists in the list + // (deleted elsewhere or backend restart) so we never act on stale names. + useEffect(() => { + if ( + !existingQuery.isLoading && + !existingQuery.isFetching && + existingSelection !== '' && + existingQuery.data !== undefined && + !existingQuery.data.some((lc) => lc.name === existingSelection) + ) { + setExistingSelection(''); + } + }, [existingQuery.data, existingQuery.isLoading, existingQuery.isFetching, existingSelection]); + + const dtNum = parsePositiveNumber(dt); + const canRun = eventList !== '' && dtNum !== null && !running; + const rebinNum = parsePositiveNumber(rebinFactor); + + const handleGenerate = (): void => { + if (!dtNum || !eventList) return; + const name = outputName.trim() || `${eventList}_lc`; + void run(() => + lightcurveApi.createFromEventList({ event_list_name: eventList, dt: dtNum, output_name: name }) + ); + }; + + const handleView = (): void => { + if (!existingSelection) return; + void run(() => lightcurveApi.getLightcurveData(existingSelection)); + }; + + const handleRebin = (): void => { + if (!result?.name || !rebinNum) return; + void run(() => + lightcurveApi.rebin({ + name: result.name, + rebin_factor: rebinNum, + output_name: `${result.name}_r${rebinNum}`, + }) + ); + }; + + const handleDelete = async (name: string): Promise => { + const res = await lightcurveApi.deleteLightcurve(name); + if (res.success) { + addNotification({ type: 'success', title: 'Light Curve', message: `Deleted '${name}'` }); + await queryClient.invalidateQueries({ queryKey: LIGHTCURVES_QUERY_KEY }); + } else { + addNotification({ + type: 'error', + title: 'Light Curve', + message: res.error || res.message || 'Delete failed', + }); + } + }; + + const plotData: Data[] = result + ? [ + { + x: result.time, + y: result.counts, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + return ( + + + + + + + Generate from event list + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setOutputName(e.target.value)} + placeholder={eventList ? `${eventList}_lc` : 'name'} + /> + + + + {result?.name && ( + <> + + + Rebin '{result.name}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && rebinNum === null} + helperText={ + rebinFactor !== '' && rebinNum === null ? 'Must be a positive number' : ' ' + } + /> + + + + )} + + + + Stored light curves + + Light curve + + + + + + + { + void handleDelete(existingSelection); + setExistingSelection(''); + }} + > + + + + + + + + + + + + + + + + {result?.name ? `Light curve: ${result.name}` : 'Result'} + + {result && } + {result && } + {result?.count_rate_mean !== undefined && ( + + )} + + {result?.plot_stride !== undefined && result.plot_stride > 1 && ( + + Showing 1 of every {result.plot_stride} bins for display performance (full + resolution is stored in the backend). + + )} + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Generate a light curve or view a stored one. + + + )} + + + + + + ); +}; + +export default LightCurvePage; diff --git a/src/pages/QuickLook/PowerColors/index.tsx b/src/pages/QuickLook/PowerColors/index.tsx new file mode 100644 index 0000000..d1b817a --- /dev/null +++ b/src/pages/QuickLook/PowerColors/index.tsx @@ -0,0 +1,252 @@ +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + CircularProgress, + Grid, + Stack, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, PowerColorsData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; +import { computePowerColorRatios } from '@/utils/powerColors'; + +interface BandInput { + label: string; + fmin: string; + fmax: string; +} + +// Heil et al. (2015) bands; require dt <= 1/(2*16) s for the top band. +const DEFAULT_BANDS: BandInput[] = [ + { label: 'A', fmin: '0.0039', fmax: '0.031' }, + { label: 'B', fmin: '0.031', fmax: '0.25' }, + { label: 'C', fmin: '0.25', fmax: '2.0' }, + { label: 'D', fmin: '2.0', fmax: '16.0' }, +]; + +const BAND_COLORS = ['#00d4aa', '#3b82f6', '#f59e0b', '#ef4444']; + +const PowerColorsPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.03125'); + const [segmentSize, setSegmentSize] = useState('64'); + const [bands, setBands] = useState(DEFAULT_BANDS); + const { result, running, error, run } = useAnalysisRunner('Power Colors'); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + + const parsedBands = bands.map((b) => ({ + label: b.label, + fmin: parsePositiveNumber(b.fmin), + fmax: parsePositiveNumber(b.fmax), + })); + const bandsValid = parsedBands.every( + (b) => b.fmin !== null && b.fmax !== null && b.fmax > b.fmin + ); + const nyquist = dtNum !== null ? 1 / (2 * dtNum) : null; + const bandExceedsNyquist = + nyquist !== null && parsedBands.some((b) => b.fmax !== null && b.fmax > nyquist); + const canRun = eventList !== '' && dtNum !== null && segNum !== null && bandsValid && !running; + + const updateBand = (index: number, field: 'fmin' | 'fmax', value: string): void => { + setBands((prev) => prev.map((b, i) => (i === index ? { ...b, [field]: value } : b))); + }; + + const handleRun = (): void => { + if (!dtNum || !segNum || !bandsValid) return; + const freq_ranges: Record = {}; + for (const b of parsedBands) { + freq_ranges[b.label] = [b.fmin as number, b.fmax as number]; + } + void run(() => + timingApi.calculatePowerColors({ + event_list_name: eventList, + dt: dtNum, + segment_size: segNum, + freq_ranges, + }) + ); + }; + + const bandTraces: Data[] = result + ? Object.entries(result.power_colors).map(([label, values], i) => ({ + x: result.time, + y: values, + type: 'scattergl' as const, + mode: 'lines+markers' as const, + marker: { size: 4, color: BAND_COLORS[i % BAND_COLORS.length] }, + line: { width: 1, color: BAND_COLORS[i % BAND_COLORS.length] }, + name: label, + })) + : []; + + const ratios = result + ? computePowerColorRatios( + result.power_colors, + bands.map((b) => b.label) + ) + : null; + + const scatterTrace: Data[] = ratios + ? [ + { + x: ratios.pc1, + y: ratios.pc2, + type: 'scattergl' as const, + mode: 'markers' as const, + marker: { size: 6, color: '#00d4aa' }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText="Nyquist must cover the highest band" + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText="Must exceed 1/f_min of the lowest band" + /> + Frequency bands (Hz) + {bands.map((b, i) => ( + + + {b.label} + + updateBand(i, 'fmin', e.target.value)} + /> + updateBand(i, 'fmax', e.target.value)} + /> + + ))} + {!bandsValid && ( + Each band needs 0 < f min < f max. + )} + {bandExceedsNyquist && ( + + A band's f max exceeds the Nyquist frequency 1/(2·dt); frequency bins above it are + dropped. + + )} + + + + + + + + + + {error && ( + + {error} + + )} + {result ? ( + + + + Band power vs time + + + + {ratios && ratios.pc1.length > 0 && ( + + + Power-color diagram (PC1 = C/A, PC2 = B/D) + + + Ratios use band-mean (not band-integrated) power: PC tracks match literature power + colors up to constant per-band factors, so absolute values are not comparable to + published hue diagrams. + + + + )} + + ) : ( + + + Compute band powers to populate the power-color diagram. + + + )} + + + + + + ); +}; + +export default PowerColorsPage; diff --git a/src/pages/QuickLook/PowerSpectrum/index.tsx b/src/pages/QuickLook/PowerSpectrum/index.tsx new file mode 100644 index 0000000..3a7aa5d --- /dev/null +++ b/src/pages/QuickLook/PowerSpectrum/index.tsx @@ -0,0 +1,234 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Divider, + FormControl, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Select, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { spectrumApi, PowerSpectrumData } from '@/api/spectrumApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const NORM_OPTIONS = ['leahy', 'frac', 'abs', 'none']; + +const PowerSpectrumPage: React.FC = () => { + const [eventList, setEventList] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [norm, setNorm] = useState('leahy'); + const [outputName, setOutputName] = useState(''); + const [logX, setLogX] = useState(true); + const [logY, setLogY] = useState(true); + const [rebinFactor, setRebinFactor] = useState('0.02'); + const [logRebin, setLogRebin] = useState(true); + const [lastStoredName, setLastStoredName] = useState(null); + const lastActionRef = useRef<'create' | 'rebin'>('create'); + const { result, running, error, run } = useAnalysisRunner('Power Spectrum'); + + useEffect(() => { + if (result?.name) { + setLastStoredName(result.name); + } else if (result && lastActionRef.current === 'create') { + setLastStoredName(null); + } + }, [result]); + + const dtNum = parsePositiveNumber(dt); + const rebinNum = parsePositiveNumber(rebinFactor); + const canRun = eventList !== '' && dtNum !== null && !running; + const rebinValid = logRebin ? rebinNum !== null : rebinNum !== null && rebinNum > 1; + + const handleRun = (): void => { + lastActionRef.current = 'create'; + if (!dtNum) return; + void run(() => + spectrumApi.createPowerSpectrum({ + event_list_name: eventList, + dt: dtNum, + norm, + output_name: outputName.trim() || undefined, + }) + ); + }; + + const handleRebin = (): void => { + lastActionRef.current = 'rebin'; + if (!lastStoredName || !rebinNum) return; + void run(() => + spectrumApi.rebinSpectrum({ name: lastStoredName, rebin_factor: rebinNum, log: logRebin }) + ); + }; + + const plotData: Data[] = result + ? [ + { + x: result.freq, + y: result.power, + type: 'scattergl', + mode: 'lines', + line: { color: '#00d4aa', width: 1 }, + }, + ] + : []; + + return ( + + + + + + + Parameters + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + + Normalization + + + setOutputName(e.target.value)} + placeholder={eventList ? `${eventList}_ps` : ''} + helperText="Required to enable rebinning" + /> + + + + {lastStoredName && ( + <> + + + Rebin '{lastStoredName}' + setRebinFactor(e.target.value)} + error={rebinFactor !== '' && !rebinValid} + helperText={logRebin ? 'Each bin grows by (1 + f)' : 'Must be > 1'} + /> + { + const checked = e.target.checked; + setLogRebin(checked); + if (!checked && rebinNum !== null && rebinNum <= 1) { + setRebinFactor('2'); + } + }} + /> + } + label="Logarithmic" + /> + + + + )} + + + + + + + + + + Result + + {result?.norm && } + {result && } + {result?.df !== undefined && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + setLogY(e.target.checked)} />} + label="log P" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Choose an event list and compute a power spectrum. + + + )} + + + + + + ); +}; + +export default PowerSpectrumPage; diff --git a/src/pages/QuickLook/RmsEnergySpectrum/index.tsx b/src/pages/QuickLook/RmsEnergySpectrum/index.tsx new file mode 100644 index 0000000..06673c8 --- /dev/null +++ b/src/pages/QuickLook/RmsEnergySpectrum/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const RmsEnergySpectrumPage: React.FC = () => { + return ( + + ); +}; + +export default RmsEnergySpectrumPage; diff --git a/src/pages/QuickLook/TimeLags/index.tsx b/src/pages/QuickLook/TimeLags/index.tsx new file mode 100644 index 0000000..12a3298 --- /dev/null +++ b/src/pages/QuickLook/TimeLags/index.tsx @@ -0,0 +1,213 @@ +import React, { useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + FormControlLabel, + Grid, + Stack, + Switch, + TextField, + Typography, +} from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import type { Data } from 'plotly.js'; +import PageTemplate from '@/components/common/PageTemplate'; +import PlotlyChart from '@/components/plots/PlotlyChart'; +import EventListSelector from '@/components/analysis/EventListSelector'; +import { useAnalysisRunner } from '@/hooks/useAnalysisRunner'; +import { timingApi, TimeLagsData } from '@/api/timingApi'; +import { parsePositiveNumber } from '@/utils/numbers'; + +const TimeLagsPage: React.FC = () => { + const [eventList1, setEventList1] = useState(''); + const [eventList2, setEventList2] = useState(''); + const [dt, setDt] = useState('0.0625'); + const [segmentSize, setSegmentSize] = useState('16'); + const [freqMin, setFreqMin] = useState(''); + const [freqMax, setFreqMax] = useState(''); + const [logX, setLogX] = useState(true); + const { result, running, error, run } = useAnalysisRunner('Time Lags'); + + const dtNum = parsePositiveNumber(dt); + const segNum = parsePositiveNumber(segmentSize); + const fMin = parsePositiveNumber(freqMin); + const fMax = parsePositiveNumber(freqMax); + const freqMinInvalid = freqMin !== '' && fMin === null; + const freqMaxInvalid = freqMax !== '' && fMax === null; + const freqRangePartial = (fMin !== null) !== (fMax !== null); + const freqRangeInverted = fMin !== null && fMax !== null && fMax <= fMin; + const freqRangeValid = + !freqMinInvalid && !freqMaxInvalid && !freqRangePartial && !freqRangeInverted; + const canRun = + eventList1 !== '' && + eventList2 !== '' && + dtNum !== null && + segNum !== null && + freqRangeValid && + !running; + + const freqHelperText = (raw: string, parsed: number | null): string => { + if (raw !== '' && parsed === null) return 'Must be a positive number'; + if (freqRangePartial) return 'Fill both or leave both blank'; + if (freqRangeInverted) return 'f max must be > f min'; + return ' '; + }; + + const handleRun = (): void => { + if (!dtNum || !segNum) return; + const freq_range: [number, number] | undefined = + fMin !== null && fMax !== null && fMax > fMin ? [fMin, fMax] : undefined; + void run(() => + timingApi.calculateTimeLags({ + event_list_1_name: eventList1, + event_list_2_name: eventList2, + dt: dtNum, + segment_size: segNum, + freq_range, + }) + ); + }; + + const traces: Data[] = result + ? [ + { + x: result.freq, + y: result.time_lags, + type: 'scattergl', + mode: 'lines+markers', + marker: { size: 4, color: '#00d4aa' }, + line: { color: '#00d4aa', width: 1 }, + error_y: result.time_lags_err + ? { type: 'data', array: result.time_lags_err, visible: true, color: 'rgba(0, 212, 170, 0.35)' } + : undefined, + } as Data, + ] + : []; + + return ( + + + + + + + Parameters + + + setDt(e.target.value)} + error={dt !== '' && dtNum === null} + helperText={dt !== '' && dtNum === null ? 'Must be a positive number' : ' '} + /> + setSegmentSize(e.target.value)} + error={segmentSize !== '' && segNum === null} + helperText={segmentSize !== '' && segNum === null ? 'Must be a positive number' : ' '} + /> + + setFreqMin(e.target.value)} + error={freqMinInvalid || freqRangePartial || freqRangeInverted} + helperText={freqHelperText(freqMin, fMin)} + /> + setFreqMax(e.target.value)} + error={freqMaxInvalid || freqRangePartial || freqRangeInverted} + helperText={freqHelperText(freqMax, fMax)} + /> + + + + + + + + + + + + + Result + + {result?.freq_range && ( + + )} + setLogX(e.target.checked)} />} + label="log f" + /> + + {error && ( + + {error} + + )} + {result ? ( + + ) : ( + + + Choose two event lists and compute their time lags. + + + )} + + + + + + ); +}; + +export default TimeLagsPage; diff --git a/src/pages/QuickLook/VariableEnergySpectrum/index.tsx b/src/pages/QuickLook/VariableEnergySpectrum/index.tsx new file mode 100644 index 0000000..ac3967c --- /dev/null +++ b/src/pages/QuickLook/VariableEnergySpectrum/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const VariableEnergySpectrumPage: React.FC = () => { + return ( + + ); +}; + +export default VariableEnergySpectrumPage; diff --git a/src/pages/Simulator/index.tsx b/src/pages/Simulator/index.tsx new file mode 100644 index 0000000..c0fecad --- /dev/null +++ b/src/pages/Simulator/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const SimulatorPage: React.FC = () => { + return ( + + ); +}; + +export default SimulatorPage; diff --git a/src/pages/Utilities/GTI/index.tsx b/src/pages/Utilities/GTI/index.tsx new file mode 100644 index 0000000..c76beef --- /dev/null +++ b/src/pages/Utilities/GTI/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const GTIPage: React.FC = () => { + return ( + + ); +}; + +export default GTIPage; diff --git a/src/pages/Utilities/IO/index.tsx b/src/pages/Utilities/IO/index.tsx new file mode 100644 index 0000000..7d84553 --- /dev/null +++ b/src/pages/Utilities/IO/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const IOPage: React.FC = () => { + return ( + + ); +}; + +export default IOPage; diff --git a/src/pages/Utilities/Misc/index.tsx b/src/pages/Utilities/Misc/index.tsx new file mode 100644 index 0000000..e7ba763 --- /dev/null +++ b/src/pages/Utilities/Misc/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const MiscPage: React.FC = () => { + return ( + + ); +}; + +export default MiscPage; diff --git a/src/pages/Utilities/MissionIO/index.tsx b/src/pages/Utilities/MissionIO/index.tsx new file mode 100644 index 0000000..44ce258 --- /dev/null +++ b/src/pages/Utilities/MissionIO/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const MissionIOPage: React.FC = () => { + return ( + + ); +}; + +export default MissionIOPage; diff --git a/src/pages/Utilities/StatisticalFunctions/index.tsx b/src/pages/Utilities/StatisticalFunctions/index.tsx new file mode 100644 index 0000000..f4a98ca --- /dev/null +++ b/src/pages/Utilities/StatisticalFunctions/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PageTemplate from '@/components/common/PageTemplate'; + +const StatisticalFunctionsPage: React.FC = () => { + return ( + + ); +}; + +export default StatisticalFunctionsPage; diff --git a/src/store/jobStore.ts b/src/store/jobStore.ts new file mode 100644 index 0000000..4406901 --- /dev/null +++ b/src/store/jobStore.ts @@ -0,0 +1,221 @@ +/** + * Zustand store for managing background job state. + * + * This store tracks all jobs in the queue, including their status, + * progress, and results. It receives updates from the SSE stream + * and provides methods for managing jobs. + */ + +import { create } from 'zustand'; +import type { Job, JobStatus, JobStreamEvent } from '@/types/job'; + +interface JobStore { + /** All jobs indexed by ID */ + jobs: Record; + + /** Whether the SSE connection is active */ + isConnected: boolean; + + /** Connection error message, if any */ + connectionError: string | null; + + /** Reconnection attempt count */ + reconnectAttempts: number; + + // Actions + + /** Set SSE connection status */ + setConnected: (connected: boolean, error?: string | null) => void; + + /** Increment reconnection attempts */ + incrementReconnectAttempts: () => void; + + /** Reset reconnection attempts */ + resetReconnectAttempts: () => void; + + /** Add or update a job */ + upsertJob: (job: Job) => void; + + /** Update an existing job */ + updateJob: (jobId: string, updates: Partial) => void; + + /** Remove a job */ + removeJob: (jobId: string) => void; + + /** Handle an SSE event */ + handleEvent: (event: JobStreamEvent) => void; + + /** Clear all completed/failed/cancelled jobs */ + clearCompletedJobs: () => void; + + /** Set multiple jobs at once (for initial state) */ + setJobs: (jobs: Job[]) => void; + + // Computed selectors (as functions) + + /** Get all jobs as an array, sorted by creation time (newest first) */ + getJobsArray: () => Job[]; + + /** Get active (pending or running) jobs */ + getActiveJobs: () => Job[]; + + /** Get completed jobs (completed, failed, or cancelled) */ + getCompletedJobs: () => Job[]; + + /** Get count of active jobs */ + getActiveJobCount: () => number; + + /** Get a specific job by ID */ + getJob: (jobId: string) => Job | undefined; +} + +const isActiveStatus = (status: JobStatus): boolean => { + return status === 'pending' || status === 'running'; +}; + +const isCompletedStatus = (status: JobStatus): boolean => { + return status === 'completed' || status === 'failed' || status === 'cancelled'; +}; + +export const useJobStore = create((set, get) => ({ + // Initial state + jobs: {}, + isConnected: false, + connectionError: null, + reconnectAttempts: 0, + + // Actions + setConnected: (connected: boolean, error: string | null = null): void => { + set({ isConnected: connected, connectionError: error }); + }, + + incrementReconnectAttempts: (): void => { + set((state) => ({ reconnectAttempts: state.reconnectAttempts + 1 })); + }, + + resetReconnectAttempts: (): void => { + set({ reconnectAttempts: 0 }); + }, + + upsertJob: (job: Job): void => { + set((state) => ({ + jobs: { ...state.jobs, [job.id]: job }, + })); + }, + + updateJob: (jobId: string, updates: Partial): void => { + set((state) => { + const existing = state.jobs[jobId]; + if (!existing) return state; + + return { + jobs: { + ...state.jobs, + [jobId]: { ...existing, ...updates }, + }, + }; + }); + }, + + removeJob: (jobId: string): void => { + set((state) => { + const { [jobId]: _, ...rest } = state.jobs; + return { jobs: rest }; + }); + }, + + handleEvent: (event: JobStreamEvent): void => { + const { type } = event; + + switch (type) { + case 'initial_state': { + // Set all jobs from initial state + const jobsMap: Record = {}; + for (const job of event.jobs) { + jobsMap[job.id] = job; + } + set({ jobs: jobsMap }); + break; + } + + case 'job_created': + case 'job_started': + case 'job_progress': + case 'job_completed': + case 'job_failed': + case 'job_cancelled': { + // Update the job + set((state) => ({ + jobs: { ...state.jobs, [event.job.id]: event.job }, + })); + break; + } + + case 'heartbeat': + // Heartbeat doesn't change state, just confirms connection is alive + break; + + default: + console.warn('[JobStore] Unknown event type:', type); + } + }, + + clearCompletedJobs: (): void => { + set((state) => { + const filtered: Record = {}; + for (const [id, job] of Object.entries(state.jobs)) { + if (!isCompletedStatus(job.status)) { + filtered[id] = job; + } + } + return { jobs: filtered }; + }); + }, + + setJobs: (jobs: Job[]): void => { + const jobsMap: Record = {}; + for (const job of jobs) { + jobsMap[job.id] = job; + } + set({ jobs: jobsMap }); + }, + + // Computed selectors + getJobsArray: (): Job[] => { + const { jobs } = get(); + return Object.values(jobs).sort((a, b) => { + // Sort by created_at descending (newest first) + return b.created_at.localeCompare(a.created_at); + }); + }, + + getActiveJobs: (): Job[] => { + const { jobs } = get(); + return Object.values(jobs) + .filter((job) => isActiveStatus(job.status)) + .sort((a, b) => b.created_at.localeCompare(a.created_at)); + }, + + getCompletedJobs: (): Job[] => { + const { jobs } = get(); + return Object.values(jobs) + .filter((job) => isCompletedStatus(job.status)) + .sort((a, b) => { + // Sort by completed_at descending (newest first) + const aTime = a.completed_at || a.created_at; + const bTime = b.completed_at || b.created_at; + return bTime.localeCompare(aTime); + }); + }, + + getActiveJobCount: (): number => { + const { jobs } = get(); + return Object.values(jobs).filter((job) => isActiveStatus(job.status)).length; + }, + + getJob: (jobId: string): Job | undefined => { + return get().jobs[jobId]; + }, +})); + +export default useJobStore; diff --git a/src/store/logStore.ts b/src/store/logStore.ts new file mode 100644 index 0000000..cc72346 --- /dev/null +++ b/src/store/logStore.ts @@ -0,0 +1,161 @@ +import { create } from 'zustand'; + +/** + * Log entry type + */ +export interface LogEntry { + id: string; + timestamp: Date; + level: 'info' | 'warn' | 'error' | 'debug'; + source: 'python' | 'electron' | 'frontend'; + message: string; +} + +/** + * Log store state interface + */ +interface LogStoreState { + logs: LogEntry[]; + maxLogs: number; + isOpen: boolean; + filter: { + levels: Set; + sources: Set; + search: string; + }; +} + +/** + * Log store actions interface + */ +interface LogStoreActions { + addLog: (log: Omit) => void; + clearLogs: () => void; + togglePanel: () => void; + setOpen: (isOpen: boolean) => void; + setFilter: (filter: Partial) => void; + toggleLevelFilter: (level: LogEntry['level']) => void; + toggleSourceFilter: (source: LogEntry['source']) => void; + setSearchFilter: (search: string) => void; +} + +type LogStore = LogStoreState & LogStoreActions; + +/** + * Generate unique ID for log entries + */ +const generateId = (): string => { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Zustand store for application logs + */ +export const useLogStore = create((set) => ({ + logs: [], + maxLogs: 1000, + isOpen: false, + filter: { + levels: new Set(['info', 'warn', 'error', 'debug']), + sources: new Set(['python', 'electron', 'frontend']), + search: '', + }, + + addLog: (log): void => { + set((state) => { + const newLog: LogEntry = { + ...log, + id: generateId(), + timestamp: new Date(), + }; + + // Keep only the last maxLogs entries + const logs = [...state.logs, newLog]; + if (logs.length > state.maxLogs) { + logs.shift(); + } + + return { logs }; + }); + }, + + clearLogs: (): void => { + set({ logs: [] }); + }, + + togglePanel: (): void => { + set((state) => ({ isOpen: !state.isOpen })); + }, + + setOpen: (isOpen): void => { + set({ isOpen }); + }, + + setFilter: (filter): void => { + set((state) => ({ + filter: { ...state.filter, ...filter }, + })); + }, + + toggleLevelFilter: (level): void => { + set((state) => { + const levels = new Set(state.filter.levels); + if (levels.has(level)) { + levels.delete(level); + } else { + levels.add(level); + } + return { filter: { ...state.filter, levels } }; + }); + }, + + toggleSourceFilter: (source): void => { + set((state) => { + const sources = new Set(state.filter.sources); + if (sources.has(source)) { + sources.delete(source); + } else { + sources.add(source); + } + return { filter: { ...state.filter, sources } }; + }); + }, + + setSearchFilter: (search): void => { + set((state) => ({ + filter: { ...state.filter, search }, + })); + }, +})); + +/** + * Selector for filtered logs + */ +export const selectFilteredLogs = (state: LogStore): LogEntry[] => { + return state.logs.filter((log) => { + // Filter by level + if (!state.filter.levels.has(log.level)) { + return false; + } + + // Filter by source + if (!state.filter.sources.has(log.source)) { + return false; + } + + // Filter by search text + if (state.filter.search) { + const searchLower = state.filter.search.toLowerCase(); + return log.message.toLowerCase().includes(searchLower); + } + + return true; + }); +}; + +/** + * Helper function to add logs from outside React components + */ +export const addLog = (log: Omit): void => { + useLogStore.getState().addLog(log); +}; diff --git a/src/store/uiStore.ts b/src/store/uiStore.ts new file mode 100644 index 0000000..61430fe --- /dev/null +++ b/src/store/uiStore.ts @@ -0,0 +1,140 @@ +/** + * UI state store for global application state + */ + +import { create } from 'zustand'; + +export interface ProcessResources { + memoryMb: number; + cpuPercent: number; +} + +export interface AppResources { + // Individual process resources + backend: ProcessResources | null; + electronMain: ProcessResources | null; + electronRenderer: ProcessResources | null; + // Combined totals (what the app is actually using) + totalMemoryMb: number; + totalCpuPercent: number; // Raw sum (can exceed 100% on multi-core) + // System reference info + systemMemoryTotalMb: number; + systemMemoryAvailableMb: number; + systemCpuCount: number; + // Calculated app percentage of system capacity + appMemoryPercent: number; + appCpuPercent: number; // Normalized to total system CPU capacity (0-100%) +} + +// Legacy interface for backwards compatibility +export interface SystemResources { + cpuPercent: number; + memoryUsedGb: number; + memoryTotalGb: number; + memoryPercent: number; +} + +interface UIState { + // Panel visibility + logPanelOpen: boolean; + rightToolbarCollapsed: boolean; + + // Processing state + isProcessing: boolean; + processingMessage: string; + processingProgress: number | null; // null = indeterminate + + // System resources (legacy) + systemResources: SystemResources | null; + // App-specific resources (new) + appResources: AppResources | null; + + // Notifications + notifications: Notification[]; + unreadNotificationCount: number; + + // Search + searchOpen: boolean; + searchQuery: string; + + // Actions + toggleLogPanel: () => void; + setLogPanelOpen: (open: boolean) => void; + toggleRightToolbar: () => void; + + setProcessing: (isProcessing: boolean, message?: string, progress?: number | null) => void; + + setSystemResources: (resources: SystemResources | null) => void; + setAppResources: (resources: AppResources | null) => void; + + addNotification: (notification: Omit) => void; + markNotificationRead: (id: string) => void; + clearNotifications: () => void; + + setSearchOpen: (open: boolean) => void; + setSearchQuery: (query: string) => void; +} + +/** Notification type for UI alerts */ +export type NotificationType = 'info' | 'success' | 'warning' | 'error'; + +export interface Notification { + id: string; + timestamp: Date; + type: NotificationType; + title: string; + message: string; + read: boolean; +} + +export const useUIStore = create((set) => ({ + // Initial state + logPanelOpen: false, + rightToolbarCollapsed: false, + isProcessing: false, + processingMessage: '', + processingProgress: null, + systemResources: null, + appResources: null, + notifications: [], + unreadNotificationCount: 0, + searchOpen: false, + searchQuery: '', + + // Actions + toggleLogPanel: () => set((state) => ({ logPanelOpen: !state.logPanelOpen })), + setLogPanelOpen: (open) => set({ logPanelOpen: open }), + toggleRightToolbar: () => set((state) => ({ rightToolbarCollapsed: !state.rightToolbarCollapsed })), + + setProcessing: (isProcessing, message = '', progress = null) => + set({ isProcessing, processingMessage: message, processingProgress: progress }), + + setSystemResources: (resources) => set({ systemResources: resources }), + setAppResources: (resources) => set({ appResources: resources }), + + addNotification: (notification) => set((state) => { + const newNotification: Notification = { + ...notification, + id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date(), + read: false, + }; + return { + notifications: [newNotification, ...state.notifications].slice(0, 50), // Keep last 50 + unreadNotificationCount: state.unreadNotificationCount + 1, + }; + }), + + markNotificationRead: (id) => set((state) => { + const notifications = state.notifications.map((n) => + n.id === id ? { ...n, read: true } : n + ); + const unreadCount = notifications.filter((n) => !n.read).length; + return { notifications, unreadNotificationCount: unreadCount }; + }), + + clearNotifications: () => set({ notifications: [], unreadNotificationCount: 0 }), + + setSearchOpen: (open) => set({ searchOpen: open }), + setSearchQuery: (query) => set({ searchQuery: query }), +})); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..42fc380 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,16 @@ +import '@testing-library/jest-dom/vitest'; + +// MUI useMediaQuery requires matchMedia, absent in jsdom +if (!window.matchMedia) { + window.matchMedia = (query: string): MediaQueryList => + ({ + matches: false, + media: query, + onchange: null, + addListener: () => undefined, + removeListener: () => undefined, + addEventListener: () => undefined, + removeEventListener: () => undefined, + dispatchEvent: () => false, + }) as MediaQueryList; +} diff --git a/src/test/testUtils.tsx b/src/test/testUtils.tsx new file mode 100644 index 0000000..3ffe01e --- /dev/null +++ b/src/test/testUtils.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +/** Render with the app's provider stack (React Query + Router) for component tests. */ +export function renderWithProviders(ui: React.ReactElement): RenderResult { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: 0 } }, + }); + return render(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts new file mode 100644 index 0000000..16ec929 --- /dev/null +++ b/src/types/electron.d.ts @@ -0,0 +1,105 @@ +/** + * Type definitions for Electron API exposed via preload script + */ + +export interface ElectronAPI { + // File System Operations + openFile: (options?: { + title?: string; + filters?: { name: string; extensions: string[] }[]; + multiple?: boolean; + }) => Promise; + + saveFile: (options?: { + title?: string; + defaultPath?: string; + filters?: { name: string; extensions: string[] }[]; + }) => Promise; + + openDirectory: () => Promise; + + readFile: (filePath: string) => Promise; + + writeFile: (filePath: string, data: ArrayBuffer | string) => Promise; + + fileExists: (filePath: string) => Promise; + + // Python Backend Communication + getBackendPort: () => Promise; + + isPythonRunning: () => Promise; + + restartPython: () => Promise; + + onPythonReady: (callback: (port: number) => void) => () => void; + + onPythonStarting: (callback: () => void) => () => void; + + onPythonError: (callback: (error: string) => void) => () => void; + + // Application Info + getAppVersion: () => Promise; + + getAppName: () => Promise; + + getPlatform: () => Promise; + + isDev: () => Promise; + + // Window Controls + minimizeWindow: () => void; + + maximizeWindow: () => void; + + closeWindow: () => void; + + toggleFullscreen: () => void; + + openDevTools: () => void; + + // Shell Operations + openExternal: (url: string) => Promise; + + showItemInFolder: (path: string) => void; + + // Clipboard Operations + copyToClipboard: (text: string) => void; + + readFromClipboard: () => Promise; + + // Log Events + onLog: ( + callback: (log: { + level: 'info' | 'warn' | 'error' | 'debug'; + source: 'python' | 'electron'; + message: string; + }) => void + ) => () => void; + + sendLog: (log: { level: 'info' | 'warn' | 'error' | 'debug'; message: string }) => void; + + signalLogReady: () => void; + + // Resource Monitoring + getElectronResources: () => Promise<{ + main: { + memory_mb: number; + heap_used_mb: number; + heap_total_mb: number; + cpu_percent?: number; + }; + renderer: { + memory_mb: number; + cpu_percent: number; + } | null; + timestamp: number; + }>; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} + +export {}; diff --git a/src/types/job.ts b/src/types/job.ts new file mode 100644 index 0000000..12e9f14 --- /dev/null +++ b/src/types/job.ts @@ -0,0 +1,179 @@ +/** + * TypeScript types for the background job queue system. + */ + +/** + * Job status enum values. + */ +export type JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + +/** + * Job type enum values. + */ +export type JobType = 'load_event_list' | 'load_batch' | 'load_from_url'; + +/** + * Represents a background job in the queue. + */ +export interface Job { + /** Unique identifier for the job (UUID) */ + id: string; + /** Type of job */ + type: JobType; + /** Current status of the job */ + status: JobStatus; + /** Progress percentage (0.0 to 1.0) */ + progress: number; + /** Human-readable progress message */ + progress_message: string; + /** Total number of items to process (for batch jobs) */ + total_items: number; + /** Number of items completed */ + completed_items: number; + /** ISO timestamp when job was created */ + created_at: string; + /** ISO timestamp when job started running */ + started_at: string | null; + /** ISO timestamp when job completed/failed/cancelled */ + completed_at: string | null; + /** Job-specific parameters */ + params: Record; + /** Result data on successful completion */ + result: Record | null; + /** Error message on failure */ + error: string | null; + /** Human-readable name for the job (shown in UI) */ + display_name: string; +} + +/** + * SSE event types for job updates. + */ +export type JobEventType = + | 'initial_state' + | 'job_created' + | 'job_started' + | 'job_progress' + | 'job_completed' + | 'job_failed' + | 'job_cancelled' + | 'heartbeat'; + +/** + * Base SSE event structure. + */ +interface BaseJobStreamEvent { + type: JobEventType; + timestamp: string; +} + +/** + * Initial state event sent when SSE connection is established. + */ +export interface InitialStateEvent extends BaseJobStreamEvent { + type: 'initial_state'; + jobs: Job[]; +} + +/** + * Job update event (created, started, progress, completed, failed, cancelled). + */ +export interface JobUpdateEvent extends BaseJobStreamEvent { + type: 'job_created' | 'job_started' | 'job_progress' | 'job_completed' | 'job_failed' | 'job_cancelled'; + job: Job; +} + +/** + * Heartbeat event to keep connection alive. + */ +export interface HeartbeatEvent extends BaseJobStreamEvent { + type: 'heartbeat'; +} + +/** + * Union type for all SSE events. + */ +export type JobStreamEvent = InitialStateEvent | JobUpdateEvent | HeartbeatEvent; + +/** + * Name conflict check result. + */ +export interface NameConflictResult { + has_conflict: boolean; + conflict_source?: 'loaded_data' | 'pending_job'; + job_id?: string; + suggested_name?: string; +} + +/** + * Request parameters for submitting a single file load job. + */ +export interface SubmitLoadJobParams { + file_path: string; + name: string; + fmt?: string; + rmf_file?: string; + additional_columns?: string[]; + high_precision?: boolean; + skip_checks?: boolean; + notes?: string; + use_partial_loading?: boolean; + partial_mode?: 'time_range' | 'event_count'; + time_range_start?: number; + time_range_end?: number; + event_start_index?: number; + event_count?: number; +} + +/** + * File configuration for batch loading. + */ +export interface BatchFileConfig { + file_path: string; + name: string; + fmt?: string; + rmf_file?: string; + additional_columns?: string[]; + high_precision?: boolean; + skip_checks?: boolean; + use_partial_loading?: boolean; + partial_mode?: 'time_range' | 'event_count'; + time_range_start?: number; + time_range_end?: number; + event_start_index?: number; + event_count?: number; + notes?: string; +} + +/** + * Request parameters for submitting a batch load job. + */ +export interface SubmitBatchJobParams { + files: BatchFileConfig[]; + use_same_settings?: boolean; + shared_fmt?: string; + shared_rmf_file?: string; + shared_additional_columns?: string[]; + shared_high_precision?: boolean; + shared_skip_checks?: boolean; + shared_use_partial_loading?: boolean; + shared_partial_mode?: 'time_range' | 'event_count'; + shared_time_range_start?: number; + shared_time_range_end?: number; + shared_event_start_index?: number; + shared_event_count?: number; +} + +/** + * Request parameters for submitting a URL load job. + */ +export interface SubmitUrlJobParams { + url: string; + name: string; + fmt?: string; + rmf_file?: string; + additional_columns?: string[]; + high_precision?: boolean; + skip_checks?: boolean; + notes?: string; +} diff --git a/src/utils/numbers.test.ts b/src/utils/numbers.test.ts new file mode 100644 index 0000000..741b68b --- /dev/null +++ b/src/utils/numbers.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { parseNumber, parsePositiveNumber } from './numbers'; + +describe('parsePositiveNumber', () => { + it('parses valid positive numbers', () => { + expect(parsePositiveNumber('0.0625')).toBe(0.0625); + expect(parsePositiveNumber('32')).toBe(32); + }); + + it('rejects zero, negatives, and junk', () => { + expect(parsePositiveNumber('0')).toBeNull(); + expect(parsePositiveNumber('-1')).toBeNull(); + expect(parsePositiveNumber('abc')).toBeNull(); + expect(parsePositiveNumber('')).toBeNull(); + expect(parsePositiveNumber('1e999')).toBeNull(); + }); +}); + +describe('parseNumber', () => { + it('parses any finite number', () => { + expect(parseNumber('-2.5')).toBe(-2.5); + expect(parseNumber('0')).toBe(0); + }); + + it('rejects non-numeric input', () => { + expect(parseNumber('1e999')).toBeNull(); + expect(parseNumber('x')).toBeNull(); + expect(parseNumber('')).toBeNull(); + expect(parseNumber(' ')).toBeNull(); + }); +}); diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts new file mode 100644 index 0000000..bea29b0 --- /dev/null +++ b/src/utils/numbers.ts @@ -0,0 +1,13 @@ +/** Parse a text-field value into a finite positive number, or null if invalid. */ +export function parsePositiveNumber(value: string): number | null { + if (value.trim() === '') return null; + const n = Number(value); + return Number.isFinite(n) && n > 0 ? n : null; +} + +/** Parse a text-field value into any finite number, or null if invalid. */ +export function parseNumber(value: string): number | null { + if (value.trim() === '') return null; + const n = Number(value); + return Number.isFinite(n) ? n : null; +} diff --git a/src/utils/powerColors.test.ts b/src/utils/powerColors.test.ts new file mode 100644 index 0000000..2a61c1d --- /dev/null +++ b/src/utils/powerColors.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { computePowerColorRatios } from './powerColors'; + +describe('computePowerColorRatios', () => { + const powerColors = { + A: [1, 2], + B: [10, 20], + C: [100, 200], + D: [5, 10], + }; + + it('computes PC1 = C/A and PC2 = B/D per segment', () => { + const result = computePowerColorRatios(powerColors, ['A', 'B', 'C', 'D']); + expect(result).not.toBeNull(); + expect(result?.pc1).toEqual([100, 100]); + expect(result?.pc2).toEqual([2, 2]); + }); + + it('returns null when a band is missing or count is not 4', () => { + expect(computePowerColorRatios(powerColors, ['A', 'B', 'C'])).toBeNull(); + expect(computePowerColorRatios({ A: [1] }, ['A', 'B', 'C', 'D'])).toBeNull(); + }); + + it('skips segments with non-positive denominators', () => { + const result = computePowerColorRatios( + { A: [0, 1], B: [1, 1], C: [1, 1], D: [1, 1] }, + ['A', 'B', 'C', 'D'] + ); + expect(result?.pc1).toEqual([1]); + expect(result?.pc2).toEqual([1]); + }); + + it('skips segments where any band value is null', () => { + const result = computePowerColorRatios( + { A: [1, null], B: [1, 1], C: [1, 1], D: [1, 1] }, + ['A', 'B', 'C', 'D'] + ); + expect(result?.pc1).toEqual([1]); + expect(result?.pc2).toEqual([1]); + }); +}); diff --git a/src/utils/powerColors.ts b/src/utils/powerColors.ts new file mode 100644 index 0000000..8502cda --- /dev/null +++ b/src/utils/powerColors.ts @@ -0,0 +1,34 @@ +/** + * Power-color ratios following the Heil et al. (2015) convention: with four + * frequency bands A < B < C < D (ascending f_min), + * PC1 = P(C) / P(A) and PC2 = P(B) / P(D) + * computed per dynamical-spectrum segment. + */ +export interface PowerColorRatios { + pc1: number[]; + pc2: number[]; +} + +export function computePowerColorRatios( + powerColors: Record>, + bandOrder: string[] +): PowerColorRatios | null { + if (bandOrder.length !== 4) return null; + const [a, b, c, d] = bandOrder.map((key) => powerColors[key]); + if (!a || !b || !c || !d) return null; + + const n = Math.min(a.length, b.length, c.length, d.length); + const pc1: number[] = []; + const pc2: number[] = []; + for (let i = 0; i < n; i++) { + const av = a[i]; + const bv = b[i]; + const cv = c[i]; + const dv = d[i]; + if (av != null && bv != null && cv != null && dv != null && av > 0 && bv > 0 && cv > 0 && dv > 0) { + pc1.push(cv / av); + pc2.push(bv / dv); + } + } + return { pc1, pc2 }; +} diff --git a/test_astropy_roundtrip.py b/test_astropy_roundtrip.py deleted file mode 100644 index 8e319d4..0000000 --- a/test_astropy_roundtrip.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Test script for Astropy export/import roundtrip functionality. - -This script verifies that EventLists can be exported to Astropy Tables -and imported back without data loss. -""" - -import numpy as np -import tempfile -import os -from stingray import EventList -from utils.state_manager import state_manager -from services import ServiceRegistry - - -def test_astropy_roundtrip(): - """Test the complete roundtrip: EventList -> Astropy Table -> EventList.""" - print("=" * 60) - print("Testing Astropy Roundtrip Functionality") - print("=" * 60) - - # Initialize services - services = ServiceRegistry(state_manager) - - # Create a test EventList - print("\n1. Creating test EventList...") - n_events = 1000 - times = np.sort(np.random.uniform(0, 100, n_events)) - energies = np.random.uniform(1, 10, n_events) - gti = np.array([[0, 100]]) - - test_event_list = EventList( - time=times, - energy=energies, - gti=gti - ) - - print(f" Created EventList with {len(test_event_list.time)} events") - print(f" Time range: {test_event_list.time[0]:.2f} - {test_event_list.time[-1]:.2f}") - print(f" Energy range: {test_event_list.energy.min():.2f} - {test_event_list.energy.max():.2f} keV") - - # Add to state - state_manager.add_event_data("test_eventlist", test_event_list) - - # Test export to different formats - formats_to_test = ["ascii.ecsv", "fits", "hdf5"] - - for fmt in formats_to_test: - print(f"\n{'=' * 60}") - print(f"Testing format: {fmt}") - print(f"{'=' * 60}") - - # Create temporary file - suffix = { - "ascii.ecsv": ".ecsv", - "fits": ".fits", - "hdf5": ".h5", - "votable": ".xml" - }.get(fmt, ".dat") - - with tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as tmp: - temp_path = tmp.name - - try: - # Export - print(f"\n2. Exporting EventList to {fmt}...") - export_result = services.data.export_event_list_to_astropy_table( - event_list_name="test_eventlist", - output_path=temp_path, - fmt=fmt - ) - - if not export_result["success"]: - print(f" FAILED: {export_result['message']}") - continue - - print(f" SUCCESS: Exported to {temp_path}") - print(f" Rows: {export_result['metadata']['n_rows']}") - print(f" File size: {os.path.getsize(temp_path) / 1024:.2f} KB") - - # Import - print(f"\n3. Importing EventList from {fmt}...") - import_name = f"imported_{fmt.replace('.', '_')}" - import_result = services.data.import_event_list_from_astropy_table( - file_path=temp_path, - name=import_name, - fmt=fmt - ) - - if not import_result["success"]: - print(f" FAILED: {import_result['message']}") - continue - - print(f" SUCCESS: Imported as '{import_name}'") - print(f" Events: {import_result['metadata']['n_events']}") - - # Verify data integrity - print(f"\n4. Verifying data integrity...") - imported_event_list = state_manager.get_event_data(import_name) - - # Check number of events - original_n_events = len(test_event_list.time) - imported_n_events = len(imported_event_list.time) - - if original_n_events != imported_n_events: - print(f" WARNING: Event count mismatch!") - print(f" Original: {original_n_events}, Imported: {imported_n_events}") - else: - print(f" Event count: {imported_n_events} (matches)") - - # Check time data - time_diff = np.abs(test_event_list.time - imported_event_list.time).max() - print(f" Max time difference: {time_diff:.2e} seconds") - - if time_diff < 1e-6: - print(f" Time data: EXACT MATCH") - else: - print(f" Time data: CLOSE MATCH (within tolerance)") - - # Check energy data - if hasattr(imported_event_list, 'energy') and imported_event_list.energy is not None: - energy_diff = np.abs(test_event_list.energy - imported_event_list.energy).max() - print(f" Max energy difference: {energy_diff:.2e} keV") - - if energy_diff < 1e-6: - print(f" Energy data: EXACT MATCH") - else: - print(f" Energy data: CLOSE MATCH (within tolerance)") - else: - print(f" Energy data: NOT PRESERVED (expected for some formats)") - - print(f"\n ROUNDTRIP TEST PASSED for {fmt}") - - except Exception as e: - print(f"\n ERROR: {str(e)}") - import traceback - traceback.print_exc() - - finally: - # Cleanup - if os.path.exists(temp_path): - os.unlink(temp_path) - print(f"\n Cleaned up temporary file: {temp_path}") - - print(f"\n{'=' * 60}") - print("All roundtrip tests completed") - print(f"{'=' * 60}") - - -if __name__ == "__main__": - test_astropy_roundtrip() diff --git a/tests/test_dataloading/test_dataingestion.py b/tests/test_dataloading/test_dataingestion.py deleted file mode 100644 index 3f2b924..0000000 --- a/tests/test_dataloading/test_dataingestion.py +++ /dev/null @@ -1,189 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from modules.DataLoading.DataIngestion import ( - create_loadingdata_output_box, - load_event_data, - save_loaded_files, - delete_selected_files, - preview_loaded_files, - clear_loaded_files, - create_event_list, - simulate_event_list, - create_warning_handler, -) - - -def test_create_loadingdata_output_box(): - content = "File loaded successfully." - output_box = create_loadingdata_output_box(content) - assert output_box.output_content == content - - -@patch("dataingestion.loaded_event_data", []) -def test_load_event_data_no_file_selected( - output_box_container, warning_box_container, warning_handler, mock_file_selector, filename_input, format_input, format_checkbox -): - # Set up file selector with no selection - mock_file_selector.value = [] - load_event_data( - event=None, - file_selector=mock_file_selector, - filename_input=filename_input, - format_input=format_input, - format_checkbox=format_checkbox, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "No file selected" in output_box_container[0].output_content - - -@patch("dataingestion.loaded_event_data", []) -@patch("dataingestion.EventList.read") -def test_load_event_data_success(mock_read, output_box_container, warning_box_container, warning_handler, mock_file_selector, filename_input, format_input, format_checkbox): - # Mock EventList read to return a valid event - mock_read.return_value = MagicMock() - - load_event_data( - event=None, - file_selector=mock_file_selector, - filename_input=filename_input, - format_input=format_input, - format_checkbox=format_checkbox, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert len(output_box_container) > 0 - assert "loaded successfully" in output_box_container[0].output_content - - -@patch("dataingestion.loaded_event_data", [("file1", MagicMock())]) -def test_load_event_data_duplicate_file( - output_box_container, warning_box_container, warning_handler, mock_file_selector, filename_input, format_input, format_checkbox -): - # Test with duplicate file name - filename_input.value = "file1" - load_event_data( - event=None, - file_selector=mock_file_selector, - filename_input=filename_input, - format_input=format_input, - format_checkbox=format_checkbox, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "already exists in memory" in output_box_container[0].output_content - - -@patch("dataingestion.os.path.exists", return_value=False) -@patch("dataingestion.loaded_event_data", [("file1", MagicMock())]) -def test_save_loaded_files_success(mock_exists, output_box_container, warning_box_container, warning_handler, filename_input, format_input, format_checkbox): - save_loaded_files( - event=None, - filename_input=filename_input, - format_input=format_input, - format_checkbox=format_checkbox, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "saved successfully" in output_box_container[0].output_content - - -@patch("dataingestion.os.path.exists", return_value=True) -@patch("dataingestion.loaded_event_data", [("file1", MagicMock())]) -def test_save_loaded_files_duplicate_name(mock_exists, output_box_container, warning_box_container, warning_handler, filename_input, format_input, format_checkbox): - save_loaded_files( - event=None, - filename_input=filename_input, - format_input=format_input, - format_checkbox=format_checkbox, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "already exists" in output_box_container[0].output_content - - -@patch("dataingestion.os.remove") -def test_delete_selected_files_success(mock_remove, output_box_container, warning_box_container, warning_handler, mock_file_selector): - delete_selected_files( - event=None, - file_selector=mock_file_selector, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "deleted successfully" in output_box_container[0].output_content - - -def test_preview_loaded_files_no_data(output_box_container, warning_box_container, warning_handler): - preview_loaded_files( - event=None, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "No valid files or light curves loaded" in output_box_container[0].output_content - - -@patch("dataingestion.loaded_event_data", [("event1", MagicMock(time=[0.1, 0.2], mjdref=58000, gti=[[0, 1]]) )]) -def test_preview_loaded_files_with_data(output_box_container, warning_box_container, warning_handler): - preview_loaded_files( - event=None, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "Event List - event1" in output_box_container[0].output_content - - -@patch("dataingestion.loaded_event_data", [("event1", MagicMock())]) -def test_clear_loaded_files(output_box_container, warning_box_container): - clear_loaded_files( - event=None, - output_box_container=output_box_container, - warning_box_container=warning_box_container, - ) - assert "cleared" in output_box_container[0].output_content - - -def test_create_event_list_missing_data(output_box_container, warning_box_container, warning_handler): - create_event_list( - event=None, - times_input=MagicMock(value=""), - energy_input=MagicMock(value=""), - pi_input=MagicMock(value=""), - gti_input=MagicMock(value=""), - mjdref_input=MagicMock(value=""), - name_input=MagicMock(value=""), - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "Please enter Photon Arrival Times and MJDREF" in output_box_container[0].output_content - - -def test_simulate_event_list(output_box_container, warning_box_container, warning_handler): - simulate_event_list( - event=None, - time_slider=MagicMock(value=10), - count_slider=MagicMock(value=5), - dt_input=MagicMock(value=0.1), - name_input=MagicMock(value="simulated_event"), - method_selector=MagicMock(value="Standard Method"), - output_box_container=output_box_container, - warning_box_container=warning_box_container, - warning_handler=warning_handler, - ) - assert "simulated successfully" in output_box_container[0].output_content - - -def test_create_warning_handler(): - handler = create_warning_handler() - with pytest.warns(None) as record: - handler.warn("Test warning", category=UserWarning) - assert len(record) == 1 - assert record[0].message.args[0] == "Test warning" diff --git a/tests/test_lazy_loader.py b/tests/test_lazy_loader.py deleted file mode 100644 index 91f87ba..0000000 --- a/tests/test_lazy_loader.py +++ /dev/null @@ -1,506 +0,0 @@ -""" -Unit tests for the LazyEventLoader class. - -This test suite covers: -- LazyEventLoader initialization and file handling -- Metadata extraction without loading full data -- Memory usage estimation -- Safety checks and risk assessment -- File size formatting -- Error handling for invalid files -""" - -import pytest -import os -import tempfile -import numpy as np -from unittest.mock import MagicMock, patch, PropertyMock -from utils.lazy_loader import LazyEventLoader, assess_loading_risk - - -# ============================================================================= -# Fixtures -# ============================================================================= - -@pytest.fixture -def mock_fits_file(): - """Create a temporary mock FITS file.""" - with tempfile.NamedTemporaryFile(suffix='.fits', delete=False) as f: - # Write some dummy data to make it a non-zero size - f.write(b'SIMPLE = T' * 100) # Fake FITS header - temp_path = f.name - - yield temp_path - - # Cleanup - if os.path.exists(temp_path): - os.remove(temp_path) - - -@pytest.fixture -def mock_fits_reader(): - """Create a mock FITSTimeseriesReader.""" - mock_reader = MagicMock() - mock_reader.gti = np.array([[0, 1000], [1100, 2000]]) - mock_reader.mjdref = 58000.0 - return mock_reader - - -# ============================================================================= -# Test: LazyEventLoader Initialization -# ============================================================================= - -def test_lazy_loader_init_with_nonexistent_file(): - """Test initialization with non-existent file raises FileNotFoundError.""" - with pytest.raises(FileNotFoundError): - LazyEventLoader("/path/to/nonexistent/file.fits") - - -def test_lazy_loader_init_with_invalid_fits(mock_fits_file): - """Test initialization with invalid FITS file raises ValueError.""" - # The mock file isn't a real FITS file, so this should fail - with pytest.raises(ValueError, match="Failed to open FITS file"): - LazyEventLoader(mock_fits_file) - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_lazy_loader_init_success(mock_reader_class, mock_fits_file): - """Test successful initialization.""" - mock_reader_class.return_value = MagicMock() - - loader = LazyEventLoader(mock_fits_file) - - assert loader.file_path == mock_fits_file - assert loader.file_size > 0 - assert loader.reader is not None - mock_reader_class.assert_called_once_with(mock_fits_file, data_kind="times") - - -# ============================================================================= -# Test: Metadata Extraction -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_get_metadata(mock_reader_class, mock_fits_file, mock_fits_reader): - """Test metadata extraction without loading event data.""" - mock_reader_class.return_value = mock_fits_reader - - loader = LazyEventLoader(mock_fits_file) - metadata = loader.get_metadata() - - # Check all expected keys present - assert 'gti' in metadata - assert 'mjdref' in metadata - assert 'n_events_estimate' in metadata - assert 'time_range' in metadata - assert 'file_size_mb' in metadata - assert 'file_size_gb' in metadata - assert 'duration_s' in metadata - assert 'estimated_count_rate' in metadata - - # Check values - assert np.array_equal(metadata['gti'], mock_fits_reader.gti) - assert metadata['mjdref'] == 58000.0 - assert metadata['duration_s'] == 1900.0 # (1000-0) + (2000-1100) - assert metadata['n_events_estimate'] > 0 - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_get_metadata_time_range(mock_reader_class, mock_fits_file, mock_fits_reader): - """Test that time_range is correctly extracted from GTIs.""" - mock_reader_class.return_value = mock_fits_reader - - loader = LazyEventLoader(mock_fits_file) - metadata = loader.get_metadata() - - time_range = metadata['time_range'] - assert time_range == (0.0, 2000.0) # min and max from GTIs - - -# ============================================================================= -# Test: Memory Estimation -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_estimate_memory_usage_fits(mock_reader_class, mock_fits_file): - """Test memory estimation for FITS files.""" - mock_reader_class.return_value = MagicMock() - - loader = LazyEventLoader(mock_fits_file) - estimated = loader.estimate_memory_usage('fits') - - # FITS multiplier is 3x (based on Stingray benchmarks: 2GB → 5.2GB = 2.6x, rounded to 3x) - expected = loader.file_size * 3 - assert estimated == expected - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_estimate_memory_usage_hdf5(mock_reader_class, mock_fits_file): - """Test memory estimation for HDF5 files.""" - mock_reader_class.return_value = MagicMock() - - loader = LazyEventLoader(mock_fits_file) - estimated = loader.estimate_memory_usage('hdf5') - - # HDF5 multiplier is 2x (more efficient format) - expected = loader.file_size * 2 - assert estimated == expected - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_estimate_memory_usage_pickle(mock_reader_class, mock_fits_file): - """Test memory estimation for pickle files.""" - mock_reader_class.return_value = MagicMock() - - loader = LazyEventLoader(mock_fits_file) - estimated = loader.estimate_memory_usage('pickle') - - # Pickle multiplier is 1.5x (most efficient format) - expected = loader.file_size * 1.5 - assert estimated == expected - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_estimate_memory_usage_unknown_format(mock_reader_class, mock_fits_file): - """Test memory estimation for unknown format defaults to conservative multiplier.""" - mock_reader_class.return_value = MagicMock() - - loader = LazyEventLoader(mock_fits_file) - estimated = loader.estimate_memory_usage('unknown_format') - - # Default multiplier is 3x (conservative default, same as FITS) - expected = loader.file_size * 3 - assert estimated == expected - - -# ============================================================================= -# Test: Safety Checks -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_can_load_safely_safe(mock_vmem, mock_reader_class, mock_fits_file): - """Test can_load_safely returns True when safe.""" - mock_reader_class.return_value = MagicMock() - - # Mock large available memory - mock_vmem.return_value.available = 16 * 1024**3 # 16 GB - - loader = LazyEventLoader(mock_fits_file) - # Small file, lots of memory -> should be safe - assert loader.can_load_safely(safety_margin=0.5) is True - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_can_load_safely_unsafe(mock_vmem, mock_reader_class, mock_fits_file): - """Test can_load_safely returns False when unsafe.""" - mock_reader_class.return_value = MagicMock() - - # Mock small available memory relative to file size - # File is ~1.1 KB, with 3x multiplier = ~3.3 KB needed - # Set available to 5 KB, so 50% margin = 2.5 KB safe limit - # 3.3 KB > 2.5 KB -> should be unsafe - mock_vmem.return_value.available = 5 * 1024 # 5 KB - - loader = LazyEventLoader(mock_fits_file) - # File needs more memory than safe limit -> should be unsafe - assert loader.can_load_safely(safety_margin=0.5) is False - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_can_load_safely_custom_margin(mock_vmem, mock_reader_class, mock_fits_file): - """Test can_load_safely with custom safety margin.""" - mock_reader_class.return_value = MagicMock() - - # Mock specific available memory - mock_vmem.return_value.available = 1 * 1024**3 # 1 GB - - loader = LazyEventLoader(mock_fits_file) - - # With high safety margin (10%), should be safer - result_high_margin = loader.can_load_safely(safety_margin=0.1) - - # With low safety margin (90%), should be less safe - result_low_margin = loader.can_load_safely(safety_margin=0.9) - - # High margin is more conservative (more likely to be unsafe) - # Low margin is less conservative (more likely to be safe) - # For small test file, both might be True, but the logic is correct - - -# ============================================================================= -# Test: System Memory Info -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.psutil.virtual_memory') -@patch('utils.lazy_loader.psutil.Process') -def test_get_system_memory_info(mock_process, mock_vmem, mock_reader_class, mock_fits_file): - """Test system memory info retrieval.""" - mock_reader_class.return_value = MagicMock() - - # Mock memory values - mock_vmem.return_value.total = 16 * 1024**3 # 16 GB - mock_vmem.return_value.available = 8 * 1024**3 # 8 GB - mock_vmem.return_value.used = 8 * 1024**3 # 8 GB - mock_vmem.return_value.percent = 50.0 - - mock_process.return_value.memory_info.return_value.rss = 256 * 1024**2 # 256 MB - - loader = LazyEventLoader(mock_fits_file) - mem_info = loader.get_system_memory_info() - - # Check all expected keys - assert 'total_mb' in mem_info - assert 'available_mb' in mem_info - assert 'used_mb' in mem_info - assert 'percent' in mem_info - assert 'process_mb' in mem_info - - # Check values - assert mem_info['total_mb'] == 16 * 1024 # 16 GB in MB - assert mem_info['available_mb'] == 8 * 1024 # 8 GB in MB - assert mem_info['percent'] == 50.0 - assert mem_info['process_mb'] == 256.0 - - -# ============================================================================= -# Test: File Size Formatting -# ============================================================================= - -def test_format_file_size_bytes(): - """Test formatting bytes.""" - assert LazyEventLoader.format_file_size(500) == "500.0 B" - - -def test_format_file_size_kilobytes(): - """Test formatting kilobytes.""" - assert LazyEventLoader.format_file_size(1500) == "1.5 KB" - - -def test_format_file_size_megabytes(): - """Test formatting megabytes.""" - assert LazyEventLoader.format_file_size(2 * 1024**2) == "2.0 MB" - - -def test_format_file_size_gigabytes(): - """Test formatting gigabytes.""" - assert LazyEventLoader.format_file_size(3.5 * 1024**3) == "3.5 GB" - - -def test_format_file_size_terabytes(): - """Test formatting terabytes.""" - assert LazyEventLoader.format_file_size(1.2 * 1024**4) == "1.2 TB" - - -# ============================================================================= -# Test: Risk Assessment Function -# ============================================================================= - -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_assess_loading_risk_safe(mock_vmem): - """Test risk assessment returns 'safe' for small files.""" - mock_vmem.return_value.available = 16 * 1024**3 # 16 GB - - file_size = 100 * 1024**2 # 100 MB - risk = assess_loading_risk(file_size, file_format='fits') - - # 100 MB * 3 = 300 MB needed - # 300 MB / 16 GB = ~0.02 (2%) -> safe - assert risk == 'safe' - - -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_assess_loading_risk_caution(mock_vmem): - """Test risk assessment returns 'caution' for medium files.""" - mock_vmem.return_value.available = 2 * 1024**3 # 2 GB - - file_size = 350 * 1024**2 # 350 MB - risk = assess_loading_risk(file_size, file_format='fits') - - # 350 MB * 3 = 1050 MB needed - # 1050 MB / 2048 MB = ~0.51 (51%) -> caution - assert risk == 'caution' - - -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_assess_loading_risk_risky(mock_vmem): - """Test risk assessment returns 'risky' for large files.""" - mock_vmem.return_value.available = 2 * 1024**3 # 2 GB - - file_size = 480 * 1024**2 # 480 MB - risk = assess_loading_risk(file_size, file_format='fits') - - # 480 MB * 3 = 1440 MB needed - # 1440 MB / 2048 MB = ~0.70 (70%) -> risky - assert risk == 'risky' - - -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_assess_loading_risk_critical(mock_vmem): - """Test risk assessment returns 'critical' for very large files.""" - mock_vmem.return_value.available = 1 * 1024**3 # 1 GB - - file_size = 350 * 1024**2 # 350 MB - risk = assess_loading_risk(file_size, file_format='fits') - - # 350 MB * 3 = 1050 MB needed - # 1050 MB / 1024 MB = ~1.03 (103%) -> critical - assert risk == 'critical' - - -@patch('utils.lazy_loader.psutil.virtual_memory') -def test_assess_loading_risk_different_formats(mock_vmem): - """Test risk assessment with different file formats.""" - mock_vmem.return_value.available = 4 * 1024**3 # 4 GB - - # Use different file sizes to test format-specific multipliers - # FITS: 1000 MB * 3 = 3000 MB (73% -> risky) - risk_fits = assess_loading_risk(1000 * 1024**2, file_format='fits', available_memory=4 * 1024**3) - - # HDF5: 850 MB * 2 = 1700 MB (41% -> caution) - risk_hdf5 = assess_loading_risk(850 * 1024**2, file_format='hdf5', available_memory=4 * 1024**3) - - # Pickle: 600 MB * 1.5 = 900 MB (22% -> safe) - risk_pickle = assess_loading_risk(600 * 1024**2, file_format='pickle', available_memory=4 * 1024**3) - - assert risk_fits in ['risky', 'critical'] - assert risk_hdf5 in ['safe', 'caution'] - assert risk_pickle == 'safe' - - -# ============================================================================= -# Test: Context Manager -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_context_manager(mock_reader_class, mock_fits_file): - """Test LazyEventLoader as context manager.""" - mock_reader_class.return_value = MagicMock() - - with LazyEventLoader(mock_fits_file) as loader: - assert loader is not None - assert isinstance(loader, LazyEventLoader) - - -# ============================================================================= -# Test: String Representation -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_repr(mock_reader_class, mock_fits_file): - """Test string representation.""" - mock_reader_class.return_value = MagicMock() - - loader = LazyEventLoader(mock_fits_file) - repr_str = repr(loader) - - assert 'LazyEventLoader' in repr_str - assert mock_fits_file in repr_str - assert 'KB' in repr_str or 'MB' in repr_str or 'GB' in repr_str - - -# ============================================================================= -# Test: Load Full (with mocking) -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.EventList') -def test_load_full(mock_eventlist_class, mock_reader_class, mock_fits_file): - """Test load_full method.""" - mock_reader_class.return_value = MagicMock() - mock_event_list = MagicMock() - mock_event_list.time = np.arange(1000) - mock_eventlist_class.read.return_value = mock_event_list - - loader = LazyEventLoader(mock_fits_file) - events = loader.load_full() - - assert events is not None - mock_eventlist_class.read.assert_called_once() - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.EventList') -def test_load_full_with_additional_columns(mock_eventlist_class, mock_reader_class, mock_fits_file): - """Test load_full with additional columns.""" - mock_reader_class.return_value = MagicMock() - mock_event_list = MagicMock() - mock_eventlist_class.read.return_value = mock_event_list - - loader = LazyEventLoader(mock_fits_file) - loader.load_full(additional_columns=['DETID', 'RAWX']) - - # Verify additional_columns was passed - call_kwargs = mock_eventlist_class.read.call_args[1] - assert 'additional_columns' in call_kwargs - assert call_kwargs['additional_columns'] == ['DETID', 'RAWX'] - - -# ============================================================================= -# Test: Stream Segments (with mocking) -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -@patch('utils.lazy_loader.time_intervals_from_gtis') -def test_stream_segments(mock_time_intervals, mock_reader_class, mock_fits_file, mock_fits_reader): - """Test stream_segments method.""" - mock_reader_class.return_value = mock_fits_reader - - # Mock time intervals - mock_time_intervals.return_value = ( - np.array([0, 100, 200]), - np.array([100, 200, 300]) - ) - - # Mock filtered times - mock_fits_reader.filter_at_time_intervals.return_value = [ - np.array([10, 20, 30]), - np.array([110, 120]), - np.array([210, 220, 230, 240]) - ] - - loader = LazyEventLoader(mock_fits_file) - segments = list(loader.stream_segments(segment_size=100)) - - assert len(segments) == 3 - assert len(segments[0]) == 3 # First segment has 3 events - assert len(segments[1]) == 2 # Second segment has 2 events - assert len(segments[2]) == 4 # Third segment has 4 events - - -# ============================================================================= -# Test: Edge Cases -# ============================================================================= - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_metadata_with_zero_duration(mock_reader_class, mock_fits_file): - """Test metadata extraction with zero duration GTIs.""" - mock_reader = MagicMock() - mock_reader.gti = np.array([[0, 0]]) # Zero duration - mock_reader.mjdref = 58000.0 - mock_reader_class.return_value = mock_reader - - loader = LazyEventLoader(mock_fits_file) - metadata = loader.get_metadata() - - # Should handle zero duration gracefully - assert metadata['duration_s'] == 0.0 - assert metadata['estimated_count_rate'] == 0 # Avoid division by zero - - -@patch('utils.lazy_loader.FITSTimeseriesReader') -def test_metadata_with_no_mjdref(mock_reader_class, mock_fits_file): - """Test metadata extraction when MJDREF is missing.""" - mock_reader = MagicMock() - mock_reader.gti = np.array([[0, 1000]]) - del mock_reader.mjdref # Remove attribute - mock_reader_class.return_value = mock_reader - - loader = LazyEventLoader(mock_fits_file) - metadata = loader.get_metadata() - - # Should default to 0.0 - assert metadata['mjdref'] == 0.0 diff --git a/tests/test_lazy_loading_integration.py b/tests/test_lazy_loading_integration.py deleted file mode 100644 index 444a186..0000000 --- a/tests/test_lazy_loading_integration.py +++ /dev/null @@ -1,642 +0,0 @@ -""" -Integration tests for lazy loading workflow. - -This test suite covers end-to-end lazy loading functionality: -- DataService integration with lazy loading -- Memory usage verification -- Performance comparison (standard vs lazy) -- Error handling with real FITS files -- StateManager integration -- Large file handling scenarios -""" - -import pytest -import os -import tempfile -import numpy as np -import psutil -from unittest.mock import patch, MagicMock -from astropy.io import fits -from stingray import EventList - -from services.data_service import DataService -from utils.state_manager import StateManager -from utils.lazy_loader import LazyEventLoader, assess_loading_risk - - -# ============================================================================= -# Fixtures -# ============================================================================= - -@pytest.fixture -def state_manager(): - """Create a fresh StateManager instance for each test.""" - return StateManager() - - -@pytest.fixture -def data_service(state_manager): - """Create DataService instance with StateManager.""" - service = DataService(state_manager) - return service - - -@pytest.fixture -def sample_evt_file(): - """Path to real small sample EVT file.""" - return "files/data/monol_testA.evt" - - -@pytest.fixture -def sample_fits_file(): - """Path to real small sample FITS file.""" - return "files/data/lcurveA.fits" - - -@pytest.fixture -def synthetic_small_fits(): - """ - Create a synthetic small FITS file (~100KB) for testing. - - Yields path to temporary file, cleaned up after test. - """ - # Create temporary file - fd, tmp_path = tempfile.mkstemp(suffix='.evt') - os.close(fd) - - try: - # Generate synthetic event data - n_events = 10000 - tstart = 0.0 - duration = 1000.0 - - times = np.sort(np.random.uniform(tstart, tstart + duration, n_events)) - energy = np.random.uniform(0.5, 10.0, n_events) - pi = (energy * 100).astype(np.int32) - - # Create FITS file structure - # Primary HDU - primary = fits.PrimaryHDU() - - # Events extension - col1 = fits.Column(name='TIME', format='D', array=times) - col2 = fits.Column(name='ENERGY', format='E', array=energy) - col3 = fits.Column(name='PI', format='J', array=pi) - - cols = fits.ColDefs([col1, col2, col3]) - events_hdu = fits.BinTableHDU.from_columns(cols) - events_hdu.header['EXTNAME'] = 'EVENTS' - events_hdu.header['TELESCOP'] = 'TEST' - events_hdu.header['INSTRUME'] = 'SYNTHETIC' - events_hdu.header['MJDREFI'] = 55000 - events_hdu.header['MJDREFF'] = 0.0 - events_hdu.header['TIMEZERO'] = 0.0 - events_hdu.header['TIMEUNIT'] = 's' - # Add required timing keywords - events_hdu.header['TSTART'] = tstart - events_hdu.header['TSTOP'] = tstart + duration - events_hdu.header['TIMESYS'] = 'TT' - events_hdu.header['TIMEREF'] = 'LOCAL' - - # GTI extension - gti_start = np.array([tstart]) - gti_stop = np.array([tstart + duration]) - - col1 = fits.Column(name='START', format='D', array=gti_start) - col2 = fits.Column(name='STOP', format='D', array=gti_stop) - - gti_cols = fits.ColDefs([col1, col2]) - gti_hdu = fits.BinTableHDU.from_columns(gti_cols) - gti_hdu.header['EXTNAME'] = 'GTI' - - # Write FITS file - hdul = fits.HDUList([primary, events_hdu, gti_hdu]) - hdul.writeto(tmp_path, overwrite=True) - - yield tmp_path - - finally: - # Cleanup - if os.path.exists(tmp_path): - os.remove(tmp_path) - - -@pytest.fixture -def synthetic_large_fits_info(): - """ - Return parameters for a hypothetical large FITS file. - - We don't actually create it (too slow/large), but return - characteristics for testing logic. - """ - return { - 'file_size': 2.5 * 1024**3, # 2.5 GB - 'n_events': 200_000_000, # 200 million events - 'duration': 50000.0, # seconds - } - - -# ============================================================================= -# Integration Tests: DataService with Lazy Loading -# ============================================================================= - -def test_load_event_list_lazy_small_file_safe(data_service, synthetic_small_fits): - """ - Test lazy loading with a small file that's safe to load. - - Should use standard loading method since file is small. - """ - result = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="test_small", - safety_margin=0.5 - ) - - # Should succeed - assert result["success"] is True - assert result["data"] is not None - assert isinstance(result["data"], EventList) - - # Should use standard method for small file - assert result["metadata"]["method"] == "standard" - assert result["metadata"]["memory_safe"] is True - - # Verify data is in state manager - assert data_service.state.has_event_data("test_small") - retrieved = data_service.state.get_event_data("test_small") - assert len(retrieved) == len(result["data"].time) - - -def test_load_event_list_lazy_duplicate_name(data_service, synthetic_small_fits): - """Test that lazy loading prevents duplicate names.""" - # Load first time - result1 = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="duplicate_test", - safety_margin=0.5 - ) - assert result1["success"] is True - - # Try loading again with same name - result2 = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="duplicate_test", - safety_margin=0.5 - ) - assert result2["success"] is False - assert "already exists" in result2["message"] - - -def test_load_event_list_lazy_nonexistent_file(data_service): - """Test lazy loading with non-existent file.""" - result = data_service.load_event_list_lazy( - file_path="/nonexistent/file.evt", - name="test_missing", - safety_margin=0.5 - ) - - assert result["success"] is False - assert result["data"] is None - assert "error" in result - - -def test_check_file_size_small_file(data_service, synthetic_small_fits): - """Test file size checking with small file.""" - result = data_service.check_file_size(synthetic_small_fits) - - assert result["success"] is True - data = result["data"] - - # Verify structure - assert "file_size_bytes" in data - assert "file_size_mb" in data - assert "file_size_gb" in data - assert "risk_level" in data - assert "recommend_lazy" in data - assert "estimated_memory_mb" in data - assert "memory_info" in data - - # Small file should be safe - assert data["risk_level"] == "safe" - assert data["recommend_lazy"] is False - assert data["file_size_gb"] < 0.1 - - -def test_check_file_size_with_real_evt(data_service, sample_evt_file): - """Test file size checking with real sample EVT file.""" - if not os.path.exists(sample_evt_file): - pytest.skip(f"Sample file {sample_evt_file} not found") - - result = data_service.check_file_size(sample_evt_file) - - assert result["success"] is True - data = result["data"] - - # Should be safe for small file - assert data["risk_level"] == "safe" - assert data["file_size_mb"] < 1.0 # Sample files are < 1MB - - -def test_get_file_metadata(data_service, synthetic_small_fits): - """Test metadata extraction without loading full data.""" - result = data_service.get_file_metadata(synthetic_small_fits) - - assert result["success"] is True - metadata = result["data"] - - # Verify metadata structure - assert "gti" in metadata - assert "mjdref" in metadata - assert "n_events_estimate" in metadata - assert "time_range" in metadata - assert "file_size_mb" in metadata - assert "duration_s" in metadata - - # Verify reasonable values - assert metadata["duration_s"] > 0 - assert metadata["n_events_estimate"] > 0 - - -def test_is_large_file(data_service, synthetic_small_fits): - """Test large file detection.""" - # Small file - assert data_service.is_large_file(synthetic_small_fits, threshold_gb=1.0) is False - - # With very small threshold - assert data_service.is_large_file(synthetic_small_fits, threshold_gb=0.00001) is True - - -# ============================================================================= -# Integration Tests: Memory Usage Monitoring -# ============================================================================= - -def test_memory_usage_during_loading(data_service, synthetic_small_fits): - """ - Test that memory usage is tracked during loading. - - Verifies performance monitoring integration. - """ - # Get initial memory - process = psutil.Process() - mem_before = process.memory_info().rss / (1024**2) # MB - - # Load file - result = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="mem_test", - safety_margin=0.5 - ) - - # Get final memory - mem_after = process.memory_info().rss / (1024**2) # MB - - # Should succeed - assert result["success"] is True - - # Memory should increase (but not by much for small file) - mem_increase = mem_after - mem_before - assert mem_increase >= 0 # Memory should not decrease - - # For small test file (~100KB), increase should be < 50 MB - assert mem_increase < 50 - - -def test_lazy_loader_memory_info(synthetic_small_fits): - """Test LazyEventLoader memory info reporting.""" - loader = LazyEventLoader(synthetic_small_fits) - mem_info = loader.get_system_memory_info() - - # Verify structure - assert "total_mb" in mem_info - assert "available_mb" in mem_info - assert "used_mb" in mem_info - assert "percent" in mem_info - assert "process_mb" in mem_info - - # Verify reasonable values - assert mem_info["total_mb"] > 0 - assert mem_info["available_mb"] > 0 - assert 0 <= mem_info["percent"] <= 100 - - -# ============================================================================= -# Integration Tests: Error Handling -# ============================================================================= - -def test_load_corrupted_fits_file(data_service): - """Test loading a corrupted FITS file.""" - # Create corrupted file - fd, tmp_path = tempfile.mkstemp(suffix='.evt') - try: - os.write(fd, b"This is not a valid FITS file") - os.close(fd) - - result = data_service.load_event_list_lazy( - file_path=tmp_path, - name="corrupted", - safety_margin=0.5 - ) - - # Should fail gracefully - assert result["success"] is False - assert "error" in result - - finally: - if os.path.exists(tmp_path): - os.remove(tmp_path) - - -def test_load_with_memory_error_simulation(data_service, synthetic_small_fits): - """ - Test handling of MemoryError during loading. - - Simulates out-of-memory condition. - """ - # Patch EventList.read to raise MemoryError - with patch('utils.lazy_loader.EventList.read', side_effect=MemoryError("Out of memory")): - result = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="oom_test", - safety_margin=0.5 - ) - - # Should fail with specific message - assert result["success"] is False - assert "Out of memory" in result["message"] or "memory" in result["message"].lower() - - -# ============================================================================= -# Integration Tests: Performance Comparison -# ============================================================================= - -def test_standard_vs_lazy_loading_workflow(data_service, synthetic_small_fits): - """ - Compare standard vs lazy loading workflow. - - For small files, both should work, but lazy adds overhead. - """ - import time - - # Test standard loading - start = time.time() - result_standard = data_service.load_event_list( - file_path=synthetic_small_fits, - name="standard_test", - fmt="ogip" - ) - time_standard = time.time() - start - - assert result_standard["success"] is True - - # Test lazy loading (with new name) - start = time.time() - result_lazy = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="lazy_test", - safety_margin=0.5 - ) - time_lazy = time.time() - start - - assert result_lazy["success"] is True - - # Both should produce same size event list - ev1 = result_standard["data"] - ev2 = result_lazy["data"] - assert len(ev1.time) == len(ev2.time) - - # Print timing info for reference - print(f"\nTiming comparison:") - print(f" Standard: {time_standard:.4f}s") - print(f" Lazy: {time_lazy:.4f}s") - print(f" Ratio: {time_lazy/time_standard:.2f}x") - - -# ============================================================================= -# Integration Tests: Risk Assessment -# ============================================================================= - -def test_assess_loading_risk_integration(synthetic_large_fits_info): - """Test risk assessment with realistic large file parameters.""" - file_size = synthetic_large_fits_info['file_size'] - - # Get actual available memory - available_mem = psutil.virtual_memory().available - - # Assess risk - risk = assess_loading_risk(file_size, file_format='fits', available_memory=available_mem) - - # For 2.5 GB file with 8x multiplier (20 GB needed): - # - If available < 33 GB: critical (>90%) - # - If available < 67 GB: risky (60-90%) - # - If available < 22 GB: caution (30-60%) - # This will vary by system - - assert risk in ['safe', 'caution', 'risky', 'critical'] - - # Log for debugging - print(f"\nRisk assessment for {file_size/(1024**3):.1f}GB file:") - print(f" Available RAM: {available_mem/(1024**3):.1f}GB") - print(f" Risk level: {risk}") - - -def test_lazy_loading_recommendation_logic(data_service, synthetic_small_fits): - """Test the logic for recommending lazy loading.""" - result = data_service.check_file_size(synthetic_small_fits) - - assert result["success"] is True - data = result["data"] - - # For small file: should NOT recommend lazy loading - assert data["recommend_lazy"] is False - - # Manually test logic with mocked large file - with patch('os.path.getsize', return_value=2.5 * 1024**3): # 2.5 GB - result_large = data_service.check_file_size("fake_large.evt") - - if result_large["success"]: - # Should recommend lazy for large file - assert result_large["data"]["recommend_lazy"] is True - assert result_large["data"]["file_size_gb"] > 1.0 - - -# ============================================================================= -# Integration Tests: Streaming Operations -# ============================================================================= - -def test_lazy_loader_streaming_segments(synthetic_small_fits): - """Test streaming segments from LazyEventLoader.""" - loader = LazyEventLoader(synthetic_small_fits) - - # Stream in 100s segments - segments = list(loader.stream_segments(segment_size=100.0)) - - # Should get multiple segments - assert len(segments) > 0 - - # Each segment should be a numpy array - for segment in segments: - assert isinstance(segment, np.ndarray) - assert len(segment) > 0 - - # Total events should match full load - total_streamed = sum(len(seg) for seg in segments) - - full_events = loader.load_full() - assert total_streamed == len(full_events.time) - - -def test_lazy_loader_lightcurve_streaming(synthetic_small_fits): - """Test streaming lightcurve creation.""" - loader = LazyEventLoader(synthetic_small_fits) - - # Create lightcurve via streaming - lc_segments = list(loader.create_lightcurve_streaming( - segment_size=100.0, - dt=1.0 - )) - - # Should get segments - assert len(lc_segments) > 0 - - # Each segment should be (times, counts) tuple - for times, counts in lc_segments: - assert isinstance(times, np.ndarray) - assert isinstance(counts, np.ndarray) - assert len(times) == len(counts) - assert len(times) > 0 - - -# ============================================================================= -# Integration Tests: Full Workflow -# ============================================================================= - -def test_complete_lazy_loading_workflow(data_service, synthetic_small_fits): - """ - Test complete workflow: check size -> load with lazy -> verify -> delete. - - This simulates the full user workflow in the dashboard. - """ - # Step 1: Check file size - check_result = data_service.check_file_size(synthetic_small_fits) - assert check_result["success"] is True - - file_info = check_result["data"] - print(f"\nFile info: {file_info['file_size_mb']:.2f} MB, risk: {file_info['risk_level']}") - - # Step 2: Get metadata (fast preview) - metadata_result = data_service.get_file_metadata(synthetic_small_fits) - assert metadata_result["success"] is True - - metadata = metadata_result["data"] - print(f"Metadata: ~{metadata['n_events_estimate']} events, {metadata['duration_s']:.1f}s duration") - - # Step 3: Load with lazy method (auto-decides standard vs lazy) - load_result = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="workflow_test", - safety_margin=0.5 - ) - assert load_result["success"] is True - - event_list = load_result["data"] - print(f"Loaded: {len(event_list.time)} events via {load_result['metadata']['method']} method") - - # Step 4: Verify data is accessible - get_result = data_service.get_event_list("workflow_test") - assert get_result["success"] is True - assert get_result["data"] is not None - - # Step 5: List all event lists - list_result = data_service.list_event_lists() - assert list_result["success"] is True - assert len(list_result["data"]) >= 1 - - # Step 6: Delete - delete_result = data_service.delete_event_list("workflow_test") - assert delete_result["success"] is True - - # Verify deleted - assert not data_service.state.has_event_data("workflow_test") - - -def test_multiple_files_mixed_loading(data_service, synthetic_small_fits): - """Test loading multiple files with different methods.""" - # Load first file with standard method - result1 = data_service.load_event_list( - file_path=synthetic_small_fits, - name="file1", - fmt="ogip" - ) - assert result1["success"] is True - - # Load second file with lazy method - result2 = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="file2", - safety_margin=0.5 - ) - assert result2["success"] is True - - # Both should be accessible - assert data_service.state.has_event_data("file1") - assert data_service.state.has_event_data("file2") - - # List should show both - list_result = data_service.list_event_lists() - assert len(list_result["data"]) == 2 - - -# ============================================================================= -# Edge Cases -# ============================================================================= - -def test_empty_file_handling(data_service): - """Test handling of empty FITS file.""" - fd, tmp_path = tempfile.mkstemp(suffix='.evt') - os.close(fd) - - try: - result = data_service.load_event_list_lazy( - file_path=tmp_path, - name="empty", - safety_margin=0.5 - ) - - # Should fail (empty file is invalid FITS) - assert result["success"] is False - - finally: - if os.path.exists(tmp_path): - os.remove(tmp_path) - - -def test_very_high_safety_margin(data_service, synthetic_small_fits): - """Test lazy loading with very conservative safety margin.""" - # 99% safety margin means only use 1% of available RAM - result = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="conservative", - safety_margin=0.01 # Only use 1% of RAM - ) - - # Should still succeed for small file - # (might use 'standard_risky' method if safety check fails) - assert result["success"] is True - - -def test_zero_safety_margin(data_service, synthetic_small_fits): - """Test lazy loading with zero safety margin (risky!).""" - # Safety margin of 0 means no safety checks - result = data_service.load_event_list_lazy( - file_path=synthetic_small_fits, - name="risky", - safety_margin=0.0 - ) - - # Should fail or warn (depends on implementation) - # Small file should still load - assert result["success"] is True or "warning" in result["message"].lower() - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "--tb=short"]) diff --git a/tests/test_performance_monitoring.py b/tests/test_performance_monitoring.py deleted file mode 100644 index 6e3c6cb..0000000 --- a/tests/test_performance_monitoring.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Test Script for Performance Monitoring - -This script tests that the PerformanceMonitor tracks operations correctly -and provides accurate statistics. -""" - -import sys -import os -import time - -# Add parent directory to path -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from utils.performance_monitor import performance_monitor -import numpy as np - -def test_operation_tracking(): - """Test that operations are tracked correctly.""" - print("=" * 60) - print("Testing Performance Monitoring") - print("=" * 60) - - # Clear any existing history - performance_monitor.clear_history() - - print(f"\nInitial state:") - summary = performance_monitor.get_summary() - print(f" Total operations: {summary['total_operations']}") - print(f" Unique operations: {summary['unique_operations']}") - - # Test 1: Track a simple operation - print(f"\n{'='*60}") - print("Test 1: Tracking Simple Operation") - print(f"{'='*60}") - - with performance_monitor.track_operation("test_operation_1"): - # Simulate some work - time.sleep(0.1) - result = sum(range(1000)) - - summary = performance_monitor.get_summary() - print(f"\nAfter 1 operation:") - print(f" Total operations: {summary['total_operations']}") - print(f" Average duration: {summary['avg_duration_ms']:.2f} ms") - print(f" Success rate: {summary['success_rate']:.1f}%") - - # Test 2: Track multiple operations - print(f"\n{'='*60}") - print("Test 2: Tracking Multiple Operations") - print(f"{'='*60}") - - for i in range(5): - with performance_monitor.track_operation("batch_operation"): - time.sleep(0.05) - _ = np.random.rand(100) - - stats = performance_monitor.get_operation_stats("batch_operation") - print(f"\nStats for 'batch_operation':") - print(f" Count: {stats['count']}") - print(f" Average: {stats['avg_ms']:.2f} ms") - print(f" Min: {stats['min_ms']:.2f} ms") - print(f" Max: {stats['max_ms']:.2f} ms") - print(f" Median: {stats['median_ms']:.2f} ms") - print(f" Success rate: {stats['success_rate']:.1f}%") - - # Test 3: Track failed operation - print(f"\n{'='*60}") - print("Test 3: Tracking Failed Operation") - print(f"{'='*60}") - - try: - with performance_monitor.track_operation("failing_operation"): - raise ValueError("Intentional test error") - except ValueError: - pass # Expected - - failed_ops = performance_monitor.get_failed_operations(limit=10) - print(f"\nFailed operations: {len(failed_ops)}") - if failed_ops: - failed_op = failed_ops[0] - print(f" Operation: {failed_op.operation_name}") - print(f" Duration: {failed_op.duration_ms:.2f} ms") - print(f" Error: {failed_op.metadata.get('error', 'N/A')}") - - # Test 4: Get recent operations - print(f"\n{'='*60}") - print("Test 4: Recent Operations") - print(f"{'='*60}") - - recent = performance_monitor.get_recent_operations(limit=5) - print(f"\nLast {len(recent)} operations:") - for op in recent: - status = "[OK]" if op.success else "[X]" - print(f" {status} {op.operation_name}: {op.duration_ms:.2f} ms") - - # Test 5: Summary statistics - print(f"\n{'='*60}") - print("Test 5: Summary Statistics") - print(f"{'='*60}") - - summary = performance_monitor.get_summary() - print(f"\nOverall Summary:") - print(f" Total operations: {summary['total_operations']}") - print(f" Unique operations: {summary['unique_operations']}") - print(f" Total duration: {summary['total_duration_ms']:.2f} ms") - print(f" Average duration: {summary['avg_duration_ms']:.2f} ms") - print(f" Success rate: {summary['success_rate']:.1f}%") - print(f" Most frequent: {summary['most_frequent']}") - print(f" Slowest: {summary['slowest']}") - - print(f"\n{'='*60}") - print("[PASS] All performance monitoring tests completed successfully!") - print(f"{'='*60}") - - return summary['total_operations'] > 0 - -def test_metadata_tracking(): - """Test that metadata is tracked correctly.""" - print(f"\n{'='*60}") - print("Test 6: Metadata Tracking") - print(f"{'='*60}") - - with performance_monitor.track_operation("metadata_test", - file_size=1024, - format="ogip"): - time.sleep(0.05) - - recent = performance_monitor.get_recent_operations(limit=1) - if recent: - op = recent[0] - print(f"\nOperation: {op.operation_name}") - print(f"Metadata:") - for key, value in op.metadata.items(): - print(f" {key}: {value}") - - print(f"\n{'='*60}") - print("[PASS] Metadata tracking test completed successfully!") - print(f"{'='*60}") - - return True - -def test_slow_operations(): - """Test identification of slow operations.""" - print(f"\n{'='*60}") - print("Test 7: Slow Operations Detection") - print(f"{'='*60}") - - # Create some fast and slow operations - with performance_monitor.track_operation("fast_operation"): - time.sleep(0.01) - - with performance_monitor.track_operation("slow_operation"): - time.sleep(0.15) - - # Get slow operations (threshold: 100ms) - slow_ops = performance_monitor.get_slow_operations(threshold_ms=100.0, limit=10) - print(f"\nOperations slower than 100ms: {len(slow_ops)}") - for op in slow_ops: - print(f" {op.operation_name}: {op.duration_ms:.2f} ms") - - print(f"\n{'='*60}") - print("[PASS] Slow operations detection test completed successfully!") - print(f"{'='*60}") - - return len(slow_ops) > 0 - -if __name__ == "__main__": - try: - # Run tests - test1_passed = test_operation_tracking() - test2_passed = test_metadata_tracking() - test3_passed = test_slow_operations() - - # Summary - print(f"\n{'='*60}") - print("TEST SUMMARY") - print(f"{'='*60}") - print(f"Operation Tracking Test: {'[PASS] PASSED' if test1_passed else '[FAIL] FAILED'}") - print(f"Metadata Tracking Test: {'[PASS] PASSED' if test2_passed else '[FAIL] FAILED'}") - print(f"Slow Operations Test: {'[PASS] PASSED' if test3_passed else '[FAIL] FAILED'}") - print(f"{'='*60}\n") - - if test1_passed and test2_passed and test3_passed: - print("SUCCESS: All tests passed! Performance monitoring is working correctly.") - sys.exit(0) - else: - print("[WARN] Some tests failed. Please review the output above.") - sys.exit(1) - - except Exception as e: - print(f"\n[FAIL] Test failed with error: {e}") - import traceback - traceback.print_exc() - sys.exit(1) diff --git a/tests/test_reactive_updates.py b/tests/test_reactive_updates.py deleted file mode 100644 index 5c15d70..0000000 --- a/tests/test_reactive_updates.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -Test Script for Reactive State Updates - -This script tests that the StateManager's reactive parameters trigger -UI updates when state changes occur. -""" - -import sys -import os - -# Add parent directory to path -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from utils.state_manager import StateManager -from stingray.events import EventList -import numpy as np - -def test_reactive_parameters(): - """Test that reactive parameters update correctly.""" - print("=" * 60) - print("Testing Reactive State Updates") - print("=" * 60) - - # Create state manager - state = StateManager() - - print(f"\nInitial state:") - print(f" Event data count: {state.event_data_count}") - print(f" Light curve count: {state.light_curve_count}") - print(f" Timeseries count: {state.timeseries_count}") - print(f" Memory usage: {state.memory_usage_mb:.2f} MB") - print(f" Last operation: '{state.last_operation}'") - - # Test 1: Add event data - print(f"\n{'='*60}") - print("Test 1: Adding Event Data") - print(f"{'='*60}") - - # Create mock event list - times = np.arange(0, 100, 0.1) - event_list = EventList(times, gti=np.array([[0, 100]])) - - # Add to state - state.add_event_data("test_event_1", event_list) - - print(f"\nAfter adding 1 event list:") - print(f" Event data count: {state.event_data_count}") - print(f" Last operation: '{state.last_operation}'") - - # Test 2: Add more event data - print(f"\n{'='*60}") - print("Test 2: Adding More Event Data") - print(f"{'='*60}") - - event_list_2 = EventList(times, gti=np.array([[0, 100]])) - state.add_event_data("test_event_2", event_list_2) - - print(f"\nAfter adding 2nd event list:") - print(f" Event data count: {state.event_data_count}") - print(f" Last operation: '{state.last_operation}'") - - # Test 3: Remove event data - print(f"\n{'='*60}") - print("Test 3: Removing Event Data") - print(f"{'='*60}") - - state.remove_event_data("test_event_1") - - print(f"\nAfter removing 1 event list:") - print(f" Event data count: {state.event_data_count}") - print(f" Last operation: '{state.last_operation}'") - - # Test 4: Clear all data - print(f"\n{'='*60}") - print("Test 4: Clearing All Data") - print(f"{'='*60}") - - state.clear_event_data() - - print(f"\nAfter clearing all event data:") - print(f" Event data count: {state.event_data_count}") - print(f" Last operation: '{state.last_operation}'") - - # Test 5: Test stats - print(f"\n{'='*60}") - print("Test 5: State Statistics") - print(f"{'='*60}") - - # Add some data again - state.add_event_data("test_1", event_list) - state.add_event_data("test_2", event_list_2) - - stats = state.get_stats() - print(f"\nState statistics:") - for key, value in stats.items(): - print(f" {key}: {value}") - - print(f"\n{'='*60}") - print("[PASS] All reactive update tests completed successfully!") - print(f"{'='*60}") - - return True - -def test_param_watchers(): - """Test that param watchers can be attached and triggered.""" - print(f"\n{'='*60}") - print("Test 6: Parameter Watchers") - print(f"{'='*60}") - - state = StateManager() - - # Track changes - changes = [] - - def on_event_count_change(event): - changes.append(('event_data_count', event.new)) - print(f" [OK] event_data_count changed to: {event.new}") - - def on_operation_change(event): - changes.append(('last_operation', event.new)) - print(f" [OK] last_operation changed to: '{event.new}'") - - # Attach watchers - state.param.watch(on_event_count_change, 'event_data_count') - state.param.watch(on_operation_change, 'last_operation') - - print("\nAttached watchers. Now adding event data...") - - # Create and add event list - times = np.arange(0, 100, 0.1) - event_list = EventList(times, gti=np.array([[0, 100]])) - state.add_event_data("watched_event", event_list) - - print(f"\nChanges detected: {len(changes)}") - for param_name, value in changes: - print(f" - {param_name}: {value}") - - print(f"\n{'='*60}") - print("[PASS] Parameter watcher tests completed successfully!") - print(f"{'='*60}") - - return len(changes) > 0 - -if __name__ == "__main__": - try: - # Run tests - test1_passed = test_reactive_parameters() - test2_passed = test_param_watchers() - - # Summary - print(f"\n{'='*60}") - print("TEST SUMMARY") - print(f"{'='*60}") - print(f"Reactive Parameters Test: {'[PASS] PASSED' if test1_passed else '[FAIL] FAILED'}") - print(f"Parameter Watchers Test: {'[PASS] PASSED' if test2_passed else '[FAIL] FAILED'}") - print(f"{'='*60}\n") - - if test1_passed and test2_passed: - print("SUCCESS: All tests passed! Reactive state updates are working correctly.") - sys.exit(0) - else: - print("[WARN] Some tests failed. Please review the output above.") - sys.exit(1) - - except Exception as e: - print(f"\n[FAIL] Test failed with error: {e}") - import traceback - traceback.print_exc() - sys.exit(1) diff --git a/tests/test_state_manager.py b/tests/test_state_manager.py deleted file mode 100644 index f8cb940..0000000 --- a/tests/test_state_manager.py +++ /dev/null @@ -1,480 +0,0 @@ -""" -Unit tests for the StateManager class. - -This test suite covers: -- Event data management (add, get, remove, update, clear) -- Light curve management -- Time series management -- Memory limits and LRU eviction -- Observer pattern -- Error handling and validation -""" - -import pytest -from unittest.mock import MagicMock, patch -from utils.state_manager import StateManager - - -# ============================================================================= -# Fixtures -# ============================================================================= - -@pytest.fixture -def state_manager(): - """Create a fresh StateManager instance for each test.""" - return StateManager() - - -@pytest.fixture -def mock_event_list(): - """Create a mock EventList object.""" - mock = MagicMock() - mock.__sizeof__ = MagicMock(return_value=1024 * 1024) # 1 MB - return mock - - -@pytest.fixture -def mock_light_curve(): - """Create a mock Lightcurve object.""" - mock = MagicMock() - mock.__sizeof__ = MagicMock(return_value=512 * 1024) # 512 KB - return mock - - -@pytest.fixture -def mock_timeseries(): - """Create a mock timeseries object.""" - mock = MagicMock() - mock.__sizeof__ = MagicMock(return_value=256 * 1024) # 256 KB - return mock - - -# ============================================================================= -# Event Data Management Tests -# ============================================================================= - -class TestEventDataManagement: - """Tests for event data management methods.""" - - def test_add_event_data_success(self, state_manager, mock_event_list): - """Test successfully adding event data.""" - state_manager.add_event_data("test_event", mock_event_list) - - assert state_manager.has_event_data("test_event") - assert state_manager.get_event_data("test_event") == mock_event_list - assert len(state_manager.get_event_data()) == 1 - - def test_add_event_data_duplicate_name_raises_error(self, state_manager, mock_event_list): - """Test that adding duplicate name raises ValueError.""" - state_manager.add_event_data("test_event", mock_event_list) - - with pytest.raises(ValueError, match="already exists"): - state_manager.add_event_data("test_event", mock_event_list) - - def test_add_event_data_empty_name_raises_error(self, state_manager, mock_event_list): - """Test that empty name raises ValueError.""" - with pytest.raises(ValueError, match="cannot be empty"): - state_manager.add_event_data("", mock_event_list) - - with pytest.raises(ValueError, match="cannot be empty"): - state_manager.add_event_data(" ", mock_event_list) - - def test_get_event_data_by_name(self, state_manager, mock_event_list): - """Test retrieving event data by name.""" - state_manager.add_event_data("test_event", mock_event_list) - - result = state_manager.get_event_data("test_event") - assert result == mock_event_list - - def test_get_event_data_nonexistent_returns_none(self, state_manager): - """Test that getting nonexistent data returns None.""" - result = state_manager.get_event_data("nonexistent") - assert result is None - - def test_get_all_event_data(self, state_manager, mock_event_list): - """Test retrieving all event data.""" - mock_event_list2 = MagicMock() - mock_event_list2.__sizeof__ = MagicMock(return_value=1024 * 1024) - - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_event_data("event2", mock_event_list2) - - all_data = state_manager.get_event_data() - assert len(all_data) == 2 - assert all_data[0] == ("event1", mock_event_list) - assert all_data[1] == ("event2", mock_event_list2) - - def test_get_event_data_names(self, state_manager, mock_event_list): - """Test retrieving all event data names.""" - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_event_data("event2", mock_event_list) - - names = state_manager.get_event_data_names() - assert names == ["event1", "event2"] - - def test_remove_event_data_success(self, state_manager, mock_event_list): - """Test successfully removing event data.""" - state_manager.add_event_data("test_event", mock_event_list) - - result = state_manager.remove_event_data("test_event") - assert result is True - assert not state_manager.has_event_data("test_event") - - def test_remove_event_data_nonexistent_returns_false(self, state_manager): - """Test that removing nonexistent data returns False.""" - result = state_manager.remove_event_data("nonexistent") - assert result is False - - def test_update_event_data_success(self, state_manager, mock_event_list): - """Test successfully updating event data.""" - state_manager.add_event_data("test_event", mock_event_list) - - new_mock = MagicMock() - new_mock.__sizeof__ = MagicMock(return_value=1024 * 1024) - - result = state_manager.update_event_data("test_event", new_mock) - assert result is True - assert state_manager.get_event_data("test_event") == new_mock - - def test_update_event_data_nonexistent_returns_false(self, state_manager, mock_event_list): - """Test that updating nonexistent data returns False.""" - result = state_manager.update_event_data("nonexistent", mock_event_list) - assert result is False - - def test_clear_event_data(self, state_manager, mock_event_list): - """Test clearing all event data.""" - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_event_data("event2", mock_event_list) - - state_manager.clear_event_data() - assert len(state_manager.get_event_data()) == 0 - - def test_has_event_data(self, state_manager, mock_event_list): - """Test checking if event data exists.""" - assert not state_manager.has_event_data("test_event") - - state_manager.add_event_data("test_event", mock_event_list) - assert state_manager.has_event_data("test_event") - - -# ============================================================================= -# Light Curve Management Tests -# ============================================================================= - -class TestLightCurveManagement: - """Tests for light curve management methods.""" - - def test_add_light_curve_success(self, state_manager, mock_light_curve): - """Test successfully adding light curve.""" - state_manager.add_light_curve("test_lc", mock_light_curve) - - assert state_manager.has_light_curve("test_lc") - assert state_manager.get_light_curve("test_lc") == mock_light_curve - - def test_add_light_curve_duplicate_raises_error(self, state_manager, mock_light_curve): - """Test that adding duplicate name raises ValueError.""" - state_manager.add_light_curve("test_lc", mock_light_curve) - - with pytest.raises(ValueError, match="already exists"): - state_manager.add_light_curve("test_lc", mock_light_curve) - - def test_get_all_light_curves(self, state_manager, mock_light_curve): - """Test retrieving all light curves.""" - state_manager.add_light_curve("lc1", mock_light_curve) - state_manager.add_light_curve("lc2", mock_light_curve) - - all_lcs = state_manager.get_light_curve() - assert len(all_lcs) == 2 - - def test_get_light_curve_names(self, state_manager, mock_light_curve): - """Test retrieving all light curve names.""" - state_manager.add_light_curve("lc1", mock_light_curve) - state_manager.add_light_curve("lc2", mock_light_curve) - - names = state_manager.get_light_curve_names() - assert names == ["lc1", "lc2"] - - def test_remove_light_curve(self, state_manager, mock_light_curve): - """Test removing light curve.""" - state_manager.add_light_curve("test_lc", mock_light_curve) - - result = state_manager.remove_light_curve("test_lc") - assert result is True - assert not state_manager.has_light_curve("test_lc") - - def test_update_light_curve(self, state_manager, mock_light_curve): - """Test updating light curve.""" - state_manager.add_light_curve("test_lc", mock_light_curve) - - new_mock = MagicMock() - new_mock.__sizeof__ = MagicMock(return_value=512 * 1024) - - result = state_manager.update_light_curve("test_lc", new_mock) - assert result is True - assert state_manager.get_light_curve("test_lc") == new_mock - - def test_clear_light_curves(self, state_manager, mock_light_curve): - """Test clearing all light curves.""" - state_manager.add_light_curve("lc1", mock_light_curve) - state_manager.add_light_curve("lc2", mock_light_curve) - - state_manager.clear_light_curves() - assert len(state_manager.get_light_curve()) == 0 - - -# ============================================================================= -# Time Series Management Tests -# ============================================================================= - -class TestTimeSeriesManagement: - """Tests for time series management methods.""" - - def test_add_timeseries_success(self, state_manager, mock_timeseries): - """Test successfully adding timeseries.""" - state_manager.add_timeseries_data("test_ts", mock_timeseries) - - assert state_manager.has_timeseries_data("test_ts") - assert state_manager.get_timeseries_data("test_ts") == mock_timeseries - - def test_add_timeseries_duplicate_raises_error(self, state_manager, mock_timeseries): - """Test that adding duplicate name raises ValueError.""" - state_manager.add_timeseries_data("test_ts", mock_timeseries) - - with pytest.raises(ValueError, match="already exists"): - state_manager.add_timeseries_data("test_ts", mock_timeseries) - - def test_remove_timeseries(self, state_manager, mock_timeseries): - """Test removing timeseries.""" - state_manager.add_timeseries_data("test_ts", mock_timeseries) - - result = state_manager.remove_timeseries_data("test_ts") - assert result is True - assert not state_manager.has_timeseries_data("test_ts") - - -# ============================================================================= -# Memory Management Tests -# ============================================================================= - -class TestMemoryManagement: - """Tests for memory management features.""" - - def test_max_event_lists_eviction(self, state_manager, mock_event_list): - """Test that oldest event is evicted when MAX_EVENT_LISTS is reached.""" - state_manager.MAX_EVENT_LISTS = 3 - - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_event_data("event2", mock_event_list) - state_manager.add_event_data("event3", mock_event_list) - - # Adding 4th should evict event1 - state_manager.add_event_data("event4", mock_event_list) - - assert not state_manager.has_event_data("event1") - assert state_manager.has_event_data("event2") - assert state_manager.has_event_data("event3") - assert state_manager.has_event_data("event4") - assert len(state_manager.get_event_data()) == 3 - - def test_memory_usage_calculation(self, state_manager, mock_event_list): - """Test memory usage calculation.""" - state_manager.add_event_data("event1", mock_event_list) - - usage = state_manager.get_memory_usage() - assert usage['current_mb'] > 0 - assert usage['max_mb'] > 0 - assert 0 <= usage['usage_percent'] <= 100 - - @patch('psutil.virtual_memory') - def test_dynamic_memory_limit(self, mock_vm): - """Test that memory limit is dynamically calculated from system RAM.""" - # Mock system with 16 GB RAM - mock_vm.return_value.total = 16 * 1024 * 1024 * 1024 # 16 GB in bytes - - sm = StateManager() - - # Should be 80% of 16 GB = 12.8 GB = 13107.2 MB - expected_limit = (16 * 1024 * 0.80) - assert abs(sm.MAX_MEMORY_MB - expected_limit) < 1 # Allow small rounding error - - def test_set_memory_usage_percent(self, state_manager): - """Test changing memory usage percentage.""" - original_limit = state_manager.MAX_MEMORY_MB - - state_manager.set_memory_usage_percent(0.50) # 50% - - # New limit should be approximately half of 80% limit - assert state_manager.MAX_MEMORY_MB < original_limit - - def test_set_memory_usage_percent_invalid_raises_error(self, state_manager): - """Test that invalid percentage raises ValueError.""" - with pytest.raises(ValueError, match="between 0.1"): - state_manager.set_memory_usage_percent(0.05) # Too low - - with pytest.raises(ValueError, match="between 0.1"): - state_manager.set_memory_usage_percent(1.5) # Too high - - def test_get_system_memory_info(self, state_manager): - """Test getting system memory information.""" - info = state_manager.get_system_memory_info() - - assert 'total_mb' in info - assert 'available_mb' in info - assert 'allocated_limit_mb' in info - assert info['total_mb'] > 0 - - -# ============================================================================= -# Observer Pattern Tests -# ============================================================================= - -class TestObserverPattern: - """Tests for observer pattern implementation.""" - - def test_register_observer(self, state_manager): - """Test registering an observer.""" - callback = MagicMock() - - state_manager.register_observer(callback) - assert callback in state_manager._observers - - def test_observer_notified_on_add(self, state_manager, mock_event_list): - """Test that observers are notified when data is added.""" - callback = MagicMock() - state_manager.register_observer(callback) - - state_manager.add_event_data("test_event", mock_event_list) - - callback.assert_called_once() - call_args = callback.call_args[0] - assert call_args[0] == 'event_data_added' - assert call_args[1]['name'] == 'test_event' - - def test_observer_notified_on_remove(self, state_manager, mock_event_list): - """Test that observers are notified when data is removed.""" - callback = MagicMock() - state_manager.add_event_data("test_event", mock_event_list) - - state_manager.register_observer(callback) - state_manager.remove_event_data("test_event") - - callback.assert_called_once() - call_args = callback.call_args[0] - assert call_args[0] == 'event_data_removed' - - def test_observer_notified_on_clear(self, state_manager, mock_event_list): - """Test that observers are notified when data is cleared.""" - callback = MagicMock() - state_manager.add_event_data("test_event", mock_event_list) - - state_manager.register_observer(callback) - state_manager.clear_event_data() - - callback.assert_called_once() - call_args = callback.call_args[0] - assert call_args[0] == 'event_data_cleared' - - def test_unregister_observer(self, state_manager, mock_event_list): - """Test unregistering an observer.""" - callback = MagicMock() - state_manager.register_observer(callback) - state_manager.unregister_observer(callback) - - assert callback not in state_manager._observers - - # Should not be called after unregistering - state_manager.add_event_data("test_event", mock_event_list) - callback.assert_not_called() - - def test_observer_error_doesnt_break_state(self, state_manager, mock_event_list): - """Test that errors in observers don't break state management.""" - def bad_callback(event_type, data): - raise Exception("Observer error!") - - state_manager.register_observer(bad_callback) - - # Should not raise exception - state_manager.add_event_data("test_event", mock_event_list) - - # Data should still be added - assert state_manager.has_event_data("test_event") - - -# ============================================================================= -# Clear All Tests -# ============================================================================= - -class TestClearAll: - """Tests for clearing all state.""" - - def test_clear_all(self, state_manager, mock_event_list, mock_light_curve, mock_timeseries): - """Test clearing all state data.""" - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_light_curve("lc1", mock_light_curve) - state_manager.add_timeseries_data("ts1", mock_timeseries) - - state_manager.clear_all() - - assert len(state_manager.get_event_data()) == 0 - assert len(state_manager.get_light_curve()) == 0 - assert len(state_manager.get_timeseries_data()) == 0 - - -# ============================================================================= -# Statistics Tests -# ============================================================================= - -class TestStatistics: - """Tests for statistics and info methods.""" - - def test_get_stats(self, state_manager, mock_event_list): - """Test getting statistics.""" - state_manager.add_event_data("event1", mock_event_list) - - stats = state_manager.get_stats() - - assert stats['total_additions'] >= 1 - assert stats['event_data_count'] == 1 - assert stats['total_items'] == 1 - assert 'memory_usage' in stats - assert 'system_memory' in stats - - def test_repr(self, state_manager, mock_event_list): - """Test string representation.""" - state_manager.add_event_data("event1", mock_event_list) - - repr_str = repr(state_manager) - assert "StateManager" in repr_str - assert "event_data=1" in repr_str - - -# ============================================================================= -# Integration Tests -# ============================================================================= - -class TestIntegration: - """Integration tests for combined operations.""" - - def test_mixed_data_types(self, state_manager, mock_event_list, mock_light_curve, mock_timeseries): - """Test managing multiple data types simultaneously.""" - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_light_curve("lc1", mock_light_curve) - state_manager.add_timeseries_data("ts1", mock_timeseries) - - assert state_manager.has_event_data("event1") - assert state_manager.has_light_curve("lc1") - assert state_manager.has_timeseries_data("ts1") - - stats = state_manager.get_stats() - assert stats['total_items'] == 3 - - def test_eviction_statistics(self, state_manager, mock_event_list): - """Test that eviction updates statistics.""" - state_manager.MAX_EVENT_LISTS = 2 - - state_manager.add_event_data("event1", mock_event_list) - state_manager.add_event_data("event2", mock_event_list) - state_manager.add_event_data("event3", mock_event_list) # Should trigger eviction - - stats = state_manager.get_stats() - assert stats['total_evictions'] == 1 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..adcc688 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@components/*": ["src/components/*"], + "@pages/*": ["src/pages/*"], + "@api/*": ["src/api/*"], + "@store/*": ["src/store/*"], + "@hooks/*": ["src/hooks/*"], + "@types/*": ["src/types/*"], + "@utils/*": ["src/utils/*"] + } + }, + "include": ["src/**/*", "electron/**/*"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..ad7bc16 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts", "electron.vite.config.ts", "vitest.config.ts"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1a85fd7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + test: { + environment: 'jsdom', + // Required for @testing-library/react v16 auto-cleanup between tests + globals: true, + setupFiles: ['src/test/setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + }, +});