diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100755 index fc57f110..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,34 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/python -{ - "name": "Python 3", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", - "features": { - "ghcr.io/devcontainers-contrib/features/poetry:2": {}, - "ghcr.io/devcontainers-contrib/features/tox:2": {} - }, - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "pip3 install --user -r requirements.txt", - - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-vscode.makefile-tools" - ] - } - }, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6f0efd3e..8f6f6777 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,6 +1,8 @@ name: unit tests on: [push, pull_request] +permissions: + contents: read jobs: test: @@ -11,10 +13,6 @@ jobs: python-version: ["3.12", "3.13", "3.14"] steps: - - uses: awalsh128/cache-apt-pkgs-action@latest - with: - packages: freeglut3-dev - if: matrix.os == 'ubuntu-latest' - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v6 @@ -31,7 +29,6 @@ jobs: uv run black . --check --diff --color uv run ruff check uv run py.test --mpl --mypy --cov src --cov-append --cov-report=term-missing --junitxml=junit.xml - - name: Store coverage results as artifacts uses: actions/upload-artifact@v4 with: @@ -40,9 +37,10 @@ jobs: include-hidden-files: true if: matrix.os == 'ubuntu-latest' - name: Upload test results to Codecov - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} coverage-combine: @@ -54,6 +52,8 @@ jobs: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v6 + with: + enable-cache: true - name: Download coverage results uses: actions/download-artifact@v4 with: diff --git a/docs/api/aimbat/_tui.md b/docs/api/aimbat/_tui.md new file mode 100644 index 00000000..cceb3fc0 --- /dev/null +++ b/docs/api/aimbat/_tui.md @@ -0,0 +1,9 @@ +::: aimbat._tui + options: + heading_level: 1 + toc_label: aimbat._tui + show_root_heading: true + show_root_toc_entry: true + inherited_members: true + show_submodules: true + summary: true diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 235e43a7..00000000 --- a/flake.lock +++ /dev/null @@ -1,111 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixgl": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1762090880, - "narHash": "sha256-fbRQzIGPkjZa83MowjbD2ALaJf9y6KMDdJBQMKFeY/8=", - "owner": "nix-community", - "repo": "nixgl", - "rev": "b6105297e6f0cd041670c3e8628394d4ee247ed5", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixgl", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1746378225, - "narHash": "sha256-OeRSuL8PUjIfL3Q0fTbNJD/fmv1R+K2JAOqWJd3Oceg=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "93e8cdce7afc64297cfec447c311470788131cd9", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1771848320, - "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "2fc6539b481e1d2569f25f8799236694180c0993", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixgl": "nixgl", - "nixpkgs": "nixpkgs_2", - "systems": "systems_2" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index d9b1ea80..00000000 --- a/flake.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - description = "aimbat dev shell"; - - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - systems.url = "github:nix-systems/default"; - nixgl.url = "github:nix-community/nixgl"; - }; - - outputs = { - nixpkgs, - systems, - nixgl, - ... - }: let - eachSystem = nixpkgs.lib.genAttrs (import systems); - in { - devShells = eachSystem (system: let - pkgs = import nixpkgs { - inherit system; - overlays = [nixgl.overlay]; - }; - in { - default = pkgs.mkShell { - nativeBuildInputs = with pkgs; [ - bashInteractive - sqlitebrowser - uv - ruff - (python314.withPackages (ps: with ps; [tox])) - python313 - python312 - gnumake - ]; - - shellHook = '' - export LD_LIBRARY_PATH=${with pkgs; - lib.makeLibraryPath [ - stdenv.cc.cc.lib - zlib - zstd - libX11 - libGL - glib - libxkbcommon - fontconfig - freetype - dbus - wayland - ]}:$LD_LIBRARY_PATH - export UV_PYTHON=$(which python3.14) - export UV_NO_MANAGED_PYTHON=true - [ ! -d .venv ] && uv venv --system-site-packages - uv sync --locked --all-extras - export MPLBACKEND=QtAgg - source .venv/bin/activate - ''; - }; - }); - formatter = eachSystem (system: (import nixpkgs {inherit system;}).nixpkgs-fmt); - }; -} diff --git a/pyproject.toml b/pyproject.toml index f8f7e467..73cd6e81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,12 +22,12 @@ dependencies = [ "sqlmodel>=0.0.24", "rich>=13.9.4", "matplotlib>=3.10.6", - "pyside6>=6.9.2", - "pyqtgraph>=0.13.7", "cyclopts>=3.24.0", "pydantic-settings>=2.10.1", "pandas>=3.0.1", "pandas-stubs>=3.0.0.260204", + "textual>=8.0.0", + "textual-fspicker>=1.0.0", ] [project.urls] @@ -35,6 +35,7 @@ Documentation = "https://aimbat.pysmo.org" [project.scripts] aimbat = "aimbat.app:app" +aimbat-tui = "aimbat._tui.app:main" [dependency-groups] test = [ @@ -43,12 +44,12 @@ test = [ "pytest-dependency>=0.6.0", "pytest-mpl>=0.17.0", "pytest-mypy>=1.0.1", - "pytest-qt>=4.5.0", "pytest-sugar>=1.1.1", "ruff>=0.13.0", ] dev = [ "black>=26.1.0", + "textual-dev>=1.8.0", ] docs = [ "git-cliff>=2.12.0", diff --git a/src/aimbat/_cli/common.py b/src/aimbat/_cli/common.py index 5bbaedaf..ababcc53 100644 --- a/src/aimbat/_cli/common.py +++ b/src/aimbat/_cli/common.py @@ -25,13 +25,6 @@ def __post_init__(self) -> None: configure_logging() -@Parameter(name="*") -@dataclass -class PlotParameters: - use_qt: bool = False - "Use pyqtgraph instead of matplotlib for plots (where applicable)." - - @Parameter(name="*") @dataclass class IccsPlotParameters: diff --git a/src/aimbat/_cli/event.py b/src/aimbat/_cli/event.py index 8f78822b..66ba82b4 100644 --- a/src/aimbat/_cli/event.py +++ b/src/aimbat/_cli/event.py @@ -118,7 +118,7 @@ def cli_event_parameter_get( @simple_exception def cli_event_parameter_set( name: EventParameter, - value: Timedelta | str, + value: str, *, global_parameters: GlobalParameters = GlobalParameters(), ) -> None: @@ -126,14 +126,23 @@ def cli_event_parameter_set( Args: name: Event parameter name. - value: Event parameter value. + value: Event parameter value. For timedelta parameters, bare numbers + are interpreted as seconds. """ from aimbat.db import engine from aimbat.core import set_event_parameter from sqlmodel import Session + _TIMEDELTA_PARAMS = (EventParameter.WINDOW_PRE, EventParameter.WINDOW_POST) + parsed_value: Timedelta | str = value + if name in _TIMEDELTA_PARAMS: + try: + parsed_value = Timedelta(seconds=float(value)) + except ValueError: + parsed_value = Timedelta(value) + with Session(engine) as session: - set_event_parameter(session, name, value) + set_event_parameter(session, name, parsed_value) @parameter.command(name="dump") diff --git a/src/aimbat/_cli/pick.py b/src/aimbat/_cli/pick.py index 50034bdd..05b57ac0 100644 --- a/src/aimbat/_cli/pick.py +++ b/src/aimbat/_cli/pick.py @@ -41,6 +41,7 @@ def cli_update_phase_pick( iccs_parameters.context, iccs_parameters.all, use_seismogram_image, + return_fig=False, ) @@ -73,6 +74,7 @@ def cli_pick_timewindow( iccs_parameters.context, iccs_parameters.all, use_seismogram_image, + return_fig=False, ) @@ -95,7 +97,13 @@ def cli_pick_min_ccnorm( with Session(engine) as session: iccs = create_iccs_instance(session) - update_min_ccnorm(session, iccs, iccs_parameters.context, iccs_parameters.all) + update_min_ccnorm( + session, + iccs, + iccs_parameters.context, + iccs_parameters.all, + return_fig=False, + ) if __name__ == "__main__": diff --git a/src/aimbat/_cli/plot.py b/src/aimbat/_cli/plot.py index d8b7b037..7c50e3c4 100644 --- a/src/aimbat/_cli/plot.py +++ b/src/aimbat/_cli/plot.py @@ -13,7 +13,6 @@ from .common import ( GlobalParameters, IccsPlotParameters, - PlotParameters, simple_exception, ) from cyclopts import App @@ -25,23 +24,15 @@ @simple_exception def cli_seismogram_plot( *, - plot_parameters: PlotParameters = PlotParameters(), global_parameters: GlobalParameters = GlobalParameters(), ) -> None: """Plot raw seismograms for the active event sorted by epicentral distance.""" from aimbat.db import engine from aimbat.core import plot_all_seismograms from sqlmodel import Session - import pyqtgraph as pg # type: ignore - - if plot_parameters.use_qt: - pg.mkQApp() with Session(engine) as session: - plot_all_seismograms(session, plot_parameters.use_qt) - - if plot_parameters.use_qt: - pg.exec() + plot_all_seismograms(session, return_fig=False) @app.command(name="stack") @@ -58,7 +49,7 @@ def cli_iccs_plot_stack( with Session(engine) as session: iccs = create_iccs_instance(session) - plot_stack(iccs, iccs_parameters.context, iccs_parameters.all) + plot_stack(iccs, iccs_parameters.context, iccs_parameters.all, return_fig=False) @app.command(name="image") @@ -75,7 +66,9 @@ def cli_iccs_plot_image( with Session(engine) as session: iccs = create_iccs_instance(session) - plot_iccs_seismograms(iccs, iccs_parameters.context, iccs_parameters.all) + plot_iccs_seismograms( + iccs, iccs_parameters.context, iccs_parameters.all, return_fig=False + ) if __name__ == "__main__": diff --git a/src/aimbat/_cli/seismogram.py b/src/aimbat/_cli/seismogram.py index 449d8c79..be007cf2 100644 --- a/src/aimbat/_cli/seismogram.py +++ b/src/aimbat/_cli/seismogram.py @@ -116,6 +116,22 @@ def cli_seismogram_parameter_set( set_seismogram_parameter_by_id(session, seismogram_id, name, value) +@parameter.command(name="reset") +@simple_exception +def cli_seismogram_parameter_reset( + seismogram_id: Annotated[uuid.UUID, id_parameter(AimbatSeismogram)], + *, + global_parameters: GlobalParameters = GlobalParameters(), +) -> None: + """Reset all processing parameters to their default values.""" + from aimbat.db import engine + from aimbat.core import reset_seismogram_parameters_by_id + from sqlmodel import Session + + with Session(engine) as session: + reset_seismogram_parameters_by_id(session, seismogram_id) + + @parameter.command(name="dump") @simple_exception def cli_seismogram_parameter_dump( diff --git a/src/aimbat/_cli/snapshot.py b/src/aimbat/_cli/snapshot.py index a5a6cf51..10f39f02 100644 --- a/src/aimbat/_cli/snapshot.py +++ b/src/aimbat/_cli/snapshot.py @@ -108,5 +108,23 @@ def cli_snapshot_list( print_snapshot_table(session, table_parameters.short, all_events) +@app.command(name="details") +@simple_exception +def cli_snapshot_details( + snapshot_id: Annotated[uuid.UUID, id_parameter(AimbatSnapshot)], + table_parameters: TableParameters = TableParameters(), + global_parameters: GlobalParameters = GlobalParameters(), +) -> None: + """Print information on the event parameters saved in a snapshot.""" + from aimbat.db import engine + from aimbat.core import print_snapshot_parameters_table_by_id + from sqlmodel import Session + + with Session(engine) as session: + print_snapshot_parameters_table_by_id( + session, snapshot_id, table_parameters.short + ) + + if __name__ == "__main__": app() diff --git a/src/aimbat/_config.py b/src/aimbat/_config.py index a70baf56..30e34bbb 100644 --- a/src/aimbat/_config.py +++ b/src/aimbat/_config.py @@ -107,6 +107,17 @@ class Settings(BaseSettings): description="Initial relative begin time of window.", ) + tui_dark_theme: str = Field( + default="catppuccin-mocha", + min_length=1, + description="TUI dark theme name (from available Textual themes).", + ) + tui_light_theme: str = Field( + default="catppuccin-latte", + min_length=1, + description="TUI light theme name (from available Textual themes).", + ) + @model_validator(mode="after") def set_computed_defaults(self) -> Self: """Set defaults that depend on other fields.""" diff --git a/src/aimbat/_tui/__init__.py b/src/aimbat/_tui/__init__.py new file mode 100644 index 00000000..2f69d1fb --- /dev/null +++ b/src/aimbat/_tui/__init__.py @@ -0,0 +1 @@ +"""AIMBAT Terminal User Interface.""" diff --git a/src/aimbat/_tui/_widgets.py b/src/aimbat/_tui/_widgets.py new file mode 100644 index 00000000..aea768fc --- /dev/null +++ b/src/aimbat/_tui/_widgets.py @@ -0,0 +1,30 @@ +"""Reusable Textual widgets for the AIMBAT TUI.""" + +from textual.binding import Binding +from textual.widgets import DataTable + +__all__ = ["VimDataTable"] + + +class VimDataTable(DataTable): + """DataTable with vim-style navigation keys. + + Adds j/k for cursor down/up, h/l for cursor left/right (aliases for the + existing cursor actions) and g/G for jumping to the first/last row. + """ + + BINDINGS = [ + Binding("h", "cursor_left", "Cursor left", show=False), + Binding("j", "cursor_down", "Cursor down", show=False), + Binding("k", "cursor_up", "Cursor up", show=False), + Binding("l", "cursor_right", "Cursor right", show=False), + Binding("g", "scroll_home", "Scroll to top", show=False), + Binding("G", "scroll_end", "Scroll to bottom", show=False), + ] + + def action_scroll_home(self) -> None: + self.move_cursor(row=0) + + def action_scroll_end(self) -> None: + if self.row_count > 0: + self.move_cursor(row=self.row_count - 1) diff --git a/src/aimbat/_tui/aimbat.tcss b/src/aimbat/_tui/aimbat.tcss new file mode 100644 index 00000000..038c866f --- /dev/null +++ b/src/aimbat/_tui/aimbat.tcss @@ -0,0 +1,193 @@ +/* AIMBAT TUI styles — semantic variables are resolved by the active theme. + $primary / $secondary / $accent / $surface etc. are set by the Theme object. */ + +/* ---- Active-event status bar ---- */ +#event-bar { + dock: top; + background: $surface; + color: $accent; + height: 1; + padding: 0 2; + text-style: bold; +} + +TabbedContent { + height: 1fr; +} + +/* ---- Shared modal title + hint styles ---- */ +.modal-title { + width: 100%; + content-align: center middle; + padding-bottom: 1; +} + +.modal-hint { + color: $text-muted; + width: 100%; + content-align: center middle; + padding-top: 1; +} + +/* ---- Confirm modal ---- */ +ConfirmModal { + align: center middle; +} + +#confirm-dialog { + background: $background; + border: solid $warning; + width: 52; + height: auto; + padding: 1 2; +} + +#confirm-dialog .modal-title { + color: $foreground; +} + +/* ---- Event-switcher modal ---- */ +EventSwitcherModal { + align: center middle; +} + +#switcher-dialog { + background: $background; + border: solid $primary; + width: 90%; + max-width: 110; + height: 70%; + padding: 1 2; +} + +#switcher-dialog .modal-title { + color: $secondary; + text-style: bold; +} + +/* ---- Interactive Tools modal ---- */ +InteractiveToolsModal { + align: center middle; +} + +#tools-dialog { + background: $background; + border: solid $primary; + width: 58; + height: auto; + padding: 1 2; +} + +#tools-dialog .modal-title { + color: $secondary; + text-style: bold; +} + +#tools-table { + height: auto; + background: $background; + border: none; +} + +#tools-options { + color: $foreground; + content-align: center middle; + padding-top: 1; + height: auto; +} + +/* ---- Align modal ---- */ +AlignModal { + align: center middle; +} + +#align-dialog { + background: $background; + border: solid $primary; + width: 60; + height: auto; + padding: 1 2; +} + +#align-dialog .modal-title { + color: $secondary; + text-style: bold; +} + +#align-table { + height: auto; + background: $background; + border: none; +} + +#align-options { + color: $foreground; + content-align: center middle; + padding-top: 1; + height: auto; +} + +/* ---- Parameter-edit modal ---- */ +ParameterInputModal, +SnapshotCommentModal { + align: center middle; +} + +#param-edit-dialog { + background: $background; + border: solid $primary; + width: 52; + height: auto; + padding: 1 2; +} + +#param-edit-dialog .modal-title { + color: $secondary; + text-style: bold; +} + +#param-edit-dialog Input { + background: $surface; + border: tall $primary; +} + +/* ---- Snapshot details modal ---- */ +SnapshotDetailsModal { + align: center middle; +} + +#snapshot-details-dialog { + background: $background; + border: solid $primary; + width: 60; + height: auto; + padding: 1 2; +} + +#snapshot-details-dialog .modal-title { + color: $secondary; + text-style: bold; +} + +#snapshot-details-table { + background: $background; + border: none; +} + +/* ---- Row-action menu modal ---- */ +ActionMenuModal { + align: center middle; +} + +#action-menu-dialog { + background: $background; + border: solid $primary; + width: 46; + height: auto; + padding: 1 2; +} + +#action-table { + background: $background; + border: none; +} diff --git a/src/aimbat/_tui/app.py b/src/aimbat/_tui/app.py new file mode 100644 index 00000000..c9a3fa7d --- /dev/null +++ b/src/aimbat/_tui/app.py @@ -0,0 +1,848 @@ +"""AIMBAT Terminal User Interface application.""" + +from __future__ import annotations + +import uuid +from pathlib import Path + +from pydantic import ValidationError + +from rich.console import Console +from rich.panel import Panel + +from pandas import Timedelta +from pysmo.tools.iccs import ICCS +from sqlalchemy.exc import NoResultFound +from sqlmodel import Session +from textual import on, work +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.screen import ModalScreen +from textual.widgets import ( + DataTable, + Footer, + Header, + Static, + TabbedContent, + TabPane, + Tabs, +) +from textual_fspicker import FileOpen, Filters + +from aimbat._types import EventParameter, SeismogramParameter +from aimbat.models._parameters import AimbatEventParametersBase +from aimbat.core import ( + add_data_to_project, + create_iccs_instance, + create_snapshot, + delete_seismogram_by_id, + reset_seismogram_parameters_by_id, + sync_iccs_parameters, + delete_snapshot_by_id, + delete_station_by_id, + get_active_event, + rollback_to_snapshot_by_id, + run_iccs, + run_mccc, + set_event_parameter, + update_min_ccnorm, + update_pick, + update_timewindow, +) +from aimbat import settings +from aimbat.db import engine +from aimbat.io import DataType, DATATYPE_SUFFIXES +from aimbat.models import AimbatSeismogram, AimbatSnapshot +from aimbat._tui._widgets import VimDataTable +from aimbat._tui.modals import ( + ActionMenuModal, + AlignModal, + ConfirmModal, + EventSwitcherModal, + InteractiveToolsModal, + ParameterInputModal, + SnapshotCommentModal, + SnapshotDetailsModal, +) + +_DEFAULT_THEME = settings.tui_dark_theme +_LIGHT_THEME = settings.tui_light_theme + + +# Extend this dict to add new per-row actions to any tab. +_TAB_ROW_ACTIONS: dict[str, list[tuple[str, str]]] = { + "tab-seismograms": [ + ("toggle_select", "Toggle select"), + ("toggle_flip", "Toggle flip"), + ("reset", "Reset parameters"), + ("delete", "Delete seismogram"), + ], + "tab-stations": [("delete", "Delete station and all seismograms")], + "tab-snapshots": [ + ("show_details", "Show details"), + ("rollback", "Rollback to this snapshot"), + ("delete", "Delete snapshot"), + ], +} + + +# --------------------------------------------------------------------------- +# Main application +# --------------------------------------------------------------------------- + + +class AimbatTUI(App[None]): + """AIMBAT Terminal User Interface.""" + + TITLE = "AIMBAT" + CSS_PATH = "aimbat.tcss" + + BINDINGS = [ + Binding("e", "switch_event", "Events", show=True), + Binding("d", "add_data", "Add Data", show=True), + Binding("p", "open_interactive_tools", "Interactive Tools", show=True), + Binding("a", "open_align", "Align", show=True), + Binding("n", "new_snapshot", "New Snapshot", show=True), + Binding("r", "refresh", "Refresh", show=True), + Binding("t", "toggle_theme", "Theme", show=True), + Binding("H", "vim_left", "Vim left", show=False), + Binding("L", "vim_right", "Vim right", show=False), + Binding("q", "quit", "Quit", show=True), + ] + + def compose(self) -> ComposeResult: + yield Header() + yield Static(id="event-bar") + with TabbedContent(initial="tab-seismograms"): + with TabPane("Seismograms", id="tab-seismograms"): + yield VimDataTable(id="seismogram-table") + with TabPane("Parameters", id="tab-parameters"): + yield VimDataTable(id="parameter-table") + with TabPane("Stations", id="tab-stations"): + yield VimDataTable(id="station-table") + with TabPane("Snapshots", id="tab-snapshots"): + yield VimDataTable(id="snapshot-table") + yield Footer() + + def on_mount(self) -> None: + self._iccs: ICCS | None = None + self._active_tab: str = "tab-seismograms" + + self.theme = _DEFAULT_THEME + + self._setup_seismogram_table() + self._setup_parameter_table() + self._setup_station_table() + self._setup_snapshot_table() + + self._create_iccs() + self.refresh_all() + + @on(TabbedContent.TabActivated) + def on_tab_activated(self, event: TabbedContent.TabActivated) -> None: + if event.pane.id: + self._active_tab = event.pane.id + self.refresh_bindings() + if not isinstance(self.focused, Tabs): + try: + event.pane.query_one(DataTable).focus() + except Exception: + pass + + def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None: + tab = getattr(self, "_active_tab", "") + if action == "new_snapshot": + return True if tab == "tab-snapshots" else False + return True + + # ------------------------------------------------------------------ + # ICCS lifecycle + # ------------------------------------------------------------------ + + def _create_iccs(self) -> None: + """Discard the existing ICCS instance and create a new one in a background worker. + + ICCS construction reads waveform data, so it must not block the asyncio event loop. + """ + self._iccs = None + self._worker_create_iccs() + + @work(thread=True) + def _worker_create_iccs(self) -> None: + """Background worker: create ICCS instance without blocking the UI.""" + try: + with Session(engine) as session: + new_iccs = create_iccs_instance(session) + except (NoResultFound, RuntimeError): + return + except Exception as exc: + self.call_from_thread( + self.notify, f"ICCS init failed: {exc}", severity="error" + ) + return + self.call_from_thread(self._assign_iccs, new_iccs) + + def _assign_iccs(self, iccs: ICCS) -> None: + """Main-thread callback: store the new ICCS instance and refresh status.""" + self._iccs = iccs + self._refresh_event_bar() + self._refresh_seismograms() + + # ------------------------------------------------------------------ + # Table setup + # ------------------------------------------------------------------ + + def _setup_seismogram_table(self) -> None: + t = self.query_one("#seismogram-table", DataTable) + t.cursor_type = "row" + t.add_columns( + "ID", "Network", "Station", "Channel", "Select", "Flip", "Δt (s)", "CC" + ) + + def _setup_parameter_table(self) -> None: + t = self.query_one("#parameter-table", DataTable) + t.cursor_type = "row" + t.add_columns("Parameter", "Value", "Description") + + def _setup_station_table(self) -> None: + t = self.query_one("#station-table", DataTable) + t.cursor_type = "row" + t.add_columns( + "ID", "Network", "Name", "Location", "Channel", "Lat °", "Lon °", "Elev m" + ) + + def _setup_snapshot_table(self) -> None: + t = self.query_one("#snapshot-table", DataTable) + t.cursor_type = "row" + t.add_columns("ID", "Date (UTC)", "Comment", "Seismograms", "Select", "Flip") + + # ------------------------------------------------------------------ + # Data refresh + # ------------------------------------------------------------------ + + def refresh_all(self) -> None: + self._refresh_event_bar() + self._refresh_seismograms() + self._refresh_parameters() + self._refresh_stations() + self._refresh_snapshots() + + def _refresh_event_bar(self) -> None: + bar = self.query_one("#event-bar", Static) + iccs_status = " ● ICCS ready" if self._iccs is not None else " ○ no ICCS" + try: + with Session(engine) as session: + event = get_active_event(session) + time_str = str(event.time)[:19] if event.time else "unknown" + lat = f"{event.latitude:.3f}°" if event.latitude is not None else "?" + lon = f"{event.longitude:.3f}°" if event.longitude is not None else "?" + depth = ( + f" depth {event.depth / 1000:.1f} km" + if event.depth is not None + else "" + ) + bar.update( + f"Active event: {time_str} | {lat}, {lon}{depth}" + f" [dim]{iccs_status} e = switch event[/dim]" + ) + except NoResultFound: + bar.update("[red]No active event — press e to select one[/red]") + except RuntimeError as exc: + bar.update(f"[red]{exc}[/red]") + + def _refresh_seismograms(self) -> None: + table = self.query_one("#seismogram-table", DataTable) + saved_row = table.cursor_row + table.clear() + + ccnorm_map: dict[uuid.UUID, float] = {} + if self._iccs is not None: + try: + for iccs_seis, ccnorm in zip( + self._iccs.seismograms, self._iccs.ccnorms + ): + ccnorm_map[iccs_seis.extra["id"]] = float(ccnorm) + except Exception: + pass + + try: + with Session(engine) as session: + event = get_active_event(session) + seismograms = sorted( + event.seismograms, + key=lambda s: ccnorm_map.get(s.id, -2.0), + reverse=True, + ) + for seis in seismograms: + station = seis.station + params = seis.parameters + short_id = str(seis.id)[:8] + net = station.network if station else "—" + name = station.name if station else "—" + chan = station.channel if station else "—" + selected = "✓" if params and params.select else "✗" + flipped = "↕" if params and params.flip else " " + t1 = params.t1 if params else None + if seis.t0 and t1: + dt = f"{(t1 - seis.t0).total_seconds():.3f}" + else: + dt = "—" + cc = f"{ccnorm_map[seis.id]:.3f}" if seis.id in ccnorm_map else "—" + table.add_row( + short_id, + net, + name, + chan, + selected, + flipped, + dt, + cc, + key=str(seis.id), + ) + except (NoResultFound, RuntimeError): + pass + if table.row_count > 0: + table.move_cursor(row=min(saved_row, table.row_count - 1)) + + def _refresh_parameters(self) -> None: + table = self.query_one("#parameter-table", DataTable) + saved_row = table.cursor_row + table.clear() + try: + with Session(engine) as session: + event = get_active_event(session) + p = event.parameters + for attr, field_info in AimbatEventParametersBase.model_fields.items(): + value = getattr(p, attr) + if isinstance(value, bool): + display = "✓" if value else "✗" + elif isinstance(value, Timedelta): + display = f"{value.total_seconds():.2f}" + else: + display = f"{value}" + label = field_info.title or attr + desc = field_info.description or "" + table.add_row(label, display, desc, key=attr) + except (NoResultFound, RuntimeError): + pass + if table.row_count > 0: + table.move_cursor(row=min(saved_row, table.row_count - 1)) + + def _refresh_stations(self) -> None: + table = self.query_one("#station-table", DataTable) + saved_row = table.cursor_row + table.clear() + try: + with Session(engine) as session: + event = get_active_event(session) + seen: set[uuid.UUID] = set() + for seis in event.seismograms: + st = seis.station + if st and st.id not in seen: + seen.add(st.id) + short_id = str(st.id)[:8] + lat = f"{st.latitude:.3f}" if st.latitude is not None else "—" + lon = f"{st.longitude:.3f}" if st.longitude is not None else "—" + elev = ( + f"{st.elevation:.0f}" if st.elevation is not None else "—" + ) + table.add_row( + short_id, + st.network, + st.name, + st.location or "—", + st.channel, + lat, + lon, + elev, + key=str(st.id), + ) + except (NoResultFound, RuntimeError): + pass + if table.row_count > 0: + table.move_cursor(row=min(saved_row, table.row_count - 1)) + + def _refresh_snapshots(self) -> None: + table = self.query_one("#snapshot-table", DataTable) + saved_row = table.cursor_row + table.clear() + try: + with Session(engine) as session: + event = get_active_event(session) + for snap in event.snapshots: + short_id = str(snap.id)[:8] + date_str = str(snap.date)[:19] if snap.date else "—" + comment = snap.comment or "—" + seismogram_count = str(snap.seismogram_count) + selected_count = str(snap.selected_seismogram_count) + flipped_count = str(snap.flipped_seismogram_count) + table.add_row( + short_id, + date_str, + comment, + seismogram_count, + selected_count, + flipped_count, + key=str(snap.id), + ) + except (NoResultFound, RuntimeError): + pass + if table.row_count > 0: + table.move_cursor(row=min(saved_row, table.row_count - 1)) + + # ------------------------------------------------------------------ + # Parameter editing + # ------------------------------------------------------------------ + + @on(DataTable.RowSelected, "#seismogram-table") + def seismogram_row_selected(self, event: DataTable.RowSelected) -> None: + if event.row_key.value: + self._open_row_action_menu( + "tab-seismograms", + event.row_key.value, + f"Seismogram {event.row_key.value[:8]}", + ) + + @on(DataTable.RowSelected, "#station-table") + def station_row_selected(self, event: DataTable.RowSelected) -> None: + if event.row_key.value: + self._open_row_action_menu( + "tab-stations", + event.row_key.value, + f"Station {event.row_key.value[:8]}", + ) + + @on(DataTable.RowSelected, "#snapshot-table") + def snapshot_row_selected(self, event: DataTable.RowSelected) -> None: + if event.row_key.value: + self._open_row_action_menu( + "tab-snapshots", + event.row_key.value, + f"Snapshot {event.row_key.value[:8]}", + ) + + @on(DataTable.RowSelected, "#parameter-table") + def parameter_row_selected(self, event: DataTable.RowSelected) -> None: + attr = event.row_key.value + if not attr: + return + self._edit_parameter(attr) + + def _edit_parameter(self, attr: str) -> None: + """Toggle a bool parameter, or open input modal for others.""" + try: + with Session(engine) as session: + active_event = get_active_event(session) + current = getattr(active_event.parameters, attr) + except (NoResultFound, RuntimeError) as exc: + self.notify(str(exc), severity="error") + return + + if isinstance(current, bool): + self._apply_parameter(attr, not current) + return + + # Numeric / timedelta — open input modal + if isinstance(current, Timedelta): + current_str = f"{current.total_seconds():.2f}" + unit = "s" + else: + current_str = f"{current}" + unit = "" + + def on_input(raw: str | None) -> None: + if raw is None: + return + try: + if isinstance(current, Timedelta): + new_val: object = Timedelta(seconds=float(raw)) + else: + new_val = float(raw) + self._apply_parameter(attr, new_val) + except ValueError as exc: + self.notify(str(exc), severity="error") + + label = AimbatEventParametersBase.model_fields[attr].title or attr + self.push_screen(ParameterInputModal(label, current_str, unit), on_input) + + def _apply_parameter(self, attr: str, value: object) -> None: + """Write a parameter to the DB and sync to the in-memory ICCS object.""" + # Validate with ICCS first — before touching the DB — so invalid values + # are rejected without being persisted. + if self._iccs is not None and hasattr(self._iccs, attr): + try: + setattr(self._iccs, attr, value) + self._iccs.clear_cache() + except ValueError as exc: + self.notify(str(exc), severity="error") + return + + try: + with Session(engine) as session: + if attr in {p.value for p in EventParameter}: + set_event_parameter(session, EventParameter(attr), value) # type: ignore[call-overload] + else: + # mccc_damp / mccc_min_ccnorm — not in EventParameter enum + active_event = get_active_event(session) + validated = AimbatEventParametersBase.model_validate( + active_event.parameters, update={attr: value} + ) + setattr(active_event.parameters, attr, getattr(validated, attr)) + session.add(active_event) + session.commit() + except ValidationError as exc: + msgs = "; ".join( + e["msg"].removeprefix("Value error, ") for e in exc.errors() + ) + self.notify(msgs, severity="error") + self._create_iccs() # revert ICCS to DB state + return + except Exception as exc: + self.notify(str(exc), severity="error") + self._create_iccs() # revert ICCS to DB state + return + + # Parameter change may have fixed previously invalid ranges. + if self._iccs is None: + self._create_iccs() + + self._refresh_parameters() + self._refresh_seismograms() + self._refresh_event_bar() + self.notify(f"{attr} updated", timeout=2) + + # ------------------------------------------------------------------ + # Row-action menu helpers + # ------------------------------------------------------------------ + + def _open_row_action_menu(self, tab: str, item_id: str, title: str) -> None: + actions = _TAB_ROW_ACTIONS.get(tab, []) + if not actions: + return + + def on_action(action: str | None) -> None: + self._handle_row_action(tab, item_id, action) + + self.push_screen(ActionMenuModal(title, actions), on_action) + + def _handle_row_action(self, tab: str, item_id: str, action: str | None) -> None: + if action == "delete": + self._confirm_delete(tab, item_id) + elif action == "rollback": + self._confirm_rollback(item_id) + elif action == "show_details": + self._show_snapshot_details(item_id) + elif action == "toggle_select": + self._toggle_seismogram_bool(item_id, SeismogramParameter.SELECT) + elif action == "toggle_flip": + self._toggle_seismogram_bool(item_id, SeismogramParameter.FLIP) + elif action == "reset": + self._reset_seismogram_parameters(item_id) + + def _toggle_seismogram_bool(self, item_id: str, param: SeismogramParameter) -> None: + try: + seis_uuid = uuid.UUID(item_id) + with Session(engine) as session: + seis = session.get(AimbatSeismogram, seis_uuid) + if seis is None: + raise ValueError(f"Seismogram {item_id} not found") + new_value = not getattr(seis.parameters, param) + setattr(seis.parameters, param, new_value) + session.add(seis) + session.commit() + if self._iccs is not None: + for iccs_seis in self._iccs.seismograms: + if iccs_seis.extra.get("id") == seis_uuid: + setattr(iccs_seis, param, new_value) + self._iccs.clear_cache() + break + self._refresh_seismograms() + self.notify(f"{param} toggled", timeout=2) + except Exception as exc: + self.notify(str(exc), severity="error") + + def _reset_seismogram_parameters(self, item_id: str) -> None: + try: + with Session(engine) as session: + reset_seismogram_parameters_by_id(session, uuid.UUID(item_id)) + self.refresh_all() + self.notify("Seismogram parameters reset", timeout=2) + except Exception as exc: + self.notify(str(exc), severity="error") + + def _confirm_delete(self, tab: str, item_id: str) -> None: + messages = { + "tab-seismograms": "Delete this seismogram?", + "tab-stations": "Delete this station and all its seismograms?", + "tab-snapshots": "Delete this snapshot?", + } + msg = messages.get(tab) + if not msg: + return + + def on_confirm(confirmed: bool | None) -> None: + if not confirmed: + return + try: + if tab == "tab-seismograms": + with Session(engine) as session: + delete_seismogram_by_id(session, uuid.UUID(item_id)) + self._create_iccs() + self.refresh_all() + self.notify("Seismogram deleted", timeout=2) + elif tab == "tab-stations": + with Session(engine) as session: + delete_station_by_id(session, uuid.UUID(item_id)) + self._create_iccs() + self.refresh_all() + self.notify("Station deleted", timeout=2) + elif tab == "tab-snapshots": + with Session(engine) as session: + delete_snapshot_by_id(session, uuid.UUID(item_id)) + self._refresh_snapshots() + self.notify("Snapshot deleted", timeout=2) + except Exception as exc: + self.notify(str(exc), severity="error") + + self.push_screen(ConfirmModal(msg), on_confirm) + + def _show_snapshot_details(self, snap_id: str) -> None: + try: + with Session(engine) as session: + snap = session.get(AimbatSnapshot, uuid.UUID(snap_id)) + if snap is None: + return + p = snap.event_parameters_snapshot + rows: list[tuple[str, str]] = [] + for attr, field_info in AimbatEventParametersBase.model_fields.items(): + value = getattr(p, attr) + if isinstance(value, bool): + display = "✓" if value else "✗" + elif isinstance(value, Timedelta): + display = f"{value.total_seconds():.2f}" + else: + display = f"{value}" + label = field_info.title or attr + rows.append((label, display)) + self.push_screen(SnapshotDetailsModal(f"Snapshot {snap_id[:8]}", rows)) + except Exception as exc: + self.notify(str(exc), severity="error") + + def _confirm_rollback(self, snap_id: str) -> None: + def on_confirm(confirmed: bool | None) -> None: + if not confirmed: + return + try: + with Session(engine) as session: + rollback_to_snapshot_by_id(session, uuid.UUID(snap_id)) + if self._iccs is not None: + sync_iccs_parameters(session, self._iccs) + if self._iccs is None: + self._create_iccs() + self.refresh_all() + self.notify("Rolled back to snapshot", timeout=3) + except Exception as exc: + self.notify(str(exc), severity="error") + + self.push_screen(ConfirmModal("Roll back to this snapshot?"), on_confirm) + + # ------------------------------------------------------------------ + # Actions + # ------------------------------------------------------------------ + + def action_switch_event(self) -> None: + def on_result(event_id: uuid.UUID | None) -> None: + if event_id is not None: + self._create_iccs() + self.refresh_all() + + self.push_screen(EventSwitcherModal(), on_result) + + def action_add_data(self) -> None: + actions = [(dt.value, dt.name.replace("_", " ")) for dt in DataType] + + def on_type(selected: str | None) -> None: + if selected is None: + return + data_type = DataType(selected) + suffixes = DATATYPE_SUFFIXES[data_type] + label = data_type.name.replace("_", " ") + + def on_file(path: Path | None) -> None: + if path is None: + return + try: + with Session(engine) as session: + add_data_to_project( + session, [path], data_type, disable_progress_bar=True + ) + session.commit() + self.notify(f"Added: {path.name}", severity="information") + self.refresh_all() + except Exception as exc: + self.notify(str(exc), severity="error") + + self.push_screen( + FileOpen( + ".", + title=f"Add {label}", + filters=Filters( + (f"{label} files", lambda p: p.suffix.lower() in suffixes), + ("All files", lambda _: True), + ), + ), + on_file, + ) + + self.push_screen(ActionMenuModal("Add Data", actions), on_type) + + def action_open_interactive_tools(self) -> None: + if self._iccs is None: + self.notify("No active event — activate one first", severity="warning") + return + + def on_result(result: tuple[str, bool, bool] | None) -> None: + if result is not None: + self._run_pick_tool(*result) + + self.push_screen(InteractiveToolsModal(), on_result) + + def _run_pick_tool(self, tool: str, context: bool, all_seis: bool) -> None: + """Run an interactive pick tool, suspending Textual while matplotlib is active. + + Uses the long-lived ICCS instance (waveform data already loaded) and runs + matplotlib on the main thread via App.suspend(), which is the correct + Textual pattern for blocking terminal-adjacent processes. + """ + if self._iccs is None: + self.notify("ICCS not ready — please wait", severity="warning") + return + _TOOL_LABELS = { + "phase": "Phase arrival (t1)", + "window": "Time window", + "ccnorm": "Min CC norm", + } + tool_label = _TOOL_LABELS.get(tool, tool) + + try: + with self.suspend(): + console = Console() + console.clear() + console.print( + Panel( + f"[bold]{tool_label}[/bold]\n\n" + "Close the matplotlib window to return to AIMBAT.", + title="Interactive Tool Running", + border_style="bright_blue", + padding=(1, 4), + ) + ) + with Session(engine) as session: + if tool == "phase": + update_pick( + session, + self._iccs, + context, + all_seis, + False, + return_fig=False, + ) + elif tool == "window": + update_timewindow( + session, + self._iccs, + context, + all_seis, + False, + return_fig=False, + ) + elif tool == "ccnorm": + update_min_ccnorm( + session, + self._iccs, + context, + all_seis, + return_fig=False, + ) + console.clear() + except Exception as exc: + self.notify(str(exc), severity="error") + return + self._refresh_parameters() + self._refresh_seismograms() + self._refresh_event_bar() + self.notify("Done", timeout=2) + + def action_open_align(self) -> None: + if self._iccs is None: + self.notify("No active event — activate one first", severity="warning") + return + + def on_result(result: tuple[str, bool, bool, bool] | None) -> None: + if result is not None: + self._run_align_tool(self._iccs, *result) + + self.push_screen(AlignModal(), on_result) + + @work(thread=True) + def _run_align_tool( + self, + iccs: ICCS, + algorithm: str, + autoflip: bool, + autoselect: bool, + all_seis: bool, + ) -> None: + """Run ICCS or MCCC in a background thread.""" + try: + with Session(engine) as session: + if algorithm == "iccs": + run_iccs(session, iccs, autoflip, autoselect) + elif algorithm == "mccc": + run_mccc(session, iccs, all_seis) + except Exception as exc: + self.call_from_thread(self.notify, str(exc), severity="error") + return + self.call_from_thread(self._post_align_complete) + + def _post_align_complete(self) -> None: + self.refresh_all() + self.notify("Alignment complete", timeout=3) + + def action_new_snapshot(self) -> None: + def on_comment(comment: str | None) -> None: + if comment is None: + return + try: + with Session(engine) as session: + create_snapshot(session, comment or None) + self._refresh_snapshots() + self.notify("Snapshot created", timeout=2) + except Exception as exc: + self.notify(str(exc), severity="error") + + self.push_screen(SnapshotCommentModal(), on_comment) + + def action_vim_left(self) -> None: + if not isinstance(self.screen, ModalScreen): + self.query_one(TabbedContent).query_one(Tabs).action_previous_tab() + + def action_vim_right(self) -> None: + if not isinstance(self.screen, ModalScreen): + self.query_one(TabbedContent).query_one(Tabs).action_next_tab() + + def action_toggle_theme(self) -> None: + self.theme = _LIGHT_THEME if self.theme == _DEFAULT_THEME else _DEFAULT_THEME + + def action_refresh(self) -> None: + self.refresh_all() + self.notify("Refreshed", timeout=1) + + +def main() -> None: + """Entry point for the AIMBAT TUI.""" + AimbatTUI().run() + + +if __name__ == "__main__": + main() diff --git a/src/aimbat/_tui/modals.py b/src/aimbat/_tui/modals.py new file mode 100644 index 00000000..7f383962 --- /dev/null +++ b/src/aimbat/_tui/modals.py @@ -0,0 +1,583 @@ +"""Modal screens for the AIMBAT TUI.""" + +from __future__ import annotations + +import uuid +from enum import StrEnum + +from sqlalchemy.exc import NoResultFound +from sqlmodel import Session, select +from textual import on +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Container +from textual.screen import ModalScreen +from textual.widgets import DataTable, Input, Label, Static + +from aimbat._tui._widgets import VimDataTable + +from aimbat.core import delete_event_by_id, get_active_event, set_active_event_by_id +from aimbat.db import engine +from aimbat.models import AimbatEvent + + +class _CSS(StrEnum): + """CSS class names shared across modal widgets.""" + + TITLE = "modal-title" + HINT = "modal-hint" + + +class _Hint(StrEnum): + """Hint label strings shown at the bottom of modal dialogs.""" + + SAVE_CANCEL = ( + "[@click='screen.save']⏎ save[/] [@click='screen.cancel']⎋ cancel[/]" + ) + NAVIGATE_ACTIVATE_CANCEL = "↑↓ navigate [@click='screen.select']⏎ activate[/] [@click='screen.toggle_completed']c complete[/] [@click='screen.delete_event']⌫ delete[/] [@click='screen.cancel']⎋ cancel[/]" + NAVIGATE_SELECT_CANCEL = "↑↓ navigate [@click='screen.select']⏎ select[/] [@click='screen.cancel']⎋ cancel[/]" + NAVIGATE_RUN_CANCEL = "↑↓ navigate [@click='screen.select']⏎ run[/] [@click='screen.cancel']⎋ cancel[/]" + CONFIRM_CANCEL = "[@click='screen.confirm'][bold]y[/bold] / ⏎ confirm[/] [@click='screen.cancel'][bold]n[/bold] / ⎋ cancel[/]" + CLOSE = "[@click='screen.cancel']⎋ close[/]" + + +__all__ = [ + "ActionMenuModal", + "AlignModal", + "ConfirmModal", + "EventSwitcherModal", + "InteractiveToolsModal", + "ParameterInputModal", + "SnapshotCommentModal", + "SnapshotDetailsModal", +] + + +# --------------------------------------------------------------------------- +# Event-switcher modal +# --------------------------------------------------------------------------- + + +class EventSwitcherModal(ModalScreen[uuid.UUID | None]): + """Modal screen for selecting and activating a seismic event.""" + + BINDINGS = [ + Binding("escape", "cancel", "Cancel", show=False), + Binding("c", "toggle_completed", "Complete", show=True), + Binding("backspace", "delete_event", "Delete", show=True), + ] + + def __init__(self) -> None: + super().__init__() + self._selected_event_id: str | None = None + + def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None: + if action in {"delete_event", "toggle_completed"}: + return True if self._selected_event_id else False + return True + + def compose(self) -> ComposeResult: + with Container(id="switcher-dialog"): + yield Label("Switch Active Event", classes=_CSS.TITLE) + yield VimDataTable(id="event-table") + yield Label(_Hint.NAVIGATE_ACTIVATE_CANCEL, classes=_CSS.HINT) + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.cursor_type = "row" + table.add_columns( + "", + "", + "ID", + "Time (UTC)", + "Lat °", + "Lon °", + "Depth km", + "Seismograms", + "Stations", + ) + self._populate(table) + + def _populate(self, table: DataTable) -> None: + try: + with Session(engine) as session: + events = session.exec(select(AimbatEvent)).all() + active_id: uuid.UUID | None = None + try: + active_id = get_active_event(session).id + except NoResultFound: + pass + + for event in events: + active_marker = "●" if event.id == active_id else " " + done_marker = "✓" if event.parameters.completed else " " + short_id = str(event.id)[:8] + time_str = str(event.time)[:19] if event.time else "—" + lat = f"{event.latitude:.3f}" if event.latitude is not None else "—" + lon = ( + f"{event.longitude:.3f}" if event.longitude is not None else "—" + ) + depth = ( + f"{event.depth / 1000:.1f}" if event.depth is not None else "—" + ) + table.add_row( + active_marker, + done_marker, + short_id, + time_str, + lat, + lon, + depth, + str(event.seismogram_count), + str(event.station_count), + key=str(event.id), + ) + except RuntimeError as exc: + self.notify(str(exc), severity="error") + self.dismiss(None) + + def _refresh_table(self) -> None: + table = self.query_one("#event-table", DataTable) + saved_row = table.cursor_row + table.clear() + self._populate(table) + if table.row_count > 0: + table.move_cursor(row=min(saved_row, table.row_count - 1)) + + @on(DataTable.RowHighlighted, "#event-table") + def row_highlighted(self, event: DataTable.RowHighlighted) -> None: + self._selected_event_id = event.row_key.value if event.row_key else None + self.refresh_bindings() + + @on(DataTable.RowSelected, "#event-table") + def row_selected(self, event: DataTable.RowSelected) -> None: + row_key = event.row_key.value + if not row_key: + return + try: + event_uuid = uuid.UUID(row_key) + with Session(engine) as session: + set_active_event_by_id(session, event_uuid) + session.commit() + self.dismiss(event_uuid) + except Exception as exc: + self.notify(str(exc), severity="error") + + def action_toggle_completed(self) -> None: + event_id = self._selected_event_id + if not event_id: + return + try: + with Session(engine) as session: + event = session.get(AimbatEvent, uuid.UUID(event_id)) + if event is None: + return + event.parameters.completed = not event.parameters.completed + session.add(event) + session.commit() + self._refresh_table() + except Exception as exc: + self.notify(str(exc), severity="error") + + def action_delete_event(self) -> None: + event_id = self._selected_event_id + if not event_id: + return + + def on_confirm(confirmed: bool | None) -> None: + if not confirmed: + return + try: + with Session(engine) as session: + delete_event_by_id(session, uuid.UUID(event_id)) + self._selected_event_id = None + self._refresh_table() + self.notify("Event deleted", timeout=2) + except Exception as exc: + self.notify(str(exc), severity="error") + + self.app.push_screen( + ConfirmModal("Delete this event and all its data?"), on_confirm + ) + + def action_select(self) -> None: + self.query_one(DataTable).action_select_cursor() + + def action_cancel(self) -> None: + self.dismiss(None) + + +# --------------------------------------------------------------------------- +# Parameter-edit modal +# --------------------------------------------------------------------------- + + +class ParameterInputModal(ModalScreen[str | None]): + """Modal for entering a new numeric/timedelta parameter value.""" + + BINDINGS = [Binding("escape", "cancel", "Cancel", show=False)] + + def __init__(self, param_name: str, current: str, unit: str) -> None: + super().__init__() + self._param_name = param_name + self._current = current + self._unit = unit + + def compose(self) -> ComposeResult: + hint = f"Current: {self._current} {self._unit}".strip() + with Container(id="param-edit-dialog"): + yield Label(f"Edit: {self._param_name}", classes=_CSS.TITLE) + yield Label(hint, classes=_CSS.HINT) + yield Input(value=self._current, id="param-input") + yield Label( + _Hint.SAVE_CANCEL, + classes=_CSS.HINT, + ) + + def on_mount(self) -> None: + self.query_one(Input).focus() + + @on(Input.Submitted) + def submitted(self, event: Input.Submitted) -> None: + self.dismiss(event.value.strip()) + + def action_save(self) -> None: + self.dismiss(self.query_one("#param-input", Input).value.strip()) + + def action_cancel(self) -> None: + self.dismiss(None) + + +# --------------------------------------------------------------------------- +# Confirm modal +# --------------------------------------------------------------------------- + + +class ConfirmModal(ModalScreen[bool | None]): + """Generic yes/no confirmation dialog. + + Dismisses True on confirm, False on cancel. + """ + + BINDINGS = [ + Binding("y", "confirm", show=False), + Binding("enter", "confirm", show=False), + Binding("n", "cancel", show=False), + Binding("escape", "cancel", show=False), + ] + + def __init__(self, message: str) -> None: + super().__init__() + self._message = message + + def compose(self) -> ComposeResult: + with Container(id="confirm-dialog"): + yield Label(self._message, classes=_CSS.TITLE) + yield Label( + _Hint.CONFIRM_CANCEL, + classes=_CSS.HINT, + ) + + def action_confirm(self) -> None: + self.dismiss(True) + + def action_cancel(self) -> None: + self.dismiss(False) + + +# --------------------------------------------------------------------------- +# Snapshot comment modal +# --------------------------------------------------------------------------- + + +class SnapshotCommentModal(ModalScreen[str | None]): + """Prompt for an optional snapshot comment. + + Dismisses with the comment string (empty string = no comment) or None if + the user cancels. + """ + + BINDINGS = [Binding("escape", "cancel", "Cancel", show=False)] + + def compose(self) -> ComposeResult: + with Container(id="param-edit-dialog"): + yield Label("New Snapshot", classes=_CSS.TITLE) + yield Input(placeholder="Comment (optional)", id="param-input") + yield Label( + _Hint.SAVE_CANCEL, + classes=_CSS.HINT, + ) + + def on_mount(self) -> None: + self.query_one(Input).focus() + + @on(Input.Submitted) + def submitted(self, event: Input.Submitted) -> None: + self.dismiss(event.value.strip()) + + def action_save(self) -> None: + self.dismiss(self.query_one("#param-input", Input).value.strip()) + + def action_cancel(self) -> None: + self.dismiss(None) + + +# --------------------------------------------------------------------------- +# Row-action context menu modal +# --------------------------------------------------------------------------- + + +class ActionMenuModal(ModalScreen[str | None]): + """Generic context-action menu for a selected table row. + + Dismisses with the chosen action key, or None on cancel. + """ + + BINDINGS = [ + Binding("escape", "cancel", show=False), + ] + + def __init__(self, title: str, actions: list[tuple[str, str]]) -> None: + super().__init__() + self._title = title + self._actions = actions # [(action_key, display_label), ...] + + def compose(self) -> ComposeResult: + with Container(id="action-menu-dialog"): + yield Label(self._title, classes=_CSS.TITLE) + yield VimDataTable(id="action-table", show_header=False) + yield Label( + _Hint.NAVIGATE_SELECT_CANCEL, + classes=_CSS.HINT, + ) + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.cursor_type = "row" + table.add_column("action") + for key, label in self._actions: + table.add_row(label, key=key) + table.styles.height = len(self._actions) + table.focus() + + @on(DataTable.RowSelected) + def row_selected(self, event: DataTable.RowSelected) -> None: + self.dismiss(event.row_key.value) + + def action_select(self) -> None: + self.query_one(DataTable).action_select_cursor() + + def action_cancel(self) -> None: + self.dismiss(None) + + +# --------------------------------------------------------------------------- +# Interactive Tools modal (pick phase / window / ccnorm) +# --------------------------------------------------------------------------- + +_PICK_TOOLS: list[tuple[str, str]] = [ + ("phase", "Phase arrival (t1)"), + ("window", "Time window"), + ("ccnorm", "Min CC norm"), +] + + +class InteractiveToolsModal(ModalScreen[tuple[str, bool, bool] | None]): + """Menu for launching interactive matplotlib pick tools. + + Options are toggled with key bindings so no Checkbox widgets are needed. + Dismisses with (tool_key, context, all_seismograms) or None on cancel. + """ + + BINDINGS = [ + Binding("escape", "cancel", "Cancel", show=False), + Binding("c", "toggle_context", "Context", show=False), + Binding("a", "toggle_all", "All", show=False), + ] + + def __init__(self) -> None: + super().__init__() + self._use_context = True + self._all_seis = False + + def compose(self) -> ComposeResult: + with Container(id="tools-dialog"): + yield Label("Interactive Tools", classes=_CSS.TITLE) + yield VimDataTable(id="tools-table", show_header=False) + yield Static(id="tools-options") + yield Label( + _Hint.NAVIGATE_RUN_CANCEL, + classes=_CSS.HINT, + ) + + def on_mount(self) -> None: + table = self.query_one("#tools-table", DataTable) + table.cursor_type = "row" + table.add_column("tool") + for key, label in _PICK_TOOLS: + table.add_row(label, key=key) + self._update_options() + table.focus() + + def _update_options(self) -> None: + ctx = "✓" if self._use_context else "✗" + al = "✓" if self._all_seis else "✗" + self.query_one("#tools-options", Static).update( + f" [@click='screen.toggle_context'][dim]c[/dim] Context: {ctx}[/]" + f" [@click='screen.toggle_all'][dim]a[/dim] All seismograms: {al}[/]" + ) + + @on(DataTable.RowSelected, "#tools-table") + def row_selected(self, event: DataTable.RowSelected) -> None: + key = event.row_key.value + if key: + self.dismiss((key, self._use_context, self._all_seis)) + + def action_toggle_context(self) -> None: + self._use_context = not self._use_context + self._update_options() + + def action_toggle_all(self) -> None: + self._all_seis = not self._all_seis + self._update_options() + + def action_select(self) -> None: + self.query_one(DataTable).action_select_cursor() + + def action_cancel(self) -> None: + self.dismiss(None) + + +# --------------------------------------------------------------------------- +# Align modal (ICCS / MCCC) +# --------------------------------------------------------------------------- + +_ALIGN_ALGORITHMS: list[tuple[str, str]] = [ + ("iccs", "ICCS — Iterative Cross-Correlation and Stack"), + ("mccc", "MCCC — Multi-Channel Cross-Correlation"), +] + + +class AlignModal(ModalScreen[tuple[str, bool, bool, bool] | None]): + """Menu for running ICCS or MCCC alignment. + + Dismisses with (algorithm, autoflip, autoselect, all_seismograms) or None. + ICCS options: autoflip (f), autoselect (s). + MCCC options: all seismograms (a). + """ + + BINDINGS = [ + Binding("escape", "cancel", "Cancel", show=False), + Binding("f", "toggle_autoflip", "Autoflip", show=False), + Binding("s", "toggle_autoselect", "Autoselect", show=False), + Binding("a", "toggle_all", "All", show=False), + ] + + def __init__(self) -> None: + super().__init__() + self._autoflip = False + self._autoselect = False + self._all_seis = False + self._highlighted_algorithm: str = "iccs" + + def compose(self) -> ComposeResult: + with Container(id="align-dialog"): + yield Label("Align Seismograms", classes=_CSS.TITLE) + yield VimDataTable(id="align-table", show_header=False) + yield Static(id="align-options") + yield Label( + _Hint.NAVIGATE_RUN_CANCEL, + classes=_CSS.HINT, + ) + + def on_mount(self) -> None: + table = self.query_one("#align-table", DataTable) + table.cursor_type = "row" + table.add_column("algorithm") + for key, label in _ALIGN_ALGORITHMS: + table.add_row(label, key=key) + self._update_options() + table.focus() + + def _update_options(self) -> None: + opts = self.query_one("#align-options", Static) + if self._highlighted_algorithm == "iccs": + fl = "✓" if self._autoflip else "✗" + sl = "✓" if self._autoselect else "✗" + opts.update( + f" [@click='screen.toggle_autoflip'][dim]f[/dim] Autoflip: {fl}[/]" + f" [@click='screen.toggle_autoselect'][dim]s[/dim] Autoselect: {sl}[/]" + ) + else: + al = "✓" if self._all_seis else "✗" + opts.update( + f" [@click='screen.toggle_all'][dim]a[/dim] All seismograms: {al}[/]" + ) + + @on(DataTable.RowHighlighted, "#align-table") + def row_highlighted(self, event: DataTable.RowHighlighted) -> None: + self._highlighted_algorithm = event.row_key.value or "iccs" + self._update_options() + + @on(DataTable.RowSelected, "#align-table") + def row_selected(self, event: DataTable.RowSelected) -> None: + key = event.row_key.value + if key: + self.dismiss((key, self._autoflip, self._autoselect, self._all_seis)) + + def action_toggle_autoflip(self) -> None: + if self._highlighted_algorithm == "iccs": + self._autoflip = not self._autoflip + self._update_options() + + def action_toggle_autoselect(self) -> None: + if self._highlighted_algorithm == "iccs": + self._autoselect = not self._autoselect + self._update_options() + + def action_toggle_all(self) -> None: + if self._highlighted_algorithm == "mccc": + self._all_seis = not self._all_seis + self._update_options() + + def action_select(self) -> None: + self.query_one(DataTable).action_select_cursor() + + def action_cancel(self) -> None: + self.dismiss(None) + + +# --------------------------------------------------------------------------- +# Snapshot details modal +# --------------------------------------------------------------------------- + + +class SnapshotDetailsModal(ModalScreen[None]): + """Read-only view of the event parameters captured in a snapshot.""" + + BINDINGS = [ + Binding("escape", "cancel", show=False), + ] + + def __init__(self, title: str, rows: list[tuple[str, str]]) -> None: + super().__init__() + self._title = title + self._rows = rows # [(label, value), ...] + + def compose(self) -> ComposeResult: + with Container(id="snapshot-details-dialog"): + yield Label(self._title, classes=_CSS.TITLE) + yield VimDataTable(id="snapshot-details-table", show_header=True) + yield Label(_Hint.CLOSE, classes=_CSS.HINT) + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.cursor_type = "row" + table.add_columns("Parameter", "Value") + for row in self._rows: + table.add_row(*row) + table.styles.height = len(self._rows) + 2 + table.focus() + + def action_cancel(self) -> None: + self.dismiss(None) diff --git a/src/aimbat/app.py b/src/aimbat/app.py index 883f018b..51142baf 100644 --- a/src/aimbat/app.py +++ b/src/aimbat/app.py @@ -37,6 +37,7 @@ app.command("aimbat._cli.seismogram:app", name="seismogram") app.command("aimbat._cli.snapshot:app", name="snapshot") app.command("aimbat._cli.utils:app", name="utils") +app.command("aimbat._tui.app:main", name="tui") if __name__ == "__main__": diff --git a/src/aimbat/core/_active_event.py b/src/aimbat/core/_active_event.py index cf040fc7..ae318a10 100644 --- a/src/aimbat/core/_active_event.py +++ b/src/aimbat/core/_active_event.py @@ -33,13 +33,13 @@ def get_active_event(session: Session) -> AimbatEvent: logger.debug("Attempting to determine active event.") - select_active_event = select(AimbatEvent).where(AimbatEvent.active == 1) + statement = select(AimbatEvent).where(AimbatEvent.active == 1) # NOTE: While there technically can be no active event in the database, # we typically don't really want to go beyond this point when that is the # case. Hence we call `one` rather than `one_or_none`. try: - active_event = session.exec(select_active_event).one() + active_event = session.exec(statement).one() except NoResultFound: raise NoResultFound(f"No active event found. {HINTS.ACTIVATE_EVENT}") diff --git a/src/aimbat/core/_data.py b/src/aimbat/core/_data.py index 238b715b..147c49e3 100644 --- a/src/aimbat/core/_data.py +++ b/src/aimbat/core/_data.py @@ -45,14 +45,14 @@ def _create_station( new_aimbat_station = create_station(datasource, datatype) - select_aimbat_station = ( + statement = ( select(AimbatStation) .where(AimbatStation.name == new_aimbat_station.name) .where(AimbatStation.network == new_aimbat_station.network) .where(AimbatStation.channel == new_aimbat_station.channel) .where(AimbatStation.location == new_aimbat_station.location) ) - aimbat_station = session.exec(select_aimbat_station).one_or_none() + aimbat_station = session.exec(statement).one_or_none() if aimbat_station is None: aimbat_station = new_aimbat_station @@ -74,10 +74,8 @@ def _create_event( new_aimbat_event = create_event(datasource, datatype) - select_aimbat_event = select(AimbatEvent).where( - AimbatEvent.time == new_aimbat_event.time - ) - aimbat_event = session.exec(select_aimbat_event).one_or_none() + statement = select(AimbatEvent).where(AimbatEvent.time == new_aimbat_event.time) + aimbat_event = session.exec(statement).one_or_none() if aimbat_event is None: aimbat_event = new_aimbat_event @@ -97,13 +95,13 @@ def _create_seismogram( new_aimbat_seismogram = create_seismogram(datasource, datatype) - select_aimbat_seismogram = ( + statement = ( select(AimbatSeismogram) .join(AimbatDataSource) .where(AimbatDataSource.sourcename == str(datasource)) ) - aimbat_seismogram = session.exec(select_aimbat_seismogram).one_or_none() + aimbat_seismogram = session.exec(statement).one_or_none() if aimbat_seismogram is None: logger.debug(f"Adding seismogram with data source {datasource} to project.") aimbat_seismogram = new_aimbat_seismogram @@ -131,7 +129,9 @@ def _process_datasource( # Resolve station — use the provided UUID, extract from the source, or skip if station_id is not None: aimbat_station: AimbatStation | None = session.get(AimbatStation, station_id) - logger.debug(f"Using station {aimbat_station.name} - {aimbat_station.network} (ID={station_id}).") # type: ignore[union-attr] + logger.debug( + f"Using station {getattr(aimbat_station, 'name', 'Unknown')} - {getattr(aimbat_station, 'network', 'Unknown')} (ID={station_id})." + ) elif supports_station_creation(datatype): aimbat_station = _create_station(session, datasource, datatype) else: @@ -167,10 +167,10 @@ def _process_datasource( aimbat_seismogram.station = aimbat_station aimbat_seismogram.event = aimbat_event - select_aimbat_data_source = select(AimbatDataSource).where( + statement = select(AimbatDataSource).where( AimbatDataSource.sourcename == str(datasource) ) - aimbat_data_source = session.exec(select_aimbat_data_source).one_or_none() + aimbat_data_source = session.exec(statement).one_or_none() if aimbat_data_source is None: logger.debug(f"Adding data source {datasource} to project.") aimbat_data_source = AimbatDataSource.model_validate( diff --git a/src/aimbat/core/_event.py b/src/aimbat/core/_event.py index 0c30a1b8..23a615f7 100644 --- a/src/aimbat/core/_event.py +++ b/src/aimbat/core/_event.py @@ -8,7 +8,7 @@ json_to_table, TABLE_STYLING, ) -from aimbat.models._models import ( +from aimbat.models import ( AimbatEvent, AimbatEventParameters, AimbatStation, @@ -86,13 +86,13 @@ def get_completed_events(session: Session) -> Sequence[AimbatEvent]: session: SQL session. """ - select_completed_events = ( + statement = ( select(AimbatEvent) .join(AimbatEventParameters) .where(AimbatEventParameters.completed == 1) ) - return session.exec(select_completed_events).all() + return session.exec(statement).all() def get_events_using_station( @@ -109,14 +109,14 @@ def get_events_using_station( logger.info(f"Getting events for station: {station.id}.") - select_events = ( + statement = ( select(AimbatEvent) .join(AimbatSeismogram) .join(AimbatStation) .where(AimbatStation.id == station.id) ) - events = session.exec(select_events).all() + events = session.exec(statement).all() logger.debug(f"Found {len(events)}.") diff --git a/src/aimbat/core/_iccs.py b/src/aimbat/core/_iccs.py index 6d4101c8..8ad3bf0a 100644 --- a/src/aimbat/core/_iccs.py +++ b/src/aimbat/core/_iccs.py @@ -4,20 +4,25 @@ from aimbat import settings from aimbat.logger import logger from aimbat.models import AimbatSeismogram -from pysmo.tools.signal import mccc +from aimbat.models._parameters import ( + AimbatEventParametersBase, + AimbatSeismogramParametersBase, +) from pysmo.tools.iccs import ( ICCS, + MiniICCSSeismogram, plot_seismograms as _plot_seismograms, plot_stack as _plot_stack, update_min_ccnorm as _update_min_ccnorm, update_pick as _update_pick, update_timewindow as _update_timewindow, ) +from pysmo.functions import clone_to_mini from sqlmodel import Session -from typing import cast __all__ = [ "create_iccs_instance", + "sync_iccs_parameters", "run_iccs", "run_mccc", "plot_stack", @@ -31,6 +36,9 @@ def create_iccs_instance(session: Session) -> ICCS: """Create an ICCS instance for the active event. + Seismogram data is copied into MiniICCSSeismogram objects so the session + does not need to remain open after this call. + Args: session: Database session. @@ -41,19 +49,73 @@ def create_iccs_instance(session: Session) -> ICCS: logger.info("Creating ICCS instance for active event.") active_event = get_active_event(session) + p = active_event.parameters + + seismograms = [ + clone_to_mini(MiniICCSSeismogram, seis, update={"extra": {"id": seis.id}}) + for seis in active_event.seismograms + ] return ICCS( - seismograms=active_event.seismograms, - window_pre=active_event.parameters.window_pre, - window_post=active_event.parameters.window_post, - bandpass_apply=active_event.parameters.bandpass_apply, - bandpass_fmin=active_event.parameters.bandpass_fmin, - bandpass_fmax=active_event.parameters.bandpass_fmax, - min_ccnorm=active_event.parameters.min_ccnorm, + seismograms=seismograms, + window_pre=p.window_pre, + window_post=p.window_post, + bandpass_apply=p.bandpass_apply, + bandpass_fmin=p.bandpass_fmin, + bandpass_fmax=p.bandpass_fmax, + min_ccnorm=p.min_ccnorm, context_width=settings.context_width, ) +def _write_back_seismograms(session: Session, iccs: ICCS) -> None: + """Write t1, flip, and select from ICCS seismograms back to the database. + + Args: + session: Database session. + iccs: ICCS instance whose seismograms carry UUIDs in their extra dict. + """ + for seis in iccs.seismograms: + db_seis = session.get(AimbatSeismogram, seis.extra["id"]) + if db_seis is not None: + db_seis.parameters.t1 = seis.t1 + db_seis.parameters.flip = seis.flip + db_seis.parameters.select = seis.select + session.commit() + + +def sync_iccs_parameters(session: Session, iccs: ICCS) -> None: + """Sync an existing ICCS instance's parameters from the database. + + Updates event-level and per-seismogram parameters without re-reading waveform + data. Use this after operations that change parameters but not the + seismogram list (e.g. rolling back to a snapshot). + + Args: + session: Database session. + iccs: ICCS instance to update in-place. + """ + + logger.info("Syncing ICCS parameters from database.") + + active_event = get_active_event(session) + event_params = AimbatEventParametersBase.model_validate(active_event.parameters) + for field_name in AimbatEventParametersBase.model_fields: + if hasattr(iccs, field_name): + setattr(iccs, field_name, getattr(event_params, field_name)) + + for iccs_seis in iccs.seismograms: + db_seis = session.get(AimbatSeismogram, iccs_seis.extra["id"]) + if db_seis is not None: + seis_params = AimbatSeismogramParametersBase.model_validate( + db_seis.parameters + ) + for field_name in AimbatSeismogramParametersBase.model_fields: + setattr(iccs_seis, field_name, getattr(seis_params, field_name)) + + iccs.clear_cache() + + def run_iccs(session: Session, iccs: ICCS, autoflip: bool, autoselect: bool) -> None: """Run ICCS algorithm. @@ -68,14 +130,14 @@ def run_iccs(session: Session, iccs: ICCS, autoflip: bool, autoselect: bool) -> results = iccs(autoflip=autoflip, autoselect=autoselect) logger.info(f"ICCS {results = }") - session.commit() + _write_back_seismograms(session, iccs) def run_mccc(session: Session, iccs: ICCS, all_seismograms: bool) -> None: """Run MCCC algorithm. Args: - session: SQLModel session. + session: Database session. iccs: ICCS instance. all_seismograms: Whether to include all seismograms in the MCCC processing, or just the selected ones. """ @@ -83,60 +145,59 @@ def run_mccc(session: Session, iccs: ICCS, all_seismograms: bool) -> None: logger.info(f"Running MCCC with {all_seismograms=}.") active_event = get_active_event(session) - min_cc = active_event.parameters.mccc_min_ccnorm - damping = active_event.parameters.mccc_damp - - cc_seismograms = ( - iccs.cc_seismograms - if all_seismograms - else [s for s in iccs.cc_seismograms if s.parent_seismogram.select] + iccs.run_mccc( + all_seismograms=all_seismograms, + min_cc=active_event.parameters.mccc_min_ccnorm, + damping=active_event.parameters.mccc_damp, ) + _write_back_seismograms(session, iccs) - delay_times, delay_stdev, rmse = mccc(cc_seismograms, min_cc, damping) - - for cc_seismogram, delay_time in zip(cc_seismograms, delay_times): - logger.debug( - f"Applying MCCC delay time delta {delay_time.total_seconds():.2f} s to seismogram {cast(AimbatSeismogram, cc_seismogram.parent_seismogram).id}." - ) - t1 = ( - cc_seismogram.parent_seismogram.t1 - or cc_seismogram.parent_seismogram.t0 - delay_time - ) - cc_seismogram.parent_seismogram.t1 = t1 - session.commit() - - -def plot_stack(iccs: ICCS, context: bool, all: bool) -> None: +def plot_stack(iccs: ICCS, context: bool, all: bool, return_fig: bool) -> tuple | None: """Plot the ICCS stack. Args: iccs: ICCS instance. context: Whether to use seismograms with extra context. all: Whether to plot all seismograms. + return_fig: If True, return the figure and axes objects instead of showing the plot. + + Returns: + A tuple of (Figure, Axes) if return_fig is True, otherwise None. """ logger.info("Plotting ICCS stack for active event.") - _plot_stack(iccs, context, all) + return _plot_stack(iccs, context, all, return_fig=return_fig) # type: ignore[call-overload] -def plot_iccs_seismograms(iccs: ICCS, context: bool, all: bool) -> None: +def plot_iccs_seismograms( + iccs: ICCS, context: bool, all: bool, return_fig: bool +) -> tuple | None: """Plot the ICCS seismograms as an image. Args: iccs: ICCS instance. context: Whether to use seismograms with extra context. all: Whether to plot all seismograms. + return_fig: If True, return the figure and axes objects instead of showing the plot. + + Returns: + A tuple of (Figure, Axes) if return_fig is True, otherwise None. """ logger.info("Plotting ICCS seismograms for active event.") - _plot_seismograms(iccs, context, all) + return _plot_seismograms(iccs, context, all, return_fig=return_fig) # type: ignore[call-overload] def update_pick( - session: Session, iccs: ICCS, context: bool, all: bool, use_seismogram_image: bool -) -> None: + session: Session, + iccs: ICCS, + context: bool, + all: bool, + use_seismogram_image: bool, + return_fig: bool, +) -> tuple | None: """Update the pick for the active event. Args: @@ -144,17 +205,36 @@ def update_pick( context: Whether to use seismograms with extra context. all: Whether to plot all seismograms. use_seismogram_image: Whether to use the seismogram image to update pick. + return_fig: If True, return the figure and axes objects instead of showing the plot. + + Returns: + A tuple of (Figure, Axes, widgets) if return_fig is True, otherwise None. """ logger.info("Updating pick for active event.") - _update_pick(iccs, context, all, use_seismogram_image) - session.commit() + result = _update_pick( # type: ignore[call-overload] + iccs, context, all, use_seismogram_image, return_fig=return_fig + ) + + if not return_fig: + _write_back_seismograms(session, iccs) + return None + + logger.warning( + "Returning figure and axes objects instead of showing the plot. This is intended for testing purposes; in normal usage, return_fig should be False." + ) + return result def update_timewindow( - session: Session, iccs: ICCS, context: bool, all: bool, use_seismogram_image: bool -) -> None: + session: Session, + iccs: ICCS, + context: bool, + all: bool, + use_seismogram_image: bool, + return_fig: bool, +) -> tuple | None: """Update the time window for the active event. Args: @@ -162,35 +242,57 @@ def update_timewindow( context: Whether to use seismograms with extra context. all: Whether to plot all seismograms. use_seismogram_image: Whether to use the seismogram image to update pick. + return_fig: If True, return the figure and axes objects instead of showing the plot. + + Returns: + A tuple of (Figure, Axes, widgets) if return_fig is True, otherwise None. """ logger.info("Updating time window for active event.") - logger.debug(f"Current {iccs.window_pre = }, {iccs.window_post = }.") - _update_timewindow(iccs, context, all, use_seismogram_image) - logger.debug(f"Updated {iccs.window_pre = }, {iccs.window_post = }.") + result = _update_timewindow( # type: ignore[call-overload] + iccs, context, all, use_seismogram_image, return_fig=return_fig + ) - active_event = get_active_event(session) - active_event.parameters.window_pre = iccs.window_pre - active_event.parameters.window_post = iccs.window_post - session.commit() + if not return_fig: + active_event = get_active_event(session) + active_event.parameters.window_pre = iccs.window_pre + active_event.parameters.window_post = iccs.window_post + session.commit() + return None + + logger.warning( + "Returning figure and axes objects instead of showing the plot. This is intended for testing purposes; in normal usage, return_fig should be False." + ) + return result -def update_min_ccnorm(session: Session, iccs: ICCS, context: bool, all: bool) -> None: +def update_min_ccnorm( + session: Session, iccs: ICCS, context: bool, all: bool, return_fig: bool +) -> tuple | None: """Update the minimum cross correlation coefficient for the active event. Args: iccs: ICCS instance. context: Whether to use seismograms with extra context. all: Whether to plot all seismograms. + return_fig: If True, return the figure and axes objects instead of showing the plot. + + Returns: + A tuple of (Figure, Axes, widgets) if return_fig is True, otherwise None. """ logger.info("Updating minimum cross correlation coefficient for active event.") - logger.debug(f"Current {iccs.min_ccnorm = }.") - _update_min_ccnorm(iccs, context, all) - logger.debug(f"Updated {iccs.min_ccnorm = }.") + result = _update_min_ccnorm(iccs, context, all, return_fig=return_fig) # type: ignore[call-overload] - active_event = get_active_event(session) - active_event.parameters.min_ccnorm = float(iccs.min_ccnorm) - session.commit() + if not return_fig: + active_event = get_active_event(session) + active_event.parameters.min_ccnorm = float(iccs.min_ccnorm) + session.commit() + return None + + logger.warning( + "Returning figure and axes objects instead of showing the plot. This is intended for testing purposes; in normal usage, return_fig should be False." + ) + return result diff --git a/src/aimbat/core/_seismogram.py b/src/aimbat/core/_seismogram.py index cec82343..ba79d56e 100644 --- a/src/aimbat/core/_seismogram.py +++ b/src/aimbat/core/_seismogram.py @@ -1,3 +1,7 @@ +import aimbat.core._event as event +import uuid +import matplotlib.pyplot as plt +import matplotlib.dates as mdates from aimbat.core import get_active_event from aimbat.logger import logger from aimbat.utils import ( @@ -19,7 +23,7 @@ ) from pysmo import MiniSeismogram from pysmo.functions import detrend, normalize, clone_to_mini -from pysmo.tools.plotutils import time_array, unix_time_array +from pysmo.tools.plotutils import time_array from pysmo.tools.azdist import distance from pandas import Timestamp from rich.console import Console @@ -27,14 +31,8 @@ from sqlalchemy.exc import NoResultFound from typing import overload from collections.abc import Sequence -from matplotlib.figure import Figure from pydantic import TypeAdapter from typing import Any, Literal -import aimbat.core._event as event -import uuid -import matplotlib.pyplot as plt -import matplotlib.dates as mdates -import pyqtgraph as pg # type: ignore __all__ = [ "delete_seismogram_by_id", @@ -43,6 +41,8 @@ "get_seismogram_parameter", "set_seismogram_parameter_by_id", "set_seismogram_parameter", + "reset_seismogram_parameters_by_id", + "reset_seismogram_parameters", "get_selected_seismograms", "dump_seismogram_table_to_json", "print_seismogram_table", @@ -85,6 +85,48 @@ def delete_seismogram(session: Session, seismogram: AimbatSeismogram) -> None: session.commit() +def reset_seismogram_parameters_by_id( + session: Session, seismogram_id: uuid.UUID +) -> None: + """Reset an AimbatSeismogram's parameters to their default values by ID. + + Args: + session: Database session. + seismogram_id: Seismogram ID. + + Raises: + NoResultFound: If no AimbatSeismogram is found with the given ID. + """ + + logger.debug(f"Getting seismogram with id={seismogram_id}.") + + seismogram = session.get(AimbatSeismogram, seismogram_id) + if seismogram is None: + raise NoResultFound(f"No AimbatSeismogram found with {seismogram_id=}") + reset_seismogram_parameters(session, seismogram) + + +def reset_seismogram_parameters(session: Session, seismogram: AimbatSeismogram) -> None: + """Reset an AimbatSeismogram's parameters to their default values. + + All fields defined on AimbatSeismogramParametersBase are reset to the + values produced by a fresh default instance, so newly added fields are + picked up automatically. + + Args: + session: Database session. + seismogram: Seismogram whose parameters should be reset. + """ + + logger.info(f"Resetting parameters for seismogram {seismogram.id}.") + + defaults = AimbatSeismogramParametersBase() + for field_name in AimbatSeismogramParametersBase.model_fields: + setattr(seismogram.parameters, field_name, getattr(defaults, field_name)) + session.add(seismogram) + session.commit() + + def get_seismogram_parameter_by_id( session: Session, seismogram_id: uuid.UUID, name: SeismogramParameter ) -> bool | Timestamp: @@ -247,14 +289,14 @@ def get_selected_seismograms( if all_events is True: logger.debug("Selecting seismograms for all events.") - select_seismograms = ( + statement = ( select(AimbatSeismogram) .join(AimbatSeismogramParameters) .where(AimbatSeismogramParameters.select == 1) ) else: logger.debug("Selecting seismograms for active event only.") - select_seismograms = ( + statement = ( select(AimbatSeismogram) .join(AimbatSeismogramParameters) .join(AimbatEvent) @@ -262,7 +304,7 @@ def get_selected_seismograms( .where(AimbatEvent.active == 1) ) - seismograms = session.exec(select_seismograms).all() + seismograms = session.exec(statement).all() logger.debug(f"Found {len(seismograms)} selected seismograms.") @@ -435,21 +477,33 @@ def print_seismogram_parameter_table(session: Session, short: bool) -> None: ) -def plot_all_seismograms(session: Session, use_qt: bool = False) -> Figure: +@overload +def plot_all_seismograms( + session: Session, return_fig: Literal[True] +) -> tuple[plt.Figure, plt.Axes]: ... + + +@overload +def plot_all_seismograms(session: Session, return_fig: Literal[False]) -> None: ... + + +def plot_all_seismograms( + session: Session, return_fig: bool +) -> tuple[plt.Figure, plt.Axes] | None: """Plot all seismograms for a particular event ordered by great circle distance. Args: - use_qt: Plot with pqtgraph instead of pyplot - """ + session: Database session. + return_fig: Whether to return the figure and axes objects instead of showing the plot. - active_event = get_active_event(session) + Returns: + figure and axes objects if return_fig is True, otherwise None. + """ - if active_event is None: + if (active_event := get_active_event(session)) is None: raise RuntimeError("No active event set.") - seismograms = active_event.seismograms - - if len(seismograms) == 0: + if len(seismograms := active_event.seismograms) == 0: raise RuntimeError("No seismograms found in active event.") distance_dict = { @@ -464,38 +518,27 @@ def plot_all_seismograms(session: Session, use_qt: bool = False) -> Figure: xlabel = "Time of day" ylabel = "Epicentral distance [km]" - plot_widget = None - if use_qt: - plot_widget = pg.plot(title=title) - axis = pg.DateAxisItem() - plot_widget.setAxisItems({"bottom": axis}) - plot_widget.setLabel("bottom", xlabel) - plot_widget.setLabel("left", ylabel) - else: - fig, ax = plt.subplots() + fig, ax = plt.subplots() for seismogram in seismograms: clone = clone_to_mini(MiniSeismogram, seismogram) detrend(clone) normalize(clone) plot_data = clone.data * scaling_factor + distance_dict[seismogram.id] - if use_qt and plot_widget is not None: - times = unix_time_array(clone) - plot_widget.plot(times, plot_data) - else: - times = time_array(clone) - ax.plot( - times, - plot_data, - scalex=True, - scaley=True, - ) - if not use_qt: - plt.xlabel(xlabel=xlabel) - plt.ylabel(ylabel=ylabel) - plt.gcf().autofmt_xdate() - fmt = mdates.DateFormatter("%H:%M:%S") - plt.gca().xaxis.set_major_formatter(fmt) - plt.title(title) - plt.show() - return fig + times = time_array(clone) + ax.plot( + times, + plot_data, + scalex=True, + scaley=True, + ) + plt.xlabel(xlabel=xlabel) + plt.ylabel(ylabel=ylabel) + plt.gcf().autofmt_xdate() + fmt = mdates.DateFormatter("%H:%M:%S") + plt.gca().xaxis.set_major_formatter(fmt) + plt.title(title) + if return_fig: + return fig, ax + plt.show() + return None diff --git a/src/aimbat/core/_snapshot.py b/src/aimbat/core/_snapshot.py index 968a0b9d..c533d490 100644 --- a/src/aimbat/core/_snapshot.py +++ b/src/aimbat/core/_snapshot.py @@ -3,7 +3,7 @@ from aimbat.core import get_active_event from aimbat.logger import logger from aimbat.utils import uuid_shortener, json_to_table, TABLE_STYLING -from aimbat.models._models import ( +from aimbat.models import ( AimbatSnapshot, AimbatEvent, AimbatEventParametersSnapshot, @@ -30,6 +30,8 @@ "get_snapshots", "dump_snapshot_tables_to_json", "print_snapshot_table", + "print_snapshot_parameters_table_by_id", + "print_snapshot_parameters_table", ] @@ -118,24 +120,27 @@ def rollback_to_snapshot(session: Session, snapshot: AimbatSnapshot) -> None: f"Using event parameters snapshot with id={snapshot.event_parameters_snapshot.id} for rollback." ) current_event_parameters = snapshot.event.parameters + # setting attributes explicitly brings them into the session - for k, v in rollback_event_parameters.model_dump().items(): + for k in AimbatEventParametersBase.model_fields.keys(): + v = getattr(rollback_event_parameters, k) + logger.debug(f"Setting event parameter {k} to {v!r} for rollback.") setattr(current_event_parameters, k, v) session.add(current_event_parameters) for seismogram_parameters_snapshot in snapshot.seismogram_parameters_snapshots: - # create object with just the parameters rollback_seismogram_parameters = AimbatSeismogramParametersBase.model_validate( seismogram_parameters_snapshot ) logger.debug( f"Using seismogram parameters snapshot with id={seismogram_parameters_snapshot.id} for rollback." ) - # setting attributes explicitly brings them into the session current_seismogram_parameters = seismogram_parameters_snapshot.parameters - for parameter, value in rollback_seismogram_parameters.model_dump().items(): - setattr(current_seismogram_parameters, parameter, value) + for k in AimbatSeismogramParametersBase.model_fields.keys(): + v = getattr(rollback_seismogram_parameters, k) + logger.debug(f"Setting seismogram parameter {k} to {v!r} for rollback.") + setattr(current_seismogram_parameters, k, v) session.add(current_seismogram_parameters) session.commit() @@ -323,9 +328,49 @@ def print_snapshot_table(session: Session, short: bool, all_events: bool) -> Non "header": "# Seismograms", "style": TABLE_STYLING.linked, }, + "selected_seismogram_count": { + "header": "# Selected", + "style": TABLE_STYLING.linked, + }, + "flipped_seismogram_count": { + "header": "# Flipped", + "style": TABLE_STYLING.linked, + }, "event_id": { "header": "Event ID (shortened)" if short else "Event ID", "style": TABLE_STYLING.linked, }, }, ) + + +def print_snapshot_parameters_table_by_id( + session: Session, snapshot_id: uuid.UUID, short: bool +) -> None: + """Print a pretty table with AIMBAT snapshot parameters for a given snapshot id.""" + snapshot = session.get(AimbatSnapshot, snapshot_id) + + if snapshot is None: + raise ValueError( + f"Unable to print snapshot parameters: snapshot with id={snapshot_id} not found." + ) + + print_snapshot_parameters_table(session, snapshot.event_parameters_snapshot, short) + + +def print_snapshot_parameters_table( + session: Session, snapshot: AimbatEventParametersSnapshot, short: bool +) -> None: + json_to_table( + data=snapshot.model_dump(mode="json"), + title=f"Saved event parameters in snapshot:: {uuid_shortener(session, snapshot.snapshot) if short else str(snapshot.snapshot.id)}", + skip_keys=["id", "snapshot_id", "parameters_id"], + common_column_kwargs={"highlight": True}, + column_kwargs={ + "Key": { + "header": "Parameter", + "justify": "left", + "style": TABLE_STYLING.id, + }, + }, + ) diff --git a/src/aimbat/db.py b/src/aimbat/db.py index faeaf63b..f156f3af 100644 --- a/src/aimbat/db.py +++ b/src/aimbat/db.py @@ -36,3 +36,13 @@ def set_sqlite_pragma( cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() + + @event.listens_for(engine, "handle_error") + def _handle_missing_schema(exception_context) -> None: # type: ignore[no-untyped-def] + """Convert 'no such table' errors to a user-friendly RuntimeError.""" + if not exception_context.is_disconnect and "no such table" in str( + exception_context.original_exception + ): + raise RuntimeError( + "No AIMBAT project found. Run: aimbat project create" + ) from exception_context.original_exception diff --git a/src/aimbat/io/_data.py b/src/aimbat/io/_data.py index e4c56ad8..681148e1 100644 --- a/src/aimbat/io/_data.py +++ b/src/aimbat/io/_data.py @@ -2,6 +2,7 @@ __all__ = [ "DataType", + "DATATYPE_SUFFIXES", ] @@ -16,3 +17,10 @@ class DataType(StrEnum): JSON_STATION = auto() """JSON file containing a single seismic station record.""" + + +DATATYPE_SUFFIXES: dict[DataType, list[str]] = { + DataType.SAC: [".sac", ".bhz", ".bhn", ".bhe"], + DataType.JSON_EVENT: [".json"], + DataType.JSON_STATION: [".json"], +} diff --git a/src/aimbat/models/__init__.py b/src/aimbat/models/__init__.py index 1b468404..18653fa3 100644 --- a/src/aimbat/models/__init__.py +++ b/src/aimbat/models/__init__.py @@ -29,6 +29,7 @@ _internal_names = set(dir()) from ._models import * +from ._readers import * __all__ = [s for s in dir() if not s.startswith("_") and s not in _internal_names] diff --git a/src/aimbat/models/_models.py b/src/aimbat/models/_models.py index 8b04fedf..4fc7d811 100644 --- a/src/aimbat/models/_models.py +++ b/src/aimbat/models/_models.py @@ -14,10 +14,12 @@ from ._parameters import AimbatEventParametersBase, AimbatSeismogramParametersBase from datetime import timezone from sqlmodel import Relationship, SQLModel, Field, col, select -from sqlalchemy import func +from sqlalchemy import func, Column, PickleType +from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import column_property from pydantic import computed_field -from typing import Self, TYPE_CHECKING +from collections.abc import Hashable +from typing import Any, TYPE_CHECKING from pandas import Timestamp __all__ = [ @@ -192,6 +194,12 @@ class AimbatSnapshot(SQLModel, table=True): event: "AimbatEvent" = Relationship(back_populates="snapshots") "The event this snapshot belongs to." + if TYPE_CHECKING: + # defined as column properties below, but add same default values for type checking purposes + seismogram_count: int = 0 + selected_seismogram_count: int = 0 + flipped_seismogram_count: int = 0 + class AimbatSeismogram(SQLModel, table=True): """Class to store seismogram metadata.""" @@ -226,6 +234,11 @@ class AimbatSeismogram(SQLModel, table=True): ondelete="CASCADE", description="Foreign key referencing the parent event.", ) + extra: dict[Hashable, Any] = Field( + default_factory=dict, + sa_column=Column(MutableDict.as_mutable(PickleType)), + description="Dictionary to store any additional metadata for the seismogram.", + ) event: "AimbatEvent" = Relationship(back_populates="seismograms") "The event this seismogram belongs to." parameters: "AimbatSeismogramParameters" = Relationship( @@ -357,10 +370,15 @@ class AimbatEvent(SQLModel, table=True): "List of snapshots." if TYPE_CHECKING: + # Column properties defined below, but add same default values for type checking purposes seismogram_count: int = 0 station_count: int = 0 +# ---------------------------------------------------- +# Column properties +# ----------------------------------------------------- + AimbatEvent.seismogram_count = column_property( # type: ignore[assignment] select(func.count(col(AimbatSeismogram.id))) .where(col(AimbatSeismogram.event_id) == col(AimbatEvent.id)) @@ -377,55 +395,37 @@ class AimbatEvent(SQLModel, table=True): ) "Number of unique stations for this event." +AimbatSnapshot.seismogram_count = column_property( # type: ignore[assignment] + select(func.count(col(AimbatSeismogramParametersSnapshot.id))) + .where( + col(AimbatSeismogramParametersSnapshot.snapshot_id) == col(AimbatSnapshot.id) + ) + .correlate_except(AimbatSeismogramParametersSnapshot) + .scalar_subquery() +) +"Number of seismogram parameter snapshots associated with this snapshot." -class _AimbatEventRead(SQLModel): - """Read model for AimbatEvent including computed counts.""" - - id: uuid.UUID - active: bool | None - time: PydanticTimestamp - latitude: float - longitude: float - depth: float | None - completed: bool = False - seismogram_count: int - station_count: int - - @classmethod - def from_event(cls, event: AimbatEvent) -> Self: - """Create an AimbatEventRead from an AimbatEvent ORM instance.""" - return cls( - id=event.id, - active=event.active, - time=event.time, - latitude=event.latitude, - longitude=event.longitude, - depth=event.depth, - completed=event.parameters.completed, - seismogram_count=event.seismogram_count, - station_count=event.station_count, - ) - - -class _AimbatSnapshotRead(SQLModel): - """Read model for AimbatSnapshot with a seismogram count.""" - - id: uuid.UUID - date: PydanticTimestamp - comment: str | None - event_id: uuid.UUID - seismogram_count: int - - @classmethod - def from_snapshot(cls, snapshot: AimbatSnapshot) -> Self: - """Create an AimbatSnapshotRead from an AimbatSnapshot ORM instance.""" - return cls( - id=snapshot.id, - date=snapshot.date, - comment=snapshot.comment, - event_id=snapshot.event_id, - seismogram_count=len(snapshot.seismogram_parameters_snapshots), - ) +AimbatSnapshot.selected_seismogram_count = column_property( # type: ignore[assignment] + select(func.count(col(AimbatSeismogramParametersSnapshot.id))) + .where( + (col(AimbatSeismogramParametersSnapshot.snapshot_id) == col(AimbatSnapshot.id)) + & (col(AimbatSeismogramParametersSnapshot.select) == True) # noqa: E712 + ) + .correlate_except(AimbatSeismogramParametersSnapshot) + .scalar_subquery() +) +"Number of seismogram parameter snapshots associated with this snapshot that are marked as selected." + +AimbatSnapshot.flipped_seismogram_count = column_property( # type: ignore[assignment] + select(func.count(col(AimbatSeismogramParametersSnapshot.id))) + .where( + (col(AimbatSeismogramParametersSnapshot.snapshot_id) == col(AimbatSnapshot.id)) + & (col(AimbatSeismogramParametersSnapshot.flip) == True) # noqa: E712 + ) + .correlate_except(AimbatSeismogramParametersSnapshot) + .scalar_subquery() +) +"Number of seismogram parameter snapshots associated with this snapshot that are marked as flipped." type AimbatTypes = ( diff --git a/src/aimbat/models/_parameters.py b/src/aimbat/models/_parameters.py index 5a7a58b3..6d1b8afc 100644 --- a/src/aimbat/models/_parameters.py +++ b/src/aimbat/models/_parameters.py @@ -26,47 +26,58 @@ class AimbatEventParametersBase(SQLModel): event parameters. """ - completed: bool = Field(default=False, description="Mark an event as completed.") + completed: bool = Field( + default=False, + title="Completed", + description="Mark an event as completed.", + ) min_ccnorm: float = Field( ge=0.0, le=1.0, default_factory=lambda: settings.min_ccnorm, + title="Min CC norm", description="Minimum cross-correlation used when automatically de-selecting seismograms.", ) window_pre: PydanticNegativeTimedelta = Field( sa_type=SAPandasTimedelta, default_factory=lambda: settings.window_pre, - description="Pre-pick window length.", + title="Window pre", + description="Pre-pick window length in seconds.", ) window_post: PydanticPositiveTimedelta = Field( sa_type=SAPandasTimedelta, default_factory=lambda: settings.window_post, - description="Post-pick window length.", + title="Window post", + description="Post-pick window length in seconds.", ) bandpass_apply: bool = Field( default_factory=lambda: settings.bandpass_apply, + title="Bandpass apply", description="Whether to apply bandpass filter to seismograms.", ) bandpass_fmin: float = Field( default_factory=lambda: settings.bandpass_fmin, ge=0, - description="Minimum frequency for bandpass filter (ignored if `bandpass_apply` is False).", + title="Bandpass f min", + description="Minimum frequency for bandpass filter in Hz (ignored if `bandpass_apply` is False).", ) bandpass_fmax: float = Field( default_factory=lambda: settings.bandpass_fmax, gt=0, - description="Maximum frequency for bandpass filter (ignored if `bandpass_apply` is False).", + title="Bandpass f max", + description="Maximum frequency for bandpass filter in Hz (ignored if `bandpass_apply` is False).", ) mccc_damp: float = Field( default_factory=lambda: settings.mccc_damp, ge=0, + title="MCCC damp", description="Damping factor for MCCC algorithm.", ) @@ -74,6 +85,7 @@ class AimbatEventParametersBase(SQLModel): default_factory=lambda: settings.mccc_min_ccnorm, ge=0, le=1, + title="MCCC min CC norm", description="Minimum correlation coefficient required to include a pair in the MCCC inversion.", ) diff --git a/src/aimbat/models/_readers.py b/src/aimbat/models/_readers.py new file mode 100644 index 00000000..64e11c29 --- /dev/null +++ b/src/aimbat/models/_readers.py @@ -0,0 +1,63 @@ +import uuid +from sqlmodel import SQLModel +from typing import Self, TYPE_CHECKING +from aimbat._types import PydanticTimestamp + +if TYPE_CHECKING: + from ._models import AimbatEvent, AimbatSnapshot + +__all__ = ["_AimbatEventRead", "_AimbatSnapshotRead"] + + +class _AimbatEventRead(SQLModel): + """Read model for AimbatEvent including computed counts.""" + + id: uuid.UUID + active: bool | None + time: PydanticTimestamp + latitude: float + longitude: float + depth: float | None + completed: bool = False + seismogram_count: int + station_count: int + + @classmethod + def from_event(cls, event: "AimbatEvent") -> Self: + """Create an AimbatEventRead from an AimbatEvent ORM instance.""" + return cls( + id=event.id, + active=event.active, + time=event.time, + latitude=event.latitude, + longitude=event.longitude, + depth=event.depth, + completed=event.parameters.completed, + seismogram_count=event.seismogram_count, + station_count=event.station_count, + ) + + +class _AimbatSnapshotRead(SQLModel): + """Read model for AimbatSnapshot with a seismogram count.""" + + id: uuid.UUID + date: PydanticTimestamp + comment: str | None + event_id: uuid.UUID + seismogram_count: int + selected_seismogram_count: int + flipped_seismogram_count: int + + @classmethod + def from_snapshot(cls, snapshot: "AimbatSnapshot") -> Self: + """Create an AimbatSnapshotRead from an AimbatSnapshot ORM instance.""" + return cls( + id=snapshot.id, + date=snapshot.date, + comment=snapshot.comment, + event_id=snapshot.event_id, + seismogram_count=snapshot.seismogram_count, + selected_seismogram_count=snapshot.selected_seismogram_count, + flipped_seismogram_count=snapshot.flipped_seismogram_count, + ) diff --git a/tests/functional/test_cli_parameters.py b/tests/functional/test_cli_parameters.py index 618695e1..da89a7cf 100644 --- a/tests/functional/test_cli_parameters.py +++ b/tests/functional/test_cli_parameters.py @@ -236,6 +236,76 @@ def test_set_bandpass_fmax( ), "'bandpass_fmax' should be 2.0 after being set" +# =================================================================== +# Event parameter — set timedelta +# =================================================================== + + +@pytest.mark.cli +class TestEventParameterSetTimedelta: + """Tests for setting timedelta event parameters and verifying via dump.""" + + def test_set_window_pre_as_bare_number( + self, + loaded_engine: Engine, + cli: Callable[[str], None], + cli_json: Callable[[str], list | dict], + ) -> None: + """Verifies that a bare number is interpreted as seconds for window_pre. + + Args: + loaded_engine: The monkeypatched engine with data loaded. + cli: The in-process CLI callable. + cli_json: The in-process CLI JSON dump callable. + """ + cli("event parameter set window_pre -20") + after = cli_json("event parameter dump") + assert isinstance(after, dict), "Dump should return a dict for active event" + assert after["window_pre"] == pytest.approx( + -20.0 + ), "'window_pre' should be -20.0 seconds after being set with a bare number" + + def test_set_window_post_as_bare_number( + self, + loaded_engine: Engine, + cli: Callable[[str], None], + cli_json: Callable[[str], list | dict], + ) -> None: + """Verifies that a bare number is interpreted as seconds for window_post. + + Args: + loaded_engine: The monkeypatched engine with data loaded. + cli: The in-process CLI callable. + cli_json: The in-process CLI JSON dump callable. + """ + cli("event parameter set window_post 30") + after = cli_json("event parameter dump") + assert isinstance(after, dict), "Dump should return a dict for active event" + assert after["window_post"] == pytest.approx( + 30.0 + ), "'window_post' should be 30.0 seconds after being set with a bare number" + + def test_set_window_pre_with_unit_string( + self, + loaded_engine: Engine, + cli: Callable[[str], None], + cli_json: Callable[[str], list | dict], + ) -> None: + """Verifies that a pandas-style unit string (e.g. '10s') is accepted for window_post. + + Args: + loaded_engine: The monkeypatched engine with data loaded. + cli: The in-process CLI callable. + cli_json: The in-process CLI JSON dump callable. + """ + cli("event parameter set window_post 20s") + after = cli_json("event parameter dump") + assert isinstance(after, dict), "Dump should return a dict for active event" + assert after["window_post"] == pytest.approx( + 20.0 + ), "'window_post' should be 20.0 seconds after being set with '20s'" + + # =================================================================== # Event parameter — dump # =================================================================== diff --git a/tests/functional/test_cli_snapshots.py b/tests/functional/test_cli_snapshots.py index 4e05ca17..1a4201a4 100644 --- a/tests/functional/test_cli_snapshots.py +++ b/tests/functional/test_cli_snapshots.py @@ -572,3 +572,86 @@ def test_list_short( assert ( len(capsys.readouterr().out) > 0 ), "Expected output from snapshot list --short" + + +# =================================================================== +# Snapshot details +# =================================================================== + + +@pytest.mark.cli +class TestSnapshotDetails: + """Tests for the ``snapshot details`` CLI command.""" + + def test_details_produces_output( + self, + loaded_engine: Engine, + cli: Callable[[str], None], + cli_json: Callable[[str], list | dict], + capsys: pytest.CaptureFixture[str], + ) -> None: + """Verifies that the details command produces output for a valid snapshot ID. + + Args: + loaded_engine: The monkeypatched engine with data loaded. + cli: The in-process CLI callable. + cli_json: The in-process CLI JSON dump callable. + capsys: The pytest capsys fixture. + """ + cli("snapshot create") + data = cli_json("snapshot dump") + assert isinstance(data, dict), "Dump should return a dict" + snapshot_id = data["snapshots"][0]["id"] + + cli(f"snapshot details {snapshot_id}") + assert len(capsys.readouterr().out) > 0, "Expected output from snapshot details" + + def test_details_produces_output_with_short_id( + self, + loaded_engine: Engine, + cli: Callable[[str], None], + cli_json: Callable[[str], list | dict], + capsys: pytest.CaptureFixture[str], + ) -> None: + """Verifies that the details command works with a shortened snapshot ID. + + Args: + loaded_engine: The monkeypatched engine with data loaded. + cli: The in-process CLI callable. + cli_json: The in-process CLI JSON dump callable. + capsys: The pytest capsys fixture. + """ + cli("snapshot create") + data = cli_json("snapshot dump") + assert isinstance(data, dict), "Dump should return a dict" + short_id = data["snapshots"][0]["id"][:8] + + cli(f"snapshot details {short_id}") + assert ( + len(capsys.readouterr().out) > 0 + ), "Expected output from snapshot details with short ID" + + def test_details_short_flag( + self, + loaded_engine: Engine, + cli: Callable[[str], None], + cli_json: Callable[[str], list | dict], + capsys: pytest.CaptureFixture[str], + ) -> None: + """Verifies that ``--short`` produces output. + + Args: + loaded_engine: The monkeypatched engine with data loaded. + cli: The in-process CLI callable. + cli_json: The in-process CLI JSON dump callable. + capsys: The pytest capsys fixture. + """ + cli("snapshot create") + data = cli_json("snapshot dump") + assert isinstance(data, dict), "Dump should return a dict" + snapshot_id = data["snapshots"][0]["id"] + + cli(f"snapshot details {snapshot_id} --short") + assert ( + len(capsys.readouterr().out) > 0 + ), "Expected output from snapshot details --short" diff --git a/tests/integration/core/test_seismogram.py b/tests/integration/core/test_seismogram.py index 19a0cb35..e2856308 100644 --- a/tests/integration/core/test_seismogram.py +++ b/tests/integration/core/test_seismogram.py @@ -10,6 +10,8 @@ get_seismogram_parameter_by_id, set_seismogram_parameter, set_seismogram_parameter_by_id, + reset_seismogram_parameters, + reset_seismogram_parameters_by_id, get_selected_seismograms, dump_seismogram_table_to_json, dump_seismogram_parameter_table_to_json, @@ -17,6 +19,7 @@ print_seismogram_parameter_table, plot_all_seismograms, ) +from aimbat.models._parameters import AimbatSeismogramParametersBase from aimbat._types import SeismogramParameter from aimbat.models import AimbatSeismogram from matplotlib.figure import Figure @@ -217,6 +220,53 @@ def test_set_by_id_not_found(self, session: Session) -> None: ) +class TestResetSeismogramParameters: + """Tests for resetting seismogram parameters to their defaults.""" + + def test_reset_parameters( + self, session: Session, seismogram: AimbatSeismogram + ) -> None: + """Verifies that all parameters are restored to their default values after reset. + + Args: + session: The database session. + seismogram: An AimbatSeismogram whose parameters are modified then reset. + """ + set_seismogram_parameter(session, seismogram, SeismogramParameter.FLIP, True) + set_seismogram_parameter(session, seismogram, SeismogramParameter.SELECT, False) + set_seismogram_parameter( + session, seismogram, SeismogramParameter.T1, seismogram.t0 + ) + reset_seismogram_parameters(session, seismogram) + defaults = AimbatSeismogramParametersBase() + for field_name in AimbatSeismogramParametersBase.model_fields: + assert getattr(seismogram.parameters, field_name) == getattr( + defaults, field_name + ) + + def test_reset_parameters_by_id( + self, session: Session, seismogram: AimbatSeismogram + ) -> None: + """Verifies that reset via ID produces the same result as reset by instance. + + Args: + session: The database session. + seismogram: An AimbatSeismogram whose parameters are modified then reset. + """ + set_seismogram_parameter(session, seismogram, SeismogramParameter.FLIP, True) + reset_seismogram_parameters_by_id(session, seismogram.id) + assert seismogram.parameters.flip is False + + def test_reset_parameters_by_id_not_found(self, session: Session) -> None: + """Verifies that resetting a non-existent seismogram ID raises NoResultFound. + + Args: + session: The database session. + """ + with pytest.raises(NoResultFound): + reset_seismogram_parameters_by_id(session, uuid.uuid4()) + + class TestGetSelectedSeismograms: """Tests for retrieving selected seismograms.""" @@ -412,5 +462,5 @@ def test_returns_figure(self, session: Session) -> None: Args: session: The database session. """ - fig = plot_all_seismograms(session) + fig, _ = plot_all_seismograms(session, return_fig=True) assert isinstance(fig, Figure) diff --git a/tests/integration/core/test_snapshots.py b/tests/integration/core/test_snapshots.py index 8605bad7..64623e20 100644 --- a/tests/integration/core/test_snapshots.py +++ b/tests/integration/core/test_snapshots.py @@ -3,6 +3,7 @@ import json import uuid import pytest +from pandas import Timedelta, Timestamp from aimbat.core._snapshot import ( create_snapshot, delete_snapshot, @@ -12,6 +13,8 @@ rollback_to_snapshot_by_id, dump_snapshot_tables_to_json, print_snapshot_table, + print_snapshot_parameters_table, + print_snapshot_parameters_table_by_id, ) from aimbat.core import get_active_event from aimbat.models import AimbatSnapshot, AimbatSeismogram @@ -218,6 +221,77 @@ def test_rollback_by_id(self, session: Session, snapshot: AimbatSnapshot) -> Non session.refresh(active_event) assert active_event.parameters.min_ccnorm == original_min_ccnorm + def test_rollback_restores_all_event_parameters( + self, session: Session, snapshot: AimbatSnapshot + ) -> None: + """Verifies that every event parameter, including timedelta fields, is restored. + + Args: + session: The database session. + snapshot: An AimbatSnapshot capturing the original parameters. + """ + active_event = get_active_event(session) + params = active_event.parameters + snap = snapshot.event_parameters_snapshot + + # Mutate every event parameter to a value distinct from the snapshot + params.completed = not snap.completed + params.min_ccnorm = 0.0 if snap.min_ccnorm != 0.0 else 0.5 + params.window_pre = Timedelta(seconds=-1) + params.window_post = Timedelta(seconds=1) + params.bandpass_apply = not snap.bandpass_apply + params.bandpass_fmin = 0.1 + params.bandpass_fmax = 1.0 + params.mccc_damp = 0.0 if snap.mccc_damp != 0.0 else 1.0 + params.mccc_min_ccnorm = 0.0 if snap.mccc_min_ccnorm != 0.0 else 0.5 + session.add(params) + session.commit() + + rollback_to_snapshot(session, snapshot) + session.refresh(params) + + assert params.completed == snap.completed + assert params.min_ccnorm == snap.min_ccnorm + assert params.window_pre == snap.window_pre + assert params.window_post == snap.window_post + assert params.bandpass_apply == snap.bandpass_apply + assert params.bandpass_fmin == snap.bandpass_fmin + assert params.bandpass_fmax == snap.bandpass_fmax + assert params.mccc_damp == snap.mccc_damp + assert params.mccc_min_ccnorm == snap.mccc_min_ccnorm + + def test_rollback_restores_all_seismogram_parameters( + self, session: Session, snapshot: AimbatSnapshot + ) -> None: + """Verifies that every seismogram parameter, including the timestamp t1, is restored. + + Args: + session: The database session. + snapshot: An AimbatSnapshot capturing the original parameters. + """ + active_event = get_active_event(session) + seismogram = active_event.seismograms[0] + params = seismogram.parameters + snap = next( + s + for s in snapshot.seismogram_parameters_snapshots + if s.seismogram_parameters_id == params.id + ) + + # Mutate every seismogram parameter to a value distinct from the snapshot + params.flip = not snap.flip + params.select = not snap.select + params.t1 = Timestamp("2000-01-01", tz="UTC") + session.add(params) + session.commit() + + rollback_to_snapshot(session, snapshot) + session.refresh(params) + + assert params.flip == snap.flip + assert params.select == snap.select + assert params.t1 == snap.t1 + def test_rollback_by_id_not_found(self, session: Session) -> None: """Verifies that rolling back to a non-existent snapshot ID raises ValueError. @@ -337,6 +411,87 @@ def test_seismogram_parameters_count( assert len(result["seismogram_parameters"]) <= n_seismograms +class TestPrintSnapshotParametersTable: + """Tests for printing the snapshot parameters table.""" + + def test_print_short( + self, + session: Session, + snapshot: AimbatSnapshot, + capsys: pytest.CaptureFixture, + ) -> None: + """Verifies that output is produced with short=True. + + Args: + session: The database session. + snapshot: An AimbatSnapshot whose event parameters are displayed. + capsys: The pytest capsys fixture. + """ + print_snapshot_parameters_table( + session, snapshot.event_parameters_snapshot, short=True + ) + assert len(capsys.readouterr().out) > 0 + + def test_print_long( + self, + session: Session, + snapshot: AimbatSnapshot, + capsys: pytest.CaptureFixture, + ) -> None: + """Verifies that output is produced with short=False. + + Args: + session: The database session. + snapshot: An AimbatSnapshot whose event parameters are displayed. + capsys: The pytest capsys fixture. + """ + print_snapshot_parameters_table( + session, snapshot.event_parameters_snapshot, short=False + ) + assert len(capsys.readouterr().out) > 0 + + def test_print_by_id_short( + self, + session: Session, + snapshot: AimbatSnapshot, + capsys: pytest.CaptureFixture, + ) -> None: + """Verifies that output is produced when looking up by snapshot ID with short=True. + + Args: + session: The database session. + snapshot: An AimbatSnapshot whose ID is used for lookup. + capsys: The pytest capsys fixture. + """ + print_snapshot_parameters_table_by_id(session, snapshot.id, short=True) + assert len(capsys.readouterr().out) > 0 + + def test_print_by_id_long( + self, + session: Session, + snapshot: AimbatSnapshot, + capsys: pytest.CaptureFixture, + ) -> None: + """Verifies that output is produced when looking up by snapshot ID with short=False. + + Args: + session: The database session. + snapshot: An AimbatSnapshot whose ID is used for lookup. + capsys: The pytest capsys fixture. + """ + print_snapshot_parameters_table_by_id(session, snapshot.id, short=False) + assert len(capsys.readouterr().out) > 0 + + def test_print_by_id_not_found(self, session: Session) -> None: + """Verifies that a ValueError is raised for an unknown snapshot ID. + + Args: + session: The database session. + """ + with pytest.raises(ValueError): + print_snapshot_parameters_table_by_id(session, uuid.uuid4(), short=False) + + class TestPrintSnapshotTable: """Tests for printing the snapshot table.""" diff --git a/tests/integration/models/test_operations.py b/tests/integration/models/test_operations.py index ab7abce4..33bfe15e 100644 --- a/tests/integration/models/test_operations.py +++ b/tests/integration/models/test_operations.py @@ -1,6 +1,7 @@ """Integration tests for ORM relationships and cascade deletes in AIMBAT models.""" import pytest +from aimbat.core import get_active_event from aimbat.core._snapshot import create_snapshot from aimbat.models import ( AimbatDataSource, @@ -245,6 +246,76 @@ def test_snapshot_back_reference_to_event(self, session: Session) -> None: snapshot = session.exec(select(AimbatSnapshot)).one() assert isinstance(snapshot.event, AimbatEvent) + def test_snapshot_seismogram_count(self, session: Session) -> None: + """Verifies that seismogram_count matches the number of seismogram parameter snapshots. + + Args: + session: The database session. + """ + create_snapshot(session) + snapshot = session.exec(select(AimbatSnapshot)).one() + session.refresh(snapshot) + assert snapshot.seismogram_count == len( + snapshot.seismogram_parameters_snapshots + ) + + def test_snapshot_selected_seismogram_count(self, session: Session) -> None: + """Verifies that selected_seismogram_count reflects snapshots marked as selected. + + Args: + session: The database session. + """ + create_snapshot(session) + snapshot = session.exec(select(AimbatSnapshot)).one() + session.refresh(snapshot) + expected = sum(1 for s in snapshot.seismogram_parameters_snapshots if s.select) + assert snapshot.selected_seismogram_count == expected + + def test_snapshot_flipped_seismogram_count(self, session: Session) -> None: + """Verifies that flipped_seismogram_count reflects snapshots marked as flipped. + + Args: + session: The database session. + """ + create_snapshot(session) + snapshot = session.exec(select(AimbatSnapshot)).one() + session.refresh(snapshot) + expected = sum(1 for s in snapshot.seismogram_parameters_snapshots if s.flip) + assert snapshot.flipped_seismogram_count == expected + + def test_snapshot_counts_reflect_toggled_flip_and_select( + self, session: Session + ) -> None: + """Verifies snapshot counts reflect toggled flip and select on seismograms. + + Flips one seismogram and deselects another, then takes a snapshot and + checks that flipped_seismogram_count and selected_seismogram_count + capture the changes. + + Args: + session: The database session. + """ + active_event = get_active_event(session) + seismograms = active_event.seismograms + assert len(seismograms) >= 2 + + to_flip = seismograms[0] + to_deselect = seismograms[1] + + to_flip.parameters.flip = True + to_deselect.parameters.select = False + session.add(to_flip.parameters) + session.add(to_deselect.parameters) + session.commit() + + create_snapshot(session) + snapshot = session.exec(select(AimbatSnapshot)).one() + session.refresh(snapshot) + + total = len(snapshot.seismogram_parameters_snapshots) + assert snapshot.flipped_seismogram_count == 1 + assert snapshot.selected_seismogram_count == total - 1 + # --------------------------------------------------------------------------- # Cascade delete diff --git a/tests/integration/tui/test_app.py b/tests/integration/tui/test_app.py new file mode 100644 index 00000000..1042ca2a --- /dev/null +++ b/tests/integration/tui/test_app.py @@ -0,0 +1,17 @@ +"""Integration tests for the AIMBAT TUI application.""" + +from aimbat.models._parameters import AimbatEventParametersBase + + +def test_event_parameters_have_titles() -> None: + """Verify all AimbatEventParametersBase fields have a title set. + + This guards against parameters being added to the model without also setting + the title metadata used by the TUI. + """ + missing = [ + name + for name, field_info in AimbatEventParametersBase.model_fields.items() + if not field_info.title + ] + assert not missing, f"Fields missing title in AimbatEventParametersBase: {missing}" diff --git a/tests/unit/_cli/test_common.py b/tests/unit/_cli/test_common.py index 0ec166e3..4bee4447 100644 --- a/tests/unit/_cli/test_common.py +++ b/tests/unit/_cli/test_common.py @@ -3,7 +3,6 @@ import pytest from aimbat._cli.common import ( GlobalParameters, - PlotParameters, IccsPlotParameters, TableParameters, CliHints, @@ -33,20 +32,6 @@ def test_debug_false_does_not_change_log_level(self) -> None: assert settings.log_level == original -class TestPlotParameters: - """Tests for the PlotParameters dataclass.""" - - def test_default_use_qt_is_false(self) -> None: - """Verifies that use_qt defaults to False.""" - params = PlotParameters() - assert params.use_qt is False - - def test_use_qt_can_be_set_true(self) -> None: - """Verifies that use_qt can be set to True.""" - params = PlotParameters(use_qt=True) - assert params.use_qt is True - - class TestIccsPlotParameters: """Tests for the IccsPlotParameters dataclass.""" diff --git a/tests/unit/test_app.py b/tests/unit/test_app_entrypoint.py similarity index 100% rename from tests/unit/test_app.py rename to tests/unit/test_app_entrypoint.py diff --git a/tox.ini b/tox.ini index 927fd3c3..b3075eb5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,4 @@ [tox] -requires = - tox-uv env_list = lint, py{312,313,314}, report isolated_build = true @@ -12,15 +10,17 @@ python = [testenv] description = run the tests with pytest +passenv = + UV_* setenv = py{312,313,314}: COVERAGE_FILE = {env:COVERAGE_FILE:.coverage.{envname}} depends = report: py312, py313, py314 allowlist_externals = uv commands_pre = - uv sync --active --no-managed-python --python python --no-group docs + uv sync --active --inexact --python python --no-group docs commands = - py.test --mpl --mypy --cov=aimbat --cov-append --cov-report=term-missing --junitxml=junit.xml + py.test --mpl --mypy --cov --cov-append --cov-report=term-missing --junitxml=junit.xml [testenv:report] description = create test report @@ -45,7 +45,6 @@ skipsdist = true deps = ruff black - black[jupyter] commands_pre = commands = black . --check diff --git a/uv.lock b/uv.lock index 5151f6a6..e5ceeb4d 100644 --- a/uv.lock +++ b/uv.lock @@ -20,16 +20,17 @@ dependencies = [ { name = "pandas" }, { name = "pandas-stubs" }, { name = "pydantic-settings" }, - { name = "pyqtgraph" }, - { name = "pyside6" }, { name = "pysmo" }, { name = "rich" }, { name = "sqlmodel" }, + { name = "textual" }, + { name = "textual-fspicker" }, ] [package.dev-dependencies] dev = [ { name = "black" }, + { name = "textual-dev" }, ] docs = [ { name = "git-cliff" }, @@ -43,7 +44,6 @@ test = [ { name = "pytest-dependency" }, { name = "pytest-mpl" }, { name = "pytest-mypy" }, - { name = "pytest-qt" }, { name = "pytest-sugar" }, { name = "ruff" }, ] @@ -56,15 +56,18 @@ requires-dist = [ { name = "pandas", specifier = ">=3.0.1" }, { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, - { name = "pyqtgraph", specifier = ">=0.13.7" }, - { name = "pyside6", specifier = ">=6.9.2" }, { name = "pysmo", git = "https://github.com/pysmo/pysmo?rev=master" }, { name = "rich", specifier = ">=13.9.4" }, { name = "sqlmodel", specifier = ">=0.0.24" }, + { name = "textual", specifier = ">=8.0.0" }, + { name = "textual-fspicker", specifier = ">=1.0.0" }, ] [package.metadata.requires-dev] -dev = [{ name = "black", specifier = ">=26.1.0" }] +dev = [ + { name = "black", specifier = ">=26.1.0" }, + { name = "textual-dev", specifier = ">=1.8.0" }, +] docs = [ { name = "git-cliff", specifier = ">=2.12.0" }, { name = "griffe-fieldz", specifier = ">=0.5.0" }, @@ -77,11 +80,130 @@ test = [ { name = "pytest-dependency", specifier = ">=0.6.0" }, { name = "pytest-mpl", specifier = ">=0.17.0" }, { name = "pytest-mypy", specifier = ">=1.0.1" }, - { name = "pytest-qt", specifier = ">=4.5.0" }, { name = "pytest-sugar", specifier = ">=1.1.1" }, { name = "ruff", specifier = ">=0.13.0" }, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiohttp-jinja2" +version = "1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/39/da5a94dd89b1af7241fb7fc99ae4e73505b5f898b540b6aba6dc7afe600e/aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2", size = 53057, upload-time = "2023-11-18T15:30:52.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/90/65238d4246307195411b87a07d03539049819b022c01bcc773826f600138/aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7", size = 11736, upload-time = "2023-11-18T15:30:50.743Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -113,15 +235,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "beartype" -version = "0.22.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, -] - [[package]] name = "black" version = "26.1.0" @@ -412,11 +525,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.24.3" +version = "3.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, ] [[package]] @@ -460,6 +573,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -748,6 +950,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -782,6 +996,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -899,6 +1118,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1000,6 +1231,149 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -1302,6 +1676,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -1489,73 +1947,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, ] -[[package]] -name = "pyqtgraph" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "numpy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, -] - -[[package]] -name = "pyside6" -version = "6.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyside6-addons" }, - { name = "pyside6-essentials" }, - { name = "shiboken6" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/0f/5736889fc850794623692cb369e295a994175e51295fa52134626f486296/pyside6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4b084293caa7845d0064aaf6af258e0f7caae03a14a33537d0a552131afddaf0", size = 563185, upload-time = "2026-02-02T08:50:47.161Z" }, - { url = "https://files.pythonhosted.org/packages/35/d3/ab5cd2fac3d34469c7376e0cd18eec92905dbe44748c70bda7699a2a7206/pyside6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1b89ce8558d4b4f35b85bff1db90d680912e4d3ce9e79ff804d6fef1d1a151ef", size = 563357, upload-time = "2026-02-02T08:50:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8c/55bbd50c138c8dc12edc9f25e9d94760a33e574905468e98dff399094baa/pyside6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:0439f5e9b10ebe6177981bac9e219096ec970ac6ec215bef055279802ba50601", size = 563357, upload-time = "2026-02-02T08:50:50.077Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d4/673b8112b4a260377f760be835c4e357163fdaf68a56a1aec59aeb8e584b/pyside6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:032bad6b18a17fcbf4dddd0397f49b07f8aae7f1a45b7e4de7037bf7fd6e0edf", size = 569554, upload-time = "2026-02-02T08:50:51.147Z" }, - { url = "https://files.pythonhosted.org/packages/14/95/bda648fcccf61fe58cb417284716ae30acdddd44f7d4cbad6eea4ccaa872/pyside6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:65a59ad0bc92525639e3268d590948ce07a80ee97b55e7a9200db41d493cac31", size = 553828, upload-time = "2026-02-02T08:50:52.244Z" }, -] - -[[package]] -name = "pyside6-addons" -version = "6.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyside6-essentials" }, - { name = "shiboken6" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/06/c283567628ffa2cefc3c72374ad607f1dfc9842a03db65f1347b9ae52bee/pyside6_addons-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:0de7d0c9535e17d5e3b634b61314a1867f3b0f6d35c3d7cdc99efc353192faff", size = 322745605, upload-time = "2026-02-02T08:39:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/a5/69/e1ab8c756fd3984b1fd7b186446227f524f6b561160bfbfdba8874b4709a/pyside6_addons-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:030a851163b51dbf0063be59e9ddb6a9e760bde89a28e461ccc81a224d286eaf", size = 170718434, upload-time = "2026-02-02T08:40:55.989Z" }, - { url = "https://files.pythonhosted.org/packages/df/e5/18ba86ba86d1231c486d36f9accfe862ed6eb52ca0b698aeaf6e837a87ca/pyside6_addons-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:fcee0373e3fd7b98f014094e5e37b4a39e4de7c5a47c13f654a7d557d4a426ad", size = 166423836, upload-time = "2026-02-02T08:42:44.918Z" }, - { url = "https://files.pythonhosted.org/packages/99/13/503bec9201881968c372cb634069535e80aec2489f3907d676e151a1023f/pyside6_addons-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:c20150068525a17494f3b6576c5d61c417cf9a5870659e29f5ebd83cd20a78ea", size = 164712775, upload-time = "2026-02-02T08:43:23.729Z" }, - { url = "https://files.pythonhosted.org/packages/b6/39/44d6710b4dd18d745077b5fc6ded4ba6f32987a6e49c5834529e50f02155/pyside6_addons-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:3d18db739b46946ba7b722d8ad4cc2097135033aa6ea57076e64d591e6a345f3", size = 34041396, upload-time = "2026-02-02T08:43:31.246Z" }, -] - -[[package]] -name = "pyside6-essentials" -version = "6.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "shiboken6" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/2e/5f18a77f5e0bd730bacec93a690d0ef3c96a9711d213653eacecbf241b8d/pyside6_essentials-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:1dee2cb9803ff135f881dadeb5c0edcef793d1ec4f8a9140a1348cecb71074e1", size = 105913067, upload-time = "2026-02-02T08:45:37.508Z" }, - { url = "https://files.pythonhosted.org/packages/99/20/3a6ca95052e1744b5a3eba164e2dd451d358a3dcaf78179de4b45c8e3f47/pyside6_essentials-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:660aea45bfa36f1e06f799b934c2a7df963bd31abc5083e8bb8a5bfaef45686b", size = 77027153, upload-time = "2026-02-02T08:45:53.09Z" }, - { url = "https://files.pythonhosted.org/packages/93/a6/6073e4ddc2a5c7b3941606e4bc8bbaadcf0737f57450620b0793041c8d22/pyside6_essentials-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c2b028e4c6f8047a02c31f373408e23b4eedfd405f56c6aba8d0525c29472835", size = 76114242, upload-time = "2026-02-02T08:46:07.184Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/616bbbd009efd3e17bf9a2db09d90c6764c010565cd2bdea2a240bfd18f7/pyside6_essentials-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:0741018c2b6395038cad4c41775cfae3f13a409e87995ac9f7d89e5b1fb6b22a", size = 74546490, upload-time = "2026-02-02T08:46:26.395Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f9/c9757a984c4ffb6d12fab69e966d95dfc862a5d44e12b7900f3a03780b76/pyside6_essentials-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:db5f4913648bb6afddb8b347edae151ee2378f12bceb03c8b2515a530a4b38d9", size = 55258626, upload-time = "2026-02-02T08:46:36.788Z" }, -] - [[package]] name = "pysmo" -version = "1.0.0.dev21+g04a801340" -source = { git = "https://github.com/pysmo/pysmo?rev=master#04a801340c853ef5fad7f061a1da88c10497b3d2" } +version = "1.0.0.dev31+gaa71a1da1" +source = { git = "https://github.com/pysmo/pysmo?rev=master#aa71a1da1e068d62a23292520755e432c0872c85" } dependencies = [ + { name = "annotated-types" }, { name = "attrs" }, - { name = "beartype" }, { name = "cattrs" }, { name = "httpx" }, { name = "matplotlib" }, @@ -1637,20 +2035,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/93/25ed3c02e15c4ef1b04cbda7c708ffc5da755986aaacfb48db1f9e84a996/pytest_mypy-1.0.1-py3-none-any.whl", hash = "sha256:ad7133c9b92c802e032f2596590ebede7eea7c418e61d60d5cdd571b55c72056", size = 8701, upload-time = "2025-04-02T19:31:14.914Z" }, ] -[[package]] -name = "pytest-qt" -version = "4.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pluggy" }, - { name = "pytest" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d3/61/8bdec02663c18bf5016709b909411dce04a868710477dc9b9844ffcf8dd2/pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f", size = 128702, upload-time = "2025-07-01T17:24:39.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/d0/8339b888ad64a3d4e508fed8245a402b503846e1972c10ad60955883dcbb/pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff", size = 37214, upload-time = "2025-07-01T17:24:38.226Z" }, -] - [[package]] name = "pytest-sugar" version = "1.1.1" @@ -1678,11 +2062,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -1905,18 +2289,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] -[[package]] -name = "shiboken6" -version = "6.10.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/38/3912eb08a3b865b5fcdb4bdce8076cacc211986cee587f5cb62e637791af/shiboken6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:3bd4e94e9a3c8c1fa8362fd752d399ef39265d5264e4e37bae61cdaa2a00c8c7", size = 479829, upload-time = "2026-02-02T08:50:22.495Z" }, - { url = "https://files.pythonhosted.org/packages/52/88/292e0576489c46624ab419ee284ac5a59ae10e2eb34a58b6abca51dfd290/shiboken6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ace0790032d9cb0adda644b94ee28d59410180d9773643bb6cf8438c361987ad", size = 273052, upload-time = "2026-02-02T08:50:24.539Z" }, - { url = "https://files.pythonhosted.org/packages/06/c2/03d44d34e8264e1f25671677fece95b414c70fd85dcc2be8d5e821ee2628/shiboken6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:f74d3ed1f92658077d0630c39e694eb043aeb1d830a5d275176c45d07147427f", size = 269868, upload-time = "2026-02-02T08:50:25.662Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/5ca52c0ef86b3d01572131b6709bd531a080995f7e680720e9424328ce1d/shiboken6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:10f3c8c5e1b8bee779346f21c10dbc14cff068f0b0b4e62420c82a6bf36ac2e7", size = 1222052, upload-time = "2026-02-02T08:50:27.502Z" }, - { url = "https://files.pythonhosted.org/packages/46/52/421fd378313c89b67ee7d584bf4e9ec088fa1804891b8d74e02b16703457/shiboken6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:20c671645d70835af212ee05df60361d734c5305edb2746e9875c6a31283f963", size = 1784089, upload-time = "2026-02-02T08:50:29.069Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -1928,48 +2300,48 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.47" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323, upload-time = "2026-02-24T16:34:27.947Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1", size = 2155687, upload-time = "2026-02-24T17:05:46.451Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356", size = 3336978, upload-time = "2026-02-24T17:18:04.597Z" }, - { url = "https://files.pythonhosted.org/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7", size = 3349939, upload-time = "2026-02-24T17:27:18.937Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec", size = 3279648, upload-time = "2026-02-24T17:18:07.038Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602", size = 3314695, upload-time = "2026-02-24T17:27:20.93Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9", size = 2115502, upload-time = "2026-02-24T17:22:52.546Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb", size = 2142435, upload-time = "2026-02-24T17:22:54.268Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e5/0af64ce7d8f60ec5328c10084e2f449e7912a9b8bdbefdcfb44454a25f49/sqlalchemy-2.0.47-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456a135b790da5d3c6b53d0ef71ac7b7d280b7f41eb0c438986352bf03ca7143", size = 2152551, upload-time = "2026-02-24T17:05:47.675Z" }, - { url = "https://files.pythonhosted.org/packages/63/79/746b8d15f6940e2ac469ce22d7aa5b1124b1ab820bad9b046eb3000c88a6/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09a2f7698e44b3135433387da5d8846cf7cc7c10e5425af7c05fee609df978b6", size = 3278782, upload-time = "2026-02-24T17:18:10.012Z" }, - { url = "https://files.pythonhosted.org/packages/91/b1/bd793ddb34345d1ed43b13ab2d88c95d7d4eb2e28f5b5a99128b9cc2bca2/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bbc72e6a177c78d724f9106aaddc0d26a2ada89c6332b5935414eccf04cbd5", size = 3295155, upload-time = "2026-02-24T17:27:22.827Z" }, - { url = "https://files.pythonhosted.org/packages/97/84/7213def33f94e5ca6f5718d259bc9f29de0363134648425aa218d4356b23/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75460456b043b78b6006e41bdf5b86747ee42eafaf7fffa3b24a6e9a456a2092", size = 3226834, upload-time = "2026-02-24T17:18:11.465Z" }, - { url = "https://files.pythonhosted.org/packages/ef/06/456810204f4dc29b5f025b1b0a03b4bd6b600ebf3c1040aebd90a257fa33/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d9adaa616c3bc7d80f9ded57cd84b51d6617cad6a5456621d858c9f23aaee01", size = 3265001, upload-time = "2026-02-24T17:27:24.813Z" }, - { url = "https://files.pythonhosted.org/packages/fb/20/df3920a4b2217dbd7390a5bd277c1902e0393f42baaf49f49b3c935e7328/sqlalchemy-2.0.47-cp313-cp313-win32.whl", hash = "sha256:76e09f974382a496a5ed985db9343628b1cb1ac911f27342e4cc46a8bac10476", size = 2113647, upload-time = "2026-02-24T17:22:55.747Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/7873ddf69918efbfabd7211829f4bd8019739d0a719253112d305d3ba51d/sqlalchemy-2.0.47-cp313-cp313-win_amd64.whl", hash = "sha256:0664089b0bf6724a0bfb49a0cf4d4da24868a0a5c8e937cd7db356d5dcdf2c66", size = 2139425, upload-time = "2026-02-24T17:22:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/54/fa/61ad9731370c90ac7ea5bf8f5eaa12c48bb4beec41c0fa0360becf4ac10d/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed0c967c701ae13da98eb220f9ddab3044ab63504c1ba24ad6a59b26826ad003", size = 3558809, upload-time = "2026-02-24T17:12:15.232Z" }, - { url = "https://files.pythonhosted.org/packages/33/d5/221fac96f0529391fe374875633804c866f2b21a9c6d3a6ca57d9c12cfd7/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3537943a61fd25b241e976426a0c6814434b93cf9b09d39e8e78f3c9eb9a487", size = 3525480, upload-time = "2026-02-24T17:27:59.602Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/8247d53998c3673e4a8d1958eba75c6f5cc3b39082029d400bb1f2a911ae/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57f7e336a64a0dba686c66392d46b9bc7af2c57d55ce6dc1697b4ef32b043ceb", size = 3466569, upload-time = "2026-02-24T17:12:16.94Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/c1f0eea1bac6790845f71420a7fe2f2a0566203aa57543117d4af3b77d1c/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dff735a621858680217cb5142b779bad40ef7322ddbb7c12062190db6879772e", size = 3475770, upload-time = "2026-02-24T17:28:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ed/2f43f92474ea0c43c204657dc47d9d002cd738b96ca2af8e6d29a9b5e42d/sqlalchemy-2.0.47-cp313-cp313t-win32.whl", hash = "sha256:3893dc096bb3cca9608ea3487372ffcea3ae9b162f40e4d3c51dd49db1d1b2dc", size = 2141300, upload-time = "2026-02-24T17:14:37.024Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a9/8b73f9f1695b6e92f7aaf1711135a1e3bbeb78bca9eded35cb79180d3c6d/sqlalchemy-2.0.47-cp313-cp313t-win_amd64.whl", hash = "sha256:b5103427466f4b3e61f04833ae01f9a914b1280a2a8bcde3a9d7ab11f3755b42", size = 2173053, upload-time = "2026-02-24T17:14:38.688Z" }, - { url = "https://files.pythonhosted.org/packages/c1/30/98243209aae58ed80e090ea988d5182244ca7ab3ff59e6d850c3dfc7651e/sqlalchemy-2.0.47-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b03010a5a5dfe71676bc83f2473ebe082478e32d77e6f082c8fe15a31c3b42a6", size = 2154355, upload-time = "2026-02-24T17:05:48.959Z" }, - { url = "https://files.pythonhosted.org/packages/ab/62/12ca6ea92055fe486d6558a2a4efe93e194ff597463849c01f88e5adb99d/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e3371aa9024520883a415a09cc20c33cfd3eeccf9e0f4f4c367f940b9cbd44", size = 3274486, upload-time = "2026-02-24T17:18:13.659Z" }, - { url = "https://files.pythonhosted.org/packages/97/88/7dfbdeaa8d42b1584e65d6cc713e9d33b6fa563e0d546d5cb87e545bb0e5/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9449f747e50d518c6e1b40cc379e48bfc796453c47b15e627ea901c201e48a6", size = 3279481, upload-time = "2026-02-24T17:27:26.491Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b7/75e1c1970616a9dd64a8a6fd788248da2ddaf81c95f4875f2a1e8aee4128/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:21410f60d5cac1d6bfe360e05bd91b179be4fa0aa6eea6be46054971d277608f", size = 3224269, upload-time = "2026-02-24T17:18:15.078Z" }, - { url = "https://files.pythonhosted.org/packages/31/ac/eec1a13b891df9a8bc203334caf6e6aac60b02f61b018ef3b4124b8c4120/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:819841dd5bb4324c284c09e2874cf96fe6338bfb57a64548d9b81a4e39c9871f", size = 3246262, upload-time = "2026-02-24T17:27:27.986Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b0/661b0245b06421058610da39f8ceb34abcc90b49f90f256380968d761dbe/sqlalchemy-2.0.47-cp314-cp314-win32.whl", hash = "sha256:e255ee44821a7ef45649c43064cf94e74f81f61b4df70547304b97a351e9b7db", size = 2116528, upload-time = "2026-02-24T17:22:59.363Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ef/1035a90d899e61810791c052004958be622a2cf3eb3df71c3fe20778c5d0/sqlalchemy-2.0.47-cp314-cp314-win_amd64.whl", hash = "sha256:209467ff73ea1518fe1a5aaed9ba75bb9e33b2666e2553af9ccd13387bf192cb", size = 2142181, upload-time = "2026-02-24T17:23:01.001Z" }, - { url = "https://files.pythonhosted.org/packages/76/bb/17a1dd09cbba91258218ceb582225f14b5364d2683f9f5a274f72f2d764f/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e78fd9186946afaa287f8a1fe147ead06e5d566b08c0afcb601226e9c7322a64", size = 3563477, upload-time = "2026-02-24T17:12:18.46Z" }, - { url = "https://files.pythonhosted.org/packages/66/8f/1a03d24c40cc321ef2f2231f05420d140bb06a84f7047eaa7eaa21d230ba/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5740e2f31b5987ed9619d6912ae5b750c03637f2078850da3002934c9532f172", size = 3528568, upload-time = "2026-02-24T17:28:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/fd/53/d56a213055d6b038a5384f0db5ece7343334aca230ff3f0fa1561106f22c/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb9ac00d03de93acb210e8ec7243fefe3e012515bf5fd2f0898c8dff38bc77a4", size = 3472284, upload-time = "2026-02-24T17:12:20.319Z" }, - { url = "https://files.pythonhosted.org/packages/ff/19/c235d81b9cfdd6130bf63143b7bade0dc4afa46c4b634d5d6b2a96bea233/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c72a0b9eb2672d70d112cb149fbaf172d466bc691014c496aaac594f1988e706", size = 3478410, upload-time = "2026-02-24T17:28:05.892Z" }, - { url = "https://files.pythonhosted.org/packages/0e/db/cafdeca5ecdaa3bb0811ba5449501da677ce0d83be8d05c5822da72d2e86/sqlalchemy-2.0.47-cp314-cp314t-win32.whl", hash = "sha256:c200db1128d72a71dc3c31c24b42eb9fd85b2b3e5a3c9ba1e751c11ac31250ff", size = 2147164, upload-time = "2026-02-24T17:14:40.783Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5e/ff41a010e9e0f76418b02ad352060a4341bb15f0af66cedc924ab376c7c6/sqlalchemy-2.0.47-cp314-cp314t-win_amd64.whl", hash = "sha256:669837759b84e575407355dcff912835892058aea9b80bd1cb76d6a151cf37f7", size = 2182154, upload-time = "2026-02-24T17:14:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] @@ -1994,6 +2366,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] +[[package]] +name = "textual" +version = "8.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/08/c6bcb1e3c4c9528ec9049f4ac685afdafc72866664270f0deb416ccbba2a/textual-8.0.2.tar.gz", hash = "sha256:7b342f3ee9a5f2f1bd42d7b598cae00ff1275da68536769510db4b7fe8cabf5d", size = 6099270, upload-time = "2026-03-03T20:23:46.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/bc/0cd17f96f00b6e8bfbca64c574088c85f3c614912b3030f313752e30a099/textual-8.0.2-py3-none-any.whl", hash = "sha256:4ceadbe0e8a30eb80f9995000f4d031f711420a31b02da38f3482957b7c50ce4", size = 719174, upload-time = "2026-03-03T20:23:50.46Z" }, +] + +[[package]] +name = "textual-dev" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "msgpack" }, + { name = "textual" }, + { name = "textual-serve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/fd/fd5ad9527b536c306a5a860a33a45e13983a59804b48b91ea52b42ba030a/textual_dev-1.8.0.tar.gz", hash = "sha256:7e56867b0341405a95e938cac0647e6d2763d38d0df08710469ad6b6a8db76df", size = 25026, upload-time = "2025-10-11T09:47:01.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/c6/1dc08ceee6d0bdf219c500cb2fbd605b681b63507c4ff27a6477f12f8245/textual_dev-1.8.0-py3-none-any.whl", hash = "sha256:227b6d24a485fbbc77e302aa21f4fdf3083beb57eb45cd95bae082c81cbeddeb", size = 27541, upload-time = "2025-10-11T09:47:00.531Z" }, +] + +[[package]] +name = "textual-fspicker" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/fd/dc3160123af550838d50a4fa7f62e357d7ad2fc9b4220ead9160661bcd1b/textual_fspicker-1.0.0.tar.gz", hash = "sha256:462608dbe6a14edff679fc6116addcf288f4a79f8e4fffd240f9ce2caaf9e655", size = 16517, upload-time = "2026-02-18T20:02:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/2f/bf13ae8a1f17186a122968c7ba0f18716d63d2fa67a9e6f905a20067c022/textual_fspicker-1.0.0-py3-none-any.whl", hash = "sha256:71c80258f8885a67abf85d68b42ac8defc8226281481717af722346cbc3e0305", size = 24932, upload-time = "2026-02-18T20:02:15.492Z" }, +] + +[[package]] +name = "textual-serve" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiohttp-jinja2" }, + { name = "jinja2" }, + { name = "rich" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/7e/62fecc552853ec6a178cb1faa2d6f73b34d5512924770e7b08b58ff14148/textual_serve-1.1.3.tar.gz", hash = "sha256:f8f636ae2f5fd651b79d965473c3e9383d3521cdf896f9bc289709185da3f683", size = 448340, upload-time = "2025-11-01T16:22:36.723Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/fe/108e7773349d500cf363328c3d0b7123e03feda51e310a3a5b136ac8ca71/textual_serve-1.1.3-py3-none-any.whl", hash = "sha256:207a472bc6604e725b1adab4ab8bf12f4c4dc25b04eea31e4d04731d8bf30f18", size = 447339, upload-time = "2025-11-01T16:22:35.209Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -2024,6 +2458,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" @@ -2057,6 +2500,110 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + [[package]] name = "zensical" version = "0.0.24" diff --git a/zensical.toml b/zensical.toml index de95e485..3b3f9e74 100644 --- a/zensical.toml +++ b/zensical.toml @@ -24,16 +24,17 @@ nav = [ ] }, { "API reference" = [ "api/aimbat.md", - "api/aimbat/app.md", - "api/aimbat/_types.md", - "api/aimbat/_cli.md", "api/aimbat/core.md", - "api/aimbat/db.md", + "api/aimbat/models.md", "api/aimbat/io.md", + "api/aimbat/db.md", "api/aimbat/logger.md", - "api/aimbat/models.md", - # "api/aimbat/ui.md", "api/aimbat/utils.md", + "api/aimbat/_types.md", + "api/aimbat/app.md", + "api/aimbat/_cli.md", + "api/aimbat/_tui.md", + # "api/aimbat/ui.md", ] }, { "Getting Help" = "help.md"}, {"Contributors" = "contributors.md"},